package config import ( "bytes" "encoding/json" "strings" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" ) // SanitizePayloadRules validates raw JSON payload rule params and drops invalid rules. func (cfg *Config) SanitizePayloadRules() { if cfg == nil { return } cfg.Payload.DefaultRaw = sanitizePayloadRawRules(cfg.Payload.DefaultRaw, "default-raw") cfg.Payload.OverrideRaw = sanitizePayloadRawRules(cfg.Payload.OverrideRaw, "override-raw") } func sanitizePayloadRawRules(rules []PayloadRule, section string) []PayloadRule { if len(rules) == 0 { return rules } out := make([]PayloadRule, 0, len(rules)) for i := range rules { rule := rules[i] if len(rule.Params) == 0 { continue } invalid := false for path, value := range rule.Params { raw, ok := payloadRawString(value) if !ok { continue } trimmed := bytes.TrimSpace(raw) if len(trimmed) == 0 || !json.Valid(trimmed) { log.WithFields(log.Fields{ "section": section, "rule_index": i + 1, "param": path, }).Warn("payload rule dropped: invalid raw JSON") invalid = true break } } if invalid { continue } out = append(out, rule) } return out } func payloadRawString(value any) ([]byte, bool) { switch typed := value.(type) { case string: return []byte(typed), true case []byte: return typed, true default: return nil, false } } // SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases. // It trims whitespace, normalizes channel keys to lower-case, drops empty entries, // allows multiple aliases per upstream name, and ensures aliases are unique within each channel. func (cfg *Config) SanitizeOAuthModelAlias() { if cfg == nil || len(cfg.OAuthModelAlias) == 0 { return } out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias)) for rawChannel, aliases := range cfg.OAuthModelAlias { channel := strings.ToLower(strings.TrimSpace(rawChannel)) if channel == "" || len(aliases) == 0 { continue } seenAlias := make(map[string]struct{}, len(aliases)) clean := make([]OAuthModelAlias, 0, len(aliases)) for _, entry := range aliases { name := strings.TrimSpace(entry.Name) alias := strings.TrimSpace(entry.Alias) if name == "" || alias == "" { continue } if strings.EqualFold(name, alias) { continue } aliasKey := strings.ToLower(alias) if _, ok := seenAlias[aliasKey]; ok { continue } seenAlias[aliasKey] = struct{}{} clean = append(clean, OAuthModelAlias{Name: name, Alias: alias, Fork: entry.Fork}) } if len(clean) > 0 { out[channel] = clean } } cfg.OAuthModelAlias = out } // SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are // not actionable, specifically those missing a BaseURL. It trims whitespace before // evaluation and preserves the relative order of remaining entries. func (cfg *Config) SanitizeOpenAICompatibility() { if cfg == nil || len(cfg.OpenAICompatibility) == 0 { return } out := make([]OpenAICompatibility, 0, len(cfg.OpenAICompatibility)) for i := range cfg.OpenAICompatibility { e := cfg.OpenAICompatibility[i] e.Name = strings.TrimSpace(e.Name) e.Prefix = normalizeModelPrefix(e.Prefix) e.BaseURL = strings.TrimSpace(e.BaseURL) e.Headers = NormalizeHeaders(e.Headers) if e.BaseURL == "" { // Skip providers with no base-url; treated as removed continue } out = append(out, e) } cfg.OpenAICompatibility = out } // SanitizeCodexKeys removes Codex API key entries missing a BaseURL. // It trims whitespace and preserves order for remaining entries. func (cfg *Config) SanitizeCodexKeys() { if cfg == nil || len(cfg.CodexKey) == 0 { return } out := make([]CodexKey, 0, len(cfg.CodexKey)) for i := range cfg.CodexKey { e := cfg.CodexKey[i] e.Prefix = normalizeModelPrefix(e.Prefix) e.BaseURL = strings.TrimSpace(e.BaseURL) e.Headers = NormalizeHeaders(e.Headers) e.ExcludedModels = NormalizeExcludedModels(e.ExcludedModels) if e.BaseURL == "" { continue } out = append(out, e) } cfg.CodexKey = out } // SanitizeClaudeKeys normalizes headers and API key entries for Claude credentials. func (cfg *Config) SanitizeClaudeKeys() { if cfg == nil || len(cfg.ClaudeKey) == 0 { return } for i := range cfg.ClaudeKey { entry := &cfg.ClaudeKey[i] entry.Prefix = normalizeModelPrefix(entry.Prefix) entry.Headers = NormalizeHeaders(entry.Headers) entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.APIKey = strings.TrimSpace(entry.APIKey) // Sanitize APIKeyEntries for j := range entry.APIKeyEntries { apiKeyEntry := &entry.APIKeyEntries[j] apiKeyEntry.APIKey = strings.TrimSpace(apiKeyEntry.APIKey) apiKeyEntry.ProxyURL = strings.TrimSpace(apiKeyEntry.ProxyURL) } } } // SanitizeGeminiKeys deduplicates and normalizes Gemini credentials. func (cfg *Config) SanitizeGeminiKeys() { if cfg == nil { return } seen := make(map[string]struct{}, len(cfg.GeminiKey)) out := cfg.GeminiKey[:0] for i := range cfg.GeminiKey { entry := cfg.GeminiKey[i] entry.APIKey = strings.TrimSpace(entry.APIKey) if entry.APIKey == "" { continue } entry.Prefix = normalizeModelPrefix(entry.Prefix) entry.BaseURL = strings.TrimSpace(entry.BaseURL) entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) entry.Headers = NormalizeHeaders(entry.Headers) entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) if _, exists := seen[entry.APIKey]; exists { continue } seen[entry.APIKey] = struct{}{} out = append(out, entry) } cfg.GeminiKey = out } func normalizeModelPrefix(prefix string) string { trimmed := strings.TrimSpace(prefix) trimmed = strings.Trim(trimmed, "/") if trimmed == "" { return "" } if strings.Contains(trimmed, "/") { return "" } return trimmed } func syncInlineAccessProvider(cfg *Config) { if cfg == nil { return } if len(cfg.APIKeys) == 0 { if provider := cfg.ConfigAPIKeyProvider(); provider != nil && len(provider.APIKeys) > 0 { cfg.APIKeys = append([]string(nil), provider.APIKeys...) } } cfg.Access.Providers = nil } // looksLikeBcrypt returns true if the provided string appears to be a bcrypt hash. func looksLikeBcrypt(s string) bool { return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") } // NormalizeHeaders trims header keys and values and removes empty pairs. func NormalizeHeaders(headers map[string]string) map[string]string { if len(headers) == 0 { return nil } clean := make(map[string]string, len(headers)) for k, v := range headers { key := strings.TrimSpace(k) val := strings.TrimSpace(v) if key == "" || val == "" { continue } clean[key] = val } if len(clean) == 0 { return nil } return clean } // NormalizeExcludedModels trims, lowercases, and deduplicates model exclusion patterns. // It preserves the order of first occurrences and drops empty entries. func NormalizeExcludedModels(models []string) []string { if len(models) == 0 { return nil } seen := make(map[string]struct{}, len(models)) out := make([]string, 0, len(models)) for _, raw := range models { trimmed := strings.ToLower(strings.TrimSpace(raw)) if trimmed == "" { continue } if _, exists := seen[trimmed]; exists { continue } seen[trimmed] = struct{}{} out = append(out, trimmed) } if len(out) == 0 { return nil } return out } // NormalizeOAuthExcludedModels cleans provider -> excluded models mappings by normalizing provider keys // and applying model exclusion normalization to each entry. func NormalizeOAuthExcludedModels(entries map[string][]string) map[string][]string { if len(entries) == 0 { return nil } out := make(map[string][]string, len(entries)) for provider, models := range entries { key := strings.ToLower(strings.TrimSpace(provider)) if key == "" { continue } normalized := NormalizeExcludedModels(models) if len(normalized) == 0 { continue } out[key] = normalized } if len(out) == 0 { return nil } return out } // hashSecret hashes the given secret using bcrypt. func hashSecret(secret string) (string, error) { // Use default cost for simplicity. hashedBytes, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) if err != nil { return "", err } return string(hashedBytes), nil } func sanitizeConfigForPersist(cfg *Config) *Config { if cfg == nil { return nil } clone := *cfg clone.SDKConfig = cfg.SDKConfig clone.SDKConfig.Access = AccessConfig{} return &clone }