Add new rate limiting flow (#3230) Bruception

* Add new rate limiting flow

* Oops

* Fix nit

* Fix some bugs

* Split key generation functions

* Remove 429

* Change message for root limiter

* Flag 429 and add config

* Add status code config

* Check enabled flag

* Add custom status for ape keys

* Bump coverage

* swapped conditions around
whats the point of checking if the status code is in the array if the whole thing is turned off anyway

Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
Bruce Berrios 2022-06-28 07:45:57 -04:00 committed by GitHub
parent bda7788bb2
commit 9da5e441be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 368 additions and 179 deletions

View file

@ -9,8 +9,8 @@ export default {
// These percentages should never decrease
statements: 38,
branches: 38,
functions: 21,
lines: 41,
functions: 22,
lines: 42,
},
},
};

View file

@ -29,6 +29,7 @@
"object-hash": "3.0.0",
"path": "0.12.7",
"prom-client": "14.0.1",
"rate-limiter-flexible": "2.3.7",
"simple-git": "2.45.1",
"string-similarity": "4.0.4",
"swagger-stats": "0.99.2",
@ -7167,6 +7168,11 @@
"node": ">= 0.6"
}
},
"node_modules/rate-limiter-flexible": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz",
"integrity": "sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw=="
},
"node_modules/raw-body": {
"version": "2.4.0",
"license": "MIT",
@ -13810,6 +13816,11 @@
"range-parser": {
"version": "1.2.1"
},
"rate-limiter-flexible": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz",
"integrity": "sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw=="
},
"raw-body": {
"version": "2.4.0",
"requires": {

View file

@ -36,6 +36,7 @@
"object-hash": "3.0.0",
"path": "0.12.7",
"prom-client": "14.0.1",
"rate-limiter-flexible": "2.3.7",
"simple-git": "2.45.1",
"string-similarity": "4.0.4",
"swagger-stats": "0.99.2",

View file

@ -14,11 +14,13 @@ const buildNumberInput = (schema, parentState, key) => {
input.classList.add("base-input");
input.type = "number";
input.value = parentState[key];
input.min = schema.min || 0;
const min = schema.min || 0;
input.min = min;
input.addEventListener("change", () => {
const normalizedValue = parseFloat(input.value, 10);
parentState[key] = normalizedValue;
parentState[key] = Math.max(normalizedValue, min);
});
return input;
@ -164,7 +166,7 @@ const render = (state, schema) => {
items,
element,
state,
`${currentKey}[${index}]`,
index,
`${path}[${index}]`
);

View file

@ -40,16 +40,16 @@ router.use(
router.get(
"/",
RateLimit.apeKeysGet,
authenticateRequest(),
RateLimit.apeKeysGet,
checkIfUserCanManageApeKeys,
asyncHandler(ApeKeyController.getApeKeys)
);
router.post(
"/",
RateLimit.apeKeysGenerate,
authenticateRequest(),
RateLimit.apeKeysGenerate,
checkIfUserCanManageApeKeys,
validateRequest({
body: {
@ -62,8 +62,8 @@ router.post(
router.patch(
"/:apeKeyId",
RateLimit.apeKeysUpdate,
authenticateRequest(),
RateLimit.apeKeysUpdate,
checkIfUserCanManageApeKeys,
validateRequest({
params: {
@ -79,8 +79,8 @@ router.patch(
router.delete(
"/:apeKeyId",
RateLimit.apeKeysDelete,
authenticateRequest(),
RateLimit.apeKeysDelete,
checkIfUserCanManageApeKeys,
validateRequest({
params: {

View file

@ -9,15 +9,15 @@ const router = Router();
router.get(
"/",
RateLimit.configGet,
authenticateRequest(),
RateLimit.configGet,
asyncHandler(ConfigController.getConfig)
);
router.patch(
"/",
RateLimit.configUpdate,
authenticateRequest(),
RateLimit.configUpdate,
validateRequest({
body: {
config: configSchema.required(),

View file

@ -1,7 +1,7 @@
import joi from "joi";
import { Router } from "express";
import * as RateLimit from "../../middlewares/rate-limit";
import apeRateLimit from "../../middlewares/ape-rate-limit";
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
import { authenticateRequest } from "../../middlewares/auth";
import * as LeaderboardController from "../controllers/leaderboard";
import {
@ -38,8 +38,8 @@ const requireDailyLeaderboardsEnabled = validateConfiguration({
router.get(
"/",
RateLimit.leaderboardsGet,
authenticateRequest({ isPublic: true, acceptApeKeys: true }),
withApeRateLimiter(RateLimit.leaderboardsGet),
validateRequest({
query: LEADERBOARD_VALIDATION_SCHEMA_WITH_LIMIT,
}),
@ -48,9 +48,8 @@ router.get(
router.get(
"/rank",
RateLimit.leaderboardsGet,
authenticateRequest({ acceptApeKeys: true }),
apeRateLimit,
withApeRateLimiter(RateLimit.leaderboardsGet),
validateRequest({
query: BASE_LEADERBOARD_VALIDATION_SCHEMA,
}),
@ -60,8 +59,8 @@ router.get(
router.get(
"/daily",
requireDailyLeaderboardsEnabled,
RateLimit.leaderboardsGet,
authenticateRequest({ isPublic: true }),
RateLimit.leaderboardsGet,
validateRequest({
query: DAILY_LEADERBOARD_VALIDATION_SCHEMA,
}),
@ -71,8 +70,8 @@ router.get(
router.get(
"/daily/rank",
requireDailyLeaderboardsEnabled,
RateLimit.leaderboardsGet,
authenticateRequest(),
RateLimit.leaderboardsGet,
validateRequest({
query: DAILY_LEADERBOARD_VALIDATION_SCHEMA,
}),

View file

@ -20,15 +20,15 @@ const presetNameSchema = joi
router.get(
"/",
RateLimit.presetsGet,
authenticateRequest(),
RateLimit.presetsGet,
asyncHandler(PresetController.getPresets)
);
router.post(
"/",
RateLimit.presetsAdd,
authenticateRequest(),
RateLimit.presetsAdd,
validateRequest({
body: {
name: presetNameSchema,
@ -42,8 +42,8 @@ router.post(
router.patch(
"/",
RateLimit.presetsEdit,
authenticateRequest(),
RateLimit.presetsEdit,
validateRequest({
body: {
_id: joi.string().required(),
@ -60,8 +60,8 @@ router.patch(
router.delete(
"/:presetId",
RateLimit.presetsRemove,
authenticateRequest(),
RateLimit.presetsRemove,
validateRequest({
params: {
presetId: joi.string().required(),

View file

@ -1,7 +1,7 @@
import { Router } from "express";
import * as PsaController from "../controllers/psa";
import * as RateLimit from "../../middlewares/rate-limit";
import { asyncHandler } from "../../middlewares/api-utils";
import { Router } from "express";
const router = Router();

View file

@ -20,8 +20,8 @@ const checkIfUserIsQuoteMod = checkUserPermissions({
router.get(
"/",
RateLimit.newQuotesGet,
authenticateRequest(),
RateLimit.newQuotesGet,
checkIfUserIsQuoteMod,
asyncHandler(QuoteController.getQuotes)
);
@ -35,8 +35,8 @@ router.post(
invalidMessage:
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
}),
RateLimit.newQuotesAdd,
authenticateRequest(),
RateLimit.newQuotesAdd,
validateRequest({
body: {
text: joi.string().min(60).required(),
@ -51,8 +51,8 @@ router.post(
router.post(
"/approve",
RateLimit.newQuotesAction,
authenticateRequest(),
RateLimit.newQuotesAction,
validateRequest({
body: {
quoteId: joi.string().required(),
@ -67,8 +67,8 @@ router.post(
router.post(
"/reject",
RateLimit.newQuotesAction,
authenticateRequest(),
RateLimit.newQuotesAction,
validateRequest({
body: {
quoteId: joi.string().required(),
@ -80,8 +80,8 @@ router.post(
router.get(
"/rating",
RateLimit.quoteRatingsGet,
authenticateRequest(),
RateLimit.quoteRatingsGet,
validateRequest({
query: {
quoteId: joi.string().regex(/^\d+$/).required(),
@ -93,8 +93,8 @@ router.get(
router.post(
"/rating",
RateLimit.quoteRatingsSubmit,
authenticateRequest(),
RateLimit.quoteRatingsSubmit,
validateRequest({
body: {
quoteId: joi.number().required(),
@ -113,8 +113,8 @@ router.post(
},
invalidMessage: "Quote reporting is unavailable.",
}),
RateLimit.quoteReportSubmit,
authenticateRequest(),
RateLimit.quoteReportSubmit,
validateRequest({
body: {
quoteId: joi.string().required(),

View file

@ -9,14 +9,14 @@ import * as RateLimit from "../../middlewares/rate-limit";
import { Router } from "express";
import { authenticateRequest } from "../../middlewares/auth";
import joi from "joi";
import apeRateLimit from "../../middlewares/ape-rate-limit";
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
const router = Router();
router.get(
"/",
RateLimit.resultsGet,
authenticateRequest(),
RateLimit.resultsGet,
asyncHandler(ResultController.getResults)
);
@ -28,8 +28,8 @@ router.post(
},
invalidMessage: "Results are not being saved at this time.",
}),
RateLimit.resultsAdd,
authenticateRequest(),
RateLimit.resultsAdd,
validateRequest({
body: {
result: resultSchema,
@ -40,8 +40,8 @@ router.post(
router.patch(
"/tags",
RateLimit.resultsTagsUpdate,
authenticateRequest(),
RateLimit.resultsTagsUpdate,
validateRequest({
body: {
tagIds: joi.array().items(joi.string()).required(),
@ -53,18 +53,17 @@ router.patch(
router.delete(
"/",
RateLimit.resultsDeleteAll,
authenticateRequest(),
RateLimit.resultsDeleteAll,
asyncHandler(ResultController.deleteAll)
);
router.get(
"/last",
RateLimit.resultsGet,
authenticateRequest({
acceptApeKeys: true,
}),
apeRateLimit,
withApeRateLimiter(RateLimit.resultsGet),
asyncHandler(ResultController.getLastResult)
);

View file

@ -8,7 +8,7 @@ import {
validateConfiguration,
} from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
import apeRateLimit from "../../middlewares/ape-rate-limit";
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
import { containsProfanity, isUsernameValid } from "../../utils/validation";
import filterSchema from "../schemas/filter-schema";
@ -82,15 +82,15 @@ const quoteIdSchema = joi.string().min(1).max(5).regex(/\d+/).required();
router.get(
"/",
RateLimit.userGet,
authenticateRequest(),
RateLimit.userGet,
asyncHandler(UserController.getUser)
);
router.post(
"/signup",
RateLimit.userSignup,
authenticateRequest(),
RateLimit.userSignup,
validateRequest({
body: {
email: joi.string().email(),
@ -114,15 +114,15 @@ router.get(
router.delete(
"/",
RateLimit.userDelete,
authenticateRequest(),
RateLimit.userDelete,
asyncHandler(UserController.deleteUser)
);
router.patch(
"/name",
RateLimit.userUpdateName,
authenticateRequest(),
RateLimit.userUpdateName,
validateRequest({
body: {
name: usernameValidation,
@ -133,8 +133,8 @@ router.patch(
router.patch(
"/leaderboardMemory",
RateLimit.userUpdateLBMemory,
authenticateRequest(),
RateLimit.userUpdateLBMemory,
validateRequest({
body: {
mode: joi
@ -151,8 +151,8 @@ router.patch(
router.patch(
"/email",
RateLimit.userUpdateEmail,
authenticateRequest(),
RateLimit.userUpdateEmail,
validateRequest({
body: {
newEmail: joi.string().email().required(),
@ -164,8 +164,8 @@ router.patch(
router.delete(
"/personalBests",
RateLimit.userClearPB,
authenticateRequest(),
RateLimit.userClearPB,
asyncHandler(UserController.clearPb)
);
@ -178,9 +178,9 @@ const requireFilterPresetsEnabled = validateConfiguration({
router.post(
"/resultFilterPresets",
RateLimit.userCustomFilterAdd,
requireFilterPresetsEnabled,
authenticateRequest(),
RateLimit.userCustomFilterAdd,
validateRequest({
body: filterSchema,
}),
@ -189,9 +189,9 @@ router.post(
router.delete(
"/resultFilterPresets/:presetId",
RateLimit.userCustomFilterRemove,
requireFilterPresetsEnabled,
authenticateRequest(),
RateLimit.userCustomFilterRemove,
validateRequest({
params: {
presetId: joi.string().required(),
@ -202,15 +202,15 @@ router.delete(
router.get(
"/tags",
RateLimit.userTagsGet,
authenticateRequest(),
RateLimit.userTagsGet,
asyncHandler(UserController.getTags)
);
router.post(
"/tags",
RateLimit.userTagsAdd,
authenticateRequest(),
RateLimit.userTagsAdd,
validateRequest({
body: {
tagName: tagNameValidation,
@ -221,8 +221,8 @@ router.post(
router.patch(
"/tags",
RateLimit.userTagsEdit,
authenticateRequest(),
RateLimit.userTagsEdit,
validateRequest({
body: {
tagId: joi.string().required(),
@ -234,8 +234,8 @@ router.patch(
router.delete(
"/tags/:tagId",
RateLimit.userTagsRemove,
authenticateRequest(),
RateLimit.userTagsRemove,
validateRequest({
params: {
tagId: joi.string().required(),
@ -246,8 +246,8 @@ router.delete(
router.delete(
"/tags/:tagId/personalBest",
RateLimit.userTagsClearPB,
authenticateRequest(),
RateLimit.userTagsClearPB,
validateRequest({
params: {
tagId: joi.string().required(),
@ -258,15 +258,15 @@ router.delete(
router.get(
"/customThemes",
RateLimit.userCustomThemeGet,
authenticateRequest(),
RateLimit.userCustomThemeGet,
asyncHandler(UserController.getCustomThemes)
);
router.post(
"/customThemes",
RateLimit.userCustomThemeAdd,
authenticateRequest(),
RateLimit.userCustomThemeAdd,
validateRequest({
body: {
name: customThemeNameValidation,
@ -278,8 +278,8 @@ router.post(
router.delete(
"/customThemes",
RateLimit.userCustomThemeRemove,
authenticateRequest(),
RateLimit.userCustomThemeRemove,
validateRequest({
body: {
themeId: customThemeIdValidation,
@ -290,8 +290,8 @@ router.delete(
router.patch(
"/customThemes",
RateLimit.userCustomThemeEdit,
authenticateRequest(),
RateLimit.userCustomThemeEdit,
validateRequest({
body: {
themeId: customThemeIdValidation,
@ -313,9 +313,9 @@ const requireDiscordIntegrationEnabled = validateConfiguration({
router.post(
"/discord/link",
RateLimit.userDiscordLink,
requireDiscordIntegrationEnabled,
authenticateRequest(),
RateLimit.userDiscordLink,
validateRequest({
body: {
tokenType: joi.string().required(),
@ -327,18 +327,17 @@ router.post(
router.post(
"/discord/unlink",
RateLimit.userDiscordUnlink,
authenticateRequest(),
RateLimit.userDiscordUnlink,
asyncHandler(UserController.unlinkDiscord)
);
router.get(
"/personalBests",
RateLimit.userGet,
authenticateRequest({
acceptApeKeys: true,
}),
apeRateLimit,
withApeRateLimiter(RateLimit.userGet),
validateRequest({
query: {
mode: joi.string().required(),
@ -350,25 +349,24 @@ router.get(
router.get(
"/stats",
RateLimit.userGet,
authenticateRequest({
acceptApeKeys: true,
}),
apeRateLimit,
withApeRateLimiter(RateLimit.userGet),
asyncHandler(UserController.getStats)
);
router.get(
"/favoriteQuotes",
RateLimit.quoteFavoriteGet,
authenticateRequest(),
RateLimit.quoteFavoriteGet,
asyncHandler(UserController.getFavoriteQuotes)
);
router.post(
"/favoriteQuotes",
RateLimit.quoteFavoritePost,
authenticateRequest(),
RateLimit.quoteFavoritePost,
validateRequest({
body: {
language: languageSchema,
@ -380,8 +378,8 @@ router.post(
router.delete(
"/favoriteQuotes",
RateLimit.quoteFavoriteDelete,
authenticateRequest(),
RateLimit.quoteFavoriteDelete,
validateRequest({
body: {
language: languageSchema,
@ -400,11 +398,11 @@ const requireProfilesEnabled = validateConfiguration({
router.get(
"/:uid/profile",
RateLimit.userProfileGet,
requireProfilesEnabled,
authenticateRequest({
isPublic: true,
}),
RateLimit.userProfileGet,
validateRequest({
params: {
uid: joi.string().required(),
@ -427,9 +425,9 @@ const profileDetailsBase = joi
router.patch(
"/profile",
RateLimit.userProfileUpdate,
requireProfilesEnabled,
authenticateRequest(),
RateLimit.userProfileUpdate,
validateRequest({
body: {
bio: profileDetailsBase.max(150),

View file

@ -4,6 +4,10 @@ import addApiRoutes from "./api/routes";
import express, { urlencoded, json } from "express";
import contextMiddleware from "./middlewares/context";
import errorHandlingMiddleware from "./middlewares/error";
import {
badAuthRateLimiterHandler,
rootRateLimiter,
} from "./middlewares/rate-limit";
function buildApp(): express.Application {
const app = express();
@ -17,6 +21,9 @@ function buildApp(): express.Application {
app.use(contextMiddleware);
app.use(badAuthRateLimiterHandler);
app.use(rootRateLimiter);
addApiRoutes(app);
app.use(errorHandlingMiddleware);

View file

@ -42,7 +42,13 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
enabled: false,
},
},
rateLimiting: {
badAuthentication: {
enabled: false,
penalty: 0,
flaggedStatusCodes: [],
},
},
dailyLeaderboards: {
enabled: false,
maxResults: 0,
@ -54,7 +60,42 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
},
};
export const CONFIGURATION_FORM_SCHEMA = {
interface BaseSchema {
type: string;
label?: string;
}
interface NumberSchema extends BaseSchema {
type: "number";
min?: number;
}
interface BooleanSchema extends BaseSchema {
type: "boolean";
}
interface StringSchema extends BaseSchema {
type: "string";
}
interface ArraySchema extends BaseSchema {
type: "array";
items: Schema;
}
interface ObjectSchema extends BaseSchema {
type: "object";
fields: Record<string, Schema>;
}
type Schema =
| ObjectSchema
| ArraySchema
| StringSchema
| NumberSchema
| BooleanSchema;
export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
type: "object",
label: "Server Configuration",
fields: {
@ -198,7 +239,36 @@ export const CONFIGURATION_FORM_SCHEMA = {
},
},
},
rateLimiting: {
type: "object",
label: "Rate Limiting",
fields: {
badAuthentication: {
type: "object",
label: "Bad Authentication Rate Limiter",
fields: {
enabled: {
type: "boolean",
label: "Enabled",
},
penalty: {
type: "number",
label: "Penalty",
min: 0,
},
flaggedStatusCodes: {
type: "array",
label: "Flagged Status Codes",
items: {
label: "Status Code",
type: "number",
min: 0,
},
},
},
},
},
},
dailyLeaderboards: {
type: "object",
label: "Daily Leaderboards",

View file

@ -16,6 +16,7 @@ interface Statuses {
APE_KEY_INVALID: Status;
APE_KEY_INACTIVE: Status;
APE_KEY_MALFORMED: Status;
APE_KEY_RATE_LIMIT_EXCEEDED: Status;
}
const statuses: Statuses = {
@ -59,6 +60,10 @@ const statuses: Statuses = {
code: 472,
message: "ApeKey is malformed",
},
APE_KEY_RATE_LIMIT_EXCEEDED: {
code: 479,
message: "ApeKey rate limit exceeded",
},
};
const CUSTOM_STATUS_CODES = new Set(

View file

@ -1,31 +1,47 @@
import { Response, NextFunction } from "express";
import rateLimit, { Options } from "express-rate-limit";
import MonkeyError from "../utils/error";
import { Response, NextFunction, RequestHandler } from "express";
import statuses from "../constants/monkey-status-codes";
import rateLimit, {
RateLimitRequestHandler,
Options,
} from "express-rate-limit";
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1;
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 1 : 1;
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
return req?.ctx?.decodedToken?.uid;
};
const customHandler = (
const ONE_MINUTE = 1000 * 60;
const {
APE_KEY_RATE_LIMIT_EXCEEDED: { message, code },
} = statuses;
export const customHandler = (
_req: MonkeyTypes.Request,
_res: Response,
_next: NextFunction,
_options: Options
): void => {
throw new MonkeyError(429, "Too many attempts, please try again later.");
throw new MonkeyError(code, message);
};
const ONE_MINUTE = 1000 * 60;
export default rateLimit({
const apeRateLimiter = rateLimit({
windowMs: ONE_MINUTE,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getKey,
handler: customHandler,
skip: (req: MonkeyTypes.Request, _res) => {
const decodedToken = req?.ctx?.decodedToken;
return decodedToken?.type !== "ApeKey";
},
});
export function withApeRateLimiter(
defaultRateLimiter: RateLimitRequestHandler
): RequestHandler {
return (req: MonkeyTypes.Request, res: Response, next: NextFunction) => {
if (req.ctx.decodedToken.type === "ApeKey") {
return apeRateLimiter(req, res, next);
}
return defaultRateLimiter(req, res, next);
};
}

View file

@ -150,7 +150,7 @@ async function authenticateWithApeKey(
options: RequestAuthenticationOptions
): Promise<MonkeyTypes.DecodedToken> {
if (!configuration.apeKeys.acceptKeys) {
throw new MonkeyError(403, "ApeKeys are not being accepted at this time");
throw new MonkeyError(503, "ApeKeys are not being accepted at this time");
}
if (!options.acceptApeKeys) {

View file

@ -2,8 +2,9 @@ import * as db from "../init/db";
import { v4 as uuidv4 } from "uuid";
import Logger from "../utils/logger";
import MonkeyError from "../utils/error";
import { MonkeyResponse, handleMonkeyResponse } from "../utils/monkey-response";
import { incrementBadAuth } from "./rate-limit";
import { NextFunction, Response } from "express";
import { MonkeyResponse, handleMonkeyResponse } from "../utils/monkey-response";
async function errorHandlingMiddleware(
error: Error,
@ -33,6 +34,8 @@ async function errorHandlingMiddleware(
monkeyResponse.message = `Oops! Our monkeys dropped their bananas. Please try again later. - ${monkeyResponse.data.errorId}`;
}
await incrementBadAuth(req, res, monkeyResponse.status);
if (process.env.MODE !== "dev" && monkeyResponse.status >= 500) {
const { uid, errorId } = monkeyResponse.data;

View file

@ -1,17 +1,25 @@
import { Response, NextFunction } from "express";
import rateLimit, { Options } from "express-rate-limit";
import MonkeyError from "../utils/error";
import { Response, NextFunction } from "express";
import { RateLimiterMemory } from "rate-limiter-flexible";
import rateLimit, { Options } from "express-rate-limit";
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1;
const getAddress = (req: MonkeyTypes.Request, _res: Response): string => {
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
return (req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.ip ||
"255.255.255.255") as string;
};
const customHandler = (
const getKeyWithUid = (req: MonkeyTypes.Request, _res: Response): string => {
const uid = req?.ctx?.decodedToken?.uid;
const useUid = uid.length > 0 && uid;
return (useUid || getKey(req, _res)) as string;
};
export const customHandler = (
_req: MonkeyTypes.Request,
_res: Response,
_next: NextFunction,
@ -20,65 +28,129 @@ const customHandler = (
throw new MonkeyError(429, "Too many attempts, please try again later.");
};
const ONE_HOUR = 1000 * 60 * 60;
const ONE_HOUR_SECONDS = 60 * 60;
const ONE_HOUR_MS = 1000 * ONE_HOUR_SECONDS;
// Root Rate Limit
export const rootRateLimiter = rateLimit({
windowMs: ONE_HOUR_MS,
max: 2000 * REQUEST_MULTIPLIER,
keyGenerator: getKey,
handler: (_req, _res, _next, _options): void => {
throw new MonkeyError(
429,
"Maximum API request limit reached. Please try again later."
);
},
});
// Bad Authentication Rate Limiter
const badAuthRateLimiter = new RateLimiterMemory({
points: 30 * REQUEST_MULTIPLIER,
duration: ONE_HOUR_SECONDS,
});
export async function badAuthRateLimiterHandler(
req: MonkeyTypes.Request,
res: Response,
next: NextFunction
): Promise<void> {
if (!req.ctx.configuration.rateLimiting.badAuthentication.enabled) {
return next();
}
try {
const key = getKey(req, res);
const rateLimitStatus = await badAuthRateLimiter.get(key);
if (rateLimitStatus !== null && rateLimitStatus?.remainingPoints <= 0) {
throw new MonkeyError(
429,
"Too many bad authentication attempts, please try again later."
);
}
} catch (error) {
return next(error);
}
next();
}
export async function incrementBadAuth(
req: MonkeyTypes.Request,
res: Response,
status: number
): Promise<void> {
const { enabled, penalty, flaggedStatusCodes } =
req.ctx.configuration.rateLimiting.badAuthentication;
if (!enabled || !flaggedStatusCodes.includes(status)) {
return;
}
try {
const key = getKey(req, res);
await badAuthRateLimiter.penalty(key, penalty);
} catch (error) {}
}
// Config Routing
export const configUpdate = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const configGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 120 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// Leaderboards Routing
export const leaderboardsGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// New Quotes Routing
export const newQuotesGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const newQuotesAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const newQuotesAction = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// Quote Ratings Routing
export const quoteRatingsGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const quoteRatingsSubmit = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
@ -86,7 +158,7 @@ export const quoteRatingsSubmit = rateLimit({
export const quoteReportSubmit = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
@ -94,50 +166,50 @@ export const quoteReportSubmit = rateLimit({
export const quoteFavoriteGet = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const quoteFavoritePost = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const quoteFavoriteDelete = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// Presets Routing
export const presetsGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const presetsAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const presetsRemove = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const presetsEdit = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
@ -145,229 +217,229 @@ export const presetsEdit = rateLimit({
export const psaGet = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// Results Routing
export const resultsGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const resultsAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const resultsTagsUpdate = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 100 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const resultsDeleteAll = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 10 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const resultsLeaderboardGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const resultsLeaderboardQualificationGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// Users Routing
export const userGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userSignup = rateLimit({
windowMs: 24 * ONE_HOUR, // 1 day
windowMs: 24 * ONE_HOUR_MS, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userDelete = rateLimit({
windowMs: 24 * ONE_HOUR, // 1 day
windowMs: 24 * ONE_HOUR_MS, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCheckName = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userUpdateName = rateLimit({
windowMs: 24 * ONE_HOUR, // 1 day
windowMs: 24 * ONE_HOUR_MS, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userUpdateLBMemory = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userUpdateEmail = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userClearPB = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomFilterAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomFilterRemove = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userTagsGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userTagsRemove = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userTagsClearPB = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userTagsEdit = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userTagsAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomThemeGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomThemeAdd = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomThemeRemove = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userCustomThemeEdit = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userDiscordLink = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 15 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const usersTagsEdit = userDiscordLink;
export const userDiscordUnlink = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 15 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userProfileGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 100 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userProfileUpdate = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
// ApeKeys Routing
export const apeKeysGet = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 120 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const apeKeysGenerate = rateLimit({
windowMs: ONE_HOUR,
windowMs: ONE_HOUR_MS,
max: 15 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
keyGenerator: getKeyWithUid,
handler: customHandler,
});

View file

@ -48,7 +48,13 @@ declare namespace MonkeyTypes {
apeKeyBytes: number;
apeKeySaltRounds: number;
};
rateLimiting: {
badAuthentication: {
enabled: boolean;
penalty: number;
flaggedStatusCodes: number[];
};
};
dailyLeaderboards: {
enabled: boolean;
leaderboardExpirationTimeInDays: number;

View file

@ -5,7 +5,7 @@ class MonkeyError extends Error {
errorId: string;
uid?: string;
constructor(status: number, message: string, stack?: string, uid?: string) {
constructor(status: number, message?: string, stack?: string, uid?: string) {
super();
this.status = status ?? 500;
this.errorId = uuidv4();