Skip to content

Instantly share code, notes, and snippets.

@nfd9001
Created March 12, 2026 05:49
Show Gist options
  • Select an option

  • Save nfd9001/d3f68e588fe33be5651fb813b7a898a8 to your computer and use it in GitHub Desktop.

Select an option

Save nfd9001/d3f68e588fe33be5651fb813b7a898a8 to your computer and use it in GitHub Desktop.
marathon arg cnc server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BSTX // PATH SUBMISSION TERMINAL</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=VT323&display=swap');
:root {
--lime: #00ff41;
--lime-dim: #00c032;
--lime-dark: #003d10;
--lime-glow: rgba(0,255,65,0.15);
--bg: #050a05;
--panel: #080f08;
--border: #1a3d1a;
--text: #00ff41;
--text-dim: #337a33;
--red: #ff3030;
--amber: #ffaa00;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Share Tech Mono', monospace;
font-size: 13px;
min-height: 100vh;
overflow-x: hidden;
cursor: crosshair;
}
/* Scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.08) 2px,
rgba(0,0,0,0.08) 4px
);
pointer-events: none;
z-index: 1000;
}
/* CRT flicker */
@keyframes flicker {
0%,100% { opacity: 1; }
92% { opacity: 1; }
93% { opacity: 0.85; }
94% { opacity: 1; }
96% { opacity: 0.9; }
97% { opacity: 1; }
}
body { animation: flicker 8s infinite; }
@keyframes blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } }
@keyndef pulse-border {
0%,100% { box-shadow: 0 0 4px var(--lime-glow), inset 0 0 4px var(--lime-glow); }
50% { box-shadow: 0 0 12px rgba(0,255,65,0.3), inset 0 0 8px rgba(0,255,65,0.1); }
}
@keyframes pulse-border {
0%,100% { box-shadow: 0 0 4px var(--lime-glow), inset 0 0 4px var(--lime-glow); }
50% { box-shadow: 0 0 12px rgba(0,255,65,0.3), inset 0 0 8px rgba(0,255,65,0.1); }
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Layout */
.header {
border-bottom: 1px solid var(--border);
padding: 12px 20px;
display: flex;
align-items: baseline;
gap: 16px;
}
.header-title {
font-family: 'VT323', monospace;
font-size: 28px;
letter-spacing: 4px;
color: var(--lime);
text-shadow: 0 0 20px rgba(0,255,65,0.6);
}
.header-sub {
color: var(--text-dim);
font-size: 11px;
letter-spacing: 2px;
}
.header-status {
margin-left: auto;
font-size: 11px;
color: var(--text-dim);
}
.header-status span {
color: var(--lime);
}
.main {
display: grid;
grid-template-columns: 1fr 320px;
gap: 0;
height: calc(100vh - 53px);
}
/* Left panel */
.left {
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 20px;
gap: 20px;
overflow-y: auto;
}
/* Right panel */
.right {
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Section labels */
.section-label {
font-size: 10px;
letter-spacing: 3px;
color: var(--text-dim);
text-transform: uppercase;
margin-bottom: 10px;
border-bottom: 1px solid var(--border);
padding-bottom: 6px;
}
/* Stats row */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.stat-box {
border: 1px solid var(--border);
padding: 10px 12px;
background: var(--panel);
animation: pulse-border 4s ease-in-out infinite;
}
.stat-box:nth-child(2) { animation-delay: 1s; }
.stat-box:nth-child(3) { animation-delay: 2s; }
.stat-box:nth-child(4) { animation-delay: 3s; }
.stat-label {
font-size: 9px;
letter-spacing: 2px;
color: var(--text-dim);
margin-bottom: 4px;
}
.stat-value {
font-family: 'VT323', monospace;
font-size: 32px;
line-height: 1;
color: var(--lime);
text-shadow: 0 0 10px rgba(0,255,65,0.4);
}
.stat-value.red { color: var(--red); text-shadow: 0 0 10px rgba(255,48,48,0.4); }
.stat-value.amber { color: var(--amber); text-shadow: 0 0 10px rgba(255,170,0,0.4); }
/* Solved banner */
.solved-banner {
display: none;
border: 1px solid var(--lime);
background: var(--lime-dark);
padding: 14px 18px;
font-family: 'VT323', monospace;
font-size: 22px;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(0,255,65,0.8);
animation: pulse-border 1s ease-in-out infinite;
}
.solved-banner.visible { display: block; }
.solved-path { font-size: 14px; color: var(--lime-dim); margin-top: 4px; font-family: 'Share Tech Mono', monospace; letter-spacing: 2px; }
/* Submit form */
.form-area {
display: flex;
flex-direction: column;
gap: 10px;
}
.path-input-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.path-input {
flex: 1;
background: var(--panel);
border: 1px solid var(--border);
color: var(--lime);
font-family: 'Share Tech Mono', monospace;
font-size: 15px;
padding: 10px 14px;
letter-spacing: 3px;
text-transform: uppercase;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.path-input::placeholder { color: var(--text-dim); letter-spacing: 2px; font-size: 12px; }
.path-input:focus {
border-color: var(--lime-dim);
box-shadow: 0 0 8px var(--lime-glow);
}
.path-input.error { border-color: var(--red); }
.path-input.ok { border-color: var(--lime); box-shadow: 0 0 8px var(--lime-glow); }
.btn {
background: transparent;
border: 1px solid var(--lime-dim);
color: var(--lime);
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
letter-spacing: 2px;
padding: 10px 18px;
cursor: pointer;
text-transform: uppercase;
transition: background 0.1s, box-shadow 0.1s;
white-space: nowrap;
}
.btn:hover {
background: var(--lime-dark);
box-shadow: 0 0 10px var(--lime-glow);
}
.btn:active { background: rgba(0,255,65,0.2); }
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn.bulk { border-color: var(--text-dim); font-size: 11px; align-self: flex-start; }
.bulk-input {
background: var(--panel);
border: 1px solid var(--border);
color: var(--lime);
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
padding: 10px 14px;
letter-spacing: 2px;
text-transform: uppercase;
outline: none;
resize: vertical;
min-height: 90px;
line-height: 1.6;
width: 100%;
transition: border-color 0.15s;
display: none;
}
.bulk-input.visible { display: block; }
.bulk-input:focus { border-color: var(--lime-dim); box-shadow: 0 0 8px var(--lime-glow); }
.bulk-input::placeholder { color: var(--text-dim); }
.msg {
font-size: 11px;
letter-spacing: 1px;
min-height: 16px;
transition: color 0.2s;
animation: slide-in 0.15s ease;
}
.msg.ok { color: var(--lime); }
.msg.err { color: var(--red); }
.msg.warn { color: var(--amber); }
/* Queue table */
.queue-wrap {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.queue-table-wrap {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
thead th {
font-size: 9px;
letter-spacing: 2px;
color: var(--text-dim);
text-align: left;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
background: var(--bg);
}
tbody tr {
border-bottom: 1px solid rgba(26,61,26,0.5);
animation: slide-in 0.2s ease;
}
tbody tr:hover { background: var(--lime-dark); }
td {
padding: 5px 10px;
letter-spacing: 1px;
}
.td-path { font-size: 13px; color: var(--lime); }
.td-status { font-size: 10px; letter-spacing: 2px; }
.s-pending { color: var(--text-dim); }
.s-claimed { color: var(--amber); }
.s-success { color: var(--lime); text-shadow: 0 0 8px rgba(0,255,65,0.6); }
.s-failed { color: var(--red); }
.td-worker { color: var(--text-dim); font-size: 10px; }
/* Right panel: log */
.log-header {
padding: 10px 14px;
font-size: 9px;
letter-spacing: 3px;
color: var(--text-dim);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.log-clear { font-size: 9px; cursor: pointer; color: var(--text-dim); background: none; border: none; font-family: inherit; letter-spacing: 1px; }
.log-clear:hover { color: var(--lime); }
.log-body {
flex: 1;
overflow-y: auto;
padding: 10px 14px;
display: flex;
flex-direction: column-reverse;
gap: 3px;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.log-entry {
font-size: 11px;
line-height: 1.5;
animation: slide-in 0.15s ease;
}
.log-ts { color: var(--text-dim); margin-right: 6px; }
.log-ok { color: var(--lime); }
.log-err { color: var(--red); }
.log-warn { color: var(--amber); }
.log-info { color: var(--text-dim); }
/* Spinner */
.spinner {
display: inline-block;
width: 10px; height: 10px;
border: 1px solid var(--text-dim);
border-top-color: var(--lime);
border-radius: 50%;
animation: spin 0.6s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
/* Cursor blink */
.cursor::after {
content: '█';
animation: blink 1s step-end infinite;
color: var(--lime);
}
</style>
</head>
<body>
<header class="header">
<div class="header-title">BSTX // C&amp;C</div>
<div class="header-sub">PATH SUBMISSION TERMINAL</div>
<div class="header-status">SERVER: <span id="srv-status">CONNECTING</span> &nbsp;|&nbsp; QUEUE: <span id="srv-queue">—</span></div>
</header>
<div class="main">
<div class="left">
<!-- Stats -->
<div>
<div class="section-label">QUEUE STATUS</div>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-label">PENDING</div>
<div class="stat-value" id="stat-pending">—</div>
</div>
<div class="stat-box">
<div class="stat-label">CLAIMED</div>
<div class="stat-value amber" id="stat-claimed">—</div>
</div>
<div class="stat-box">
<div class="stat-label">FAILED</div>
<div class="stat-value red" id="stat-failed">—</div>
</div>
<div class="stat-box">
<div class="stat-label">SUCCESS</div>
<div class="stat-value" id="stat-success">—</div>
</div>
</div>
</div>
<!-- Solved banner -->
<div class="solved-banner" id="solved-banner">
★ MAZE SOLVED
<div class="solved-path" id="solved-path"></div>
</div>
<!-- Submit -->
<div class="form-area">
<div class="section-label">SUBMIT PATH</div>
<div class="path-input-row">
<input class="path-input cursor" id="path-input" type="text"
placeholder="e.g. UULDRR..." maxlength="64" autocomplete="off" spellcheck="false">
<button class="btn" id="submit-btn" onclick="submitSingle()">SUBMIT</button>
</div>
<div class="msg" id="msg"></div>
<button class="btn bulk" onclick="toggleBulk()">[ BULK SUBMIT ]</button>
<textarea class="bulk-input" id="bulk-input"
placeholder="One path per line:&#10;UULDRR&#10;RRULLD&#10;LLUURR"></textarea>
<div style="display:flex;gap:8px;align-items:center" id="bulk-row" style="display:none">
<button class="btn" id="bulk-btn" onclick="submitBulk()" style="display:none">SUBMIT ALL</button>
</div>
</div>
<!-- Queue table -->
<div class="queue-wrap">
<div class="section-label" style="display:flex;justify-content:space-between">
<span>PATH QUEUE</span>
<span style="color:var(--text-dim);font-size:9px;cursor:pointer" onclick="refreshAll()">↻ REFRESH</span>
</div>
<div class="queue-table-wrap">
<table>
<thead>
<tr>
<th>PATH</th>
<th>STATUS</th>
<th>WORKER</th>
</tr>
</thead>
<tbody id="queue-tbody">
<tr><td colspan="3" style="color:var(--text-dim);padding:12px 10px">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Right: log -->
<div class="right">
<div class="log-header">
<span>ACTIVITY LOG</span>
<button class="log-clear" onclick="clearLog()">CLEAR</button>
</div>
<div class="log-body" id="log-body"></div>
</div>
</div>
<script>
const CNC = ''; // same origin — server serves this file on port 8080
// ── Logging ────────────────────────────────────────────────────────────────
function log(msg, type='info') {
const body = document.getElementById('log-body');
const ts = new Date().toTimeString().slice(0,8);
const el = document.createElement('div');
el.className = `log-entry log-${type}`;
el.innerHTML = `<span class="log-ts">${ts}</span>${msg}`;
body.prepend(el);
if (body.children.length > 200) body.lastChild.remove();
}
function clearLog() { document.getElementById('log-body').innerHTML = ''; }
// ── Fetch helpers ──────────────────────────────────────────────────────────
async function apiFetch(path, opts={}) {
const r = await fetch(CNC + path, opts);
return r.json();
}
// ── Status polling ─────────────────────────────────────────────────────────
async function refreshStatus() {
try {
const data = await apiFetch('/status');
const s = data.stats || {};
document.getElementById('stat-pending').textContent = s.pending ?? '—';
document.getElementById('stat-claimed').textContent = s.claimed ?? '—';
document.getElementById('stat-failed').textContent = s.failed ?? '—';
document.getElementById('stat-success').textContent = s.success ?? '—';
document.getElementById('srv-status').textContent = 'ONLINE';
document.getElementById('srv-queue').textContent =
((s.pending||0) + (s.claimed||0)) + ' / ' + (data.queue_cap || '?');
if (data.solved) {
const banner = document.getElementById('solved-banner');
banner.classList.add('visible');
document.getElementById('solved-path').textContent = '↳ ' + data.winning_path;
}
} catch(e) {
document.getElementById('srv-status').textContent = 'OFFLINE';
}
}
async function refreshQueue() {
try {
const data = await apiFetch('/queue');
const tbody = document.getElementById('queue-tbody');
if (!data.paths || data.paths.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" style="color:var(--text-dim);padding:12px 10px">Queue empty.</td></tr>';
return;
}
// Sort: pending first, then claimed, then failed, then success
const order = { pending:0, claimed:1, failed:2, success:3 };
const sorted = [...data.paths].sort((a,b) => (order[a.status]||9)-(order[b.status]||9));
tbody.innerHTML = sorted.map(p => `
<tr>
<td class="td-path">${p.path}</td>
<td class="td-status s-${p.status}">${p.status.toUpperCase()}</td>
<td class="td-worker">${p.worker || ''}</td>
</tr>`).join('');
} catch(e) {}
}
async function refreshAll() { await Promise.all([refreshStatus(), refreshQueue()]); }
// ── Input validation ───────────────────────────────────────────────────────
const VALID = /^[UDLRudlr]+$/;
function setMsg(text, type='ok') {
const el = document.getElementById('msg');
el.textContent = text;
el.className = 'msg ' + type;
}
document.getElementById('path-input').addEventListener('input', function() {
const v = this.value.trim().toUpperCase();
this.value = v;
if (!v) { this.className = 'path-input cursor'; setMsg(''); return; }
if (VALID.test(v)) {
this.className = 'path-input cursor ok';
setMsg(`${v.length} step${v.length!==1?'s':''} — looks good`, 'ok');
} else {
this.className = 'path-input cursor error';
setMsg('Invalid characters — use U D L R only', 'err');
}
});
document.getElementById('path-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitSingle();
});
// ── Single submit ──────────────────────────────────────────────────────────
async function submitSingle() {
const input = document.getElementById('path-input');
const path = input.value.trim().toUpperCase();
if (!path) { setMsg('Enter a path first', 'warn'); return; }
if (!VALID.test(path)) { setMsg('Invalid characters', 'err'); return; }
const btn = document.getElementById('submit-btn');
btn.disabled = true;
setMsg('Submitting...', 'info');
try {
const data = await apiFetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
if (data.ok) {
log(`Submitted: <strong>${path}</strong>`, 'ok');
setMsg(`✓ Queued (depth: ${data.queue_depth})`, 'ok');
input.value = '';
input.className = 'path-input cursor';
refreshAll();
} else if (data.error === 'Path already in queue') {
log(`Dupe: ${path} already queued [${data.status}]`, 'warn');
setMsg(`Already in queue (${data.status})`, 'warn');
} else {
log(`Error submitting ${path}: ${data.error}`, 'err');
setMsg(data.error || 'Error', 'err');
}
} catch(e) {
setMsg('Server unreachable', 'err');
log('Server unreachable', 'err');
}
btn.disabled = false;
}
// ── Bulk submit ────────────────────────────────────────────────────────────
let bulkVisible = false;
function toggleBulk() {
bulkVisible = !bulkVisible;
document.getElementById('bulk-input').classList.toggle('visible', bulkVisible);
document.getElementById('bulk-btn').style.display = bulkVisible ? 'inline-block' : 'none';
}
async function submitBulk() {
const raw = document.getElementById('bulk-input').value;
const paths = raw.split('\n')
.map(l => l.trim().toUpperCase())
.filter(l => l.length > 0);
if (paths.length === 0) { setMsg('No paths entered', 'warn'); return; }
const btn = document.getElementById('bulk-btn');
btn.disabled = true;
setMsg(`Submitting ${paths.length} paths...`, 'info');
try {
const data = await apiFetch('/submit_bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths })
});
const msg = `+${data.added} queued, ${data.dupes} dupes, ${data.invalid} invalid`;
log(`Bulk: ${msg}`, data.added > 0 ? 'ok' : 'warn');
setMsg(msg, data.added > 0 ? 'ok' : 'warn');
if (data.added > 0) {
document.getElementById('bulk-input').value = '';
refreshAll();
}
} catch(e) {
setMsg('Server unreachable', 'err');
}
btn.disabled = false;
}
// ── Init ───────────────────────────────────────────────────────────────────
log('Terminal initialized', 'info');
log('Connecting to C&C server...', 'info');
refreshAll().then(() => log('Server contact established', 'ok'));
setInterval(refreshAll, 5000);
</script>
</body>
</html>
// BSTX Maze C&C Server
// Usage: node server.js
// Workers poll /claim, report back to /complete or /fail
// Community can submit candidates via /submit
const http = require("http");
const url = require("url");
const fs = require("fs");
const path = require("path");
// ── Config ────────────────────────────────────────────────────────────────────
const PORT = 3000;
const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute window
const RATE_LIMIT_MAX = 5; // max submissions per IP per window
const QUEUE_CAP = 500; // max pending+unclaimed paths
const CLAIM_TIMEOUT_MS = 5 * 60_000; // reclaim after 5 min silence
const PATH_REGEX = /^[UDLRuldrnesw1234567890]{1,64}$/i; // valid path chars
// ── State ─────────────────────────────────────────────────────────────────────
// status: "pending" | "claimed" | "success" | "failed"
const paths = new Map(); // path_string -> { status, claimedBy, claimedAt, result, submittedAt }
const rateLimits = new Map(); // ip -> [timestamp, ...]
let solved = null; // set when a success is reported
// ── Helpers ───────────────────────────────────────────────────────────────────
function now() { return Date.now(); }
function json(res, status, obj) {
const body = JSON.stringify(obj, null, 2);
res.writeHead(status, {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
});
res.end(body);
}
function checkRateLimit(ip) {
const windowStart = now() - RATE_LIMIT_WINDOW_MS;
const hits = (rateLimits.get(ip) || []).filter(t => t > windowStart);
hits.push(now());
rateLimits.set(ip, hits);
return hits.length <= RATE_LIMIT_MAX;
}
function reclaimStale() {
const cutoff = now() - CLAIM_TIMEOUT_MS;
let reclaimed = 0;
for (const [path, entry] of paths) {
if (entry.status === "claimed" && entry.claimedAt < cutoff) {
entry.status = "pending";
entry.claimedBy = null;
entry.claimedAt = null;
reclaimed++;
}
}
if (reclaimed > 0) console.log(`[reclaim] ${reclaimed} stale claims returned to queue`);
}
function getStats() {
const counts = { pending: 0, claimed: 0, success: 0, failed: 0 };
for (const { status } of paths.values()) counts[status]++;
return counts;
}
function pendingCount() {
let n = 0;
for (const { status } of paths.values()) if (status === "pending") n++;
return n;
}
function totalQueued() {
let n = 0;
for (const { status } of paths.values()) if (status === "pending" || status === "claimed") n++;
return n;
}
// ── Route handlers ────────────────────────────────────────────────────────────
// GET /status — dashboard overview
function handleStatus(req, res) {
reclaimStale();
const stats = getStats();
const recent_failures = [...paths.entries()]
.filter(([, e]) => e.status === "failed")
.slice(-10)
.map(([p, e]) => ({ path: p, result: e.result }));
json(res, 200, {
solved,
stats,
queue_cap: QUEUE_CAP,
recent_failures,
...(solved ? { winning_path: solved } : {}),
});
}
// POST /submit — community submits a candidate path
// Body: { "path": "LLURRD" }
function handleSubmit(req, res, ip, body) {
if (!checkRateLimit(ip)) {
return json(res, 429, { error: "Rate limit exceeded", retry_after_ms: RATE_LIMIT_WINDOW_MS });
}
let parsed;
try { parsed = JSON.parse(body); }
catch { return json(res, 400, { error: "Invalid JSON" }); }
const path = (parsed.path || "").trim().toUpperCase();
if (!PATH_REGEX.test(path)) {
return json(res, 400, { error: "Invalid path format. Use direction chars e.g. ULDRULDR" });
}
if (paths.has(path)) {
return json(res, 409, { error: "Path already in queue", status: paths.get(path).status });
}
if (totalQueued() >= QUEUE_CAP) {
return json(res, 503, { error: "Queue full", queue_cap: QUEUE_CAP });
}
paths.set(path, { status: "pending", claimedBy: null, claimedAt: null, result: null, submittedAt: now() });
console.log(`[submit] ${ip} -> ${path} (queue: ${totalQueued()})`);
json(res, 201, { ok: true, path, queue_depth: totalQueued() });
}
// POST /submit_bulk — submit multiple paths at once (for seeding from analysis)
// Body: { "paths": ["LLUR", "RRDU", ...] }
function handleSubmitBulk(req, res, ip, body) {
if (!checkRateLimit(ip)) {
return json(res, 429, { error: "Rate limit exceeded" });
}
let parsed;
try { parsed = JSON.parse(body); }
catch { return json(res, 400, { error: "Invalid JSON" }); }
const incoming = (parsed.paths || []);
if (!Array.isArray(incoming) || incoming.length > 200) {
return json(res, 400, { error: "paths must be an array of up to 200 strings" });
}
let added = 0, dupes = 0, invalid = 0, capped = 0;
for (const raw of incoming) {
const path = (raw || "").trim().toUpperCase();
if (!PATH_REGEX.test(path)) { invalid++; continue; }
if (paths.has(path)) { dupes++; continue; }
if (totalQueued() >= QUEUE_CAP) { capped++; continue; }
paths.set(path, { status: "pending", claimedBy: null, claimedAt: null, result: null, submittedAt: now() });
added++;
}
console.log(`[bulk] ${ip}: +${added} added, ${dupes} dupes, ${invalid} invalid, ${capped} capped`);
json(res, 200, { ok: true, added, dupes, invalid, capped, queue_depth: totalQueued() });
}
// GET /claim?worker=ID — worker claims the next pending path
function handleClaim(req, res, query) {
reclaimStale();
if (solved) {
return json(res, 200, { solved: true, winning_path: solved });
}
const worker = query.worker || "anonymous";
for (const [path, entry] of paths) {
if (entry.status === "pending") {
entry.status = "claimed";
entry.claimedBy = worker;
entry.claimedAt = now();
console.log(`[claim] ${worker} -> ${path}`);
return json(res, 200, { path, worker, claimed_at: entry.claimedAt });
}
}
json(res, 204, { message: "No paths available", stats: getStats() });
}
// POST /complete — worker reports result
// Body: { "path": "LLURRD", "success": true/false, "result": "optional notes" }
function handleComplete(req, res, body) {
let parsed;
try { parsed = JSON.parse(body); }
catch { return json(res, 400, { error: "Invalid JSON" }); }
const { path, success, result } = parsed;
const entry = paths.get((path || "").toUpperCase());
if (!entry) return json(res, 404, { error: "Unknown path" });
entry.status = success ? "success" : "failed";
entry.result = result || null;
entry.completedAt = now();
if (success && !solved) {
solved = path.toUpperCase();
console.log(`[SOLVED] Winning path: ${solved}`);
}
console.log(`[complete] ${path} -> ${success ? "SUCCESS" : "failed"}`);
json(res, 200, { ok: true, path, status: entry.status });
}
// GET /queue — list all paths and their statuses (for debugging)
function handleQueue(res) {
const all = [...paths.entries()].map(([p, e]) => ({
path: p,
status: e.status,
worker: e.claimedBy,
submitted: e.submittedAt,
}));
json(res, 200, { total: all.length, paths: all });
}
// ── Request router ────────────────────────────────────────────────────────────
function readBody(req) {
return new Promise((resolve) => {
let body = "";
req.on("data", chunk => { body += chunk; if (body.length > 10000) body = body.slice(0, 10000); });
req.on("end", () => resolve(body));
});
}
const server = http.createServer(async (req, res) => {
const parsed = url.parse(req.url, true);
const path = parsed.pathname;
const query = parsed.query;
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "unknown";
const method = req.method.toUpperCase();
// CORS preflight
if (method === "OPTIONS") {
res.writeHead(204, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,POST", "Access-Control-Allow-Headers": "Content-Type" });
return res.end();
}
try {
if (path === "/" && method === "GET") {
const fs = require("fs"), fsp = require("path");
return fs.readFile(fsp.join(__dirname, "index.html"), (err, data) => {
if (err) { res.writeHead(404); return res.end("index.html not found"); }
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
});
}
if (path === "/status" && method === "GET") return handleStatus(req, res);
if (path === "/claim" && method === "GET") return handleClaim(req, res, query);
if (path === "/queue" && method === "GET") return handleQueue(res);
if (method === "POST") {
const body = await readBody(req);
if (path === "/submit") return handleSubmit(req, res, ip, body);
if (path === "/submit_bulk") return handleSubmitBulk(req, res, ip, body);
if (path === "/complete") return handleComplete(req, res, body);
}
json(res, 404, { error: "Not found", endpoints: ["/status", "/claim", "/complete", "/submit", "/submit_bulk", "/queue"] });
} catch (err) {
console.error(err);
json(res, 500, { error: "Internal server error" });
}
});
server.listen(PORT, () => {
console.log(`BSTX C&C server running on http://localhost:${PORT}`);
console.log(`Endpoints:
GET /status — overview + stats
GET /claim — worker claims next path (?worker=myname)
GET /queue — full path list
POST /submit — submit a candidate { "path": "LLURRD" }
POST /submit_bulk — submit many candidates { "paths": ["LLURRD", ...] }
POST /complete — report result { "path": "LLURRD", "success": true, "result": "notes" }`);
});
// Periodic stale reclaim
setInterval(reclaimStale, 60_000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment