Files
tripz-llc/backend/core/Auth/RateLimiter.php
T
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

135 lines
5.3 KiB
PHP

<?php
// ============================================================
// core/Auth/RateLimiter.php
// Sliding Window Rate Limiting باستخدام Redis
// ============================================================
class RateLimiter
{
private ?Redis $redis;
// حدود مختلفة لكل نوع endpoint
private const LIMITS = [
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
'tester_login' => ['requests' => 3, 'window' => 60], // 3 محاولات / دقيقة
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
'api' => ['requests' => 180, 'window' => 60], // 180 طلب / دقيقة (الإنتاج الرسمى)
'ride' => ['requests' => 60, 'window' => 60], // 60 طلب / دقيقة (الإنتاج الرسمي)
'upload' => ['requests' => 10, 'window' => 300], // 10 رفع / 5 دقائق
'complaint' => ['requests' => 5, 'window' => 600], // 5 شكاوى / 10 دقائق (كل شكوى تستدعي Gemini + واتساب)
];
public function __construct(?Redis $redis)
{
$this->redis = $redis;
}
// ── فحص الحد ─────────────────────────────────────────────
// $identifier: IP:userId أو IP فقط
// $type: login | otp | api | ride | upload
public function check(string $identifier, string $type = 'api'): bool
{
if (getenv('DISABLE_RATE_LIMITER') === 'true' || ($_ENV['DISABLE_RATE_LIMITER'] ?? '') === 'true') {
return true;
}
if (!$this->redis) {
// HIGH-01 FIX: fallback مع ملف بدلاً من تمرير كل الطلبات
return $this->fileBasedCheck($identifier, $type);
}
$limit = self::LIMITS[$type] ?? self::LIMITS['api'];
$window = $limit['window'];
$max = $limit['requests'];
$key = "rate:{$type}:{$identifier}";
$current = $this->redis->incr($key);
if ($current === 1) {
$this->redis->expire($key, $window);
}
return $current <= $max;
}
// ── تطبيق الحد وإيقاف الطلب إن تجاوز ─────────────────────
public function enforce(string $identifier, string $type = 'api'): void
{
if (getenv('DISABLE_RATE_LIMITER') === 'true' || ($_ENV['DISABLE_RATE_LIMITER'] ?? '') === 'true') {
return;
}
if (!$this->check($identifier, $type)) {
$limit = self::LIMITS[$type] ?? self::LIMITS['api'];
$window = $limit['window'];
error_log("[RATE_LIMIT] Blocked: $identifier | type: $type");
http_response_code(429);
header("Retry-After: $window");
echo json_encode([
'error' => 'Too many requests. Please slow down.',
'retry_after' => $window,
]);
exit;
}
}
// ── بناء معرّف المستخدم ────────────────────────────────────
public static function identifier(?string $userId = null): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return $userId ? "{$ip}:{$userId}" : $ip;
}
// ── إعادة تعيين عداد (مثلاً بعد تسجيل دخول ناجح) ───────────
public function reset(string $identifier, string $type = 'login'): void
{
if ($this->redis) {
$this->redis->del("rate:{$type}:{$identifier}");
} else {
// HIGH-01: مسح ملف الفل باك عند إعادة التعيين
$key = self::sanitizeKey("rate:{$type}:{$identifier}");
$tmpFile = sys_get_temp_dir() . "/rate_{$key}.json";
if (file_exists($tmpFile)) {
@unlink($tmpFile);
}
}
}
// ── Fallback باستخدام ملفات مؤقتة عند تعطل Redis ───────────
private function fileBasedCheck(string $identifier, string $type): bool
{
$limit = self::LIMITS[$type] ?? self::LIMITS['api'];
$window = $limit['window'];
$max = $limit['requests'];
$key = self::sanitizeKey("rate:{$type}:{$identifier}");
$tmpFile = sys_get_temp_dir() . "/rate_{$key}.json";
$now = time();
$data = [];
if (file_exists($tmpFile)) {
$data = json_decode(file_get_contents($tmpFile), true) ?: [];
}
// تنظيف النوافذ القديمة
$data = array_filter($data, fn($ts) => $ts > ($now - $window));
if (count($data) >= $max) {
error_log("[RATE_LIMIT_FB] File-based block: $identifier | type: $type");
return false;
}
$data[] = $now;
file_put_contents($tmpFile, json_encode($data));
return true;
}
private static function sanitizeKey(string $key): string
{
return preg_replace('/[^a-zA-Z0-9_\-:]/', '_', $key);
}
}