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
JavaScript
;
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");
}