mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
1b91a295e1
* fix: Use verified JWT for rate limiting * PR feedback * Prefer guards
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
import { createHash } from "crypto";
|
|
import type { IRateLimiterStoreOptions } from "rate-limiter-flexible";
|
|
import { RateLimiterRedis, RateLimiterMemory } from "rate-limiter-flexible";
|
|
import env from "@server/env";
|
|
import Logger from "@server/logging/Logger";
|
|
import Redis from "@server/storage/redis";
|
|
|
|
export default class RateLimiter {
|
|
constructor() {
|
|
throw Error(`Cannot instantiate class!`);
|
|
}
|
|
|
|
static readonly RATE_LIMITER_REDIS_KEY_PREFIX = "rl";
|
|
|
|
static readonly TOKEN_CACHE_KEY_PREFIX = "rl:tok:";
|
|
|
|
static readonly TOKEN_CACHE_TTL_SECONDS = 3600;
|
|
|
|
static readonly rateLimiterMap = new Map<string, RateLimiterRedis>();
|
|
|
|
static readonly insuranceRateLimiter = new RateLimiterMemory({
|
|
points: env.RATE_LIMITER_REQUESTS,
|
|
duration: env.RATE_LIMITER_DURATION_WINDOW,
|
|
});
|
|
|
|
private static _defaultRateLimiter: RateLimiterRedis | undefined;
|
|
|
|
static get defaultRateLimiter(): RateLimiterRedis {
|
|
if (!this._defaultRateLimiter) {
|
|
this._defaultRateLimiter = new RateLimiterRedis({
|
|
storeClient: Redis.defaultClient,
|
|
points: env.RATE_LIMITER_REQUESTS,
|
|
duration: env.RATE_LIMITER_DURATION_WINDOW,
|
|
keyPrefix: this.RATE_LIMITER_REDIS_KEY_PREFIX,
|
|
insuranceLimiter: this.insuranceRateLimiter,
|
|
});
|
|
}
|
|
return this._defaultRateLimiter;
|
|
}
|
|
|
|
static getRateLimiter(path: string): RateLimiterRedis {
|
|
return this.rateLimiterMap.get(path) || this.defaultRateLimiter;
|
|
}
|
|
|
|
static setRateLimiter(path: string, config: IRateLimiterStoreOptions): void {
|
|
const rateLimiter = new RateLimiterRedis(config);
|
|
this.rateLimiterMap.set(path, rateLimiter);
|
|
}
|
|
|
|
static hasRateLimiter(path: string): boolean {
|
|
return this.rateLimiterMap.has(path);
|
|
}
|
|
|
|
/**
|
|
* Caches the user id associated with a verified authentication token so that
|
|
* subsequent requests can be keyed by user without re-validating the token.
|
|
* Errors are swallowed — a failed cache write just means the next request
|
|
* falls back to IP-based keying.
|
|
*
|
|
* @param token The authentication token that was just verified.
|
|
* @param userId The id of the user the token belongs to.
|
|
*/
|
|
static async cacheUserForToken(token: string, userId: string): Promise<void> {
|
|
try {
|
|
await Redis.defaultClient.set(
|
|
this.tokenCacheKey(token),
|
|
userId,
|
|
"EX",
|
|
this.TOKEN_CACHE_TTL_SECONDS
|
|
);
|
|
} catch (err) {
|
|
Logger.warn("Failed to cache user for rate limiter token", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Looks up the cached user id for a previously verified token. Returns null
|
|
* on cache miss or Redis error.
|
|
*
|
|
* @param token The authentication token presented on the current request.
|
|
* @returns The associated user id, or null if unknown.
|
|
*/
|
|
static async getCachedUserIdForToken(token: string): Promise<string | null> {
|
|
try {
|
|
return await Redis.defaultClient.get(this.tokenCacheKey(token));
|
|
} catch (err) {
|
|
Logger.warn("Failed to read cached user for rate limiter token", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the cached user id for a token, for example on logout so that a
|
|
* revoked token immediately stops keying rate limits per user.
|
|
*
|
|
* @param token The authentication token being invalidated.
|
|
*/
|
|
static async clearCachedToken(token: string): Promise<void> {
|
|
try {
|
|
await Redis.defaultClient.del(this.tokenCacheKey(token));
|
|
} catch (err) {
|
|
Logger.warn("Failed to clear cached rate limiter token", err);
|
|
}
|
|
}
|
|
|
|
private static tokenCacheKey(token: string): string {
|
|
const hash = createHash("sha256").update(token).digest("hex");
|
|
return `${this.TOKEN_CACHE_KEY_PREFIX}${hash}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Re-useable configuration for rate limiter middleware.
|
|
*/
|
|
export const RateLimiterStrategy = {
|
|
/** Allows five requests per minute, per IP address */
|
|
FivePerMinute: {
|
|
duration: 60,
|
|
requests: 5,
|
|
},
|
|
/** Allows ten requests per minute, per IP address */
|
|
TenPerMinute: {
|
|
duration: 60,
|
|
requests: 10,
|
|
},
|
|
/** Allows twenty five requests per minute, per IP address */
|
|
TwentyFivePerMinute: {
|
|
duration: 60,
|
|
requests: 25,
|
|
},
|
|
/** Allows one hundred requests per minute, per IP address */
|
|
OneHundredPerMinute: {
|
|
duration: 60,
|
|
requests: 100,
|
|
},
|
|
/** Allows one thousand requests per hour, per IP address */
|
|
OneThousandPerHour: {
|
|
duration: 3600,
|
|
requests: 1000,
|
|
},
|
|
/** Allows one hunred requests per hour, per IP address */
|
|
OneHundredPerHour: {
|
|
duration: 3600,
|
|
requests: 100,
|
|
},
|
|
/** Allows fifty requests per hour, per IP address */
|
|
FiftyPerHour: {
|
|
duration: 3600,
|
|
requests: 50,
|
|
},
|
|
/** Allows ten requests per hour, per IP address */
|
|
TenPerHour: {
|
|
duration: 3600,
|
|
requests: 10,
|
|
},
|
|
/** Allows five requests per hour, per IP address */
|
|
FivePerHour: {
|
|
duration: 3600,
|
|
requests: 5,
|
|
},
|
|
};
|