action, $this->apiAllowedActions)) { $me = new User(); $me->loadByApikey($apiKey); if ($me->id) { $this->me = $me; $this->isApiCall = true; $this->needlogin = false; if (!defined('INTERNAL_USER_ID')) { define('INTERNAL_USER_ID', $me->id); } header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, X-API-Key"); return; } } $this->needlogin = true; $me = new User(); $me->loadMe(); $this->layout()->set("me", $me); $this->me = $me; if (!$this->me->is("Admin")) $this->redirect("Dashboard"); } protected function indexAction() { $this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]); Helper::renderVue3($this, $this->mod, "Radius", [ 'CAN_BILLING' => $this->me->can("Billing"), 'HIDE_PAGE_TITLE' => true, 'USER_ID' => $this->me->id, ]); } protected function proxyUnsecureHTTPRequestToRadiusAction() { $this->log->debug("proxyUnsecureHTTPRequestToRadiusAction", $_GET); $url = "http://radius.xinon.at/api.php?" . http_build_query($_GET); $url = str_replace("proxyUnsecureHTTPRequestToRadius", "", $url); $opts = [ "http" => [ "method" => "GET", "header" => "Authorization: Basic " . base64_encode("admin:saveman"), ] ]; header("Content-Type: application/json"); $context = stream_context_create($opts); $response = file_get_contents($url, false, $context); echo $response; die(); } protected function genieacsRunSpeedtestAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsRunSpeedtestAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); $acs->getParameterValues($resolvedId, [ 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Start', 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.StartBidirect', 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.WANAccess', 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result' ]); sleep(2); $acs->setParameterValues($resolvedId, [ 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Start' => 1, 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.StartBidirect' => 1, 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.WANAccess' => true ]); sleep(3); $device = $acs->getDevice($resolvedId); $managementIp = GenieACS::getManagementIP($device); $externalIp = GenieACS::getExternalIP($device); $ip = $externalIp ?: $managementIp; if (!$ip) self::sendError("Could not determine device IP"); $url = self::FRITZBOX_API_URL . "/run-speedtest"; $data = json_encode(['ip' => $ip]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data ] ]; $context = stream_context_create($opts); $response = file_get_contents($url, false, $context); if ($response === false) self::sendError("Failed to connect to speedtest server"); self::returnJson(['success' => true, 'message' => 'Speedtest started', 'ip' => $ip, 'serverResponse' => json_decode($response, true)]); } catch (Exception $e) { self::sendError("Error running speedtest: " . $e->getMessage()); } } protected function genieacsGetSpeedtestResultAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsGetSpeedtestResultAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); $acs->getParameterValues($resolvedId, ['InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result']); $device = $acs->getDevice($resolvedId); if (!$device) self::sendError("Device not found"); // Extract speedtest result parameter $paramName = 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result'; $rawValue = null; if (isset($device[$paramName]) && isset($device[$paramName]['value'][0])) { $rawValue = $device[$paramName]['value'][0]; } if (!$rawValue || !is_string($rawValue) || !str_contains($rawValue, 'BPS')) { self::returnJson(['success' => true, 'result' => null]); return; } // Parse the result string (format: "BPS 12345678 Bytes 9876543 Packets 1234") $parsed = $this->parseSpeedtestResult($rawValue); self::returnJson(['success' => true, 'result' => $parsed]); } catch (Exception $e) { $this->log->debug("Speedtest Result Error", ['error' => $e->getMessage()]); self::sendError($e->getMessage()); } } private function parseSpeedtestResult($raw) { try { preg_match('/BPS\s+(\d+)/', $raw, $bpsMatch); preg_match('/Bytes\s+(\d+)/', $raw, $bytesMatch); preg_match('/Packets\s+(\d+)/', $raw, $packetsMatch); if (!$bpsMatch) return null; $bps = (int)$bpsMatch[1]; $bytes = $bytesMatch ? (int)$bytesMatch[1] : 0; $packets = $packetsMatch ? (int)$packetsMatch[1] : 0; return [ 'raw' => $raw, 'bps' => $bps, 'bpsFormatted' => $this->formatBits($bps), 'bytes' => $bytes, 'bytesFormatted' => $this->formatBytes($bytes), 'packets' => $packets ]; } catch (Exception $e) { $this->log->debug("Error parsing speedtest result", ['error' => $e->getMessage()]); return null; } } private function formatBits($bps) { if (!$bps) return '0 Mbit/s'; $mbits = $bps / 1000000; return number_format($mbits, 2, ',', '.') . ' Mbit/s'; } private function formatBytes($bytes) { if ($bytes == 0) return '0 B'; $units = ['B', 'KB', 'MB', 'GB', 'TB']; $i = floor(log($bytes) / log(1024)); return number_format($bytes / pow(1024, $i), 2, ',', '.') . ' ' . $units[$i]; } private function getGenieACS() { $host = defined('GENIEACS_HOST') ? GENIEACS_HOST : 'http://acs.xinon.at:3000'; $username = defined('GENIEACS_USERNAME') ? GENIEACS_USERNAME : 'admin'; $password = defined('GENIEACS_PASSWORD') ? GENIEACS_PASSWORD : 'savemanfb545aw'; return new GenieACS($host, $username, $password); } private function resolveDeviceId(string $deviceId, GenieACS $acs): ?string { if (strpos($deviceId, ':') !== false) { $device = $acs->getDeviceByMac($deviceId); if ($device) { $resolvedId = GenieACS::getDeviceId($device); if ($resolvedId) return $resolvedId; if (isset($device['_id'])) return $device['_id']; } return null; } return $deviceId; } protected function genieacsGetDeviceByIpAction() { try { $ip = $_GET['ip'] ?? null; $this->log->debug("genieacsGetDeviceByIpAction", ['ip' => $ip]); if (!$ip) self::sendError("IP address is required"); $acs = $this->getGenieACS(); $devices = $acs->getDevices(); if (!$devices) { self::returnJson(['success' => false, 'message' => 'No devices found']); return; } $matchedDevice = null; foreach ($devices as $device) { if (GenieACS::getExternalIP($device) === $ip) { $matchedDevice = $device; break; } } if (!$matchedDevice) { self::returnJson(['success' => false, 'message' => 'No device found with this IP']); return; } self::returnJson([ 'success' => true, 'deviceId' => GenieACS::getDeviceId($matchedDevice), 'deviceInfo' => GenieACS::getDeviceInfo($matchedDevice), 'ip' => $ip, 'managementIp' => GenieACS::getManagementIP($matchedDevice) ]); } catch (Exception $e) { $this->log->debug("GetDeviceByIp Error", ['error' => $e->getMessage()]); self::sendError("Error fetching device: " . $e->getMessage()); } } protected function genieacsGetDeviceByMacAction() { try { $mac = $_GET['mac'] ?? null; $this->log->debug("genieacsGetDeviceByMacAction", ['mac' => $mac]); if (!$mac) self::sendError("MAC address is required"); $acs = $this->getGenieACS(); $matchedDevice = $acs->getDeviceByMac($mac); if (!$matchedDevice) { self::returnJson(['success' => false, 'message' => 'No device found with this MAC address']); return; } self::returnJson([ 'success' => true, 'deviceId' => GenieACS::getDeviceId($matchedDevice), 'deviceInfo' => GenieACS::getDeviceInfo($matchedDevice), 'mac' => $mac, 'externalIp' => GenieACS::getExternalIP($matchedDevice), 'managementIp' => GenieACS::getManagementIP($matchedDevice) ]); } catch (Exception $e) { $this->log->debug("GetDeviceByMac Error", ['error' => $e->getMessage()]); self::sendError("Error fetching device: " . $e->getMessage()); } } protected function genieacsRefreshDeviceAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsRefreshDeviceAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); $acs->getParameterValues($resolvedId, [ 'InternetGatewayDevice.DeviceInfo.HardwareVersion', 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', 'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress', 'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress', 'InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID', 'InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase', 'InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName', 'InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress', 'InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress' ]); $device = $acs->getDevice($resolvedId); self::returnJson([ 'success' => true, 'deviceInfo' => GenieACS::getDeviceInfo($device), 'externalIp' => GenieACS::getExternalIP($device), 'managementIp' => GenieACS::getManagementIP($device) ]); } catch (Exception $e) { $this->log->debug("RefreshDevice Error", ['error' => $e->getMessage()]); self::sendError("Error refreshing device: " . $e->getMessage()); } } protected function genieacsRebootDeviceAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsRebootDeviceAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $acs->rebootDevice($deviceId); self::returnJson(['success' => true, 'message' => 'Reboot task created']); } catch (Exception $e) { $this->log->debug("Reboot Error", ['error' => $e->getMessage()]); self::sendError("Error rebooting device: " . $e->getMessage()); } } protected function genieacsGetDeviceInfoAction() { try { $deviceId = $_GET['deviceId'] ?? null; $this->log->debug("genieacsGetDeviceInfoAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $device = $acs->getDevice($deviceId); if (!$device) self::sendError("Device not found"); self::returnJson([ 'success' => true, 'deviceInfo' => GenieACS::getDeviceInfo($device), 'externalIp' => GenieACS::getExternalIP($device), 'macAddress' => GenieACS::getMacAddress($device), 'fullData' => $device ]); } catch (Exception $e) { $this->log->debug("GetDeviceInfo Error", ['error' => $e->getMessage()]); self::sendError("Error getting device info: " . $e->getMessage()); } } protected function genieacsPingAction() { try { $ip = $_GET['ip'] ?? null; $this->log->debug("genieacsPingAction", ['ip' => $ip]); if (!$ip) self::sendError("IP address is required"); $acs = $this->getGenieACS(); $result = $acs->ping($ip); self::returnJson(['success' => true, 'result' => $result]); } catch (Exception $e) { $this->log->debug("Ping Error", ['error' => $e->getMessage()]); self::sendError("Error pinging: " . $e->getMessage()); } } protected function genieacsRemoteAccessAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $forceRecreate = $input['forceRecreate'] ?? false; $this->log->debug("genieacsRemoteAccessAction", ['deviceId' => $deviceId, 'forceRecreate' => $forceRecreate]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $result = $acs->createRemoteUser($deviceId, $forceRecreate); if ($result) { self::returnJson(['success' => true] + $result); } else { self::sendError("Could not retrieve TR069 username from device"); } } catch (Exception $e) { $this->log->debug("Remote Access Error", ['error' => $e->getMessage()]); self::sendError("Error configuring remote access: " . $e->getMessage()); } } protected function genieacsEventLogAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsEventLogAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); $creds = $acs->createRemoteUser($resolvedId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); $url = self::FRITZBOX_API_URL . "/read-fritz-eventlog"; $data = json_encode([ 'fritz_ip' => $creds['ip'], 'fritz_port' => self::FRITZBOX_PORT, 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'] ]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 60 ] ]; $context = stream_context_create($opts); $response = file_get_contents($url, false, $context); if ($response) { $json = json_decode($response, true); if ($json && isset($json['data'])) { self::returnJson(['success' => true, 'events' => $json['data']]); return; } } self::sendError("Failed to fetch event log"); } catch (Exception $e) { $this->log->debug("Event Log Error", ['error' => $e->getMessage()]); self::sendError("Error: " . $e->getMessage()); } } protected function genieacsFritzboxWlanKeyAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $forceRefresh = $input['forceRefresh'] ?? false; $this->log->debug("genieacsFritzboxWlanKeyAction", ['deviceId' => $deviceId, 'forceRefresh' => $forceRefresh]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); // Check cache first (1 hour TTL = 3600s) $cacheKey = "fritzbox_wlan_" . $resolvedId; if (!$forceRefresh) { $cached = $acs->getCachePublic($cacheKey, 3600); if ($cached) { $this->log->debug("WLAN Key: returning cached data"); self::returnJson(['success' => true, 'wlan' => $cached, 'cached' => true]); return; } } $creds = $acs->createRemoteUser($resolvedId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); $url = self::FRITZBOX_API_URL . "/read-fritz-wlan-key"; $data = json_encode([ 'fritz_ip' => $creds['ip'], 'fritz_port' => self::FRITZBOX_PORT, 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'] ]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 60 ] ]; $context = stream_context_create($opts); $response = file_get_contents($url, false, $context); if ($response) { $json = json_decode($response, true); if ($json && isset($json['wlan'])) { // Also fetch LAN/DHCP config from GenieACS device data $device = $acs->getDevice($resolvedId); $lanConfig = $this->extractLanConfig($device); $result = array_merge($json['wlan'], ['lan' => $lanConfig]); // Cache the response $acs->setCachePublic($cacheKey, $result); self::returnJson(['success' => true, 'wlan' => $result, 'cached' => false]); return; } } self::sendError("Failed to fetch WLAN key data"); } catch (Exception $e) { $this->log->debug("WLAN Key Error", ['error' => $e->getMessage()]); self::sendError("Error: " . $e->getMessage()); } } protected function genieacsNetworkStructureAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $forceRefresh = $input['forceRefresh'] ?? false; $this->log->debug("genieacsNetworkStructureAction", ['deviceId' => $deviceId, 'forceRefresh' => $forceRefresh]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $resolvedId = $this->resolveDeviceId($deviceId, $acs); if (!$resolvedId) self::sendError("Device not found in GenieACS"); // Check cache first (15 min TTL = 900s) $cacheKey = "fritzbox_network_" . $resolvedId; if (!$forceRefresh) { $cached = $acs->getCachePublic($cacheKey, 900); if ($cached) { self::returnJson(['root' => $cached, 'cached' => true]); return; } } $creds = $acs->createRemoteUser($resolvedId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); $url = self::FRITZBOX_API_URL . "/read-fritz"; $data = json_encode([ 'fritz_ip' => $creds['ip'], 'fritz_port' => self::FRITZBOX_PORT, 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'], 'page' => 'netDev' ]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . self::FRITZBOX_API_KEY . "\r\n" . "Content-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 60 ] ]; $context = stream_context_create($opts); $response = file_get_contents($url, false, $context); if ($response) { $json = json_decode($response, true); // Check deeper structure: data -> data -> fbox/active if ($json && isset($json['data']['data'])) { $this->ensureMacDb(); $raw = $json['data']['data']; $fbox = $raw['fbox'][0] ?? null; $active = $raw['active'] ?? []; if (!$fbox) { self::returnJson(['root' => null]); return; } // 2. Enrich active devices with Vendor and Initialize Children foreach ($active as &$dev) { $dev['children'] = []; if (isset($dev['mac']) && $dev['mac']) { $dev['vendor'] = $this->getVendor($dev['mac']); } } unset($dev); // 3. Prepare Root if (isset($fbox['mac']) && $fbox['mac']) { $fbox['vendor'] = $this->getVendor($fbox['mac']); } $fbox['children'] = []; $fbox['model'] = 'fbox'; $fbox['name'] = $fbox['name'] ?? 'FRITZ!Box'; $fboxIp = $fbox['ipv4']['ip'] ?? '192.168.178.1'; // 4. Map Active Devices by IP $deviceMap = []; foreach ($active as &$dev) { if (isset($dev['ipv4']['ip']) && $dev['ipv4']['ip']) { $deviceMap[$dev['ipv4']['ip']] = &$dev; } } unset($dev); // 5. Build Tree foreach ($active as &$dev) { $parentIp = null; // Attempt to extract IP from parent URL if (!empty($dev['parent']['url'])) { if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', urldecode($dev['parent']['url']), $matches)) { $parentIp = $matches[1]; } } elseif (!empty($dev['parent']['name']) && $dev['parent']['name'] === 'fritz.repeater') { // Fallback: if parent name is generic repeater but no URL/IP, strictly it's a child of root? // But usually repeaters have IPs. If no IP found, attach to root. } // Attach to parent if found, otherwise Root if ($parentIp && isset($deviceMap[$parentIp]) && $parentIp !== $dev['ipv4']['ip']) { $deviceMap[$parentIp]['children'][] = &$dev; } else { // Check if parent is Root (via IP or name) // If parent IP is Root IP, or no parent found -> Root $fbox['children'][] = &$dev; } } unset($dev); // 6. Sort Function (Recursive) $sortChildren = function(&$node) use (&$sortChildren) { if (!empty($node['children'])) { usort($node['children'], function($a, $b) { // Ethernet First $aType = strtolower($a['type'] ?? ''); $bType = strtolower($b['type'] ?? ''); // Check for 'repeater' in name to prioritize repeaters/access points in sorting if needed // User said: "middle-mans" (repeaters) then devices. // Let's prioritize Repeaters/LAN Bridges. $aIsRepeater = stripos($a['name'] ?? '', 'repeater') !== false; $bIsRepeater = stripos($b['name'] ?? '', 'repeater') !== false; if ($aIsRepeater && !$bIsRepeater) return -1; if (!$aIsRepeater && $bIsRepeater) return 1; if ($aType === 'ethernet' && $bType !== 'ethernet') return -1; if ($aType !== 'ethernet' && $bType === 'ethernet') return 1; // Then by Name return strcasecmp($a['name'] ?? '', $b['name'] ?? ''); }); foreach ($node['children'] as &$child) { $sortChildren($child); } } }; $sortChildren($fbox); // Cache the processed result $acs->setCachePublic($cacheKey, $fbox); self::returnJson(['root' => $fbox, 'cached' => false]); } } self::sendError("Failed to fetch network structure"); } catch (Exception $e) { $this->log->debug("Network Structure Error", ['error' => $e->getMessage()]); self::sendError("Error: " . $e->getMessage()); } } private function ensureMacDb() { $path = TEMP_DIR . '/mac-vendors.csv'; if (!file_exists($path)) { $this->log->debug("Downloading MAC Vendor DB..."); $ctx = stream_context_create(['http'=> ['timeout' => 30]]); $data = @file_get_contents("https://maclookup.app/downloads/csv-database/get-db", false, $ctx); if ($data) file_put_contents($path, $data); } } /** * Extract LAN/DHCP configuration from GenieACS device data */ private function extractLanConfig($device) { if (!is_array($device)) return null; $getParam = function($key) use ($device) { if (isset($device[$key]['value'][0])) { return $device[$key]['value'][0]; } return null; }; return [ // LAN IP Configuration 'ip' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress'), 'subnet' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask'), // DHCP Configuration 'dhcp_start' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress'), 'dhcp_end' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress'), 'dns_servers' => $getParam('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers'), ]; } private function getVendor($mac) { $mac = strtoupper(str_replace([':', '-', '.'], '', $mac)); if (strlen($mac) < 6) return null; $path = TEMP_DIR . '/mac-vendors.csv'; if (!file_exists($path)) return null; // Format as XX:XX:XX $prefix = substr($mac, 0, 2) . ':' . substr($mac, 2, 2) . ':' . substr($mac, 4, 2); // Use grep for speed if available, else fallback to basic search? // Assuming Linux env as per docker context. $cmd = "grep -m 1 \"^" . $prefix . "\" " . escapeshellarg($path); $output = shell_exec($cmd); if ($output) { $parts = str_getcsv($output); if (isset($parts[1])) return $parts[1]; } return null; } // ========== AVM Scanner Methods ========== private static $avmScannerStateFile = null; private static $avmPrefixes = [ '00:04:0E', '00:15:0C', '00:1A:4F', '00:1C:4A', '00:1F:3F', '00:24:FE', '04:B4:FE', '08:96:D7', '08:B6:57', '0C:72:74', '1C:ED:6F', '24:65:11', '2C:3A:FD', '2C:91:AB', '34:31:C4', '34:81:C4', '34:E1:A9', '38:10:D5', '3C:37:12', '3C:A6:2F', '44:4E:6D', '48:5D:35', '50:E6:36', '5C:49:79', '60:B5:8D', '74:42:7F', '7C:FF:4D', '80:23:95', '98:9B:CB', '98:A9:65', '9C:C7:A6', 'B0:F2:08', 'B4:FC:7D', 'BC:05:43', 'C0:25:06', 'C8:0E:14', 'CC:CE:1E', 'D0:12:CB', 'D4:24:DD', 'DC:15:C8', 'DC:39:6F', 'E0:08:55', 'E0:28:6D', 'E8:DF:70', 'F0:B0:14' ]; private function getAvmScannerStatePath(): string { return BASEDIR . '/files/avm_scanner.json'; } private function loadAvmScannerState(): array { $path = $this->getAvmScannerStatePath(); if (file_exists($path)) { $content = file_get_contents($path); $state = json_decode($content, true); if (is_array($state)) return $state; } return [ 'scanning' => false, 'stopRequested' => false, 'progress' => ['current' => 0, 'total' => 0], 'currentDevice' => null, 'startedAt' => null, 'startedBy' => null, 'lastUpdated' => date('c'), 'devices' => [] ]; } private function saveAvmScannerState(array $state): void { $dir = dirname($this->getAvmScannerStatePath()); if (!is_dir($dir)) @mkdir($dir, 0777, true); $state['lastUpdated'] = date('c'); file_put_contents($this->getAvmScannerStatePath(), json_encode($state, JSON_PRETTY_PRINT)); } private function isAvmMac(string $mac): bool { $mac = strtoupper(trim($mac)); $prefix = substr($mac, 0, 8); return in_array($prefix, self::$avmPrefixes); } private function fetchRadiusUsersFromApi(): array { $url = "http://radius.xinon.at/api.php"; $opts = [ "http" => [ "method" => "GET", "header" => "Authorization: Basic " . base64_encode("admin:saveman"), "timeout" => 120 ] ]; $context = stream_context_create($opts); $response = @file_get_contents($url, false, $context); if ($response === false) return []; $data = json_decode($response, true); return is_array($data) ? $data : []; } private function fetchRadacctForUser(string $username): ?array { $url = "http://radius.xinon.at/api.php?action2=fetchRadacct&username=" . urlencode($username); $opts = [ "http" => [ "method" => "GET", "header" => "Authorization: Basic " . base64_encode("admin:saveman"), "timeout" => 10 ] ]; $context = stream_context_create($opts); $response = @file_get_contents($url, false, $context); if ($response === false) return null; $data = json_decode($response, true); return is_array($data) ? $data : null; } protected function avmScannerGetStateAction() { $state = $this->loadAvmScannerState(); header('Content-Type: application/json'); echo json_encode($state); die(); } protected function avmScannerGetUsersAction() { $users = $this->fetchRadiusUsersFromApi(); $debug = $_GET['debug'] ?? false; $macUsers = 0; $avmMacUsers = 0; $notInAcs = 0; $macPrefixCounts = []; $avmUsers = []; foreach ($users as $user) { $username = trim($user['username'] ?? ''); if (!preg_match('/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/i', $username)) continue; $macUsers++; $prefix = strtoupper(substr($username, 0, 8)); $macPrefixCounts[$prefix] = ($macPrefixCounts[$prefix] ?? 0) + 1; if (!$this->isAvmMac($username)) continue; $avmMacUsers++; $info = $user['info'] ?? ''; if (stripos($info, 'ACS') !== false) continue; $notInAcs++; $user['username'] = $username; $avmUsers[] = $user; } if ($debug) { arsort($macPrefixCounts); header('Content-Type: application/json'); echo json_encode([ 'totalFromApi' => count($users), 'macAddressUsers' => $macUsers, 'avmMacUsers' => $avmMacUsers, 'notInAcs' => $notInAcs, 'topMacPrefixes' => array_slice($macPrefixCounts, 0, 30, true), 'avmPrefixes' => self::$avmPrefixes ]); die(); } header('Content-Type: application/json'); echo json_encode(['users' => $avmUsers, 'count' => count($avmUsers)]); die(); } protected function avmScannerStartAction() { $state = $this->loadAvmScannerState(); if ($state['scanning']) { header('Content-Type: application/json'); echo json_encode(['success' => false, 'message' => 'Scan already running']); die(); } // Count how many users will be scanned (for immediate feedback) $users = $this->fetchRadiusUsersFromApi(); $count = 0; foreach ($users as $user) { $username = trim($user['username'] ?? ''); if (!preg_match('/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/i', $username)) continue; if (!$this->isAvmMac($username)) continue; $info = $user['info'] ?? ''; if (stripos($info, 'ACS') !== false) continue; $count++; } // Spawn background script using nohup to ensure it runs independently $scriptPath = BASEDIR . '/scripts/avm_scanner.php'; $logPath = BASEDIR . '/files/avm_scanner.log'; $cmd = "nohup php " . escapeshellarg($scriptPath) . " >> " . escapeshellarg($logPath) . " 2>&1 &"; shell_exec($cmd); header('Content-Type: application/json'); echo json_encode(['success' => true, 'message' => 'Scan started', 'total' => $count]); die(); } protected function avmScannerStopAction() { $state = $this->loadAvmScannerState(); $state['stopRequested'] = true; $this->saveAvmScannerState($state); header('Content-Type: application/json'); echo json_encode(['success' => true, 'message' => 'Stop requested']); die(); } protected function avmScannerToggleErledigtAction() { $input = json_decode(file_get_contents('php://input'), true); $mac = $input['mac'] ?? null; if (!$mac) { header('Content-Type: application/json'); http_response_code(400); echo json_encode(['error' => 'MAC address required']); die(); } $state = $this->loadAvmScannerState(); foreach ($state['devices'] as &$device) { if ($device['mac'] === $mac) { $device['erledigt'] = !($device['erledigt'] ?? false); break; } } $this->saveAvmScannerState($state); header('Content-Type: application/json'); echo json_encode(['success' => true]); die(); } private function detectFritzPort(string $ip): ?int { $url = "https://acs.xinon.at/detect-port"; $data = json_encode(['fritz_ip' => $ip, 'timeout' => 3]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\nContent-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 15 ], "ssl" => ["verify_peer" => false, "verify_peer_name" => false] ]; $context = stream_context_create($opts); $response = @file_get_contents($url, false, $context); if ($response) { $json = json_decode($response, true); if ($json && $json['success'] && isset($json['port'])) { return (int)$json['port']; } } return null; } private function detectFritzDevice(string $ip, int $port): ?array { $url = "https://acs.xinon.at/detect-device"; $data = json_encode(['fritz_ip' => $ip, 'fritz_port' => (string)$port]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\nContent-Length: " . strlen($data) . "\r\n", "content" => $data, "timeout" => 15 ], "ssl" => ["verify_peer" => false, "verify_peer_name" => false] ]; $context = stream_context_create($opts); $response = @file_get_contents($url, false, $context); if ($response) { $json = json_decode($response, true); if ($json && $json['success'] && isset($json['device'])) { return $json['device']; } } return null; } }