express-rate-limiter-ts
Version:
A powerful, flexible and modern rate limiter middleware for Express.js written in TypeScript.
163 lines (162 loc) • 8.24 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rateLimiter = rateLimiter;
exports.getRateLimiterStats = getRateLimiterStats;
const memoryStore_1 = require("./storage/memoryStore");
function isInList(list, req, key) {
if (!list)
return false;
if (Array.isArray(list))
return list.includes(key);
if (typeof list === 'function')
return list(req);
return false;
}
// Yardımcı fonksiyon: Rate limit header'larını ayarla
function setRateLimitHeaders(res, limit, remaining, resetTime) {
// Klasik header'lar
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining));
res.setHeader('X-RateLimit-Reset', resetTime.getTime());
// IETF draft header'lar
res.setHeader('RateLimit-Limit', limit);
res.setHeader('RateLimit-Remaining', Math.max(0, remaining));
// Reset saniye cinsinden (RFC önerisi)
const resetSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1000);
res.setHeader('RateLimit-Reset', resetSeconds > 0 ? resetSeconds : 0);
}
function rateLimiter(options, store) {
// --- Yapılandırma ve ortam kontrolleri ---
if (!options.windowMs || typeof options.windowMs !== 'number' || options.windowMs <= 0) {
console.warn('[express-rate-limiter-ts] Hatalı veya eksik windowMs!');
}
if (!options.max || typeof options.max !== 'number' || options.max <= 0) {
console.warn('[express-rate-limiter-ts] Hatalı veya eksik max!');
}
if (options.identifierType && !['ip', 'header', 'jwt'].includes(options.identifierType)) {
console.warn('[express-rate-limiter-ts] Geçersiz identifierType:', options.identifierType);
}
if (options.identifierType === 'header' && !options.identifierHeader) {
console.warn('[express-rate-limiter-ts] identifierType header seçildi ancak identifierHeader belirtilmedi. Varsayılan x-api-key kullanılacak.');
}
// MemoryStore production uyarısı
if (!store && process.env.NODE_ENV === 'production') {
console.warn('[express-rate-limiter-ts] UYARI: MemoryStore production ortamında önerilmez! Dağıtık sistemlerde veri kaybı ve tutarsızlık yaşanabilir.');
}
// Trust proxy uyarısı (bilgilendirme)
if (process.env.EXPRESS_RATE_LIMITER_LOG_PROXY !== 'off') {
console.info('[express-rate-limiter-ts] Bilgi: Eğer uygulamanız proxy arkasında çalışıyorsa, Express trust proxy ayarını yapılandırmayı unutmayın.');
}
const { windowMs, max, message = 'Too many requests', statusCode = 429, keyGenerator, skip, logger, routes = {}, identifierType = 'ip', identifierHeader = 'x-api-key', stats = false, whitelist, blacklist, customResponse, banSeconds = 0, dynamicMax, } = options;
const usedStore = store || new memoryStore_1.MemoryStore(windowMs);
return function (req, res, next) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
// Route bazlı özel ayarları uygula
const routeKey = ((_a = req.route) === null || _a === void 0 ? void 0 : _a.path) || req.path;
const routeOptions = routes[routeKey] || {};
const mergedOptions = Object.assign(Object.assign({}, options), routeOptions);
if (mergedOptions.skip && mergedOptions.skip(req)) {
next();
return;
}
// Her istek geldiğinde istatistik güncelle
if (stats)
yield usedStore.recordStat(routeKey);
// Anahtar oluşturma
let key = '';
if (mergedOptions.identifierType === 'header') {
const headerName = mergedOptions.identifierHeader || 'x-api-key';
key = req.header(headerName) || req.ip || '';
}
else if (mergedOptions.identifierType === 'jwt') {
const auth = req.header('authorization') || '';
const token = auth.split(' ')[1];
try {
const payload = token ? JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()) : {};
key = payload.userId || payload.sub || req.ip || '';
}
catch (_d) {
key = req.ip || '';
}
}
else {
key = req.ip || '';
}
if (mergedOptions.keyGenerator) {
key = mergedOptions.keyGenerator(req);
}
// Whitelist kontrolü
if (isInList(mergedOptions.whitelist, req, key)) {
next();
return;
}
// Blacklist kontrolü
if (isInList(mergedOptions.blacklist, req, key)) {
(_b = logger === null || logger === void 0 ? void 0 : logger.warn) === null || _b === void 0 ? void 0 : _b.call(logger, `Blacklisted key: ${key}`, { route: routeKey, key });
res.status(403).json({ success: false, message: 'Erişiminiz engellendi', key });
return;
}
// Ban kontrolü
const banInfo = yield usedStore.isBanned(key);
if (banInfo.banned) {
res.status(429).json({
success: false,
message: 'Çok fazla istek nedeniyle geçici olarak engellendiniz',
banExpiresAt: banInfo.banExpiresAt,
reason: banInfo.reason,
});
return;
}
// Dinamik max
const effectiveMax = typeof mergedOptions.dynamicMax === 'function' ? mergedOptions.dynamicMax(req) : mergedOptions.max;
// Limitleme işlemi
const rateInfo = yield usedStore.incr(key);
rateInfo.exceeded = rateInfo.current > effectiveMax;
// Kalan istek ve limit kontrolü
if (rateInfo.exceeded) {
(_c = logger === null || logger === void 0 ? void 0 : logger.warn) === null || _c === void 0 ? void 0 : _c.call(logger, `Rate limit exceeded for key: ${key}`, { route: routeKey, key });
// Ban uygula
if (banSeconds > 0) {
yield usedStore.ban(key, banSeconds, 'Limit çok aşıldı');
rateInfo.banned = true;
rateInfo.banExpiresAt = new Date(Date.now() + banSeconds * 1000);
}
// Custom response
if (typeof mergedOptions.customResponse === 'function') {
mergedOptions.customResponse(req, res, rateInfo);
return;
}
setRateLimitHeaders(res, effectiveMax, 0, rateInfo.resetTime);
res.status(mergedOptions.statusCode || 429).json({
success: false,
message: mergedOptions.message,
limit: effectiveMax,
current: rateInfo.current,
resetTime: rateInfo.resetTime,
banned: rateInfo.banned,
banExpiresAt: rateInfo.banExpiresAt,
});
return;
}
setRateLimitHeaders(res, effectiveMax, effectiveMax - rateInfo.current, rateInfo.resetTime);
next();
});
};
}
// Admin panel için istatistik ve ban listesi fonksiyonları:
function getRateLimiterStats(store) {
return {
stats: store.getStats(),
banned: store.getBanned(),
};
}