Update car monitor asset reload and telemetry

This commit is contained in:
seo committed 2026-08-12 21:35:21 +09:00
1 parent 6f965f8f3e
commit 5d75ad81bd
6 files changed
+126 -158

No files matched your search

+2 -3
View File
@@ -59,12 +59,11 @@ $assets = [];
foreach (collect_asset_files($root, $scanRoots) as $fullPath) { foreach (collect_asset_files($root, $scanRoots) as $fullPath) {
$relativePath = ltrim(str_replace($rootPath ?: $root, '', $fullPath), DIRECTORY_SEPARATOR); $relativePath = ltrim(str_replace($rootPath ?: $root, '', $fullPath), DIRECTORY_SEPARATOR);
$fileHash = is_readable($fullPath) ? (hash_file('sha256', $fullPath) ?: 'hash-failed') : 'unreadable'; $fileHash = is_readable($fullPath) ? (hash_file('sha256', $fullPath) ?: 'hash-failed') : 'unreadable';
$fileVersion = (string)filemtime($fullPath) . '-' . substr($fileHash, 0, 10); $fileVersion = substr($fileHash, 0, 16);
$publicPath = '/car/' . str_replace(DIRECTORY_SEPARATOR, '/', $relativePath); $publicPath = '/custom/car/' . str_replace(DIRECTORY_SEPARATOR, '/', $relativePath);
$fingerprints[] = implode('|', [ $fingerprints[] = implode('|', [
ltrim($publicPath, '/'), ltrim($publicPath, '/'),
(string)filesize($fullPath), (string)filesize($fullPath),
(string)filemtime($fullPath),
$fileHash, $fileHash,
]); ]);
$assets[$publicPath] = $fileVersion; $assets[$publicPath] = $fileVersion;
+9 -6
View File
@@ -214,20 +214,23 @@
}).then(function (data) { }).then(function (data) {
if (!data || data.result !== 'ok' || !data.version) return; if (!data || data.result !== 'ok' || !data.version) return;
var token = data.version.slice(0, 12);
var previous = storeGet(storageKey); var previous = storeGet(storageKey);
var mismatch = loadedAssetMismatch(data.assets); var versionChanged = previous && previous !== data.version;
var forceReload = bfcacheReturn; var latestToken = String(data.version).slice(0, 12);
var hasLatestToken = assetTokenFromLocation() === latestToken;
var mismatch = !hasLatestToken && loadedAssetMismatch(data.assets);
var wasBfcacheReturn = bfcacheReturn;
bfcacheReturn = false; bfcacheReturn = false;
storeSet(storageKey, data.version); storeSet(storageKey, data.version);
if (assetTokenFromLocation() !== token || mismatch || forceReload) { if ((mismatch || wasBfcacheReturn) && !hasLatestToken) {
reloadWith(data.version, mismatch || forceReload); reloadWith(data.version, true);
return; return;
} }
if (previous && previous !== data.version) { if (versionChanged) {
reloadWith(data.version, false); reloadWith(data.version, false);
return;
} }
}).then(function () { }).then(function () {
running = false; running = false;
+4 -4
View File
@@ -2,21 +2,21 @@
"name": "Car Monitor", "name": "Car Monitor",
"short_name": "Car Monitor", "short_name": "Car Monitor",
"description": "Vehicle status monitor", "description": "Vehicle status monitor",
"start_url": "/car/monitor.php?pwa=1", "start_url": "/custom/car/monitor.php?pwa=1",
"scope": "/car/", "scope": "/custom/car/",
"display": "standalone", "display": "standalone",
"orientation": "portrait", "orientation": "portrait",
"background_color": "#0f172a", "background_color": "#0f172a",
"theme_color": "#0f172a", "theme_color": "#0f172a",
"icons": [ "icons": [
{ {
"src": "/car/assets/icon-192.png", "src": "/custom/car/assets/icon-192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
}, },
{ {
"src": "/car/assets/icon-512.png", "src": "/custom/car/assets/icon-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
+73 -3
View File
@@ -3,7 +3,7 @@ declare(strict_types=1);
date_default_timezone_set('Asia/Seoul'); date_default_timezone_set('Asia/Seoul');
$carSecretFile = '/home/seo/secret/car.php'; $carSecretFile = __DIR__ . '/../../config/car.php';
if (!is_file($carSecretFile)) { if (!is_file($carSecretFile)) {
throw new RuntimeException('Missing car secret config: ' . $carSecretFile); throw new RuntimeException('Missing car secret config: ' . $carSecretFile);
} }
@@ -40,6 +40,7 @@ define('ALLOWED_IPS', array_values(array_filter(
const ALLOWED_CMD = ['se', 'ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu']; const ALLOWED_CMD = ['se', 'ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu'];
const CONTROL_CMD = ['ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu']; const CONTROL_CMD = ['ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu'];
const CARRIER_BILLING_UNIT_BYTES = 512; const CARRIER_BILLING_UNIT_BYTES = 512;
const TCP_STALE_QUEUE_BATCH_LIMIT = 200;
const RAW_FULL_REGEX = '/(?<modem>\d{11})\/R:(?<r>[a-z])\/E:(?<E0>[io]{5}\d{3}[io])\/D:(?<D0>[oi]{7})\/L:(?<L0>o{5})\/F:(?<F0>[ots][oi]\d{4}[oi]{4})\/S:(?<S0>[^\/]+)/'; const RAW_FULL_REGEX = '/(?<modem>\d{11})\/R:(?<r>[a-z])\/E:(?<E0>[io]{5}\d{3}[io])\/D:(?<D0>[oi]{7})\/L:(?<L0>o{5})\/F:(?<F0>[ots][oi]\d{4}[oi]{4})\/S:(?<S0>[^\/]+)/';
@@ -271,7 +272,7 @@ function tcp_queue_update(PDO $pdo, string $requestId, array $values): void
function tcp_queue_insert(string $cmd, string $usageSource, string $requestId): array function tcp_queue_insert(string $cmd, string $usageSource, string $requestId): array
{ {
$pdo = db(); $pdo = db(true);
db_ensure_tcp_queue_schema($pdo); db_ensure_tcp_queue_schema($pdo);
$now = microtime(true); $now = microtime(true);
@@ -305,6 +306,25 @@ function tcp_queue_insert(string $cmd, string $usageSource, string $requestId):
]; ];
} }
function tcp_queue_has_pending_collector_se(PDO $pdo): bool
{
db_ensure_tcp_queue_schema($pdo);
tcp_queue_expire_queued_before_send($pdo);
$stmt = $pdo->prepare("
SELECT 1
FROM car_tcp_request_queue
WHERE source = 'collector_se'
AND cmd = 'se'
AND status IN ('queued', 'processing', 'ui_timeout_pending')
AND usage_deadline_ts > :now
LIMIT 1
");
$stmt->execute([':now' => microtime(true)]);
return (bool)$stmt->fetchColumn();
}
function tcp_queue_get(string $requestId): ?array function tcp_queue_get(string $requestId): ?array
{ {
$pdo = db(); $pdo = db();
@@ -323,7 +343,16 @@ function tcp_queue_claim_next(PDO $pdo): ?array
$pdo->beginTransaction(); $pdo->beginTransaction();
try { try {
$stmt = $pdo->query("SELECT * FROM car_tcp_request_queue WHERE status = 'queued' ORDER BY id ASC LIMIT 1 FOR UPDATE"); $stmt = $pdo->query("
SELECT *
FROM car_tcp_request_queue
WHERE status = 'queued'
ORDER BY
CASE WHEN cmd IN ('ef', 'en', 'hn', 'hf', 'dl', 'du', 'tu') THEN 0 ELSE 1 END,
id ASC
LIMIT 1
FOR UPDATE
");
$row = $stmt->fetch(); $row = $stmt->fetch();
if (!$row) { if (!$row) {
$pdo->commit(); $pdo->commit();
@@ -356,8 +385,36 @@ function tcp_queue_has_usage(PDO $pdo, string $requestId): bool
return (bool)$stmt->fetchColumn(); return (bool)$stmt->fetchColumn();
} }
function tcp_queue_expire_queued_before_send(PDO $pdo): void
{
$stmt = $pdo->prepare("
UPDATE car_tcp_request_queue
SET status = 'expired_before_send',
updated_at = :updated_at,
tcp_ok = 0,
tcp_error = 'queue_expired_before_send',
sent_bytes = 0,
received_bytes = 0,
total_bytes = 0,
connect_ms = 0,
read_ms = 0,
tcp_ms = 0
WHERE status = 'queued'
AND ui_deadline_ts > 0
AND ui_deadline_ts < :now
ORDER BY id ASC
LIMIT " . TCP_STALE_QUEUE_BATCH_LIMIT
);
$stmt->execute([
':updated_at' => tcp_queue_now(),
':now' => microtime(true),
]);
}
function tcp_queue_recover_stale(PDO $pdo): void function tcp_queue_recover_stale(PDO $pdo): void
{ {
tcp_queue_expire_queued_before_send($pdo);
$now = microtime(true); $now = microtime(true);
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT * SELECT *
@@ -684,6 +741,19 @@ function tcp_request(
$requestId = substr(bin2hex(random_bytes(16)), 0, 32); $requestId = substr(bin2hex(random_bytes(16)), 0, 32);
} }
if ($cmd === 'se' && $usageSource === 'collector_se') {
try {
if (tcp_queue_has_pending_collector_se(db(true))) {
$tcpError = 'skipped_duplicate_pending_collector_se';
return '';
}
} catch (Throwable $e) {
$tcpError = 'queue_pending_check_failed';
record_tcp_usage($usageSource, $cmd, 0, 0, false, $tcpError, $requestId);
return '';
}
}
try { try {
tcp_queue_insert($cmd, $usageSource, $requestId); tcp_queue_insert($cmd, $usageSource, $requestId);
} catch (Throwable $e) { } catch (Throwable $e) {
+36 -140
View File
@@ -3,7 +3,9 @@ declare(strict_types=1);
date_default_timezone_set('Asia/Seoul'); date_default_timezone_set('Asia/Seoul');
$carSecretFile = '/home/seo/secret/car.php'; require_once __DIR__ . '/../common/auth.php';
$carSecretFile = __DIR__ . '/../../config/car.php';
if (!is_file($carSecretFile)) { if (!is_file($carSecretFile)) {
throw new RuntimeException('Missing car secret config: ' . $carSecretFile); throw new RuntimeException('Missing car secret config: ' . $carSecretFile);
} }
@@ -19,8 +21,6 @@ define('DB_CHARSET', (string)($carDbConfig['charset'] ?? 'utf8mb4'));
const LOG_LIMIT = 300; const LOG_LIMIT = 300;
const CHART_LIMIT = 500; const CHART_LIMIT = 500;
const CAR_MONITOR_COOKIE = 'car_monitor_access';
const CAR_MONITOR_AUTH_TTL = 2592000;
const CAR_MONITOR_SETTINGS_COOKIE = 'car_monitor_settings_access'; const CAR_MONITOR_SETTINGS_COOKIE = 'car_monitor_settings_access';
const CAR_MONITOR_SETTINGS_TTL = 900; const CAR_MONITOR_SETTINGS_TTL = 900;
const CAR_MONITOR_SETTINGS_PASSWORD = '1026!)Corjs'; const CAR_MONITOR_SETTINGS_PASSWORD = '1026!)Corjs';
@@ -40,14 +40,13 @@ const DATA_CURRENT_BILLING_USAGE_BYTES = [
function car_asset_url(string $path): string function car_asset_url(string $path): string
{ {
$relativePath = preg_replace('#^/car/#', '', $path) ?? ltrim($path, '/'); $relativePath = preg_replace('#^/custom/car/#', '', $path) ?? ltrim($path, '/');
$fullPath = __DIR__ . '/' . ltrim($relativePath, '/'); $fullPath = __DIR__ . '/' . ltrim($relativePath, '/');
$version = 'dev'; $version = 'dev';
if (is_file($fullPath)) { if (is_file($fullPath)) {
$mtime = filemtime($fullPath) ?: time(); $hash = is_readable($fullPath) ? substr(hash_file('sha256', $fullPath) ?: '', 0, 16) : '';
$hash = is_readable($fullPath) ? substr(hash_file('sha256', $fullPath) ?: '', 0, 10) : ''; $version = $hash !== '' ? $hash : 'unreadable';
$version = $mtime . ($hash !== '' ? '-' . $hash : '');
} }
return $path . '?v=' . rawurlencode($version); return $path . '?v=' . rawurlencode($version);
@@ -879,62 +878,13 @@ function car_monitor_cookie_options(int $expires): array
{ {
return [ return [
'expires' => $expires, 'expires' => $expires,
'path' => '/car/', 'path' => '/custom/car/',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => true, 'httponly' => true,
'samesite' => 'Strict', 'samesite' => 'Strict',
]; ];
} }
function car_monitor_cookie_value(int $expires, bool $admin = false): string
{
$body = base64_encode(json_encode([
'exp' => $expires,
'iat' => time(),
'admin' => $admin,
], JSON_UNESCAPED_SLASHES));
$sig = hash_hmac('sha256', $body, car_monitor_cookie_secret());
return $body . '.' . $sig;
}
function car_monitor_issue_cookie(bool $admin = false): void
{
$expires = time() + CAR_MONITOR_AUTH_TTL;
setcookie(CAR_MONITOR_COOKIE, car_monitor_cookie_value($expires, $admin), car_monitor_cookie_options($expires));
}
function car_monitor_cookie_payload(): ?array
{
$cookie = $_COOKIE[CAR_MONITOR_COOKIE] ?? '';
if (!is_string($cookie) || strpos($cookie, '.') === false) {
return null;
}
[$body, $sig] = explode('.', $cookie, 2);
$expected = hash_hmac('sha256', $body, car_monitor_cookie_secret());
if (!hash_equals($expected, $sig)) {
return null;
}
$decoded = base64_decode($body, true);
if ($decoded === false) {
return null;
}
$payload = json_decode($decoded, true);
if (!is_array($payload)) {
return null;
}
return (int)($payload['exp'] ?? 0) >= time() ? $payload : null;
}
function car_monitor_has_cookie(): bool
{
return is_array(car_monitor_cookie_payload());
}
function car_monitor_settings_cookie_value(int $expires): string function car_monitor_settings_cookie_value(int $expires): string
{ {
$body = base64_encode(json_encode([ $body = base64_encode(json_encode([
@@ -993,72 +943,22 @@ function car_monitor_launcher_session(): array
return $session; return $session;
} }
$cookieHeader = trim((string)($_SERVER['HTTP_COOKIE'] ?? '')); if (custom_is_site_admin()) {
if ($cookieHeader !== '') { custom_issue_auth_cookie(true);
$remoteSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?session=1', null, $cookieHeader); $session = ['authenticated' => true, 'admin' => true];
if (is_array($remoteSession) && !empty($remoteSession['authenticated'])) {
car_monitor_issue_cookie();
$session = ['authenticated' => true];
return $session;
}
}
$cookiePayload = car_monitor_cookie_payload();
if (is_array($cookiePayload)) {
$session = ['authenticated' => true];
return $session; return $session;
} }
$session = ['authenticated' => false]; if (custom_has_auth_cookie()) {
$session = [
'authenticated' => true,
'admin' => custom_auth_cookie_is_admin(),
];
return $session; return $session;
}
function car_monitor_launcher_api_request(string $method, string $url, ?array $payload = null, string $cookieHeader = ''): ?array
{
$body = false;
if (function_exists('curl_init')) {
$ch = curl_init($url);
if ($ch !== false) {
$headers = ['User-Agent: car-monitor-auth/1.0'];
if ($cookieHeader !== '') {
$headers[] = 'Cookie: ' . $cookieHeader;
}
if ($payload !== null) {
$headers[] = 'Content-Type: application/json';
} }
curl_setopt_array($ch, [ $session = ['authenticated' => false, 'admin' => false];
CURLOPT_RETURNTRANSFER => true, return $session;
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 4,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_HTTPHEADER => $headers,
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
}
$body = curl_exec($ch);
curl_close($ch);
}
}
if ($body === false) {
$context = stream_context_create([
'http' => [
'method' => strtoupper($method),
'timeout' => 4,
'header' => ($cookieHeader !== '' ? "Cookie: {$cookieHeader}\r\n" : '')
. "User-Agent: car-monitor-auth/1.0\r\n"
. ($payload !== null ? "Content-Type: application/json\r\n" : ''),
'content' => $payload !== null ? json_encode($payload, JSON_UNESCAPED_UNICODE) : '',
],
]);
$body = @file_get_contents($url, false, $context);
}
$data = is_string($body) ? json_decode($body, true) : null;
return is_array($data) ? $data : null;
} }
function car_monitor_has_access(): bool function car_monitor_has_access(): bool
@@ -1069,6 +969,10 @@ function car_monitor_has_access(): bool
function car_monitor_require_access(bool $json = false): void function car_monitor_require_access(bool $json = false): void
{ {
if (function_exists('custom_block_login_denied')) {
custom_block_login_denied($json);
}
if (car_monitor_has_access()) { if (car_monitor_has_access()) {
return; return;
} }
@@ -1101,7 +1005,7 @@ function car_monitor_require_access(bool $json = false): void
window.addEventListener('DOMContentLoaded', function () { window.addEventListener('DOMContentLoaded', function () {
window.CustomAuthPin && window.CustomAuthPin.render({ window.CustomAuthPin && window.CustomAuthPin.render({
mount: '#findMyShortcut', mount: '#findMyShortcut',
apiUrl: '/car/monitor.php?mode=auth-login', apiUrl: '/custom/car/monitor.php?mode=auth-login',
title: '비밀번호', title: '비밀번호',
placeholder: '비밀번호', placeholder: '비밀번호',
backLabel: '채건닷컴', backLabel: '채건닷컴',
@@ -1127,20 +1031,16 @@ function car_monitor_issue_pwa_cookie_if_requested(): void
return; return;
} }
$cookieHeader = trim((string)($_SERVER['HTTP_COOKIE'] ?? '')); header('Location: /custom/car/monitor.php', true, 302);
if ($cookieHeader !== '') {
$remoteSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?session=1', null, $cookieHeader);
if (is_array($remoteSession) && !empty($remoteSession['authenticated'])) {
car_monitor_issue_cookie();
}
}
header('Location: /car/monitor.php', true, 302);
exit; exit;
} }
function car_monitor_auth_api(): void function car_monitor_auth_api(): void
{ {
if (function_exists('custom_block_login_denied')) {
custom_block_login_denied(true);
}
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['failMessages'])) { if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['failMessages'])) {
json_response(['failMessages' => ['비밀번호가 틀립니다.']]); json_response(['failMessages' => ['비밀번호가 틀립니다.']]);
} }
@@ -1155,12 +1055,8 @@ function car_monitor_auth_api(): void
json_response(['result' => 'fail', 'message' => 'password_digits_only'], 401); json_response(['result' => 'fail', 'message' => 'password_digits_only'], 401);
} }
$remote = car_monitor_launcher_api_request('POST', 'https://chaegeon.com/custom/launcher/api.php', [ if (custom_verify_password($password)) {
'action' => 'login', custom_issue_auth_cookie(false);
'password' => $password,
]);
if (is_array($remote) && ($remote['result'] ?? '') === 'ok') {
car_monitor_issue_cookie();
json_response(['result' => 'ok']); json_response(['result' => 'ok']);
} }
@@ -1350,13 +1246,13 @@ $carMonitorSession = car_monitor_launcher_session();
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Car Monitor</title> <title>Car Monitor</title>
<link rel="icon" href="<?= htmlspecialchars(car_asset_url('/car/assets/favicon.svg'), ENT_QUOTES, 'UTF-8') ?>" type="image/svg+xml"> <link rel="icon" href="<?= htmlspecialchars(car_asset_url('/custom/car/assets/favicon.svg'), ENT_QUOTES, 'UTF-8') ?>" type="image/svg+xml">
<link rel="shortcut icon" href="<?= htmlspecialchars(car_asset_url('/car/favicon.ico'), ENT_QUOTES, 'UTF-8') ?>"> <link rel="shortcut icon" href="<?= htmlspecialchars(car_asset_url('/custom/car/favicon.ico'), ENT_QUOTES, 'UTF-8') ?>">
<link rel="icon" href="<?= htmlspecialchars(car_asset_url('/car/assets/icon-32.png'), ENT_QUOTES, 'UTF-8') ?>" type="image/png" sizes="32x32"> <link rel="icon" href="<?= htmlspecialchars(car_asset_url('/custom/car/assets/icon-32.png'), ENT_QUOTES, 'UTF-8') ?>" type="image/png" sizes="32x32">
<link rel="icon" href="<?= htmlspecialchars(car_asset_url('/car/assets/icon-192.png'), ENT_QUOTES, 'UTF-8') ?>" type="image/png" sizes="192x192"> <link rel="icon" href="<?= htmlspecialchars(car_asset_url('/custom/car/assets/icon-192.png'), ENT_QUOTES, 'UTF-8') ?>" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="<?= htmlspecialchars(car_asset_url('/car/assets/apple-touch-icon.png'), ENT_QUOTES, 'UTF-8') ?>"> <link rel="apple-touch-icon" href="<?= htmlspecialchars(car_asset_url('/custom/car/assets/apple-touch-icon.png'), ENT_QUOTES, 'UTF-8') ?>">
<link rel="manifest" href="<?= htmlspecialchars(car_asset_url('/car/assets/site.webmanifest'), ENT_QUOTES, 'UTF-8') ?>"> <link rel="manifest" href="<?= htmlspecialchars(car_asset_url('/custom/car/assets/site.webmanifest'), ENT_QUOTES, 'UTF-8') ?>">
<script src="<?= htmlspecialchars(car_asset_url('/car/assets/asset-reload.js'), ENT_QUOTES, 'UTF-8') ?>" data-service="car" defer></script> <script src="<?= htmlspecialchars(car_asset_url('/custom/car/assets/asset-reload.js'), ENT_QUOTES, 'UTF-8') ?>" data-service="car" defer></script>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Gowun+Dodum&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Gowun+Dodum&display=swap" rel="stylesheet">
+2 -2
View File
@@ -26,11 +26,11 @@ register_shutdown_function(static function () use ($lockFp): void {
@fclose($lockFp); @fclose($lockFp);
}); });
db_ensure_tcp_queue_schema(db()); db_ensure_tcp_queue_schema(db(true));
while (true) { while (true) {
try { try {
$job = tcp_queue_claim_next(db()); $job = tcp_queue_claim_next(db(true));
if (!$job) { if (!$job) {
usleep(WORKER_IDLE_SLEEP_USEC); usleep(WORKER_IDLE_SLEEP_USEC);
continue; continue;