88 lines
2.5 KiB
JavaScript
88 lines
2.5 KiB
JavaScript
/**
|
|
* @cc-platform/server/rate — In-memory rate limiter middleware
|
|
*
|
|
* Extracted from Inventory-Manager-CC/web/server/server.js which has rate
|
|
* limiting while remoteturtle does not. Both services benefit from this
|
|
* protection.
|
|
*
|
|
* Usage:
|
|
* const { createRateLimiter } = require('@cc-platform/server/rate');
|
|
* const limiter = createRateLimiter({ windowMs: 60000, maxRequests: 30 });
|
|
* app.use(limiter);
|
|
*
|
|
* // Or protect specific routes:
|
|
* app.post('/api/sensitive', limiter, handler);
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* Create an in-memory rate limiter middleware.
|
|
*
|
|
* @param {Object} [opts={}] - Options
|
|
* @param {number} [opts.windowMs=60000] - Time window in milliseconds
|
|
* @param {number} [opts.maxRequests=30] - Maximum requests per window per IP
|
|
* @param {string} [opts.message] - Custom error message
|
|
* @param {boolean} [opts.mutationsOnly=true] - Only rate-limit non-GET methods
|
|
* @returns {Function} Express middleware
|
|
*/
|
|
function createRateLimiter(opts = {}) {
|
|
const windowMs = opts.windowMs || 60000;
|
|
const maxRequests = opts.maxRequests || 30;
|
|
const message = opts.message || 'Too many requests — please try again later';
|
|
const mutationsOnly = opts.mutationsOnly !== false;
|
|
|
|
// Map of IP → { count, resetTime }
|
|
const clients = new Map();
|
|
|
|
// Periodic cleanup of expired entries
|
|
const cleanupInterval = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [ip, entry] of clients) {
|
|
if (now > entry.resetTime) {
|
|
clients.delete(ip);
|
|
}
|
|
}
|
|
}, windowMs);
|
|
|
|
// Don't prevent process exit
|
|
if (cleanupInterval.unref) {
|
|
cleanupInterval.unref();
|
|
}
|
|
|
|
return function rateLimiter(req, res, next) {
|
|
// Skip GET/HEAD/OPTIONS if mutationsOnly
|
|
if (mutationsOnly && ['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
return next();
|
|
}
|
|
|
|
const ip = req.ip || req.socket.remoteAddress || 'unknown';
|
|
const now = Date.now();
|
|
|
|
let entry = clients.get(ip);
|
|
if (!entry || now > entry.resetTime) {
|
|
entry = { count: 0, resetTime: now + windowMs };
|
|
clients.set(ip, entry);
|
|
}
|
|
|
|
entry.count++;
|
|
|
|
if (entry.count > maxRequests) {
|
|
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
res.set('Retry-After', String(retryAfter));
|
|
return res.status(429).json({
|
|
error: message,
|
|
retryAfter,
|
|
});
|
|
}
|
|
|
|
// Add rate limit headers
|
|
res.set('X-RateLimit-Limit', String(maxRequests));
|
|
res.set('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
|
|
|
|
return next();
|
|
};
|
|
}
|
|
|
|
module.exports = { createRateLimiter };
|