utils.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. "use strict";
  2. const crypto = require("node:crypto");
  3. /** @typedef {import("./index").IncomingMessage} IncomingMessage */
  4. /** @typedef {import("./index").ServerResponse} ServerResponse */
  5. /** @typedef {import("./index").OutputFileSystem} OutputFileSystem */
  6. /** @typedef {import("./index").EXPECTED_ANY} EXPECTED_ANY */
  7. const matchHtmlRegExp = /["'&<>]/;
  8. /**
  9. * @param {string} string raw HTML
  10. * @returns {string} escaped HTML
  11. */
  12. function escapeHtml(string) {
  13. const str = `${string}`;
  14. const match = matchHtmlRegExp.exec(str);
  15. if (!match) {
  16. return str;
  17. }
  18. let escape;
  19. let html = "";
  20. let index = 0;
  21. let lastIndex = 0;
  22. for ({
  23. index
  24. } = match; index < str.length; index++) {
  25. switch (str.charCodeAt(index)) {
  26. // "
  27. case 34:
  28. escape = "&quot;";
  29. break;
  30. // &
  31. case 38:
  32. escape = "&amp;";
  33. break;
  34. // '
  35. case 39:
  36. escape = "&#39;";
  37. break;
  38. // <
  39. case 60:
  40. escape = "&lt;";
  41. break;
  42. // >
  43. case 62:
  44. escape = "&gt;";
  45. break;
  46. default:
  47. continue;
  48. }
  49. if (lastIndex !== index) {
  50. // eslint-disable-next-line unicorn/prefer-string-slice
  51. html += str.substring(lastIndex, index);
  52. }
  53. lastIndex = index + 1;
  54. html += escape;
  55. }
  56. // eslint-disable-next-line unicorn/prefer-string-slice
  57. return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
  58. }
  59. /** @typedef {import("fs").Stats} Stats */
  60. /** @typedef {import("fs").ReadStream} ReadStream */
  61. /**
  62. * Parse an HTTP Date into a number.
  63. * @param {string} date date
  64. * @returns {number} timestamp
  65. */
  66. function parseHttpDate(date) {
  67. const timestamp = date && Date.parse(date);
  68. // istanbul ignore next: guard against date.js Date.parse patching
  69. return typeof timestamp === "number" ? timestamp : Number.NaN;
  70. }
  71. /**
  72. * @param {"bytes"} type type
  73. * @param {number} size size
  74. * @param {import("range-parser").Range=} range range
  75. * @returns {string} value of content range header
  76. */
  77. function getValueContentRangeHeader(type, size, range) {
  78. return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
  79. }
  80. /**
  81. * Generate a tag for a stat.
  82. * @param {Stats} stats stats
  83. * @returns {{ hash: string, buffer?: Buffer }} etag
  84. */
  85. function statTag(stats) {
  86. const mtime = stats.mtime.getTime().toString(16);
  87. const size = stats.size.toString(16);
  88. return {
  89. hash: `W/"${size}-${mtime}"`
  90. };
  91. }
  92. /**
  93. * Generate an entity tag.
  94. * @param {Buffer | ReadStream} entity entity
  95. * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
  96. */
  97. async function entityTag(entity) {
  98. const sha1 = crypto.createHash("sha1");
  99. if (!Buffer.isBuffer(entity)) {
  100. let byteLength = 0;
  101. /** @type {Buffer[]} */
  102. const buffers = [];
  103. await new Promise((resolve, reject) => {
  104. entity.on("data", chunk => {
  105. sha1.update(chunk);
  106. buffers.push(/** @type {Buffer} */chunk);
  107. byteLength += /** @type {Buffer} */chunk.byteLength;
  108. }).on("end", () => {
  109. resolve(sha1);
  110. }).on("error", reject);
  111. });
  112. return {
  113. buffer: Buffer.concat(buffers),
  114. hash: `"${byteLength.toString(16)}-${sha1.digest("base64").slice(0, 27)}"`
  115. };
  116. }
  117. if (entity.byteLength === 0) {
  118. // Fast-path empty
  119. return {
  120. hash: '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'
  121. };
  122. }
  123. // Compute hash of entity
  124. const hash = sha1.update(entity).digest("base64").slice(0, 27);
  125. // Compute length of entity
  126. const {
  127. byteLength
  128. } = entity;
  129. return {
  130. hash: `"${byteLength.toString(16)}-${hash}"`
  131. };
  132. }
  133. /**
  134. * Create a simple ETag.
  135. * @param {Buffer | ReadStream | Stats} entity entity
  136. * @returns {Promise<{ hash: string, buffer?: Buffer }>} etag
  137. */
  138. async function etag(entity) {
  139. const isStrong = Buffer.isBuffer(entity) || typeof (/** @type {ReadStream} */entity.pipe) === "function";
  140. return isStrong ? entityTag(/** @type {Buffer | ReadStream} */entity) : statTag(/** @type {import("fs").Stats} */entity);
  141. }
  142. const cacheStore = new WeakMap();
  143. /**
  144. * @template T
  145. * @typedef {(...args: EXPECTED_ANY) => T} FunctionReturning
  146. */
  147. /**
  148. * @template T
  149. * @param {FunctionReturning<T>} fn memorized function
  150. * @param {({ cache?: Map<string, { data: T }> } | undefined)=} cache cache
  151. * @param {((value: T) => T)=} callback callback
  152. * @returns {FunctionReturning<T>} new function
  153. */
  154. function memorize(fn, {
  155. cache = new Map()
  156. } = {}, callback = undefined) {
  157. /**
  158. * @param {EXPECTED_ANY[]} arguments_ args
  159. * @returns {EXPECTED_ANY} result
  160. */
  161. const memoized = (...arguments_) => {
  162. const [key] = arguments_;
  163. const cacheItem = cache.get(key);
  164. if (cacheItem) {
  165. return cacheItem.data;
  166. }
  167. // @ts-expect-error
  168. let result = fn.apply(this, arguments_);
  169. if (callback) {
  170. result = callback(result);
  171. }
  172. cache.set(key, {
  173. data: result
  174. });
  175. return result;
  176. };
  177. cacheStore.set(memoized, cache);
  178. return memoized;
  179. }
  180. /**
  181. * Parse a HTTP token list.
  182. * @param {string} str str
  183. * @returns {string[]} tokens
  184. */
  185. function parseTokenList(str) {
  186. let end = 0;
  187. let start = 0;
  188. const list = [];
  189. // gather tokens
  190. for (let i = 0, len = str.length; i < len; i++) {
  191. switch (str.charCodeAt(i)) {
  192. case 0x20 /* */:
  193. if (start === end) {
  194. end = i + 1;
  195. start = end;
  196. }
  197. break;
  198. case 0x2c /* , */:
  199. if (start !== end) {
  200. list.push(str.slice(start, end));
  201. }
  202. end = i + 1;
  203. start = end;
  204. break;
  205. default:
  206. end = i + 1;
  207. break;
  208. }
  209. }
  210. // final token
  211. if (start !== end) {
  212. list.push(str.slice(start, end));
  213. }
  214. return list;
  215. }
  216. /**
  217. * @typedef {object} ExpectedIncomingMessage
  218. * @property {((name: string) => string | string[] | undefined)=} getHeader get header extra method
  219. * @property {(() => string | undefined)=} getMethod get method extra method
  220. * @property {(() => string | undefined)=} getURL get URL extra method
  221. * @property {string=} originalUrl an extra option for `fastify` (and `@fastify/express`) to get original URL
  222. * @property {string=} id an extra option for `fastify` (and `@fastify/express`) to get ID of request
  223. */
  224. /**
  225. * @typedef {object} ExpectedServerResponse
  226. * @property {((status: number) => void)=} setStatusCode set status code
  227. * @property {(() => number)=} getStatusCode get status code
  228. * @property {((name: string) => string | string[] | undefined | number)} getHeader get header
  229. * @property {((name: string, value: number | string | Readonly<string[]>) => ExpectedServerResponse)=} setHeader set header
  230. * @property {((name: string) => void)=} removeHeader remove header
  231. * @property {((data: string | Buffer) => void)=} send send
  232. * @property {((data?: string | Buffer) => void)=} finish finish
  233. * @property {(() => string[])=} getResponseHeaders get response header
  234. * @property {(() => boolean)=} getHeadersSent get headers sent
  235. * @property {((data: EXPECTED_ANY) => void)=} stream stream
  236. * @property {(() => EXPECTED_ANY)=} getOutgoing get outgoing
  237. * @property {((name: string, value: EXPECTED_ANY) => void)=} setState set state
  238. */
  239. /**
  240. * @template {IncomingMessage & ExpectedIncomingMessage} Request
  241. * @param {Request} req req
  242. * @param {string} name name
  243. * @returns {string | string[] | undefined} request header
  244. */
  245. function getRequestHeader(req, name) {
  246. // Pseudo API
  247. if (typeof req.getHeader === "function") {
  248. return req.getHeader(name);
  249. }
  250. return req.headers[name];
  251. }
  252. /**
  253. * @template {IncomingMessage & ExpectedIncomingMessage} Request
  254. * @param {Request} req req
  255. * @returns {string | undefined} request method
  256. */
  257. function getRequestMethod(req) {
  258. // Pseudo API
  259. if (typeof req.getMethod === "function") {
  260. return req.getMethod();
  261. }
  262. return req.method;
  263. }
  264. /**
  265. * @template {IncomingMessage & ExpectedIncomingMessage} Request
  266. * @param {Request} req req
  267. * @returns {string | undefined} request URL
  268. */
  269. function getRequestURL(req) {
  270. // Pseudo API
  271. if (typeof req.getURL === "function") {
  272. return req.getURL();
  273. }
  274. // Fastify decodes URI by default, our logic is based on encoded URI.
  275. // `req.url` may be modified by middleware (e.g. connect-history-api-fallback), in which case we use req.url instead.
  276. // `req.id` is a special property of `fastify`
  277. else if (req.id && req.originalUrl) {
  278. try {
  279. if (req.url === decodeURI(req.originalUrl)) {
  280. return req.originalUrl;
  281. }
  282. } catch {
  283. // decodeURI can throw on malformed sequences, fall through
  284. }
  285. }
  286. return req.url;
  287. }
  288. /**
  289. * @template {ServerResponse & ExpectedServerResponse} Response
  290. * @param {Response} res res
  291. * @param {number} code code
  292. * @returns {void}
  293. */
  294. function setStatusCode(res, code) {
  295. // Pseudo API
  296. if (typeof res.setStatusCode === "function") {
  297. res.setStatusCode(code);
  298. return;
  299. }
  300. // Node.js API
  301. res.statusCode = code;
  302. }
  303. /**
  304. * @template {ServerResponse & ExpectedServerResponse} Response
  305. * @param {Response} res res
  306. * @returns {number} status code
  307. */
  308. function getStatusCode(res) {
  309. // Pseudo API
  310. if (typeof res.getStatusCode === "function") {
  311. return res.getStatusCode();
  312. }
  313. return res.statusCode;
  314. }
  315. /**
  316. * @template {ServerResponse & ExpectedServerResponse} Response
  317. * @param {Response} res res
  318. * @param {string} name name
  319. * @returns {string | string[] | undefined | number} header
  320. */
  321. function getResponseHeader(res, name) {
  322. // Real and Pseudo API
  323. return res.getHeader(name);
  324. }
  325. /**
  326. * @template {ServerResponse & ExpectedServerResponse} Response
  327. * @param {Response} res res
  328. * @param {string} name name
  329. * @param {number | string | Readonly<string[]>} value value
  330. * @returns {Response} response
  331. */
  332. function setResponseHeader(res, name, value) {
  333. // Real and Pseudo API
  334. return res.setHeader(name, value);
  335. }
  336. /**
  337. * @template {ServerResponse & ExpectedServerResponse} Response
  338. * @param {Response} res res
  339. * @param {string} name name
  340. * @returns {void}
  341. */
  342. function removeResponseHeader(res, name) {
  343. // Real and Pseudo API
  344. res.removeHeader(name);
  345. }
  346. /**
  347. * @template {ServerResponse & ExpectedServerResponse} Response
  348. * @param {Response} res res
  349. * @returns {string[]} header names
  350. */
  351. function getResponseHeaders(res) {
  352. // Pseudo API
  353. if (typeof res.getResponseHeaders === "function") {
  354. return res.getResponseHeaders();
  355. }
  356. return res.getHeaderNames();
  357. }
  358. /**
  359. * @template {ServerResponse & ExpectedServerResponse} Response
  360. * @param {Response} res res
  361. * @returns {boolean} true when headers were sent, otherwise false
  362. */
  363. function getHeadersSent(res) {
  364. // Pseudo API
  365. if (typeof res.getHeadersSent === "function") {
  366. return res.getHeadersSent();
  367. }
  368. return res.headersSent;
  369. }
  370. /**
  371. * @template {ServerResponse & ExpectedServerResponse} Response
  372. * @param {Response} res res
  373. * @param {import("fs").ReadStream} bufferOrStream buffer or stream
  374. */
  375. function pipe(res, bufferOrStream) {
  376. // Pseudo API and Koa API
  377. if (typeof res.stream === "function") {
  378. // Writable stream into Readable stream
  379. res.stream(bufferOrStream);
  380. return;
  381. }
  382. // Node.js API and Express API and Hapi API
  383. bufferOrStream.pipe(res);
  384. }
  385. /**
  386. * @template {ServerResponse & ExpectedServerResponse} Response
  387. * @param {Response} res res
  388. * @param {string | Buffer} bufferOrString buffer or string
  389. * @returns {void}
  390. */
  391. function send(res, bufferOrString) {
  392. // Pseudo API and Express API and Koa API
  393. if (typeof res.send === "function") {
  394. res.send(bufferOrString);
  395. return;
  396. }
  397. res.end(bufferOrString);
  398. }
  399. /**
  400. * @template {ServerResponse & ExpectedServerResponse} Response
  401. * @param {Response} res res
  402. * @param {(string | Buffer)=} data data
  403. */
  404. function finish(res, data) {
  405. // Pseudo API and Express API and Koa API
  406. if (typeof res.finish === "function") {
  407. res.finish(data);
  408. return;
  409. }
  410. // Pseudo API and Express API and Koa API
  411. res.end(data);
  412. }
  413. /**
  414. * @param {string} filename filename
  415. * @param {OutputFileSystem} outputFileSystem output file system
  416. * @param {number} start start
  417. * @param {number} end end
  418. * @returns {{ bufferOrStream: (Buffer | import("fs").ReadStream), byteLength: number }} result with buffer or stream and byte length
  419. */
  420. function createReadStreamOrReadFile(filename, outputFileSystem, start, end) {
  421. /** @type {string | Buffer | import("fs").ReadStream} */
  422. let bufferOrStream;
  423. /** @type {number} */
  424. let byteLength;
  425. // Stream logic
  426. const isFsSupportsStream = typeof outputFileSystem.createReadStream === "function";
  427. if (isFsSupportsStream) {
  428. bufferOrStream = /** @type {import("fs").createReadStream} */
  429. outputFileSystem.createReadStream(filename, {
  430. start,
  431. end
  432. });
  433. byteLength = end === 0 ? 0 : end - start + 1;
  434. } else {
  435. bufferOrStream = outputFileSystem.readFileSync(filename);
  436. ({
  437. byteLength
  438. } = bufferOrStream);
  439. byteLength = bufferOrStream.byteLength;
  440. }
  441. return {
  442. bufferOrStream,
  443. byteLength
  444. };
  445. }
  446. /**
  447. * @param {import("fs").ReadStream} stream stream
  448. * @param {boolean} suppress do need suppress?
  449. * @returns {void}
  450. */
  451. function destroyStream(stream, suppress) {
  452. if (stream.destroyed) {
  453. return;
  454. }
  455. stream.destroy();
  456. if (typeof stream.addListener === "function" && suppress) {
  457. stream.removeAllListeners("error");
  458. stream.addListener("error", () => {});
  459. }
  460. }
  461. /**
  462. * @template {ServerResponse & ExpectedServerResponse} Response
  463. * @param {Response} res res
  464. * @returns {Response} res res
  465. */
  466. function getOutgoing(res) {
  467. // Pseudo API and Express API and Koa API
  468. if (typeof res.getOutgoing === "function") {
  469. return res.getOutgoing();
  470. }
  471. return res;
  472. }
  473. /**
  474. * @template {ServerResponse & ExpectedServerResponse} Response
  475. * @param {Response} res res
  476. */
  477. function initState(res) {
  478. if (typeof res.setState === "function") {
  479. return;
  480. }
  481. // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
  482. res.locals ||= {};
  483. }
  484. /**
  485. * @template {ServerResponse & ExpectedServerResponse} Response
  486. * @param {Response} res res
  487. * @param {string} name name
  488. * @param {EXPECTED_ANY} value state
  489. * @returns {void}
  490. */
  491. function setState(res, name, value) {
  492. if (typeof res.setState === "function") {
  493. res.setState(name, value);
  494. return;
  495. }
  496. /** @type {Record<string, EXPECTED_ANY>} */
  497. res.locals[name] = value;
  498. }
  499. // Convert a Node.js `Readable` into a Web `ReadableStream` ourselves so we can
  500. // hand it to Hono in a form `@hono/node-server` fast-paths through
  501. // `responseViaCache` -> `writeFromReadableStream`. Avoids Node's internal
  502. // `Readable.toWeb` adapter, which races on late `error`/`close` events from
  503. // `fs.ReadStream` and throws "Invalid state: ReadableStream already closed"
  504. // (notably on Windows + Node 20). All controller transitions are guarded.
  505. // TODO remove this helper (and its use in honoWrapper) when the upstream race
  506. // is fixed and the minimum supported Node version no longer reproduces it:
  507. // - https://github.com/honojs/node-server/issues/233
  508. // - https://github.com/honojs/node-server/pull/299
  509. /**
  510. * @param {import("fs").ReadStream} stream node readable stream
  511. * @returns {ReadableStream<Uint8Array>} web readable stream
  512. */
  513. function nodeReadableToWebStream(stream) {
  514. // ReadableStream has been a global since Node 16.5 and is stable in practice
  515. // since Node 18; eslint-plugin-n flags it as experimental.
  516. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  517. return new ReadableStream({
  518. start(controller) {
  519. let closed = false;
  520. /** @type {() => void} */
  521. let cleanup;
  522. /**
  523. * @param {Buffer | string} chunk chunk
  524. */
  525. const onData = chunk => {
  526. if (closed) return;
  527. try {
  528. controller.enqueue(chunk instanceof Uint8Array ? chunk : Buffer.from(chunk));
  529. } catch {
  530. // Controller already closed/errored; nothing to do.
  531. }
  532. if (controller.desiredSize !== null && controller.desiredSize <= 0) {
  533. stream.pause();
  534. }
  535. };
  536. const onEnd = () => {
  537. if (closed) return;
  538. closed = true;
  539. cleanup();
  540. try {
  541. controller.close();
  542. } catch {
  543. // Already closed.
  544. }
  545. };
  546. /**
  547. * @param {Error} err err
  548. */
  549. const onError = err => {
  550. if (closed) return;
  551. closed = true;
  552. cleanup();
  553. try {
  554. controller.error(err);
  555. } catch {
  556. // Already closed/errored.
  557. }
  558. };
  559. cleanup = () => {
  560. stream.off("data", onData);
  561. stream.off("end", onEnd);
  562. stream.off("error", onError);
  563. };
  564. // Stream may have already finished by the time we wrap it (empty file
  565. // path resolved on the `end` event in `res.stream`).
  566. if (stream.readableEnded || stream.destroyed) {
  567. try {
  568. controller.close();
  569. } catch {
  570. // Already closed.
  571. }
  572. return;
  573. }
  574. stream.on("data", onData);
  575. stream.once("end", onEnd);
  576. stream.once("error", onError);
  577. },
  578. pull() {
  579. if (typeof stream.resume === "function") {
  580. stream.resume();
  581. }
  582. },
  583. cancel(reason) {
  584. if (typeof stream.destroy === "function") {
  585. stream.destroy(reason instanceof Error ? reason : undefined);
  586. }
  587. }
  588. });
  589. }
  590. module.exports = {
  591. createReadStreamOrReadFile,
  592. destroyStream,
  593. escapeHtml,
  594. etag,
  595. finish,
  596. getHeadersSent,
  597. getOutgoing,
  598. getRequestHeader,
  599. getRequestMethod,
  600. getRequestURL,
  601. getResponseHeader,
  602. getResponseHeaders,
  603. getStatusCode,
  604. getValueContentRangeHeader,
  605. initState,
  606. memorize,
  607. nodeReadableToWebStream,
  608. parseHttpDate,
  609. parseTokenList,
  610. pipe,
  611. removeResponseHeader,
  612. send,
  613. setResponseHeader,
  614. setState,
  615. setStatusCode
  616. };