WitNote / internal /config /validate.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package config
import (
"fmt"
"strconv"
"strings"
)
// ValidationError represents a configuration validation error.
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidateFileConfig validates a FileConfig and returns all errors found.
func ValidateFileConfig(fc *FileConfig) []error {
var errs []error
// Server validation
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),
})
}
// Instance defaults validation
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),
})
}
// Multi-instance validation
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),
})
}
}
// Attach validation
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),
})
}
}
// IDPI validation
errs = append(errs, validateIDPIConfig(fc.Security.IDPI)...)
// Timeouts validation
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 {
// Accept common bind addresses
validBinds := map[string]bool{
"127.0.0.1": true,
"0.0.0.0": true,
"localhost": true,
"::1": true,
"::": true,
}
if validBinds[bind] {
return nil
}
// Basic IP format check (not exhaustive, just sanity)
// If it contains a dot, assume it's an IPv4 attempt
// If it contains a colon, assume it's an IPv6 attempt
// This is intentionally loose — the OS will reject truly invalid addresses
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"}
}
// validateIDPIConfig validates the security.idpi sub-section.
// Validation is skipped when IDPI is disabled; a zero-value IDPIConfig is always valid.
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
}
// ValidAllocationPolicies returns all valid allocation policy values.
func ValidAllocationPolicies() []string {
return []string{"fcfs", "round_robin", "random"}
}
// ValidAttachSchemes returns all valid attach URL schemes.
func ValidAttachSchemes() []string {
return []string{"ws", "wss", "http", "https"}
}