index.cjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
  9. var __export = (target, all) => {
  10. for (var name in all)
  11. __defProp(target, name, { get: all[name], enumerable: true });
  12. };
  13. var __copyProps = (to, from, except, desc) => {
  14. if (from && typeof from === "object" || typeof from === "function") {
  15. for (let key of __getOwnPropNames(from))
  16. if (!__hasOwnProp.call(to, key) && key !== except)
  17. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  18. }
  19. return to;
  20. };
  21. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  22. // If the importer is in node compatibility mode or this is not an ESM
  23. // file that has been converted to a CommonJS file using a Babel-
  24. // compatible transform (i.e. "__esModule" has not been set), then set
  25. // "default" to the CommonJS "module.exports" for node compatibility.
  26. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  27. mod
  28. ));
  29. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  30. // src/index.js
  31. var index_exports = {};
  32. __export(index_exports, {
  33. HttpStaticRoute: () => HttpStaticRoute,
  34. HttpStaticRouter: () => HttpStaticRouter
  35. });
  36. module.exports = __toCommonJS(index_exports);
  37. // src/http-static-route.js
  38. var import_js_format = require("@e22m4u/js-format");
  39. var _HttpStaticRoute = class _HttpStaticRoute {
  40. /**
  41. * Remote path.
  42. *
  43. * @type {string}
  44. */
  45. remotePath;
  46. /**
  47. * Resource path.
  48. *
  49. * @type {string}
  50. */
  51. resourcePath;
  52. /**
  53. * RegExp.
  54. *
  55. * @type {RegExp}
  56. */
  57. regexp;
  58. /**
  59. * Is file.
  60. *
  61. * @type {boolean}
  62. */
  63. isFile;
  64. /**
  65. * Constructor.
  66. *
  67. * @param {string} remotePath
  68. * @param {string} resourcePath
  69. * @param {RegExp} regexp
  70. * @param {boolean} isFile
  71. */
  72. constructor(remotePath, resourcePath, regexp, isFile) {
  73. if (typeof remotePath !== "string") {
  74. throw new import_js_format.InvalidArgumentError(
  75. 'Parameter "remotePath" must be a String, but %v was given.',
  76. remotePath
  77. );
  78. }
  79. if (typeof resourcePath !== "string") {
  80. throw new import_js_format.InvalidArgumentError(
  81. 'Parameter "resourcePath" must be a String, but %v was given.',
  82. resourcePath
  83. );
  84. }
  85. if (!(regexp instanceof RegExp)) {
  86. throw new import_js_format.InvalidArgumentError(
  87. 'Parameter "regexp" must be an instance of RegExp, but %v was given.',
  88. regexp
  89. );
  90. }
  91. if (typeof isFile !== "boolean") {
  92. throw new import_js_format.InvalidArgumentError(
  93. 'Parameter "isFile" must be a String, but %v was given.',
  94. isFile
  95. );
  96. }
  97. this.remotePath = remotePath;
  98. this.resourcePath = resourcePath;
  99. this.regexp = regexp;
  100. this.isFile = isFile;
  101. }
  102. };
  103. __name(_HttpStaticRoute, "HttpStaticRoute");
  104. var HttpStaticRoute = _HttpStaticRoute;
  105. // src/http-static-router.js
  106. var import_path = __toESM(require("path"), 1);
  107. var import_mime_types = __toESM(require("mime-types"), 1);
  108. var import_fs = __toESM(require("fs"), 1);
  109. var import_http = require("http");
  110. var import_js_format2 = require("@e22m4u/js-format");
  111. // src/utils/escape-regexp.js
  112. function escapeRegexp(input) {
  113. return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  114. }
  115. __name(escapeRegexp, "escapeRegexp");
  116. // src/utils/normalize-path.js
  117. function normalizePath(value, noStartingSlash = false) {
  118. if (typeof value !== "string") {
  119. return "/";
  120. }
  121. const res = value.trim().replace(/\/+/g, "/").replace(/(^\/|\/$)/g, "");
  122. return noStartingSlash ? res : "/" + res;
  123. }
  124. __name(normalizePath, "normalizePath");
  125. // src/http-static-router.js
  126. var import_js_service = require("@e22m4u/js-service");
  127. var _HttpStaticRouter = class _HttpStaticRouter extends import_js_service.DebuggableService {
  128. /**
  129. * Routes.
  130. *
  131. * @protected
  132. * @type {HttpStaticRoute[]}
  133. */
  134. _routes = [];
  135. /**
  136. * Options.
  137. *
  138. * @type {object}
  139. */
  140. _options = {};
  141. /**
  142. * Constructor.
  143. *
  144. * @param {object} options
  145. */
  146. constructor(options = {}) {
  147. if ((0, import_js_service.isServiceContainer)(options)) {
  148. options = {};
  149. }
  150. super(void 0, {
  151. noEnvironmentNamespace: true,
  152. namespace: "jsHttpStaticRouter"
  153. });
  154. if (!options || typeof options !== "object" || Array.isArray(options)) {
  155. throw new import_js_format2.InvalidArgumentError(
  156. 'Parameter "options" must be an Object, but %v was given.',
  157. options
  158. );
  159. }
  160. if (options.trailingSlash !== void 0) {
  161. if (typeof options.trailingSlash !== "boolean") {
  162. throw new import_js_format2.InvalidArgumentError(
  163. 'Option "trailingSlash" must be a Boolean, but %v was given.',
  164. options.trailingSlash
  165. );
  166. }
  167. }
  168. this._options = { ...options };
  169. }
  170. /**
  171. * Add route.
  172. *
  173. * @param {string} remotePath
  174. * @param {string} resourcePath
  175. * @returns {object}
  176. */
  177. addRoute(remotePath, resourcePath) {
  178. if (typeof remotePath !== "string") {
  179. throw new import_js_format2.InvalidArgumentError(
  180. "Remote path must be a String, but %v was given.",
  181. remotePath
  182. );
  183. }
  184. if (typeof resourcePath !== "string") {
  185. throw new import_js_format2.InvalidArgumentError(
  186. "Resource path must be a String, but %v was given.",
  187. resourcePath
  188. );
  189. }
  190. const debug = this.getDebuggerFor(this.addRoute);
  191. resourcePath = import_path.default.resolve(resourcePath);
  192. debug("Adding a new route.");
  193. debug("Resource path is %v.", resourcePath);
  194. debug("Remote path is %v.", remotePath);
  195. let stats;
  196. try {
  197. stats = import_fs.default.statSync(resourcePath);
  198. } catch (error) {
  199. console.error(error);
  200. throw new import_js_format2.InvalidArgumentError(
  201. "Resource path %v does not exist.",
  202. resourcePath
  203. );
  204. }
  205. const isFile = stats.isFile();
  206. debug("Resource type is %s.", isFile ? "File" : "Folder");
  207. const normalizedRemotePath = normalizePath(remotePath);
  208. const escapedRemotePath = escapeRegexp(normalizedRemotePath);
  209. const regexp = isFile ? new RegExp(`^${escapedRemotePath}/*$`) : new RegExp(`^${escapedRemotePath}(?:$|\\/)`);
  210. const route = new HttpStaticRoute(remotePath, resourcePath, regexp, isFile);
  211. this._routes.push(route);
  212. this._routes.sort((a, b) => b.remotePath.length - a.remotePath.length);
  213. return this;
  214. }
  215. /**
  216. * Match route.
  217. *
  218. * @param {IncomingMessage} req
  219. * @returns {object|undefined}
  220. */
  221. matchRoute(req) {
  222. if (!(req instanceof import_http.IncomingMessage)) {
  223. throw new import_js_format2.InvalidArgumentError(
  224. 'Parameter "req" must be an instance of IncomingMessage, but %v was given.',
  225. req
  226. );
  227. }
  228. const debug = this.getDebuggerFor(this.matchRoute);
  229. debug("Matching routes with incoming request.");
  230. const url = (req.url || "/").replace(/\?.*$/, "");
  231. debug("Incoming request is %s %v.", req.method, url);
  232. if (req.method !== "GET" && req.method !== "HEAD") {
  233. debug("Method not allowed.");
  234. return;
  235. }
  236. debug("Walking through %v routes.", this._routes.length);
  237. const route = this._routes.find((route2) => {
  238. const res = route2.regexp.test(url);
  239. const phrase = res ? "matched" : "not matched";
  240. debug("Resource %v %s.", route2.resourcePath, phrase);
  241. return res;
  242. });
  243. route ? debug("Resource %v matched.", route.resourcePath) : debug("No route matched.");
  244. return route;
  245. }
  246. /**
  247. * Send file by route.
  248. *
  249. * @param {HttpStaticRoute} route
  250. * @param {import('http').IncomingMessage} req
  251. * @param {import('http').ServerResponse} res
  252. */
  253. sendFileByRoute(route, req, res) {
  254. if (!(route instanceof HttpStaticRoute)) {
  255. throw new import_js_format2.InvalidArgumentError(
  256. 'Parameter "route" must be an instance of HttpStaticRoute, but %v was given.',
  257. route
  258. );
  259. }
  260. if (!(req instanceof import_http.IncomingMessage)) {
  261. throw new import_js_format2.InvalidArgumentError(
  262. 'Parameter "req" must be an instance of IncomingMessage, but %v was given.',
  263. req
  264. );
  265. }
  266. if (!(res instanceof import_http.ServerResponse)) {
  267. throw new import_js_format2.InvalidArgumentError(
  268. 'Parameter "res" must be an instance of ServerResponse, but %v was given.',
  269. res
  270. );
  271. }
  272. const reqUrl = req.url || "/";
  273. const reqPath = reqUrl.replace(/\?.*$/, "");
  274. if (!this._options.trailingSlash && reqPath !== "/" && /\/$/.test(reqPath)) {
  275. const searchMatch = reqUrl.match(/\?.*$/);
  276. const search = searchMatch ? searchMatch[0] : "";
  277. const normalizedPath = reqPath.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
  278. res.writeHead(302, { location: `${normalizedPath}${search}` });
  279. res.end();
  280. return;
  281. }
  282. if (/\/{2,}/.test(reqUrl)) {
  283. const normalizedUrl = reqUrl.replace(/\/{2,}/g, "/");
  284. res.writeHead(302, { location: normalizedUrl });
  285. res.end();
  286. return;
  287. }
  288. let targetPath = route.resourcePath;
  289. if (!route.isFile) {
  290. const relativePath = reqPath.replace(route.regexp, "");
  291. targetPath = import_path.default.join(route.resourcePath, relativePath);
  292. }
  293. targetPath = import_path.default.resolve(targetPath);
  294. const resourceRoot = import_path.default.resolve(route.resourcePath);
  295. if (!targetPath.startsWith(resourceRoot)) {
  296. res.writeHead(403, { "content-type": "text/plain" });
  297. res.end("403 Forbidden");
  298. return;
  299. }
  300. import_fs.default.stat(targetPath, (statsError, stats) => {
  301. if (statsError) {
  302. return this._handleFsError(statsError, res);
  303. }
  304. if (stats.isDirectory()) {
  305. if (this._options.trailingSlash) {
  306. if (/[^/]$/.test(reqPath)) {
  307. const searchMatch = reqUrl.match(/\?.*$/);
  308. const search = searchMatch ? searchMatch[0] : "";
  309. const normalizedPath = reqPath.replace(/\/{2,}/g, "/");
  310. res.writeHead(302, { location: `${normalizedPath}/${search}` });
  311. res.end();
  312. return;
  313. }
  314. }
  315. targetPath = import_path.default.join(targetPath, "index.html");
  316. } else {
  317. if (reqPath !== "/" && /\/$/.test(reqPath)) {
  318. const searchMatch = reqUrl.match(/\?.*$/);
  319. const search = searchMatch ? searchMatch[0] : "";
  320. const normalizedPath = reqPath.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
  321. res.writeHead(302, { location: `${normalizedPath}${search}` });
  322. res.end();
  323. return;
  324. }
  325. }
  326. const extname = import_path.default.extname(targetPath);
  327. const contentType = import_mime_types.default.contentType(extname) || "application/octet-stream";
  328. const fileStream = (0, import_fs.createReadStream)(targetPath);
  329. fileStream.on("error", (error) => {
  330. this._handleFsError(error, res);
  331. });
  332. fileStream.on("open", () => {
  333. res.writeHead(200, { "content-type": contentType });
  334. if (req.method === "HEAD") {
  335. res.end();
  336. return;
  337. }
  338. fileStream.pipe(res);
  339. });
  340. req.on("close", () => {
  341. fileStream.destroy();
  342. });
  343. });
  344. }
  345. /**
  346. * Handle filesystem error.
  347. *
  348. * @param {object} error
  349. * @param {object} res
  350. * @returns {undefined}
  351. */
  352. _handleFsError(error, res) {
  353. if (res.headersSent) {
  354. return;
  355. }
  356. if ("code" in error && error.code === "ENOENT") {
  357. res.writeHead(404, { "content-type": "text/plain" });
  358. res.write("404 Not Found");
  359. res.end();
  360. } else {
  361. res.writeHead(500, { "content-type": "text/plain" });
  362. res.write("500 Internal Server Error");
  363. res.end();
  364. }
  365. }
  366. };
  367. __name(_HttpStaticRouter, "HttpStaticRouter");
  368. var HttpStaticRouter = _HttpStaticRouter;
  369. // Annotate the CommonJS export names for ESM import in node:
  370. 0 && (module.exports = {
  371. HttpStaticRoute,
  372. HttpStaticRouter
  373. });