mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
176 lines
4.5 KiB
Go
176 lines
4.5 KiB
Go
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)))
|
|
}
|