PolHunter shell: default-deny gates (OUTBOUND, SIGNUPS, CURTAIN), health, placeholder page
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// PolHunter: gamified visits across the network, paid in POL.
|
||||
//
|
||||
// This is the shell the hunt engine will grow inside. What it does today is refuse to do anything
|
||||
// dangerous by default, because the day before it was built a test area in this same network
|
||||
// emailed 213 real people. Three gates, all default-deny, all read from the environment:
|
||||
//
|
||||
// OUTBOUND=on nothing can send mail (or anything else outward) without it. There is no
|
||||
// mailer in this app yet; when one arrives it must check outbound() first.
|
||||
// SIGNUPS=open the sign-in door stays shut until this is set. Hunters will sign in with
|
||||
// their InstantAdPay account; that bridge does not exist yet either.
|
||||
// CURTAIN=<secret> while set, every request gets a contentless "Coming soon" page unless the
|
||||
// browser has visited ?k=<secret> once. Lift it by unsetting the variable.
|
||||
//
|
||||
// A missing variable means silence, never delivery.
|
||||
'use strict';
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public');
|
||||
const CURTAIN = String(process.env.CURTAIN || '').trim();
|
||||
const outbound = () => process.env.OUTBOUND === 'on';
|
||||
const signupsOpen = () => process.env.SIGNUPS === 'open';
|
||||
|
||||
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (e) {}
|
||||
|
||||
const CURTAIN_PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow">
|
||||
<title>Coming soon</title><style>
|
||||
*{margin:0;padding:0;box-sizing:border-box} html,body{height:100%}
|
||||
body{display:flex;align-items:center;justify-content:center;padding:24px;background:#0d1117;color:#e6edf3;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif}
|
||||
.card{max-width:420px;text-align:center} h1{font-size:clamp(28px,7vw,44px);font-weight:700;letter-spacing:-.5px;margin-bottom:14px} p{color:#8b949e}
|
||||
</style></head><body><div class="card"><h1>Coming soon</h1><p>This site is still being built.</p></div></body></html>`;
|
||||
|
||||
function curtained(req, res, u) {
|
||||
if (!CURTAIN) return false;
|
||||
if (u.searchParams.get('k') === CURTAIN) {
|
||||
u.searchParams.delete('k');
|
||||
res.writeHead(302, {
|
||||
'Set-Cookie': 'ph.pass=' + encodeURIComponent(CURTAIN) + '; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax; Secure',
|
||||
Location: u.pathname + (u.searchParams.toString() ? '?' + u.searchParams : ''),
|
||||
'Cache-Control': 'no-store'
|
||||
});
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
const m = /(?:^|;\s*)ph\.pass=([^;]*)/.exec(req.headers.cookie || '');
|
||||
if (m && decodeURIComponent(m[1]) === CURTAIN) return false;
|
||||
res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'X-Robots-Tag': 'noindex, nofollow' });
|
||||
res.end(req.method === 'HEAD' ? '' : CURTAIN_PAGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.json': 'application/json' };
|
||||
function sendFile(res, file) {
|
||||
fs.readFile(file, (err, buf) => {
|
||||
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); }
|
||||
res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-store' });
|
||||
res.end(buf);
|
||||
});
|
||||
}
|
||||
const json = (res, code, body) => { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(body)); };
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const p = u.pathname;
|
||||
if (p === '/health') return json(res, 200, { ok: true, outbound: outbound(), signups: signupsOpen(), curtain: !!CURTAIN });
|
||||
if (curtained(req, res, u)) return; // nothing below runs for an uninvited visitor
|
||||
|
||||
if (p === '/api/config') return json(res, 200, { name: 'PolHunter', signupsOpen: signupsOpen(), outbound: outbound() });
|
||||
if (p.startsWith('/api/')) return json(res, 404, { error: 'Not built yet.' });
|
||||
|
||||
// static: the public dir, index for /
|
||||
const safe = path.normalize(p).replace(/^(\.\.[/\\])+/, '');
|
||||
const file = path.join(PUBLIC_DIR, safe === '/' || safe === '\\' ? 'index.html' : safe);
|
||||
if (!file.startsWith(PUBLIC_DIR)) { res.writeHead(400); return res.end(); }
|
||||
sendFile(res, file);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => console.log(`PolHunter on :${PORT} — outbound: ${outbound() ? 'ON' : 'OFF'} — sign-ups: ${signupsOpen() ? 'open' : 'CLOSED'} — curtain: ${CURTAIN ? 'up' : 'down'}`));
|
||||
Reference in New Issue
Block a user