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:
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);
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user