package config import ( "context" "os" "path/filepath" "reflect" "sync" "time" ) var sslCommitMu sync.Mutex // WriteSSLConfig 是初始化及自动续期的内部入口,只允许更新 SSL 引用和 HTTPS 开关。 // 旧证书保持不动;已有人工候选或签发期间域名、证书策略变化时拒绝提交。 func WriteSSLConfig(expected, next *Config) error { if expected == nil || next == nil { return ErrInvalidSSLReference } sslCommitMu.Lock() defer sslCommitMu.Unlock() retainPreviousSSL(ROOT_PATH, &next.SSL, expected.SSL) legacyConfigMu.Lock() defer legacyConfigMu.Unlock() data, err := readConfigFile("config.json") if err != nil && !os.IsNotExist(err) { return err } partitioned := false if len(data) > 0 { partitioned, err = isPartitionedConfig(data) if err != nil { return err } } if !partitioned { merged := *next if len(data) > 0 { live, err := readConfig() if err != nil { return err } if !sameSSLPolicy(live, expected) { return ErrGenerationConflict } // 签发期间其他设置可能已经保存,只合并本入口负责的 SSL 字段。 merged = *live merged.SSL = next.SSL merged.SSLType = next.SSLType merged.SSLPrivateKeyPath = next.SSLPrivateKeyPath merged.SSLPublicKeyPath = next.SSLPublicKeyPath merged.HttpsEnabled = next.HttpsEnabled } if err := writeConfig(&merged); err != nil { return err } *next = merged maintainManagedSSL(ROOT_PATH, next.SSL, expected.SSL) return nil } manager := CurrentManager() if manager == nil { manager, err = NewConfigManager(ROOT_PATH, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: !DevelopmentMode}}) if err != nil { return err } SetRuntimeManager(manager) } if filepath.Clean(manager.loader.root) != filepath.Clean(ROOT_PATH) { return ErrGenerationConflict } manager.writeMu.Lock() defer manager.writeMu.Unlock() manager.mu.RLock() current := cloneRuntimeConfig(manager.current) pending := manager.pending != nil manager.mu.RUnlock() if pending { return ErrGenerationConflict } live, err := readConfig() if err != nil { return err } if !sameSSLPolicy(live, expected) { return ErrGenerationConflict } candidate := cloneRuntimeConfig(current) candidate.SSL.TLS = next.SSL candidate.Root.HTTP.HTTPSEnabled = next.HttpsEnabled == 1 if err := validateCustomSSLReferences(ROOT_PATH, candidate); err != nil { return err } setCandidateGeneration(&candidate, current.Generation+1) if err := ValidateRuntimeConfig(candidate, manager.options); err != nil { return err } if err := ValidateRuntimeSecrets(manager.vault, candidate); err != nil { return err } if err := manager.snapshots.Validate(candidate); err != nil { return err } committed, err := runtimeCompatibility(candidate) if err != nil { return err } id, err := newRollbackTicketID() if err != nil { return err } if _, _, err := manager.transactions.CreateBackupIfDue(current, time.Now().UTC()); err != nil { return err } if err := manager.transactions.Commit(current, candidate); err != nil { return err } // 已落盘候选必须留在管理器中;运行态发布失败后管理员可显式恢复,而不能取消。 manager.mu.Lock() manager.pending = &pendingConfig{ ID: id, Candidate: candidate, Section: SectionSSL, Committed: true, Actor: AuditActor{UserID: "system", Role: "system"}, PreviousRestart: append([]ServiceName(nil), manager.status.PendingRestart...), } manager.status.PendingRestart = mergeServices(manager.status.PendingRestart, servicesForChange(SectionSSL, current, candidate)) manager.mu.Unlock() if err := manager.publishSnapshot(candidate); err != nil { return err } now := time.Now().UTC() manager.mu.Lock() manager.current = cloneRuntimeConfig(candidate) manager.pending = nil manager.status.SetupState = candidate.SetupState manager.status.DegradedReasons = nil manager.status.ActiveGeneration = candidate.Generation manager.status.LastValidAt = &now manager.mu.Unlock() // 兼容快照与本次已提交候选一致,不再静默忽略提交后的磁盘读取失败。 *next = *committed PublishConfigSnapshot(committed) maintainManagedSSL(ROOT_PATH, candidate.SSL.TLS, current.SSL.TLS) return manager.writeAudit(context.Background(), ConfigAuditEvent{ Operation: ConfigOperationUpdate, Section: SectionSSL, ActorID: "system", ActorRole: "system", FromGeneration: current.Generation, ToGeneration: candidate.Generation, Changes: AuditChanges(current, candidate), ChangedFields: []string{"tls"}, Result: "success", CreatedAt: now, }) } func sameSSLPolicy(left, right *Config) bool { return reflect.DeepEqual(left.SSL, right.SSL) && left.SSLType == right.SSLType && left.SSLPrivateKeyPath == right.SSLPrivateKeyPath && left.SSLPublicKeyPath == right.SSLPublicKeyPath && left.Domain == right.Domain && left.WebDomain == right.WebDomain && reflect.DeepEqual(left.Domains, right.Domains) && left.HttpsEnabled == right.HttpsEnabled && left.TLSMode == right.TLSMode } // 材料和续期策略均可热更新;只有实际监听配置发生变化才提示重启。 func servicesForChange(section Section, previous, next RuntimeConfig) []ServiceName { if section == SectionSSL { if !reflect.DeepEqual(previous.Root.HTTP, next.Root.HTTP) { return []ServiceName{ServiceHTTP} } return nil } return servicesForSection(section) } func mergeServices(previous, next []ServiceName) []ServiceName { result := append([]ServiceName(nil), previous...) for _, service := range next { found := false for _, existing := range result { found = found || service == existing } if !found { result = append(result, service) } } return result }