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
JavaScript
/**
* 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