rjweb-server
Version:
Easy and Robust Way to create a Web Server with Many Easy-to-use Features in NodeJS
271 lines (270 loc) • 9.98 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Route_1 = __importDefault(require("../Route"));
const Http_1 = __importDefault(require("./Http"));
const parseURL_1 = __importDefault(require("../../functions/parseURL"));
const RateLimit_1 = __importDefault(require("./RateLimit"));
const path_1 = __importDefault(require("path"));
const utils_1 = require("@rjweb/utils");
const Ws_1 = __importDefault(require("./Ws"));
class Path {
computePath(path) {
if (path instanceof RegExp)
return path;
return (0, parseURL_1.default)(this.prefix.concat('/', path)).path;
}
/**
* Create a new Path
* @since 6.0.0
*/ constructor(prefix, global, validators = [], ratelimits, promises, openApi) {
this.validators = validators;
this.routesHttp = [];
this.routesStatic = [];
this.routesWS = [];
this.prefix = prefix;
this._global = global;
if (ratelimits) {
this._httpRatelimit = ratelimits[0];
this._wsRatelimit = ratelimits[1];
}
else {
this._httpRatelimit = new RateLimit_1.default()['data'];
this._wsRatelimit = new RateLimit_1.default()['data'];
}
this.promises = promises ?? [];
this.openApi = openApi ?? {};
}
/**
* Add OpenAPI Documentation to all HTTP Endpoints in this Path (and all children)
* @since 9.0.0
*/ document(item) {
this.openApi = utils_1.object.deepMerge(this.openApi, item);
return this;
}
/**
* Add a Ratelimit to all HTTP Endpoints in this Path (and all children)
*
* When a User requests an Endpoint, that will count against their hit count, if
* the hits exceeds the `<maxHits>` in `<timeWindow>ms`, they wont be able to access
* the route for `<penalty>ms`.
* @example
* ```
* import { time } from "@rjweb/utils"
*
* server.path('/', (path) => path
* .httpRateLimit((limit) => limit
* .hits(5)
* .window(time(20).s())
* .penalty(0)
* ) // This will allow 5 requests every 20 seconds
* .http('GET', '/hello', (ws) => ws
* .onRequest(async(ctr) => {
* ctr.print('Hello bro!')
* })
* )
* )
*
* server.rateLimit('httpRequest', (ctr) => {
* ctr.print(`Please wait ${ctr.getRateLimit()?.resetIn}ms!!!!!`)
* })
* ```
* @since 8.6.0
*/ httpRatelimit(callback) {
const limit = new RateLimit_1.default();
limit['data'] = Object.assign({}, this._httpRatelimit);
callback(limit);
this._httpRatelimit = limit['data'];
return this;
}
/**
* Add a Ratelimit to all WebSocket Endpoints in this Path (and all children)
*
* When a User sends a message to a Socket, that will count against their hit count, if
* the hits exceeds the `<maxHits>` in `<timeWindow>ms`, they wont be able to access
* the route for `<penalty>ms`.
* @example
* ```
* import { time } from "@rjweb/utils"
*
* server.path('/', (path) => path
* .httpRateLimit((limit) => limit
* .hits(5)
* .window(time(20).s())
* .penalty(0)
* ) // This will allow 5 messages every 20 seconds
* .ws('/echo', (ws) => ws
* .onMessage(async(ctr) => {
* ctr.print(await ctr.rawMessageBytes())
* })
* )
* )
*
* server.rateLimit('wsMessage', (ctr) => {
* ctr.print(`Please wait ${ctr.getRateLimit()?.resetIn}ms!!!!!`)
* })
* ```
* @since 8.6.0
*/ wsRatelimit(callback) {
const limit = new RateLimit_1.default();
limit['data'] = Object.assign({}, this._wsRatelimit);
callback(limit);
this._wsRatelimit = limit['data'];
return this;
}
/**
* Create a redirection route
* @example
* ```
* const server = new Server(...)
*
* server.path('/', (path) => path
* .redirect('/google', 'https://google.com')
* )
* ```
* @since 3.10.0
*/ redirect(path, redirect, type) {
for (const p of Array.isArray(path) ? path : [path]) {
const route = new Route_1.default('http', 'GET', this.computePath(p), {
onRequest(ctr) {
return ctr.redirect(redirect, type);
}
});
route.validators.unshift(...this.validators);
this.routesHttp.push(route);
}
return this;
}
/**
* Create a subpath of this Path
* @since 6.0.0
*/ path(prefix, callback) {
const path = new Path(this.prefix.concat('/', prefix), this._global, [...this.validators], [this._httpRatelimit, this._wsRatelimit], this.promises, this.openApi);
callback(path);
this.routesHttp.push(...path.routesHttp);
this.routesStatic.push(...path.routesStatic);
this.routesWS.push(...path.routesWS);
return this;
}
/**
* Use a Validator on all Endpoints in this Path
*
* This will attach a Validator to all Endpoints in this Path
* @since 9.0.0
*/ validate(validator) {
this.validators.push(validator);
this.openApi = utils_1.object.deepMerge(this.openApi, validator.openApi);
return this;
}
/**
* Serve Static Files
* @example
* ```
* // All Files in "./public" will be served dynamically so they wont be loaded as routes by default
* // Due to the stripHtmlEnding Option being on files will be served differently, /index.html -> /; /about.html -> /about; /contributors/index.html -> /contributors
* const server = new Server(...)
*
* server.path('/', (path) => path
* .static('./public', {
* stripHtmlEnding: true // If enabled will remove .html ending from files
* })
* )
* ```
* @since 3.1.0
*/ static(folder, options = {}) {
const route = new Route_1.default('static', 'GET', this.computePath('/'), {
folder: path_1.default.posix.resolve(folder),
stripHtmlEnding: options.stripHtmlEnding ?? false
});
route.validators.unshift(...this.validators);
this.routesStatic.push(route);
if (options.preProcessFiles) {
this.promises.push(new Promise(async (resolve) => {
for await (const file of utils_1.filesystem.walk(route.data.folder, { async: true, recursive: true })) {
if (file.isDirectory() || !file.path)
continue;
const $path = path_1.default.posix.relative(route.data.folder, file.path).replace('./', '');
if (!route.data.stripHtmlEnding) {
this._global.cache.staticFiles.set(this.computePath($path).toString(), [path_1.default.posix.resolve(file.path), route]);
}
else {
{ // path/index.html
if (file.name.endsWith('index.html')) {
this._global.cache.staticFiles.set(this.computePath($path.replace('index.html', '')).toString(), [path_1.default.posix.resolve(file.path), route]);
}
}
{ // path.html
if (file.name.endsWith('.html')) {
this._global.cache.staticFiles.set(this.computePath($path.replace('.html', '')).toString(), [path_1.default.posix.resolve(file.path), route]);
}
}
{ // path
this._global.cache.staticFiles.set(this.computePath($path).toString(), [path_1.default.posix.resolve(file.path), route]);
}
}
}
resolve();
}));
}
return this;
}
/**
* Add a new HTTP Route
* @example
* ```
* const server = new Server(...)
*
* server.path('/', (path) => path
* .http('GET', '/hello', (ws) => ws
* .onRequest((ctr) => {
* ctr.print('Hello!')
* })
* )
* )
* ```
* @since 6.0.0
*/ http(method, path, callback) {
for (const p of Array.isArray(path) ? path : [path]) {
const http = new Http_1.default(method, this.computePath(p), this._httpRatelimit);
http['route'].openApi = Object.assign({}, this.openApi);
callback(http);
const url = http['route'].urlData;
if (url.type === 'regexp')
url.prefix = this.computePath('/').toString();
http['route'].validators.unshift(...this.validators);
this.routesHttp.push(http['route']);
}
return this;
}
/**
* Add a new WebSocket Route
* @example
* ```
* const server = new Server(...)
*
* server.path('/', (path) => path
* .ws('/echo', (ws) => ws
* .onMessage((ctr) => {
* ctr.print(ctr.rawMessageBytes())
* })
* )
* )
* ```
* @since 6.0.0
*/ ws(path, callback) {
for (const p of Array.isArray(path) ? path : [path]) {
const ws = new Ws_1.default(this.computePath(p), this._wsRatelimit);
ws['route'].openApi = Object.assign({}, this.openApi);
callback(ws);
const url = ws['route'].urlData;
if (url.type === 'regexp')
url.prefix = this.computePath('/').toString();
ws['route'].validators.unshift(...this.validators);
this.routesWS.push(ws['route']);
}
return this;
}
}
exports.default = Path;