UNPKG

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
"use strict"; 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(), }; }