557 lines
14 KiB
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")
|
|
}
|
|
}
|
|
}
|