UNPKG

rjweb-server

Version:

Easy and Robust Way to create a Web Server with Many Easy-to-use Features in NodeJS

570 lines (569 loc) 24 kB
"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 __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); var handleHTTPRequest_exports = {}; __export(handleHTTPRequest_exports, { default: () => handleHTTPRequest }); module.exports = __toCommonJS(handleHTTPRequest_exports); var import_parsePath = __toESM(require("../parsePath")); var import_URLObject = __toESM(require("../../classes/URLObject")); var import_path = require("path"); var import_parseContent = __toESM(require("../parseContent")); var import_handleDashboard = require("./handleDashboard"); var import_fs = require("fs"); var import_handleCompressType = __toESM(require("../handleCompressType")); var import_handleDecompressType = __toESM(require("../handleDecompressType")); var import_miniEventEmitter = __toESM(require("../../classes/miniEventEmitter")); var import_valueCollection = __toESM(require("../../classes/valueCollection")); var import_handleEvent = __toESM(require("../handleEvent")); var import__ = require("../.."); var import_statusEnum = __toESM(require("../../misc/statusEnum")); var import_HttpRequest = require("../../classes/web/HttpRequest"); var import_toETag = __toESM(require("../toETag")); var import_parseKV = __toESM(require("../parseKV")); var import_writeHTTPMeta = __toESM(require("../writeHTTPMeta")); var import_getCompressMethod = __toESM(require("../getCompressMethod")); const fileExists = async (location) => { location = (0, import_path.resolve)(location); try { const res = await import_fs.promises.stat(location); return res.isFile(); } catch { return false; } }; async function handleHTTPRequest(req, res, socket, requestType, ctg) { const queryString = req.getQuery(), url = new import_URLObject.default(req.getUrl() + (queryString ? `?${queryString}` : ""), req.getMethod()); ctg.logger.debug("HTTP Request recieved"); ctg.requests.increase(); const ctx = { executeSelf: () => true, handleError(err) { if (!err) return; ctx.error = err; ctx.execute.event = "httpError"; }, setExecuteSelf(callback) { ctx.executeSelf = callback; }, url, continueSend: true, executeCode: true, remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(), error: null, headers: new import_valueCollection.default(), cookies: (0, import_parseKV.default)(req.getHeader("cookie"), "=", ";"), params: new import_valueCollection.default(), queries: (0, import_parseKV.default)(url.query), fragments: (0, import_parseKV.default)(url.fragments), events: new import_miniEventEmitter.default(), isProxy: false, isAborted: false, refListeners: [], body: { type: "unknown", chunks: [], raw: Buffer.allocUnsafe(0), parsed: "" }, execute: { route: null, found: false, file: null, event: "none" }, response: { headers: { ...ctg.defaults.headers }, cookies: {}, status: 200, statusMessage: void 0, isCompressed: false, content: [], contentPrettify: false } }; req.forEachHeader((header, value) => { ctx.headers.set(header, value); }); if (ctg.options.poweredBy) ctx.response.headers["rjweb-server"] = import__.Version; ctx.response.headers["accept-ranges"] = "none"; if (ctg.options.proxy.enabled) { ctx.response.headers["proxy-authenticate"] = `Basic realm="Access rjweb-server@${import__.Version}"`; if (ctg.options.proxy.forceProxy) { if (!ctx.headers.has("proxy-authorization")) { const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.PROXY_AUTHENTICATION_REQUIRED; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end("No Proxy Authentication Provided"); }); else return; } } if (ctx.headers.has("proxy-authorization")) { if (!ctg.options.proxy.credentials.authenticate) ctx.isProxy = true; else if (ctx.headers.get("proxy-authorization") !== "Basic ".concat(Buffer.from(`${ctg.options.proxy.credentials.username}:${ctg.options.proxy.credentials.password}`).toString("base64"))) { const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.PROXY_AUTHENTICATION_REQUIRED; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end("Invalid Proxy Authentication Provided"); }); else return; } else ctx.isProxy = true; } } if (ctx.headers.has("content-length") && isNaN(parseInt(ctx.headers.get("content-length")))) { const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.LENGTH_REQUIRED; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end(); }); else return; } else if (ctx.headers.has("content-length") && parseInt(ctx.headers.get("content-length")) > ctg.options.body.maxSize) { const result = await (0, import_parseContent.default)(ctg.options.body.message, false, ctg.logger); const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.PAYLOAD_TOO_LARGE; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end(result.content); }); else return; } res.onAborted(() => { ctx.events.send("requestAborted"); ctx.isAborted = true; }); if (ctg.options.cors && requestType === "http") { ctx.response.headers["access-control-allow-headers"] = "*"; ctx.response.headers["access-control-allow-origin"] = "*"; ctx.response.headers["access-control-request-method"] = "*"; ctx.response.headers["access-control-allow-methods"] = "*"; if (ctx.url.method === "OPTIONS") { ctg.logger.debug("OPTIONS CORS Early End Request succeeded"); const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { meta(); if (!ctx.isAborted) res.end(); }); else return; } } const ctr = new ctg.classContexts.http(ctg.controller, ctx, req, res, requestType); if (ctx.execute.route && "context" in ctx.execute.route) ctr["@"] = ctx.execute.route.context.keep ? ctx.execute.route.context.data : Object.assign({}, ctx.execute.route.context.data); Object.assign(ctr["@"], ctg.defaults.globContext); ctx.events.listen("startRequest", async () => { if (ctx.executeCode && ctg.middlewares.length > 0 && !ctx.error) { for (let middlewareIndex = 0; middlewareIndex < ctg.middlewares.length; middlewareIndex++) { const middleware = ctg.middlewares[middlewareIndex]; if (!middleware.data.httpEvent) continue; try { await Promise.resolve(middleware.data.httpEvent(middleware.localContext, () => ctx.executeCode = false, ctr, ctx, ctg)); if (ctx.error) throw ctx.error; } catch (err) { ctx.handleError(err); break; } } } await (0, import_handleEvent.default)("httpRequest", ctr, ctx, ctg); if (ctx.executeCode && ctx.execute.found && ctx.execute.route.data.validations.length > 0 && !ctx.error) { for (let validateIndex = 0; validateIndex < ctx.execute.route.data.validations.length; validateIndex++) { const validate = ctx.execute.route.data.validations[validateIndex]; try { await Promise.resolve(validate(ctr, () => ctx.executeCode = false)); if (!ctx.executeCode) break; } catch (err) { ctx.handleError(err); break; } } } if (ctx.executeCode && requestType === "upgrade" && ctx.execute.found && ctx.execute.route.type === "websocket" && ctx.execute.route.onUpgrade) { try { await Promise.resolve(ctx.execute.route.onUpgrade(ctr, () => ctx.executeCode = false)); } catch (err) { ctx.handleError(err); } } let didExecute404 = false; const runPageLogic = (eventOnly) => new Promise(async (resolve) => { if (!ctx.execute.found && !didExecute404) { ctx.execute.event = "route404"; didExecute404 = true; } if (ctx.execute.event !== "none") { await (0, import_handleEvent.default)(ctx.execute.event, ctr, ctx, ctg); return resolve(); } ; if (eventOnly) return resolve(); if (!ctx.execute.route) return; if (ctx.execute.route.type === "websocket" && ctx.executeCode) { try { ctx.continueSend = false; ctg.webSockets.opened.increase(); resolve(true); const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { ctg.logger.debug("Upgraded http request to websocket"); ctx.response.status = import_statusEnum.default.SWITCHING_PROTOCOLS; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) return res.upgrade( { ctx, custom: ctr["@"] }, ctx.headers.get("sec-websocket-key", ""), ctx.headers.get("sec-websocket-protocol", ""), ctx.headers.get("sec-websocket-extensions", ""), socket ); }); } catch (err) { ctx.error = err; ctx.execute.event = "httpError"; await runPageLogic(true); } return resolve(); } if (ctx.execute.route.type === "static" && ctx.executeCode) { ctr.printFile(ctx.execute.file, { compress: ctx.execute.route.data.doCompress }); return resolve(); } if (ctx.execute.route.type === "http" && ctx.executeCode) { try { await Promise.resolve(ctx.execute.route.onRequest(ctr)); } catch (err) { ctx.error = err; ctx.execute.event = "httpError"; await runPageLogic(true); } return resolve(); } return resolve(); }); if (await runPageLogic()) return; try { const result = await Promise.resolve(ctx.executeSelf()); ctx.continueSend = result; if (result) await runPageLogic(true); } catch (err) { ctx.handleError(err); await runPageLogic(true); } if (ctx.continueSend) try { const results = await Promise.all([...ctx.response.content.map((c) => (0, import_parseContent.default)(c, ctx.response.contentPrettify, ctg.logger))]); const response = { content: Buffer.concat(results.map((r) => r.content)), headers: Object.assign({}, ...results.map((r) => r.headers)) }; Object.assign(ctx.response.headers, response.headers); const [compressMethod, compressHeader, compressWrite] = (0, import_getCompressMethod.default)(!ctx.response.isCompressed, ctx.headers.get("accept-encoding", ""), res, response.content.byteLength, ctg); ctx.response.headers["content-encoding"] = compressHeader; if (compressHeader) ctx.response.headers["vary"] = "accept-encoding"; let eTag; if (ctg.options.performance.eTag) { eTag = (0, import_toETag.default)(response.content, ctx.response.headers, ctx.response.cookies, ctx.response.status); ctg.logger.debug("generated etag for content of bytelen", response.content.byteLength); if (eTag) ctx.response.headers["etag"] = Buffer.from(`W/"${eTag}"`); } const meta = await (0, import_writeHTTPMeta.default)(res, ctx); if (!ctx.isAborted) return res.cork(() => { let endEarly = false; if (ctg.options.performance.eTag && eTag && ctx.headers.get("if-none-match") === `W/"${eTag}"`) { ctg.logger.debug("ended etag request early because of match"); ctx.response.status = import_statusEnum.default.NOT_MODIFIED; ctx.response.statusMessage = void 0; endEarly = true; } meta(); if (endEarly) { if (!ctx.isAborted) res.end(); return; } if (compressHeader) ctg.logger.debug("negotiated to use", compressHeader); const compression = (0, import_handleCompressType.default)(compressMethod); const destroyStream = () => { compression.destroy(); }; compression.on("data", (content) => { res.content = (0, import_HttpRequest.toArrayBuffer)(content); if (!ctx.isAborted) { try { res.contentOffset = res.getWriteOffset(); const ok = compressWrite(res.content); if (!ok) { res.onWritable((offset) => { const sliced = res.content.slice(offset - res.contentOffset); const ok2 = compressWrite(sliced); if (ok2) { ctg.data.outgoing.increase(sliced.byteLength); ctg.logger.debug("sent http body chunk with bytelen", sliced.byteLength); } return ok2; }); } else { ctg.data.outgoing.increase(content.byteLength); ctg.logger.debug("sent http body chunk with bytelen", content.byteLength, "(delayed)"); } } catch { } } }).once("close", () => { if (compressHeader && !ctx.isAborted) res.cork(() => res.end()); destroyStream(); ctx.events.unlist("requestAborted", destroyStream); return; }); compression.end(response.content); ctx.events.listen("requestAborted", destroyStream); }); } catch (err) { ctg.logger.debug(`Ending Request ${ctr.url.href} discarded unknown:`, err); } }); const actualUrl = ctx.url.path.split("/"); if (requestType === "http") { if (ctg.cache.routes.has(`route::normal::${ctx.url.path}::${ctx.url.method}`)) { const url2 = ctg.cache.routes.get(`route::normal::${ctx.url.path}::${ctx.url.method}`); ctx.params["data"] = url2.params["data"]; ctx.execute.route = url2.route; ctx.execute.found = true; } else if (ctx.url.method === "GET" && ctg.cache.routes.has(`route::static::${ctx.url.path}`)) { const url2 = ctg.cache.routes.get(`route::static::${ctx.url.path}`); ctx.execute.file = url2.file; ctx.execute.route = url2.route; ctx.execute.found = true; } if (!ctx.execute.found) { const route = ctg.routes.normal.find((r) => r.path.matches(ctx.url.method, ctx.params, ctx.url.path, actualUrl)); if (route) { ctx.execute = { found: true, event: ctx.execute.event, route, file: null }; ctg.cache.routes.set(`route::normal::${ctx.url.path}::${ctx.url.method}`, { route, params: ctx.params }); } } if (ctx.url.method === "GET") { const foundStatic = (file, url2) => { ctx.execute.found = true; ctx.execute.route = url2; ctx.execute.file = file; ctg.cache.routes.set(`route::static::${ctx.url.path}`, { route: url2, file }); }; if (!ctx.execute.found) for (let staticNumber = 0; staticNumber < ctg.routes.static.length; staticNumber++) { if (ctx.execute.found) break; const url2 = ctg.routes.static[staticNumber]; if (url2.path.data.type !== "normal") continue; if (!ctx.url.path.startsWith(url2.path.data.value)) continue; const urlPath = (0, import_parsePath.default)(ctx.url.path.replace(url2.path.path, "")).substring(1); if (url2.data.hideHTML) { if (await fileExists(url2.location + "/" + urlPath + "/index.html")) foundStatic((0, import_path.resolve)(url2.location + "/" + urlPath + "/index.html"), url2); else if (await fileExists(url2.location + "/" + urlPath + ".html")) foundStatic((0, import_path.resolve)(url2.location + "/" + urlPath + ".html"), url2); else if (await fileExists(url2.location + "/" + urlPath)) foundStatic((0, import_path.resolve)(url2.location + "/" + urlPath), url2); } else if (await fileExists(url2.location + "/" + urlPath)) foundStatic((0, import_path.resolve)(url2.location + "/" + urlPath), url2); } if (ctg.options.dashboard.enabled && ctx.url.path === (0, import_parsePath.default)(ctg.options.dashboard.path)) { ctx.execute.route = (0, import_handleDashboard.dashboardIndexRoute)(ctg); ctx.execute.found = true; } const htmlBuilderRoute = ctg.routes.htmlBuilder.find((h) => h.path.path === ctx.url.path); if (htmlBuilderRoute) { ctx.execute.route = htmlBuilderRoute; ctx.execute.found = true; } } } else { if (ctg.options.dashboard.enabled && ctx.url.path === (0, import_parsePath.default)([ctg.options.dashboard.path, "/ws"])) { ctx.execute.route = (0, import_handleDashboard.dashboardWsRoute)(ctg); ctx.execute.found = true; } if (ctg.cache.routes.has(`route::ws::${ctx.url.path}`)) { const url2 = ctg.cache.routes.get(`route::ws::${ctx.url.path}`); ctx.params["data"] = url2.params["data"]; ctx.execute.route = url2.route; ctx.execute.found = true; } if (!ctx.execute.found) { const route = ctg.routes.websocket.find((r) => r.path.matches(ctx.url.method, ctx.params, ctx.url.path, actualUrl)); if (route) { ctx.execute = { found: true, event: ctx.execute.event, route, file: null }; ctg.cache.routes.set(`route::ws::${ctx.url.path}`, { route, params: ctx.params }); } } } if (ctx.execute.found && (ctx.execute.route?.type === "http" || ctx.execute.route?.type === "websocket")) { for (const [key, value] of Object.entries(ctx.execute.route.data.headers)) { ctx.response.headers[key] = value; } } if (ctx.executeCode && requestType === "http" && ctx.url.method !== "GET") { const meta = await (0, import_writeHTTPMeta.default)(res, ctx); const deCompression = (0, import_handleDecompressType.default)(ctg.options.performance.decompressBodies ? import_handleDecompressType.DecompressMapping[ctx.headers.get("content-encoding", "none")] : "none"); let totalBytes = 0; deCompression.on("data", async (data) => { ctg.logger.debug(`processed http body chunk (${ctx.headers.get("content-encoding", "no compression")}) with bytelen`, data.byteLength); ctx.body.chunks.push(data); }).once("error", () => { deCompression.destroy(); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.BAD_REQUEST; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end("Invalid Content-Encoding Header or Content"); }); }).once("close", () => { ctg.logger.debug("Finished http body streaming with", ctx.body.chunks.length, "chunks"); ctx.events.send("startRequest"); }); if (!ctx.isAborted) res.onData(async (rawChunk, isLast) => { ctg.logger.debug("Recieved http body chunk with bytelen", rawChunk.byteLength, "is last:", isLast); if (ctx.error) return; if (!ctx.executeCode) return; if (ctx.execute.route && "onRawBody" in ctx.execute.route) { const buffer = Buffer.from(rawChunk), sendBuffer = Buffer.allocUnsafe(buffer.byteLength); buffer.copy(sendBuffer); try { if (!ctx.isAborted) await Promise.resolve(ctx.execute.route.onRawBody(ctr, () => ctx.executeCode = false, sendBuffer, isLast)); } catch (err) { ctx.handleError(err); ctx.events.send("startRequest"); } if (isLast) deCompression.end(); } else { try { const buffer = Buffer.from(rawChunk), sendBuffer = Buffer.allocUnsafe(buffer.byteLength); buffer.copy(sendBuffer); totalBytes += sendBuffer.byteLength; deCompression.write(sendBuffer); ctg.data.incoming.increase(sendBuffer.byteLength); if (totalBytes > ctg.options.body.maxSize) { const result = await (0, import_parseContent.default)(ctg.options.body.message, false, ctg.logger); ctg.logger.debug("big http body request aborted"); deCompression.destroy(); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.PAYLOAD_TOO_LARGE; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end(result.content); }); else return; } else if (ctx.headers.has("content-length") && totalBytes > parseInt(ctx.headers.get("content-length"))) { ctg.logger.debug("invalid http body request aborted"); deCompression.destroy(); if (!ctx.isAborted) return res.cork(() => { ctx.response.status = import_statusEnum.default.BAD_REQUEST; ctx.response.statusMessage = void 0; meta(); if (!ctx.isAborted) res.end("Invalid Content-Length Header"); }); else return; } if (isLast) deCompression.end(); } catch { } } }); } if (!ctx.executeCode || requestType === "upgrade" || ctx.url.method === "GET") ctx.events.send("startRequest"); }