diff --git a/README.md b/README.md index c6b6ae5..1783022 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `public/api.php?action=fan`: 팬 모드와 PWM 저장/적용 - `public/api.php?action=wifi`: 허용된 WiFi/DHCP service 제어 - `public/api.php?action=wifi_alias`: MAC별 WiFi 호스트명 저장 +- `public/api.php?action=wake_lan_status`: LAN 클라이언트 WOL 대상 상태 조회 +- `public/api.php?action=wake_lan`: LAN 클라이언트에 WOL magic packet 전송 - `public/api.php?action=settings`: 설정 가능한 Control 동작값 조회 - `public/api.php?action=settings_save`: 설정 모달 값 저장 - `public/api.php?action=settings_reset`: 설정 모달 값을 기본값으로 초기화 @@ -74,7 +76,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `wifi_observed_sessions`: 5G WiFi/LAN client의 최초/마지막 감지 시간 - `wifi_client_aliases`: MAC별 수동 WiFi 호스트명 - `app_settings`: 설정 모달에서 저장한 Control 동작값 오버라이드 -- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정 +- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정, WOL 전용 API 토큰 - UPS/배터리 기능은 `/home/seo/secret/control.php`의 `battery.enabled`가 명시적으로 true일 때만 동작합니다. 현재 운영 기준은 UPS 제거 상태이므로 기본값은 false입니다. ## UPS/배터리 상태 @@ -216,6 +218,7 @@ control-wifi-observe.service ## 보안 - 로그인 세션과 CSRF token을 사용합니다. +- `/custom/wakeonlan` 서버 프록시가 쓰는 `wake_lan`, `wake_lan_status`만 전용 API 토큰으로 호출할 수 있습니다. 이 토큰은 해당 두 액션에만 유효하며, 일반 POST 제어는 기존 로그인 세션과 CSRF token을 계속 요구합니다. - Rhymix 사이트 관리자 세션 또는 `/custom/common` 관리자 인증 쿠키가 확인되면 Control 내부 로그인 세션을 발급합니다. - Control 로그아웃은 Control remember token과 세션을 지우고, `/custom/common` 공통 인증 쿠키도 함께 만료합니다. - POST 조작 API는 CSRF 검증을 통과해야 합니다. diff --git a/config/config.php b/config/config.php index 27f92e3..fb0b23f 100644 --- a/config/config.php +++ b/config/config.php @@ -696,6 +696,26 @@ function signed_in(): bool return auto_login_from_cookie(); } +function control_api_token_valid(string $scope): bool +{ + global $controlSecretConfig; + + $provided = (string)( + $_SERVER['HTTP_X_CONTROL_API_TOKEN'] + ?? $_POST['api_token'] + ?? '' + ); + + if ($provided === '') { + return false; + } + + $tokens = is_array($controlSecretConfig['api_tokens'] ?? null) ? $controlSecretConfig['api_tokens'] : []; + $expected = (string)($tokens[$scope] ?? ''); + + return $expected !== '' && hash_equals($expected, $provided); +} + function control_admin_session_from_rhymix_or_custom(): bool { static $checked = null; diff --git a/public/api.php b/public/api.php index e219721..5f6c12c 100644 --- a/public/api.php +++ b/public/api.php @@ -9,7 +9,11 @@ if (!$controlApiLibrary && session_status() !== PHP_SESSION_ACTIVE) { require_once __DIR__ . '/../config/config.php'; -if (!$controlApiLibrary && !signed_in()) { +$controlApiAction = $_GET['action'] ?? $_POST['action'] ?? 'status'; +$controlApiTokenScope = in_array($controlApiAction, ['wake_lan', 'wake_lan_status'], true) ? 'wake_lan' : ''; +$controlApiTokenAuthenticated = $controlApiTokenScope !== '' && control_api_token_valid($controlApiTokenScope); + +if (!$controlApiLibrary && !$controlApiTokenAuthenticated && !signed_in()) { json_out([ 'ok' => false, 'error' => 'login_required', @@ -2862,6 +2866,35 @@ function send_wake_on_lan(string $mac, string $broadcast = '192.168.50.255', int ]; } +function wake_lan_status(string $mac): array +{ + $mac = strtolower(trim($mac)); + if (!wifi_valid_mac($mac)) { + throw new InvalidArgumentException('bad_mac'); + } + + $leases = dnsmasq_leases(); + $activeLanMacs = array_fill_keys(lan_fdb_macs(), true); + $observedLanMacs = array_fill_keys(observed_lan_macs($leases), true); + $known = isset($leases[$mac]) || isset($activeLanMacs[$mac]) || isset($observedLanMacs[$mac]); + + if (!$known) { + throw new InvalidArgumentException('unknown_dhcp_client'); + } + + $lease = $leases[$mac] ?? []; + $isOnline = isset($activeLanMacs[$mac]); + + return [ + 'mac' => $mac, + 'ip' => $lease['ip'] ?? null, + 'hostname' => $lease['hostname'] ?? null, + 'state' => $isOnline ? 'on' : 'off', + 'presence' => $isOnline ? 'fdb' : (isset($observedLanMacs[$mac]) ? 'observed' : 'lease'), + 'wol_available' => !$isOnline, + ]; +} + function wifi_time_ms(string $value): int { if (preg_match('/(\d+)/', $value, $m)) { @@ -3630,9 +3663,11 @@ function collect_snapshot(bool $applyFan = true): array function control_api_dispatch(): void { - $action = $_GET['action'] ?? $_POST['action'] ?? 'status'; + global $controlApiAction, $controlApiTokenAuthenticated; - if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { + $action = $controlApiAction; + + if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && !$controlApiTokenAuthenticated) { require_csrf(); } @@ -3839,6 +3874,15 @@ function control_api_dispatch(): void ]); } + if ($action === 'wake_lan_status') { + json_out([ + 'ok' => true, + 'data' => [ + 'wake' => wake_lan_status((string)($_GET['mac'] ?? $_POST['mac'] ?? '')), + ], + ]); + } + if ($action === 'collect') { json_out([ 'ok' => true,