UNPKG

rjweb-server

Version:

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

356 lines (355 loc) 13.6 kB
import ValueCollection from "./valueCollection"; import parseOptions from "../functions/parseOptions"; import RouteList from "./router"; import handleHTTPRequest from "../functions/web/handleHTTPRequest"; import handleWSOpen from "../functions/web/handleWSOpen"; import handleWSMessage from "../functions/web/handleWSMessage"; import handleWSClose from "../functions/web/handleWSClose"; import { currentVersion } from "./middlewareBuilder"; import RouteFile from "./router/file"; import { getFilesRecursively } from "rjutils-collection"; import { HttpRequest, WsClose, WsConnect, WsMessage } from "../types/external"; import mergeClasses from "../functions/mergeClasses"; import generateOpenAPI3 from "../functions/generateOpenAPI3"; import DataStat from "./dataStat"; import Logger from "./logger"; import { promises as fs } from "fs"; import uWebsocket from "@rjweb/uws"; import path from "path"; import os from "os"; class Server extends RouteList { /** * Initialize a new Server * @example * ``` * const controller = new Server({ * port: 8000 * }) * * module.exports.server = controller * ``` * @since 3.0.0 */ constructor(options = {}, middlewares = [], globContext = {}) { super(); this.server = uWebsocket.App(); this.socket = 0; /** * Route File Builder * @example * ``` * const { server } = require('../index.js') * * module.exports = new server.routeFile((file) => file * .http(...) * ) * ``` * @since 7.0.0 */ this.routeFile = RouteFile; this.middlewares = middlewares; const fullOptions = parseOptions(options); this.globalContext = { controller: this, contentTypes: {}, logger: new Logger(fullOptions.logging), options: fullOptions, requests: new DataStat(), webSockets: { opened: new DataStat(), messages: { incoming: new DataStat(), outgoing: new DataStat() } }, classContexts: { http: mergeClasses(HttpRequest, ...this.middlewares.map((m) => m.data.classModifications.http)), wsConnect: mergeClasses(WsConnect, ...this.middlewares.map((m) => m.data.classModifications.http)), wsMessage: mergeClasses(WsMessage, ...this.middlewares.map((m) => m.data.classModifications.http)), wsClose: mergeClasses(WsClose, ...this.middlewares.map((m) => m.data.classModifications.http)) }, defaults: { globContext, headers: {} }, middlewares: this.middlewares, data: { incoming: new DataStat(), outgoing: new DataStat() }, routes: { normal: [], websocket: [], static: [], htmlBuilder: [] }, cache: { files: new ValueCollection(void 0, void 0, fullOptions.cache, fullOptions.cacheLimit), middlewares: new ValueCollection(void 0, void 0, fullOptions.cache, fullOptions.cacheLimit), routes: new ValueCollection(void 0, void 0, fullOptions.cache, fullOptions.cacheLimit) } }; } /** * Override the set Server Options * @example * ``` * const controller = new Server({ }) * * controller.setOptions({ * port: 6900 * }) * ``` * @since 3.0.0 */ setOptions(options) { this.globalContext.options = parseOptions(options); return this; } /** * Get OpenAPI 3.1 Defininitions for this Server (make sure to let all routes load before calling) * @example * ``` * const controller = new Server({ port: 4200 }) * * controller.path('/', (path) => path * .http('GET', '/openapi', (http) => http * .onRequest((ctr) => { * ctr.print(ctr.controller.getOpenApi3Def('http://localhost:4200')) * }) * ) * ) * ``` * @since 7.6.0 */ getOpenAPI3Def(serverUrl) { return generateOpenAPI3(this.globalContext, serverUrl); } /** * Start the Server * @example * ``` * const controller = new Server({ }) * * controller.start() * .then((port) => { * console.log(`Server started on port ${port}`) * }) * .catch((err) => { * console.error(err) * }) * ``` * @since 3.0.0 */ start() { return new Promise(async (resolve, reject) => { let stopExecution = false; const externalPaths = await this.loadExternalPaths(); this.globalContext.webSockets = { opened: new DataStat(), messages: { incoming: new DataStat(), outgoing: new DataStat() } }; this.globalContext.data = { incoming: new DataStat(), outgoing: new DataStat() }; this.globalContext.requests = new DataStat(); for (let middlewareIndex = 0; middlewareIndex < this.middlewares.length; middlewareIndex++) { const middleware = this.middlewares[middlewareIndex]; if (!("data" in middleware)) { reject(new Error(`Middleware ${middleware.name} is too old!`)); stopExecution = true; break; } ; if (!("initEvent" in middleware.data)) continue; try { if (middleware.version !== currentVersion) throw new Error(`Middleware version cannot be higher or lower than currently supported version (${middleware.version} != ${currentVersion})`); await Promise.resolve(middleware.data.initEvent(middleware.localContext, middleware.config, this.globalContext)); } catch (error) { reject(error); stopExecution = true; break; } } ; if (stopExecution) return; if (this.globalContext.options.ssl.enabled) { try { await fs.readFile(path.resolve(this.globalContext.options.ssl.keyFile)); await fs.readFile(path.resolve(this.globalContext.options.ssl.certFile)); } catch { throw new Error(`Cant access your SSL Key or Cert file! (${this.globalContext.options.ssl.keyFile} / ${this.globalContext.options.ssl.certFile})`); } try { if (this.globalContext.options.ssl.caFile) await fs.readFile(path.resolve(this.globalContext.options.ssl.caFile)); if (this.globalContext.options.ssl.dhParamFile) await fs.readFile(path.resolve(this.globalContext.options.ssl.dhParamFile)); } catch { throw new Error(`Cant access your SSL Ca or Dhparam file! (${this.globalContext.options.ssl.caFile} / ${this.globalContext.options.ssl.dhParamFile})`); } this.server = uWebsocket.SSLApp({ key_file_name: path.resolve(this.globalContext.options.ssl.keyFile), cert_file_name: path.resolve(this.globalContext.options.ssl.certFile), ca_file_name: this.globalContext.options.ssl.caFile ? path.resolve(this.globalContext.options.ssl.caFile) : void 0, dh_params_file_name: this.globalContext.options.ssl.dhParamFile ? path.resolve(this.globalContext.options.ssl.dhParamFile) : void 0 }); } else this.server = uWebsocket.App(); this.server.any("/*", (res, req) => { handleHTTPRequest(req, res, null, "http", this.globalContext); }).ws("/*", { maxBackpressure: 512 * 1024 * 1024, sendPingsAutomatically: true, compression: this.globalContext.options.wsCompression.enabled ? uWebsocket.SHARED_COMPRESSOR : void 0, maxPayloadLength: this.globalContext.options.message.maxSize, upgrade: (res, req, connection) => { handleHTTPRequest(req, res, connection, "upgrade", this.globalContext); }, open: (ws) => { handleWSOpen(ws, this.globalContext); }, message: this.globalContext.options.message.enabled ? (ws, message) => { handleWSMessage(ws, message, this.globalContext); } : void 0, close: (ws, code, message) => { handleWSClose(ws, message, this.globalContext); } }); const routes = await this.getData(); this.globalContext.routes.normal = routes.routes; this.globalContext.routes.websocket = routes.webSockets; this.globalContext.routes.static = routes.statics; this.globalContext.contentTypes = routes.contentTypes; this.globalContext.defaults.headers = routes.defaultHeaders; this.globalContext.routes.normal.push(...externalPaths.routes); this.globalContext.routes.websocket.push(...externalPaths.webSockets); this.server.listen(this.globalContext.options.bind, this.globalContext.options.port, (listen) => { if (!listen) return reject(new Error(`Failed to start server on port ${this.globalContext.options.port}.`)); this.socket = listen; return resolve(uWebsocket.getSocketPort(listen)); }); }); } /** * Reload the Server * @example * ``` * const controller = new Server({ }) * * controller.reload() * .then((port) => { * console.log(`Server reloaded and started on port ${port}`) * }) * .catch((err) => { * console.error(err) * }) * ``` * @since 3.0.0 */ async reload() { this.globalContext.cache.files.clear(); this.globalContext.cache.routes.clear(); const routes = await this.getData(); this.globalContext.routes.normal = routes.routes; this.globalContext.routes.websocket = routes.webSockets; this.globalContext.routes.static = routes.statics; this.globalContext.contentTypes = routes.contentTypes; this.globalContext.defaults.headers = routes.defaultHeaders; const externalPaths = await this.loadExternalPaths(); this.globalContext.routes.normal.push(...externalPaths.routes); this.globalContext.routes.websocket.push(...externalPaths.webSockets); this.stop(); return this.start(); } /** * Stop the Server * @example * ``` * const controller = new Server({ }) * * controller.stop() * .then(() => { * console.log('Server stopped') * }) * .catch((err) => { * console.error(err) * }) * ``` * @since 3.0.0 */ async stop() { this.globalContext.cache.files.clear(); this.globalContext.cache.routes.clear(); uWebsocket.closeSocket(this.socket); return; } /** * Load all External Paths */ async loadExternalPaths() { const loadedRoutes = { routes: [], webSockets: [] }; for (const loadPath of (await this.getData()).loadPaths) { this.globalContext.logger.debug("Loading Route Path", loadPath); if (loadPath.type === "cjs") { for (const file of (await getFilesRecursively(loadPath.path, true)).filter((f) => f.endsWith("js"))) { this.globalContext.logger.debug("Loading cjs Route file", file); const route = require(file); if (!route || !(route instanceof RouteFile)) throw new Error(`Invalid Route @ ${file}`); const routeInfos = await route.getData(loadPath.prefix); for (const routeInfo of routeInfos.routes) { if (loadPath.fileBasedRouting) routeInfo.path.addSuffix(path.posix.normalize(path.posix.normalize(file).replace(path.posix.normalize(loadPath.path), "").replaceAll("\\", "/")).replace(/index|\.(js|ts|cjs|cts|mjs|mts)/g, "")); routeInfo.data.validations.push(...loadPath.validations); routeInfo.data.headers = Object.assign(routeInfo.data.headers, loadPath.headers); } for (const routeInfo of routeInfos.webSockets) { if (loadPath.fileBasedRouting) routeInfo.path.addSuffix(path.posix.normalize(path.posix.normalize(file).replace(path.posix.normalize(loadPath.path), "").replaceAll("\\", "/")).replace(/index|\.(js|ts|cjs|cts|mjs|mts)/g, "")); routeInfo.data.validations.push(...loadPath.validations); } loadedRoutes.routes.push(...routeInfos.routes); loadedRoutes.webSockets.push(...routeInfos.webSockets); } } else { for (const file of (await getFilesRecursively(loadPath.path, true)).filter((f) => f.endsWith("js"))) { this.globalContext.logger.debug("Loading esm Route file", file); const importFile = os.platform() === "win32" ? `file:///${file}` : file; const route = (await import(importFile)).default; if (!route || !(route instanceof RouteFile)) throw new Error(`Invalid Route @ ${file}`); const routeInfos = await route.getData(loadPath.prefix); for (const routeInfo of routeInfos.routes) { if (loadPath.fileBasedRouting) routeInfo.path.addSuffix(path.posix.normalize(path.posix.normalize(file).replace(path.posix.normalize(loadPath.path), "").replaceAll("\\", "/")).replace(/index|\.(js|ts|cjs|cts|mjs|mts)/g, "")); routeInfo.data.validations.push(...loadPath.validations); routeInfo.data.headers = Object.assign(routeInfo.data.headers, loadPath.headers); } for (const routeInfo of routeInfos.webSockets) { if (loadPath.fileBasedRouting) routeInfo.path.addSuffix(path.posix.normalize(path.posix.normalize(file).replace(path.posix.normalize(loadPath.path), "").replaceAll("\\", "/")).replace(/index|\.(js|ts|cjs|cts|mjs|mts)/g, "")); routeInfo.data.validations.push(...loadPath.validations); } loadedRoutes.routes.push(...routeInfos.routes); loadedRoutes.webSockets.push(...routeInfos.webSockets); } } } return loadedRoutes; } } export { Server as default };