292 lines
6.0 KiB
JavaScript
292 lines
6.0 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
|
||
|
|
||
|
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} ExpectedRequest
|
||
|
* @property {(name: string) => string | undefined} get
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} ExpectedResponse
|
||
|
* @property {(name: string) => string | string[] | undefined} get
|
||
|
* @property {(name: string, value: number | string | string[]) => void} set
|
||
|
* @property {(status: number) => void} status
|
||
|
* @property {(data: any) => void} send
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Response} res
|
||
|
* @returns {string[]}
|
||
|
*/
|
||
|
function getHeaderNames(res) {
|
||
|
if (typeof res.getHeaderNames !== "function") {
|
||
|
// @ts-ignore
|
||
|
// eslint-disable-next-line no-underscore-dangle
|
||
|
return Object.keys(res._headers || {});
|
||
|
}
|
||
|
|
||
|
return res.getHeaderNames();
|
||
|
}
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @param {Request} req
|
||
|
* @param {string} name
|
||
|
* @returns {string | undefined}
|
||
|
*/
|
||
|
|
||
|
|
||
|
function getHeaderFromRequest(req, name) {
|
||
|
// Express API
|
||
|
if (typeof
|
||
|
/** @type {Request & ExpectedRequest} */
|
||
|
req.get === "function") {
|
||
|
return (
|
||
|
/** @type {Request & ExpectedRequest} */
|
||
|
req.get(name)
|
||
|
);
|
||
|
} // Node.js API
|
||
|
// @ts-ignore
|
||
|
|
||
|
|
||
|
return req.headers[name];
|
||
|
}
|
||
|
/**
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Response} res
|
||
|
* @param {string} name
|
||
|
* @returns {number | string | string[] | undefined}
|
||
|
*/
|
||
|
|
||
|
|
||
|
function getHeaderFromResponse(res, name) {
|
||
|
// Express API
|
||
|
if (typeof
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.get === "function") {
|
||
|
return (
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.get(name)
|
||
|
);
|
||
|
} // Node.js API
|
||
|
|
||
|
|
||
|
return res.getHeader(name);
|
||
|
}
|
||
|
/**
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Response} res
|
||
|
* @param {string} name
|
||
|
* @param {number | string | string[]} value
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
|
||
|
|
||
|
function setHeaderForResponse(res, name, value) {
|
||
|
// Express API
|
||
|
if (typeof
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.set === "function") {
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.set(name, typeof value === "number" ? String(value) : value);
|
||
|
return;
|
||
|
} // Node.js API
|
||
|
|
||
|
|
||
|
res.setHeader(name, value);
|
||
|
}
|
||
|
/**
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Response} res
|
||
|
* @param {number} code
|
||
|
*/
|
||
|
|
||
|
|
||
|
function setStatusCode(res, code) {
|
||
|
if (typeof
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.status === "function") {
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.status(code);
|
||
|
return;
|
||
|
} // eslint-disable-next-line no-param-reassign
|
||
|
|
||
|
|
||
|
res.statusCode = code;
|
||
|
}
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Request} req
|
||
|
* @param {Response} res
|
||
|
* @param {string | Buffer | import("fs").ReadStream} bufferOtStream
|
||
|
* @param {number} byteLength
|
||
|
*/
|
||
|
|
||
|
|
||
|
function send(req, res, bufferOtStream, byteLength) {
|
||
|
if (typeof
|
||
|
/** @type {import("fs").ReadStream} */
|
||
|
bufferOtStream.pipe === "function") {
|
||
|
setHeaderForResponse(res, "Content-Length", byteLength);
|
||
|
|
||
|
if (req.method === "HEAD") {
|
||
|
res.end();
|
||
|
return;
|
||
|
}
|
||
|
/** @type {import("fs").ReadStream} */
|
||
|
|
||
|
|
||
|
bufferOtStream.pipe(res);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.send === "function") {
|
||
|
/** @type {Response & ExpectedResponse} */
|
||
|
res.send(bufferOtStream);
|
||
|
return;
|
||
|
} // Only Node.js API used
|
||
|
|
||
|
|
||
|
res.setHeader("Content-Length", byteLength);
|
||
|
|
||
|
if (req.method === "HEAD") {
|
||
|
res.end();
|
||
|
} else {
|
||
|
res.end(bufferOtStream);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Response} res
|
||
|
*/
|
||
|
|
||
|
|
||
|
function clearHeadersForResponse(res) {
|
||
|
const headers = getHeaderNames(res);
|
||
|
|
||
|
for (let i = 0; i < headers.length; i++) {
|
||
|
res.removeHeader(headers[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const matchHtmlRegExp = /["'&<>]/;
|
||
|
/**
|
||
|
* @param {string} string raw HTML
|
||
|
* @returns {string} escaped HTML
|
||
|
*/
|
||
|
|
||
|
function escapeHtml(string) {
|
||
|
const str = `${string}`;
|
||
|
const match = matchHtmlRegExp.exec(str);
|
||
|
|
||
|
if (!match) {
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
let escape;
|
||
|
let html = "";
|
||
|
let index = 0;
|
||
|
let lastIndex = 0;
|
||
|
|
||
|
for (({
|
||
|
index
|
||
|
} = match); index < str.length; index++) {
|
||
|
switch (str.charCodeAt(index)) {
|
||
|
// "
|
||
|
case 34:
|
||
|
escape = """;
|
||
|
break;
|
||
|
// &
|
||
|
|
||
|
case 38:
|
||
|
escape = "&";
|
||
|
break;
|
||
|
// '
|
||
|
|
||
|
case 39:
|
||
|
escape = "'";
|
||
|
break;
|
||
|
// <
|
||
|
|
||
|
case 60:
|
||
|
escape = "<";
|
||
|
break;
|
||
|
// >
|
||
|
|
||
|
case 62:
|
||
|
escape = ">";
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// eslint-disable-next-line no-continue
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (lastIndex !== index) {
|
||
|
html += str.substring(lastIndex, index);
|
||
|
}
|
||
|
|
||
|
lastIndex = index + 1;
|
||
|
html += escape;
|
||
|
}
|
||
|
|
||
|
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
|
||
|
}
|
||
|
/** @type {Record<number, string>} */
|
||
|
|
||
|
|
||
|
const statuses = {
|
||
|
400: "Bad Request",
|
||
|
403: "Forbidden",
|
||
|
404: "Not Found",
|
||
|
416: "Range Not Satisfiable",
|
||
|
500: "Internal Server Error"
|
||
|
};
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Request} req response
|
||
|
* @param {Response} res response
|
||
|
* @param {number} status status
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
|
||
|
function sendError(req, res, status) {
|
||
|
const content = statuses[status] || String(status);
|
||
|
const document = `<!DOCTYPE html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<meta charset="utf-8">
|
||
|
<title>Error</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<pre>${escapeHtml(content)}</pre>
|
||
|
</body>
|
||
|
</html>`; // Clear existing headers
|
||
|
|
||
|
clearHeadersForResponse(res); // Send basic response
|
||
|
|
||
|
setStatusCode(res, status);
|
||
|
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
|
||
|
setHeaderForResponse(res, "Content-Security-Policy", "default-src 'none'");
|
||
|
setHeaderForResponse(res, "X-Content-Type-Options", "nosniff");
|
||
|
const byteLength = Buffer.byteLength(document);
|
||
|
setHeaderForResponse(res, "Content-Length", byteLength);
|
||
|
res.end(document);
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
getHeaderNames,
|
||
|
getHeaderFromRequest,
|
||
|
getHeaderFromResponse,
|
||
|
setHeaderForResponse,
|
||
|
setStatusCode,
|
||
|
send,
|
||
|
sendError
|
||
|
};
|