Update: 2026-08-03 11:52:03

This commit is contained in:
Hamza-Ayed
2026-08-03 11:52:03 +03:00
parent 4abfd30ddd
commit ec467b01c6
20 changed files with 152 additions and 5 deletions
+9 -1
View File
@@ -79,7 +79,15 @@ class RateLimiter
// ── بناء معرّف المستخدم ────────────────────────────────────
public static function identifier(?string $userId = null): string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// كان REMOTE_ADDR وحده، وهو خلف دوكر دائماً عنوان الوسيط نفسه
// (172.24.0.1) — فيتشارك كل المستخدمين دلواً واحداً، ومحاولات دخول
// فاشلة من عميل واحد تحجب الجميع بـ 429 (ظهر ذلك في سجل الإنتاج).
// clientIp() تستخرج العميل الحقيقي من X-Forwarded-For ولا تثق به
// إلا إذا كان REMOTE_ADDR وسيطاً موثوقاً.
$ip = function_exists('clientIp')
? clientIp()
: ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
return $userId ? "{$ip}:{$userId}" : $ip;
}
+9 -1
View File
@@ -73,7 +73,15 @@ class Database
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_PERSISTENT => true,
// الاتصالات الدائمة أُوقفت: كانت تُنتج "Packets out of order.
// Expected 1 received 0" بتكرار في الإنتاج — وهو عرض معروف لإعادة
// استخدام اتصال دائم تُرك في حالة غير نظيفة، أو أغلقه MySQL عند
// wait_timeout بينما ما زال PHP يعدّه حياً. والأسوأ أن التحذير
// يُطبع قبل الرؤوس فيُفسد الاستجابة بـ "headers already sent".
//
// الكلفة اتصال جديد لكل طلب، وهي مقبولة هنا: MySQL على شبكة دوكر
// نفسها و max-connections=300. الاستقرار أولى من هذا التوفير.
PDO::ATTR_PERSISTENT => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
PDO::ATTR_TIMEOUT => 10,
];
+49
View File
@@ -310,3 +310,52 @@ function maskPhone(?string $phone): ?string
$tail = substr($d, -2);
return $head . str_repeat('*', $len - 5) . $tail;
}
/**
* عنوان العميل الحقيقي خلف الوسطاء.
*
* سلسلة الطلب: العميل ← nginx المضيف ← nginx الحاوية ← php-fpm، فيصل
* REMOTE_ADDR دائماً كعنوان الوسيط. الاعتماد عليه وحده جعل RateLimiter يضع
* كل المستخدمين في دلو واحد، فمحاولات دخول فاشلة من عميل واحد تحجب الجميع.
*
* X-Forwarded-For قابل للتزوير من العميل، لذا لا نثق به إلا إذا كان
* REMOTE_ADDR نفسه وسيطاً موثوقاً (شبكات خاصة/لوكال). خلاف ذلك نعيد
* REMOTE_ADDR كما هو.
*/
function clientIp(): string
{
$remote = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$isTrustedProxy = $remote !== 'unknown' && (
filter_var(
$remote,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === false
);
if (!$isTrustedProxy) {
return $remote;
}
$forwarded = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($forwarded === '') {
return $remote;
}
// أول عنوان عام في السلسلة هو العميل؛ نتخطّى العناوين الخاصة التي
// أضافها الوسطاء.
foreach (explode(',', $forwarded) as $candidate) {
$candidate = trim($candidate);
if ($candidate === '') continue;
if (filter_var(
$candidate,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) !== false) {
return $candidate;
}
}
return $remote;
}