232 lines
9.0 KiB
PHP
232 lines
9.0 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');
|
|
} 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();
|