|
@@ -0,0 +1,218 @@
|
|
|
|
|
+"use strict";
|
|
|
|
|
+var __create = Object.create;
|
|
|
|
|
+var __defProp = Object.defineProperty;
|
|
|
|
|
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
|
|
+var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
|
|
+var __getProtoOf = Object.getPrototypeOf;
|
|
|
|
|
+var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
|
|
+var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
|
|
|
+var __export = (target, all) => {
|
|
|
|
|
+ for (var name in all)
|
|
|
|
|
+ __defProp(target, name, { get: all[name], enumerable: true });
|
|
|
|
|
+};
|
|
|
|
|
+var __copyProps = (to, from, except, desc) => {
|
|
|
|
|
+ if (from && typeof from === "object" || typeof from === "function") {
|
|
|
|
|
+ for (let key of __getOwnPropNames(from))
|
|
|
|
|
+ if (!__hasOwnProp.call(to, key) && key !== except)
|
|
|
|
|
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
|
|
|
+ }
|
|
|
|
|
+ return to;
|
|
|
|
|
+};
|
|
|
|
|
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
|
|
|
+ // If the importer is in node compatibility mode or this is not an ESM
|
|
|
|
|
+ // file that has been converted to a CommonJS file using a Babel-
|
|
|
|
|
+ // compatible transform (i.e. "__esModule" has not been set), then set
|
|
|
|
|
+ // "default" to the CommonJS "module.exports" for node compatibility.
|
|
|
|
|
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
|
|
|
+ mod
|
|
|
|
|
+));
|
|
|
|
|
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
|
+
|
|
|
|
|
+// src/index.js
|
|
|
|
|
+var index_exports = {};
|
|
|
|
|
+__export(index_exports, {
|
|
|
|
|
+ HttpStaticRouter: () => HttpStaticRouter
|
|
|
|
|
+});
|
|
|
|
|
+module.exports = __toCommonJS(index_exports);
|
|
|
|
|
+
|
|
|
|
|
+// src/http-static-router.js
|
|
|
|
|
+var import_path = __toESM(require("path"), 1);
|
|
|
|
|
+var import_mime_types = __toESM(require("mime-types"), 1);
|
|
|
|
|
+var import_fs = __toESM(require("fs"), 1);
|
|
|
|
|
+
|
|
|
|
|
+// src/utils/escape-regexp.js
|
|
|
|
|
+function escapeRegexp(input) {
|
|
|
|
|
+ return String(input).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
|
+}
|
|
|
|
|
+__name(escapeRegexp, "escapeRegexp");
|
|
|
|
|
+
|
|
|
|
|
+// src/utils/normalize-path.js
|
|
|
|
|
+function normalizePath(value, noStartingSlash = false) {
|
|
|
|
|
+ if (typeof value !== "string") {
|
|
|
|
|
+ return "/";
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = value.trim().replace(/\/+/g, "/").replace(/(^\/|\/$)/g, "");
|
|
|
|
|
+ return noStartingSlash ? res : "/" + res;
|
|
|
|
|
+}
|
|
|
|
|
+__name(normalizePath, "normalizePath");
|
|
|
|
|
+
|
|
|
|
|
+// src/http-static-router.js
|
|
|
|
|
+var import_js_service = require("@e22m4u/js-service");
|
|
|
|
|
+var import_js_format = require("@e22m4u/js-format");
|
|
|
|
|
+var _HttpStaticRouter = class _HttpStaticRouter extends import_js_service.DebuggableService {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Routes.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @protected
|
|
|
|
|
+ */
|
|
|
|
|
+ _routes = [];
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Constructor.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {import('@e22m4u/js-service').ServiceContainer} container
|
|
|
|
|
+ */
|
|
|
|
|
+ constructor(container) {
|
|
|
|
|
+ super(container, {
|
|
|
|
|
+ noEnvironmentNamespace: true,
|
|
|
|
|
+ namespace: "jsHttpStaticRouter"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Add route.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string} remotePath
|
|
|
|
|
+ * @param {string} resourcePath
|
|
|
|
|
+ * @returns {object}
|
|
|
|
|
+ */
|
|
|
|
|
+ addRoute(remotePath, resourcePath) {
|
|
|
|
|
+ const debug = this.getDebuggerFor(this.addRoute);
|
|
|
|
|
+ resourcePath = import_path.default.resolve(resourcePath);
|
|
|
|
|
+ debug("Adding a new route.");
|
|
|
|
|
+ debug("Resource path is %v.", resourcePath);
|
|
|
|
|
+ debug("Remote path is %v.", remotePath);
|
|
|
|
|
+ let stats;
|
|
|
|
|
+ try {
|
|
|
|
|
+ stats = import_fs.default.statSync(resourcePath);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(error);
|
|
|
|
|
+ throw new import_js_format.InvalidArgumentError(
|
|
|
|
|
+ "Static resource path does not exist %v.",
|
|
|
|
|
+ resourcePath
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ const isFile = stats.isFile();
|
|
|
|
|
+ debug("Resource type is %s.", isFile ? "File" : "Folder");
|
|
|
|
|
+ const normalizedRemotePath = normalizePath(remotePath);
|
|
|
|
|
+ const escapedRemotePath = escapeRegexp(normalizedRemotePath);
|
|
|
|
|
+ const regexp = isFile ? new RegExp(`^${escapedRemotePath}$`) : new RegExp(`^${escapedRemotePath}(?:$|\\/)`);
|
|
|
|
|
+ const route = { remotePath, resourcePath, regexp, isFile };
|
|
|
|
|
+ this._routes.push(route);
|
|
|
|
|
+ this._routes.sort((a, b) => b.remotePath.length - a.remotePath.length);
|
|
|
|
|
+ return this;
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Match route.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {import('http').IncomingMessage} req
|
|
|
|
|
+ * @returns {object|undefined}
|
|
|
|
|
+ */
|
|
|
|
|
+ matchRoute(req) {
|
|
|
|
|
+ const debug = this.getDebuggerFor(this.matchRoute);
|
|
|
|
|
+ debug("Matching routes with incoming request.");
|
|
|
|
|
+ const url = (req.url || "/").replace(/\?.*$/, "");
|
|
|
|
|
+ debug("Incoming request is %s %v.", req.method, url);
|
|
|
|
|
+ if (req.method !== "GET" && req.method !== "HEAD") {
|
|
|
|
|
+ debug("Method not allowed.");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ debug("Walking through %v routes.", this._routes.length);
|
|
|
|
|
+ const route = this._routes.find((route2) => {
|
|
|
|
|
+ const res = route2.regexp.test(url);
|
|
|
|
|
+ const phrase = res ? "matched" : "not matched";
|
|
|
|
|
+ debug("Resource %v %s.", route2.resourcePath, phrase);
|
|
|
|
|
+ return res;
|
|
|
|
|
+ });
|
|
|
|
|
+ route ? debug("Resource %v matched.", route.resourcePath) : debug("No route matched.");
|
|
|
|
|
+ return route;
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Send file by route.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {object} route
|
|
|
|
|
+ * @param {import('http').IncomingMessage} req
|
|
|
|
|
+ * @param {import('http').ServerResponse} res
|
|
|
|
|
+ */
|
|
|
|
|
+ sendFileByRoute(route, req, res) {
|
|
|
|
|
+ const reqUrl = req.url || "/";
|
|
|
|
|
+ const reqPath = reqUrl.replace(/\?.*$/, "");
|
|
|
|
|
+ let targetPath = route.resourcePath;
|
|
|
|
|
+ if (!route.isFile) {
|
|
|
|
|
+ const relativePath = reqPath.replace(route.regexp, "");
|
|
|
|
|
+ targetPath = import_path.default.join(route.resourcePath, relativePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ targetPath = import_path.default.resolve(targetPath);
|
|
|
|
|
+ const resourceRoot = import_path.default.resolve(route.resourcePath);
|
|
|
|
|
+ if (!targetPath.startsWith(resourceRoot)) {
|
|
|
|
|
+ res.writeHead(403, { "content-type": "text/plain" });
|
|
|
|
|
+ res.end("403 Forbidden");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ import_fs.default.stat(targetPath, (statsError, stats) => {
|
|
|
|
|
+ if (statsError) {
|
|
|
|
|
+ return _handleFsError(statsError, res);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (stats.isDirectory()) {
|
|
|
|
|
+ if (/[^/]$/.test(reqPath)) {
|
|
|
|
|
+ const searchMatch = reqUrl.match(/\?.*$/);
|
|
|
|
|
+ const search = searchMatch ? searchMatch[0] : "";
|
|
|
|
|
+ const normalizedPath = reqUrl.replace(/\/{2,}/g, "/");
|
|
|
|
|
+ res.writeHead(302, { location: `${normalizedPath}/${search}` });
|
|
|
|
|
+ res.end();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/\/{2,}/.test(reqUrl)) {
|
|
|
|
|
+ const normalizedUrl = reqUrl.replace(/\/{2,}/g, "/");
|
|
|
|
|
+ res.writeHead(302, { location: normalizedUrl });
|
|
|
|
|
+ res.end();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ targetPath = import_path.default.join(targetPath, "index.html");
|
|
|
|
|
+ }
|
|
|
|
|
+ const extname = import_path.default.extname(targetPath);
|
|
|
|
|
+ const contentType = import_mime_types.default.contentType(extname) || "application/octet-stream";
|
|
|
|
|
+ const fileStream = (0, import_fs.createReadStream)(targetPath);
|
|
|
|
|
+ fileStream.on("error", (error) => {
|
|
|
|
|
+ _handleFsError(error, res);
|
|
|
|
|
+ });
|
|
|
|
|
+ fileStream.on("open", () => {
|
|
|
|
|
+ res.writeHead(200, { "content-type": contentType });
|
|
|
|
|
+ if (req.method === "HEAD") {
|
|
|
|
|
+ res.end();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ fileStream.pipe(res);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+__name(_HttpStaticRouter, "HttpStaticRouter");
|
|
|
|
|
+var HttpStaticRouter = _HttpStaticRouter;
|
|
|
|
|
+function _handleFsError(error, res) {
|
|
|
|
|
+ if (res.headersSent) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("code" in error && error.code === "ENOENT") {
|
|
|
|
|
+ res.writeHead(404, { "content-type": "text/plain" });
|
|
|
|
|
+ res.write("404 Not Found");
|
|
|
|
|
+ res.end();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res.writeHead(500, { "content-type": "text/plain" });
|
|
|
|
|
+ res.write("500 Internal Server Error");
|
|
|
|
|
+ res.end();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+__name(_handleFsError, "_handleFsError");
|
|
|
|
|
+// Annotate the CommonJS export names for ESM import in node:
|
|
|
|
|
+0 && (module.exports = {
|
|
|
|
|
+ HttpStaticRouter
|
|
|
|
|
+});
|