200 lines
6.8 KiB
Go
200 lines
6.8 KiB
Go
package setup
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/mail"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/passwordpolicy"
|
|
)
|
|
|
|
type Input struct {
|
|
AdminUsername string
|
|
AdminEmail string
|
|
AdminPassword string
|
|
ServerPort int
|
|
EmailHTTPEnabled bool
|
|
EmailProvider string
|
|
EmailEndpoint string
|
|
EmailHTTPPath string
|
|
EmailHTTPLoginPath string
|
|
EmailHTTPAuthMode string
|
|
EmailHTTPCredentialRef string
|
|
EmailHTTPHeaders map[string]string
|
|
EmailEventMode string
|
|
EmailIncludeDeploymentDetails bool
|
|
EmailIncludeCertificateSerial bool
|
|
EmailIncludeManagementURL bool
|
|
EmailSenderName string
|
|
EmailSenderEmail string
|
|
EmailRecipients []string
|
|
EmailTimeoutSecs int
|
|
EmailRetryCount int
|
|
EmailRetryIntervalsSecs []int
|
|
DNSProvider string
|
|
DNSCredentialRef string
|
|
DNSVerified bool
|
|
CertificateRoot string
|
|
WebHostname string
|
|
WebPublicIP string
|
|
WebEnabled bool
|
|
HTTPSMode string
|
|
}
|
|
|
|
func Apply(cfg *config.Config, input Input) error {
|
|
if cfg == nil {
|
|
return fmt.Errorf("配置不能为空")
|
|
}
|
|
if strings.TrimSpace(input.AdminUsername) == "" {
|
|
return fmt.Errorf("超级管理员用户名不能为空")
|
|
}
|
|
adminEmail, err := normalizeAdminEmail(input.AdminEmail)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := passwordpolicy.Validate(input.AdminPassword, input.AdminUsername); err != nil {
|
|
return err
|
|
}
|
|
if input.ServerPort < 1 || input.ServerPort > 65535 {
|
|
return fmt.Errorf("主服务端口无效:%d", input.ServerPort)
|
|
}
|
|
if strings.TrimSpace(input.DNSProvider) == "" {
|
|
return fmt.Errorf("DNS 服务商不能为空")
|
|
}
|
|
if strings.TrimSpace(input.DNSCredentialRef) == "" {
|
|
return fmt.Errorf("DNS 凭据引用不能为空;明文密钥不会直接写入配置文件")
|
|
}
|
|
if !input.DNSVerified {
|
|
return fmt.Errorf("DNS API 尚未通过验证测试")
|
|
}
|
|
if strings.TrimSpace(input.WebHostname) == "" && strings.TrimSpace(input.WebPublicIP) == "" {
|
|
return fmt.Errorf("管理页面必须配置域名或公网 IP")
|
|
}
|
|
if input.WebHostname != "" && input.WebPublicIP != "" {
|
|
return fmt.Errorf("管理页面不能同时配置域名和公网 IP")
|
|
}
|
|
if input.WebPublicIP != "" && net.ParseIP(input.WebPublicIP) == nil {
|
|
return fmt.Errorf("管理页面公网 IP 格式无效")
|
|
}
|
|
if input.HTTPSMode == "" {
|
|
input.HTTPSMode = "direct"
|
|
}
|
|
if input.HTTPSMode != "direct" && input.HTTPSMode != "proxy" {
|
|
return fmt.Errorf("HTTPS 模式无效:%s", input.HTTPSMode)
|
|
}
|
|
if strings.TrimSpace(input.CertificateRoot) == "" {
|
|
input.CertificateRoot = cfg.Paths.Certificate
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("生成管理员密码摘要失败:%w", err)
|
|
}
|
|
cfg.Admin.Username = strings.TrimSpace(input.AdminUsername)
|
|
cfg.Admin.Email = adminEmail
|
|
cfg.Admin.PasswordHash = string(hash)
|
|
cfg.ACME.AccountEmail = adminEmail
|
|
cfg.Server.Port = input.ServerPort
|
|
cfg.Server.HTTPSMode = input.HTTPSMode
|
|
cfg.Web.Enabled = input.WebEnabled
|
|
cfg.Web.Hostname = strings.TrimSpace(input.WebHostname)
|
|
cfg.Web.PublicIP = strings.TrimSpace(input.WebPublicIP)
|
|
cfg.Web.InitReady = true
|
|
cfg.Paths.Certificate = strings.TrimSpace(input.CertificateRoot)
|
|
cfg.DNS.Provider = strings.TrimSpace(input.DNSProvider)
|
|
cfg.DNS.CredentialRef = strings.TrimSpace(input.DNSCredentialRef)
|
|
cfg.DNS.VerificationState = "verified"
|
|
cfg.Notification.HTTPEnabled = input.EmailHTTPEnabled
|
|
cfg.Notification.SMTPEnabled = false
|
|
cfg.Notification.Provider = strings.TrimSpace(input.EmailProvider)
|
|
cfg.Notification.Endpoint = strings.TrimSpace(input.EmailEndpoint)
|
|
if strings.TrimSpace(input.EmailHTTPPath) != "" {
|
|
cfg.Notification.HTTPPath = strings.TrimSpace(input.EmailHTTPPath)
|
|
}
|
|
if strings.TrimSpace(input.EmailHTTPLoginPath) != "" {
|
|
cfg.Notification.HTTPLoginPath = strings.TrimSpace(input.EmailHTTPLoginPath)
|
|
}
|
|
if strings.TrimSpace(input.EmailHTTPAuthMode) != "" {
|
|
cfg.Notification.HTTPAuthMode = strings.TrimSpace(input.EmailHTTPAuthMode)
|
|
}
|
|
cfg.Notification.HTTPCredentialRef = strings.TrimSpace(input.EmailHTTPCredentialRef)
|
|
if input.EmailHTTPHeaders != nil {
|
|
cfg.Notification.HTTPHeaders = copyHeaders(input.EmailHTTPHeaders)
|
|
}
|
|
if strings.TrimSpace(input.EmailEventMode) != "" {
|
|
cfg.Notification.EventMode = strings.TrimSpace(input.EmailEventMode)
|
|
}
|
|
cfg.Notification.IncludeDeploymentDetails = input.EmailIncludeDeploymentDetails
|
|
cfg.Notification.IncludeCertificateSerial = input.EmailIncludeCertificateSerial
|
|
cfg.Notification.IncludeManagementURL = input.EmailIncludeManagementURL
|
|
if strings.TrimSpace(input.EmailSenderName) != "" {
|
|
cfg.Notification.SenderName = strings.TrimSpace(input.EmailSenderName)
|
|
}
|
|
if strings.TrimSpace(input.EmailSenderEmail) != "" {
|
|
cfg.Notification.SenderEmail = strings.ToLower(strings.TrimSpace(input.EmailSenderEmail))
|
|
} else {
|
|
cfg.Notification.SenderEmail = adminEmail
|
|
}
|
|
if input.EmailTimeoutSecs > 0 {
|
|
cfg.Notification.TimeoutSecs = input.EmailTimeoutSecs
|
|
}
|
|
if input.EmailRetryCount > 0 {
|
|
cfg.Notification.RetryCount = input.EmailRetryCount
|
|
}
|
|
if input.EmailRetryIntervalsSecs != nil {
|
|
cfg.Notification.RetryIntervalsSecs = append([]int(nil), input.EmailRetryIntervalsSecs...)
|
|
}
|
|
cfg.Notification.Recipients = cleanRecipients(input.EmailRecipients)
|
|
if len(cfg.Notification.Recipients) == 0 {
|
|
cfg.Notification.Recipients = []string{adminEmail}
|
|
}
|
|
cfg.SetupReady = true
|
|
return cfg.Validate()
|
|
}
|
|
|
|
func copyHeaders(values map[string]string) map[string]string {
|
|
result := make(map[string]string, len(values))
|
|
for key, value := range values {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
result[key] = strings.TrimSpace(value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func cleanRecipients(values []string) []string {
|
|
items := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
for _, part := range strings.Split(value, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[part]; ok {
|
|
continue
|
|
}
|
|
seen[part] = struct{}{}
|
|
items = append(items, part)
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func normalizeAdminEmail(value string) (string, error) {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return "", fmt.Errorf("超级管理员绑定邮箱不能为空")
|
|
}
|
|
parsed, err := mail.ParseAddress(value)
|
|
if err != nil || parsed.Address != value || parsed.Name != "" {
|
|
return "", fmt.Errorf("超级管理员绑定邮箱格式无效")
|
|
}
|
|
return value, nil
|
|
}
|