fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes

WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
This commit is contained in:
AetherForge
2026-06-04 20:41:44 -07:00
parent 6bfce5d5ab
commit 8466c7aa9b
101 changed files with 3369 additions and 1054 deletions

View File

@@ -15,6 +15,7 @@ import (
"strings"
"sync"
"time"
"unicode"
"crypto-miner-server/internal/builder"
"crypto-miner-server/internal/db"
@@ -342,6 +343,18 @@ func reconcileLoginSidecar(dataDir, sidecarPath string, users map[string]string)
}
// generateRandomPassword returns an 8-character password (4 random bytes as hex).
func validateDashboardUsername(username string) error {
if len(username) < 3 || len(username) > 32 {
return fmt.Errorf("username must be 3-32 characters")
}
for _, c := range username {
if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '-' {
return fmt.Errorf("username contains invalid characters")
}
}
return nil
}
func generateRandomPassword() string {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
@@ -371,12 +384,22 @@ func saveUser(username, password string) error {
return err
}
usersMu.Unlock()
authSessionCacheMu.Lock()
authSessionCache = map[string]time.Time{}
authSessionCacheMu.Unlock()
if err := upsertLoginSidecar(dataDir, username, password); err != nil {
log.Printf("[Auth] WARNING: could not update login-credentials.json: %v", err)
}
return nil
}
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
// must not trigger that — only bare browser navigations without these headers should.
func isSPAAuthRequest(r *http.Request) bool {
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
}
func basicAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
@@ -438,7 +461,9 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
if !isSPAAuthRequest(r) {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
@@ -451,7 +476,9 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
usersMu.RUnlock()
if !exists || !checkPassword(storedHash, pass) {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
if !isSPAAuthRequest(r) {
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
@@ -463,7 +490,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
})
}
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, dropperHandler *DropperHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, serverVersion ...string) http.Handler {
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, dropperHandler *DropperHandler, pathForgeHandler *builder.PathForgeHandler, pathTracerHandler *PathTracerHandler, webRoot string, dataDir string, publicURLOverride func() string, listenPort int, serverVersion ...string) http.Handler {
ensureUsersLoaded(dataDir)
version := "AetherForge"
@@ -482,7 +509,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
// X-Fleet-Secret is required by agent-facing endpoints; include it so
// browser-based callers (dev tools, custom dashboards) are not blocked
// by CORS preflight when sending that header.
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Fleet-Secret"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Fleet-Secret", "X-AetherForge-Client"},
AllowCredentials: false,
}))
@@ -492,13 +519,25 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
h := NewHandler(database)
r.Get("/health", h.HealthCheck)
r.Post("/auth/ws-ticket", func(w http.ResponseWriter, req *http.Request) {
user := AuthUsername(req)
if user == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ticket := issueWSTicket(user)
writeJSON(w, map[string]interface{}{
"ticket": ticket,
"expires_in": int(wsTicketTTL.Seconds()),
})
})
r.Get("/server/ready", h.ServerReady)
r.Get("/server/info", func(w http.ResponseWriter, r *http.Request) {
override := ""
if publicURLOverride != nil {
override = publicURLOverride()
}
GetServerInfo(w, r, override)
GetServerInfo(w, r, override, listenPort)
})
// Dashboard
@@ -583,7 +622,13 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
hint := newSecret
if n := len(hint); n > 8 {
hint = hint[:8] + "..."
} else if n > 0 {
hint += "..."
}
writeJSON(w, map[string]interface{}{"ok": true, "hint": hint})
})
// User Management
@@ -592,8 +637,24 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil || payload.Username == "" || payload.Password == "" {
http.Error(w, "Invalid username or password", http.StatusBadRequest)
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
payload.Username = strings.TrimSpace(payload.Username)
if err := validateDashboardUsername(payload.Username); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(payload.Password) < 4 || len(payload.Password) > 128 {
http.Error(w, "password must be 4-128 characters", http.StatusBadRequest)
return
}
usersMu.RLock()
_, exists := authUsers[payload.Username]
usersMu.RUnlock()
if exists {
http.Error(w, "username already exists", http.StatusConflict)
return
}
if err := saveUser(payload.Username, payload.Password); err != nil {