This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export function httpError(status, message) {
|
||||
const error = new Error(message);
|
||||
error.status = status;
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { requestLimits } from '../../config/limits.mjs';
|
||||
import { httpError } from './httpError.mjs';
|
||||
|
||||
export async function readJsonBody(request) {
|
||||
const chunks = [];
|
||||
let bodySize = 0;
|
||||
|
||||
for await (const chunk of request) {
|
||||
bodySize += chunk.length;
|
||||
|
||||
if (bodySize > requestLimits.maxBodySize) {
|
||||
throw httpError(413, 'Request body is too large');
|
||||
}
|
||||
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
if (chunks.length === 0) {
|
||||
throw httpError(400, 'Request body is empty');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
} catch {
|
||||
throw httpError(400, 'Request body must be valid JSON');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { sendJson } from './sendJson.mjs';
|
||||
|
||||
export function sendError(response, error) {
|
||||
const status = error.status || 500;
|
||||
const message = status >= 500 ? 'Internal server error' : error.message;
|
||||
|
||||
if (status >= 500) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return sendJson(response, { error: message }, status);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function sendJson(response, body, status = 200) {
|
||||
const payload = `${JSON.stringify(body)}\n`;
|
||||
response.writeHead(status, {
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
});
|
||||
response.end(payload);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const mimeTypes = {
|
||||
'.csv': 'text/csv; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.gif': 'image/gif',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.md': 'text/markdown; charset=utf-8',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.pdf': 'application/pdf',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.zip': 'application/zip',
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { resolve, sep } from 'node:path';
|
||||
|
||||
export function resolveInside(root, pathname) {
|
||||
let decodedPath;
|
||||
try {
|
||||
decodedPath = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePath = resolve(root, decodedPath.replace(/^\/+/, ''));
|
||||
return filePath === root || filePath.startsWith(`${root}${sep}`) ? filePath : null;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { extname } from 'node:path';
|
||||
import { httpError } from '../http/httpError.mjs';
|
||||
import { mimeTypes } from './mimeTypes.mjs';
|
||||
import { resolveInside } from './resolveInside.mjs';
|
||||
|
||||
export async function serveFile(response, root, pathname, method) {
|
||||
const filePath = resolveInside(root, pathname);
|
||||
|
||||
if (!filePath) {
|
||||
throw httpError(403, 'Forbidden');
|
||||
}
|
||||
|
||||
const fileStat = await getFileStat(filePath);
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Length': fileStat.size,
|
||||
'Content-Type': mimeTypes[extname(filePath).toLowerCase()] || 'application/octet-stream',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
});
|
||||
|
||||
if (method === 'HEAD') {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
createReadStream(filePath).pipe(response);
|
||||
}
|
||||
|
||||
async function getFileStat(filePath) {
|
||||
let fileStat;
|
||||
try {
|
||||
fileStat = await stat(filePath);
|
||||
} catch {
|
||||
throw httpError(404, 'Not found');
|
||||
}
|
||||
|
||||
if (!fileStat.isFile()) {
|
||||
throw httpError(404, 'Not found');
|
||||
}
|
||||
|
||||
return fileStat;
|
||||
}
|
||||
Reference in New Issue
Block a user