diff --git a/api/src/middlewares/TestClient.ts b/api/src/middlewares/TestClient.ts
deleted file mode 100644
index ecf87681..00000000
--- a/api/src/middlewares/TestClient.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-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 { Config } from "@fosscord/util";
-
-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(__dirname, "..", "..", "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 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}\`,`);
- }
- // inline plugins
- var files = fs.readdirSync(path.join(__dirname, "..", "..", "assets", "preload-plugins"));
- var plugins = "";
- files.forEach(x =>{if(x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(__dirname, "..", "..", "assets", "preload-plugins", x))}</script>\n`; });
- html = html.replaceAll("<!-- preload plugin marker -->", plugins);
-
- // plugins
- files = fs.readdirSync(path.join(__dirname, "..", "..", "assets", "plugins"));
- plugins = "";
- 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(__dirname, "..", "..", "assets", "preload-plugins"));
- plugins = "";
- files.forEach(x =>{if(x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(__dirname, "..", "..", "assets", "preload-plugins", x))}</script>\n`; });
- html = html.replaceAll("<!-- preload plugin marker -->", plugins);
-
-
- app.use("/assets", express.static(path.join(__dirname, "..", "..", "assets")));
-
- app.get("/assets/:file", async (req: Request, res: Response) => {
- delete req.headers.host;
- var response: FetchResponse;
- 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
- }
- });
- buffer = await response.buffer();
- } else {
- response = cache.response;
- buffer = cache.buffer;
- }
-
- response.headers.forEach((value, name) => {
- if (
- [
- "content-length",
- "content-security-policy",
- "strict-transport-security",
- "set-cookie",
- "transfer-encoding",
- "expect-ct",
- "access-control-allow-origin",
- "content-encoding"
- ].includes(name.toLowerCase())
- ) {
- return;
- }
- res.set(name, value);
- });
- assetCache.set(req.params.file, { buffer, response });
-
- return res.send(buffer);
- });
- app.get("/developers*", (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(!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(__dirname, "..", "..", "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(!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/api/src/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 5a08caf3..2d9ccf57 100644
--- a/api/src/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
+import { HTTPError } from "@fosscord/util";
import { checkToken, Config, Rights } from "@fosscord/util";
export const NO_AUTHORIZATION_ROUTES = [
@@ -7,6 +7,7 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/login",
"/auth/register",
"/auth/location-metadata",
+ "/auth/mfa/totp",
// Routes with a seperate auth system
"/webhooks/",
// Public information endpoints
diff --git a/api/src/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts
index 4cb376bc..35db3c6f 100644
--- a/api/src/middlewares/BodyParser.ts
+++ b/src/api/middlewares/BodyParser.ts
@@ -1,6 +1,6 @@
import bodyParser, { OptionsJson } from "body-parser";
import { NextFunction, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
+import { HTTPError } from "@fosscord/util";
export function BodyParser(opts?: OptionsJson) {
const jsonParser = bodyParser.json(opts);
diff --git a/api/src/middlewares/CORS.ts b/src/api/middlewares/CORS.ts
index 20260cf9..20260cf9 100644
--- a/api/src/middlewares/CORS.ts
+++ b/src/api/middlewares/CORS.ts
diff --git a/api/src/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts
index 2012b91c..8a046e06 100644
--- a/api/src/middlewares/ErrorHandler.ts
+++ b/src/api/middlewares/ErrorHandler.ts
@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
+import { HTTPError } from "@fosscord/util";
import { ApiError, FieldError } from "@fosscord/util";
const EntityNotFoundErrorRegex = /"(\w+)"/;
diff --git a/api/src/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 1a38cfcf..47180b62 100644
--- a/api/src/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -1,4 +1,4 @@
-import { Config, listenEvent } from "@fosscord/util";
+import { Config, getRights, listenEvent, Rights } from "@fosscord/util";
import { NextFunction, Request, Response, Router } from "express";
import { getIpAdress } from "@fosscord/api";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
@@ -9,6 +9,7 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
/*
? bucket limit? Max actions/sec per bucket?
+(ANSWER: a small fosscord instance might not need a complex rate limiting system)
TODO: delay database requests to include multiple queries
TODO: different for methods (GET/POST)
@@ -27,7 +28,7 @@ type RateLimit = {
expires_at: Date;
};
-var Cache = new Map<string, RateLimit>();
+let Cache = new Map<string, RateLimit>();
const EventRateLimit = "RATELIMIT";
export default function rateLimit(opts: {
@@ -44,21 +45,27 @@ export default function rateLimit(opts: {
onlyIp?: boolean;
}): 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;
+ }
+
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
- var executor_id = getIpAdress(req);
+ let executor_id = getIpAdress(req);
if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
- var max_hits = opts.count;
+ 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;
- const offender = Cache.get(executor_id + bucket_id);
+ let offender = Cache.get(executor_id + bucket_id);
if (offender) {
- const reset = offender.expires_at.getTime();
- const resetAfterMs = reset - Date.now();
- const resetAfterSec = resetAfterMs / 1000;
+ let reset = offender.expires_at.getTime();
+ let resetAfterMs = reset - Date.now();
+ let resetAfterSec = Math.ceil(resetAfterMs / 1000);
if (resetAfterMs <= 0) {
offender.hits = 0;
@@ -70,6 +77,11 @@ export default function rateLimit(opts: {
if (offender.blocked) {
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);
+ resetAfterMs = reset - Date.now();
+ resetAfterSec = Math.ceil(resetAfterMs / 1000);
console.log("blocked bucket: " + bucket_id, { resetAfterMs });
return (
@@ -151,9 +163,9 @@ export async function initRateLimits(app: Router) {
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;
- var limit = Cache.get(id);
+ let limit = Cache.get(id);
if (!limit) {
limit = {
id: opts.bucket_id,
@@ -171,7 +183,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
}
/*
- var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id });
+ let ratelimit = await RateLimit.findOne({ where: { id: opts.bucket_id, executor_id: opts.executor_id } });
if (!ratelimit) {
ratelimit = new RateLimit({
id: opts.bucket_id,
diff --git a/api/src/middlewares/Translation.ts b/src/api/middlewares/Translation.ts
index baabf221..64b03bf8 100644
--- a/api/src/middlewares/Translation.ts
+++ b/src/api/middlewares/Translation.ts
@@ -6,8 +6,8 @@ import i18nextBackend from "i18next-node-fs-backend";
import { Router } from "express";
export async function initTranslation(router: Router) {
- const languages = fs.readdirSync(path.join(__dirname, "..", "..", "locales"));
- const namespaces = fs.readdirSync(path.join(__dirname, "..", "..", "locales", "en"));
+ const languages = fs.readdirSync(path.join(__dirname, "..", "..", "..", "assets", "locales"));
+ const namespaces = fs.readdirSync(path.join(__dirname, "..", "..", "..", "assets", "locales", "en"));
const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5));
await i18next
@@ -19,7 +19,7 @@ export async function initTranslation(router: Router) {
fallbackLng: "en",
ns,
backend: {
- loadPath: __dirname + "/../../locales/{{lng}}/{{ns}}.json"
+ loadPath: __dirname + "/../../../assets/locales/{{lng}}/{{ns}}.json"
},
load: "all"
});
diff --git a/api/src/middlewares/index.ts b/src/api/middlewares/index.ts
index f0c50dbe..f0c50dbe 100644
--- a/api/src/middlewares/index.ts
+++ b/src/api/middlewares/index.ts
|