| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- "use strict";
- const crypto = require("node:crypto");
- /** @typedef {import("./index").IncomingMessage} IncomingMessage */
- /** @typedef {import("./index").ServerResponse} ServerResponse */
- /** @typedef {import("./index").OutputFileSystem} OutputFileSystem */
- /** @typedef {import("./index").EXPECTED_ANY} EXPECTED_ANY */
- 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:
- continue;
- }
- if (lastIndex !== index) {
- // eslint-disable-next-line unicorn/prefer-string-slice
- html += str.substring(lastIndex, index);
- }
- lastIndex = index + 1;
- html += escape;
- }
- // eslint-disable-next-line unicorn/prefer-string-slice
- return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
- }
- /** @typedef {import("fs").Stats} Stats */
- /** @typedef {import("fs").ReadStream} ReadStream */
- /**
- * Parse an HTTP Date into a number.
- * @param {string} date date
- * @returns {number} timestamp
- */
- function parseHttpDate(date) {
- const timestamp = date && Date.parse(date);
- // istanbul ignore next: guard against date.js Date.parse patching
- return typeof timestamp === "number" ? timestamp : Number.NaN;
- }
- /**
- * @param {"bytes"} type type
- * @param {number} size size
- * @param {import("range-parser").Range=} range range
- * @returns {string} value of content range header
- */
- function getValueContentRangeHeader(type, size, range) {
- return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
- }
- /**
- * Generate a tag for a stat.
- * @param {Stats} stats stats
- * @returns {{ hash: string, buffer?: Buffer }} etag
- */
- function statTag(stats) {
- const mtime = stats.mtime.getTime().toString(16);
- const size = stats.size.toString(16);
- return {
- hash: `W/"${size}-${mtime}"`
- };
- }
- /**
- * Generate an entity tag.
- * @param {Buffer | ReadStream} entity entity
- * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
- */
- async function entityTag(entity) {
- const sha1 = crypto.createHash("sha1");
- if (!Buffer.isBuffer(entity)) {
- let byteLength = 0;
- /** @type {Buffer[]} */
- const buffers = [];
- await new Promise((resolve, reject) => {
- entity.on("data", chunk => {
- sha1.update(chunk);
- buffers.push(/** @type {Buffer} */chunk);
- byteLength += /** @type {Buffer} */chunk.byteLength;
- }).on("end", () => {
- resolve(sha1);
- }).on("error", reject);
- });
- return {
- buffer: Buffer.concat(buffers),
- hash: `"${byteLength.toString(16)}-${sha1.digest("base64").slice(0, 27)}"`
- };
- }
- if (entity.byteLength === 0) {
- // Fast-path empty
- return {
- hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
- };
- }
- // Compute hash of entity
- const hash = sha1.update(entity).digest("base64").slice(0, 27);
- // Compute length of entity
- const {
- byteLength
- } = entity;
- return {
- hash: `"${byteLength.toString(16)}-${hash}"`
- };
- }
- /**
- * Create a simple ETag.
- * @param {Buffer | ReadStream | Stats} entity entity
- * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
- */
- async function etag(entity) {
- const isStrong = Buffer.isBuffer(entity) || typeof (/** @type {ReadStream} */entity.pipe) === "function";
- return isStrong ? entityTag(/** @type {Buffer | ReadStream} */entity) : statTag(/** @type {import("fs").Stats} */entity);
- }
- const cacheStore = new WeakMap();
- /**
- * @template T
- * @typedef {(...args: EXPECTED_ANY) => T} FunctionReturning
- */
- /**
- * @template T
- * @param {FunctionReturning<T>} fn memorized function
- * @param {({ cache?: Map<string, { data: T }> } | undefined)=} cache cache
- * @param {((value: T) => T)=} callback callback
- * @returns {FunctionReturning<T>} new function
- */
- function memorize(fn, {
- cache = new Map()
- } = {}, callback = undefined) {
- /**
- * @param {EXPECTED_ANY[]} arguments_ args
- * @returns {EXPECTED_ANY} result
- */
- const memoized = (...arguments_) => {
- const [key] = arguments_;
- const cacheItem = cache.get(key);
- if (cacheItem) {
- return cacheItem.data;
- }
- // @ts-expect-error
- let result = fn.apply(this, arguments_);
- if (callback) {
- result = callback(result);
- }
- cache.set(key, {
- data: result
- });
- return result;
- };
- cacheStore.set(memoized, cache);
- return memoized;
- }
- /**
- * Parse a HTTP token list.
- * @param {string} str str
- * @returns {string[]} tokens
- */
- function parseTokenList(str) {
- let end = 0;
- let start = 0;
- const list = [];
- // gather tokens
- for (let i = 0, len = str.length; i < len; i++) {
- switch (str.charCodeAt(i)) {
- case 0x20 /* */:
- if (start === end) {
- end = i + 1;
- start = end;
- }
- break;
- case 0x2c /* , */:
- if (start !== end) {
- list.push(str.slice(start, end));
- }
- end = i + 1;
- start = end;
- break;
- default:
- end = i + 1;
- break;
- }
- }
- // final token
- if (start !== end) {
- list.push(str.slice(start, end));
- }
- return list;
- }
- /**
- * @typedef {object} ExpectedIncomingMessage
- * @property {((name: string) => string | string[] | undefined)=} getHeader get header extra method
- * @property {(() => string | undefined)=} getMethod get method extra method
- * @property {(() => string | undefined)=} getURL get URL extra method
- * @property {string=} originalUrl an extra option for `fastify` (and `@fastify/express`) to get original URL
- * @property {string=} id an extra option for `fastify` (and `@fastify/express`) to get ID of request
- */
- /**
- * @typedef {object} ExpectedServerResponse
- * @property {((status: number) => void)=} setStatusCode set status code
- * @property {(() => number)=} getStatusCode get status code
- * @property {((name: string) => string | string[] | undefined | number)} getHeader get header
- * @property {((name: string, value: number | string | Readonly<string[]>) => ExpectedServerResponse)=} setHeader set header
- * @property {((name: string) => void)=} removeHeader remove header
- * @property {((data: string | Buffer) => void)=} send send
- * @property {((data?: string | Buffer) => void)=} finish finish
- * @property {(() => string[])=} getResponseHeaders get response header
- * @property {(() => boolean)=} getHeadersSent get headers sent
- * @property {((data: EXPECTED_ANY) => void)=} stream stream
- * @property {(() => EXPECTED_ANY)=} getOutgoing get outgoing
- * @property {((name: string, value: EXPECTED_ANY) => void)=} setState set state
- */
- /**
- * @template {IncomingMessage & ExpectedIncomingMessage} Request
- * @param {Request} req req
- * @param {string} name name
- * @returns {string | string[] | undefined} request header
- */
- function getRequestHeader(req, name) {
- // Pseudo API
- if (typeof req.getHeader === "function") {
- return req.getHeader(name);
- }
- return req.headers[name];
- }
- /**
- * @template {IncomingMessage & ExpectedIncomingMessage} Request
- * @param {Request} req req
- * @returns {string | undefined} request method
- */
- function getRequestMethod(req) {
- // Pseudo API
- if (typeof req.getMethod === "function") {
- return req.getMethod();
- }
- return req.method;
- }
- /**
- * @template {IncomingMessage & ExpectedIncomingMessage} Request
- * @param {Request} req req
- * @returns {string | undefined} request URL
- */
- function getRequestURL(req) {
- // Pseudo API
- if (typeof req.getURL === "function") {
- return req.getURL();
- }
- // Fastify decodes URI by default, our logic is based on encoded URI.
- // `req.url` may be modified by middleware (e.g. connect-history-api-fallback), in which case we use req.url instead.
- // `req.id` is a special property of `fastify`
- else if (req.id && req.originalUrl) {
- try {
- if (req.url === decodeURI(req.originalUrl)) {
- return req.originalUrl;
- }
- } catch {
- // decodeURI can throw on malformed sequences, fall through
- }
- }
- return req.url;
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {number} code code
- * @returns {void}
- */
- function setStatusCode(res, code) {
- // Pseudo API
- if (typeof res.setStatusCode === "function") {
- res.setStatusCode(code);
- return;
- }
- // Node.js API
- res.statusCode = code;
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @returns {number} status code
- */
- function getStatusCode(res) {
- // Pseudo API
- if (typeof res.getStatusCode === "function") {
- return res.getStatusCode();
- }
- return res.statusCode;
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {string} name name
- * @returns {string | string[] | undefined | number} header
- */
- function getResponseHeader(res, name) {
- // Real and Pseudo API
- return res.getHeader(name);
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {string} name name
- * @param {number | string | Readonly<string[]>} value value
- * @returns {Response} response
- */
- function setResponseHeader(res, name, value) {
- // Real and Pseudo API
- return res.setHeader(name, value);
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {string} name name
- * @returns {void}
- */
- function removeResponseHeader(res, name) {
- // Real and Pseudo API
- res.removeHeader(name);
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @returns {string[]} header names
- */
- function getResponseHeaders(res) {
- // Pseudo API
- if (typeof res.getResponseHeaders === "function") {
- return res.getResponseHeaders();
- }
- return res.getHeaderNames();
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @returns {boolean} true when headers were sent, otherwise false
- */
- function getHeadersSent(res) {
- // Pseudo API
- if (typeof res.getHeadersSent === "function") {
- return res.getHeadersSent();
- }
- return res.headersSent;
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {import("fs").ReadStream} bufferOrStream buffer or stream
- */
- function pipe(res, bufferOrStream) {
- // Pseudo API and Koa API
- if (typeof res.stream === "function") {
- // Writable stream into Readable stream
- res.stream(bufferOrStream);
- return;
- }
- // Node.js API and Express API and Hapi API
- bufferOrStream.pipe(res);
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {string | Buffer} bufferOrString buffer or string
- * @returns {void}
- */
- function send(res, bufferOrString) {
- // Pseudo API and Express API and Koa API
- if (typeof res.send === "function") {
- res.send(bufferOrString);
- return;
- }
- res.end(bufferOrString);
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {(string | Buffer)=} data data
- */
- function finish(res, data) {
- // Pseudo API and Express API and Koa API
- if (typeof res.finish === "function") {
- res.finish(data);
- return;
- }
- // Pseudo API and Express API and Koa API
- res.end(data);
- }
- /**
- * @param {string} filename filename
- * @param {OutputFileSystem} outputFileSystem output file system
- * @param {number} start start
- * @param {number} end end
- * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} result with buffer or stream and byte length
- */
- function createReadStreamOrReadFile(filename, outputFileSystem, start, end) {
- /** @type {string | Buffer | import("fs").ReadStream} */
- let bufferOrStream;
- /** @type {number} */
- let byteLength;
- // Stream logic
- const isFsSupportsStream = typeof outputFileSystem.createReadStream === "function";
- if (isFsSupportsStream) {
- bufferOrStream = /** @type {import("fs").createReadStream} */
- outputFileSystem.createReadStream(filename, {
- start,
- end
- });
- byteLength = end === 0 ? 0 : end - start + 1;
- } else {
- bufferOrStream = outputFileSystem.readFileSync(filename);
- ({
- byteLength
- } = bufferOrStream);
- byteLength = bufferOrStream.byteLength;
- }
- return {
- bufferOrStream,
- byteLength
- };
- }
- /**
- * @param {import("fs").ReadStream} stream stream
- * @param {boolean} suppress do need suppress?
- * @returns {void}
- */
- function destroyStream(stream, suppress) {
- if (stream.destroyed) {
- return;
- }
- stream.destroy();
- if (typeof stream.addListener === "function" && suppress) {
- stream.removeAllListeners("error");
- stream.addListener("error", () => {});
- }
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @returns {Response} res res
- */
- function getOutgoing(res) {
- // Pseudo API and Express API and Koa API
- if (typeof res.getOutgoing === "function") {
- return res.getOutgoing();
- }
- return res;
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- */
- function initState(res) {
- if (typeof res.setState === "function") {
- return;
- }
- // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
- res.locals ||= {};
- }
- /**
- * @template {ServerResponse & ExpectedServerResponse} Response
- * @param {Response} res res
- * @param {string} name name
- * @param {EXPECTED_ANY} value state
- * @returns {void}
- */
- function setState(res, name, value) {
- if (typeof res.setState === "function") {
- res.setState(name, value);
- return;
- }
- /** @type {Record<string, EXPECTED_ANY>} */
- res.locals[name] = value;
- }
- // Convert a Node.js `Readable` into a Web `ReadableStream` ourselves so we can
- // hand it to Hono in a form `@hono/node-server` fast-paths through
- // `responseViaCache` -> `writeFromReadableStream`. Avoids Node's internal
- // `Readable.toWeb` adapter, which races on late `error`/`close` events from
- // `fs.ReadStream` and throws "Invalid state: ReadableStream already closed"
- // (notably on Windows + Node 20). All controller transitions are guarded.
- // TODO remove this helper (and its use in honoWrapper) when the upstream race
- // is fixed and the minimum supported Node version no longer reproduces it:
- // - https://github.com/honojs/node-server/issues/233
- // - https://github.com/honojs/node-server/pull/299
- /**
- * @param {import("fs").ReadStream} stream node readable stream
- * @returns {ReadableStream<Uint8Array>} web readable stream
- */
- function nodeReadableToWebStream(stream) {
- // ReadableStream has been a global since Node 16.5 and is stable in practice
- // since Node 18; eslint-plugin-n flags it as experimental.
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
- return new ReadableStream({
- start(controller) {
- let closed = false;
- /** @type {() => void} */
- let cleanup;
- /**
- * @param {Buffer | string} chunk chunk
- */
- const onData = chunk => {
- if (closed) return;
- try {
- controller.enqueue(chunk instanceof Uint8Array ? chunk : Buffer.from(chunk));
- } catch {
- // Controller already closed/errored; nothing to do.
- }
- if (controller.desiredSize !== null && controller.desiredSize <= 0) {
- stream.pause();
- }
- };
- const onEnd = () => {
- if (closed) return;
- closed = true;
- cleanup();
- try {
- controller.close();
- } catch {
- // Already closed.
- }
- };
- /**
- * @param {Error} err err
- */
- const onError = err => {
- if (closed) return;
- closed = true;
- cleanup();
- try {
- controller.error(err);
- } catch {
- // Already closed/errored.
- }
- };
- cleanup = () => {
- stream.off("data", onData);
- stream.off("end", onEnd);
- stream.off("error", onError);
- };
- // Stream may have already finished by the time we wrap it (empty file
- // path resolved on the `end` event in `res.stream`).
- if (stream.readableEnded || stream.destroyed) {
- try {
- controller.close();
- } catch {
- // Already closed.
- }
- return;
- }
- stream.on("data", onData);
- stream.once("end", onEnd);
- stream.once("error", onError);
- },
- pull() {
- if (typeof stream.resume === "function") {
- stream.resume();
- }
- },
- cancel(reason) {
- if (typeof stream.destroy === "function") {
- stream.destroy(reason instanceof Error ? reason : undefined);
- }
- }
- });
- }
- module.exports = {
- createReadStreamOrReadFile,
- destroyStream,
- escapeHtml,
- etag,
- finish,
- getHeadersSent,
- getOutgoing,
- getRequestHeader,
- getRequestMethod,
- getRequestURL,
- getResponseHeader,
- getResponseHeaders,
- getStatusCode,
- getValueContentRangeHeader,
- initState,
- memorize,
- nodeReadableToWebStream,
- parseHttpDate,
- parseTokenList,
- pipe,
- removeResponseHeader,
- send,
- setResponseHeader,
- setState,
- setStatusCode
- };
|