feat: Upstash implementation for rate limiting/redis (#9514)

* Introduce rate limiting that works on the edge

* typo

* Log once on init

* Update rateLimit.ts

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
sean-brydonandzomars authored and GitHub committed 2023-06-19 10:01:06 +00:00
1 parent ed65b2a3ab
commit 8eccd3658e
9 files changed
+124 -44

No files matched your search

@@ -14,7 +14,7 @@ import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { isENVDev } from "@calcom/lib/env";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import rateLimiter from "@calcom/lib/rateLimit";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
@@ -102,11 +102,14 @@ const providers: Provider[] = [
if (!user) {
throw new Error(ErrorCode.IncorrectUsernamePassword);
}
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
const limiter = await rateLimiter();
const rateLimit = await limiter({
identifier: user.email,
});
await limiter.check(10, user.email); // 10 requests per minute
if (!rateLimit.success) {
throw new Error(ErrorCode.RateLimitExceeded);
}
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
+6 -7
View File
@@ -4,17 +4,13 @@ import { sendEmailVerificationLink } from "@calcom/emails/email-manager";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import rateLimit from "@calcom/lib/rateLimit";
import rateLimiter from "@calcom/lib/rateLimit";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
const log = logger.getChildLogger({ prefix: [`[[Auth] `] });
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
interface VerifyEmailType {
username?: string;
email: string;
@@ -43,9 +39,12 @@ export const sendEmailVerification = async ({ email, language, username }: Verif
token,
});
const { isRateLimited } = limiter.check(10, email); // 10 requests per minute
const limiter = await rateLimiter();
const rateLimit = await limiter({
identifier: email,
});
if (isRateLimited) {
if (!rateLimit.success) {
throw new TRPCError({
code: "TOO_MANY_REQUESTS",
message: "An unexpected error occurred, please try again later.",
+10
View File
@@ -28,3 +28,13 @@ export function isIpInBanlist(request: Request | NextApiRequest) {
}
return false;
}
export function isIpInBanListString(identifer: string) {
const rawBanListJson = process.env.IP_BANLIST || "[]";
const banList = banlistSchema.parse(JSON.parse(rawBanListJson));
if (banList.includes(identifer)) {
console.log(`Found banned IP: ${identifer} in IP_BANLIST`);
return true;
}
return false;
}
+44 -22
View File
@@ -1,27 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import cache from "memory-cache";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { isIpInBanListString } from "./getIP";
import logger from "./logger";
const rateLimit = (options: { intervalInMs: number }) => {
return {
check: (requestLimit: number, uniqueIdentifier: string) => {
const count = cache.get(uniqueIdentifier) || [0];
if (count[0] === 0) {
cache.put(uniqueIdentifier, count, options.intervalInMs);
}
count[0] += 1;
const log = logger.getChildLogger({ prefix: ["RateLimit"] });
const currentUsage = count[0];
const isRateLimited = currentUsage >= requestLimit;
if (isRateLimited) {
throw new Error(ErrorCode.RateLimitExceeded);
}
return { isRateLimited, requestLimit, remaining: isRateLimited ? 0 : requestLimit - currentUsage };
},
};
type RateLimitHelper = {
rateLimitingType?: "core" | "forcedSlowMode";
identifier: string;
};
export default rateLimit;
function rateLimiter() {
const UPSATCH_ENV_FOUND = process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN;
if (!UPSATCH_ENV_FOUND) {
log.warn("Disabled due to not finding UPSTASH env variables");
return () => ({ success: true });
}
const redis = Redis.fromEnv();
const limiter = {
core: new Ratelimit({
redis,
analytics: true,
prefix: "ratelimit",
limiter: Ratelimit.fixedWindow(10, "60s"),
}),
forcedSlowMode: new Ratelimit({
redis,
analytics: true,
prefix: "ratelimit:slowmode",
limiter: Ratelimit.fixedWindow(1, "30s"),
}),
};
async function rateLimit({ rateLimitingType = "core", identifier }: RateLimitHelper) {
if (isIpInBanListString(identifier)) {
return await limiter.forcedSlowMode.limit(identifier);
}
return await limiter[rateLimitingType].limit(identifier);
}
return rateLimit;
}
export default rateLimiter;