Created
March 12, 2026 05:49
-
-
Save nfd9001/d3f68e588fe33be5651fb813b7a898a8 to your computer and use it in GitHub Desktop.
marathon arg cnc server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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&C</div> | |
| <div class="header-sub">PATH SUBMISSION TERMINAL</div> | |
| <div class="header-status">SERVER: <span id="srv-status">CONNECTING</span> | 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: UULDRR RRULLD 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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