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

354 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() }