297 lines
8.2 KiB
Go
297 lines
8.2 KiB
Go
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
|
|
}
|
|
|
|
// 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 cloneHTTPConfig(source HTTPConfig) HTTPConfig {
|
|
clone := source
|
|
clone.TrustedProxyCIDRs = append([]string(nil), source.TrustedProxyCIDRs...)
|
|
return clone
|
|
}
|