Update: 2026-07-10 03:04:06
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"require": {
|
||||
"firebase/php-jwt": "^7.0",
|
||||
"workerman/phpsocket.io": "^2.2"
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
echo 'Hello World :-)';
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
// ride_server/intaleq/connect.php
|
||||
// Bootstrap for standalone REST endpoints under ride_server/intaleq/.
|
||||
// Modeled on loction_server/siro/connect.php (the JWT-enforcing variant).
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/load_env.php';
|
||||
|
||||
// .env path mirrors ride_server/passenger_socket.php's own already-deployed
|
||||
// convention on this box — confirm/adjust with ops at deploy time.
|
||||
$env_file = file_exists(__DIR__ . '/../ride-keys/.env')
|
||||
? __DIR__ . '/../ride-keys/.env'
|
||||
: (file_exists('/home/intaleq-rides/env/.env') ? '/home/intaleq-rides/env/.env' : '');
|
||||
if (!empty($env_file)) {
|
||||
loadEnvironment($env_file);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
// --- CORS + JSON headers ---
|
||||
header("Access-Control-Allow-Origin: https://intaleqapp.com");
|
||||
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||
header('Content-Type: application/json');
|
||||
|
||||
date_default_timezone_set('Asia/Amman');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- JWT auth — enforced. This endpoint returns ride status tied to a
|
||||
// specific passenger/driver, and backend's connect.php enforces JWT for the
|
||||
// same call today; dropping auth here would be a regression. ---
|
||||
$decodedToken = authenticateJWT();
|
||||
$user_id = $decodedToken->sub ?? $decodedToken->user_id ?? null;
|
||||
|
||||
// --- DB connection: same physical database backend's Database::get('main')
|
||||
// resolves to (confirmed distinct from Database::get('ride') — the getRideStatus
|
||||
// endpoint this replaces has always read from 'main', so the fallback query
|
||||
// below stays on 'main' too, for zero behavior change vs. today). Env var
|
||||
// names match backend/core/Database/Database.php's 'main' entry exactly;
|
||||
// falling back to loction_server's simpler names only if ops sets this box
|
||||
// up differently. Actual values must be confirmed/provisioned by ops. ---
|
||||
$dbname = getenv('DB_PRIMARY_NAME_V2') ?: getenv('dbname');
|
||||
$dbhost = getenv('DB_PRIMARY_HOST_V2') ?: 'localhost';
|
||||
$dbuser = getenv('DB_PRIMARY_USER_V2') ?: getenv('USER');
|
||||
$dbpass = getenv('DB_PRIMARY_PASS_V2') ?: getenv('PASS');
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host=$dbhost;dbname=$dbname;charset=utf8mb4";
|
||||
$con = new PDO($dsn, $dbuser, $dbpass, [
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8",
|
||||
PDO::ATTR_TIMEOUT => 5,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log("[ride_server/connect] DB connection failed: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'A database error occurred.']);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// ride_server/intaleq/functions.php
|
||||
// Bootstrap helpers for standalone REST endpoints under ride_server/intaleq/.
|
||||
// Mirrors the env/key conventions already deployed on this box for
|
||||
// ride_server/passenger_socket.php (/home/intaleq-rides/...), not invented.
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
|
||||
// Shared secret used to authenticate to loction_server's internal HTTP API —
|
||||
// same mechanism ride_server/passenger_socket.php already uses on this box.
|
||||
function getInternalSocketKey(): string {
|
||||
$key = getenv('INTERNAL_SOCKET_KEY');
|
||||
if ($key) return trim($key);
|
||||
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/intaleq-rides/.internal_socket_key';
|
||||
if (file_exists($path)) return trim((string) @file_get_contents($path));
|
||||
return '';
|
||||
}
|
||||
|
||||
// JWT signing secret — must resolve to the same value backend's
|
||||
// core/Auth/JwtService.php signs tokens with, or every token fails here.
|
||||
function getJwtSecret(): string {
|
||||
$keyPath = getenv('JWT_SECRET_KEY_PATH');
|
||||
if ($keyPath && file_exists($keyPath)) {
|
||||
return trim(file_get_contents($keyPath));
|
||||
}
|
||||
return getenv('JWT_SECRET_KEY') ?: '';
|
||||
}
|
||||
|
||||
function authenticateJWT() {
|
||||
$secretKey = getJwtSecret();
|
||||
if (!$secretKey) {
|
||||
error_log("[ride_server] JWT secret not configured.");
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Internal server configuration error.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
$token = null;
|
||||
if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
if (!$token) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Authorization token required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
return JWT::decode($token, new Key($secretKey, 'HS256'));
|
||||
} catch (ExpiredException $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Token expired']);
|
||||
exit;
|
||||
} catch (SignatureInvalidException $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Invalid token signature']);
|
||||
exit;
|
||||
} catch (BeforeValidException $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Token not yet valid']);
|
||||
exit;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Invalid token']);
|
||||
exit;
|
||||
}
|
||||
// NOTE: signature/expiry only — does not replicate backend's
|
||||
// core/Auth/JwtService::authenticate() JTI-blacklist (revoked-token)
|
||||
// check or X-Device-FP verification. Flagged as a follow-up in the
|
||||
// plan; not blocking for this read-only endpoint.
|
||||
}
|
||||
|
||||
// Flutter's CRUD().get() always issues an HTTP POST under the hood
|
||||
// (see siro_rider/lib/controller/functions/crud.dart _makeRequest/doPost) —
|
||||
// so every field this endpoint reads comes through $_POST, never $_GET.
|
||||
function filterRequest($requestname, $type = 'string') {
|
||||
if (isset($_POST[$requestname]) && $_POST[$requestname] !== '') {
|
||||
$value = trim($_POST[$requestname]);
|
||||
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
|
||||
if ($type === 'numeric') {
|
||||
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false ? $value : null;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function printFailure($message = "none") {
|
||||
echo json_encode(["status" => "failure", "message" => $message]);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
function loadEnvironment($env_file) {
|
||||
if (!file_exists($env_file)) {
|
||||
error_log("❌ .env not found: $env_file");
|
||||
return false;
|
||||
}
|
||||
|
||||
$lines = file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line) || strpos($line, '#') === 0) continue;
|
||||
$parts = explode('=', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
[$keyName, $value] = $parts;
|
||||
$value = trim($value, "\"'");
|
||||
putenv("$keyName=$value");
|
||||
$_ENV[$keyName] = $value;
|
||||
$_SERVER[$keyName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
// ride_server/intaleq/ride/rides/getRideStatus.php
|
||||
// Serves https://rides.intaleq.xyz/intaleq/ride/rides/getRideStatus.php
|
||||
// Same response contract as the legacy backend/ride/rides/getRideStatus.php:
|
||||
// {"status":"success","data":"<ride status string>"}
|
||||
//
|
||||
// Reads Redis truth via loction_server's internal HTTP API first (same
|
||||
// "ask location server, fall back to DB" shape as backend/ride/location/get.php);
|
||||
// on any miss/timeout/error, falls back to a direct MySQL SELECT so behavior
|
||||
// never regresses to "no answer."
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$id = filterRequest("id");
|
||||
$rideId = (int) $id;
|
||||
|
||||
if (empty($id) || $rideId <= 0) {
|
||||
printFailure("Missing ride ID.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── 1. Try Redis via the location server's internal HTTP API ──────────────
|
||||
$status = null;
|
||||
|
||||
$locationServerUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
$internalKey = getInternalSocketKey();
|
||||
|
||||
if ($internalKey) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $locationServerUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||
'action' => 'get_ride_state',
|
||||
'ride_id' => $rideId,
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
// Foreground/synchronous read — the answer IS the response, so this is
|
||||
// deliberately longer than the 200-500ms fire-and-forget write timeouts
|
||||
// used elsewhere in this codebase.
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 500);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1500);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlErr = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if (!$curlErr && $httpCode === 200 && $response) {
|
||||
$json = json_decode($response, true);
|
||||
if (is_array($json) && ($json['status'] ?? false) === true
|
||||
&& !empty($json['data']['status'])) {
|
||||
$status = $json['data']['status'];
|
||||
}
|
||||
} else {
|
||||
error_log("[getRideStatus] location-server read miss/error for ride=$rideId: "
|
||||
. ($curlErr ?: "HTTP $httpCode"));
|
||||
}
|
||||
} else {
|
||||
error_log("[getRideStatus] internal key not configured — skipping Redis read, using DB fallback.");
|
||||
}
|
||||
|
||||
// ── 2. Fallback: direct MySQL read (identical query to the legacy file) ───
|
||||
if ($status === null) {
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT `status` FROM `ride` WHERE `id` = :id");
|
||||
$stmt->bindParam(':id', $rideId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row && isset($row['status'])) {
|
||||
$status = $row['status'];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("[getRideStatus] DB fallback error for ride=$rideId: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($status !== null) {
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"data" => $status
|
||||
]);
|
||||
} else {
|
||||
printFailure("Ride not found.");
|
||||
}
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* passenger_socket.php
|
||||
* =====================
|
||||
* WebSocket Server للركاب — بورت 3030
|
||||
* Internal HTTP Server — بورت 3031
|
||||
*/
|
||||
|
||||
use Workerman\Worker;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// نظام تسجيل الأحداث (Logging System)
|
||||
// ---------------------------------------------------------
|
||||
$LOG_FILE = __DIR__ . '/socket_debug.log';
|
||||
|
||||
function socket_log($message, $data = null) {
|
||||
global $LOG_FILE;
|
||||
$date = date('Y-m-d H:i:s');
|
||||
$logMsg = "[$date] $message";
|
||||
if ($data !== null) {
|
||||
$logMsg .= " | DATA: " . (is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
$logMsg .= PHP_EOL;
|
||||
|
||||
echo $logMsg; // للطباعة في الكونسول إذا كان يعمل في الـ Foreground
|
||||
@file_put_contents($LOG_FILE, $logMsg, FILE_APPEND); // الكتابة في الملف
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
socket_log("=== STARTING PASSENGER 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, "\"'"));
|
||||
}
|
||||
}
|
||||
loadEnvironment('/home/intaleq-rides/env/.env');
|
||||
|
||||
function getInternalSocketKey(): string {
|
||||
$key = getenv('INTERNAL_SOCKET_KEY');
|
||||
if ($key) return trim($key);
|
||||
$path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/intaleq-rides/.internal_socket_key';
|
||||
if (file_exists($path)) return trim((string) @file_get_contents($path));
|
||||
return '';
|
||||
}
|
||||
|
||||
$INTERNAL_KEY = getInternalSocketKey();
|
||||
|
||||
function getJwtSecret(): string {
|
||||
$keyPath = getenv('JWT_SECRET_KEY_PATH');
|
||||
if ($keyPath && file_exists($keyPath)) {
|
||||
return trim(file_get_contents($keyPath));
|
||||
}
|
||||
return getenv('JWT_SECRET_KEY') ?: '';
|
||||
}
|
||||
|
||||
if (empty($INTERNAL_KEY)) {
|
||||
socket_log("[CRITICAL_ERROR] Internal key missing! Exiting.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$PORT = 3030;
|
||||
$INTERNAL_PORT = 3031;
|
||||
|
||||
$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'] ?? '');
|
||||
|
||||
if ($action === 'update_ride_status') {
|
||||
|
||||
$passengerId = $post['passenger_id'] ?? null;
|
||||
$rawPayload = $post['payload'] ?? null;
|
||||
|
||||
if (!$passengerId || !$rawPayload) {
|
||||
socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_ride_status", $post);
|
||||
$connection->send('Error: Missing passenger_id or payload');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = is_string($rawPayload)
|
||||
? (json_decode($rawPayload, true) ?? $rawPayload)
|
||||
: $rawPayload;
|
||||
|
||||
socket_log("[HTTP_SUCCESS] Emitting 'ride_status_change' to Passenger #$passengerId", $payload);
|
||||
$io->to('passenger_' . $passengerId)->emit('ride_status_change', $payload);
|
||||
|
||||
$connection->send('OK');
|
||||
|
||||
} elseif ($action === 'update_driver_location') {
|
||||
|
||||
$passengerId = $post['passenger_id'] ?? null;
|
||||
$rawPayload = $post['payload'] ?? null;
|
||||
|
||||
if (!$passengerId || !$rawPayload) {
|
||||
socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_driver_location", $post);
|
||||
$connection->send('Error: Missing passenger_id or payload');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = is_string($rawPayload)
|
||||
? (json_decode($rawPayload, true) ?? $rawPayload)
|
||||
: $rawPayload;
|
||||
|
||||
socket_log("[HTTP_SUCCESS] Emitting 'driver_location_update' to Passenger #$passengerId", $payload);
|
||||
$io->to('passenger_' . $passengerId)->emit('driver_location_update', $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'] ?? [];
|
||||
$passengerId = $query['id'] ?? null;
|
||||
$jwtToken = $query['jwt'] ?? ''; // JWT Token for authentication
|
||||
$clientIp = $socket->conn->remoteAddress ?? 'Unknown';
|
||||
|
||||
if (!$passengerId || empty($jwtToken)) {
|
||||
socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID or JWT missing) from IP: $clientIp");
|
||||
$socket->disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$secretKey = getJwtSecret();
|
||||
if (empty($secretKey)) {
|
||||
socket_log("[WARNING] JWT Secret is not configured on the server!");
|
||||
} else {
|
||||
$decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256'));
|
||||
if ((string)$decoded->sub !== (string)$passengerId || $decoded->role !== 'passenger') {
|
||||
socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp");
|
||||
$socket->disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
socket_log("[SOCKET_REJECTED] Connection rejected: JWT Verification failed -> " . $e->getMessage() . " from IP: $clientIp");
|
||||
$socket->disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
$socket->join('passenger_' . $passengerId);
|
||||
socket_log("[SOCKET_CONNECTED] Passenger Connected: #$passengerId (IP: $clientIp)");
|
||||
|
||||
$socket->on('heartbeat', function ($data) {
|
||||
// يمكن تفعيل السطر التالي للتأكد من النبضات إذا أردت دقة شديدة، لكنه قد يملأ ملف الـ log
|
||||
// socket_log("[SOCKET_HEARTBEAT] Received from Passenger #$passengerId");
|
||||
});
|
||||
|
||||
$socket->on('disconnect', function () use ($passengerId, $clientIp) {
|
||||
socket_log("[SOCKET_DISCONNECTED] Passenger Disconnected: #$passengerId (IP: $clientIp)");
|
||||
});
|
||||
});
|
||||
|
||||
Worker::runAll();
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user