| 1 | const config = require('./config'); |
| 2 | |
| 3 | const STATIC_HEADERS = { |
| 4 | 'X-Content-Type-Options': 'nosniff', |
| 5 | 'X-Frame-Options': 'DENY', |
| 6 | 'Referrer-Policy': 'no-referrer', |
| 7 | 'Permissions-Policy': 'interest-cohort=()', |
| 8 | 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains' |
| 9 | }; |
| 10 | |
| 11 | function securityHeaders() { return { ...STATIC_HEADERS }; } |
| 12 | |
| 13 | function htmlHeaders() { |
| 14 | return { |
| 15 | 'Content-Type': 'text/html; charset=utf-8', |
| 16 | 'Content-Security-Policy': config.get('security.csp'), |
| 17 | ...STATIC_HEADERS |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | function json(res, code, obj, extra) { |
| 22 | const body = JSON.stringify(obj); |
| 23 | res.writeHead(code, { |
| 24 | 'Content-Type': 'application/json', |
| 25 | 'Content-Length': Buffer.byteLength(body), |
| 26 | ...STATIC_HEADERS, |
| 27 | ...(extra || {}) |
| 28 | }); |
| 29 | res.end(body); |
| 30 | } |
| 31 | |
| 32 | function html(res, code, body, extra) { |
| 33 | res.writeHead(code, { ...htmlHeaders(), ...(extra || {}) }); |
| 34 | res.end(body); |
| 35 | } |
| 36 | |
| 37 | function text(res, code, body, extra) { |
| 38 | res.writeHead(code, { |
| 39 | 'Content-Type': 'text/plain; charset=utf-8', |
| 40 | ...STATIC_HEADERS, |
| 41 | ...(extra || {}) |
| 42 | }); |
| 43 | res.end(body); |
| 44 | } |
| 45 | |
| 46 | function redirect(res, location) { |
| 47 | res.writeHead(302, { Location: location, ...STATIC_HEADERS }); |
| 48 | res.end(); |
| 49 | } |
| 50 | |
| 51 | function notFound(res) { text(res, 404, 'Not found'); } |
| 52 | function unauthorized(res) { json(res, 401, { error: 'Not authenticated' }); } |
| 53 | function forbidden(res, msg) { json(res, 403, { error: msg || 'Forbidden' }); } |
| 54 | function badRequest(res, msg) { json(res, 400, { error: msg || 'Bad request' }); } |
| 55 | function tooMany(res, msg) { json(res, 429, { error: msg || 'Too many requests' }); } |
| 56 | function serverError(res, msg) { json(res, 500, { error: msg || 'Server error' }); } |
| 57 | |
| 58 | function readJson(req, cb) { |
| 59 | let buf = ''; |
| 60 | req.on('data', chunk => { |
| 61 | buf += chunk; |
| 62 | if (buf.length > 100_000) { req.destroy(); cb(new Error('Request body too large')); } |
| 63 | }); |
| 64 | req.on('end', () => { |
| 65 | try { cb(null, buf ? JSON.parse(buf) : {}); } |
| 66 | catch (e) { cb(new Error('Invalid JSON')); } |
| 67 | }); |
| 68 | req.on('error', cb); |
| 69 | } |
| 70 | |
| 71 | function clientIp(req) { |
| 72 | if (config.get('security.trustProxy')) { |
| 73 | const xf = req.headers['x-forwarded-for']; |
| 74 | if (xf) return xf.split(',')[0].trim(); |
| 75 | } |
| 76 | return req.socket.remoteAddress || 'unknown'; |
| 77 | } |
| 78 | |
| 79 | module.exports = { |
| 80 | securityHeaders, htmlHeaders, |
| 81 | json, html, text, redirect, |
| 82 | notFound, unauthorized, forbidden, badRequest, tooMany, serverError, |
| 83 | readJson, clientIp |
| 84 | }; |
| 85 | |