| 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
|
|---|
| 4 |
|
|---|
| 5 | /** @typedef {import("../index.js").ServerResponse} ServerResponse */
|
|---|
| 6 |
|
|---|
| 7 | /**
|
|---|
| 8 | * @typedef {Object} ExpectedRequest
|
|---|
| 9 | * @property {(name: string) => string | undefined} get
|
|---|
| 10 | */
|
|---|
| 11 |
|
|---|
| 12 | /**
|
|---|
| 13 | * @typedef {Object} ExpectedResponse
|
|---|
| 14 | * @property {(name: string) => string | string[] | undefined} get
|
|---|
| 15 | * @property {(name: string, value: number | string | string[]) => void} set
|
|---|
| 16 | * @property {(status: number) => void} status
|
|---|
| 17 | * @property {(data: any) => void} send
|
|---|
| 18 | */
|
|---|
| 19 |
|
|---|
| 20 | /**
|
|---|
| 21 | * @template {ServerResponse} Response
|
|---|
| 22 | * @param {Response} res
|
|---|
| 23 | * @returns {string[]}
|
|---|
| 24 | */
|
|---|
| 25 | function getHeaderNames(res) {
|
|---|
| 26 | if (typeof res.getHeaderNames !== "function") {
|
|---|
| 27 | // @ts-ignore
|
|---|
| 28 | // eslint-disable-next-line no-underscore-dangle
|
|---|
| 29 | return Object.keys(res._headers || {});
|
|---|
| 30 | }
|
|---|
| 31 |
|
|---|
| 32 | return res.getHeaderNames();
|
|---|
| 33 | }
|
|---|
| 34 | /**
|
|---|
| 35 | * @template {IncomingMessage} Request
|
|---|
| 36 | * @param {Request} req
|
|---|
| 37 | * @param {string} name
|
|---|
| 38 | * @returns {string | undefined}
|
|---|
| 39 | */
|
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 | function getHeaderFromRequest(req, name) {
|
|---|
| 43 | // Express API
|
|---|
| 44 | if (typeof
|
|---|
| 45 | /** @type {Request & ExpectedRequest} */
|
|---|
| 46 | req.get === "function") {
|
|---|
| 47 | return (
|
|---|
| 48 | /** @type {Request & ExpectedRequest} */
|
|---|
| 49 | req.get(name)
|
|---|
| 50 | );
|
|---|
| 51 | } // Node.js API
|
|---|
| 52 | // @ts-ignore
|
|---|
| 53 |
|
|---|
| 54 |
|
|---|
| 55 | return req.headers[name];
|
|---|
| 56 | }
|
|---|
| 57 | /**
|
|---|
| 58 | * @template {ServerResponse} Response
|
|---|
| 59 | * @param {Response} res
|
|---|
| 60 | * @param {string} name
|
|---|
| 61 | * @returns {number | string | string[] | undefined}
|
|---|
| 62 | */
|
|---|
| 63 |
|
|---|
| 64 |
|
|---|
| 65 | function getHeaderFromResponse(res, name) {
|
|---|
| 66 | // Express API
|
|---|
| 67 | if (typeof
|
|---|
| 68 | /** @type {Response & ExpectedResponse} */
|
|---|
| 69 | res.get === "function") {
|
|---|
| 70 | return (
|
|---|
| 71 | /** @type {Response & ExpectedResponse} */
|
|---|
| 72 | res.get(name)
|
|---|
| 73 | );
|
|---|
| 74 | } // Node.js API
|
|---|
| 75 |
|
|---|
| 76 |
|
|---|
| 77 | return res.getHeader(name);
|
|---|
| 78 | }
|
|---|
| 79 | /**
|
|---|
| 80 | * @template {ServerResponse} Response
|
|---|
| 81 | * @param {Response} res
|
|---|
| 82 | * @param {string} name
|
|---|
| 83 | * @param {number | string | string[]} value
|
|---|
| 84 | * @returns {void}
|
|---|
| 85 | */
|
|---|
| 86 |
|
|---|
| 87 |
|
|---|
| 88 | function setHeaderForResponse(res, name, value) {
|
|---|
| 89 | // Express API
|
|---|
| 90 | if (typeof
|
|---|
| 91 | /** @type {Response & ExpectedResponse} */
|
|---|
| 92 | res.set === "function") {
|
|---|
| 93 | /** @type {Response & ExpectedResponse} */
|
|---|
| 94 | res.set(name, typeof value === "number" ? String(value) : value);
|
|---|
| 95 | return;
|
|---|
| 96 | } // Node.js API
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 | res.setHeader(name, value);
|
|---|
| 100 | }
|
|---|
| 101 | /**
|
|---|
| 102 | * @template {ServerResponse} Response
|
|---|
| 103 | * @param {Response} res
|
|---|
| 104 | * @param {number} code
|
|---|
| 105 | */
|
|---|
| 106 |
|
|---|
| 107 |
|
|---|
| 108 | function setStatusCode(res, code) {
|
|---|
| 109 | if (typeof
|
|---|
| 110 | /** @type {Response & ExpectedResponse} */
|
|---|
| 111 | res.status === "function") {
|
|---|
| 112 | /** @type {Response & ExpectedResponse} */
|
|---|
| 113 | res.status(code);
|
|---|
| 114 | return;
|
|---|
| 115 | } // eslint-disable-next-line no-param-reassign
|
|---|
| 116 |
|
|---|
| 117 |
|
|---|
| 118 | res.statusCode = code;
|
|---|
| 119 | }
|
|---|
| 120 | /**
|
|---|
| 121 | * @template {IncomingMessage} Request
|
|---|
| 122 | * @template {ServerResponse} Response
|
|---|
| 123 | * @param {Request} req
|
|---|
| 124 | * @param {Response} res
|
|---|
| 125 | * @param {string | Buffer | import("fs").ReadStream} bufferOtStream
|
|---|
| 126 | * @param {number} byteLength
|
|---|
| 127 | */
|
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 | function send(req, res, bufferOtStream, byteLength) {
|
|---|
| 131 | if (typeof
|
|---|
| 132 | /** @type {import("fs").ReadStream} */
|
|---|
| 133 | bufferOtStream.pipe === "function") {
|
|---|
| 134 | setHeaderForResponse(res, "Content-Length", byteLength);
|
|---|
| 135 |
|
|---|
| 136 | if (req.method === "HEAD") {
|
|---|
| 137 | res.end();
|
|---|
| 138 | return;
|
|---|
| 139 | }
|
|---|
| 140 | /** @type {import("fs").ReadStream} */
|
|---|
| 141 |
|
|---|
| 142 |
|
|---|
| 143 | bufferOtStream.pipe(res);
|
|---|
| 144 | return;
|
|---|
| 145 | }
|
|---|
| 146 |
|
|---|
| 147 | if (typeof
|
|---|
| 148 | /** @type {Response & ExpectedResponse} */
|
|---|
| 149 | res.send === "function") {
|
|---|
| 150 | /** @type {Response & ExpectedResponse} */
|
|---|
| 151 | res.send(bufferOtStream);
|
|---|
| 152 | return;
|
|---|
| 153 | } // Only Node.js API used
|
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 | res.setHeader("Content-Length", byteLength);
|
|---|
| 157 |
|
|---|
| 158 | if (req.method === "HEAD") {
|
|---|
| 159 | res.end();
|
|---|
| 160 | } else {
|
|---|
| 161 | res.end(bufferOtStream);
|
|---|
| 162 | }
|
|---|
| 163 | }
|
|---|
| 164 | /**
|
|---|
| 165 | * @template {ServerResponse} Response
|
|---|
| 166 | * @param {Response} res
|
|---|
| 167 | */
|
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 | function clearHeadersForResponse(res) {
|
|---|
| 171 | const headers = getHeaderNames(res);
|
|---|
| 172 |
|
|---|
| 173 | for (let i = 0; i < headers.length; i++) {
|
|---|
| 174 | res.removeHeader(headers[i]);
|
|---|
| 175 | }
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | const matchHtmlRegExp = /["'&<>]/;
|
|---|
| 179 | /**
|
|---|
| 180 | * @param {string} string raw HTML
|
|---|
| 181 | * @returns {string} escaped HTML
|
|---|
| 182 | */
|
|---|
| 183 |
|
|---|
| 184 | function escapeHtml(string) {
|
|---|
| 185 | const str = `${string}`;
|
|---|
| 186 | const match = matchHtmlRegExp.exec(str);
|
|---|
| 187 |
|
|---|
| 188 | if (!match) {
|
|---|
| 189 | return str;
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | let escape;
|
|---|
| 193 | let html = "";
|
|---|
| 194 | let index = 0;
|
|---|
| 195 | let lastIndex = 0;
|
|---|
| 196 |
|
|---|
| 197 | for (({
|
|---|
| 198 | index
|
|---|
| 199 | } = match); index < str.length; index++) {
|
|---|
| 200 | switch (str.charCodeAt(index)) {
|
|---|
| 201 | // "
|
|---|
| 202 | case 34:
|
|---|
| 203 | escape = """;
|
|---|
| 204 | break;
|
|---|
| 205 | // &
|
|---|
| 206 |
|
|---|
| 207 | case 38:
|
|---|
| 208 | escape = "&";
|
|---|
| 209 | break;
|
|---|
| 210 | // '
|
|---|
| 211 |
|
|---|
| 212 | case 39:
|
|---|
| 213 | escape = "'";
|
|---|
| 214 | break;
|
|---|
| 215 | // <
|
|---|
| 216 |
|
|---|
| 217 | case 60:
|
|---|
| 218 | escape = "<";
|
|---|
| 219 | break;
|
|---|
| 220 | // >
|
|---|
| 221 |
|
|---|
| 222 | case 62:
|
|---|
| 223 | escape = ">";
|
|---|
| 224 | break;
|
|---|
| 225 |
|
|---|
| 226 | default:
|
|---|
| 227 | // eslint-disable-next-line no-continue
|
|---|
| 228 | continue;
|
|---|
| 229 | }
|
|---|
| 230 |
|
|---|
| 231 | if (lastIndex !== index) {
|
|---|
| 232 | html += str.substring(lastIndex, index);
|
|---|
| 233 | }
|
|---|
| 234 |
|
|---|
| 235 | lastIndex = index + 1;
|
|---|
| 236 | html += escape;
|
|---|
| 237 | }
|
|---|
| 238 |
|
|---|
| 239 | return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
|
|---|
| 240 | }
|
|---|
| 241 | /** @type {Record<number, string>} */
|
|---|
| 242 |
|
|---|
| 243 |
|
|---|
| 244 | const statuses = {
|
|---|
| 245 | 400: "Bad Request",
|
|---|
| 246 | 403: "Forbidden",
|
|---|
| 247 | 404: "Not Found",
|
|---|
| 248 | 416: "Range Not Satisfiable",
|
|---|
| 249 | 500: "Internal Server Error"
|
|---|
| 250 | };
|
|---|
| 251 | /**
|
|---|
| 252 | * @template {IncomingMessage} Request
|
|---|
| 253 | * @template {ServerResponse} Response
|
|---|
| 254 | * @param {Request} req response
|
|---|
| 255 | * @param {Response} res response
|
|---|
| 256 | * @param {number} status status
|
|---|
| 257 | * @returns {void}
|
|---|
| 258 | */
|
|---|
| 259 |
|
|---|
| 260 | function sendError(req, res, status) {
|
|---|
| 261 | const content = statuses[status] || String(status);
|
|---|
| 262 | const document = `<!DOCTYPE html>
|
|---|
| 263 | <html lang="en">
|
|---|
| 264 | <head>
|
|---|
| 265 | <meta charset="utf-8">
|
|---|
| 266 | <title>Error</title>
|
|---|
| 267 | </head>
|
|---|
| 268 | <body>
|
|---|
| 269 | <pre>${escapeHtml(content)}</pre>
|
|---|
| 270 | </body>
|
|---|
| 271 | </html>`; // Clear existing headers
|
|---|
| 272 |
|
|---|
| 273 | clearHeadersForResponse(res); // Send basic response
|
|---|
| 274 |
|
|---|
| 275 | setStatusCode(res, status);
|
|---|
| 276 | setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
|
|---|
| 277 | setHeaderForResponse(res, "Content-Security-Policy", "default-src 'none'");
|
|---|
| 278 | setHeaderForResponse(res, "X-Content-Type-Options", "nosniff");
|
|---|
| 279 | const byteLength = Buffer.byteLength(document);
|
|---|
| 280 | setHeaderForResponse(res, "Content-Length", byteLength);
|
|---|
| 281 | res.end(document);
|
|---|
| 282 | }
|
|---|
| 283 |
|
|---|
| 284 | module.exports = {
|
|---|
| 285 | getHeaderNames,
|
|---|
| 286 | getHeaderFromRequest,
|
|---|
| 287 | getHeaderFromResponse,
|
|---|
| 288 | setHeaderForResponse,
|
|---|
| 289 | setStatusCode,
|
|---|
| 290 | send,
|
|---|
| 291 | sendError
|
|---|
| 292 | }; |
|---|