diff options
Diffstat (limited to 'src/api/middlewares')
-rw-r--r-- | src/api/middlewares/Authentication.ts | 18 | ||||
-rw-r--r-- | src/api/middlewares/BodyParser.ts | 3 | ||||
-rw-r--r-- | src/api/middlewares/CORS.ts | 12 | ||||
-rw-r--r-- | src/api/middlewares/ErrorHandler.ts | 25 | ||||
-rw-r--r-- | src/api/middlewares/RateLimit.ts | 70 | ||||
-rw-r--r-- | src/api/middlewares/TestClient.ts | 103 | ||||
-rw-r--r-- | src/api/middlewares/Translation.ts | 14 |
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, {})); |