354 lines
10 KiB
Go
354 lines
10 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"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
|
||
|
||
Distributors string // 逗号分隔的分发方式,如 "srs-hls,cf,cdn"
|
||
CDNRTMPURL string // 第三方 CDN RTMP 推流地址模板,如 rtmp://cdn.example.com/live
|
||
CDNName string
|
||
|
||
DatabaseURL string // 嵌入式 DB DSN(TURSO_DATABASE_URL / DATABASE_URL),默认 file:./data/sync-live.db
|
||
AuthToken string // 远程 libSQL 凭证,嵌入模式可为空
|
||
|
||
mu *sync.RWMutex `json:"-"`
|
||
overrides map[string]string `json:"-"`
|
||
runtimeFile string `json:"-"`
|
||
}
|
||
|
||
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",
|
||
|
||
Distributors: getenv("DISTRIBUTORS", "srs-hls,cf,cdn"),
|
||
CDNRTMPURL: getenv("CDN_RTMP_URL", ""),
|
||
CDNName: getenv("CDN_NAME", "cdn"),
|
||
|
||
DatabaseURL: getenv("TURSO_DATABASE_URL", "file:./data/sync-live.db?cache=shared&_journal_mode=WAL"),
|
||
AuthToken: getenv("TURSO_AUTH_TOKEN", ""),
|
||
|
||
mu: &sync.RWMutex{},
|
||
overrides: map[string]string{},
|
||
runtimeFile: "data/runtime_config.json",
|
||
}
|
||
// 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
|
||
}
|
||
_ = c.LoadRuntime()
|
||
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 (c *Config) ValidateDistributors() error {
|
||
// 运行时允许 cdn 列表占位,即使未配置 RTMP URL 也仅影响分发就绪状态,不阻断配置保存
|
||
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) DistributorList() []string {
|
||
return ParseDistributors(c.Distributors)
|
||
}
|
||
|
||
// ParseDistributors parses and normalizes distributors string.
|
||
func ParseDistributors(raw string) []string {
|
||
parts := strings.Split(raw, ",")
|
||
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 == "" {
|
||
continue
|
||
}
|
||
// normalize aliases
|
||
if p == "srs" || p == "hls" {
|
||
p = "srs-hls"
|
||
}
|
||
if p == "cloudflare" || p == "cf-sfu" {
|
||
p = "cf"
|
||
}
|
||
// only keep known kinds
|
||
if p != "srs-hls" && p != "cf" && p != "cdn" {
|
||
continue
|
||
}
|
||
if _, ok := seen[p]; !ok {
|
||
seen[p] = struct{}{}
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
|
||
// EffectiveDistributorsForRoom returns per-room distributors if override present, else global.
|
||
func (c *Config) EffectiveDistributorsForRoom(roomDistributors *string) []string {
|
||
if roomDistributors != nil {
|
||
return ParseDistributors(*roomDistributors)
|
||
}
|
||
return c.DistributorList()
|
||
}
|
||
|
||
func NormalizeDistributorsString(raw string) string {
|
||
return strings.Join(ParseDistributors(raw), ",")
|
||
}
|
||
|
||
|
||
func ValidateDistributorsString(raw string) error {
|
||
// use ParseDistributors then re-validate known set; unknown silently dropped, so check if input contains unknown non-empty token
|
||
parts := strings.Split(raw, ",")
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(strings.ToLower(p))
|
||
if p == "" {
|
||
continue
|
||
}
|
||
normalized := p
|
||
if p == "srs" || p == "hls" {
|
||
normalized = "srs-hls"
|
||
}
|
||
if p == "cloudflare" || p == "cf-sfu" {
|
||
normalized = "cf"
|
||
}
|
||
if normalized != "srs-hls" && normalized != "cf" && normalized != "cdn" {
|
||
return fmt.Errorf("unsupported distributor %q", p)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) CDNEnabled() bool {
|
||
return c.CDNRTMPURL != ""
|
||
}
|
||
|
||
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 != ""
|
||
}
|
||
|
||
func (c *Config) RuntimeFilePath() string {
|
||
if c.runtimeFile != "" {
|
||
return c.runtimeFile
|
||
}
|
||
return "data/runtime_config.json"
|
||
}
|
||
|
||
func (c *Config) RLock() { c.ensureMu(); c.mu.RLock() }
|
||
func (c *Config) RUnlock() { c.mu.RUnlock() }
|
||
func (c *Config) Lock() { c.ensureMu(); c.mu.Lock() }
|
||
func (c *Config) Unlock() { c.mu.Unlock() }
|