/** * Rate Limiter Middleware للحماية من Brute Force * للتطبيقات المنشورة على غيمة (ghaymah.systems) */ const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); // ═══════════════════════════════════════════════════════════════ // إعداد Redis (من غيمة) // ═══════════════════════════════════════════════════════════════ const redis = new Redis(process.env.GHAYMAH_REDIS_URL || 'redis://localhost:6379'); // ═══════════════════════════════════════════════════════════════ // Rate Limiter لـ Login API // ═══════════════════════════════════════════════════════════════ const loginLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => redis.call(...args), prefix: 'rl:login:' }), // 5 محاولات كل 15 دقيقة windowMs: 15 * 60 * 1000, max: 5, // رسالة الخطأ message: { error: 'محاولات كثيرة جداً', message: 'تم حظرك مؤقتاً. حاول مرة أخرى بعد 15 دقيقة', retryAfter: 15 * 60 }, // Headers قياسية standardHeaders: true, legacyHeaders: false, // تخطي الطلبات الناجحة skipSuccessfulRequests: false, // معالج مخصص عند تجاوز الحد handler: (req, res, next, options) => { // تسجيل للمراقبة console.warn(`[SECURITY] Rate limit exceeded`, { ip: req.ip, path: req.path, userAgent: req.get('User-Agent'), timestamp: new Date().toISOString() }); // إرسال metric لـ Prometheus if (global.metrics) { global.metrics.rateLimitExceeded.inc({ path: '/api/login', ip: req.ip }); } res.status(429).json(options.message); }, // تحديد المفتاح (IP + username إن وجد) keyGenerator: (req) => { const username = req.body?.username || 'unknown'; return `${req.ip}:${username}`; } }); // ═══════════════════════════════════════════════════════════════ // Rate Limiter عام للـ API // ═══════════════════════════════════════════════════════════════ const apiLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => redis.call(...args), prefix: 'rl:api:' }), // 100 طلب كل دقيقة windowMs: 60 * 1000, max: 100, message: { error: 'طلبات كثيرة', message: 'تجاوزت الحد المسموح. حاول لاحقاً' }, standardHeaders: true }); // ═══════════════════════════════════════════════════════════════ // Rate Limiter للـ endpoints الحساسة // ═══════════════════════════════════════════════════════════════ const sensitiveEndpointLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => redis.call(...args), prefix: 'rl:sensitive:' }), // 10 طلبات كل 5 دقائق windowMs: 5 * 60 * 1000, max: 10, message: { error: 'وصول محدود', message: 'هذا الـ endpoint محمي. حاول لاحقاً' } }); // ═══════════════════════════════════════════════════════════════ // Account Lockout بعد محاولات فاشلة // ═══════════════════════════════════════════════════════════════ const failedAttempts = new Map(); // في الإنتاج استخدم Redis const accountLockout = async (req, res, next) => { const username = req.body?.username; if (!username) return next(); const key = `lockout:${username}`; const lockoutData = await redis.get(key); if (lockoutData) { const data = JSON.parse(lockoutData); // التحقق من القفل if (data.locked && Date.now() < data.lockedUntil) { const remainingMinutes = Math.ceil((data.lockedUntil - Date.now()) / 60000); return res.status(423).json({ error: 'الحساب مقفل', message: `تم قفل الحساب بسبب محاولات فاشلة متكررة. حاول بعد ${remainingMinutes} دقيقة`, lockedUntil: new Date(data.lockedUntil).toISOString() }); } } next(); }; // تسجيل المحاولة الفاشلة const recordFailedAttempt = async (username) => { const key = `lockout:${username}`; let data = await redis.get(key); if (data) { data = JSON.parse(data); data.attempts += 1; } else { data = { attempts: 1, locked: false }; } // قفل بعد 5 محاولات if (data.attempts >= 5) { data.locked = true; data.lockedUntil = Date.now() + (30 * 60 * 1000); // 30 دقيقة console.warn(`[SECURITY] Account locked: ${username}`, { attempts: data.attempts, lockedUntil: new Date(data.lockedUntil).toISOString() }); } // حفظ لمدة ساعة await redis.setex(key, 3600, JSON.stringify(data)); return data; }; // مسح المحاولات بعد نجاح الدخول const clearFailedAttempts = async (username) => { await redis.del(`lockout:${username}`); }; // ═══════════════════════════════════════════════════════════════ // Exports // ═══════════════════════════════════════════════════════════════ module.exports = { loginLimiter, apiLimiter, sensitiveEndpointLimiter, accountLockout, recordFailedAttempt, clearFailedAttempts }; // ═══════════════════════════════════════════════════════════════ // مثال الاستخدام في Express // ═══════════════════════════════════════════════════════════════ /* const express = require('express'); const { loginLimiter, apiLimiter, accountLockout, recordFailedAttempt, clearFailedAttempts } = require('./rate-limiter'); const app = express(); // تطبيق على جميع الـ APIs app.use('/api', apiLimiter); // تطبيق على Login app.post('/api/login', loginLimiter, accountLockout, async (req, res) => { const { username, password } = req.body; const user = await authenticate(username, password); if (!user) { await recordFailedAttempt(username); return res.status(401).json({ error: 'بيانات خاطئة' }); } // نجاح - مسح المحاولات الفاشلة await clearFailedAttempts(username); const token = generateToken(user); res.json({ token }); } ); */