From 2e365718edf793792307a4c1ca71a99dbfa49b49 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 3 Oct 2025 18:55:48 +0200 Subject: [PATCH] Attempt to vendor lambert-server --- package-lock.json | Bin 372723 -> 361802 bytes package.json | 4 +- src/util/util/lambert-server/HTTPError.ts | 5 + src/util/util/lambert-server/Server.ts | 174 ++++++++++++++++++++++ src/util/util/lambert-server/Utils.ts | 40 +++++ src/util/util/lambert-server/check.ts | 127 ++++++++++++++++ src/util/util/lambert-server/index.ts | 4 + tsconfig.json | 3 +- 8 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 src/util/util/lambert-server/HTTPError.ts create mode 100644 src/util/util/lambert-server/Server.ts create mode 100644 src/util/util/lambert-server/Utils.ts create mode 100644 src/util/util/lambert-server/check.ts create mode 100644 src/util/util/lambert-server/index.ts diff --git a/package-lock.json b/package-lock.json index 3e3c2aab4d4f41989fc935ff7ad7e7f64192911b..fb9a33ce7b1b71b88d549c5c95f9727b43d2b74d 100644 GIT binary patch delta 141 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 diff --git a/package.json b/package.json index d443044e..5aafa609 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,6 @@ "image-size": "^2.0.2", "json-bigint": "^1.0.0", "jsonwebtoken": "^9.0.2", - "lambert-server": "^1.2.12", "missing-native-js-functions": "^2.0.0", "module-alias": "^2.2.3", "morgan": "^1.10.1", @@ -119,7 +118,8 @@ "@spacebar/cdn": "dist/cdn", "@spacebar/gateway": "dist/gateway", "@spacebar/util": "dist/util", - "@spacebar/webrtc": "dist/webrtc" + "@spacebar/webrtc": "dist/webrtc", + "lambert-server": "dist/util/util/lambert-server" }, "optionalDependencies": { "@sendgrid/mail": "^8.1.6", diff --git a/src/util/util/lambert-server/HTTPError.ts b/src/util/util/lambert-server/HTTPError.ts new file mode 100644 index 00000000..70ba92a8 --- /dev/null +++ b/src/util/util/lambert-server/HTTPError.ts @@ -0,0 +1,5 @@ +export class HTTPError extends Error { + constructor(message: string, public code: number = 400) { + super(message); + } +} diff --git a/src/util/util/lambert-server/Server.ts b/src/util/util/lambert-server/Server.ts new file mode 100644 index 00000000..2325ef49 --- /dev/null +++ b/src/util/util/lambert-server/Server.ts @@ -0,0 +1,174 @@ +import express, { Application, NextFunction, Request, Response, Router } from "express"; +import { traverseDirectory } from "./Utils"; +import { Server as HTTPServer } from "http"; +import { HTTPError } from "./HTTPError"; +import "express-async-errors"; +import "missing-native-js-functions"; +import bodyParser from "body-parser"; +// import helmet from "helmet"; +import http from "http"; +import chalk from "chalk"; + +declare global { + namespace Express { + interface Request { + server: Server; + } + } +} + +export type ServerOptions = { + port: number; + host: string; + production: boolean; + serverInitLogging: boolean; + errorHandler?: { (err: Error, req: Request, res: Response, next: NextFunction): any }; + jsonBody: boolean; + server: http.Server; + app: Application; +}; + +// Overwrite default options for Router with default value true for mergeParams +const oldRouter = express.Router; +express.Router = function (options?: express.RouterOptions | undefined): Router { + if (!options) options = {}; + if (options.mergeParams == null) options.mergeParams = true; + + return oldRouter(options); +}; + +export class Server { + public app: Application; + public http: HTTPServer; + public options: ServerOptions; + public routes: Router[]; + + constructor(opts?: Partial) { + if (!opts) opts = {}; + if (!opts.port) opts.port = 8080; + if (!opts.host) opts.host = "0.0.0.0"; + if (opts.production == null) opts.production = false; + if (opts.serverInitLogging == null) opts.serverInitLogging = true; + if (opts.errorHandler == null) opts.errorHandler = this.errorHandler; + if (opts.jsonBody == null) opts.jsonBody = true; + if (opts.server) this.http = opts.server; + + this.options = opts; + + if (opts.app) this.app = opts.app; + else this.app = express(); + } + + // protected secureExpress() { + // this.app.use(helmet.contentSecurityPolicy()); + // this.app.use(helmet.expectCt); + // this.app.use(helmet.originAgentCluster()); + // this.app.use(helmet.referrerPolicy({ policy: "same-origin" })); + // this.app.use(helmet.hidePoweredBy()); + // this.app.use(helmet.noSniff()); + // this.app.use(helmet.dnsPrefetchControl({ allow: true })); + // this.app.use(helmet.ieNoOpen()); + // this.app.use(helmet.frameguard({ action: "deny" })); + // this.app.use(helmet.permittedCrossDomainPolicies({ permittedPolicies: "none" })); + // } + + public errorHandler = (error: Error, req: Request, res: Response, next: NextFunction) => { + try { + let code; + let message = error?.toString(); + + if (error instanceof HTTPError && error.code) code = error.code || 400; + else { + console.error(error); + if (this.options.production) { + message = "Internal Server Error"; + } + code = 500; + } + + res.status(code).json({ success: false, code: code, error: true, message }); + } catch (e) { + console.error(e); + return res.status(500).json({ success: false, code: 500, error: true, message: "Internal Server Error" }); + } + }; + + async start() { + const server = this.http || this.app; + if (!server.listening) { + await new Promise((res) => { + this.http = server.listen(this.options.port, () => res()); + }); + if(this.options.serverInitLogging) this.log("info", `[Server] started on ${this.options.host}:${this.options.port}`); + } + } + + async registerRoutes(root: string) { + this.app.use((req, res, next) => { + req.server = this; + next(); + }); + if (this.options.jsonBody) this.app.use(bodyParser.json()); + const result = await traverseDirectory({ dirname: root, recursive: true }, this.registerRoute.bind(this, root)); + if (this.options.errorHandler) this.app.use(this.options.errorHandler); + // if (this.options.production) this.secureExpress(); + return result; + } + + log(l: "info" | "error" | "warn" | "verbose", ...args: any[]) { + // @ts-ignore + if (!console[l]) l = "verbose"; + + const level = l === "verbose" ? "log" : l; + + var color: "red" | "yellow" | "blue" | "reset"; + + switch (level) { + case "error": + color = "red"; + break; + case "warn": + color = "yellow"; + break; + case "info": + color = "blue"; + case "log": + default: + color = "reset"; + } + + if (this.options.production && l === "verbose") return; + + console[level](chalk[color](`[${new Date().toTimeString().split(" ")[0]}]`), ...args); + } + + registerRoute(root: string, file: string): any { + if (root.endsWith("/") || root.endsWith("\\")) root = root.slice(0, -1); // removes slash at the end of the root dir + let path = file.replace(root, ""); // remove root from path and + path = path.split(".").slice(0, -1).join("."); // trancate .js/.ts file extension of path + path = path.replaceAll("#", ":").replaceAll("!", "?").replaceAll("\\", "/"); + if (path.endsWith("/index")) path = path.slice(0, -6); // delete index from path + if (!path.length) path = "/"; // first root index.js file must have a / path + + try { + var router = require(file); + if (router.router) router = router.router; + if (router.default) router = router.default; + if (!router || router?.prototype?.constructor?.name !== "router") + throw `File doesn't export any default router`; + + if (this.options.errorHandler) router.use(this.options.errorHandler); + this.app.use(path, router); + + if(this.options.serverInitLogging) this.log("verbose", `[Server] Route ${path} registered`); + + return router; + } catch (error) { + console.error(new Error(`[Server] Failed to register route ${path}: ${error}`)); + } + } + + stop() { + return new Promise((res) => this.http.close(() => res())); + } +} diff --git a/src/util/util/lambert-server/Utils.ts b/src/util/util/lambert-server/Utils.ts new file mode 100644 index 00000000..2e30c24d --- /dev/null +++ b/src/util/util/lambert-server/Utils.ts @@ -0,0 +1,40 @@ +import fs from "fs"; +import "missing-native-js-functions"; + +export interface traverseDirectoryOptions { + dirname: string; + filter?: RegExp; + excludeDirs?: RegExp; + recursive?: boolean; +} + +const DEFAULT_EXCLUDE_DIR = /^\./; +const DEFAULT_FILTER = /^([^\.].*)(?( + options: traverseDirectoryOptions, + action: (path: string) => T +): Promise { + if (!options.filter) options.filter = DEFAULT_FILTER; + if (!options.excludeDirs) options.excludeDirs = DEFAULT_EXCLUDE_DIR; + + const routes = fs.readdirSync(options.dirname); + const promises = []>routes + .sort((a, b) => (a.startsWith("#") ? 1 : -1)) // load #parameter routes last + .map(async (file) => { + const path = options.dirname + file; + const stat = fs.lstatSync(path); + if (path.match(options.excludeDirs)) return; + + if (path.match(options.filter) && stat.isFile()) { + return action(path); + } else if (options.recursive && stat.isDirectory()) { + return traverseDirectory({ ...options, dirname: path + "/" }, action); + } + }); + const result = await Promise.all(promises); + + const t = <(T | undefined)[]>result.flat(); + + return t.filter((x) => x != undefined); +} diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts new file mode 100644 index 00000000..c025bcbf --- /dev/null +++ b/src/util/util/lambert-server/check.ts @@ -0,0 +1,127 @@ +import "missing-native-js-functions"; +import { NextFunction, Request, Response } from "express"; +import { HTTPError } from "."; + +const OPTIONAL_PREFIX = "$"; +const EMAIL_REGEX = + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +export function check(schema: any) { + return (req: Request, res: Response, next: NextFunction) => { + try { + const result = instanceOf(schema, req.body, { path: "body" }); + if (result === true) return next(); + throw result; + } catch (error) { + next(new HTTPError((error as any).toString(), 400)); + } + }; +} + +export class Tuple { + public types: any[]; + constructor(...types: any[]) { + this.types = types; + } +} + +export class Email { + constructor(public email: string) {} + check() { + return !!this.email.match(EMAIL_REGEX); + } +} + +export function instanceOf( + type: any, + value: any, + { path = "", optional = false }: { path?: string; optional?: boolean } = {} +): Boolean { + if (!type) return true; // no type was specified + + if (value == null) { + if (optional) return true; + throw `${path} is required`; + } + + switch (type) { + case String: + if (typeof value === "string") return true; + throw `${path} must be a string`; + case Number: + value = Number(value); + if (typeof value === "number" && !isNaN(value)) return true; + throw `${path} must be a number`; + case BigInt: + try { + value = BigInt(value); + if (typeof value === "bigint") return true; + } catch (error) {} + throw `${path} must be a bigint`; + case Boolean: + if (value == "true") value = true; + if (value == "false") value = false; + if (typeof value === "boolean") return true; + throw `${path} must be a boolean`; + case Object: + if (typeof value === "object" && value !== null) return true; + throw `${path} must be a object`; + } + + if (typeof type === "object") { + if (Array.isArray(type)) { + if (!Array.isArray(value)) throw `${path} must be an array`; + if (!type.length) return true; // type array didn't specify any type + + return value.every((val, i) => instanceOf(type[0], val, { path: `${path}[${i}]`, optional })); + } + if (type?.constructor?.name != "Object") { + if (type instanceof Tuple) { + if ( + (type).types.some((x) => { + try { + return instanceOf(x, value, { path, optional }); + } catch (error) { + return false; + } + }) + ) { + return true; + } + throw `${path} must be one of ${type.types}`; + } + if (type instanceof Email) { + if ((type).check()) return true; + throw `${path} is not a valid E-Mail`; + } + if (value instanceof type) return true; + throw `${path} must be an instance of ${type}`; + } + if (typeof value !== "object") throw `${path} must be a object`; + + const diff = Object.keys(value).missing( + Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x)) + ); + + if (diff.length) throw `Unkown key ${diff}`; + + return Object.keys(type).every((key) => { + let newKey = key; + const OPTIONAL = key.startsWith(OPTIONAL_PREFIX); + if (OPTIONAL) newKey = newKey.slice(OPTIONAL_PREFIX.length); + + return instanceOf(type[key], value[newKey], { + path: `${path}.${newKey}`, + optional: OPTIONAL, + }); + }); + } else if (typeof type === "number" || typeof type === "string" || typeof type === "boolean") { + if (value === type) return true; + throw `${path} must be ${value}`; + } else if (typeof type === "bigint") { + if (BigInt(value) === type) return true; + throw `${path} must be ${value}`; + } + + return type == value; +} diff --git a/src/util/util/lambert-server/index.ts b/src/util/util/lambert-server/index.ts new file mode 100644 index 00000000..6575a7d3 --- /dev/null +++ b/src/util/util/lambert-server/index.ts @@ -0,0 +1,4 @@ +export * from "./check"; +export * from "./Server"; +export * from "./Utils"; +export * from "./HTTPError"; diff --git a/tsconfig.json b/tsconfig.json index f5cfbe04..3ca6bc72 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,8 @@ "@spacebar/gateway*": ["./gateway"], "@spacebar/cdn*": ["./cdn"], "@spacebar/util*": ["./util"], - "@spacebar/webrtc*": ["./webrtc"] + "@spacebar/webrtc*": ["./webrtc"], + "lambert-server*": ["./util/util/lambert-server"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */