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(); // Set speedtest parameters on the device $acs->setParameterValues($deviceId, [ 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Start' => 1, 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.StartBidirect' => 1, 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.WANAccess' => true ]); // Get device and extract IP $device = $acs->getDevice($deviceId); $ip = GenieACS::getExternalIP($device); if (!$ip) self::sendError("Could not determine device IP"); // Trigger speedtest via external API $url = "http://acs.xinon.at:5000/run-speedtest"; $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; $data = json_encode(['ip' => $ip]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . $apiKey . "\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']); } catch (Exception $e) { $this->log->debug("Speedtest Error", ['error' => $e->getMessage()]); 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(); // Request parameter refresh $acs->getParameterValues($deviceId, ['InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result']); // Get device info with full data $device = $acs->getDevice($deviceId); 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); } 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 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(); $creds = $acs->createRemoteUser($deviceId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); $url = "http://acs.xinon.at:5000/read-fritz-eventlog"; $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; $data = json_encode([ 'fritz_ip' => $creds['ip'], 'fritz_port' => "9090", 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'] ]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . $apiKey . "\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 genieacsNetworkStructureAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsNetworkStructureAction", ['deviceId' => $deviceId]); if (!$deviceId) self::sendError("Device ID is required"); $acs = $this->getGenieACS(); $creds = $acs->createRemoteUser($deviceId); if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); $url = "http://acs.xinon.at:5000/read-fritz"; $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; $data = json_encode([ 'fritz_ip' => $creds['ip'], 'fritz_port' => "9090", 'fritz_user' => $creds['username'], 'fritz_pass' => $creds['password'], 'page' => 'netDev' ]); $opts = [ "http" => [ "method" => "POST", "header" => "Content-Type: application/json\r\n" . "X-API-Key: " . $apiKey . "\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); self::returnJson(['root' => $fbox]); } } 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); } } 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; } }