// Zero-dependency static server + contact endpoint + admin area. Needs Node 18+. const http = require('http'), fs = require('fs'), path = require('path'), crypto = require('crypto'); const PORT = process.env.PORT || 8080; const PUBLIC = path.join(__dirname, 'public'); const STORE = path.join(__dirname, 'storage', 'messages.jsonl'); const ADMIN_SERVICES = path.join(__dirname, 'config', 'admin-services.json'); const WEBHOOK = process.env.WEBHOOK_URL || ''; // Discord webhook or ntfy topic URL const BEHIND_PROXY = process.env.TRUST_PROXY === '1'; // trust X-Forwarded-* headers const ADMIN_PW = process.env.ADMIN_PASSWORD || ''; const SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'); // set it to survive restarts if (!ADMIN_PW) console.warn('ADMIN_PASSWORD is not set: the admin area is disabled.'); const MIME = { '.html':'text/html; charset=utf-8', '.css':'text/css', '.js':'text/javascript', '.json':'application/json', '.svg':'image/svg+xml', '.png':'image/png', '.jpg':'image/jpeg', '.webp':'image/webp', '.ico':'image/x-icon', '.woff2':'font/woff2' }; // --- helpers const send = (res, code, body, type = 'application/json', extra = {}) => { res.writeHead(code, { 'Content-Type': type, 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', ...extra }); res.end(body); }; const json = (res, code, obj, extra = {}) => send(res, code, JSON.stringify(obj), 'application/json', { 'Cache-Control': 'no-store', ...extra }); const readJson = req => new Promise((ok, no) => { let r = ''; req.on('data', c => { r += c; if (r.length > 10000) { req.destroy(); no(new Error('too large')); } }); req.on('end', () => { try { ok(JSON.parse(r || '{}')); } catch { ok(null); } }); req.on('error', no); }); const clientIp = req => (BEHIND_PROXY && req.headers['x-forwarded-for']?.split(',')[0].trim()) || req.socket.remoteAddress; const recent = (map, ip, win) => { const n = Date.now(), a = (map.get(ip) || []).filter(t => n - t < win); map.set(ip, a); return a; }; const push = (map, ip) => map.get(ip).push(Date.now()); // --- sessions: signed, expiring cookie const sha = s => crypto.createHash('sha256').update(s).digest(); const sign = v => crypto.createHmac('sha256', SECRET).update(v).digest('hex'); const makeToken = () => { const exp = Date.now() + 7 * 864e5; return exp + '.' + sign(String(exp)); }; const validToken = t => { const [exp, sig] = (t || '').split('.'); if (!exp || !sig || Date.now() > +exp) return false; const good = sign(exp); return sig.length === good.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(good)); }; const cookie = (req, name) => (req.headers.cookie || '').split(';').map(c => c.trim()).find(c => c.startsWith(name + '='))?.slice(name.length + 1); const isAdmin = req => !!ADMIN_PW && validToken(cookie(req, 'nke_admin')); const setCookie = (req, val, age) => 'nke_admin=' + val + `; HttpOnly; SameSite=Strict; Path=/; Max-Age=${age}` + (BEHIND_PROXY && req.headers['x-forwarded-proto'] === 'https' ? '; Secure' : ''); // --- messages const readMsgs = () => { try { return fs.readFileSync(STORE, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)); } catch { return []; } }; const writeMsgs = list => fs.writeFileSync(STORE, list.map(m => JSON.stringify(m)).join('\n') + (list.length ? '\n' : '')); async function notify(m) { if (!WEBHOOK) return; const text = `New message from ${m.name} <${m.email}>\n\n${m.message}`; try { if (/discord(app)?\.com/.test(WEBHOOK)) await fetch(WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: text.slice(0, 1900) }) }); else await fetch(WEBHOOK, { method: 'POST', headers: { Title: 'nkelabs contact form' }, body: text }); } catch (e) { console.error('webhook failed:', e.message); } } const contactHits = new Map(), loginFails = new Map(); async function api(req, res, url) { const p = url.pathname, ip = clientIp(req); if (p === '/api/contact' && req.method === 'POST') { const d = await readJson(req); if (!d) return json(res, 400, { error: 'Invalid request.' }); if (d.website) return json(res, 200, { ok: true }); // honeypot if (recent(contactHits, ip, 36e5).length >= 5) return json(res, 429, { error: 'Too many messages. Try again in an hour.' }); const m = { name: String(d.name || '').trim().slice(0, 80), email: String(d.email || '').trim().slice(0, 120), message: String(d.message || '').trim().slice(0, 4000) }; if (!m.name || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(m.email) || m.message.length < 5) return json(res, 400, { error: 'Check your name, email and message.' }); push(contactHits, ip); fs.mkdirSync(path.dirname(STORE), { recursive: true }); fs.appendFileSync(STORE, JSON.stringify({ id: crypto.randomUUID(), ...m, ip, at: new Date().toISOString() }) + '\n'); notify(m); return json(res, 200, { ok: true }); } if (p === '/api/session' && req.method === 'GET') return json(res, 200, { admin: isAdmin(req), enabled: !!ADMIN_PW }); if (p === '/api/login' && req.method === 'POST') { if (!ADMIN_PW) return json(res, 503, { error: 'Admin is disabled. Set ADMIN_PASSWORD and restart.' }); if (recent(loginFails, ip, 9e5).length >= 5) return json(res, 429, { error: 'Too many attempts. Wait 15 minutes.' }); const d = await readJson(req); if (d && crypto.timingSafeEqual(sha(String(d.password || '')), sha(ADMIN_PW))) return json(res, 200, { ok: true }, { 'Set-Cookie': setCookie(req, makeToken(), 604800) }); push(loginFails, ip); return json(res, 401, { error: 'Wrong password.' }); } if (p === '/api/logout' && req.method === 'POST') return json(res, 200, { ok: true }, { 'Set-Cookie': setCookie(req, '', 0) }); if (p.startsWith('/api/admin/')) { if (!isAdmin(req)) return json(res, 401, { error: 'Not signed in.' }); if (p === '/api/admin/services' && req.method === 'GET') { try { return json(res, 200, JSON.parse(fs.readFileSync(ADMIN_SERVICES, 'utf8'))); } catch { return json(res, 200, []); } } if (p === '/api/admin/messages' && req.method === 'GET') return json(res, 200, readMsgs().reverse()); const del = p.match(/^\/api\/admin\/messages\/(.+)$/); if (del && req.method === 'DELETE') { const id = decodeURIComponent(del[1]); writeMsgs(readMsgs().filter(m => (m.id || m.at) !== id)); return json(res, 200, { ok: true }); } } return json(res, 404, { error: 'Not found.' }); } http.createServer(async (req, res) => { const url = new URL(req.url, 'http://x'); try { if (url.pathname.startsWith('/api/')) return await api(req, res, url); if (req.method !== 'GET' && req.method !== 'HEAD') return json(res, 405, { error: 'Method not allowed.' }); let file = path.join(PUBLIC, path.normalize(decodeURIComponent(url.pathname))); if (!file.startsWith(PUBLIC)) return json(res, 403, { error: 'Forbidden.' }); if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html'); if (!fs.existsSync(file)) return send(res, 404, fs.readFileSync(path.join(PUBLIC, '404.html')), MIME['.html']); send(res, 200, fs.readFileSync(file), MIME[path.extname(file)] || 'application/octet-stream'); } catch (e) { if (!res.headersSent) json(res, 500, { error: 'Server error.' }); } }).listen(PORT, () => console.log(`nkelabs listening on :${PORT}`));