package config import ( "fmt" "os" "strings" "time" ) // Config 承载直播 SFU 分流服务的运行参数。 // 优先级:环境变量 > 默认值。Cloudflare Realtime 为主 SFU,SRS 为本地对照后端。 // 默认 DB 使用 libSQL 嵌入式 (file) 持久化房间拓扑;同时支持 JWT/Casbin 登录。 type Config struct { HTTPPort string GRPCPort string ProviderOrder string // 逗号分隔的后端顺序,如 "cloudflare,srs" SRSBaseURL string SRSHttpURL string SRSApp string SRSSecret string SRSCandidate string CFAppID string CFAppSecret string CFBaseURL string CFStunURL string TokenSecret string // 推流 JWT 签名密钥(SRS 入口鉴权) TokenRequired bool JWTSecret string JWTTTL time.Duration AuthModel string AuthPolicy string AuthUserFile string AllowRegister bool SMTPHost string SMTPPort int SMTPUser string SMTPPass string SMTPFrom string SMTPFromName string SMTPTLS bool SMTPEnabled bool EmailVerifyRequired bool EmailVerifyTTL time.Duration EmailVerifyBaseURL string OAuthEnabled bool OAuthCallbackBase string OAuthGoogleID string OAuthGoogleSecret string OAuthGithubID string OAuthGithubSecret string OAuthGitlabID string OAuthGitlabSecret string OAuthGenericID string OAuthGenericSecret string OAuthGenericURL string OAuthGenericName string AuthBossEnabled bool DatabaseURL string // 嵌入式 DB DSN(TURSO_DATABASE_URL / DATABASE_URL),默认 file:./data/sync-live.db AuthToken string // 远程 libSQL 凭证,嵌入模式可为空 } func Load() *Config { c := &Config{ HTTPPort: getenv("HTTP_PORT", "8088"), GRPCPort: getenv("GRPC_PORT", "9090"), ProviderOrder: getenv("SFU_PROVIDER", "cloudflare,srs"), SRSBaseURL: getenv("SRS_API_BASE", "http://localhost:1985"), SRSHttpURL: getenv("SRS_HTTP_BASE", "http://localhost:8080"), SRSApp: getenv("SRS_APP", "live"), SRSSecret: getenv("SRS_SECRET", ""), SRSCandidate: getenv("SRS_CANDIDATE", "127.0.0.1"), CFAppID: getenv("CF_APP_ID", ""), CFAppSecret: getenv("CF_APP_SECRET", ""), CFBaseURL: getenv("CF_BASE_URL", "https://rtc.live.cloudflare.com/v1"), CFStunURL: getenv("CF_STUN_URL", "stun:stun.cloudflare.com:3478"), TokenSecret: getenv("SFU_TOKEN_SECRET", ""), TokenRequired: getenv("SFU_TOKEN_REQUIRED", "0") == "1", JWTSecret: getenv("JWT_SECRET", ""), JWTTTL: parseDuration(getenv("JWT_TTL", "2h"), 2*time.Hour), AuthModel: getenv("CASBIN_MODEL", "internal/auth/model.conf"), AuthPolicy: getenv("CASBIN_POLICY", "internal/auth/policy.csv"), AuthUserFile: getenv("AUTH_USER_FILE", "data/users.json"), AllowRegister: getenv("ALLOW_REGISTER", "1") == "1", SMTPHost: getenv("SMTP_HOST", ""), SMTPPort: parseInt(getenv("SMTP_PORT", "587"), 587), SMTPUser: getenv("SMTP_USER", ""), SMTPPass: getenv("SMTP_PASS", ""), SMTPFrom: getenv("SMTP_FROM", ""), SMTPFromName: getenv("SMTP_FROM_NAME", "SyncLive"), SMTPTLS: getenv("SMTP_TLS", "1") == "1", SMTPEnabled: getenv("SMTP_ENABLED", "0") == "1", EmailVerifyRequired: getenv("EMAIL_VERIFY_REQUIRED", "0") == "1", EmailVerifyTTL: parseDuration(getenv("EMAIL_VERIFY_TTL", "24h"), 24*time.Hour), EmailVerifyBaseURL: getenv("EMAIL_VERIFY_BASE_URL", ""), OAuthEnabled: getenv("OAUTH_ENABLED", "0") == "1", OAuthCallbackBase: getenv("OAUTH_CALLBACK_BASE", ""), OAuthGoogleID: getenv("OAUTH_GOOGLE_CLIENT_ID", ""), OAuthGoogleSecret: getenv("OAUTH_GOOGLE_CLIENT_SECRET", ""), OAuthGithubID: getenv("OAUTH_GITHUB_CLIENT_ID", ""), OAuthGithubSecret: getenv("OAUTH_GITHUB_CLIENT_SECRET", ""), OAuthGitlabID: getenv("OAUTH_GITLAB_CLIENT_ID", ""), OAuthGitlabSecret: getenv("OAUTH_GITLAB_CLIENT_SECRET", ""), OAuthGenericID: getenv("OAUTH_GENERIC_CLIENT_ID", ""), OAuthGenericSecret: getenv("OAUTH_GENERIC_CLIENT_SECRET", ""), OAuthGenericURL: getenv("OAUTH_GENERIC_URL", ""), OAuthGenericName: getenv("OAUTH_GENERIC_NAME", "generic"), AuthBossEnabled: getenv("AUTHBOSS_ENABLED", "0") == "1", DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/sync-live.db?cache=shared&_journal_mode=WAL"), AuthToken: getenv("TURSO_AUTH_TOKEN", ""), } // JWT 密钥优先使用专用配置;否则回退到推流密钥(两者都应显式配置)。 if c.JWTSecret == "" { c.JWTSecret = c.TokenSecret } if c.DatabaseURL == "file:./data/sync-live.db?cache=shared&_journal_mode=WAL" { if v := os.Getenv("DATABASE_URL"); v != "" { c.DatabaseURL = v } else if v := os.Getenv("LIBSQL_URL"); v != "" { c.DatabaseURL = v } } if c.AuthToken == "" { if v := os.Getenv("TURSO_AUTH_TOKEN"); v != "" { c.AuthToken = v } else if v := os.Getenv("LIBSQL_AUTH_TOKEN"); v != "" { c.AuthToken = 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 } return c } // Validate 校验正式运行所需的敏感配置,缺省会返回错误,避免误以不安全默认密钥上线。 func (c *Config) Validate() error { if c.JWTSecret == "" { return fmt.Errorf("JWT_SECRET 必须显式配置,禁止依赖不安全默认密钥") } if c.TokenRequired && c.TokenSecret == "" { return fmt.Errorf("已启用 SFU_TOKEN_REQUIRED,但 SFU_TOKEN_SECRET 未配置") } return nil } func getenv(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func parseDuration(s string, def time.Duration) time.Duration { if s == "" { return def } if d, err := time.ParseDuration(s); err == nil { return d } return def } func parseInt(s string, def int) int { if s == "" { return def } var v int if _, err := fmt.Sscanf(s, "%d", &v); err == nil { return v } return def } func (c *Config) ProviderList() []string { parts := strings.Split(c.ProviderOrder, ",") out := make([]string, 0, len(parts)) seen := make(map[string]struct{}, len(parts)) for _, p := range parts { p = strings.TrimSpace(strings.ToLower(p)) if p == "" || p == "both" || p == "all" { if p == "both" || p == "all" { for _, b := range []string{"cloudflare", "srs"} { if _, ok := seen[b]; !ok { seen[b] = struct{}{} out = append(out, b) } } } continue } if _, ok := seen[p]; !ok { seen[p] = struct{}{} out = append(out, p) } } return out } func (c *Config) DSN() string { if c.AuthToken != "" && strings.HasPrefix(c.DatabaseURL, "libsql://") && !strings.Contains(c.DatabaseURL, "authToken") { sep := "?" if strings.Contains(c.DatabaseURL, "?") { sep = "&" } return c.DatabaseURL + sep + "authToken=" + c.AuthToken } return c.DatabaseURL } func (c *Config) IsRemoteTurso() bool { return strings.HasPrefix(c.DatabaseURL, "libsql://") || strings.HasPrefix(c.DatabaseURL, "https://") } func (c *Config) SMTPConfigured() bool { return c.SMTPHost != "" && c.SMTPUser != "" && c.SMTPPass != "" } func (c *Config) OAuthConfigured() bool { return c.OAuthGoogleID != "" || c.OAuthGithubID != "" || c.OAuthGitlabID != "" }