mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
修复了一些已知问题
This commit is contained in:
+37
-30
@@ -17,7 +17,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -92,10 +91,12 @@ func updateSSLSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if target == "" {
|
||||
target = detectedRequestHost(r)
|
||||
}
|
||||
if target == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "SSL target is required"})
|
||||
normalizedTarget, err := config.NormalizeSSLCertificateTarget(target)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
target = normalizedTarget
|
||||
|
||||
next, err := resolveSSLModeCertificate(mode, target, strings.TrimSpace(req.Email), req.CertPEM, req.KeyPEM)
|
||||
if err != nil {
|
||||
@@ -220,12 +221,10 @@ func saveUploadedCertificate(certPEM, keyPEM string) (string, string, error) {
|
||||
if _, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)); err != nil {
|
||||
return "", "", fmt.Errorf("certificate/private key mismatch: %v", err)
|
||||
}
|
||||
dir := sslStorageDir()
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
certPath, keyPath, err := config.UploadedSSLPaths()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
certPath := filepath.Join(dir, "uploaded-fullchain.pem")
|
||||
keyPath := filepath.Join(dir, "uploaded-privkey.pem")
|
||||
if err := os.WriteFile(certPath, []byte(certPEM+"\n"), 0600); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -237,9 +236,11 @@ func saveUploadedCertificate(certPEM, keyPEM string) (string, string, error) {
|
||||
|
||||
func generateSelfSignedCertificate(target string) (string, string, error) {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return "", "", fmt.Errorf("self-signed certificate target is required")
|
||||
normalizedTarget, err := config.NormalizeSSLCertificateTarget(target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
target = normalizedTarget
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -273,12 +274,10 @@ func generateSelfSignedCertificate(target string) (string, string, error) {
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
dir := sslStorageDir()
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
certPath, keyPath, err := config.SelfSignedSSLPaths()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
certPath := filepath.Join(dir, "self-signed-fullchain.pem")
|
||||
keyPath := filepath.Join(dir, "self-signed-privkey.pem")
|
||||
certOut := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyOut := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
||||
if err := os.WriteFile(certPath, certOut, 0600); err != nil {
|
||||
@@ -295,9 +294,11 @@ func requestLetsEncryptCertificate(target, email string) (string, string, error)
|
||||
return "", "", fmt.Errorf("certbot is not installed on this server")
|
||||
}
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return "", "", fmt.Errorf("Let's Encrypt target is required")
|
||||
normalizedTarget, err := config.NormalizeSSLCertificateTarget(target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
target = normalizedTarget
|
||||
args := []string{"certonly", "--non-interactive", "--agree-tos", "--standalone"}
|
||||
if email != "" {
|
||||
args = append(args, "--email", email)
|
||||
@@ -317,12 +318,14 @@ func requestLetsEncryptCertificate(target, email string) (string, string, error)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Let's Encrypt request failed: %s", strings.TrimSpace(string(output)))
|
||||
}
|
||||
certPath := filepath.Join("/etc/letsencrypt/live", target, "fullchain.pem")
|
||||
keyPath := filepath.Join("/etc/letsencrypt/live", target, "privkey.pem")
|
||||
if _, err := os.Stat(certPath); err != nil {
|
||||
certPath, keyPath, err := config.LetsEncryptSSLPaths(target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if _, err := config.ReadableFileStat(certPath); err != nil {
|
||||
return "", "", fmt.Errorf("Let's Encrypt certificate file not found after issuance: %s", certPath)
|
||||
}
|
||||
if _, err := os.Stat(keyPath); err != nil {
|
||||
if _, err := config.ReadableFileStat(keyPath); err != nil {
|
||||
return "", "", fmt.Errorf("Let's Encrypt private key file not found after issuance: %s", keyPath)
|
||||
}
|
||||
return certPath, keyPath, nil
|
||||
@@ -347,11 +350,19 @@ func ensureCertbotSupportsIPCertificates() error {
|
||||
}
|
||||
|
||||
func validateCertificatePair(certPath, keyPath string) error {
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
safeCertPath, err := config.ResolveSSLPath(certPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
safeKeyPath, err := config.ResolveSSLPath(keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certPEM, err := os.ReadFile(safeCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyPEM, err := os.ReadFile(safeKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -443,7 +454,11 @@ func readLeafCertificate(certPath string) (*x509.Certificate, error) {
|
||||
if certPath == "" {
|
||||
return nil, errors.New("certificate path is empty")
|
||||
}
|
||||
data, err := os.ReadFile(certPath)
|
||||
safeCertPath, err := config.ResolveSSLPath(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(safeCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -494,14 +509,6 @@ func firstPublicInterfaceIP() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func sslStorageDir() string {
|
||||
dataDir := config.AppConfig.DataDir
|
||||
if dataDir == "" {
|
||||
dataDir = "/root/.clicd"
|
||||
}
|
||||
return filepath.Join(dataDir, "ssl")
|
||||
}
|
||||
|
||||
func maskExistingPath(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const letsEncryptLiveDir = "/etc/letsencrypt/live"
|
||||
|
||||
var dnsNamePattern = regexp.MustCompile(`^[A-Za-z0-9.-]+$`)
|
||||
|
||||
func SSLStorageDir() string {
|
||||
dataDir := ""
|
||||
if AppConfig != nil {
|
||||
dataDir = AppConfig.DataDir
|
||||
}
|
||||
if dataDir == "" {
|
||||
dataDir = getDataDir()
|
||||
}
|
||||
return filepath.Join(dataDir, "ssl")
|
||||
}
|
||||
|
||||
func UploadedSSLPaths() (string, string, error) {
|
||||
dir, err := safeSSLStorageDir()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return filepath.Join(dir, "uploaded-fullchain.pem"), filepath.Join(dir, "uploaded-privkey.pem"), nil
|
||||
}
|
||||
|
||||
func SelfSignedSSLPaths() (string, string, error) {
|
||||
dir, err := safeSSLStorageDir()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return filepath.Join(dir, "self-signed-fullchain.pem"), filepath.Join(dir, "self-signed-privkey.pem"), nil
|
||||
}
|
||||
|
||||
func LetsEncryptSSLPaths(target string) (string, string, error) {
|
||||
name, err := NormalizeSSLCertificateTarget(target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
base := filepath.Join(letsEncryptLiveDir, name)
|
||||
return filepath.Join(base, "fullchain.pem"), filepath.Join(base, "privkey.pem"), nil
|
||||
}
|
||||
|
||||
func ResolveSSLConfigPaths(ssl SSLConfig) (string, string, error) {
|
||||
mode := NormalizeSSLMode(ssl.Mode)
|
||||
switch mode {
|
||||
case SSLModeUploaded:
|
||||
if ssl.CertPath != "" && ssl.KeyPath != "" {
|
||||
return ResolveSSLPathPair(ssl.CertPath, ssl.KeyPath)
|
||||
}
|
||||
return UploadedSSLPaths()
|
||||
case SSLModeSelfSigned:
|
||||
if ssl.CertPath != "" && ssl.KeyPath != "" {
|
||||
return ResolveSSLPathPair(ssl.CertPath, ssl.KeyPath)
|
||||
}
|
||||
return SelfSignedSSLPaths()
|
||||
case SSLModeLetsEncrypt:
|
||||
if strings.TrimSpace(ssl.Target) == "" && ssl.CertPath != "" && ssl.KeyPath != "" {
|
||||
return ResolveSSLPathPair(ssl.CertPath, ssl.KeyPath)
|
||||
}
|
||||
return LetsEncryptSSLPaths(ssl.Target)
|
||||
default:
|
||||
return "", "", fmt.Errorf("SSL is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveSSLPathPair(certPath, keyPath string) (string, string, error) {
|
||||
safeCertPath, err := ResolveSSLPath(certPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
safeKeyPath, err := ResolveSSLPath(keyPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return safeCertPath, safeKeyPath, nil
|
||||
}
|
||||
|
||||
func ResolveSSLPath(path string) (string, error) {
|
||||
cleaned, err := cleanAbsolutePath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isPathUnder(cleaned, SSLStorageDir()) || isPathUnder(cleaned, letsEncryptLiveDir) || isPathUnder(cleaned, "/etc/letsencrypt/archive") {
|
||||
return cleaned, nil
|
||||
}
|
||||
return "", fmt.Errorf("SSL path is outside allowed certificate directories")
|
||||
}
|
||||
|
||||
func ReadableFileStat(path string) (os.FileInfo, error) {
|
||||
safePath, err := ResolveSSLPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Stat(safePath)
|
||||
}
|
||||
|
||||
func NormalizeSSLCertificateTarget(target string) (string, error) {
|
||||
target = strings.TrimSpace(strings.Trim(target, "[]"))
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("SSL target is required")
|
||||
}
|
||||
if strings.Contains(target, "/") || strings.Contains(target, "\\") || strings.Contains(target, "..") {
|
||||
return "", fmt.Errorf("SSL target contains invalid path characters")
|
||||
}
|
||||
if ip := net.ParseIP(target); ip != nil {
|
||||
return ip.String(), nil
|
||||
}
|
||||
if len(target) > 253 || !dnsNamePattern.MatchString(target) {
|
||||
return "", fmt.Errorf("SSL target must be a valid IP address or DNS name")
|
||||
}
|
||||
labels := strings.Split(target, ".")
|
||||
for _, label := range labels {
|
||||
if label == "" || len(label) > 63 || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
|
||||
return "", fmt.Errorf("SSL target must be a valid IP address or DNS name")
|
||||
}
|
||||
}
|
||||
return strings.ToLower(target), nil
|
||||
}
|
||||
|
||||
func safeSSLStorageDir() (string, error) {
|
||||
dir, err := cleanAbsolutePath(SSLStorageDir())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dataDir := ""
|
||||
if AppConfig != nil {
|
||||
dataDir = AppConfig.DataDir
|
||||
}
|
||||
if dataDir == "" {
|
||||
dataDir = getDataDir()
|
||||
}
|
||||
if !isPathUnder(dir, dataDir) {
|
||||
return "", fmt.Errorf("SSL storage directory is outside the data directory")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func cleanAbsolutePath(path string) (string, error) {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return "", fmt.Errorf("path is empty")
|
||||
}
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Clean(abs), nil
|
||||
}
|
||||
|
||||
func isPathUnder(path, root string) bool {
|
||||
cleanPath, err := cleanAbsolutePath(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
cleanRoot, err := cleanAbsolutePath(root)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rel, err := filepath.Rel(cleanRoot, cleanPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"clicd/internal/api"
|
||||
@@ -213,11 +212,26 @@ func Run() error {
|
||||
}
|
||||
|
||||
if sslEnabled() {
|
||||
certPath, keyPath, err := config.ResolveSSLConfigPaths(config.AppConfig.SSL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(config.AppConfig.SSL.CertPath, config.AppConfig.SSL.KeyPath)
|
||||
return &cert, err
|
||||
safeCertPath, err := config.ResolveSSLPath(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safeKeyPath, err := config.ResolveSSLPath(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(safeCertPath, safeKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
},
|
||||
}
|
||||
log.Printf("CLICD Web Server SSL enabled on https://0.0.0.0:%d", config.AppConfig.Port)
|
||||
@@ -229,14 +243,29 @@ func Run() error {
|
||||
|
||||
func sslEnabled() bool {
|
||||
ssl := config.AppConfig.SSL
|
||||
if !ssl.Enabled || ssl.CertPath == "" || ssl.KeyPath == "" {
|
||||
if !ssl.Enabled {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(ssl.CertPath); err != nil {
|
||||
certPath, keyPath, err := config.ResolveSSLConfigPaths(ssl)
|
||||
if err != nil {
|
||||
log.Printf("SSL paths are invalid, falling back to HTTP: %v", err)
|
||||
return false
|
||||
}
|
||||
safeCertPath, err := config.ResolveSSLPath(certPath)
|
||||
if err != nil {
|
||||
log.Printf("SSL certificate path is not allowed, falling back to HTTP: %v", err)
|
||||
return false
|
||||
}
|
||||
safeKeyPath, err := config.ResolveSSLPath(keyPath)
|
||||
if err != nil {
|
||||
log.Printf("SSL private key path is not allowed, falling back to HTTP: %v", err)
|
||||
return false
|
||||
}
|
||||
if _, err := config.ReadableFileStat(safeCertPath); err != nil {
|
||||
log.Printf("SSL certificate is not readable, falling back to HTTP: %v", err)
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(ssl.KeyPath); err != nil {
|
||||
if _, err := config.ReadableFileStat(safeKeyPath); err != nil {
|
||||
log.Printf("SSL private key is not readable, falling back to HTTP: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user