Security: - Add HTTP Basic/Digest authentication middleware (enable via MINING_API_AUTH env) - Fix WebSocket origin check with proper URL parsing - Add max limit (10000) to remote log lines request - Improve CLI args validation with stricter patterns Networking: - Fix WebSocket double-close with sync.Once in PeerConnection - Add 10s dial timeout for WebSocket connections - Reset write deadline after failed sends - Fix handler race in Transport.OnMessage with RWMutex - Make EventHub.Stop() idempotent, buffer channels to prevent goroutine leaks Code Simplification: - Extract AtomicWriteFile helper to reduce duplication across 4 files - Remove redundant MinerTypeRegistry, use MinerFactory instead - Register simulated miner in MinerFactory - Remove dead portToString() code from manager.go Documentation: - Add Advanced API Authentication section to FUTURE_IDEAS.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
package mining
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"github.com/adrg/xdg"
|
|
)
|
|
|
|
// configMu protects concurrent access to config file operations
|
|
var configMu sync.RWMutex
|
|
|
|
// MinerAutostartConfig represents the configuration for a single miner's autostart settings.
|
|
type MinerAutostartConfig struct {
|
|
MinerType string `json:"minerType"`
|
|
Autostart bool `json:"autostart"`
|
|
Config *Config `json:"config,omitempty"` // Store the last used config
|
|
}
|
|
|
|
// DatabaseConfig holds configuration for SQLite database persistence.
|
|
type DatabaseConfig struct {
|
|
// Enabled determines if database persistence is active (default: true)
|
|
Enabled bool `json:"enabled"`
|
|
// RetentionDays is how long to keep historical data (default: 30)
|
|
RetentionDays int `json:"retentionDays,omitempty"`
|
|
}
|
|
|
|
// defaultDatabaseConfig returns the default database configuration.
|
|
func defaultDatabaseConfig() DatabaseConfig {
|
|
return DatabaseConfig{
|
|
Enabled: true,
|
|
RetentionDays: 30,
|
|
}
|
|
}
|
|
|
|
// MinersConfig represents the overall configuration for all miners, including autostart settings.
|
|
type MinersConfig struct {
|
|
Miners []MinerAutostartConfig `json:"miners"`
|
|
Database DatabaseConfig `json:"database"`
|
|
}
|
|
|
|
// getMinersConfigPath returns the path to the miners configuration file.
|
|
func getMinersConfigPath() (string, error) {
|
|
return xdg.ConfigFile("lethean-desktop/miners/config.json")
|
|
}
|
|
|
|
// LoadMinersConfig loads the miners configuration from the file system.
|
|
func LoadMinersConfig() (*MinersConfig, error) {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
|
|
configPath, err := getMinersConfigPath()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not determine miners config path: %w", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// Return empty config with defaults if file doesn't exist
|
|
return &MinersConfig{
|
|
Miners: []MinerAutostartConfig{},
|
|
Database: defaultDatabaseConfig(),
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to read miners config file: %w", err)
|
|
}
|
|
|
|
var cfg MinersConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal miners config: %w", err)
|
|
}
|
|
|
|
// Apply default database config if not set (for backwards compatibility)
|
|
if cfg.Database.RetentionDays == 0 {
|
|
cfg.Database = defaultDatabaseConfig()
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// SaveMinersConfig saves the miners configuration to the file system.
|
|
// Uses atomic write pattern: write to temp file, then rename.
|
|
func SaveMinersConfig(cfg *MinersConfig) error {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
|
|
configPath, err := getMinersConfigPath()
|
|
if err != nil {
|
|
return fmt.Errorf("could not determine miners config path: %w", err)
|
|
}
|
|
|
|
dir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal miners config: %w", err)
|
|
}
|
|
|
|
return AtomicWriteFile(configPath, data, 0600)
|
|
}
|
|
|
|
// UpdateMinersConfig atomically loads, modifies, and saves the miners config.
|
|
// This prevents race conditions in read-modify-write operations.
|
|
func UpdateMinersConfig(fn func(*MinersConfig) error) error {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
|
|
configPath, err := getMinersConfigPath()
|
|
if err != nil {
|
|
return fmt.Errorf("could not determine miners config path: %w", err)
|
|
}
|
|
|
|
// Load current config
|
|
var cfg MinersConfig
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
cfg = MinersConfig{
|
|
Miners: []MinerAutostartConfig{},
|
|
Database: defaultDatabaseConfig(),
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to read miners config file: %w", err)
|
|
}
|
|
} else {
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return fmt.Errorf("failed to unmarshal miners config: %w", err)
|
|
}
|
|
if cfg.Database.RetentionDays == 0 {
|
|
cfg.Database = defaultDatabaseConfig()
|
|
}
|
|
}
|
|
|
|
// Apply the modification
|
|
if err := fn(&cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save atomically
|
|
dir := filepath.Dir(configPath)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
|
|
newData, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal miners config: %w", err)
|
|
}
|
|
|
|
return AtomicWriteFile(configPath, newData, 0600)
|
|
}
|