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
JavaScript
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;
}
}