Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"require": {
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"workerman/phpsocket.io": "^2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?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');
|
||||
} 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();
|
||||
Reference in New Issue
Block a user