Mining/pkg/mining/profile_manager.go
snider 4072bdaf0d fix: Address 16 security findings from parallel code review
Critical fixes (6):
- CRIT-001/002: Add safeKeyPrefix() to prevent panic on short public keys
- CRIT-003/004: Add sync.Once pattern for thread-safe singleton initialization
- CRIT-005: Harden console ANSI parser with length limits and stricter validation
- CRIT-006: Add client-side input validation for profile creation

High priority fixes (10):
- HIGH-001: Add secondary timeout in TTMiner to prevent goroutine leak
- HIGH-002: Verify atomic flag prevents timeout middleware race
- HIGH-004: Add LimitReader (100MB) to prevent decompression bombs
- HIGH-005: Add Lines parameter validation (max 10000) in worker
- HIGH-006: Add TLS 1.2+ config with secure cipher suites
- HIGH-007: Add pool URL format and wallet length validation
- HIGH-008: Add SIGHUP handling and force cleanup on Stop() failure
- HIGH-009: Add WebSocket message size limit and event type validation
- HIGH-010: Refactor to use takeUntil(destroy$) for observable cleanup
- HIGH-011: Add sanitizeErrorDetails() with debug mode control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 17:44:49 +00:00

164 lines
3.9 KiB
Go

package mining
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/adrg/xdg"
"github.com/google/uuid"
)
const profileConfigFileName = "mining_profiles.json"
// ProfileManager handles CRUD operations for MiningProfiles.
type ProfileManager struct {
mu sync.RWMutex
profiles map[string]*MiningProfile
configPath string
}
// NewProfileManager creates and initializes a new ProfileManager.
func NewProfileManager() (*ProfileManager, error) {
configPath, err := xdg.ConfigFile(filepath.Join("lethean-desktop", profileConfigFileName))
if err != nil {
return nil, fmt.Errorf("could not resolve config path: %w", err)
}
pm := &ProfileManager{
profiles: make(map[string]*MiningProfile),
configPath: configPath,
}
if err := pm.loadProfiles(); err != nil {
// If the file doesn't exist, that's fine, but any other error is a problem.
if !os.IsNotExist(err) {
return nil, fmt.Errorf("could not load profiles: %w", err)
}
}
return pm, nil
}
// loadProfiles reads the profiles from the JSON file into memory.
func (pm *ProfileManager) loadProfiles() error {
pm.mu.Lock()
defer pm.mu.Unlock()
data, err := os.ReadFile(pm.configPath)
if err != nil {
return err
}
var profiles []*MiningProfile
if err := json.Unmarshal(data, &profiles); err != nil {
return err
}
pm.profiles = make(map[string]*MiningProfile)
for _, p := range profiles {
pm.profiles[p.ID] = p
}
return nil
}
// saveProfiles writes the current profiles from memory to the JSON file.
// This is an internal method and assumes the caller holds the appropriate lock.
// Uses atomic write pattern: write to temp file, sync, then rename.
func (pm *ProfileManager) saveProfiles() error {
profileList := make([]*MiningProfile, 0, len(pm.profiles))
for _, p := range pm.profiles {
profileList = append(profileList, p)
}
data, err := json.MarshalIndent(profileList, "", " ")
if err != nil {
return err
}
return AtomicWriteFile(pm.configPath, data, 0600)
}
// CreateProfile adds a new profile and saves it.
func (pm *ProfileManager) CreateProfile(profile *MiningProfile) (*MiningProfile, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
profile.ID = uuid.New().String()
pm.profiles[profile.ID] = profile
if err := pm.saveProfiles(); err != nil {
// Rollback
delete(pm.profiles, profile.ID)
return nil, err
}
return profile, nil
}
// GetProfile retrieves a profile by its ID.
func (pm *ProfileManager) GetProfile(id string) (*MiningProfile, bool) {
pm.mu.RLock()
defer pm.mu.RUnlock()
profile, exists := pm.profiles[id]
return profile, exists
}
// GetAllProfiles returns a list of all profiles.
func (pm *ProfileManager) GetAllProfiles() []*MiningProfile {
pm.mu.RLock()
defer pm.mu.RUnlock()
profileList := make([]*MiningProfile, 0, len(pm.profiles))
for _, p := range pm.profiles {
profileList = append(profileList, p)
}
return profileList
}
// UpdateProfile modifies an existing profile.
func (pm *ProfileManager) UpdateProfile(profile *MiningProfile) error {
pm.mu.Lock()
defer pm.mu.Unlock()
oldProfile, exists := pm.profiles[profile.ID]
if !exists {
return fmt.Errorf("profile with ID %s not found", profile.ID)
}
// Update in-memory state
pm.profiles[profile.ID] = profile
// Save to disk - rollback if save fails
if err := pm.saveProfiles(); err != nil {
// Restore old profile on save failure
pm.profiles[profile.ID] = oldProfile
return fmt.Errorf("failed to save profile: %w", err)
}
return nil
}
// DeleteProfile removes a profile by its ID.
func (pm *ProfileManager) DeleteProfile(id string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
profile, exists := pm.profiles[id]
if !exists {
return fmt.Errorf("profile with ID %s not found", id)
}
delete(pm.profiles, id)
// Save to disk - rollback if save fails
if err := pm.saveProfiles(); err != nil {
// Restore profile on save failure
pm.profiles[id] = profile
return fmt.Errorf("failed to delete profile: %w", err)
}
return nil
}