rjweb-server
Version:
Easy and Robust Way to create a Web Server with Many Easy-to-use Features in NodeJS
540 lines (539 loc) • 21.4 kB
JavaScript
import parsePath from "../parsePath";
import URLObject from "../../classes/URLObject";
import { resolve as pathResolve } from "path";
import parseContent from "../parseContent";
import { dashboardIndexRoute, dashboardWsRoute } from "./handleDashboard";
import { promises as fs } from "fs";
import handleCompressType from "../handleCompressType";
import handleDecompressType, { DecompressMapping } from "../handleDecompressType";
import MiniEventEmitter from "../../classes/miniEventEmitter";
import ValueCollection from "../../classes/valueCollection";
import handleEvent from "../handleEvent";
import { Version } from "../..";
import Status from "../../misc/statusEnum";
import { toArrayBuffer } from "../../classes/web/HttpRequest";
import toETag from "../toETag";
import parseKV from "../parseKV";
import writeHTTPMeta from "../writeHTTPMeta";
import getCompressMethod from "../getCompressMethod";
const fileExists = async (location) => {
location = pathResolve(location);
try {
const res = await fs.stat(location);
return res.isFile();
} catch {
return false;
}
};
async function handleHTTPRequest(req, res, socket, requestType, ctg) {
const queryString = req.getQuery(), url = new URLObject(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 ValueCollection(),
cookies: parseKV(req.getHeader("cookie"), "=", ";"),
params: new ValueCollection(),
queries: parseKV(url.query),
fragments: parseKV(url.fragments),
events: new MiniEventEmitter(),
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"] = Version;
ctx.response.headers["accept-ranges"] = "none";
if (ctg.options.proxy.enabled) {
ctx.response.headers["proxy-authenticate"] = `Basic realm="Access rjweb-server@${Version}"`;
if (ctg.options.proxy.forceProxy) {
if (!ctx.headers.has("proxy-authorization")) {
const meta = await writeHTTPMeta(res, ctx);
if (!ctx.isAborted)
return res.cork(() => {
ctx.response.status = Status.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 writeHTTPMeta(res, ctx);
if (!ctx.isAborted)
return res.cork(() => {
ctx.response.status = Status.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 writeHTTPMeta(res, ctx);
if (!ctx.isAborted)
return res.cork(() => {
ctx.response.status = Status.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 parseContent(ctg.options.body.message, false, ctg.logger);
const meta = await writeHTTPMeta(res, ctx);
if (!ctx.isAborted)
return res.cork(() => {
ctx.response.status = Status.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 writeHTTPMeta(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 handleEvent("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 handleEvent(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 writeHTTPMeta(res, ctx);
if (!ctx.isAborted)
return res.cork(() => {
ctg.logger.debug("Upgraded http request to websocket");
ctx.response.status = Status.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) => parseContent(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] = getCompressMethod(!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 = toETag(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 writeHTTPMeta(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 = Status.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 = handleCompressType(compressMethod);
const destroyStream = () => {
compression.destroy();
};
compression.on("data", (content) => {
res.content = 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 = parsePath(ctx.url.path.replace(url2.path.path, "")).substring(1);
if (url2.data.hideHTML) {
if (await fileExists(url2.location + "/" + urlPath + "/index.html"))
foundStatic(pathResolve(url2.location + "/" + urlPath + "/index.html"), url2);
else if (await fileExists(url2.location + "/" + urlPath + ".html"))
foundStatic(pathResolve(url2.location + "/" + urlPath + ".html"), url2);
else if (await fileExists(url2.location + "/" + urlPath))
foundStatic(pathResolve(url2.location + "/" + urlPath), url2);
} else if (await fileExists(url2.location + "/" + urlPath))
foundStatic(pathResolve(url2.location + "/" + urlPath), url2);
}
if (ctg.options.dashboard.enabled && ctx.url.path === parsePath(ctg.options.dashboard.path)) {
ctx.execute.route = 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 === parsePath([ctg.options.dashboard.path, "/ws"])) {
ctx.execute.route = 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 writeHTTPMeta(res, ctx);
const deCompression = handleDecompressType(ctg.options.performance.decompressBodies ? 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 = Status.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 parseContent(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 = Status.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 = Status.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");
}
export {
handleHTTPRequest as default
};