diff --git a/server/internal/api/dropper_handler.go b/server/internal/api/dropper_handler.go index 1c7d1c1..b920479 100644 --- a/server/internal/api/dropper_handler.go +++ b/server/internal/api/dropper_handler.go @@ -120,7 +120,8 @@ TMPDIR="$(mktemp -d)" DEST="$TMPDIR/worker" # Download and verify we got a real file, not a 404 page. -HTTP_CODE="$(curl -sL -w '%%{http_code}' -o "$DEST" '%[1]s/get?os=$OS')" +# Note: double-quotes around URL so $OS is expanded by the shell. +HTTP_CODE="$(curl -sL -w "%%{http_code}" -o "$DEST" "%[1]s/get?os=$OS")" if [ "$HTTP_CODE" != "200" ]; then cat "$DEST" >&2 die "Server returned HTTP $HTTP_CODE — forge an agent first from the dashboard." @@ -129,9 +130,8 @@ fi FILESIZE="$(wc -c < "$DEST" | tr -d ' ')" [ "$FILESIZE" -gt 1024 ] || die "Download too small ($FILESIZE bytes) — something went wrong." -# Universal bundle (ZIP) or raw binary? -MAGIC="$(head -c 2 "$DEST" | od -An -tx1 | tr -d ' \n')" -if [ "$MAGIC" = "504b" ]; then +# Universal bundle (ZIP) or raw binary? 'file' is available on all platforms. +if file "$DEST" 2>/dev/null | grep -qi "zip\|archive"; then echo "[*] Extracting universal bundle..." unzip -q "$DEST" -d "$TMPDIR/bundle" cd "$TMPDIR/bundle" diff --git a/server/internal/api/router.go b/server/internal/api/router.go index f99ce90..e5a97db 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -2,6 +2,7 @@ package api import ( "crypto/rand" + "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" "sync" + "time" "crypto-miner-server/internal/builder" "crypto-miner-server/internal/db" @@ -22,6 +24,45 @@ import ( "golang.org/x/crypto/bcrypt" ) +// authSessionCache avoids running bcrypt on every API request. +// Key: SHA-256(user+":"+password) hex — value: expiry time. +// Entries are valid for authCacheTTL after the last successful login. +// Bcrypt only runs on cache miss or expiry. +var ( + authSessionCache = map[string]time.Time{} + authSessionCacheMu sync.Mutex + authCacheTTL = 5 * time.Minute +) + +func authCacheKey(user, pass string) string { + h := sha256.Sum256([]byte(user + ":" + pass)) + return hex.EncodeToString(h[:]) +} + +func authCacheHit(user, pass string) bool { + key := authCacheKey(user, pass) + authSessionCacheMu.Lock() + defer authSessionCacheMu.Unlock() + exp, ok := authSessionCache[key] + return ok && time.Now().Before(exp) +} + +func authCacheSet(user, pass string) { + key := authCacheKey(user, pass) + authSessionCacheMu.Lock() + authSessionCache[key] = time.Now().Add(authCacheTTL) + // Prune expired entries opportunistically. + if len(authSessionCache) > 512 { + now := time.Now() + for k, v := range authSessionCache { + if now.After(v) { + delete(authSessionCache, k) + } + } + } + authSessionCacheMu.Unlock() +} + var ( // authUsers is populated from data/users.json on startup. On the very first // run (no users.json) a random password is generated, saved, and printed to @@ -209,14 +250,20 @@ func basicAuthMiddleware(next http.Handler) http.Handler { return } - usersMu.RLock() - storedHash, exists := authUsers[user] - usersMu.RUnlock() + // Fast path — skip bcrypt if this credential pair was recently validated. + // bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy. + if !authCacheHit(user, pass) { + usersMu.RLock() + storedHash, exists := authUsers[user] + usersMu.RUnlock() - if !exists || !checkPassword(storedHash, pass) { - w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return + if !exists || !checkPassword(storedHash, pass) { + w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + // Credential verified — cache it for the next few minutes. + authCacheSet(user, pass) } next.ServeHTTP(w, r) diff --git a/server/internal/pool/manager.go b/server/internal/pool/manager.go index 471c299..30914b2 100644 --- a/server/internal/pool/manager.go +++ b/server/internal/pool/manager.go @@ -7,6 +7,7 @@ import ( "time" ) + // Manager maintains Stratum connections keyed by forged pool + wallet settings. type Manager struct { mu sync.RWMutex @@ -46,17 +47,46 @@ func poolKey(cfg *Config) string { return fmt.Sprintf("%s:%d:tls=%v:wallet=%s", cfg.Host, cfg.Port, cfg.UseTLS, cfg.Wallet) } -// EnsurePoolWithBackups is like EnsurePool but also registers backup configs -// that the proxy will rotate through on reconnect. +// EnsurePoolWithBackups connects to cfg and registers backup pool configs for +// automatic failover on reconnect. If the primary cfg is unreachable it tries +// each backup in order at initial connect time as well — restoring the behaviour +// that existed before the reconnect-rotation refactor. func (m *Manager) EnsurePoolWithBackups(cfg *Config, backups []Config) (*Proxy, error) { + // Try primary first. p, err := m.EnsurePool(cfg) - if err != nil { - return nil, err + if err == nil { + // Primary connected — register backups for later reconnect rotation. + if len(backups) > 0 { + p.SetBackupConfigs(backups) + } + return p, nil } - if len(backups) > 0 { - p.SetBackupConfigs(backups) + + // Primary failed — work through the backup list. + log.Printf("[PoolManager] Primary pool unreachable (%v), trying %d backup(s)…", err, len(backups)) + for i, bp := range backups { + if bp.Host == "" || bp.Port <= 0 { + continue + } + bpCopy := bp // local copy so we can take its address + p2, err2 := m.EnsurePool(&bpCopy) + if err2 == nil { + log.Printf("[PoolManager] Connected to backup pool #%d (%s:%d)", i+1, bp.Host, bp.Port) + // Register remaining backups (skip the one we just connected to). + remaining := make([]Config, 0, len(backups)) + remaining = append(remaining, *cfg) // original primary becomes a backup + for j, b := range backups { + if j != i { + remaining = append(remaining, b) + } + } + p2.SetBackupConfigs(remaining) + return p2, nil + } + log.Printf("[PoolManager] Backup pool #%d (%s:%d) also failed: %v", i+1, bp.Host, bp.Port, err2) } - return p, nil + + return nil, fmt.Errorf("all pool endpoints unreachable (primary: %w)", err) } // EnsurePool returns a connected proxy for the forged pool settings, starting one if needed.