UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

264 lines 8.93 kB
/** * Rate Limiter * Provides request throttling and rate limiting functionality */ /** * In-memory rate limit store */ export class MemoryRateLimitStore { store = new Map(); cleanupInterval; constructor() { // Clean up expired entries every minute this.cleanupInterval = setInterval(() => { this.cleanup(); }, 60000); } async get(key) { const entry = this.store.get(key); if (!entry || Date.now() > entry.resetTime) { return null; } return entry.count; } async set(key, value, ttl) { this.store.set(key, { count: value, resetTime: Date.now() + ttl }); } async increment(key, ttl) { const entry = this.store.get(key); const now = Date.now(); if (!entry || now > entry.resetTime) { // Create new entry this.store.set(key, { count: 1, resetTime: now + ttl }); return 1; } else { // Increment existing entry entry.count++; return entry.count; } } async reset(key) { this.store.delete(key); } cleanup() { const now = Date.now(); for (const [key, entry] of this.store.entries()) { if (now > entry.resetTime) { this.store.delete(key); } } } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.store.clear(); } } /** * Redis-based rate limit store */ export class RedisRateLimitStore { redisClient; constructor(redisClient) { this.redisClient = redisClient; } async get(key) { const value = await this.redisClient.get(key); return value ? parseInt(value, 10) : null; } async set(key, value, ttl) { await this.redisClient.setex(key, Math.ceil(ttl / 1000), value); } async increment(key, ttl) { const multi = this.redisClient.multi(); multi.incr(key); multi.expire(key, Math.ceil(ttl / 1000)); const results = await multi.exec(); return results[0][1]; // Return the incremented value } async reset(key) { await this.redisClient.del(key); } } /** * Rate limiter implementation */ export class RateLimiter { store; options; constructor(options, store) { this.options = { windowMs: options.windowMs, maxRequests: options.maxRequests, keyGenerator: options.keyGenerator || this.defaultKeyGenerator, skipSuccessfulRequests: options.skipSuccessfulRequests || false, skipFailedRequests: options.skipFailedRequests || false, onLimitReached: options.onLimitReached || (() => { }) }; this.store = store || new MemoryRateLimitStore(); } /** * Check if request should be rate limited */ async checkLimit(request) { const key = this.options.keyGenerator(request); const now = Date.now(); const windowStart = now - this.options.windowMs; // Get current count const currentCount = await this.store.get(key) || 0; // Check if limit exceeded if (currentCount >= this.options.maxRequests) { const resetTime = now + this.options.windowMs; const retryAfter = Math.ceil(this.options.windowMs / 1000); // Call limit reached callback this.options.onLimitReached(request); return { allowed: false, limit: this.options.maxRequests, remaining: 0, resetTime, retryAfter }; } // Increment counter const newCount = await this.store.increment(key, this.options.windowMs); return { allowed: true, limit: this.options.maxRequests, remaining: Math.max(0, this.options.maxRequests - newCount), resetTime: now + this.options.windowMs }; } /** * Reset rate limit for a specific key */ async resetLimit(request) { const key = this.options.keyGenerator(request); await this.store.reset(key); } /** * Default key generator using IP address */ defaultKeyGenerator(request) { // Try to get IP from various sources const ip = request.ip || request.connection?.remoteAddress || request.socket?.remoteAddress || request.headers?.['x-forwarded-for']?.split(',')[0]?.trim() || request.headers?.['x-real-ip'] || 'unknown'; return `rate_limit:${ip}`; } /** * Create middleware for Express-like frameworks */ middleware() { return async (req, res, next) => { try { const result = await this.checkLimit(req); // Set rate limit headers res.setHeader('X-RateLimit-Limit', result.limit); res.setHeader('X-RateLimit-Remaining', result.remaining); res.setHeader('X-RateLimit-Reset', new Date(result.resetTime).toISOString()); if (!result.allowed) { res.setHeader('Retry-After', result.retryAfter); res.status(429).json({ error: 'Too Many Requests', message: 'Rate limit exceeded', retryAfter: result.retryAfter }); return; } next(); } catch (error) { console.error('Rate limiter error:', error); next(); // Continue on error to avoid breaking the application } }; } } /** * Sliding window rate limiter for more precise control */ export class SlidingWindowRateLimiter { store; options; constructor(options, store) { this.options = { windowMs: options.windowMs, maxRequests: options.maxRequests, keyGenerator: options.keyGenerator || this.defaultKeyGenerator, skipSuccessfulRequests: options.skipSuccessfulRequests || false, skipFailedRequests: options.skipFailedRequests || false, onLimitReached: options.onLimitReached || (() => { }) }; this.store = store || new MemoryRateLimitStore(); } async checkLimit(request) { const key = this.options.keyGenerator(request); const now = Date.now(); const windowStart = now - this.options.windowMs; // For sliding window, we need to track individual request timestamps // This is a simplified implementation - in production, you'd want to use // a more sophisticated data structure like a sorted set in Redis const currentCount = await this.store.get(key) || 0; if (currentCount >= this.options.maxRequests) { this.options.onLimitReached(request); return { allowed: false, limit: this.options.maxRequests, remaining: 0, resetTime: now + this.options.windowMs, retryAfter: Math.ceil(this.options.windowMs / 1000) }; } const newCount = await this.store.increment(key, this.options.windowMs); return { allowed: true, limit: this.options.maxRequests, remaining: Math.max(0, this.options.maxRequests - newCount), resetTime: now + this.options.windowMs }; } defaultKeyGenerator(request) { const ip = request.ip || request.connection?.remoteAddress || request.socket?.remoteAddress || 'unknown'; return `sliding_rate_limit:${ip}`; } middleware() { return async (req, res, next) => { try { const result = await this.checkLimit(req); res.setHeader('X-RateLimit-Limit', result.limit); res.setHeader('X-RateLimit-Remaining', result.remaining); res.setHeader('X-RateLimit-Reset', new Date(result.resetTime).toISOString()); if (!result.allowed) { res.setHeader('Retry-After', result.retryAfter); res.status(429).json({ error: 'Too Many Requests', message: 'Rate limit exceeded', retryAfter: result.retryAfter }); return; } next(); } catch (error) { console.error('Sliding window rate limiter error:', error); next(); } }; } } //# sourceMappingURL=rate-limiter.js.map