Smail/backend/config/runtime_access.go
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

294 lines
7.9 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 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 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
IMAP int
POP3 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 {
ports := MailPorts{SMTP: 25, IMAP: 143, POP3: 110}
policy := CurrentEmailPolicy()
if policy.SMTPPort > 0 && policy.SMTPPort <= 65535 {
ports.SMTP = policy.SMTPPort
}
if policy.IMAPPort > 0 && policy.IMAPPort <= 65535 {
ports.IMAP = policy.IMAPPort
}
if policy.POP3Port > 0 && policy.POP3Port <= 65535 {
ports.POP3 = policy.POP3Port
}
return ports
}
func cloneHTTPConfig(source HTTPConfig) HTTPConfig {
clone := source
clone.TrustedProxyCIDRs = append([]string(nil), source.TrustedProxyCIDRs...)
return clone
}