diff --git a/public/api/heartbeat.php b/public/api/heartbeat.php index af2bcae..da6e910 100644 --- a/public/api/heartbeat.php +++ b/public/api/heartbeat.php @@ -1,65 +1,6 @@ 'method_not_allowed']); - exit; -} - -$raw = file_get_contents('php://input') ?: ''; -$req = json_decode($raw, true); -if (!is_array($req)) { - http_response_code(400); - echo json_encode(['error' => 'invalid_json']); - exit; -} - -$heartbeat = $req['heartbeat'] ?? []; -if (!is_array($heartbeat)) { - $heartbeat = []; -} - -$type = (string)($heartbeat['type'] ?? 'generic'); -$srvEpoch = time(); -$srvTz = date('O'); -$clientIp = (string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '')); - -$resp = [ - 'heartbeat' => [ - 'type' => $type, - 'server_date' => $srvEpoch, - 'server_tz' => $srvTz, - ], -]; - -if ($type === 'ntp') { - $clientDate = (int)($heartbeat['date'] ?? 0); - $clientTz = (string)($heartbeat['tz'] ?? ''); - $timeSource = trim((string)($heartbeat['time_source'] ?? 'device')); - if ($timeSource === '') { - $timeSource = 'device'; - } - $correction = $clientDate > 0 ? ($srvEpoch - $clientDate) : null; - $resp['heartbeat']['client_date'] = $clientDate; - $resp['heartbeat']['client_tz'] = $clientTz; - $resp['heartbeat']['time_source'] = $timeSource; - $resp['heartbeat']['correction_seconds'] = $correction; - $resp['heartbeat']['ntp_correction_s'] = $correction; - $resp['heartbeat']['enabled'] = true; -} - -if ($type === 'ip') { - $resp['heartbeat']['external_ip'] = $clientIp; -} - -if ($type === 'ra') { - require_once __DIR__ . '/../../src/RemoteAccessRepository.php'; - $ra = RemoteAccessRepository::handleDeviceHeartbeat($heartbeat, $clientIp); - $resp['heartbeat'] = array_merge($resp['heartbeat'], $ra); -} - -echo json_encode($resp, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +HeartbeatApi::emit(); diff --git a/src/HeartbeatApi.php b/src/HeartbeatApi.php new file mode 100644 index 0000000..d3cbe49 --- /dev/null +++ b/src/HeartbeatApi.php @@ -0,0 +1,117 @@ + */ + public static function parseRequest(): array + { + $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')); + if ($method === 'POST') { + $raw = file_get_contents('php://input') ?: ''; + $req = json_decode($raw, true); + if (!is_array($req)) { + return ['error' => 'invalid_json', 'code' => 400]; + } + $heartbeat = $req['heartbeat'] ?? []; + return ['heartbeat' => is_array($heartbeat) ? $heartbeat : [], 'code' => 0]; + } + if ($method === 'GET' || $method === 'HEAD') { + $type = (string) ($_GET['type'] ?? $_GET['heartbeat_type'] ?? 'ping'); + $heartbeat = ['type' => $type]; + if ($type === 'ntp') { + $heartbeat['date'] = (int) ($_GET['date'] ?? $_GET['client_date'] ?? 0); + $heartbeat['tz'] = (string) ($_GET['tz'] ?? $_GET['client_tz'] ?? ''); + $heartbeat['time_source'] = (string) ($_GET['time_source'] ?? 'device'); + } + return ['heartbeat' => $heartbeat, 'code' => 0]; + } + return ['error' => 'method_not_allowed', 'code' => 405]; + } + + /** @param array $heartbeat */ + public static function buildResponse(array $heartbeat, string $clientIp): array + { + $type = (string) ($heartbeat['type'] ?? 'generic'); + $srvEpoch = time(); + $srvTz = date('O'); + + $resp = [ + 'heartbeat' => [ + 'type' => $type, + 'server_date' => $srvEpoch, + 'server_tz' => $srvTz, + ], + ]; + + if ($type === 'ntp') { + $clientDate = (int) ($heartbeat['date'] ?? 0); + $clientTz = (string) ($heartbeat['tz'] ?? ''); + $timeSource = trim((string) ($heartbeat['time_source'] ?? 'device')); + if ($timeSource === '') { + $timeSource = 'device'; + } + $correction = $clientDate > 0 ? ($srvEpoch - $clientDate) : null; + $resp['heartbeat']['client_date'] = $clientDate; + $resp['heartbeat']['client_tz'] = $clientTz; + $resp['heartbeat']['time_source'] = $timeSource; + $resp['heartbeat']['correction_seconds'] = $correction; + $resp['heartbeat']['ntp_correction_s'] = $correction; + $resp['heartbeat']['enabled'] = true; + } + + if ($type === 'ip') { + $resp['heartbeat']['external_ip'] = $clientIp; + } + + if ($type === 'ra') { + if (!class_exists('RemoteAccessRepository', false)) { + return ['error' => 'ra_unavailable', 'code' => 503]; + } + $ra = RemoteAccessRepository::handleDeviceHeartbeat($heartbeat, $clientIp); + $resp['heartbeat'] = array_merge($resp['heartbeat'], $ra); + } + + return ['response' => $resp, 'code' => 200]; + } + + public static function emit(): void + { + header('Content-Type: application/json; charset=utf-8'); + + $parsed = self::parseRequest(); + if (($parsed['code'] ?? 0) === 405) { + http_response_code(405); + echo json_encode(['error' => 'method_not_allowed']); + return; + } + if (($parsed['code'] ?? 0) === 400) { + http_response_code(400); + echo json_encode(['error' => 'invalid_json']); + return; + } + + $clientIp = (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '')); + if (str_contains($clientIp, ',')) { + $clientIp = trim(explode(',', $clientIp)[0]); + } + + $built = self::buildResponse((array) ($parsed['heartbeat'] ?? []), $clientIp); + if (($built['code'] ?? 200) === 503) { + http_response_code(503); + echo json_encode(['error' => 'ra_unavailable']); + return; + } + + if (class_exists('Auth', false) && Auth::user()) { + Auth::touchSession(); + } + + http_response_code(200); + echo json_encode($built['response'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } +}