Split the monolithic config.go (~120KB) into focused modules: - auth.go: JWT, TOTP, Turnstile, RateLimit configs - billing.go: Billing and Pricing configs - database.go: Database and Redis configs - gateway.go: Gateway and Upstream configs - gateway_sub.go: Gateway sub-configurations - ops_and_cache.go: Ops and Cache configs - platforms.go: Platform-specific configs - security.go: Security-related configs - server.go: Server configuration - config_defaults.go: Default values - config_defaults_detail.go: Detailed defaults - config_helpers.go: Helper functions - config_validate.go: Validation logic - config_validate_gateway.go: Gateway validation This improves: - Code maintainability and readability - Faster compilation (smaller files) - Easier navigation and debugging - Better separation of concerns
105 lines
3.4 KiB
Go
105 lines
3.4 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// normalizeStringSlice normalizes a string slice by trimming empties.
|
|
func normalizeStringSlice(values []string) []string {
|
|
if len(values) == 0 { return values }
|
|
out := make([]string, 0, len(values))
|
|
for _, v := range values {
|
|
if t := strings.TrimSpace(v); t != "" { out = append(out, t) }
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isWeakJWTSecret(secret string) bool {
|
|
lower := strings.ToLower(strings.TrimSpace(secret))
|
|
if lower == "" { return true }
|
|
weak := map[string]struct{}{
|
|
"change-me-in-production": {}, "changeme": {}, "secret": {}, "password": {},
|
|
"123456": {}, "12345678": {}, "admin": {}, "jwt-secret": {},
|
|
}
|
|
_, exists := weak[lower]
|
|
return exists
|
|
}
|
|
|
|
func generateJWTSecret(byteLength int) (string, error) {
|
|
if byteLength <= 0 { byteLength = 32 }
|
|
buf := make([]byte, byteLength)
|
|
if _, err := rand.Read(buf); err != nil { return "", err }
|
|
return hex.EncodeToString(buf), nil
|
|
}
|
|
|
|
// GetServerAddress returns server address before full config validation (for setup wizard).
|
|
func GetServerAddress() string {
|
|
v := viper.New()
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath("."); v.AddConfigPath("./config"); v.AddConfigPath("/etc/sub2api")
|
|
v.AutomaticEnv()
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.SetDefault("server.host", "0.0.0.0")
|
|
v.SetDefault("server.port", 8080)
|
|
_ = v.ReadInConfig()
|
|
return fmt.Sprintf("%s:%d", v.GetString("server.host"), v.GetInt("server.port"))
|
|
}
|
|
|
|
// ValidateAbsoluteHTTPURL validates an absolute HTTP(S) URL.
|
|
func ValidateAbsoluteHTTPURL(raw string) error {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" { return fmt.Errorf("empty url") }
|
|
u, err := url.Parse(raw)
|
|
if err != nil { return err }
|
|
if !u.IsAbs() { return fmt.Errorf("must be absolute") }
|
|
if !isHTTPScheme(u.Scheme) { return fmt.Errorf("unsupported scheme: %s", u.Scheme) }
|
|
if strings.TrimSpace(u.Host) == "" { return fmt.Errorf("missing host") }
|
|
if u.Fragment != "" { return fmt.Errorf("must not include fragment") }
|
|
return nil
|
|
}
|
|
|
|
// ValidateFrontendRedirectURL validates frontend redirect URL (absolute http(s) or relative path).
|
|
func ValidateFrontendRedirectURL(raw string) error {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" { return fmt.Errorf("empty url") }
|
|
if strings.ContainsAny(raw, "\r\n") { return fmt.Errorf("contains invalid characters") }
|
|
if strings.HasPrefix(raw, "/") {
|
|
if strings.HasPrefix(raw, "//") { return fmt.Errorf("must not start with //") }
|
|
return nil
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil { return err }
|
|
if !u.IsAbs() { return fmt.Errorf("must be absolute http(s) url or relative path") }
|
|
if !isHTTPScheme(u.Scheme) { return fmt.Errorf("unsupported scheme: %s", u.Scheme) }
|
|
if strings.TrimSpace(u.Host) == "" { return fmt.Errorf("missing host") }
|
|
if u.Fragment != "" { return fmt.Errorf("must not include fragment") }
|
|
return nil
|
|
}
|
|
|
|
func scopeContainsOpenID(scopes string) bool {
|
|
for _, scope := range strings.Fields(strings.ToLower(strings.TrimSpace(scopes))) {
|
|
if scope == "openid" { return true }
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isHTTPScheme(scheme string) bool {
|
|
return strings.EqualFold(scheme, "http") || strings.EqualFold(scheme, "https")
|
|
}
|
|
|
|
func warnIfInsecureURL(field, raw string) {
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil { return }
|
|
if strings.EqualFold(u.Scheme, "http") {
|
|
slog.Warn("url uses http scheme; use https in production to avoid token leakage", "field", field)
|
|
}
|
|
}
|