Attempt to vendor lambert-server
This commit is contained in:
parent
efba3f245d
commit
2e365718ed
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -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",
|
||||
|
||||
5
src/util/util/lambert-server/HTTPError.ts
Normal file
5
src/util/util/lambert-server/HTTPError.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class HTTPError extends Error {
|
||||
constructor(message: string, public code: number = 400) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
174
src/util/util/lambert-server/Server.ts
Normal file
174
src/util/util/lambert-server/Server.ts
Normal file
@ -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<ServerOptions>) {
|
||||
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 = <ServerOptions>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<void>((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>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<void>((res) => this.http.close(() => res()));
|
||||
}
|
||||
}
|
||||
40
src/util/util/lambert-server/Utils.ts
Normal file
40
src/util/util/lambert-server/Utils.ts
Normal file
@ -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 = /^([^\.].*)(?<!\.d)\.(ts|js)$/;
|
||||
|
||||
export async function traverseDirectory<T>(
|
||||
options: traverseDirectoryOptions,
|
||||
action: (path: string) => T
|
||||
): Promise<T[]> {
|
||||
if (!options.filter) options.filter = DEFAULT_FILTER;
|
||||
if (!options.excludeDirs) options.excludeDirs = DEFAULT_EXCLUDE_DIR;
|
||||
|
||||
const routes = fs.readdirSync(options.dirname);
|
||||
const promises = <Promise<T | T[] | undefined>[]>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(<RegExp>options.excludeDirs)) return;
|
||||
|
||||
if (path.match(<RegExp>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[]>t.filter((x) => x != undefined);
|
||||
}
|
||||
127
src/util/util/lambert-server/check.ts
Normal file
127
src/util/util/lambert-server/check.ts
Normal file
@ -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 (
|
||||
(<Tuple>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 ((<Email>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;
|
||||
}
|
||||
4
src/util/util/lambert-server/index.ts
Normal file
4
src/util/util/lambert-server/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./check";
|
||||
export * from "./Server";
|
||||
export * from "./Utils";
|
||||
export * from "./HTTPError";
|
||||
@ -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'. */
|
||||
|
||||
Reference in New Issue
Block a user