2023-12-08 02:05:23 +00:00
|
|
|
import RequestParser from '../Utils/RequestParser';
|
2024-03-20 03:43:27 +00:00
|
|
|
import BaseRouter from './BaseRouter';
|
|
|
|
|
import Logger from '../Utils/Logger';
|
2024-04-01 22:46:52 +01:00
|
|
|
import LayResponse from '../IO/LayResponse';
|
2023-12-08 02:05:23 +00:00
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
/**
|
|
|
|
|
* Router class that extends BaseRouter.
|
|
|
|
|
* @extends BaseRouter
|
|
|
|
|
*/
|
2024-03-20 03:43:27 +00:00
|
|
|
export default class Router extends BaseRouter {
|
2023-12-08 02:05:23 +00:00
|
|
|
static allMethodKey = '$all';
|
|
|
|
|
static methods = ['all', 'get', 'post', 'put', 'patch', 'head', 'options'];
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
#domain = undefined;
|
|
|
|
|
#server = undefined;
|
|
|
|
|
#routes = new Map();
|
|
|
|
|
#headerList = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructor for the Router class.
|
|
|
|
|
* @param {Server} server - The server instance.
|
|
|
|
|
*/
|
|
|
|
|
constructor(server, options = {}) {
|
2024-03-20 03:43:27 +00:00
|
|
|
super();
|
2024-04-01 22:46:52 +01:00
|
|
|
this.#server = server;
|
|
|
|
|
this.#domain = options.domain || undefined;
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
/**
|
|
|
|
|
* Get the server instance.
|
|
|
|
|
* @returns {Server} The server instance.
|
|
|
|
|
*/
|
2024-01-20 04:59:20 +00:00
|
|
|
getServer() {
|
2024-04-01 22:46:52 +01:00
|
|
|
return this.#server;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the domain of the router
|
|
|
|
|
* @returns {String} The base domain for the router.
|
|
|
|
|
*/
|
|
|
|
|
getDomain() {
|
|
|
|
|
return this.#domain;
|
2024-01-20 04:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
/**
|
|
|
|
|
* Handle the incoming request.
|
|
|
|
|
* @param {RequestContext} reqContext - The request context.
|
|
|
|
|
* @returns {Promise<Response>} The response promise.
|
|
|
|
|
*/
|
2024-03-20 03:43:27 +00:00
|
|
|
async handle(reqContext) {
|
|
|
|
|
let method = (reqContext.request.method || '').toString().toLowerCase();
|
2023-12-08 02:05:23 +00:00
|
|
|
const methodKey = `$${method.toLowerCase()}`;
|
|
|
|
|
let path;
|
2024-03-20 03:43:27 +00:00
|
|
|
let url = reqContext.request.url;
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (!(url instanceof URL) && typeof url == 'string') {
|
|
|
|
|
url = new URL(url);
|
|
|
|
|
path = url.pathname;
|
|
|
|
|
}
|
|
|
|
|
if (path.startsWith('/')) path = path.substring(1);
|
|
|
|
|
if (path.endsWith('/')) path = path.substring(0, path.length - 1);
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
let currentNode = this.#routes;
|
2024-03-28 07:07:35 +00:00
|
|
|
let fallbackNode = currentNode.has('*') ? currentNode.get('*') : undefined;
|
2023-12-08 02:05:23 +00:00
|
|
|
let paramsList = [];
|
|
|
|
|
|
|
|
|
|
if (path != '') {
|
|
|
|
|
let segments = path.split('/');
|
|
|
|
|
for (let index = 0; index < segments.length; index++) {
|
|
|
|
|
const segment = segments[index];
|
2024-03-28 07:07:35 +00:00
|
|
|
if (currentNode.has('*')) {
|
|
|
|
|
fallbackNode = currentNode.get('*');
|
|
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (currentNode.has(segment)) {
|
|
|
|
|
currentNode = currentNode.get(segment);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentNode.has(':') || currentNode.has('?')) {
|
|
|
|
|
paramsList.push(segment);
|
|
|
|
|
currentNode = currentNode.get(':') ?? currentNode.get('?');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
currentNode = fallbackNode ?? null;
|
2023-12-08 02:05:23 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!(currentNode instanceof Map) ||
|
|
|
|
|
(['get', 'head'].indexOf(method) < 0 &&
|
|
|
|
|
!currentNode.has(methodKey) &&
|
|
|
|
|
!currentNode.has(Router.allMethodKey))
|
2024-04-01 22:46:52 +01:00
|
|
|
) {LayResponse
|
2023-12-08 02:05:23 +00:00
|
|
|
return new Response(`[${method}] Method Not Allowed`, {
|
|
|
|
|
status: 405,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!currentNode.has(methodKey) &&
|
|
|
|
|
!currentNode.get(Router.allMethodKey)
|
|
|
|
|
) {
|
|
|
|
|
return new Response(`Not Implemented: [${method}, ${req.url}]`, {
|
|
|
|
|
status: 501,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let handle =
|
|
|
|
|
currentNode.get(methodKey) || currentNode.get(Router.allMethodKey);
|
|
|
|
|
|
|
|
|
|
if (typeof handle == 'object' && typeof handle.callback == 'function') {
|
|
|
|
|
if (Array.isArray(handle.params)) {
|
|
|
|
|
for (const name of handle.params) {
|
|
|
|
|
let value = paramsList[reqContext.request.params.size];
|
|
|
|
|
if (reqContext.request.params.has(name)) {
|
|
|
|
|
let yet = reqContext.request.params.get(name);
|
|
|
|
|
if (!Array.isArray(yet)) {
|
|
|
|
|
yet = [yet];
|
|
|
|
|
}
|
|
|
|
|
yet.push(value);
|
|
|
|
|
value = yet;
|
|
|
|
|
}
|
|
|
|
|
reqContext.request.params.set(name, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2024-03-18 16:27:30 +00:00
|
|
|
await reqContext.request.parseBody();
|
2024-04-01 22:46:52 +01:00
|
|
|
let retVal = await this.executeMiddleware(
|
|
|
|
|
handle.middleware,
|
|
|
|
|
reqContext.request,
|
|
|
|
|
reqContext.response,
|
|
|
|
|
async () => {
|
|
|
|
|
return await handle.callback(reqContext.request, reqContext.response);
|
|
|
|
|
},
|
|
|
|
|
5000 // Add a timeout of 5 seconds for middleware execution
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await reqContext.request.parseBody();
|
|
|
|
|
retVal = await handle.callback(
|
2023-12-08 02:05:23 +00:00
|
|
|
reqContext.request,
|
|
|
|
|
reqContext.response
|
|
|
|
|
);
|
|
|
|
|
if (typeof retVal != 'undefined') {
|
2024-03-28 07:07:35 +00:00
|
|
|
if (retVal instanceof Promise) {
|
|
|
|
|
retVal = await retVal;
|
2024-04-01 22:46:52 +01:00
|
|
|
} else if (retVal instanceof LayResponse) {
|
|
|
|
|
return await retVal.build();
|
2024-03-28 07:07:35 +00:00
|
|
|
} else if (retVal instanceof Response) {
|
2023-12-08 02:05:23 +00:00
|
|
|
return retVal;
|
|
|
|
|
} else if (typeof retVal == 'string') {
|
2024-03-28 07:07:35 +00:00
|
|
|
await reqContext.response.text(retVal);
|
2023-12-08 02:05:23 +00:00
|
|
|
} else {
|
|
|
|
|
reqContext.response.json(retVal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2024-04-01 22:46:52 +01:00
|
|
|
console.error(err)
|
|
|
|
|
Logger.error(err);
|
|
|
|
|
return new Response('Something went wrong!', { status: 500 });
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reqContext.response.redirect()) {
|
|
|
|
|
return Response.redirect(
|
|
|
|
|
reqContext.response.redirect,
|
|
|
|
|
reqContext.response.status || 302
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 07:07:35 +00:00
|
|
|
return await reqContext.response.build();
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
/**
|
|
|
|
|
* Add a route to the router.
|
|
|
|
|
* @param {string} route - The route path.
|
|
|
|
|
* @param {function} callback - The route callback function.
|
|
|
|
|
* @param {Object} options - The route options.
|
|
|
|
|
* @param {function[]} [middleware] - The middleware functions.
|
|
|
|
|
*/
|
2024-03-20 03:43:27 +00:00
|
|
|
add(route, callback) {
|
|
|
|
|
let options = arguments[2];
|
|
|
|
|
let middleware = arguments[3];
|
2024-04-01 22:46:52 +01:00
|
|
|
|
|
|
|
|
if (typeof callback !== 'function')
|
|
|
|
|
throw new TypeError('`callback` must be a function');
|
|
|
|
|
if (typeof route !== 'string')
|
|
|
|
|
throw new TypeError('`route` must be a string');
|
2024-03-20 03:43:27 +00:00
|
|
|
if (Router.methods.indexOf(options.method) < 0)
|
2023-12-08 02:05:23 +00:00
|
|
|
throw new Error(
|
2024-03-20 03:43:27 +00:00
|
|
|
`invalid \`method\` (${options.method}) given`,
|
2023-12-08 02:05:23 +00:00
|
|
|
Router.methods
|
|
|
|
|
);
|
2024-04-01 22:46:52 +01:00
|
|
|
|
2023-12-08 02:05:23 +00:00
|
|
|
if (!Array.isArray(middleware)) {
|
|
|
|
|
middleware = typeof middleware == 'function' ? [middleware] : null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 03:43:27 +00:00
|
|
|
const methodKey = `$${options.method.toLowerCase()}`;
|
2023-12-08 02:05:23 +00:00
|
|
|
if (route.startsWith('/')) route = route.substring(1);
|
|
|
|
|
if (route.endsWith('/')) route = route.substring(0, route.length - 1);
|
|
|
|
|
|
|
|
|
|
let params = [];
|
|
|
|
|
let segments = route.split('/');
|
2024-04-01 22:46:52 +01:00
|
|
|
let currentNode = this.#routes;
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (route == '') {
|
|
|
|
|
if (currentNode.get(methodKey)) {
|
2024-04-01 22:46:52 +01:00
|
|
|
if (!this.#server.option('allowRouteOverwrite')) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(
|
|
|
|
|
`overwriting of routes is prohibited: [${options.method}] \`${route}\``
|
2023-12-08 02:05:23 +00:00
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(`overwriting route for \`${route}\``);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
currentNode.set(methodKey, { callback, middleware, params: [] });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add method to the segments to store the callback with
|
|
|
|
|
// and to allow different methods for the same route
|
|
|
|
|
for (let index = 0; index < segments.length; index++) {
|
|
|
|
|
let segment = segments[index];
|
|
|
|
|
let varName = null;
|
|
|
|
|
|
|
|
|
|
if (segment.startsWith(':')) {
|
|
|
|
|
let newSegment = ':';
|
|
|
|
|
varName = segment.substring(1);
|
|
|
|
|
if (segment.endsWith('?')) {
|
|
|
|
|
// optional param
|
|
|
|
|
newSegment = '?';
|
|
|
|
|
varName = varName.substring(0, -1);
|
|
|
|
|
}
|
|
|
|
|
segment = newSegment;
|
|
|
|
|
params.push(varName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentNode.has(segment)) currentNode.set(segment, new Map());
|
|
|
|
|
|
|
|
|
|
currentNode = currentNode.get(segment);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentNode.has(methodKey)) {
|
2024-04-01 22:46:52 +01:00
|
|
|
if (!this.#server.option('allowRouteOverwrite')) {
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(
|
2023-12-08 02:05:23 +00:00
|
|
|
`overwriting of routes is prohibited: [${method}] \`${route}\``
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-20 03:43:27 +00:00
|
|
|
Logger(`overwriting route for \`${route}\``);
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNode.set(methodKey, { callback, middleware, params });
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
/**
|
|
|
|
|
* Remove a route from the router.
|
|
|
|
|
* @param {string} route - The route path.
|
|
|
|
|
* @param {string} method - The HTTP method.
|
|
|
|
|
* @param {function} [cb=null] - The callback function to remove.
|
|
|
|
|
* @returns {boolean} True if the route was removed, false otherwise.
|
|
|
|
|
*/
|
2023-12-08 02:05:23 +00:00
|
|
|
remove(route, method, cb = null) {
|
|
|
|
|
const methodKey = `$${method.toLowerCase()}`;
|
|
|
|
|
if (route.startsWith('/')) route = route.substring(1);
|
|
|
|
|
if (route.endsWith('/')) route = route.substring(0, route.length - 1);
|
|
|
|
|
|
2024-04-01 22:46:52 +01:00
|
|
|
let currentNode = this.#routes;
|
2023-12-08 02:05:23 +00:00
|
|
|
|
|
|
|
|
if (route != '') {
|
|
|
|
|
let segments = route.toString().split('/');
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < segments.length; index++) {
|
|
|
|
|
const segment = segments[index];
|
|
|
|
|
|
|
|
|
|
if (currentNode.has(segment)) {
|
|
|
|
|
currentNode = currentNode.get(segment);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentNode.has(':') || currentNode.has('?')) {
|
|
|
|
|
currentNode = currentNode.get(':') ?? currentNode.get('?');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNode = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!(currentNode instanceof Map) ||
|
|
|
|
|
!currentNode.has(methodKey) ||
|
|
|
|
|
(typeof cb == 'function' && currentNode.get(methodKey) != cb)
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return currentNode.delete(methodKey);
|
|
|
|
|
}
|
2024-04-01 22:46:52 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute the middleware chain.
|
|
|
|
|
* @param {function[]} middleware - The middleware functions.
|
|
|
|
|
* @param {Request} req - The request object.
|
|
|
|
|
* @param {Response} res - The response object.
|
|
|
|
|
* @param {function} next - The next function to call.
|
|
|
|
|
* @returns {Promise<void>} A promise that resolves when the middleware chain is completed.
|
|
|
|
|
*/
|
|
|
|
|
async executeMiddleware(middleware, req, res, next) {
|
|
|
|
|
if (!Array.isArray(middleware) || middleware.length === 0) {
|
|
|
|
|
return await next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const executeNext = async (index) => {
|
|
|
|
|
if (index >= middleware.length) {
|
|
|
|
|
return await next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentMiddleware = middleware[index];
|
|
|
|
|
|
|
|
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
reject(new Error('Middleware execution timed out'));
|
|
|
|
|
}, timeout);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await Promise.race([
|
|
|
|
|
currentMiddleware(req, res, async () => {
|
|
|
|
|
return await executeNext(index + 1);
|
|
|
|
|
}),
|
|
|
|
|
timeoutPromise,
|
|
|
|
|
]);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return await executeNext(0);
|
|
|
|
|
}
|
2023-12-08 02:05:23 +00:00
|
|
|
}
|