diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts
index a8bfe196..a300c786 100644
--- a/api/src/middlewares/Authentication.ts
+++ b/api/src/middlewares/Authentication.ts
@@ -18,9 +18,9 @@ export const API_PREFIX_TRAILING_SLASH = /^\/api(\/v\d+)?\//;
declare global {
namespace Express {
interface Request {
- user_id: any;
+ user_id: string;
user_bot: boolean;
- token: any;
+ token: string;
}
}
}
@@ -28,7 +28,7 @@ declare global {
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(); // @ts-ignore
+ if (url.startsWith("/invites") && req.method === "GET") return next();
if (
NO_AUTHORIZATION_ROUTES.some((x) => {
if (typeof x === "string") return url.startsWith(x);
@@ -47,7 +47,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
req.user_id = decoded.id;
req.user_bot = user.bot;
return next();
- } catch (error) {
- return next(new HTTPError(error.toString(), 400));
+ } catch (error: any) {
+ return next(new HTTPError(error?.toString(), 400));
}
}
diff --git a/api/src/middlewares/ErrorHandler.ts b/api/src/middlewares/ErrorHandler.ts
index d080e498..f061172a 100644
--- a/api/src/middlewares/ErrorHandler.ts
+++ b/api/src/middlewares/ErrorHandler.ts
@@ -1,9 +1,12 @@
import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
+import { EntityNotFoundError } from "typeorm";
import { FieldError } from "../util/instanceOf";
+import {ApiError} from "../util/ApiError";
+// TODO: update with new body/typorm validation
export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) {
- if (!error) next();
+ if (!error) return next();
try {
let code = 400;
@@ -12,13 +15,23 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
let errors = undefined;
if (error instanceof HTTPError && error.code) code = httpcode = error.code;
- else if (error instanceof FieldError) {
+ else if (error instanceof ApiError) {
+ code = error.code;
+ message = error.message;
+ httpcode = error.httpStatus;
+ }
+ else if (error instanceof EntityNotFoundError) {
+ message = `${(error as any).stringifyTarget} can not be found`;
+ code = 404;
+ } else if (error instanceof FieldError) {
code = Number(error.code);
message = error.message;
errors = error.errors;
} else {
- console.error(error);
+ console.error(`[Error] ${code} ${req.url}`, errors || error, "body:", req.body);
+
if (req.server?.options?.production) {
+ // don't expose internal errors to the user, instead human errors should be thrown as HTTPError
message = "Internal Server Error";
}
code = httpcode = 500;
@@ -26,8 +39,6 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne
if (httpcode > 511) httpcode = 400;
- console.error(`[Error] ${code} ${req.url} ${message}`, errors || error);
-
res.status(httpcode).json({ code: code, message, errors });
} catch (error) {
console.error(`[Internal Server Error] 500`, error);
diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index acf92606..dffbc0d9 100644
--- a/api/src/middlewares/RateLimit.ts
+++ b/api/src/middlewares/RateLimit.ts
@@ -1,11 +1,12 @@
-// @ts-nocheck
-import { db, Bucket, Config, listenEvent, emitEvent } from "@fosscord/util";
+import { Config, listenEvent } from "@fosscord/util";
import { NextFunction, Request, Response, Router } from "express";
import { getIpAdress } from "../util/ipAddress";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
// Docs: https://discord.com/developers/docs/topics/rate-limits
+// TODO: use better caching (e.g. redis) as else it creates to much pressure on the database
+
/*
? bucket limit? Max actions/sec per bucket?
@@ -18,10 +19,18 @@ TODO: different for methods (GET/POST)
*/
-var Cache = new Map<string, Bucket>();
-const EventRateLimit = "ratelimit";
+type RateLimit = {
+ id: "global" | "error" | string;
+ executor_id: string;
+ hits: number;
+ blocked: boolean;
+ expires_at: Date;
+};
+
+var Cache = new Map<string, RateLimit>();
+const EventRateLimit = "RATELIMIT";
-export default function RateLimit(opts: {
+export default function rateLimit(opts: {
bucket?: string;
window: number;
count: number;
@@ -36,23 +45,32 @@ export default function RateLimit(opts: {
}): any {
return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
- var user_id = getIpAdress(req);
- if (!opts.onlyIp && req.user_id) user_id = req.user_id;
+ var executor_id = getIpAdress(req);
+ if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
var 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(user_id + bucket_id) as Bucket | null;
+ const offender = Cache.get(executor_id + bucket_id);
- if (offender && offender.blocked) {
+ if (offender) {
const reset = offender.expires_at.getTime();
const resetAfterMs = reset - Date.now();
const resetAfterSec = resetAfterMs / 1000;
- const global = bucket_id === "global";
- if (resetAfterMs > 0) {
+ if (resetAfterMs <= 0) {
+ offender.hits = 0;
+ offender.expires_at = new Date(Date.now() + opts.window * 1000);
+ offender.blocked = false;
+
+ Cache.delete(executor_id + bucket_id);
+ }
+
+ if (offender.blocked) {
+ const global = bucket_id === "global";
+
console.log("blocked bucket: " + bucket_id, { resetAfterMs });
return (
res
@@ -67,17 +85,11 @@ export default function RateLimit(opts: {
// TODO: error rate limit message translation
.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global })
);
- } else {
- offender.hits = 0;
- offender.expires_at = new Date(Date.now() + opts.window * 1000);
- offender.blocked = false;
- // mongodb ttl didn't update yet -> manually update/delete
- db.collection("ratelimits").updateOne({ id: bucket_id, user_id }, { $set: offender });
- Cache.delete(user_id + bucket_id);
}
}
+
next();
- const hitRouteOpts = { bucket_id, user_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", () => {
@@ -97,69 +109,94 @@ export default function RateLimit(opts: {
export async function initRateLimits(app: Router) {
const { routes, global, ip, error } = Config.get().limits.rate;
await listenEvent(EventRateLimit, (event) => {
- Cache.set(event.channel_id, event.data);
+ Cache.set(event.channel_id as string, event.data);
event.acknowledge?.();
});
+ // await RateLimit.delete({ expires_at: LessThan(new Date().toISOString()) }); // cleans up if not already deleted, morethan -> older date
+ // const limits = await RateLimit.find({ blocked: true });
+ // limits.forEach((limit) => {
+ // Cache.set(limit.executor_id, limit);
+ // });
setInterval(() => {
Cache.forEach((x, key) => {
- if (Date.now() > x.expires_at) Cache.delete(key);
+ if (new Date() > x.expires_at) {
+ Cache.delete(key);
+ // RateLimit.delete({ executor_id: key });
+ }
});
- }, 1000 * 60 * 10);
+ }, 1000 * 60);
app.use(
- RateLimit({
+ rateLimit({
bucket: "global",
onlyIp: true,
...ip
})
);
- app.use(RateLimit({ bucket: "global", ...global }));
+ app.use(rateLimit({ bucket: "global", ...global }));
app.use(
- RateLimit({
+ rateLimit({
bucket: "error",
error: true,
onlyIp: true,
...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("/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 }));
}
-async function hitRoute(opts: { user_id: string; bucket_id: string; max_hits: number; window: number }) {
- const filter = { id: opts.bucket_id, user_id: opts.user_id };
- const { value } = await db.collection("ratelimits").findOneAndUpdate(
- filter,
- {
- $setOnInsert: {
- id: opts.bucket_id,
- user_id: opts.user_id,
- expires_at: new Date(Date.now() + opts.window * 1000)
- },
- $inc: {
- hits: 1
- }
- // Conditionally update blocked doesn't work
- },
- { upsert: true, returnDocument: "before" }
- );
- if (!value) return;
- const updateBlock = !value.blocked && value.hits >= opts.max_hits;
+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);
+ if (!limit) {
+ limit = {
+ id: opts.bucket_id,
+ executor_id: opts.executor_id,
+ expires_at: new Date(Date.now() + opts.window * 1000),
+ hits: 0,
+ blocked: false
+ };
+ Cache.set(id, limit);
+ }
+
+ limit.hits++;
+ if (limit.hits >= opts.max_hits) {
+ limit.blocked = true;
+ }
+
+ /*
+ var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id });
+ if (!ratelimit) {
+ ratelimit = new RateLimit({
+ id: opts.bucket_id,
+ executor_id: opts.executor_id,
+ expires_at: new Date(Date.now() + opts.window * 1000),
+ hits: 0,
+ blocked: false
+ });
+ }
+
+ ratelimit.hits++;
+
+ const updateBlock = !ratelimit.blocked && ratelimit.hits >= opts.max_hits;
if (updateBlock) {
- value.blocked = true;
- Cache.set(opts.user_id + opts.bucket_id, value);
+ ratelimit.blocked = true;
+ Cache.set(opts.executor_id + opts.bucket_id, ratelimit);
await emitEvent({
channel_id: EventRateLimit,
event: EventRateLimit,
- data: value
+ data: ratelimit
});
- await db.collection("ratelimits").updateOne(filter, { $set: { blocked: true } });
} else {
- Cache.delete(opts.user_id);
+ Cache.delete(opts.executor_id);
}
+
+ await ratelimit.save();
+ */
}
diff --git a/api/src/middlewares/Translation.ts b/api/src/middlewares/Translation.ts
index edc14707..baabf221 100644
--- a/api/src/middlewares/Translation.ts
+++ b/api/src/middlewares/Translation.ts
@@ -19,7 +19,7 @@ export async function initTranslation(router: Router) {
fallbackLng: "en",
ns,
backend: {
- loadPath: __dirname + "/../locales/{{lng}}/{{ns}}.json"
+ loadPath: __dirname + "/../../locales/{{lng}}/{{ns}}.json"
},
load: "all"
});
|