UNPKG

rjweb-server

Version:

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

276 lines (275 loc) 10.3 kB
import { ReverseProxyIps } from "../types/global"; import { as, object, size } from "@rjweb/utils"; import GlobalContext from "../types/internal/classes/GlobalContext"; import ContentTypes from "./router/ContentTypes"; import FileLoader from "./router/File"; import { currentVersion } from "./Middleware"; import Validator from "./Validator"; import Path from "./router/Path"; import mergeClasses from "../functions/mergeClasses"; import { oas31 } from "openapi3-ts"; import httpHandler from "../handlers/http"; import wsHandler from "../handlers/ws"; import RequestContext from "../types/internal/classes/RequestContext"; import HttpRequestContext from "./request/HttpRequestContext"; import WsOpenContext from "./request/WsOpenContext"; import WsMessageContext from "./request/WsMessageContext"; import WsCloseContext from "./request/WsCloseContext"; export const defaultOptions = { port: 0, bind: '0.0.0.0', version: true, compression: { http: { enabled: true, preferOrder: ['brotli', 'gzip', 'deflate'], maxSize: size(10).mb(), minSize: size(1).kb() }, ws: { enabled: true, maxSize: size(1).mb() } }, performance: { eTag: true, lastModified: true }, logging: { debug: false, error: true, warn: true }, proxy: { enabled: false, force: false, compress: false, header: 'x-forwarded-for', ips: { validate: false, mode: 'whitelist', list: [...ReverseProxyIps.LOCAL, ...ReverseProxyIps.CLOUDFLARE] }, credentials: { authenticate: false, username: 'proxy', password: 'proxy' } } }; export default class Server { /** * Construct a new Server Instance * @example * ``` * import { Server } from "rjweb-server" * import { Runtime } from "@rjweb/runtime-bun" * * const server = new Server(Runtime, { * port: 3000 * }) * ``` * @since 3.0.0 */ constructor(implementation, options, middlewares, context = {}) { this.context = context; this._status = 'stopped'; this.promises = []; this.openAPISchemas = {}; this.Validator = Validator; this.middlewares = middlewares ?? []; this.options = object.deepParse(defaultOptions, options); this.implementation = new implementation(this.options); this.global = new GlobalContext(this.options, this.implementation, { HttpRequest: mergeClasses(HttpRequestContext, ...this.middlewares.filter((middleware) => middleware.rjwebVersion === currentVersion).map((middleware) => middleware.classContexts.HttpRequest(middleware.config, HttpRequestContext))), WsOpen: mergeClasses(WsOpenContext, ...this.middlewares.filter((middleware) => middleware.rjwebVersion === currentVersion).map((middleware) => middleware.classContexts.WsOpen(middleware.config, WsOpenContext))), WsMessage: mergeClasses(WsMessageContext, ...this.middlewares.filter((middleware) => middleware.rjwebVersion === currentVersion).map((middleware) => middleware.classContexts.WsMessage(middleware.config, WsMessageContext))), WsClose: mergeClasses(WsCloseContext, ...this.middlewares.filter((middleware) => middleware.rjwebVersion === currentVersion).map((middleware) => middleware.classContexts.WsClose(middleware.config, WsCloseContext))) }); const outdated = this.middlewares.find((middleware) => middleware.rjwebVersion !== currentVersion); if (outdated) { throw new Error(`Middleware Version Mismatch\n ${outdated.infos.name}: ${outdated.infos.version} (Recieved Middleware v${outdated.rjwebVersion}, expected v${currentVersion})`); } this.global.logger.debug('Server Created!'); this.global.logger.debug('Implementation:'); this.global.logger.debug(` ${this.implementation.name()}: ${this.implementation.version()}`); this.global.logger.debug('Middlewares:'); if (this.middlewares.length) { for (const middleware of this.middlewares) { this.global.logger.debug(` ${middleware.infos.name}: ${middleware.infos.version}`); } } else { this.global.logger.debug(' (None)'); } } /** * Add a Content Type Mapping to override (or expand) content types * @since 5.3.0 */ contentTypes(callback) { const contentTypes = new ContentTypes(); callback(contentTypes); this.global.contentTypes.import(contentTypes['data']); return this; } get FileLoader() { const global = this.global, promises = this.promises; return class FakeFileLoader extends FileLoader { constructor(prefix) { super(prefix, global, undefined, undefined, promises); } }; } /** * Listen to Error Callbacks * @since 9.0.0 */ error(key, callback) { this.global.errorHandlers[key] = callback; return this; } /** * Listen to Ratelimit Callbacks * @since 9.0.0 */ rateLimit(key, callback) { this.global.rateLimitHandlers[key] = callback; return this; } /** * Listen to Not Found Callbacks * @since 9.0.0 */ notFound(callback) { this.global.notFoundHandler = callback; return this; } /** * Create a new Path * @since 6.0.0 */ path(prefix, callback) { const path = new Path(prefix, this.global, undefined, undefined, this.promises); callback(path); this.global.routes.http.push(...path['routesHttp']); this.global.routes.static.push(...path['routesStatic']); this.global.routes.ws.push(...path['routesWS']); return this; } /** * Listen to all HTTP Requests (does not override routed ones) * @since 9.0.0 */ http(callback) { this.global.httpHandler = callback; return this; } /** * Generate an OpenAPI Specification for the Server * @since 9.0.0 */ openAPI(name, version, server, contact) { const builder = new oas31.OpenApiBuilder() .addTitle(name) .addVersion(version) .addServer(server); if (contact) builder.addContact(contact); const routes = {}; for (const route of this.global.routes.http) { if (route.urlData.type === 'regexp') continue; if (!routes[route.urlData.value]) routes[route.urlData.value] = {}; if (route['urlMethod'] === 'CONNECT') continue; routes[route.urlData.value][route['urlMethod']] = route; } for (const path in routes) { const pathItem = {}; for (const [method, route] of Object.entries(routes[path])) { pathItem[method.toLowerCase()] = route['openApi']; } builder.addPath(path, pathItem); } for (const schema in this.openAPISchemas) { builder.addSchema(schema, this.openAPISchemas[schema]); } return builder.rootDoc; } /** * Add an OpenAPI Schema to the Server * @since 9.0.0 */ schema(name, schema) { this.openAPISchemas[name] = schema; return this; } /** * Get the Server's Port that its listening on * @since 9.0.0 */ port() { if (this._status === 'stopped') return null; return as(this.implementation.port()); } /** * Get the Server's Status * @since 9.0.0 */ status() { return this._status; } /** * Start the Server Instance * @example * ``` * import { Server } from "rjweb-server" * * const server = new Server({}) * * server.start().then((port) => { * console.log(`Server Started on Port ${port}`) * }) * ``` * @since 3.0.0 */ start() { if (this._status === 'listening') throw new Error('Server is already listening'); this.implementation.handle({ http: (context) => httpHandler(new RequestContext(context, this.middlewares, this, this.global), context, this, this.middlewares, this.context), ws: (ws) => wsHandler(ws.data(), ws, this, this.middlewares) }); return new Promise(async (resolve, reject) => { try { await this.implementation.start(); this._status = 'listening'; this.global.logger.debug('Running Router Promises ...'); const promisesStartTime = performance.now(); await Promise.all(this.promises); this.global.logger.debug(`Running Router Promises ... Done ${(performance.now() - promisesStartTime).toFixed(2)}ms`); this.global.logger.debug('Running Middleware Promises ...'); const middlewareStartTime = performance.now(); await Promise.all(this.middlewares.map((middleware) => middleware.callbacks.load?.(middleware.config, this, this.global))); this.global.logger.debug(`Running Middleware Promises ... Done ${(performance.now() - middlewareStartTime).toFixed(2)}ms`); resolve(this.implementation.port()); } catch (err) { this.implementation.stop(); this._status = 'stopped'; reject(err); } }); } /** * Stop the Server Instance * @example * ``` * import { Server } from "rjweb-server" * * const server = new Server({}) * * server.start().then((port) => { * console.log(`Server Started on Port ${port}`) * * setTimeout(() => { * server.stop() * console.log('Server Stopped after 5 seconds') * }, 5000) * }) * ``` * @since 3.0.0 */ stop() { if (this._status === 'stopped') throw new Error('Server is already stopped'); this.implementation.stop(); this._status = 'stopped'; return this; } }