-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add public-samples/benchmarks-website-27dn3k/src/backend/src/api/midd…
…lewares/rateLimit.middleware.ts
- Loading branch information
1 parent
90281e1
commit f1a08e7
Showing
1 changed file
with
136 additions
and
0 deletions.
There are no files selected for viewing
136 changes: 136 additions & 0 deletions
136
src/backend/src/api/middlewares/rateLimit.middleware.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/** | ||
* @file Rate Limiting Middleware | ||
* @description Implements distributed rate limiting for API endpoints using Redis | ||
* @version 1.0.0 | ||
*/ | ||
|
||
import { injectable } from 'inversify'; | ||
import rateLimit from 'express-rate-limit'; // v7.x | ||
import RedisStore from 'rate-limit-redis'; // v4.x | ||
import { Request, Response, NextFunction } from 'express'; // v4.x | ||
import Redis from 'ioredis'; // v5.x | ||
import { ServerConfig } from '../../interfaces/config.interface'; | ||
import { serverConfig } from '../../config/server.config'; | ||
|
||
// Constants for rate limiting configuration | ||
const WINDOW_MS = 3600000; // 1 hour in milliseconds | ||
const PUBLIC_MAX_REQUESTS = 1000; // Public endpoint limit | ||
const AUTH_MAX_REQUESTS = 5000; // Authenticated endpoint limit | ||
const REDIS_KEY_PREFIX = 'rl:'; // Redis key prefix for rate limiting | ||
const BYPASS_HEADER_NAME = 'X-RateLimit-Bypass'; | ||
|
||
/** | ||
* Interface for rate limiter options | ||
*/ | ||
interface RateLimiterOptions { | ||
windowMs: number; | ||
max: number; | ||
keyGenerator?: (req: Request) => string; | ||
skipFailedRequests?: boolean; | ||
} | ||
|
||
@injectable() | ||
export class RateLimitMiddleware { | ||
private redis: Redis; | ||
private store: RedisStore; | ||
|
||
constructor() { | ||
// Initialize Redis connection with cluster support | ||
this.redis = new Redis({ | ||
host: serverConfig.redis.host, | ||
port: serverConfig.redis.port, | ||
password: serverConfig.redis.password, | ||
keyPrefix: REDIS_KEY_PREFIX, | ||
enableCluster: serverConfig.redis.clusterMode, | ||
nodes: serverConfig.redis.nodes, | ||
retryStrategy: (times: number) => { | ||
return Math.min(times * 50, 2000); | ||
} | ||
}); | ||
|
||
// Initialize Redis store for rate limiting | ||
this.store = new RedisStore({ | ||
client: this.redis, | ||
prefix: REDIS_KEY_PREFIX, | ||
sendCommand: (...args: string[]) => this.redis.call(...args) | ||
}); | ||
} | ||
|
||
/** | ||
* Creates a rate limiter with specified options | ||
* @param options Rate limiter configuration options | ||
* @returns Configured rate limiter middleware | ||
*/ | ||
private createRateLimiter(options: RateLimiterOptions) { | ||
return rateLimit({ | ||
windowMs: options.windowMs, | ||
max: options.max, | ||
standardHeaders: true, | ||
legacyHeaders: false, | ||
store: this.store, | ||
keyGenerator: options.keyGenerator || ((req: Request) => { | ||
// Use X-Forwarded-For if behind proxy, fallback to remote address | ||
return (req.headers['x-forwarded-for'] as string || req.socket.remoteAddress || '').split(',')[0].trim(); | ||
}), | ||
skipFailedRequests: options.skipFailedRequests || false, | ||
handler: (req: Request, res: Response) => { | ||
res.status(429).json({ | ||
error: 'Too Many Requests', | ||
message: 'Rate limit exceeded. Please try again later.', | ||
retryAfter: Math.ceil(options.windowMs / 1000) | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Rate limiting middleware function | ||
* Applies different limits for public and authenticated routes | ||
*/ | ||
public rateLimitMiddleware = async (req: Request, res: Response, next: NextFunction) => { | ||
try { | ||
// Check for bypass token (for internal services) | ||
const bypassToken = req.headers[BYPASS_HEADER_NAME]; | ||
if (bypassToken === process.env.RATE_LIMIT_BYPASS_TOKEN) { | ||
return next(); | ||
} | ||
|
||
// Determine if route is authenticated | ||
const isAuthenticated = req.isAuthenticated?.() || false; | ||
|
||
// Create appropriate rate limiter based on authentication status | ||
const limiter = this.createRateLimiter({ | ||
windowMs: WINDOW_MS, | ||
max: isAuthenticated ? AUTH_MAX_REQUESTS : PUBLIC_MAX_REQUESTS, | ||
skipFailedRequests: true, | ||
keyGenerator: (req: Request) => { | ||
// For authenticated routes, use user ID as part of the key | ||
const baseKey = (req.headers['x-forwarded-for'] as string || req.socket.remoteAddress || '').split(',')[0].trim(); | ||
return isAuthenticated ? `${baseKey}:${req.user?.id || ''}` : baseKey; | ||
} | ||
}); | ||
|
||
// Apply rate limiting | ||
return limiter(req, res, next); | ||
} catch (error) { | ||
// Log error and fall back to default rate limiting | ||
console.error('Rate limiting error:', error); | ||
const fallbackLimiter = this.createRateLimiter({ | ||
windowMs: WINDOW_MS, | ||
max: PUBLIC_MAX_REQUESTS | ||
}); | ||
return fallbackLimiter(req, res, next); | ||
} | ||
}; | ||
|
||
/** | ||
* Cleanup method to close Redis connection | ||
*/ | ||
public cleanup() { | ||
if (this.redis) { | ||
this.redis.quit(); | ||
} | ||
} | ||
} | ||
|
||
export default RateLimitMiddleware; |