| package config |
|
|
| import ( |
| "fmt" |
| "strconv" |
| "strings" |
| ) |
|
|
| |
| type ValidationError struct { |
| Field string |
| Message string |
| } |
|
|
| func (e ValidationError) Error() string { |
| return fmt.Sprintf("%s: %s", e.Field, e.Message) |
| } |
|
|
| |
| func ValidateFileConfig(fc *FileConfig) []error { |
| var errs []error |
|
|
| |
| if fc.Server.Port != "" { |
| if err := validatePort(fc.Server.Port, "server.port"); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| if fc.Server.Bind != "" { |
| if err := validateBind(fc.Server.Bind, "server.bind"); err != nil { |
| errs = append(errs, err) |
| } |
| } |
| if fc.MultiInstance.InstancePortStart != nil && fc.MultiInstance.InstancePortEnd != nil { |
| if *fc.MultiInstance.InstancePortStart > *fc.MultiInstance.InstancePortEnd { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.instancePortStart/End", |
| Message: fmt.Sprintf("start port (%d) must be <= end port (%d)", *fc.MultiInstance.InstancePortStart, *fc.MultiInstance.InstancePortEnd), |
| }) |
| } |
| } |
| if fc.MultiInstance.Restart.MaxRestarts != nil { |
| if *fc.MultiInstance.Restart.MaxRestarts < -1 { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.restart.maxRestarts", |
| Message: fmt.Sprintf("must be >= 0 or -1 for unlimited (got %d)", *fc.MultiInstance.Restart.MaxRestarts), |
| }) |
| } |
| } |
| if fc.MultiInstance.Restart.InitBackoffSec != nil && *fc.MultiInstance.Restart.InitBackoffSec < 1 { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.restart.initBackoffSec", |
| Message: fmt.Sprintf("must be >= 1 (got %d)", *fc.MultiInstance.Restart.InitBackoffSec), |
| }) |
| } |
| if fc.MultiInstance.Restart.MaxBackoffSec != nil && *fc.MultiInstance.Restart.MaxBackoffSec < 1 { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.restart.maxBackoffSec", |
| Message: fmt.Sprintf("must be >= 1 (got %d)", *fc.MultiInstance.Restart.MaxBackoffSec), |
| }) |
| } |
| if fc.MultiInstance.Restart.StableAfterSec != nil && *fc.MultiInstance.Restart.StableAfterSec < 1 { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.restart.stableAfterSec", |
| Message: fmt.Sprintf("must be >= 1 (got %d)", *fc.MultiInstance.Restart.StableAfterSec), |
| }) |
| } |
| if fc.MultiInstance.Restart.InitBackoffSec != nil && fc.MultiInstance.Restart.MaxBackoffSec != nil && |
| *fc.MultiInstance.Restart.InitBackoffSec > *fc.MultiInstance.Restart.MaxBackoffSec { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.restart.initBackoffSec/maxBackoffSec", |
| Message: fmt.Sprintf("init backoff (%d) must be <= max backoff (%d)", *fc.MultiInstance.Restart.InitBackoffSec, *fc.MultiInstance.Restart.MaxBackoffSec), |
| }) |
| } |
|
|
| |
| if fc.InstanceDefaults.Mode != "" && fc.InstanceDefaults.Mode != "headless" && fc.InstanceDefaults.Mode != "headed" { |
| errs = append(errs, ValidationError{ |
| Field: "instanceDefaults.mode", |
| Message: fmt.Sprintf("invalid value %q (must be headless or headed)", fc.InstanceDefaults.Mode), |
| }) |
| } |
| if fc.InstanceDefaults.StealthLevel != "" { |
| if !isValidStealthLevel(fc.InstanceDefaults.StealthLevel) { |
| errs = append(errs, ValidationError{ |
| Field: "instanceDefaults.stealthLevel", |
| Message: fmt.Sprintf("invalid value %q (must be light, medium, or full)", fc.InstanceDefaults.StealthLevel), |
| }) |
| } |
| } |
| if fc.InstanceDefaults.TabEvictionPolicy != "" { |
| if !isValidEvictionPolicy(fc.InstanceDefaults.TabEvictionPolicy) { |
| errs = append(errs, ValidationError{ |
| Field: "instanceDefaults.tabEvictionPolicy", |
| Message: fmt.Sprintf("invalid value %q (must be reject, close_oldest, or close_lru)", fc.InstanceDefaults.TabEvictionPolicy), |
| }) |
| } |
| } |
| if fc.InstanceDefaults.MaxTabs != nil && *fc.InstanceDefaults.MaxTabs < 1 { |
| errs = append(errs, ValidationError{ |
| Field: "instanceDefaults.maxTabs", |
| Message: fmt.Sprintf("must be >= 1 (got %d)", *fc.InstanceDefaults.MaxTabs), |
| }) |
| } |
| if fc.InstanceDefaults.MaxParallelTabs != nil && *fc.InstanceDefaults.MaxParallelTabs < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "instanceDefaults.maxParallelTabs", |
| Message: fmt.Sprintf("must be >= 0 (got %d)", *fc.InstanceDefaults.MaxParallelTabs), |
| }) |
| } |
|
|
| |
| if fc.MultiInstance.Strategy != "" { |
| if !isValidStrategy(fc.MultiInstance.Strategy) { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.strategy", |
| Message: fmt.Sprintf("invalid value %q (must be simple, explicit, simple-autorestart, or always-on)", fc.MultiInstance.Strategy), |
| }) |
| } |
| } |
| if fc.MultiInstance.AllocationPolicy != "" { |
| if !isValidAllocationPolicy(fc.MultiInstance.AllocationPolicy) { |
| errs = append(errs, ValidationError{ |
| Field: "multiInstance.allocationPolicy", |
| Message: fmt.Sprintf("invalid value %q (must be fcfs, round_robin, or random)", fc.MultiInstance.AllocationPolicy), |
| }) |
| } |
| } |
|
|
| |
| for _, scheme := range fc.Security.Attach.AllowSchemes { |
| if !isValidAttachScheme(scheme) { |
| errs = append(errs, ValidationError{ |
| Field: "security.attach.allowSchemes", |
| Message: fmt.Sprintf("invalid value %q (must be ws, wss, http, or https)", scheme), |
| }) |
| } |
| } |
|
|
| |
| errs = append(errs, validateIDPIConfig(fc.Security.IDPI)...) |
|
|
| |
| if fc.Timeouts.ActionSec < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "timeouts.actionSec", |
| Message: fmt.Sprintf("must be >= 0 (got %d)", fc.Timeouts.ActionSec), |
| }) |
| } |
| if fc.Timeouts.NavigateSec < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "timeouts.navigateSec", |
| Message: fmt.Sprintf("must be >= 0 (got %d)", fc.Timeouts.NavigateSec), |
| }) |
| } |
| if fc.Timeouts.ShutdownSec < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "timeouts.shutdownSec", |
| Message: fmt.Sprintf("must be >= 0 (got %d)", fc.Timeouts.ShutdownSec), |
| }) |
| } |
| if fc.Timeouts.WaitNavMs < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "timeouts.waitNavMs", |
| Message: fmt.Sprintf("must be >= 0 (got %d)", fc.Timeouts.WaitNavMs), |
| }) |
| } |
|
|
| return errs |
| } |
|
|
| func validatePort(port string, field string) error { |
| p, err := strconv.Atoi(port) |
| if err != nil { |
| return ValidationError{ |
| Field: field, |
| Message: fmt.Sprintf("invalid port %q (must be a number)", port), |
| } |
| } |
| if p < 1 || p > 65535 { |
| return ValidationError{ |
| Field: field, |
| Message: fmt.Sprintf("port %d out of range (must be 1-65535)", p), |
| } |
| } |
| return nil |
| } |
|
|
| func validateBind(bind string, field string) error { |
| |
| validBinds := map[string]bool{ |
| "127.0.0.1": true, |
| "0.0.0.0": true, |
| "localhost": true, |
| "::1": true, |
| "::": true, |
| } |
| if validBinds[bind] { |
| return nil |
| } |
| |
| |
| |
| |
| return nil |
| } |
|
|
| func isValidStealthLevel(level string) bool { |
| switch level { |
| case "light", "full": |
| return true |
| default: |
| return false |
| } |
| } |
|
|
| func isValidEvictionPolicy(policy string) bool { |
| switch policy { |
| case "reject", "close_oldest", "close_lru": |
| return true |
| default: |
| return false |
| } |
| } |
|
|
| func isValidStrategy(strategy string) bool { |
| switch strategy { |
| case "simple", "explicit", "simple-autorestart", "always-on": |
| return true |
| default: |
| return false |
| } |
| } |
|
|
| func isValidAllocationPolicy(policy string) bool { |
| switch policy { |
| case "fcfs", "round_robin", "random": |
| return true |
| default: |
| return false |
| } |
| } |
|
|
| func isValidAttachScheme(scheme string) bool { |
| switch scheme { |
| case "ws", "wss", "http", "https": |
| return true |
| default: |
| return false |
| } |
| } |
|
|
| func ValidStealthLevels() []string { |
| return []string{"light", "full"} |
| } |
|
|
| func ValidEvictionPolicies() []string { |
| return []string{"reject", "close_oldest", "close_lru"} |
| } |
|
|
| func ValidStrategies() []string { |
| return []string{"simple", "explicit", "simple-autorestart", "always-on"} |
| } |
|
|
| |
| |
| func validateIDPIConfig(cfg IDPIConfig) []error { |
| if !cfg.Enabled { |
| return nil |
| } |
|
|
| var errs []error |
|
|
| for _, domain := range cfg.AllowedDomains { |
| trimmed := strings.TrimSpace(domain) |
| if trimmed == "" { |
| errs = append(errs, ValidationError{ |
| Field: "security.idpi.allowedDomains", |
| Message: "domain pattern must not be empty or whitespace-only", |
| }) |
| continue |
| } |
| if strings.ContainsAny(trimmed, " \t") { |
| errs = append(errs, ValidationError{ |
| Field: "security.idpi.allowedDomains", |
| Message: fmt.Sprintf("domain pattern %q must not contain whitespace", trimmed), |
| }) |
| } |
| if strings.HasPrefix(trimmed, "file://") { |
| errs = append(errs, ValidationError{ |
| Field: "security.idpi.allowedDomains", |
| Message: fmt.Sprintf("domain pattern %q must not use the file:// scheme; use a hostname", trimmed), |
| }) |
| } |
| } |
|
|
| for _, p := range cfg.CustomPatterns { |
| if strings.TrimSpace(p) == "" { |
| errs = append(errs, ValidationError{ |
| Field: "security.idpi.customPatterns", |
| Message: "custom pattern must not be empty or whitespace-only", |
| }) |
| } |
| } |
|
|
| if cfg.ScanTimeoutSec < 0 { |
| errs = append(errs, ValidationError{ |
| Field: "security.idpi.scanTimeoutSec", |
| Message: "scanTimeoutSec must not be negative", |
| }) |
| } |
|
|
| return errs |
| } |
|
|
| |
| func ValidAllocationPolicies() []string { |
| return []string{"fcfs", "round_robin", "random"} |
| } |
|
|
| |
| func ValidAttachSchemes() []string { |
| return []string{"ws", "wss", "http", "https"} |
| } |
|
|