@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
371 lines • 12.2 kB
JavaScript
/**
* Rate Limiting - レート制限機能
*/
// Security configuration is now handled via environment variables
import { AuditLogManager } from './types';
/**
* レート制限マネージャー
*/
export class RateLimitManager {
constructor(config) {
this.limitMap = new Map();
this.cleanupInterval = null;
// デフォルトのレート制限設定
this.config = {
windowMs: parseInt(process.env.GFTD_RATE_LIMIT_WINDOW_MS || '60000'), // 1分
maxRequests: parseInt(process.env.GFTD_RATE_LIMIT_MAX_REQUESTS || '100'),
skipSuccessfulRequests: false,
skipFailedRequests: false,
keyGenerator: (req) => req.ip || req.connection.remoteAddress || 'unknown',
...config,
};
this.startCleanupTimer();
}
/**
* シングルトンインスタンスを取得
*/
static getInstance(config) {
if (!RateLimitManager.instance) {
RateLimitManager.instance = new RateLimitManager(config);
}
return RateLimitManager.instance;
}
/**
* レート制限チェック
*/
checkLimit(key) {
const now = Date.now();
const entry = this.limitMap.get(key);
if (!entry) {
// 新しいエントリを作成
this.limitMap.set(key, {
count: 1,
firstRequest: now,
lastRequest: now,
});
return {
allowed: true,
remaining: this.config.maxRequests - 1,
resetTime: now + this.config.windowMs,
};
}
// 時間窓をチェック
const timeDiff = now - entry.firstRequest;
if (timeDiff >= this.config.windowMs) {
// 時間窓をリセット
entry.count = 1;
entry.firstRequest = now;
entry.lastRequest = now;
return {
allowed: true,
remaining: this.config.maxRequests - 1,
resetTime: now + this.config.windowMs,
};
}
// リクエスト数をチェック
if (entry.count >= this.config.maxRequests) {
return {
allowed: false,
remaining: 0,
resetTime: entry.firstRequest + this.config.windowMs,
};
}
// リクエスト数を増加
entry.count++;
entry.lastRequest = now;
return {
allowed: true,
remaining: this.config.maxRequests - entry.count,
resetTime: entry.firstRequest + this.config.windowMs,
};
}
/**
* Express.js ミドルウェアを作成
*/
static createMiddleware(options) {
const manager = RateLimitManager.getInstance(options);
return (req, res, next) => {
const key = manager.config.keyGenerator(req);
const result = manager.checkLimit(key);
// ヘッダーを設定
res.setHeader('X-RateLimit-Limit', manager.config.maxRequests);
res.setHeader('X-RateLimit-Remaining', result.remaining);
res.setHeader('X-RateLimit-Reset', new Date(result.resetTime).toISOString());
if (!result.allowed) {
// レート制限に達した場合
AuditLogManager.logRateLimitViolation(req.user?.id || req.ip || req.connection.remoteAddress || 'unknown', {
ip: req.ip || req.connection.remoteAddress || 'unknown',
path: req.path || req.url || 'unknown'
});
if (manager.config.onLimitReached) {
manager.config.onLimitReached(req, res);
}
else {
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000),
});
}
return;
}
next();
};
}
/**
* 指定されたキーの制限をリセット
*/
resetLimit(key) {
this.limitMap.delete(key);
}
/**
* 全ての制限をリセット
*/
resetAllLimits() {
this.limitMap.clear();
}
/**
* 統計情報を取得
*/
getStatistics() {
const now = Date.now();
const entries = [];
let activeKeys = 0;
for (const [key, entry] of this.limitMap.entries()) {
const timeDiff = now - entry.firstRequest;
const isActive = timeDiff < this.config.windowMs;
if (isActive) {
activeKeys++;
}
entries.push({
key,
count: entry.count,
remaining: Math.max(0, this.config.maxRequests - entry.count),
});
}
return {
totalKeys: this.limitMap.size,
activeKeys,
entries,
};
}
/**
* 期限切れエントリのクリーンアップタイマーを開始
*/
startCleanupTimer() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.cleanupInterval = setInterval(() => {
this.cleanupExpiredEntries();
}, this.config.windowMs);
}
/**
* 期限切れエントリをクリーンアップ
*/
cleanupExpiredEntries() {
const now = Date.now();
const keysToDelete = [];
for (const [key, entry] of this.limitMap.entries()) {
const timeDiff = now - entry.firstRequest;
if (timeDiff >= this.config.windowMs) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => this.limitMap.delete(key));
}
/**
* クリーンアップタイマーを停止
*/
stopCleanupTimer() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
}
/**
* スロー制限機能(段階的な制限)
*/
export class SlowDownManager {
constructor(config) {
this.config = config;
this.requestMap = new Map();
this.delayMap = new Map();
}
/**
* シングルトンインスタンスを取得
*/
static getInstance(config) {
if (!SlowDownManager.instance) {
// デフォルト設定を使用
SlowDownManager.instance = new SlowDownManager({
windowMs: config?.windowMs || parseInt(process.env.GFTD_RATE_LIMIT_WINDOW_MS || '60000'),
delayAfter: config?.delayAfter || 5,
delayMs: config?.delayMs || 100,
maxDelayMs: config?.maxDelayMs || 5000,
});
}
return SlowDownManager.instance;
}
/**
* 遅延時間を計算
*/
calculateDelay(key) {
const now = Date.now();
const entry = this.requestMap.get(key);
if (!entry) {
this.requestMap.set(key, { count: 1, firstRequest: now });
return 0;
}
const timeDiff = now - entry.firstRequest;
if (timeDiff >= this.config.windowMs) {
// 時間窓をリセット
this.requestMap.set(key, { count: 1, firstRequest: now });
return 0;
}
entry.count++;
if (entry.count <= this.config.delayAfter) {
return 0;
}
const excessRequests = entry.count - this.config.delayAfter;
const delay = Math.min(excessRequests * this.config.delayMs, this.config.maxDelayMs);
this.delayMap.set(key, delay);
return delay;
}
/**
* Express.js ミドルウェアを作成
*/
static createMiddleware(options) {
const manager = SlowDownManager.getInstance(options);
const keyGenerator = options?.keyGenerator || ((req) => req.ip || 'unknown');
return (req, res, next) => {
const key = keyGenerator(req);
const delay = manager.calculateDelay(key);
if (delay > 0) {
res.setHeader('X-Retry-After', Math.ceil(delay / 1000));
setTimeout(() => {
next();
}, delay);
}
else {
next();
}
};
}
}
/**
* 複数のレート制限戦略を組み合わせたミドルウェア
*/
export class CompositeRateLimitManager {
/**
* 複数のレート制限を組み合わせたミドルウェアを作成
*/
static createMiddleware(options) {
const middlewares = [];
// グローバルレート制限
if (options.global) {
middlewares.push(RateLimitManager.createMiddleware(options.global));
}
// ユーザーごとのレート制限
if (options.perUser) {
middlewares.push(RateLimitManager.createMiddleware({
...options.perUser,
keyGenerator: (req) => req.user?.id || req.ip || 'anonymous',
}));
}
// エンドポイントごとのレート制限
if (options.perEndpoint) {
middlewares.push(RateLimitManager.createMiddleware({
...options.perEndpoint,
keyGenerator: (req) => `${req.method}:${req.path}:${req.ip || 'unknown'}`,
}));
}
// スロー制限
if (options.slowDown) {
middlewares.push(SlowDownManager.createMiddleware(options.slowDown));
}
return (req, res, next) => {
let currentIndex = 0;
const executeNext = () => {
if (currentIndex >= middlewares.length) {
return next();
}
const middleware = middlewares[currentIndex++];
middleware(req, res, executeNext);
};
executeNext();
};
}
}
/**
* 特定のIPアドレスをブロックするミドルウェア
*/
export class IPBlockManager {
/**
* IPアドレスをブロック
*/
static blockIP(ip) {
this.blockedIPs.add(ip);
}
/**
* IPアドレスのブロックを解除
*/
static unblockIP(ip) {
this.blockedIPs.delete(ip);
}
/**
* 疑わしいIPアドレスを追跡
*/
static trackSuspiciousIP(ip) {
const now = Date.now();
const entry = this.suspiciousIPs.get(ip);
if (!entry) {
this.suspiciousIPs.set(ip, { count: 1, firstSeen: now });
}
else {
entry.count++;
// 一定回数以上の違反でブロック
if (entry.count >= 10) {
this.blockIP(ip);
AuditLogManager.logSecurityViolation(ip, 'IP_AUTO_BLOCKED', { ip, violationCount: entry.count });
}
}
}
/**
* IPブロックミドルウェアを作成
*/
static createMiddleware() {
return (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress || 'unknown';
if (this.blockedIPs.has(ip)) {
AuditLogManager.logSecurityViolation(ip, 'BLOCKED_IP_ACCESS', { ip, userAgent: req.headers['user-agent'] });
res.status(403).json({
error: 'Forbidden',
message: 'Access denied',
});
return;
}
next();
};
}
/**
* ブロック中のIPアドレス一覧を取得
*/
static getBlockedIPs() {
return Array.from(this.blockedIPs);
}
/**
* 疑わしいIPアドレスの統計を取得
*/
static getSuspiciousIPs() {
return Array.from(this.suspiciousIPs.entries()).map(([ip, entry]) => ({
ip,
count: entry.count,
firstSeen: new Date(entry.firstSeen),
}));
}
}
IPBlockManager.blockedIPs = new Set();
IPBlockManager.suspiciousIPs = new Map();
//# sourceMappingURL=rate-limit.js.map