Files
thetool/application/Radius/RadiusController.php
2025-12-13 21:27:43 +00:00

569 lines
22 KiB
PHP

<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
class RadiusController extends mfBaseController {
private User $me;
protected function init(): void {
$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();
// 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;
}
}