From 189467beabb37b0b5bce2f9425b0f1265519d3fc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:00:06 +0000 Subject: [PATCH 1/2] Implement File-Based UHST Relay Server in PHP - Implemented core UHST signaling endpoints: host, join, ping, SSE, and send message. - Used local files in 'data/' directory for session and message coordination. - Ensured concurrency safety using flock() for all file access. - Implemented SSE stream with keep-alive and immediate flushing. - Added input sanitization to prevent path traversal attacks. - Included automatic cleanup of expired session files. Co-authored-by: dimitrovs <200772+dimitrovs@users.noreply.github.com> --- index.php | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 index.php diff --git a/index.php b/index.php new file mode 100644 index 0000000..3b5b02f --- /dev/null +++ b/index.php @@ -0,0 +1,246 @@ + $hToken]); + saveSafe("$dataDir/token_$hToken.json", ['type' => 'host', 'hostId' => $hId]); + echo json_encode(['hostId' => $hId, 'hostToken' => $hToken]); + exit; +} + +// Handle Action: join +if ($action === 'join' && $_SERVER['REQUEST_METHOD'] === 'POST') { + header('Content-Type: application/json'); + if (!$hostId) { + http_response_code(400); + echo json_encode(['error' => 'hostId is required']); + exit; + } + $hostData = getSafe("$dataDir/host_$hostId.json"); + if (!$hostData) { + http_response_code(404); + echo json_encode(['error' => 'Host not found']); + exit; + } + $clientToken = bin2hex(random_bytes(16)); + saveSafe("$dataDir/token_$clientToken.json", [ + 'type' => 'client', + 'hostToken' => $hostData['hostToken'], + 'hostId' => $hostId + ]); + echo json_encode(['clientToken' => $clientToken]); + exit; +} + +// Handle Action: ping +if ($action === 'ping') { + echo "OK"; + exit; +} + +// Handle Token-based requests (SSE and Send Message) +if ($token) { + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // SSE Connection + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('X-Accel-Buffering: no'); + + echo ": Connected.\n\n"; + if (ob_get_level() > 0) ob_flush(); + flush(); + + $lastKeepAlive = time(); + while (true) { + if (connection_aborted()) break; + + $messages = popMessages($token); + if (!empty($messages)) { + foreach ($messages as $msg) { + echo "data: " . json_encode($msg) . "\n\n"; + } + if (ob_get_level() > 0) ob_flush(); + flush(); + } + + $now = time(); + if ($now - $lastKeepAlive >= 5) { + echo ": sse-keep-alive\n\n"; + if (ob_get_level() > 0) ob_flush(); + flush(); + $lastKeepAlive = $now; + + // Keep token file alive + @touch("$dataDir/token_$token.json"); + + // Also keep related host file alive if applicable + $tokenData = getSafe("$dataDir/token_$token.json"); + if ($tokenData && $tokenData['type'] === 'host') { + @touch("$dataDir/host_" . $tokenData['hostId'] . ".json"); + } + } + + usleep(20000); // 20ms for low latency + } + exit; + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Send Message + header('Content-Type: application/json'); + $input = json_decode(file_get_contents('php://input'), true); + $tokenData = getSafe("$dataDir/token_$token.json"); + + if ($tokenData) { + if ($tokenData['type'] === 'client') { + // From Client to Host + $hostToken = $tokenData['hostToken']; + // Use a stable response token for this client + $responseToken = 'res_' . $token; + if (!file_exists("$dataDir/token_$responseToken.json")) { + saveSafe("$dataDir/token_$responseToken.json", [ + 'type' => 'response', + 'clientToken' => $token + ]); + } else { + @touch("$dataDir/token_$responseToken.json"); + } + + appendMessage($hostToken, [ + 'body' => $input, + 'responseToken' => $responseToken + ]); + } elseif ($tokenData['type'] === 'response') { + // From Host to Client + $clientToken = $tokenData['clientToken']; + appendMessage($clientToken, [ + 'body' => $input + ]); + } + } + echo json_encode(['status' => 'sent']); + exit; + } +} + +http_response_code(400); +echo "Invalid request"; From 710443a6fd1ca00f4ff917f3e1071891b7022ee4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:59:58 +0000 Subject: [PATCH 2/2] Implement MySQL-Based UHST Relay Server in PHP - Switched from file-based coordination to MySQL database for state and queuing. - Implemented stateless JWT-based token system (HMAC-SHA256). - Used PDO with prepared statements for secure database access. - Added support for SQLite as a fallback for testing/low-traffic environments. - Implemented SSE with 5-second heartbeats and 20ms polling for low latency. - Added automatic schema initialization and cleanup of stale records. - Included `schema.sql` for manual database setup. Co-authored-by: dimitrovs <200772+dimitrovs@users.noreply.github.com> --- index.php | 371 +++++++++++++++++++++++++++++------------------------ schema.sql | 15 +++ 2 files changed, 221 insertions(+), 165 deletions(-) create mode 100644 schema.sql diff --git a/index.php b/index.php index 3b5b02f..c944b08 100644 --- a/index.php +++ b/index.php @@ -1,167 +1,203 @@ PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); +} catch (PDOException $e) { + http_response_code(500); + exit("Database connection failed: " . $e->getMessage()); +} + +// --- Database Initialization --- +if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite') { + $pdo->exec("CREATE TABLE IF NOT EXISTS uhst_hosts ( + host_id VARCHAR(255) PRIMARY KEY, + last_seen DATETIME DEFAULT CURRENT_TIMESTAMP + )"); -// Sanitize inputs to prevent path traversal -if ($token && !preg_match('/^[a-zA-Z0-9_\-]+$/', $token)) { - http_response_code(400); - exit("Invalid token format"); + $pdo->exec("CREATE TABLE IF NOT EXISTS uhst_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipient_token_hash VARCHAR(64), + body TEXT, + response_token TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"); + $pdo->exec("CREATE INDEX IF NOT EXISTS idx_recipient ON uhst_messages (recipient_token_hash)"); +} else { + // MySQL + $pdo->exec("CREATE TABLE IF NOT EXISTS uhst_hosts ( + host_id VARCHAR(255) PRIMARY KEY, + last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + )"); + + $pdo->exec("CREATE TABLE IF NOT EXISTS uhst_messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + recipient_token_hash VARCHAR(64) NOT NULL, + body TEXT NOT NULL, + response_token TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_recipient (recipient_token_hash) + )"); } -if ($hostId && !preg_match('/^[a-zA-Z0-9_\-]+$/', $hostId)) { - http_response_code(400); - exit("Invalid hostId format"); + +// --- JWT Utilities --- +function base64UrlEncode($data) { + return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); } -// Cleanup old files periodically (1% chance per request) -if (rand(1, 100) === 1) { - $now = time(); - $files = glob("$dataDir/*"); - if ($files) { - foreach ($files as $file) { - if (filemtime($file) < $now - 60) { - @unlink($file); - } - } +function base64UrlDecode($data) { + $remainder = strlen($data) % 4; + if ($remainder) { + $data .= str_repeat('=', 4 - $remainder); } + return base64_decode(str_replace(['-', '_'], ['+', '/'], $data)); } -/** - * Reads a JSON file with a shared lock. - */ -function getSafe($path) { - if (!file_exists($path)) return null; - @touch($path); - $fp = fopen($path, 'rb'); - if (!$fp) return null; - flock($fp, LOCK_SH); - $content = stream_get_contents($fp); - flock($fp, LOCK_UN); - fclose($fp); - return json_decode($content, true); +function signToken($payload) { + global $jwtSecret; + $header = base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); + $payload = base64UrlEncode(json_encode($payload)); + $signature = base64UrlEncode(hash_hmac('sha256', "$header.$payload", $jwtSecret, true)); + return "$header.$payload.$signature"; } -/** - * Writes data to a JSON file with an exclusive lock. - */ -function saveSafe($path, $data) { - $fp = fopen($path, 'cb'); - if (!$fp) return false; - flock($fp, LOCK_EX); - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, json_encode($data)); - fflush($fp); - flock($fp, LOCK_UN); - fclose($fp); - return true; +function verifyToken($token) { + global $jwtSecret; + $parts = explode('.', $token); + if (count($parts) !== 3) return null; + list($header, $payload, $signature) = $parts; + $expectedSignature = base64UrlEncode(hash_hmac('sha256', "$header.$payload", $jwtSecret, true)); + if ($signature !== $expectedSignature) return null; + return json_decode(base64UrlDecode($payload), true); } -/** - * Appends a message to a token's queue. - */ -function appendMessage($token, $message) { - global $dataDir; - $path = "$dataDir/queue_$token.json"; - $fp = fopen($path, 'cb+'); - if (!$fp) return false; - flock($fp, LOCK_EX); - $content = stream_get_contents($fp); - $queue = json_decode($content, true) ?: []; - $queue[] = $message; - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, json_encode($queue)); - fflush($fp); - flock($fp, LOCK_UN); - fclose($fp); - return true; +function getTokenHash($token) { + return hash('sha256', $token); } -/** - * Pops all messages from a token's queue. - */ -function popMessages($token) { - global $dataDir; - $path = "$dataDir/queue_$token.json"; - if (!file_exists($path)) return []; - $fp = fopen($path, 'cb+'); - if (!$fp) return []; - flock($fp, LOCK_EX); - $content = stream_get_contents($fp); - $queue = json_decode($content, true) ?: []; - if (!empty($queue)) { - ftruncate($fp, 0); - rewind($fp); - fwrite($fp, json_encode([])); - fflush($fp); +// --- Helper Functions --- +function getDbTimestamp($pdo) { + if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') { + return $pdo->query("SELECT UNIX_TIMESTAMP()")->fetchColumn(); + } else { + return $pdo->query("SELECT strftime('%s', 'now')")->fetchColumn(); } - flock($fp, LOCK_UN); - fclose($fp); - return $queue; } -// Handle Action: host +function isHostConnected($hostId) { + global $pdo; + $stmt = $pdo->prepare("SELECT last_seen FROM uhst_hosts WHERE host_id = ?"); + $stmt->execute([$hostId]); + $lastSeen = $stmt->fetchColumn(); + if (!$lastSeen) return false; + + $now = getDbTimestamp($pdo); + $lastSeenTs = strtotime($lastSeen . ' UTC'); + return ($now - $lastSeenTs) < 15; +} + +function updateHostHeartbeat($hostId) { + global $pdo; + if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') { + $stmt = $pdo->prepare("INSERT INTO uhst_hosts (host_id, last_seen) VALUES (?, NOW()) + ON DUPLICATE KEY UPDATE last_seen = NOW()"); + } else { + $stmt = $pdo->prepare("INSERT INTO uhst_hosts (host_id, last_seen) VALUES (?, datetime('now')) + ON CONFLICT(host_id) DO UPDATE SET last_seen = datetime('now')"); + } + $stmt->execute([$hostId]); +} + +// --- Request Handling --- +$action = $_GET['action'] ?? null; +$token = $_GET['token'] ?? null; + +// Periodically clean up old data +if (rand(1, 100) === 1) { + if ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'mysql') { + $pdo->exec("DELETE FROM uhst_messages WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 MINUTE)"); + $pdo->exec("DELETE FROM uhst_hosts WHERE last_seen < DATE_SUB(NOW(), INTERVAL 1 MINUTE)"); + } else { + $pdo->exec("DELETE FROM uhst_messages WHERE created_at < datetime('now', '-1 minute')"); + $pdo->exec("DELETE FROM uhst_hosts WHERE last_seen < datetime('now', '-1 minute')"); + } +} + +// Action: host if ($action === 'host' && $_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json'); - $hId = bin2hex(random_bytes(8)); - $hToken = bin2hex(random_bytes(16)); - saveSafe("$dataDir/host_$hId.json", ['hostToken' => $hToken]); - saveSafe("$dataDir/token_$hToken.json", ['type' => 'host', 'hostId' => $hId]); - echo json_encode(['hostId' => $hId, 'hostToken' => $hToken]); + $hostId = $_GET['hostId'] ?? bin2hex(random_bytes(8)); + if (isHostConnected($hostId)) { + http_response_code(400); + exit; + } + // We must register the host so it can be 'joined' + updateHostHeartbeat($hostId); + + $hostToken = signToken(['type' => 'HOST', 'hostId' => $hostId]); + echo json_encode(['hostId' => $hostId, 'hostToken' => $hostToken]); exit; } -// Handle Action: join +// Action: join if ($action === 'join' && $_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json'); - if (!$hostId) { + $hostId = $_GET['hostId'] ?? ''; + if (!isHostConnected($hostId)) { http_response_code(400); - echo json_encode(['error' => 'hostId is required']); - exit; - } - $hostData = getSafe("$dataDir/host_$hostId.json"); - if (!$hostData) { - http_response_code(404); - echo json_encode(['error' => 'Host not found']); + echo json_encode(['error' => 'Host not found or not connected']); exit; } - $clientToken = bin2hex(random_bytes(16)); - saveSafe("$dataDir/token_$clientToken.json", [ - 'type' => 'client', - 'hostToken' => $hostData['hostToken'], - 'hostId' => $hostId - ]); + $clientId = bin2hex(random_bytes(16)); + $clientToken = signToken(['type' => 'CLIENT', 'hostId' => $hostId, 'clientId' => $clientId]); echo json_encode(['clientToken' => $clientToken]); exit; } -// Handle Action: ping +// Action: ping if ($action === 'ping') { - echo "OK"; + header('Content-Type: application/json'); + $timestamp = $_GET['timestamp'] ?? null; + echo json_encode(['pong' => $timestamp ? (int)$timestamp : null]); exit; } -// Handle Token-based requests (SSE and Send Message) +// Token-based requests if ($token) { + $decoded = verifyToken($token); + if (!$decoded) { + http_response_code(401); + exit; + } + if ($_SERVER['REQUEST_METHOD'] === 'GET') { - // SSE Connection + // SSE Listen header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); @@ -171,34 +207,45 @@ function popMessages($token) { if (ob_get_level() > 0) ob_flush(); flush(); - $lastKeepAlive = time(); + $tokenHash = getTokenHash($token); + $lastHeartbeat = 0; + while (true) { if (connection_aborted()) break; - $messages = popMessages($token); - if (!empty($messages)) { - foreach ($messages as $msg) { - echo "data: " . json_encode($msg) . "\n\n"; - } + // Heartbeat + if (time() - $lastHeartbeat >= 5) { + echo ": sse-keep-alive\n\n"; if (ob_get_level() > 0) ob_flush(); flush(); + $lastHeartbeat = time(); + + if ($decoded['type'] === 'HOST') { + updateHostHeartbeat($decoded['hostId']); + } } - $now = time(); - if ($now - $lastKeepAlive >= 5) { - echo ": sse-keep-alive\n\n"; + // Fetch messages + $stmt = $pdo->prepare("SELECT id, body, response_token FROM uhst_messages WHERE recipient_token_hash = ? ORDER BY id ASC"); + $stmt->execute([$tokenHash]); + $messages = $stmt->fetchAll(); + + if ($messages) { + $ids = []; + foreach ($messages as $msg) { + $payload = ['body' => json_decode($msg['body'])]; + if ($msg['response_token']) { + $payload['responseToken'] = $msg['response_token']; + } + echo "data: " . json_encode($payload) . "\n\n"; + $ids[] = $msg['id']; + } if (ob_get_level() > 0) ob_flush(); flush(); - $lastKeepAlive = $now; - - // Keep token file alive - @touch("$dataDir/token_$token.json"); - // Also keep related host file alive if applicable - $tokenData = getSafe("$dataDir/token_$token.json"); - if ($tokenData && $tokenData['type'] === 'host') { - @touch("$dataDir/host_" . $tokenData['hostId'] . ".json"); - } + // Delete delivered messages + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $pdo->prepare("DELETE FROM uhst_messages WHERE id IN ($placeholders)")->execute($ids); } usleep(20000); // 20ms for low latency @@ -207,37 +254,31 @@ function popMessages($token) { } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { // Send Message header('Content-Type: application/json'); - $input = json_decode(file_get_contents('php://input'), true); - $tokenData = getSafe("$dataDir/token_$token.json"); - - if ($tokenData) { - if ($tokenData['type'] === 'client') { - // From Client to Host - $hostToken = $tokenData['hostToken']; - // Use a stable response token for this client - $responseToken = 'res_' . $token; - if (!file_exists("$dataDir/token_$responseToken.json")) { - saveSafe("$dataDir/token_$responseToken.json", [ - 'type' => 'response', - 'clientToken' => $token - ]); - } else { - @touch("$dataDir/token_$responseToken.json"); - } + $body = file_get_contents('php://input'); - appendMessage($hostToken, [ - 'body' => $input, - 'responseToken' => $responseToken - ]); - } elseif ($tokenData['type'] === 'response') { - // From Host to Client - $clientToken = $tokenData['clientToken']; - appendMessage($clientToken, [ - 'body' => $input - ]); - } + if ($decoded['type'] === 'CLIENT') { + // Client to Host + $hostId = $decoded['hostId']; + $clientId = $decoded['clientId']; + $hostToken = signToken(['type' => 'HOST', 'hostId' => $hostId]); + $hostTokenHash = getTokenHash($hostToken); + + $responseToken = signToken(['type' => 'RESPONSE', 'hostId' => $hostId, 'clientId' => $clientId]); + + $stmt = $pdo->prepare("INSERT INTO uhst_messages (recipient_token_hash, body, response_token) VALUES (?, ?, ?)"); + $stmt->execute([$hostTokenHash, $body, $responseToken]); + } elseif ($decoded['type'] === 'RESPONSE') { + // Host to Client (targeted) + $hostId = $decoded['hostId']; + $clientId = $decoded['clientId']; + $clientToken = signToken(['type' => 'CLIENT', 'hostId' => $hostId, 'clientId' => $clientId]); + $clientTokenHash = getTokenHash($clientToken); + + $stmt = $pdo->prepare("INSERT INTO uhst_messages (recipient_token_hash, body) VALUES (?, ?)"); + $stmt->execute([$clientTokenHash, $body]); } - echo json_encode(['status' => 'sent']); + + echo json_encode((object)[]); exit; } } diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..97dc6ea --- /dev/null +++ b/schema.sql @@ -0,0 +1,15 @@ +-- UHST Relay Server MySQL Schema + +CREATE TABLE IF NOT EXISTS uhst_hosts ( + host_id VARCHAR(255) PRIMARY KEY, + last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS uhst_messages ( + id INT AUTO_INCREMENT PRIMARY KEY, + recipient_token_hash VARCHAR(64) NOT NULL, + body TEXT NOT NULL, + response_token TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_recipient (recipient_token_hash) +);