mirror of
git://f0xx.org/ac/ac-platform-php
synced 2026-07-29 00:57:39 +03:00
Add HeartbeatApi with legacy GET support for old app clients.
Older builds probe heartbeat.php via GET (ntp/ip/ping); POST-only caused HTTP 405 on some devices. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
117
src/HeartbeatApi.php
Normal file
117
src/HeartbeatApi.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Device heartbeat API (ping / ntp / ip / ra).
|
||||
* Accepts POST JSON and legacy GET query params for older app builds.
|
||||
*/
|
||||
final class HeartbeatApi
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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<string, mixed> $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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user