diff --git a/monitor.php b/monitor.php
index 1df9a01..8fb41a5 100644
--- a/monitor.php
+++ b/monitor.php
@@ -17,6 +17,8 @@ define('DB_CHARSET', (string)($carDbConfig['charset'] ?? 'utf8mb4'));
const LOG_LIMIT = 300;
const CHART_LIMIT = 500;
+const CAR_MONITOR_COOKIE = 'car_monitor_access';
+const CAR_MONITOR_AUTH_TTL = 2592000;
const DATA_MONTHLY_INCLUDED_BYTES = 104857600; // 100MB
const DATA_BILLING_UNIT_BYTES = 512; // 0.5KB
const DATA_BILLING_UNIT_KRW = 0.011;
@@ -874,6 +876,72 @@ function json_response(array $payload, int $status = 200): void
exit;
}
+function car_monitor_cookie_secret(): string
+{
+ global $carSecretConfig;
+
+ $secret = (string)($carSecretConfig['monitor_auth']['secret'] ?? $carSecretConfig['auth_token'] ?? '');
+ if ($secret === '') {
+ throw new RuntimeException('Car monitor auth secret is missing.');
+ }
+
+ return $secret;
+}
+
+function car_monitor_cookie_options(int $expires): array
+{
+ return [
+ 'expires' => $expires,
+ 'path' => '/car/',
+ 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
+ 'httponly' => true,
+ 'samesite' => 'Strict',
+ ];
+}
+
+function car_monitor_cookie_value(int $expires): string
+{
+ $body = base64_encode(json_encode([
+ 'exp' => $expires,
+ 'iat' => time(),
+ ], JSON_UNESCAPED_SLASHES));
+ $sig = hash_hmac('sha256', $body, car_monitor_cookie_secret());
+
+ return $body . '.' . $sig;
+}
+
+function car_monitor_issue_cookie(): void
+{
+ $expires = time() + CAR_MONITOR_AUTH_TTL;
+ setcookie(CAR_MONITOR_COOKIE, car_monitor_cookie_value($expires), car_monitor_cookie_options($expires));
+}
+
+function car_monitor_has_cookie(): bool
+{
+ $cookie = $_COOKIE[CAR_MONITOR_COOKIE] ?? '';
+ if (!is_string($cookie) || strpos($cookie, '.') === false) {
+ return false;
+ }
+
+ [$body, $sig] = explode('.', $cookie, 2);
+ $expected = hash_hmac('sha256', $body, car_monitor_cookie_secret());
+ if (!hash_equals($expected, $sig)) {
+ return false;
+ }
+
+ $decoded = base64_decode($body, true);
+ if ($decoded === false) {
+ return false;
+ }
+
+ $payload = json_decode($decoded, true);
+ if (!is_array($payload)) {
+ return false;
+ }
+
+ return (int)($payload['exp'] ?? 0) >= time();
+}
+
function car_monitor_launcher_session(): array
{
static $session = null;
@@ -886,11 +954,19 @@ function car_monitor_launcher_session(): array
if ($cookieHeader !== '') {
$remoteSession = car_monitor_launcher_api_request('GET', 'https://chaegeon.com/custom/launcher/api.php?session=1', null, $cookieHeader);
if (is_array($remoteSession)) {
+ if (!empty($remoteSession['authenticated'])) {
+ car_monitor_issue_cookie();
+ }
$session = $remoteSession;
return $session;
}
}
+ if (car_monitor_has_cookie()) {
+ $session = ['authenticated' => true, 'admin' => false];
+ return $session;
+ }
+
$session = ['authenticated' => false, 'admin' => false];
return $session;
}
@@ -978,7 +1054,7 @@ function car_monitor_require_access(bool $json = false): void
-
+