Update: 2026-07-10 03:04:06

This commit is contained in:
Hamza-Ayed
2026-07-10 03:04:06 +03:00
parent 2ff7450d0b
commit b18fd027b6
22 changed files with 2661 additions and 96 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"require": {
"firebase/php-jwt": "^7.0",
"workerman/phpsocket.io": "^2.2"
}
}
+3
View File
@@ -0,0 +1,3 @@
<?php
echo 'Hello World :-)';
+65
View File
@@ -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;
}
+95
View File
@@ -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]);
}
+23
View File
@@ -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.");
}
+191
View File
@@ -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