225 أسطر
7.9 KiB
JavaScript
225 أسطر
7.9 KiB
JavaScript
/**
|
|
* 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 });
|
|
}
|
|
);
|
|
*/
|