Smail/backend/config/runtime_access.go
smail 606861822b
Some checks are pending
Security Checks / security (push) Waiting to run
保存统一Web域名恢复入口接手基线
2026-09-16 19:17:55 +08:00

328 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
"git.pcux.cn/xuans/Smail/utils/credential"
)
// publishedConfig 是旧配置模型的只读兼容快照。
// 新业务应通过 CurrentConfig 读取,禁止在请求处理中直接持有并修改 Instance。
var publishedConfig atomic.Pointer[Config]
var hasPublishedConfig atomic.Bool
var setupPort atomic.Int64
func IsSetupReady() bool {
value := CurrentConfig()
if value != nil && value.SetupState.Valid() {
return value.SetupState == SetupStateReady
}
return legacyInitReady()
}
// PublishConfigSnapshot 发布一份深拷贝,供运行时只读访问。
// 调用方仍可继续使用旧的 Instance 兼容路径,但不能把返回值当作可变全局状态。
func PublishConfigSnapshot(value *Config) {
hasPublishedConfig.Store(true)
if value == nil {
publishedConfig.Store(nil)
return
}
clone := cloneConfig(value)
publishedConfig.Store(clone)
}
// CurrentConfig 返回当前运行时配置的独立副本。
// 返回值可以安全地在业务代码中读取或局部修改,不会改变全局配置快照。
func CurrentConfig() *Config {
if value := publishedConfig.Load(); value != nil {
return cloneConfig(value)
}
if hasPublishedConfig.Load() {
return nil
}
if Instance == nil {
return nil
}
return cloneConfig(Instance)
}
// CurrentHTTPConfig 返回当前进程实际使用的 HTTP 配置。
// 分区配置存在时优先读取分区配置,旧版配置仅作为兼容回退。
func CurrentHTTPConfig() HTTPConfig {
if manager := CurrentManager(); manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
return ResolveHTTPPortDefaults(runtime.Root.HTTP, RuntimeSetupPort())
}
}
cfg := CurrentConfig()
if cfg == nil {
return ResolveHTTPPortDefaults(HTTPConfig{}, RuntimeSetupPort())
}
mode := strings.ToLower(strings.TrimSpace(cfg.TLSMode))
return ResolveHTTPPortDefaults(HTTPConfig{
Port: cfg.HttpPort,
HTTPSPort: cfg.HttpsPort,
HTTPSEnabled: cfg.HttpsEnabled != 2,
TLSMode: mode,
TrustedProxyCIDRs: append([]string(nil), cfg.TrustedProxyCIDRs...),
}, cfg.GetSetupPort())
}
func ResolveHTTPPortDefaults(value HTTPConfig, setupPort int) HTTPConfig {
result := cloneHTTPConfig(value)
result.TLSMode = strings.ToLower(strings.TrimSpace(result.TLSMode))
if override := environmentPort("SMAIL_HTTP_PORT_OVERRIDE"); override != 0 {
result.Port = override
} else if result.Port == 0 {
result.Port = environmentPort("SMAIL_HTTP_PORT")
if result.Port == 0 && validServicePort(setupPort) {
result.Port = setupPort
}
if result.Port == 0 {
result.Port = 80
}
}
if override := strings.ToLower(strings.TrimSpace(os.Getenv("SMAIL_HTTP_TLS_MODE_OVERRIDE"))); override == HTTPSTLSModeDirect || override == HTTPSTLSModeProxy {
result.TLSMode = override
}
if result.TLSMode == HTTPSTLSModeProxy {
result.HTTPSPort = 0
result.HTTPSEnabled = false
return result
}
if result.HTTPSPort == 0 {
result.HTTPSPort = environmentPort("SMAIL_HTTPS_PORT")
if result.HTTPSPort == 0 {
result.HTTPSPort = 443
}
}
return result
}
func environmentPort(name string) int {
port, err := strconv.Atoi(strings.TrimSpace(os.Getenv(name)))
if err != nil || !validServicePort(port) {
return 0
}
return port
}
func validServicePort(port int) bool {
return port > 0 && port <= 65535
}
// CurrentWebDomain 返回当前进程实际使用的 Web 域名。
// 分区配置存在时优先读取分区配置,旧版配置仅作为兼容回退。
func CurrentWebDomain() string {
if manager := CurrentManager(); manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
return runtime.Root.Domains.Web
}
}
cfg := CurrentConfig()
if cfg == nil {
return ""
}
return cfg.WebDomain
}
// CurrentAdminRecoveryDomain 仅用于读取旧配置。
// 公网一次性恢复入口统一使用 CurrentWebDomain,不再使用独立恢复域名。
func CurrentAdminRecoveryDomain() string {
if manager := CurrentManager(); manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
if domain := runtime.Root.Domains.AdminRecovery; domain != "" {
return domain
}
return runtime.Root.Domains.Primary
}
}
cfg := CurrentConfig()
if cfg == nil {
return ""
}
if cfg.AdminRecoveryDomain != "" {
return cfg.AdminRecoveryDomain
}
return cfg.Domain
}
// SetRuntimeSetupPort stores the temporary setup listener port outside the persisted configuration.
func SetRuntimeSetupPort(port int) {
setupPort.Store(int64(port))
}
func RuntimeSetupPort() int {
return int(setupPort.Load())
}
func cloneConfig(source *Config) *Config {
if source == nil {
return nil
}
clone := *source
clone.Domains = append([]string(nil), source.Domains...)
clone.AccessHosts = append([]string(nil), source.AccessHosts...)
clone.TrustedProxyCIDRs = append([]string(nil), source.TrustedProxyCIDRs...)
clone.PluginAllowList = append([]string(nil), source.PluginAllowList...)
if source.Tables != nil {
clone.Tables = make(map[string]string, len(source.Tables))
for key, value := range source.Tables {
clone.Tables[key] = value
}
}
if source.TablesInitData != nil {
clone.TablesInitData = make(map[string]string, len(source.TablesInitData))
for key, value := range source.TablesInitData {
clone.TablesInitData[key] = value
}
}
return &clone
}
type MailPorts struct {
SMTP int
SMTPSTARTTLS int
SMTPTLS int
IMAP int
POP3 int
POP3TLS int
}
type MailDomains struct {
Primary string
Accepted []string
}
func CurrentMailDomains() MailDomains {
manager := CurrentManager()
if manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
return MailDomains{
Primary: runtime.Root.Domains.Primary,
Accepted: append([]string(nil), runtime.Root.Domains.Accepted...),
}
}
}
cfg := CurrentConfig()
if cfg == nil {
return MailDomains{}
}
domains := MailDomains{
Primary: cfg.Domain,
Accepted: append([]string(nil), cfg.Domains...),
}
if domains.Primary == "" && len(domains.Accepted) > 0 {
domains.Primary = domains.Accepted[0]
}
if len(domains.Accepted) == 0 && domains.Primary != "" {
domains.Accepted = []string{domains.Primary}
}
return domains
}
func CurrentEmailPolicy() EmailConfig {
manager := CurrentManager()
if manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
return runtime.Email.Policy
}
}
cfg := CurrentConfig()
if cfg == nil {
return EmailConfig{}
}
return EmailConfig{
SMTPPort: cfg.SMTPPort,
SpamFilterLevel: cfg.SpamFilterLevel,
LoginVerificationPolicy: cfg.LoginPolicy,
ExternalRecoveryEnabled: cfg.ExternalRecoveryEnabled,
}
}
type SMTPRuntimeSettings struct {
Relay SMTPConfig
Password string
VerificationEnabled bool
ExternalRecoveryEnabled bool
}
func CurrentSMTPRuntimeSettings() (SMTPRuntimeSettings, error) {
settings := SMTPRuntimeSettings{}
if manager := CurrentManager(); manager != nil {
runtime := manager.Snapshot()
if runtime.Generation != 0 {
settings.Relay = runtime.SMTP.Relay
settings.ExternalRecoveryEnabled = runtime.Email.Policy.ExternalRecoveryEnabled
if settings.Relay.Username != "" {
vault, err := credential.NewVault(ROOT_PATH)
if err != nil {
return SMTPRuntimeSettings{}, fmt.Errorf("create SMTP credential vault: %w", err)
}
password, err := OpenStoredSecret(vault, settings.Relay.Password)
if err != nil {
return SMTPRuntimeSettings{}, fmt.Errorf("open SMTP relay password: %w", err)
}
settings.Password = password
}
settings.VerificationEnabled = settings.Relay.VerificationEnabled
return settings, nil
}
}
cfg := CurrentConfig()
if cfg == nil {
return SMTPRuntimeSettings{}, fmt.Errorf("runtime configuration is unavailable")
}
settings.Relay = SMTPConfig{
RelayHost: cfg.SMTPHost,
RelayPort: cfg.SMTPPort,
Username: cfg.SMTPUsername,
Security: cfg.SMTPSecurity,
From: cfg.SMTPFrom,
FromDomain: cfg.SMTPFromDomain,
VerificationEnabled: cfg.VerificationEnabled,
LoginPolicy: cfg.LoginPolicy,
}
settings.Password = cfg.SMTPPassword
settings.VerificationEnabled = cfg.VerificationEnabled
settings.ExternalRecoveryEnabled = cfg.ExternalRecoveryEnabled
return settings, nil
}
func CurrentMailPorts() MailPorts {
if manager := CurrentManager(); manager != nil {
if runtime := manager.Snapshot(); runtime.Generation != 0 {
return ResolveMailPorts(runtime.SMTP, runtime.Email)
}
}
// 旧版 SMTPPort 属于发信中继,不能用作本机监听端口。
return ResolveMailPorts(SMTPSectionConfig{}, EmailSectionConfig{})
}
func CurrentMailSecurity() string {
if manager := CurrentManager(); manager != nil {
if runtime := manager.Snapshot(); runtime.Generation != 0 {
return ResolveMailSecurity(runtime.SMTP.Listener)
}
}
return MailSecurityBoth
}
func cloneHTTPConfig(source HTTPConfig) HTTPConfig {
clone := source
clone.TrustedProxyCIDRs = append([]string(nil), source.TrustedProxyCIDRs...)
return clone
}