Files
2026-08-09 16:56:13 +03:00

251 lines
10 KiB
PHP

<?php
/**
* food_socket.php
* =====================
* WebSocket Server لطلبات الطعام — بورت 4040
* Internal HTTP Server — بورت 4041
*
* غرف Socket.IO:
* customer_food_{passenger_id} — الزبون (JWT role=passenger)
* courier_food_{driver_id} — السائق (JWT role=driver)
* merchant_food_{merchant_id} — لوحة المطعم (food session token)
*
* السوكيت ناقل إشعار لا مخزن حالة — عند إعادة الاتصال يسحب التطبيق
* order/status.php ويُصحّح نفسه. المصدر الوحيد للحقيقة هو قاعدة siro_food.
*/
use Workerman\Worker;
use PHPSocketIO\SocketIO;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
require_once __DIR__ . '/vendor/autoload.php';
$LOG_FILE = __DIR__ . '/socket_debug.log';
function socket_log($message, $data = null) {
global $LOG_FILE;
$logMsg = '[' . date('Y-m-d H:i:s') . "] $message";
if ($data !== null) {
$logMsg .= ' | DATA: ' . (is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE));
}
$logMsg .= PHP_EOL;
echo $logMsg;
@file_put_contents($LOG_FILE, $logMsg, FILE_APPEND);
}
socket_log('=== STARTING FOOD SOCKET SERVER ===');
function loadEnvironment(string $filePath): void {
if (!file_exists($filePath)) {
socket_log("[WARNING] .env not found: $filePath");
return;
}
foreach (file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
[$name, $value] = explode('=', $line, 2);
putenv(trim($name) . '=' . trim($value, "\"'"));
}
socket_log('✅ Environment loaded.');
}
$envPaths = [__DIR__ . '/../docker/.env', __DIR__ . '/.env'];
foreach ($envPaths as $p) {
if (file_exists($p)) { loadEnvironment($p); break; }
}
function getInternalKey(): string {
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) return trim((string)@file_get_contents($keyPath));
if (file_exists('/keys/.internal_socket_key')) return trim((string)@file_get_contents('/keys/.internal_socket_key'));
return getenv('INTERNAL_SOCKET_KEY') ?: '';
}
function getJwtSecret(): string {
$keyPath = getenv('JWT_SECRET_KEY_PATH');
if ($keyPath && file_exists($keyPath)) return trim(file_get_contents($keyPath));
if (file_exists('/keys/jwt_secret_key')) return trim(file_get_contents('/keys/jwt_secret_key'));
return getenv('JWT_SECRET_KEY') ?: (getenv('JWT_SECRET') ?: '');
}
// اتصال Redis — للتحقق من صلاحية جلسة لوحة المطعم (food:merchant_session:{hash})
$_redis = null;
function getFoodRedis(): ?\Redis {
global $_redis;
if ($_redis !== null) {
try { $_redis->ping(); return $_redis; }
catch (\Exception $_) { $_redis = null; }
}
try {
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_PASSWORD') ?: '';
$host = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1');
$r = new \Redis();
$r->connect($host, (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379), 1.5);
if ($redisPass) $r->auth($redisPass);
$r->setOption(\Redis::OPT_PREFIX, 'siro:');
$_redis = $r;
return $r;
} catch (\Exception $e) {
socket_log('[REDIS_ERROR] Food Redis unavailable: ' . $e->getMessage());
return null;
}
}
$INTERNAL_KEY = getInternalKey();
if (empty($INTERNAL_KEY)) {
socket_log('[CRITICAL_ERROR] Internal key missing! Exiting.');
exit(1);
}
$PORT = 4040;
$INTERNAL_PORT = 4041;
$io = new SocketIO($PORT);
$io->on('workerStart', function () use ($io, $INTERNAL_KEY, $INTERNAL_PORT) {
$innerHttp = new Worker("http://0.0.0.0:$INTERNAL_PORT");
$innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) {
$headers = $request->header();
$clientIp = $connection->getRemoteIp();
if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) {
socket_log("[HTTP_ERROR] Unauthorized internal request from IP: $clientIp");
$connection->send('Unauthorized');
return;
}
$post = $request->post();
$action = trim($post['action'] ?? '');
$rawPayload = $post['payload'] ?? null;
$payload = is_string($rawPayload) ? (json_decode($rawPayload, true) ?? []) : ($rawPayload ?? []);
if ($action === 'order_status_update') {
$orderId = $payload['order_id'] ?? null;
$passengerId = $payload['passenger_id'] ?? null;
$merchantId = $payload['merchant_id'] ?? null;
$courierId = $payload['courier_id'] ?? null;
if (!$orderId) {
$connection->send('Error: Missing order_id');
return;
}
if ($passengerId) $io->to('customer_food_' . $passengerId)->emit('food_order_update', $payload);
if ($merchantId) $io->to('merchant_food_' . $merchantId)->emit('food_order_update', $payload);
if ($courierId) $io->to('courier_food_' . $courierId)->emit('food_order_update', $payload);
socket_log("[HTTP_SUCCESS] order_status_update pushed for order #$orderId", $payload);
$connection->send('OK');
} elseif ($action === 'courier_offer') {
$courierId = $payload['courier_id'] ?? null;
if (!$courierId) { $connection->send('Error: Missing courier_id'); return; }
$io->to('courier_food_' . $courierId)->emit('food_delivery_offer', $payload);
socket_log("[HTTP_SUCCESS] courier_offer pushed to courier #$courierId", $payload);
$connection->send('OK');
} elseif ($action === 'courier_location') {
// موقع السائق أثناء التوصيل — للزبون صاحب الطلب وحده
$passengerId = $payload['passenger_id'] ?? null;
if (!$passengerId) { $connection->send('Error: Missing passenger_id'); return; }
unset($payload['passenger_id']);
$io->to('customer_food_' . $passengerId)->emit('food_courier_location', $payload);
$connection->send('OK');
} elseif ($action === 'courier_call') {
// مكالمة مقنّعة من الزبون إلى السائق — نمرّر session_id فقط،
// لا رقم هاتف ولا هوية حقيقية لأي طرف.
$courierId = $payload['courier_id'] ?? null;
$sessionId = $payload['session_id'] ?? null;
if (!$courierId || !$sessionId) {
$connection->send('Error: Missing courier_id/session_id');
return;
}
$io->to('courier_food_' . $courierId)->emit('food_incoming_call', $payload);
socket_log("[HTTP_SUCCESS] courier_call pushed to courier #$courierId (order #" . ($payload['order_id'] ?? '?') . ')');
$connection->send('OK');
} else {
socket_log("[HTTP_WARNING] Unknown action received: $action", $post);
$connection->send('Unknown action: ' . $action);
}
};
$innerHttp->listen();
socket_log("[INFO] Internal HTTP started on port $INTERNAL_PORT");
});
$io->on('connection', function ($socket) {
$query = $socket->handshake['query'] ?? [];
$role = $query['role'] ?? ''; // passenger | driver | merchant
$clientIp = $socket->conn->remoteAddress ?? 'Unknown';
if ($role === 'passenger' || $role === 'driver') {
$userId = $query['id'] ?? null;
$jwtToken = $query['jwt'] ?? '';
if (!$userId || !$jwtToken) {
socket_log("[SOCKET_REJECTED] Missing id/jwt for role=$role from IP: $clientIp");
$socket->disconnect();
return;
}
try {
$secretKey = getJwtSecret();
if (empty($secretKey)) {
socket_log('[WARNING] JWT secret not configured!');
$socket->disconnect();
return;
}
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
$expectedRole = $role === 'passenger' ? 'passenger' : 'driver';
if ((string)($decoded->user_id ?? $decoded->sub ?? '') !== (string)$userId || ($decoded->role ?? '') !== $expectedRole) {
socket_log("[SOCKET_REJECTED] Invalid JWT for $role #$userId from IP: $clientIp");
$socket->disconnect();
return;
}
} catch (\Exception $e) {
socket_log('[SOCKET_REJECTED] JWT verification failed -> ' . $e->getMessage());
$socket->disconnect();
return;
}
$room = ($role === 'passenger' ? 'customer_food_' : 'courier_food_') . $userId;
$socket->join($room);
socket_log("[SOCKET_CONNECTED] $role #$userId joined $room (IP: $clientIp)");
} elseif ($role === 'merchant') {
$merchantId = $query['id'] ?? null;
$sessionToken = $query['token'] ?? '';
if (!$merchantId || !$sessionToken) {
socket_log("[SOCKET_REJECTED] Missing id/token for role=merchant from IP: $clientIp");
$socket->disconnect();
return;
}
$redis = getFoodRedis();
if (!$redis) { $socket->disconnect(); return; }
$hash = hash('sha256', $sessionToken);
$val = $redis->get("food:merchant_session:{$hash}");
if (!$val) {
socket_log("[SOCKET_REJECTED] Invalid/expired merchant session from IP: $clientIp");
$socket->disconnect();
return;
}
$data = json_decode($val, true);
if (!$data || (string)($data['merchant_id'] ?? '') !== (string)$merchantId) {
socket_log("[SOCKET_REJECTED] Merchant session/id mismatch from IP: $clientIp");
$socket->disconnect();
return;
}
$room = 'merchant_food_' . $merchantId;
$socket->join($room);
socket_log("[SOCKET_CONNECTED] merchant #$merchantId joined $room (IP: $clientIp)");
} else {
socket_log("[SOCKET_REJECTED] Unknown role '$role' from IP: $clientIp");
$socket->disconnect();
return;
}
$socket->on('heartbeat', function () {});
});
Worker::runAll();