@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
264 lines • 8.93 kB
JavaScript
/**
* 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