summary refs log tree commit diff
path: root/src/api/middlewares
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/middlewares')
-rw-r--r--src/api/middlewares/Authentication.ts18
-rw-r--r--src/api/middlewares/BodyParser.ts3
-rw-r--r--src/api/middlewares/CORS.ts12
-rw-r--r--src/api/middlewares/ErrorHandler.ts25
-rw-r--r--src/api/middlewares/RateLimit.ts70
-rw-r--r--src/api/middlewares/TestClient.ts103
-rw-r--r--src/api/middlewares/Translation.ts14
7 files changed, 182 insertions, 63 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 1df7911b..50048b12 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -10,7 +10,7 @@ export const NO_AUTHORIZATION_ROUTES = [
 	"/auth/mfa/totp",
 	// Routes with a seperate auth system
 	"/webhooks/",
-	// Public information endpoints 
+	// Public information endpoints
 	"/ping",
 	"/gateway",
 	"/experiments",
@@ -26,7 +26,7 @@ export const NO_AUTHORIZATION_ROUTES = [
 	// Public policy pages
 	"/policies/instance",
 	// Asset delivery
-	/\/guilds\/\d+\/widget\.(json|png)/
+	/\/guilds\/\d+\/widget\.(json|png)/,
 ];
 
 export const API_PREFIX = /^\/api(\/v\d+)?/;
@@ -43,7 +43,11 @@ declare global {
 	}
 }
 
-export async function Authentication(req: Request, res: Response, next: NextFunction) {
+export async function Authentication(
+	req: Request,
+	res: Response,
+	next: NextFunction,
+) {
 	if (req.method === "OPTIONS") return res.sendStatus(204);
 	const url = req.url.replace(API_PREFIX, "");
 	if (url.startsWith("/invites") && req.method === "GET") return next();
@@ -54,12 +58,16 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
 		})
 	)
 		return next();
-	if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
+	if (!req.headers.authorization)
+		return next(new HTTPError("Missing Authorization Header", 401));
 
 	try {
 		const { jwtSecret } = Config.get().security;
 
-		const { decoded, user }: any = await checkToken(req.headers.authorization, jwtSecret);
+		const { decoded, user }: any = await checkToken(
+			req.headers.authorization,
+			jwtSecret,
+		);
 
 		req.token = decoded;
 		req.user_id = decoded.id;
diff --git a/src/api/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts
index 4cb376bc..7741f1fd 100644
--- a/src/api/middlewares/BodyParser.ts
+++ b/src/api/middlewares/BodyParser.ts
@@ -6,7 +6,8 @@ export function BodyParser(opts?: OptionsJson) {
 	const jsonParser = bodyParser.json(opts);
 
 	return (req: Request, res: Response, next: NextFunction) => {
-		if (!req.headers["content-type"]) req.headers["content-type"] = "application/json";
+		if (!req.headers["content-type"])
+			req.headers["content-type"] = "application/json";
 
 		jsonParser(req, res, (err) => {
 			if (err) {
diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts
index 20260cf9..2dce51c6 100644
--- a/src/api/middlewares/CORS.ts
+++ b/src/api/middlewares/CORS.ts
@@ -7,10 +7,16 @@ export function CORS(req: Request, res: Response, next: NextFunction) {
 	// TODO: use better CSP
 	res.set(
 		"Content-security-policy",
-		"default-src *  data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';"
+		"default-src *  data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline';",
+	);
+	res.set(
+		"Access-Control-Allow-Headers",
+		req.header("Access-Control-Request-Headers") || "*",
+	);
+	res.set(
+		"Access-Control-Allow-Methods",
+		req.header("Access-Control-Request-Methods") || "*",
 	);
-	res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*");
-	res.set("Access-Control-Allow-Methods", req.header("Access-Control-Request-Methods") || "*");
 
 	next();
 }
diff --git a/src/api/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts
index 2012b91c..bf3b011a 100644
--- a/src/api/middlewares/ErrorHandler.ts
+++ b/src/api/middlewares/ErrorHandler.ts
@@ -3,7 +3,12 @@ import { HTTPError } from "lambert-server";
 import { ApiError, FieldError } from "@fosscord/util";
 const EntityNotFoundErrorRegex = /"(\w+)"/;
 
-export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) {
+export function ErrorHandler(
+	error: Error,
+	req: Request,
+	res: Response,
+	next: NextFunction,
+) {
 	if (!error) return next();
 
 	try {
@@ -12,20 +17,28 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
 		let message = error?.toString();
 		let errors = undefined;
 
-		if (error instanceof HTTPError && error.code) code = httpcode = error.code;
+		if (error instanceof HTTPError && error.code)
+			code = httpcode = error.code;
 		else if (error instanceof ApiError) {
 			code = error.code;
 			message = error.message;
 			httpcode = error.httpStatus;
 		} else if (error.name === "EntityNotFoundError") {
-			message = `${error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"} could not be found`;
+			message = `${
+				error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"
+			} could not be found`;
 			code = httpcode = 404;
 		} else if (error instanceof FieldError) {
 			code = Number(error.code);
 			message = error.message;
 			errors = error.errors;
 		} else {
-			console.error(`[Error] ${code} ${req.url}\n`, errors || error, "\nbody:", req.body);
+			console.error(
+				`[Error] ${code} ${req.url}\n`,
+				errors || error,
+				"\nbody:",
+				req.body,
+			);
 
 			if (req.server?.options?.production) {
 				// don't expose internal errors to the user, instead human errors should be thrown as HTTPError
@@ -39,6 +52,8 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
 		res.status(httpcode).json({ code: code, message, errors });
 	} catch (error) {
 		console.error(`[Internal Server Error] 500`, error);
-		return res.status(500).json({ code: 500, message: "Internal Server Error" });
+		return res
+			.status(500)
+			.json({ code: 500, message: "Internal Server Error" });
 	}
 }
diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 57645c0b..b3976a16 100644
--- a/src/api/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -40,21 +40,32 @@ export default function rateLimit(opts: {
 	success?: boolean;
 	onlyIp?: boolean;
 }): any {
-	return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
+	return async (
+		req: Request,
+		res: Response,
+		next: NextFunction,
+	): Promise<any> => {
 		// exempt user? if so, immediately short circuit
 		if (req.user_id) {
 			const rights = await getRights(req.user_id);
 			if (rights.has("BYPASS_RATE_LIMITS")) return next();
 		}
 
-		const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
+		const bucket_id =
+			opts.bucket ||
+			req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
 		let executor_id = getIpAdress(req);
 		if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
 
 		let max_hits = opts.count;
 		if (opts.bot && req.user_bot) max_hits = opts.bot;
-		if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
-		else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
+		if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method))
+			max_hits = opts.GET;
+		else if (
+			opts.MODIFY &&
+			["POST", "DELETE", "PATCH", "PUT"].includes(req.method)
+		)
+			max_hits = opts.MODIFY;
 
 		let offender = Cache.get(executor_id + bucket_id);
 
@@ -75,11 +86,15 @@ export default function rateLimit(opts: {
 				const global = bucket_id === "global";
 				// each block violation pushes the expiry one full window further
 				reset += opts.window * 1000;
-				offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000);
+				offender.expires_at = new Date(
+					offender.expires_at.getTime() + opts.window * 1000,
+				);
 				resetAfterMs = reset - Date.now();
 				resetAfterSec = Math.ceil(resetAfterMs / 1000);
 
-				console.log(`blocked bucket: ${bucket_id} ${executor_id}`, { resetAfterMs });
+				console.log(`blocked bucket: ${bucket_id} ${executor_id}`, {
+					resetAfterMs,
+				});
 				return (
 					res
 						.status(429)
@@ -91,20 +106,33 @@ export default function rateLimit(opts: {
 						.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
 						.set("X-RateLimit-Bucket", `${bucket_id}`)
 						// TODO: error rate limit message translation
-						.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global })
+						.send({
+							message: "You are being rate limited.",
+							retry_after: resetAfterSec,
+							global,
+						})
 				);
 			}
 		}
 
 		next();
-		const hitRouteOpts = { bucket_id, executor_id, max_hits, window: opts.window };
+		const hitRouteOpts = {
+			bucket_id,
+			executor_id,
+			max_hits,
+			window: opts.window,
+		};
 
 		if (opts.error || opts.success) {
 			res.once("finish", () => {
 				// check if error and increment error rate limit
 				if (res.statusCode >= 400 && opts.error) {
 					return hitRoute(hitRouteOpts);
-				} else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) {
+				} else if (
+					res.statusCode >= 200 &&
+					res.statusCode < 300 &&
+					opts.success
+				) {
 					return hitRoute(hitRouteOpts);
 				}
 			});
@@ -141,8 +169,8 @@ export async function initRateLimits(app: Router) {
 		rateLimit({
 			bucket: "global",
 			onlyIp: true,
-			...ip
-		})
+			...ip,
+		}),
 	);
 	app.use(rateLimit({ bucket: "global", ...global }));
 	app.use(
@@ -150,17 +178,25 @@ export async function initRateLimits(app: Router) {
 			bucket: "error",
 			error: true,
 			onlyIp: true,
-			...error
-		})
+			...error,
+		}),
 	);
 	app.use("/guilds/:id", rateLimit(routes.guild));
 	app.use("/webhooks/:id", rateLimit(routes.webhook));
 	app.use("/channels/:id", rateLimit(routes.channel));
 	app.use("/auth/login", rateLimit(routes.auth.login));
-	app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register }));
+	app.use(
+		"/auth/register",
+		rateLimit({ onlyIp: true, success: true, ...routes.auth.register }),
+	);
 }
 
-async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) {
+async function hitRoute(opts: {
+	executor_id: string;
+	bucket_id: string;
+	max_hits: number;
+	window: number;
+}) {
 	const id = opts.executor_id + opts.bucket_id;
 	let limit = Cache.get(id);
 	if (!limit) {
@@ -169,7 +205,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
 			executor_id: opts.executor_id,
 			expires_at: new Date(Date.now() + opts.window * 1000),
 			hits: 0,
-			blocked: false
+			blocked: false,
 		};
 		Cache.set(id, limit);
 	}
@@ -205,4 +241,4 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
 	}
 	await ratelimit.save();
 	*/
-}
\ No newline at end of file
+}
diff --git a/src/api/middlewares/TestClient.ts b/src/api/middlewares/TestClient.ts
index e68abf98..62128b36 100644
--- a/src/api/middlewares/TestClient.ts
+++ b/src/api/middlewares/TestClient.ts
@@ -2,7 +2,7 @@ import express, { Request, Response, Application } from "express";
 import fs from "fs";
 import path from "path";
 import fetch, { Response as FetchResponse } from "node-fetch";
-import ProxyAgent from 'proxy-agent';
+import ProxyAgent from "proxy-agent";
 import { Config } from "@fosscord/util";
 
 const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
@@ -11,40 +11,67 @@ let hasWarnedAboutCache = false;
 
 export default function TestClient(app: Application) {
 	const agent = new ProxyAgent();
-	const assetCache = new Map<string, { response: FetchResponse; buffer: Buffer; }>();
-	const indexHTML = fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "index.html"), { encoding: "utf8" });
+	const assetCache = new Map<
+		string,
+		{ response: FetchResponse; buffer: Buffer }
+	>();
+	const indexHTML = fs.readFileSync(
+		path.join(ASSET_FOLDER_PATH, "client_test", "index.html"),
+		{ encoding: "utf8" },
+	);
 
 	var html = indexHTML;
-	const CDN_ENDPOINT = (Config.get().cdn.endpointClient || Config.get()?.cdn.endpointPublic || process.env.CDN || "").replace(
-		/(https?)?(:\/\/?)/g,
+	const CDN_ENDPOINT = (
+		Config.get().cdn.endpointClient ||
+		Config.get()?.cdn.endpointPublic ||
+		process.env.CDN ||
 		""
-	);
-	const GATEWAY_ENDPOINT = Config.get().gateway.endpointClient || Config.get()?.gateway.endpointPublic || process.env.GATEWAY || "";
+	).replace(/(https?)?(:\/\/?)/g, "");
+	const GATEWAY_ENDPOINT =
+		Config.get().gateway.endpointClient ||
+		Config.get()?.gateway.endpointPublic ||
+		process.env.GATEWAY ||
+		"";
 
 	if (CDN_ENDPOINT) {
 		html = html.replace(/CDN_HOST: .+/, `CDN_HOST: \`${CDN_ENDPOINT}\`,`);
 	}
 	if (GATEWAY_ENDPOINT) {
-		html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: \`${GATEWAY_ENDPOINT}\`,`);
+		html = html.replace(
+			/GATEWAY_ENDPOINT: .+/,
+			`GATEWAY_ENDPOINT: \`${GATEWAY_ENDPOINT}\`,`,
+		);
 	}
 	// inline plugins
 	var files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
 	var plugins = "";
-	files.forEach(x => { if (x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(ASSET_FOLDER_PATH, "preload-plugins", x))}</script>\n`; });
+	files.forEach((x) => {
+		if (x.endsWith(".js"))
+			plugins += `<script>${fs.readFileSync(
+				path.join(ASSET_FOLDER_PATH, "preload-plugins", x),
+			)}</script>\n`;
+	});
 	html = html.replaceAll("<!-- preload plugin marker -->", plugins);
 
 	// plugins
 	files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "plugins"));
 	plugins = "";
-	files.forEach(x => { if (x.endsWith(".js")) plugins += `<script src='/assets/plugins/${x}'></script>\n`; });
+	files.forEach((x) => {
+		if (x.endsWith(".js"))
+			plugins += `<script src='/assets/plugins/${x}'></script>\n`;
+	});
 	html = html.replaceAll("<!-- plugin marker -->", plugins);
 	//preload plugins
 	files = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins"));
 	plugins = "";
-	files.forEach(x => { if (x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(ASSET_FOLDER_PATH, "preload-plugins", x))}</script>\n`; });
+	files.forEach((x) => {
+		if (x.endsWith(".js"))
+			plugins += `<script>${fs.readFileSync(
+				path.join(ASSET_FOLDER_PATH, "preload-plugins", x),
+			)}</script>\n`;
+	});
 	html = html.replaceAll("<!-- preload plugin marker -->", plugins);
 
-
 	app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "public")));
 	app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "cache")));
 
@@ -52,7 +79,9 @@ export default function TestClient(app: Application) {
 		if (!hasWarnedAboutCache) {
 			hasWarnedAboutCache = true;
 			if (req.params.file.includes(".js"))
-				console.warn(`[TestClient] Cache miss for file ${req.params.file}! Use 'npm run generate:client' to cache and patch.`);
+				console.warn(
+					`[TestClient] Cache miss for file ${req.params.file}! Use 'npm run generate:client' to cache and patch.`,
+				);
 		}
 
 		delete req.headers.host;
@@ -60,13 +89,16 @@ export default function TestClient(app: Application) {
 		var buffer: Buffer;
 		const cache = assetCache.get(req.params.file);
 		if (!cache) {
-			response = await fetch(`https://discord.com/assets/${req.params.file}`, {
-				agent,
-				// @ts-ignore
-				headers: {
-					...req.headers
-				}
-			});
+			response = await fetch(
+				`https://discord.com/assets/${req.params.file}`,
+				{
+					agent,
+					// @ts-ignore
+					headers: {
+						...req.headers,
+					},
+				},
+			);
 			buffer = await response.buffer();
 		} else {
 			response = cache.response;
@@ -83,7 +115,7 @@ export default function TestClient(app: Application) {
 					"transfer-encoding",
 					"expect-ct",
 					"access-control-allow-origin",
-					"content-encoding"
+					"content-encoding",
 				].includes(name.toLowerCase())
 			) {
 				return;
@@ -99,20 +131,35 @@ export default function TestClient(app: Application) {
 		res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
 		res.set("content-type", "text/html");
 
-		if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance.");
-
-		res.send(fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "developers.html"), { encoding: "utf8" }));
+		if (!useTestClient)
+			return res.send(
+				"Test client is disabled on this instance. Use a stand-alone client to connect this instance.",
+			);
+
+		res.send(
+			fs.readFileSync(
+				path.join(ASSET_FOLDER_PATH, "client_test", "developers.html"),
+				{ encoding: "utf8" },
+			),
+		);
 	});
 	app.get("*", (req: Request, res: Response) => {
 		const { useTestClient } = Config.get().client;
 		res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
 		res.set("content-type", "text/html");
 
-		if (req.url.startsWith("/api") || req.url.startsWith("/__development")) return;
+		if (req.url.startsWith("/api") || req.url.startsWith("/__development"))
+			return;
 
-		if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance.");
-		if (req.url.startsWith("/invite")) return res.send(html.replace("9b2b7f0632acd0c5e781", "9f24f709a3de09b67c49"));
+		if (!useTestClient)
+			return res.send(
+				"Test client is disabled on this instance. Use a stand-alone client to connect this instance.",
+			);
+		if (req.url.startsWith("/invite"))
+			return res.send(
+				html.replace("9b2b7f0632acd0c5e781", "9f24f709a3de09b67c49"),
+			);
 
 		res.send(html);
 	});
-}
\ No newline at end of file
+}
diff --git a/src/api/middlewares/Translation.ts b/src/api/middlewares/Translation.ts
index c0b7a4b8..05038040 100644
--- a/src/api/middlewares/Translation.ts
+++ b/src/api/middlewares/Translation.ts
@@ -9,8 +9,12 @@ const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets");
 
 export async function initTranslation(router: Router) {
 	const languages = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales"));
-	const namespaces = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "locales", "en"));
-	const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5));
+	const namespaces = fs.readdirSync(
+		path.join(ASSET_FOLDER_PATH, "locales", "en"),
+	);
+	const ns = namespaces
+		.filter((x) => x.endsWith(".json"))
+		.map((x) => x.slice(0, x.length - 5));
 
 	await i18next
 		.use(i18nextBackend)
@@ -21,9 +25,11 @@ export async function initTranslation(router: Router) {
 			fallbackLng: "en",
 			ns,
 			backend: {
-				loadPath:  path.join(ASSET_FOLDER_PATH, "locales") + "/{{lng}}/{{ns}}.json",
+				loadPath:
+					path.join(ASSET_FOLDER_PATH, "locales") +
+					"/{{lng}}/{{ns}}.json",
 			},
-			load: "all"
+			load: "all",
 		});
 
 	router.use(i18nextMiddleware.handle(i18next, {}));