| package config |
|
|
| import ( |
| "encoding/json" |
| "log/slog" |
| "os" |
| "path/filepath" |
| "strconv" |
| "time" |
| ) |
|
|
| |
| func Load() *RuntimeConfig { |
| cfg := &RuntimeConfig{ |
| |
| Bind: "127.0.0.1", |
| Port: "9867", |
| InstancePortStart: 9868, |
| InstancePortEnd: 9968, |
| Token: os.Getenv("PINCHTAB_TOKEN"), |
| StateDir: userConfigDir(), |
|
|
| |
| AllowEvaluate: false, |
| AllowMacro: false, |
| AllowScreencast: false, |
| AllowDownload: false, |
| AllowUpload: false, |
| MaxRedirects: -1, |
|
|
| |
| Headless: true, |
| NoRestore: false, |
| ProfileDir: "", |
| ProfilesBaseDir: "", |
| DefaultProfile: "default", |
| ChromeVersion: "144.0.7559.133", |
| Timezone: "", |
| BlockImages: false, |
| BlockMedia: false, |
| BlockAds: false, |
| MaxTabs: 20, |
| MaxParallelTabs: 0, |
| ChromeBinary: "", |
| ChromeExtraFlags: "", |
| ExtensionPaths: nil, |
| UserAgent: "", |
| NoAnimations: false, |
| StealthLevel: "light", |
| TabEvictionPolicy: "close_lru", |
|
|
| |
| ActionTimeout: 30 * time.Second, |
| NavigateTimeout: 60 * time.Second, |
| ShutdownTimeout: 10 * time.Second, |
| WaitNavDelay: 1 * time.Second, |
|
|
| |
| Strategy: "always-on", |
| AllocationPolicy: "fcfs", |
| RestartMaxRestarts: 20, |
| RestartInitBackoff: 2 * time.Second, |
| RestartMaxBackoff: 60 * time.Second, |
| RestartStableAfter: 5 * time.Minute, |
|
|
| |
| AttachEnabled: true, |
| AttachAllowHosts: []string{"127.0.0.1", "localhost", "::1"}, |
| AttachAllowSchemes: []string{"ws", "wss"}, |
|
|
| |
| IDPI: IDPIConfig{ |
| Enabled: true, |
| AllowedDomains: append([]string(nil), defaultLocalAllowedDomains...), |
| StrictMode: true, |
| ScanContent: true, |
| WrapContent: true, |
| ScanTimeoutSec: 5, |
| }, |
|
|
| |
| Engine: "chrome", |
| } |
| finalizeProfileConfig(cfg) |
|
|
| |
| configPath := envOr("PINCHTAB_CONFIG", filepath.Join(userConfigDir(), "config.json")) |
|
|
| data, err := os.ReadFile(configPath) |
| if err != nil { |
| if !os.IsNotExist(err) { |
| slog.Warn("failed to read config file", "path", configPath, "error", err) |
| } |
| return cfg |
| } |
|
|
| slog.Debug("loading config file", "path", configPath) |
|
|
| var fc *FileConfig |
|
|
| if isLegacyConfig(data) { |
| var lc legacyFileConfig |
| if err := json.Unmarshal(data, &lc); err != nil { |
| slog.Warn("failed to parse legacy config", "path", configPath, "error", err) |
| return cfg |
| } |
| fc = convertLegacyConfig(&lc) |
| slog.Info("loaded legacy flat config, consider migrating to nested format", "path", configPath) |
| } else { |
| fc = &FileConfig{} |
| if err := json.Unmarshal(data, fc); err != nil { |
| slog.Warn("failed to parse config", "path", configPath, "error", err) |
| return cfg |
| } |
| } |
|
|
| |
| if errs := ValidateFileConfig(fc); len(errs) > 0 { |
| for _, e := range errs { |
| slog.Warn("config validation error", "path", configPath, "error", e) |
| } |
| } |
|
|
| |
| applyFileConfig(cfg, fc) |
| finalizeProfileConfig(cfg) |
|
|
| return cfg |
| } |
|
|
| func envOr(key, fallback string) string { |
| if v := os.Getenv(key); v != "" { |
| return v |
| } |
| return fallback |
| } |
|
|
| func envIntOr(key string, fallback int) int { |
| v := os.Getenv(key) |
| if v == "" { |
| return fallback |
| } |
| n, err := strconv.Atoi(v) |
| if err != nil || n < 0 { |
| return fallback |
| } |
| return n |
| } |
|
|
| func finalizeProfileConfig(cfg *RuntimeConfig) { |
| if cfg.DefaultProfile == "" { |
| cfg.DefaultProfile = "default" |
| } |
| if cfg.ProfilesBaseDir == "" { |
| cfg.ProfilesBaseDir = filepath.Join(cfg.StateDir, "profiles") |
| } |
| if cfg.ProfileDir == "" { |
| cfg.ProfileDir = filepath.Join(cfg.ProfilesBaseDir, cfg.DefaultProfile) |
| } |
| } |
|
|
| func applyFileConfig(cfg *RuntimeConfig, fc *FileConfig) { |
| |
| if fc.Server.Port != "" { |
| cfg.Port = fc.Server.Port |
| } |
| if fc.Server.Bind != "" { |
| cfg.Bind = fc.Server.Bind |
| } |
| if fc.Server.Token != "" && os.Getenv("PINCHTAB_TOKEN") == "" { |
| cfg.Token = fc.Server.Token |
| } |
| if fc.Server.StateDir != "" { |
| cfg.StateDir = fc.Server.StateDir |
| } |
| if fc.Server.Engine != "" { |
| cfg.Engine = fc.Server.Engine |
| } |
| |
| if fc.Security.AllowEvaluate != nil { |
| cfg.AllowEvaluate = *fc.Security.AllowEvaluate |
| } |
| if fc.Security.AllowMacro != nil { |
| cfg.AllowMacro = *fc.Security.AllowMacro |
| } |
| if fc.Security.AllowScreencast != nil { |
| cfg.AllowScreencast = *fc.Security.AllowScreencast |
| } |
| if fc.Security.AllowDownload != nil { |
| cfg.AllowDownload = *fc.Security.AllowDownload |
| } |
| if fc.Security.AllowUpload != nil { |
| cfg.AllowUpload = *fc.Security.AllowUpload |
| } |
| if fc.Security.MaxRedirects != nil { |
| cfg.MaxRedirects = *fc.Security.MaxRedirects |
| } |
| |
| cfg.IDPI = fc.Security.IDPI |
|
|
| |
| if fc.Browser.ChromeVersion != "" { |
| cfg.ChromeVersion = fc.Browser.ChromeVersion |
| } |
| if fc.Browser.ChromeBinary != "" { |
| cfg.ChromeBinary = fc.Browser.ChromeBinary |
| } |
| if fc.Browser.ChromeExtraFlags != "" { |
| cfg.ChromeExtraFlags = fc.Browser.ChromeExtraFlags |
| } |
| if len(fc.Browser.ExtensionPaths) > 0 { |
| cfg.ExtensionPaths = fc.Browser.ExtensionPaths |
| } |
|
|
| |
| if fc.InstanceDefaults.Mode != "" { |
| cfg.Headless = modeToHeadless(fc.InstanceDefaults.Mode, cfg.Headless) |
| } |
| if fc.InstanceDefaults.NoRestore != nil { |
| cfg.NoRestore = *fc.InstanceDefaults.NoRestore |
| } |
| if fc.InstanceDefaults.Timezone != "" { |
| cfg.Timezone = fc.InstanceDefaults.Timezone |
| } |
| if fc.InstanceDefaults.BlockImages != nil { |
| cfg.BlockImages = *fc.InstanceDefaults.BlockImages |
| } |
| if fc.InstanceDefaults.BlockMedia != nil { |
| cfg.BlockMedia = *fc.InstanceDefaults.BlockMedia |
| } |
| if fc.InstanceDefaults.BlockAds != nil { |
| cfg.BlockAds = *fc.InstanceDefaults.BlockAds |
| } |
| if fc.InstanceDefaults.MaxTabs != nil { |
| cfg.MaxTabs = *fc.InstanceDefaults.MaxTabs |
| } |
| if fc.InstanceDefaults.MaxParallelTabs != nil { |
| cfg.MaxParallelTabs = *fc.InstanceDefaults.MaxParallelTabs |
| } |
| if fc.InstanceDefaults.UserAgent != "" { |
| cfg.UserAgent = fc.InstanceDefaults.UserAgent |
| } |
| if fc.InstanceDefaults.NoAnimations != nil { |
| cfg.NoAnimations = *fc.InstanceDefaults.NoAnimations |
| } |
| if fc.InstanceDefaults.StealthLevel != "" { |
| cfg.StealthLevel = fc.InstanceDefaults.StealthLevel |
| } |
| if fc.InstanceDefaults.TabEvictionPolicy != "" { |
| cfg.TabEvictionPolicy = fc.InstanceDefaults.TabEvictionPolicy |
| } |
|
|
| |
| if fc.Profiles.BaseDir != "" { |
| cfg.ProfilesBaseDir = fc.Profiles.BaseDir |
| } |
| if fc.Profiles.DefaultProfile != "" { |
| cfg.DefaultProfile = fc.Profiles.DefaultProfile |
| } |
| cfg.ProfileDir = "" |
|
|
| |
| if fc.MultiInstance.Strategy != "" { |
| cfg.Strategy = fc.MultiInstance.Strategy |
| } |
| if fc.MultiInstance.AllocationPolicy != "" { |
| cfg.AllocationPolicy = fc.MultiInstance.AllocationPolicy |
| } |
| if fc.MultiInstance.InstancePortStart != nil { |
| cfg.InstancePortStart = *fc.MultiInstance.InstancePortStart |
| } |
| if fc.MultiInstance.InstancePortEnd != nil { |
| cfg.InstancePortEnd = *fc.MultiInstance.InstancePortEnd |
| } |
| |
| if fc.MultiInstance.Restart.MaxRestarts != nil { |
| cfg.RestartMaxRestarts = *fc.MultiInstance.Restart.MaxRestarts |
| } |
| if fc.MultiInstance.Restart.InitBackoffSec != nil { |
| cfg.RestartInitBackoff = time.Duration(*fc.MultiInstance.Restart.InitBackoffSec) * time.Second |
| } |
| if fc.MultiInstance.Restart.MaxBackoffSec != nil { |
| cfg.RestartMaxBackoff = time.Duration(*fc.MultiInstance.Restart.MaxBackoffSec) * time.Second |
| } |
| if fc.MultiInstance.Restart.StableAfterSec != nil { |
| cfg.RestartStableAfter = time.Duration(*fc.MultiInstance.Restart.StableAfterSec) * time.Second |
| } |
|
|
| |
| if fc.Security.Attach.Enabled != nil { |
| cfg.AttachEnabled = *fc.Security.Attach.Enabled |
| } |
| if len(fc.Security.Attach.AllowHosts) > 0 { |
| cfg.AttachAllowHosts = append([]string(nil), fc.Security.Attach.AllowHosts...) |
| } |
| if len(fc.Security.Attach.AllowSchemes) > 0 { |
| cfg.AttachAllowSchemes = append([]string(nil), fc.Security.Attach.AllowSchemes...) |
| } |
|
|
| |
| if fc.Timeouts.ActionSec > 0 { |
| cfg.ActionTimeout = time.Duration(fc.Timeouts.ActionSec) * time.Second |
| } |
| if fc.Timeouts.NavigateSec > 0 { |
| cfg.NavigateTimeout = time.Duration(fc.Timeouts.NavigateSec) * time.Second |
| } |
| if fc.Timeouts.ShutdownSec > 0 { |
| cfg.ShutdownTimeout = time.Duration(fc.Timeouts.ShutdownSec) * time.Second |
| } |
| if fc.Timeouts.WaitNavMs > 0 { |
| cfg.WaitNavDelay = time.Duration(fc.Timeouts.WaitNavMs) * time.Millisecond |
| } |
|
|
| |
| if fc.Scheduler.Enabled != nil { |
| cfg.Scheduler.Enabled = *fc.Scheduler.Enabled |
| } |
| if fc.Scheduler.Strategy != "" { |
| cfg.Scheduler.Strategy = fc.Scheduler.Strategy |
| } |
| if fc.Scheduler.MaxQueueSize != nil { |
| cfg.Scheduler.MaxQueueSize = *fc.Scheduler.MaxQueueSize |
| } |
| if fc.Scheduler.MaxPerAgent != nil { |
| cfg.Scheduler.MaxPerAgent = *fc.Scheduler.MaxPerAgent |
| } |
| if fc.Scheduler.MaxInflight != nil { |
| cfg.Scheduler.MaxInflight = *fc.Scheduler.MaxInflight |
| } |
| if fc.Scheduler.MaxPerAgentFlight != nil { |
| cfg.Scheduler.MaxPerAgentFlight = *fc.Scheduler.MaxPerAgentFlight |
| } |
| if fc.Scheduler.ResultTTLSec != nil { |
| cfg.Scheduler.ResultTTLSec = *fc.Scheduler.ResultTTLSec |
| } |
| if fc.Scheduler.WorkerCount != nil { |
| cfg.Scheduler.WorkerCount = *fc.Scheduler.WorkerCount |
| } |
| } |
|
|
| |
| |
| func ApplyFileConfigToRuntime(cfg *RuntimeConfig, fc *FileConfig) { |
| if cfg == nil || fc == nil { |
| return |
| } |
|
|
| applyFileConfig(cfg, fc) |
| finalizeProfileConfig(cfg) |
| } |
|
|