92 lines
2.9 KiB
JavaScript
92 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('node:path');
|
|
const zlib = require('node:zlib');
|
|
const { URL } = require('node:url');
|
|
|
|
const MAX_BODY_BYTES = 256 * 1024 * 1024;
|
|
|
|
function isCompressible(contentType) {
|
|
const t = String(contentType || '');
|
|
return /^text\//.test(t) || /application\/(json|javascript)/.test(t);
|
|
}
|
|
|
|
function send(res, status, body = '', contentType = 'text/plain; charset=utf-8', headers = {}) {
|
|
let bytes = Buffer.isBuffer(body) ? body : Buffer.from(String(body), 'utf8');
|
|
const outHeaders = {
|
|
'Content-Type': contentType,
|
|
'Cache-Control': 'no-store',
|
|
...headers
|
|
};
|
|
// Gzip text/JSON responses when the client accepts it (single-user app: sync gzip is fine).
|
|
if (res._acceptGzip && isCompressible(contentType) && bytes.length > 1024) {
|
|
bytes = zlib.gzipSync(bytes);
|
|
outHeaders['Content-Encoding'] = 'gzip';
|
|
outHeaders['Vary'] = 'Accept-Encoding';
|
|
}
|
|
outHeaders['Content-Length'] = bytes.length;
|
|
res.writeHead(status, outHeaders);
|
|
res.end(bytes);
|
|
}
|
|
|
|
function json(res, status, data, headers = {}) {
|
|
send(res, status, JSON.stringify(data), 'application/json; charset=utf-8', headers);
|
|
}
|
|
|
|
async function readBody(req) {
|
|
const chunks = [];
|
|
let size = 0;
|
|
for await (const chunk of req) {
|
|
size += chunk.length;
|
|
if (size > MAX_BODY_BYTES) throw new Error('request body too large');
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
async function readJson(req) {
|
|
const body = await readBody(req);
|
|
if (!body.length) return {};
|
|
return JSON.parse(body.toString('utf8'));
|
|
}
|
|
|
|
function safeName(name, fallback = 'file') {
|
|
return String(name || fallback)
|
|
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, 180) || fallback;
|
|
}
|
|
|
|
function mimeByExt(file) {
|
|
const ext = path.extname(file).toLowerCase();
|
|
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
if (ext === '.js' || ext === '.mjs') return 'text/javascript; charset=utf-8';
|
|
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
if (ext === '.png') return 'image/png';
|
|
if (ext === '.webp') return 'image/webp';
|
|
if (ext === '.gif') return 'image/gif';
|
|
if (ext === '.svg') return 'image/svg+xml';
|
|
if (ext === '.woff2') return 'font/woff2';
|
|
if (ext === '.woff') return 'font/woff';
|
|
if (ext === '.ttf') return 'font/ttf';
|
|
if (ext === '.otf') return 'font/otf';
|
|
return 'image/jpeg';
|
|
}
|
|
|
|
function extFromContentType(type, sourceUrl) {
|
|
const contentType = String(type || '').toLowerCase();
|
|
if (contentType.includes('png')) return '.png';
|
|
if (contentType.includes('webp')) return '.webp';
|
|
if (contentType.includes('gif')) return '.gif';
|
|
if (contentType.includes('jpeg') || contentType.includes('jpg')) return '.jpg';
|
|
try {
|
|
return path.extname(new URL(sourceUrl).pathname) || '.jpg';
|
|
} catch {
|
|
return '.jpg';
|
|
}
|
|
}
|
|
|
|
module.exports = { send, json, readBody, readJson, safeName, mimeByExt, extFromContentType, MAX_BODY_BYTES };
|