// Generated, do not edit. sha256:29c149da1bd119d466012f8c80b13c9f6ba6ac924e6326dd2b0629b05ed6ce6e import { readFile } from 'node:fs/promises'; import { join, resolve, normalize, extname, dirname, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { handleApi, readJsonBody } from './api.mjs'; import { clearedSessionCookie, passwordMatches, sendUnauthorized, sessionCookie, sessionTokenOf, } from './auth.mjs'; import { createAttemptLimiter, createSessionStore } from './sessions.mjs'; import { CONFIG } from '../shared/config.js'; import { connectSrcOrigins } from '../shared/csp.js'; import { listConnections } from './data/connections.js'; import { errorPayload } from '../shared/app-error.js'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); export const CLIENT = join(ROOT, 'client'); const SHARED = join(ROOT, 'shared'); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.wasm': 'application/wasm', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', }; function explorerUrls() { try { return listConnections('btc_explorer').map(c => c.url); } catch { return []; } } const directive = (naam, ...bronnen) => [naam, ...bronnen.flat().filter(Boolean)].join(' '); const csp = () => [ directive('default-src', `'self'`), directive('script-src', `'self'`), directive('style-src', `'self'`, `'unsafe-inline'`), directive('style-src-elem', `'self'`), directive('style-src-attr', `'unsafe-inline'`), directive('font-src', `'self'`), directive('img-src', `'self'`, 'data:'), directive('connect-src', `'self'`, connectSrcOrigins(explorerUrls())), directive('object-src', `'none'`), directive('base-uri', `'self'`), directive('frame-ancestors', `'none'`), ].join('; '); const HTML_CACHE_CONTROL = 'no-store'; const STATIC_CACHE_CONTROL = 'no-cache'; function resolveStatic(urlPath) { let rel; try { rel = decodeURIComponent(urlPath.split('?')[0]); } catch { return null; } if (rel === '/' || rel === '/index.html') return join(CLIENT, 'index.html'); if (rel === CONFIG.AUTH.LOGIN_PATH) return join(CLIENT, 'login.html'); const onder = (basis, pad) => { const abs = normalize(join(basis, pad)); return abs.startsWith(basis + sep) ? abs : null; }; if (rel.startsWith('/shared/')) return onder(SHARED, rel.slice('/shared'.length)); return onder(CLIENT, rel); } function send(res, status, body, headers = {}) { res.writeHead(status, { 'X-Content-Type-Options': 'nosniff', ...headers }); res.end(body); } function sendJson(res, status, obj, extra = {}) { send(res, status, JSON.stringify(obj), { 'Content-Type': 'application/json; charset=utf-8', ...extra }); } const LOGIN_ASSETS = new Set([ '/login.js', '/css/styles.css', '/img/favicon.svg', '/vendor/inter/inter.css', ]); const LOGIN_ASSET_PREFIX = '/vendor/inter/files/'; const magZonderSessie = (pad) => pad === CONFIG.AUTH.LOGIN_PATH || LOGIN_ASSETS.has(pad) || pad.startsWith(LOGIN_ASSET_PREFIX); export function createRequestListener({ credentials = null, secure = false } = {}) { const sessies = createSessionStore(); const limiet = createAttemptLimiter(); async function inloggen(req, res) { const adres = req.socket?.remoteAddress || 'unknown'; const wacht = limiet.retryAfterMs(adres); if (wacht > 0) { return sendJson(res, 429, { error: 'Too many attempts.', retryAfterMs: wacht }, { 'Retry-After': String(Math.ceil(wacht / 1000)) }); } let body; try { body = await readJsonBody(req); } catch (e) { return sendJson(res, 400, errorPayload(e)); } if (!passwordMatches(body?.password, credentials)) { return sendJson(res, 401, { error: 'Wrong password.', retryAfterMs: limiet.fail(adres) }); } limiet.succeed(adres); return sendJson(res, 200, { ok: true }, { 'Set-Cookie': sessionCookie(sessies.create(), { secure }) }); } function uitloggen(req, res) { const token = sessionTokenOf(req); if (token) sessies.drop(token); return sendJson(res, 200, { ok: true }, { 'Set-Cookie': clearedSessionCookie({ secure }) }); } return async (req, res) => { try { const url = req.url || '/'; const pad = url.split('?')[0]; if (credentials) { if (pad === '/api/login') { if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' }); return await inloggen(req, res); } if (pad === '/api/logout') { if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' }); return uitloggen(req, res); } const ingelogd = sessies.isValid(sessionTokenOf(req)); if (ingelogd && pad === CONFIG.AUTH.LOGIN_PATH) { return send(res, 302, '', { Location: '/', 'Cache-Control': 'no-store' }); } if (!ingelogd && !magZonderSessie(pad)) { return sendUnauthorized(res, { document: !pad.startsWith('/api/') }); } } else if (pad === CONFIG.AUTH.LOGIN_PATH) { return send(res, 302, '', { Location: '/', 'Cache-Control': 'no-store' }); } if (url.startsWith('/api/')) { const handled = await handleApi(req, res, url); if (!handled) sendJson(res, 404, { error: 'Unknown endpoint' }); return; } if (req.method !== 'GET' && req.method !== 'HEAD') { return sendJson(res, 405, { error: 'Method not allowed' }); } const file = resolveStatic(url); if (!file) return send(res, 404, 'Not found'); const data = await readFile(file); const ext = extname(file).toLowerCase(); const type = MIME[ext] || 'application/octet-stream'; const cacheControl = ext === '.html' ? HTML_CACHE_CONTROL : STATIC_CACHE_CONTROL; return send(res, 200, req.method === 'HEAD' ? '' : data, { 'Content-Type': type, 'Content-Security-Policy': csp(), 'Cache-Control': cacheControl }); } catch (err) { if (err && err.code === 'ENOENT') return send(res, 404, 'Not found'); console.error('[assetapp] requestfout:', err); return send(res, 500, 'Interne fout'); } }; }