live-sfu-demo/internal/config/runtime.go

557 lines
14 KiB
Go

package config
import (
"time"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
)
var runtimeFields = map[string]bool{
"SFU_PROVIDER": true, "DISTRIBUTORS": true, "CDN_RTMP_URL": true, "CDN_NAME": true,
"SRS_API_BASE": true, "SRS_HTTP_BASE": true, "SRS_APP": true, "SRS_SECRET": true, "SRS_CANDIDATE": true,
"CF_APP_ID": true, "CF_APP_SECRET": true, "CF_BASE_URL": true, "CF_STUN_URL": true,
"SFU_TOKEN_REQUIRED": true, "ALLOW_REGISTER": true, "JWT_TTL": true,
"SMTP_HOST": true, "SMTP_PORT": true, "SMTP_USER": true, "SMTP_PASS": true, "SMTP_FROM": true, "SMTP_FROM_NAME": true, "SMTP_TLS": true, "SMTP_ENABLED": true,
"EMAIL_VERIFY_REQUIRED": true, "EMAIL_VERIFY_TTL": true, "EMAIL_VERIFY_BASE_URL": true,
"OAUTH_ENABLED": true, "OAUTH_CALLBACK_BASE": true,
"OAUTH_GOOGLE_CLIENT_ID": true, "OAUTH_GOOGLE_CLIENT_SECRET": true,
"OAUTH_GITHUB_CLIENT_ID": true, "OAUTH_GITHUB_CLIENT_SECRET": true,
"OAUTH_GITLAB_CLIENT_ID": true, "OAUTH_GITLAB_CLIENT_SECRET": true,
"OAUTH_GENERIC_CLIENT_ID": true, "OAUTH_GENERIC_CLIENT_SECRET": true, "OAUTH_GENERIC_URL": true, "OAUTH_GENERIC_NAME": true,
}
type RuntimeView struct {
Values map[string]string `json:"values"`
Masked map[string]bool `json:"masked"`
Source map[string]string `json:"source"`
}
var secretKeys = map[string]bool{
"CF_APP_SECRET": true, "SRS_SECRET": true, "SMTP_PASS": true,
"OAUTH_GOOGLE_CLIENT_SECRET": true, "OAUTH_GITHUB_CLIENT_SECRET": true, "OAUTH_GITLAB_CLIENT_SECRET": true, "OAUTH_GENERIC_CLIENT_SECRET": true,
}
func (c *Config) ensureMu() {
if c.mu == nil {
c.mu = &sync.RWMutex{}
}
if c.runtimeFile == "" {
c.runtimeFile = "data/runtime_config.json"
}
if c.overrides == nil {
c.overrides = map[string]string{}
}
}
func (c *Config) LoadRuntime() error {
c.ensureMu()
path := c.runtimeFile
if path == "" {
path = "data/runtime_config.json"
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
var m map[string]string
if err := json.Unmarshal(data, &m); err != nil {
return fmt.Errorf("parse runtime config: %w", err)
}
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range m {
nk := normalizeRuntimeKey(k)
if !runtimeFields[nk] {
continue
}
c.overrides[nk] = v
}
c.applyOverridesLocked()
return nil
}
func (c *Config) SaveRuntime() error {
c.ensureMu()
c.mu.RLock()
m := make(map[string]string, len(c.overrides))
for k, v := range c.overrides {
m[k] = v
}
path := c.runtimeFile
c.mu.RUnlock()
if path == "" {
path = "data/runtime_config.json"
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
data, _ := json.MarshalIndent(m, "", " ")
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
return err
}
return os.Rename(tmp, path)
}
func (c *Config) ViewRuntime(masked bool) RuntimeView {
c.ensureMu()
c.mu.RLock()
defer c.mu.RUnlock()
values := map[string]string{}
maskedMap := map[string]bool{}
source := map[string]string{}
for k := range runtimeFields {
v := c.fieldValue(k)
isSecret := secretKeys[k]
_, fromRuntime := c.overrides[k]
if masked && isSecret && v != "" {
values[k] = maskSecret(v)
maskedMap[k] = true
} else {
values[k] = v
if isSecret && v != "" {
maskedMap[k] = true
}
}
if fromRuntime {
source[k] = "runtime"
} else if os.Getenv(k) != "" {
source[k] = "env"
} else {
source[k] = "default"
}
}
return RuntimeView{Values: values, Masked: maskedMap, Source: source}
}
func (c *Config) ApplyRuntimePatch(patch map[string]string) ([]string, error) {
c.ensureMu()
norm := map[string]string{}
for k, v := range patch {
nk := normalizeRuntimeKey(k)
if !runtimeFields[nk] {
return nil, fmt.Errorf("不支持的运行时配置键: %s", k)
}
if secretKeys[nk] && isMaskPlaceholder(v) {
continue
}
norm[nk] = strings.TrimSpace(v)
}
c.mu.Lock()
defer c.mu.Unlock()
changed := []string{}
old := map[string]string{}
for k, v := range norm {
old[k] = c.overrides[k]
if old[k] != v {
changed = append(changed, k)
}
if v == "" {
delete(c.overrides, k)
// revert field to env/default immediately
c.revertFieldLocked(k)
} else {
c.overrides[k] = v
}
}
c.applyOverridesLocked()
if err := c.ValidateDistributors(); err != nil {
for k := range norm {
if ov, ok := old[k]; ok {
c.overrides[k] = ov
} else {
delete(c.overrides, k)
}
}
c.applyOverridesLocked()
return nil, err
}
if v, ok := norm["CF_STUN_URL"]; ok && v != "" && !strings.Contains(v, ":") {
for k := range norm {
if ov, ok := old[k]; ok {
c.overrides[k] = ov
} else {
delete(c.overrides, k)
}
}
c.applyOverridesLocked()
return nil, fmt.Errorf("CF_STUN_URL 格式错误")
}
return changed, nil
}
func normalizeRuntimeKey(k string) string {
k = strings.TrimSpace(k)
k = strings.ToUpper(k)
k = strings.ReplaceAll(k, "-", "_")
return k
}
func isMaskPlaceholder(s string) bool {
if s == "" {
return false
}
for _, ch := range s {
if ch != '•' && ch != '*' && ch != '·' {
return false
}
}
return true
}
func maskSecret(s string) string {
if s == "" {
return ""
}
return "••••••••"
}
func (c *Config) fieldValue(key string) string {
switch key {
case "SFU_PROVIDER":
return c.ProviderOrder
case "DISTRIBUTORS":
return c.Distributors
case "CDN_RTMP_URL":
return c.CDNRTMPURL
case "CDN_NAME":
return c.CDNName
case "SRS_API_BASE":
return c.SRSBaseURL
case "SRS_HTTP_BASE":
return c.SRSHttpURL
case "SRS_APP":
return c.SRSApp
case "SRS_SECRET":
return c.SRSSecret
case "SRS_CANDIDATE":
return c.SRSCandidate
case "CF_APP_ID":
return c.CFAppID
case "CF_APP_SECRET":
return c.CFAppSecret
case "CF_BASE_URL":
return c.CFBaseURL
case "CF_STUN_URL":
return c.CFStunURL
case "SFU_TOKEN_REQUIRED":
if c.TokenRequired {
return "1"
}
return "0"
case "ALLOW_REGISTER":
if c.AllowRegister {
return "1"
}
return "0"
case "JWT_TTL":
return c.JWTTTL.String()
case "SMTP_HOST":
return c.SMTPHost
case "SMTP_PORT":
return fmt.Sprintf("%d", c.SMTPPort)
case "SMTP_USER":
return c.SMTPUser
case "SMTP_PASS":
return c.SMTPPass
case "SMTP_FROM":
return c.SMTPFrom
case "SMTP_FROM_NAME":
return c.SMTPFromName
case "SMTP_TLS":
if c.SMTPTLS {
return "1"
}
return "0"
case "SMTP_ENABLED":
if c.SMTPEnabled {
return "1"
}
return "0"
case "EMAIL_VERIFY_REQUIRED":
if c.EmailVerifyRequired {
return "1"
}
return "0"
case "EMAIL_VERIFY_TTL":
return c.EmailVerifyTTL.String()
case "EMAIL_VERIFY_BASE_URL":
return c.EmailVerifyBaseURL
case "OAUTH_ENABLED":
if c.OAuthEnabled {
return "1"
}
return "0"
case "OAUTH_CALLBACK_BASE":
return c.OAuthCallbackBase
case "OAUTH_GOOGLE_CLIENT_ID":
return c.OAuthGoogleID
case "OAUTH_GOOGLE_CLIENT_SECRET":
return c.OAuthGoogleSecret
case "OAUTH_GITHUB_CLIENT_ID":
return c.OAuthGithubID
case "OAUTH_GITHUB_CLIENT_SECRET":
return c.OAuthGithubSecret
case "OAUTH_GITLAB_CLIENT_ID":
return c.OAuthGitlabID
case "OAUTH_GITLAB_CLIENT_SECRET":
return c.OAuthGitlabSecret
case "OAUTH_GENERIC_CLIENT_ID":
return c.OAuthGenericID
case "OAUTH_GENERIC_CLIENT_SECRET":
return c.OAuthGenericSecret
case "OAUTH_GENERIC_URL":
return c.OAuthGenericURL
case "OAUTH_GENERIC_NAME":
return c.OAuthGenericName
default:
return ""
}
}
func (c *Config) applyOverridesLocked() {
for k, v := range c.overrides {
switch k {
case "SFU_PROVIDER":
c.ProviderOrder = envOr(v, c.ProviderOrder)
case "DISTRIBUTORS":
c.Distributors = envOr(v, c.Distributors)
case "CDN_RTMP_URL":
c.CDNRTMPURL = v
case "CDN_NAME":
if v != "" {
c.CDNName = v
} else {
c.CDNName = "cdn"
}
case "SRS_API_BASE":
if v != "" {
c.SRSBaseURL = v
}
case "SRS_HTTP_BASE":
if v != "" {
c.SRSHttpURL = v
}
case "SRS_APP":
if v != "" {
c.SRSApp = v
}
case "SRS_SECRET":
c.SRSSecret = v
case "SRS_CANDIDATE":
if v != "" {
c.SRSCandidate = v
}
case "CF_APP_ID":
c.CFAppID = v
case "CF_APP_SECRET":
c.CFAppSecret = v
case "CF_BASE_URL":
if v != "" {
c.CFBaseURL = v
}
case "CF_STUN_URL":
if v != "" {
c.CFStunURL = v
}
case "SFU_TOKEN_REQUIRED":
c.TokenRequired = v == "1" || strings.EqualFold(v, "true")
case "ALLOW_REGISTER":
c.AllowRegister = v == "1" || strings.EqualFold(v, "true")
case "JWT_TTL":
if d := parseDuration(v, 0); d != 0 {
c.JWTTTL = d
}
case "SMTP_HOST":
c.SMTPHost = v
case "SMTP_PORT":
if v != "" {
c.SMTPPort = parseInt(v, c.SMTPPort)
}
case "SMTP_USER":
c.SMTPUser = v
case "SMTP_PASS":
c.SMTPPass = v
case "SMTP_FROM":
c.SMTPFrom = v
case "SMTP_FROM_NAME":
if v != "" {
c.SMTPFromName = v
}
case "SMTP_TLS":
c.SMTPTLS = v == "1" || strings.EqualFold(v, "true")
case "SMTP_ENABLED":
c.SMTPEnabled = v == "1" || strings.EqualFold(v, "true")
case "EMAIL_VERIFY_REQUIRED":
c.EmailVerifyRequired = v == "1" || strings.EqualFold(v, "true")
case "EMAIL_VERIFY_TTL":
if d := parseDuration(v, 0); d != 0 {
c.EmailVerifyTTL = d
}
case "EMAIL_VERIFY_BASE_URL":
if v != "" {
c.EmailVerifyBaseURL = v
}
case "OAUTH_ENABLED":
c.OAuthEnabled = v == "1" || strings.EqualFold(v, "true")
case "OAUTH_CALLBACK_BASE":
if v != "" {
c.OAuthCallbackBase = v
}
case "OAUTH_GOOGLE_CLIENT_ID":
c.OAuthGoogleID = v
case "OAUTH_GOOGLE_CLIENT_SECRET":
c.OAuthGoogleSecret = v
case "OAUTH_GITHUB_CLIENT_ID":
c.OAuthGithubID = v
case "OAUTH_GITHUB_CLIENT_SECRET":
c.OAuthGithubSecret = v
case "OAUTH_GITLAB_CLIENT_ID":
c.OAuthGitlabID = v
case "OAUTH_GITLAB_CLIENT_SECRET":
c.OAuthGitlabSecret = v
case "OAUTH_GENERIC_CLIENT_ID":
c.OAuthGenericID = v
case "OAUTH_GENERIC_CLIENT_SECRET":
c.OAuthGenericSecret = v
case "OAUTH_GENERIC_URL":
c.OAuthGenericURL = v
case "OAUTH_GENERIC_NAME":
if v != "" {
c.OAuthGenericName = v
}
}
}
if c.SMTPFrom == "" && c.SMTPUser != "" {
c.SMTPFrom = c.SMTPUser
}
if c.EmailVerifyBaseURL == "" {
c.EmailVerifyBaseURL = "http://localhost:" + c.HTTPPort
}
if c.OAuthCallbackBase == "" {
c.OAuthCallbackBase = "http://localhost:" + c.HTTPPort
}
}
func envOr(v, fallback string) string {
if v != "" {
return v
}
return fallback
}
func revertToEnv(key, fallback string) string {
envKey := key
if v := getenv(envKey, ""); v != "" {
return v
}
// hardcoded defaults for critical keys
switch key {
case "DISTRIBUTORS":
return "srs-hls,cf,cdn"
case "SFU_PROVIDER":
return "cloudflare,srs"
case "SRS_API_BASE":
return "http://localhost:1985"
case "SRS_HTTP_BASE":
return "http://localhost:8080"
case "SRS_APP":
return "live"
case "SRS_CANDIDATE":
return "127.0.0.1"
case "CF_BASE_URL":
return "https://rtc.live.cloudflare.com/v1"
case "CF_STUN_URL":
return "stun:stun.cloudflare.com:3478"
case "CDN_NAME":
return "cdn"
}
return fallback
}
func (c *Config) GetOverrides() map[string]string { c.ensureMu(); return c.overrides }
func (c *Config) revertFieldLocked(key string) {
switch key {
case "SFU_PROVIDER":
c.ProviderOrder = revertToEnv(key, c.ProviderOrder)
case "DISTRIBUTORS":
c.Distributors = revertToEnv(key, c.Distributors)
case "CDN_RTMP_URL":
c.CDNRTMPURL = ""
case "CDN_NAME":
c.CDNName = revertToEnv(key, c.CDNName)
case "SRS_API_BASE":
c.SRSBaseURL = revertToEnv(key, c.SRSBaseURL)
case "SRS_HTTP_BASE":
c.SRSHttpURL = revertToEnv(key, c.SRSHttpURL)
case "SRS_APP":
c.SRSApp = revertToEnv(key, c.SRSApp)
case "SRS_SECRET":
c.SRSSecret = ""
case "SRS_CANDIDATE":
c.SRSCandidate = revertToEnv(key, c.SRSCandidate)
case "CF_APP_ID":
c.CFAppID = ""
case "CF_APP_SECRET":
c.CFAppSecret = ""
case "CF_BASE_URL":
c.CFBaseURL = revertToEnv(key, c.CFBaseURL)
case "CF_STUN_URL":
c.CFStunURL = revertToEnv(key, c.CFStunURL)
case "SFU_TOKEN_REQUIRED":
c.TokenRequired = getenv("SFU_TOKEN_REQUIRED", "0") == "1"
case "ALLOW_REGISTER":
c.AllowRegister = getenv("ALLOW_REGISTER", "1") == "1"
case "JWT_TTL":
c.JWTTTL = parseDuration(getenv("JWT_TTL", "2h"), 2*time.Hour)
case "SMTP_HOST":
c.SMTPHost = ""
case "SMTP_PORT":
c.SMTPPort = parseInt(getenv("SMTP_PORT", "587"), 587)
case "SMTP_USER":
c.SMTPUser = ""
case "SMTP_PASS":
c.SMTPPass = ""
case "SMTP_FROM":
c.SMTPFrom = ""
case "SMTP_FROM_NAME":
c.SMTPFromName = revertToEnv(key, "SyncLive")
case "SMTP_TLS":
c.SMTPTLS = getenv("SMTP_TLS", "1") == "1"
case "SMTP_ENABLED":
c.SMTPEnabled = getenv("SMTP_ENABLED", "0") == "1"
case "EMAIL_VERIFY_REQUIRED":
c.EmailVerifyRequired = getenv("EMAIL_VERIFY_REQUIRED", "0") == "1"
case "EMAIL_VERIFY_TTL":
c.EmailVerifyTTL = parseDuration(getenv("EMAIL_VERIFY_TTL", "24h"), 24*time.Hour)
case "EMAIL_VERIFY_BASE_URL":
c.EmailVerifyBaseURL = revertToEnv(key, "http://localhost:"+c.HTTPPort)
case "OAUTH_ENABLED":
c.OAuthEnabled = getenv("OAUTH_ENABLED", "0") == "1"
case "OAUTH_CALLBACK_BASE":
c.OAuthCallbackBase = revertToEnv(key, "http://localhost:"+c.HTTPPort)
case "OAUTH_GOOGLE_CLIENT_ID", "OAUTH_GOOGLE_CLIENT_SECRET", "OAUTH_GITHUB_CLIENT_ID", "OAUTH_GITHUB_CLIENT_SECRET", "OAUTH_GITLAB_CLIENT_ID", "OAUTH_GITLAB_CLIENT_SECRET", "OAUTH_GENERIC_CLIENT_ID", "OAUTH_GENERIC_CLIENT_SECRET", "OAUTH_GENERIC_URL", "OAUTH_GENERIC_NAME":
// generic string clear
switch key {
case "OAUTH_GOOGLE_CLIENT_ID": c.OAuthGoogleID = ""
case "OAUTH_GOOGLE_CLIENT_SECRET": c.OAuthGoogleSecret = ""
case "OAUTH_GITHUB_CLIENT_ID": c.OAuthGithubID = ""
case "OAUTH_GITHUB_CLIENT_SECRET": c.OAuthGithubSecret = ""
case "OAUTH_GITLAB_CLIENT_ID": c.OAuthGitlabID = ""
case "OAUTH_GITLAB_CLIENT_SECRET": c.OAuthGitlabSecret = ""
case "OAUTH_GENERIC_CLIENT_ID": c.OAuthGenericID = ""
case "OAUTH_GENERIC_CLIENT_SECRET": c.OAuthGenericSecret = ""
case "OAUTH_GENERIC_URL": c.OAuthGenericURL = ""
case "OAUTH_GENERIC_NAME": c.OAuthGenericName = revertToEnv(key, "generic")
}
}
}