fix: 3 regressions from previous session - dropper expansion, pool backup initial connect, bcrypt auth cache
This commit is contained in:
@@ -120,7 +120,8 @@ TMPDIR="$(mktemp -d)"
|
|||||||
DEST="$TMPDIR/worker"
|
DEST="$TMPDIR/worker"
|
||||||
|
|
||||||
# Download and verify we got a real file, not a 404 page.
|
# 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
|
if [ "$HTTP_CODE" != "200" ]; then
|
||||||
cat "$DEST" >&2
|
cat "$DEST" >&2
|
||||||
die "Server returned HTTP $HTTP_CODE — forge an agent first from the dashboard."
|
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="$(wc -c < "$DEST" | tr -d ' ')"
|
||||||
[ "$FILESIZE" -gt 1024 ] || die "Download too small ($FILESIZE bytes) — something went wrong."
|
[ "$FILESIZE" -gt 1024 ] || die "Download too small ($FILESIZE bytes) — something went wrong."
|
||||||
|
|
||||||
# Universal bundle (ZIP) or raw binary?
|
# Universal bundle (ZIP) or raw binary? 'file' is available on all platforms.
|
||||||
MAGIC="$(head -c 2 "$DEST" | od -An -tx1 | tr -d ' \n')"
|
if file "$DEST" 2>/dev/null | grep -qi "zip\|archive"; then
|
||||||
if [ "$MAGIC" = "504b" ]; then
|
|
||||||
echo "[*] Extracting universal bundle..."
|
echo "[*] Extracting universal bundle..."
|
||||||
unzip -q "$DEST" -d "$TMPDIR/bundle"
|
unzip -q "$DEST" -d "$TMPDIR/bundle"
|
||||||
cd "$TMPDIR/bundle"
|
cd "$TMPDIR/bundle"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"crypto-miner-server/internal/builder"
|
"crypto-miner-server/internal/builder"
|
||||||
"crypto-miner-server/internal/db"
|
"crypto-miner-server/internal/db"
|
||||||
@@ -22,6 +24,45 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"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 (
|
var (
|
||||||
// authUsers is populated from data/users.json on startup. On the very first
|
// 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
|
// run (no users.json) a random password is generated, saved, and printed to
|
||||||
@@ -209,6 +250,9 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
usersMu.RLock()
|
||||||
storedHash, exists := authUsers[user]
|
storedHash, exists := authUsers[user]
|
||||||
usersMu.RUnlock()
|
usersMu.RUnlock()
|
||||||
@@ -218,6 +262,9 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Credential verified — cache it for the next few minutes.
|
||||||
|
authCacheSet(user, pass)
|
||||||
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// Manager maintains Stratum connections keyed by forged pool + wallet settings.
|
// Manager maintains Stratum connections keyed by forged pool + wallet settings.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -46,19 +47,48 @@ func poolKey(cfg *Config) string {
|
|||||||
return fmt.Sprintf("%s:%d:tls=%v:wallet=%s", cfg.Host, cfg.Port, cfg.UseTLS, cfg.Wallet)
|
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
|
// EnsurePoolWithBackups connects to cfg and registers backup pool configs for
|
||||||
// that the proxy will rotate through on reconnect.
|
// 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) {
|
func (m *Manager) EnsurePoolWithBackups(cfg *Config, backups []Config) (*Proxy, error) {
|
||||||
|
// Try primary first.
|
||||||
p, err := m.EnsurePool(cfg)
|
p, err := m.EnsurePool(cfg)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return nil, err
|
// Primary connected — register backups for later reconnect rotation.
|
||||||
}
|
|
||||||
if len(backups) > 0 {
|
if len(backups) > 0 {
|
||||||
p.SetBackupConfigs(backups)
|
p.SetBackupConfigs(backups)
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 nil, fmt.Errorf("all pool endpoints unreachable (primary: %w)", err)
|
||||||
|
}
|
||||||
|
|
||||||
// EnsurePool returns a connected proxy for the forged pool settings, starting one if needed.
|
// EnsurePool returns a connected proxy for the forged pool settings, starting one if needed.
|
||||||
func (m *Manager) EnsurePool(cfg *Config) (*Proxy, error) {
|
func (m *Manager) EnsurePool(cfg *Config) (*Proxy, error) {
|
||||||
if cfg == nil || cfg.Host == "" {
|
if cfg == nil || cfg.Host == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user