compatibleAPI.js 6.0 KB

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