UNPKG

digital-samba-mcp-server

Version:

Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API

247 lines 8.34 kB
/** * Digital Samba MCP Server - Rate Limiting Module * * This module provides rate limiting functionality for the Digital Samba MCP Server. * It implements a token bucket algorithm for limiting the number of requests that can * be made in a given time period, helping to protect the Digital Samba API from abuse * and ensuring fair usage across clients. * * Features include: * - Configurable rate limits (requests per minute, per hour) * - Memory-based storage for rate limiting data * - Optional Redis-based storage for distributed deployments * - Detailed logging of rate limiting events * - Configurable response behavior for rate limited requests * * @module rate-limiter * @author Digital Samba Team * @version 0.1.0 */ // Local modules import logger from './logger.js'; /** * Default options for the rate limiter */ export const defaultOptions = { maxRequests: 60, // 60 requests per minute by default windowMs: 60 * 1000, // 1 minute headers: true, message: 'Too many requests, please try again later.', keyGenerator: (req) => { // Default to using IP address as key const xForwardedFor = req.headers['x-forwarded-for']; if (xForwardedFor && typeof xForwardedFor === 'string') { return xForwardedFor.split(',')[0].trim(); } return req.ip || '127.0.0.1'; }, skip: () => false, // Don't skip any requests by default trustProxy: false }; /** * TokenBucket rate limiter implementation * * This class implements a token bucket algorithm for rate limiting. * Each client has a bucket that's refilled at a constant rate. * When a request is made, a token is consumed from the bucket. * If there are no tokens left, the request is rate limited. */ export class TokenBucketRateLimiter { /** * Creates a new TokenBucketRateLimiter * @param options Rate limiter options */ constructor(options = {}) { this.options = { ...defaultOptions, ...options }; this.store = new Map(); this.tokensPerMs = this.options.maxRequests / this.options.windowMs; logger.info('Rate limiter initialized', { maxRequests: this.options.maxRequests, windowMs: this.options.windowMs }); } /** * Gets the current status for a client * @param key Client identifier * @returns Rate limit status */ getClientStatus(key) { const now = Date.now(); let status = this.store.get(key); if (!status) { // New client, initialize with full bucket status = { tokens: this.options.maxRequests, lastRefill: now }; this.store.set(key, status); return status; } // Refill tokens based on elapsed time const elapsedMs = now - status.lastRefill; if (elapsedMs > 0) { const tokensToAdd = elapsedMs * this.tokensPerMs; status.tokens = Math.min(status.tokens + tokensToAdd, this.options.maxRequests); status.lastRefill = now; } return status; } /** * Consumes a token for a client * @param key Client identifier * @returns Whether the token was successfully consumed */ consumeToken(key) { const status = this.getClientStatus(key); if (status.tokens >= 1) { status.tokens -= 1; return true; } return false; } /** * Gets remaining tokens for a client * @param key Client identifier * @returns Number of tokens remaining */ getRemainingTokens(key) { const status = this.getClientStatus(key); return Math.floor(status.tokens); } /** * Gets reset time for a client's rate limit * @param key Client identifier * @returns Timestamp when rate limit will reset */ getResetTime(key) { const status = this.getClientStatus(key); const tokensNeeded = this.options.maxRequests - status.tokens; const msNeeded = tokensNeeded / this.tokensPerMs; return Date.now() + msNeeded; } /** * Middleware function for Express * @returns Express middleware */ middleware() { return (req, res, next) => { // Skip rate limiting if specified if (this.options.skip && this.options.skip(req)) { return next(); } // Trust proxy if enabled let clientIp = req.ip; if (this.options.trustProxy && req.headers['x-forwarded-for']) { clientIp = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.ip; } // Generate client key const key = this.options.keyGenerator(req); // Check if client has tokens available const allowed = this.consumeToken(key); // Set rate limit headers if enabled if (this.options.headers) { res.setHeader('X-RateLimit-Limit', this.options.maxRequests.toString()); res.setHeader('X-RateLimit-Remaining', this.getRemainingTokens(key).toString()); res.setHeader('X-RateLimit-Reset', Math.ceil(this.getResetTime(key) / 1000).toString()); } if (!allowed) { logger.warn('Rate limit exceeded', { key }); if (this.options.handler) { return this.options.handler(req, res); } res.status(429).json({ error: 'Too Many Requests', message: this.options.message, status: 429 }); return; } next(); }; } /** * Gets rate limit status for a client * @param key Client identifier * @returns Client rate limit status */ getStatus(key) { const status = this.getClientStatus(key); return { remaining: Math.floor(status.tokens), limit: this.options.maxRequests, reset: this.getResetTime(key) }; } /** * Clears the rate limiter store */ reset() { this.store.clear(); logger.info('Rate limiter store cleared'); } } /** * Redis-backed token bucket rate limiter * This implementation uses Redis for storage, making it suitable for distributed deployments. * Note: This is a placeholder for the Redis implementation. * The actual implementation would use a Redis client library. */ export class RedisRateLimiter { // Redis implementation would go here // This is a placeholder class to show the interface constructor(options = {}) { // Initialize with Redis client logger.info('Redis rate limiter initialized'); throw new Error('Redis rate limiter not yet implemented'); } } /** * Creates a rate limiter middleware for express * @param options Rate limiter options * @returns Express middleware */ export function createRateLimiter(options = {}) { const limiter = new TokenBucketRateLimiter(options); return limiter.middleware(); } /** * Creates an API key based rate limiter that limits requests per API key * @param options Rate limiter options * @returns Express middleware */ export function createApiKeyRateLimiter(options = {}) { const apiKeyLimiter = new TokenBucketRateLimiter({ ...options, keyGenerator: (req) => { // Extract API key from Authorization header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return 'anonymous'; } return authHeader.split(' ')[1]; } }); return apiKeyLimiter.middleware(); } /** * Creates an IP based rate limiter that limits requests per IP address * @param options Rate limiter options * @returns Express middleware */ export function createIpRateLimiter(options = {}) { return createRateLimiter({ trustProxy: true, ...options }); } /** * Exports the default rate limiter */ export default { TokenBucketRateLimiter, RedisRateLimiter, createRateLimiter, createApiKeyRateLimiter, createIpRateLimiter }; //# sourceMappingURL=rate-limiter.js.map