Update codebase
This commit is contained in:
@@ -156,7 +156,9 @@ class JwtService
|
||||
|
||||
// 3. Issuer (Only check if configured)
|
||||
if (!empty($this->issuer) && ($decoded->iss ?? '') !== $this->issuer) {
|
||||
self::abort(401, 'Invalid token issuer: expected ' . $this->issuer . ' but got ' . ($decoded->iss ?? 'none'));
|
||||
// التفاصيل في اللوج فقط — لا تُكشف في الرد.
|
||||
error_log('[SECURITY] Issuer mismatch | expected: ' . $this->issuer . ' | got: ' . ($decoded->iss ?? 'none'));
|
||||
self::abort(401, 'Invalid token issuer');
|
||||
}
|
||||
|
||||
// 3.1 App Signature Verification (Service Only)
|
||||
@@ -228,8 +230,15 @@ class JwtService
|
||||
}
|
||||
|
||||
if ($fpInToken === null || $fpHeader === null) {
|
||||
$allHeaders = json_encode(getallheaders());
|
||||
error_log("[SECURITY] Fingerprint missing | user: $userId | fpInToken: " . ($fpInToken ?? 'NULL') . " | fpHeader: " . ($fpHeader ?? 'NULL') . " | Headers: $allHeaders");
|
||||
// ملاحظة: ممنوع تسجيل الهيدرز كاملة — كانت تُسرّب الـ
|
||||
// Authorization: Bearer <token> بالنص الصريح إلى error_log.
|
||||
// نسجّل فقط أيّ الطرفين ناقص، دون أي قيمة.
|
||||
error_log(sprintf(
|
||||
"[SECURITY] Fingerprint missing | user: %s | inToken: %s | inHeader: %s",
|
||||
$userId,
|
||||
$fpInToken === null ? 'no' : 'yes',
|
||||
$fpHeader === null ? 'no' : 'yes'
|
||||
));
|
||||
self::abort(403, 'Device verification required');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class RateLimiter
|
||||
// حدود مختلفة لكل نوع endpoint
|
||||
private const LIMITS = [
|
||||
'login' => ['requests' => 5, 'window' => 60], // 5 محاولات / دقيقة
|
||||
'tester_login' => ['requests' => 3, 'window' => 60], // 3 محاولات / دقيقة
|
||||
'tester_login' => ['requests' => 30, 'window' => 60], // 30 محاولة / دقيقة (مراجعو المتاجر يكرّرون الدخول بسرعة)
|
||||
'otp' => ['requests' => 3, 'window' => 300], // 3 محاولات / 5 دقائق
|
||||
'register' => ['requests' => 3, 'window' => 3600], // 3 محاولات / ساعة
|
||||
'api' => ['requests' => 180, 'window' => 60], // 180 طلب / دقيقة (الإنتاج الرسمى)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ class Database
|
||||
'user' => 'DB_TRANSIT_USER',
|
||||
'pass' => 'DB_TRANSIT_PASS',
|
||||
],
|
||||
'food' => [
|
||||
'name' => 'DB_FOOD_NAME',
|
||||
'host' => 'DB_FOOD_HOST',
|
||||
'user' => 'DB_FOOD_USER',
|
||||
'pass' => 'DB_FOOD_PASS',
|
||||
],
|
||||
];
|
||||
|
||||
public static function get(string $name = 'main'): PDO
|
||||
@@ -67,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,
|
||||
];
|
||||
|
||||
@@ -371,9 +371,48 @@ class SiroGeminiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
return trim(preg_replace('/```html|```/', '', $text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a driver support inquiry acting as Siro's smart assistant.
|
||||
*/
|
||||
public function answerSupportInquiry(
|
||||
string $question,
|
||||
array $driverContext,
|
||||
string $model = 'gemini-flash-lite-latest'
|
||||
): ?string {
|
||||
if (!$this->apiKey) return null;
|
||||
|
||||
$contextJson = json_encode($driverContext, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$prompt = "
|
||||
أنت مساعد الدعم الفني الذكي لتطبيق 'سيرو' (Siro) لنقل الركاب.
|
||||
وظيفتك مساعدة السائقين (الكباتن) والإجابة على استفساراتهم باحترافية، ودقة، وبلهجة ودية ومهذبة جداً.
|
||||
|
||||
بيانات السائق الحالية (سياق مهم للإجابة على سؤاله):
|
||||
$contextJson
|
||||
|
||||
سؤال السائق:
|
||||
\"$question\"
|
||||
|
||||
المطلوب منك:
|
||||
1. الإجابة مباشرة على سؤال السائق بناءً على السياق المعطى إذا كان يخص رحلاته أو رصيده المذكور في السياق.
|
||||
2. إذا سأل عن إجراءات عامة، أجب كخبير في سيرو.
|
||||
3. اجعل الإجابة ودية وواضحة جداً وقصيرة.
|
||||
4. الرد يكون باللغة العربية حصراً.
|
||||
5. لا تذكر تفاصيل تقنية (مثل json أو database)، بل تحدث كإنسان يقدم الدعم.
|
||||
";
|
||||
|
||||
$prompt .= "\n\nالرجاء إرجاع الإجابة بصيغة JSON فقط كالتالي:\n{\n \"reply\": \"نص الإجابة هنا\"\n}";
|
||||
|
||||
$response = $this->callGemini($prompt, $model);
|
||||
if ($response && isset($response['reply'])) {
|
||||
return $response['reply'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,3 +288,74 @@ function normalizePhone(string $phone): string
|
||||
return $d; // رقم خارج النطاق — يُعاد كما هو
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* إخفاء جزء من رقم الهاتف قبل عرضه لدور لا يملك صلاحية رؤيته كاملاً.
|
||||
*
|
||||
* نموذج الصلاحيات في اللوحة: `admin` يراقب بأرقام مُخفاة جزئياً،
|
||||
* و`super_admin` وحده يرى الرقم كاملاً.
|
||||
*
|
||||
* يُبقي مقدّمة الدولة وآخر رقمين حتى يبقى الرقم مميّزاً للمتابعة التشغيلية
|
||||
* دون كشف هوية صاحبه.
|
||||
*/
|
||||
function maskPhone(?string $phone): ?string
|
||||
{
|
||||
if ($phone === null || $phone === '') return $phone;
|
||||
|
||||
$d = preg_replace('/\D+/', '', $phone);
|
||||
$len = strlen($d);
|
||||
if ($len < 5) return str_repeat('*', max(0, $len));
|
||||
|
||||
$head = substr($d, 0, 3);
|
||||
$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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user