958 lines
36 KiB
PHP
958 lines
36 KiB
PHP
<?php
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\Exception;
|
|
|
|
class RadiusController extends mfBaseController {
|
|
private User $me;
|
|
private bool $isApiCall = false;
|
|
|
|
private array $apiAllowedActions = [
|
|
'ProxyUnsecureHTTPRequestToRadius',
|
|
'GenieacsRunSpeedtest',
|
|
'GenieacsGetSpeedtestResult',
|
|
'GenieacsGetDeviceByIp',
|
|
'GenieacsGetDeviceByMac',
|
|
'GenieacsRefreshDevice',
|
|
'GenieacsRebootDevice',
|
|
'GenieacsGetDeviceInfo',
|
|
'GenieacsPing',
|
|
'GenieacsRemoteAccess',
|
|
'GenieacsEventLog',
|
|
'GenieacsNetworkStructure',
|
|
];
|
|
|
|
protected function init(): void {
|
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null;
|
|
|
|
if ($apiKey && in_array($this->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 = "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', '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 = "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();
|
|
|
|
$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 = "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;
|
|
|
|
}
|
|
|
|
// ========== 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;
|
|
}
|
|
|
|
}
|
|
|
|
|