diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 194a95832..95a5f4f63 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -283,4 +283,288 @@ HTML; self::sendError("E-Mail konnte nicht gesendet werden. Bitte kontaktieren Sie den Support."); } } + + /** + * Get GenieACS instance + * @return GenieACS + */ + 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); + } + + /** + * Check if GenieACS task response indicates success + * @param mixed $result + * @return bool + */ + private static function isGenieAcsTaskSuccess($result) { + // Null or empty array can indicate success (204 No Content) + if ($result === null || (is_array($result) && empty($result))) { + return true; + } + + // Has explicit success flag + if (isset($result['success']) && $result['success']) { + return true; + } + + // Array of tasks with _id (task created successfully) + if (is_array($result)) { + // Check if it's an array of task objects + foreach ($result as $item) { + if (is_array($item) && isset($item['_id'])) { + return true; // Task was created + } + } + } + + return false; + } + + /** + * Get devices from GenieACS and match by IP + */ + protected function genieacsGetDeviceByIpAction() { + try { + $ip = $_GET['ip'] ?? null; + if (!$ip) { + self::sendError("IP address is required"); + } + + $acs = $this->getGenieACS(); + $devices = $acs->getDevices(); + + if (!$devices || !is_array($devices)) { + self::returnJson(['success' => false, 'message' => 'No devices found']); + return; + } + + // Find device by matching external IP + $matchedDevice = null; + foreach ($devices as $device) { + $externalIp = GenieACS::getExternalIP($device); + if ($externalIp === $ip) { + $matchedDevice = $device; + break; + } + } + + if (!$matchedDevice) { + self::returnJson(['success' => false, 'message' => 'No device found with this IP']); + return; + } + + $deviceId = GenieACS::getDeviceId($matchedDevice); + $deviceInfo = GenieACS::getDeviceInfo($matchedDevice); + $managementIp = GenieACS::getManagementIP($matchedDevice); + + self::returnJson([ + 'success' => true, + 'deviceId' => $deviceId, + 'deviceInfo' => $deviceInfo, + 'ip' => $ip, + 'managementIp' => $managementIp + ]); + } catch (Exception $e) { + error_log("GenieACS getDeviceByIp error: " . $e->getMessage()); + self::sendError("Error fetching device: " . $e->getMessage()); + } + } + + /** + * Reboot device via GenieACS + */ + protected function genieacsRebootDeviceAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + + if (!$deviceId) { + self::sendError("Device ID is required"); + } + + error_log("GenieACS reboot request for device: " . $deviceId); + + $acs = $this->getGenieACS(); + $result = $acs->rebootDevice($deviceId); + + error_log("GenieACS reboot result: " . json_encode($result)); + + if (self::isGenieAcsTaskSuccess($result)) { + self::returnJson(['success' => true, 'message' => 'Reboot task created', 'result' => $result]); + } else { + self::returnJson(['success' => false, 'message' => 'Failed to create reboot task', 'result' => $result]); + } + } catch (Exception $e) { + error_log("GenieACS rebootDevice error: " . $e->getMessage()); + self::sendError("Error rebooting device: " . $e->getMessage()); + } + } + + /** + * Refresh device information + */ + protected function genieacsRefreshDeviceAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + + if (!$deviceId) { + self::sendError("Device ID is required"); + } + + $acs = $this->getGenieACS(); + $result = $acs->refreshDevice($deviceId); + + if (self::isGenieAcsTaskSuccess($result)) { + self::returnJson(['success' => true, 'message' => 'Refresh task created', 'result' => $result]); + } else { + self::returnJson(['success' => false, 'message' => 'Failed to create refresh task', 'result' => $result]); + } + } catch (Exception $e) { + error_log("GenieACS refreshDevice error: " . $e->getMessage()); + self::sendError("Error refreshing device: " . $e->getMessage()); + } + } + + /** + * Get device parameters + */ + protected function genieacsGetParametersAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + $parameters = $input['parameters'] ?? []; + + if (!$deviceId || empty($parameters)) { + self::sendError("Device ID and parameters are required"); + } + + $acs = $this->getGenieACS(); + $result = $acs->getParameterValues($deviceId, $parameters); + + // Check if result is a successful task response + if (self::isGenieAcsTaskSuccess($result)) { + self::returnJson(['success' => true, 'message' => 'Get parameters task created', 'result' => $result]); + } else { + self::returnJson(['success' => false, 'message' => 'Failed to create get parameters task', 'result' => $result]); + } + } catch (Exception $e) { + error_log("GenieACS getParameters error: " . $e->getMessage()); + self::sendError("Error getting parameters: " . $e->getMessage()); + } + } + + /** + * Set device parameters + */ + protected function genieacsSetParametersAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + $parameters = $input['parameters'] ?? []; + + if (!$deviceId || empty($parameters)) { + self::sendError("Device ID and parameters are required"); + } + + $acs = $this->getGenieACS(); + $result = $acs->setParameterValues($deviceId, $parameters); + + if (self::isGenieAcsTaskSuccess($result)) { + self::returnJson(['success' => true, 'message' => 'Set parameters task created', 'result' => $result]); + } else { + self::returnJson(['success' => false, 'message' => 'Failed to create set parameters task', 'result' => $result]); + } + } catch (Exception $e) { + error_log("GenieACS setParameters error: " . $e->getMessage()); + self::sendError("Error setting parameters: " . $e->getMessage()); + } + } + + /** + * Get full device information + */ + protected function genieacsGetDeviceInfoAction() { + try { + $deviceId = $_GET['deviceId'] ?? null; + + if (!$deviceId) { + self::sendError("Device ID is required"); + } + + $acs = $this->getGenieACS(); + $device = $acs->getDevice($deviceId); + + if (!$device) { + self::sendError("Device not found"); + } + + $deviceInfo = GenieACS::getDeviceInfo($device); + $externalIp = GenieACS::getExternalIP($device); + $macAddress = GenieACS::getMacAddress($device); + + self::returnJson([ + 'success' => true, + 'deviceInfo' => $deviceInfo, + 'externalIp' => $externalIp, + 'macAddress' => $macAddress, + 'fullData' => $device + ]); + } catch (Exception $e) { + error_log("GenieACS getDeviceInfo error: " . $e->getMessage()); + self::sendError("Error getting device info: " . $e->getMessage()); + } + } + + /** + * Ping an IP address via GenieACS + */ + protected function genieacsPingAction() { + try { + $ip = $_GET['ip'] ?? null; + + if (!$ip) { + self::sendError("IP address is required"); + } + + $acs = $this->getGenieACS(); + $result = $acs->ping($ip); + + self::returnJson(['success' => true, 'result' => $result]); + } catch (Exception $e) { + error_log("GenieACS ping error: " . $e->getMessage()); + self::sendError("Error pinging: " . $e->getMessage()); + } + } + + /** + * Factory reset device via GenieACS + */ + protected function genieacsFactoryResetAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + + if (!$deviceId) { + self::sendError("Device ID is required"); + } + + $acs = $this->getGenieACS(); + $result = $acs->factoryResetDevice($deviceId); + + if (self::isGenieAcsTaskSuccess($result)) { + self::returnJson(['success' => true, 'message' => 'Factory reset task created', 'result' => $result]); + } else { + self::returnJson(['success' => false, 'message' => 'Failed to create factory reset task', 'result' => $result]); + } + } catch (Exception $e) { + error_log("GenieACS factoryReset error: " . $e->getMessage()); + self::sendError("Error factory resetting device: " . $e->getMessage()); + } + } } diff --git a/lib/GenieACS/GenieACS.php b/lib/GenieACS/GenieACS.php new file mode 100644 index 000000000..335036e0d --- /dev/null +++ b/lib/GenieACS/GenieACS.php @@ -0,0 +1,471 @@ +log = mfLoghandler::singleton(); + + $this->baseurl = rtrim($baseurl, '/'); + $this->username = $username; + $this->password = $password; + + if (!$this->baseurl || !$this->username || !$this->password) { + throw new Exception("Invalid Arguments"); + } + } + + /** + * Authenticate and retrieve JWT token + * @return bool + * @throws Exception + */ + private function _authenticate() { + $session_key = "genieacs.{$this->baseurl}.jwt"; + $session = new mfConfig($session_key); + + // Check if we have a valid cached token (valid for 1 hour) + if ($session->value() && (time() - $session->edit) < 3600) { + $this->jwt_token = $session->value(); + return true; + } + + $url = $this->baseurl . '/login'; + + $ctx_options = [ + "http" => [ + "ignore_errors" => true, + "method" => "POST", + "header" => [ + "Accept: application/json, text/*", + "Content-Type: application/json; charset=UTF-8", + ], + "content" => json_encode([ + "username" => $this->username, + "password" => $this->password, + ]), + ] + ]; + + $ctx = stream_context_create($ctx_options); + $response = file_get_contents($url, false, $ctx); + + // Extract JWT from response headers + if (isset($http_response_header)) { + foreach ($http_response_header as $header) { + if (stripos($header, 'set-cookie') !== false && stripos($header, 'genieacs-ui-jwt=') !== false) { + preg_match('/genieacs-ui-jwt=([^;]+)/', $header, $matches); + if (isset($matches[1])) { + $this->jwt_token = $matches[1]; + + // Cache the token + $session->value($this->jwt_token); + $session->save(); + + return true; + } + } + } + } + + throw new Exception("Authentication failed - could not retrieve JWT token"); + } + + /** + * Make a GET request to the API + * @param string $endpoint + * @return array|null + * @throws Exception + */ + private function _get($endpoint) { + if (!$this->jwt_token) { + $this->_authenticate(); + } + + $url = $this->baseurl . $endpoint; + + $ctx_options = [ + 'http' => [ + 'ignore_errors' => true, + 'method' => 'GET', + 'header' => [ + 'Cookie: genieacs-ui-jwt=' . $this->jwt_token, + 'Accept: application/json', + ], + ] + ]; + + $ctx = stream_context_create($ctx_options); + $response = file_get_contents($url, false, $ctx); + + // Check if we got a 401 and need to re-authenticate + if (isset($http_response_header)) { + foreach ($http_response_header as $header) { + if (stripos($header, 'HTTP/') === 0 && stripos($header, '401') !== false) { + // Token expired, re-authenticate and retry + $this->jwt_token = null; + $this->_authenticate(); + return $this->_get($endpoint); + } + } + } + + return json_decode($response, true); + } + + /** + * Make a POST request to the API + * @param string $endpoint + * @param array $data + * @return array|null + * @throws Exception + */ + private function _post($endpoint, $data) { + if (!$this->jwt_token) { + $this->_authenticate(); + } + + $url = $this->baseurl . $endpoint; + $jsonData = json_encode($data); + + $ctx_options = [ + 'http' => [ + 'ignore_errors' => true, + 'method' => 'POST', + 'header' => [ + 'Cookie: genieacs-ui-jwt=' . $this->jwt_token, + 'Accept: application/json', + 'Content-Type: application/json', + 'Content-Length: ' . strlen($jsonData) + ], + 'content' => $jsonData, + ] + ]; + + $ctx = stream_context_create($ctx_options); + $response = file_get_contents($url, false, $ctx); + + // Log for debugging + error_log("GenieACS POST to $url: " . $jsonData); + error_log("GenieACS response: " . ($response ?: 'empty')); + + // Check if we got a 401 and need to re-authenticate + if (isset($http_response_header)) { + foreach ($http_response_header as $header) { + error_log("GenieACS response header: " . $header); + if (stripos($header, 'HTTP/') === 0 && stripos($header, '401') !== false) { + // Token expired, re-authenticate and retry + $this->jwt_token = null; + $this->_authenticate(); + return $this->_post($endpoint, $data); + } + } + } + + // If response is empty or false, it might still be successful (204 No Content) + if ($response === false || $response === '') { + // Check if status code indicates success + if (isset($http_response_header)) { + foreach ($http_response_header as $header) { + if (stripos($header, 'HTTP/') === 0) { + if (stripos($header, '200') !== false || stripos($header, '202') !== false || stripos($header, '204') !== false) { + return ['success' => true]; + } + } + } + } + } + + return json_decode($response, true); + } + + /** + * Get all devices + * @return array|null + * @throws Exception + */ + public function getDevices() { + return $this->_get('/api/devices'); + } + + /** + * Get a specific device by ID + * @param string $deviceId Device ID (will be URL-encoded automatically) + * @return array|null + * @throws Exception + */ + public function getDevice($deviceId) { + return $this->_get('/api/devices/' . rawurlencode($deviceId)); + } + + /** + * Create a task for a device + * @param string $deviceId Device ID (will be URL-encoded automatically) + * @param array $tasks Array of tasks to execute + * @return array|null + * @throws Exception + * + * Example: + * $tasks = [ + * ["name" => "getParameterValues", "parameterNames" => ["InternetGatewayDevice.User.1.Username"]] + * ]; + */ + public function createTask($deviceId, $tasks) { + return $this->_post('/api/devices/' . rawurlencode($deviceId) . '/tasks', $tasks); + } + + /** + * Get parameter values from a device + * @param string $deviceId URL-encoded device ID + * @param array $parameterNames Array of parameter names to retrieve + * @return array|null + * @throws Exception + */ + public function getParameterValues($deviceId, $parameterNames) { + $tasks = [ + [ + "name" => "getParameterValues", + "parameterNames" => $parameterNames + ] + ]; + return $this->createTask($deviceId, $tasks); + } + + /** + * Set parameter values on a device + * @param string $deviceId URL-encoded device ID + * @param array $parameterValues Array of parameter name => value pairs + * @return array|null + * @throws Exception + */ + public function setParameterValues($deviceId, $parameterValues) { + $tasks = [ + [ + "name" => "setParameterValues", + "parameterValues" => $parameterValues + ] + ]; + return $this->createTask($deviceId, $tasks); + } + + /** + * Refresh device information + * @param string $deviceId URL-encoded device ID + * @return array|null + * @throws Exception + */ + public function refreshDevice($deviceId) { + $tasks = [ + [ + "name" => "refreshObject", + "objectName" => "" + ] + ]; + return $this->createTask($deviceId, $tasks); + } + + /** + * Reboot a device + * @param string $deviceId URL-encoded device ID + * @return array|null + * @throws Exception + */ + public function rebootDevice($deviceId) { + $tasks = [ + [ + "name" => "reboot" + ] + ]; + return $this->createTask($deviceId, $tasks); + } + + /** + * Factory reset a device + * @param string $deviceId URL-encoded device ID + * @return array|null + * @throws Exception + */ + public function factoryResetDevice($deviceId) { + $tasks = [ + [ + "name" => "factoryReset" + ] + ]; + return $this->createTask($deviceId, $tasks); + } + + /** + * Ping an IP address + * @param string $ip IP address to ping + * @return array|null + * @throws Exception + * + * Returns: { + * "packetsTransmitted": 3, + * "packetsReceived": 3, + * "packetLoss": 0, + * "min": 2.674, + * "avg": 3.054, + * "max": 3.34, + * "mdev": 0.28 + * } + */ + public function ping($ip) { + return $this->_get('/api/ping/' . $ip); + } + + /** + * Download a file from a device + * @param string $deviceId URL-encoded device ID + * @param string $fileType File type to download + * @param string $fileName Optional file name + * @return array|null + * @throws Exception + */ + public function downloadFile($deviceId, $fileType, $fileName = null) { + $task = [ + "name" => "download", + "fileType" => $fileType + ]; + + if ($fileName) { + $task["fileName"] = $fileName; + } + + return $this->createTask($deviceId, [$task]); + } + + /** + * Parse device data from API response to extract useful information + * @param array $deviceData Raw device data from API + * @return array Parsed device information + */ + public static function parseDeviceData($deviceData) { + $parsed = []; + + foreach ($deviceData as $key => $value) { + // Extract simple values + if (isset($value['value']) && is_array($value['value'])) { + $parsed[$key] = $value['value'][0]; + } + } + + return $parsed; + } + + /** + * Get device ID from device data + * @param array $deviceData Raw device data from API + * @return string|null Device ID + */ + public static function getDeviceId($deviceData) { + if (isset($deviceData['DeviceID.ID']['value'][0])) { + return $deviceData['DeviceID.ID']['value'][0]; + } + return null; + } + + /** + * Get MAC address from device data + * @param array $deviceData Raw device data from API + * @return string|null MAC address + */ + public static function getMacAddress($deviceData) { + // Try WAN connection MAC address first + if (isset($deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress']['value'][0])) { + return $deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress']['value'][0]; + } + return null; + } + + /** + * Get external IP address from device data + * @param array $deviceData Raw device data from API + * @return string|null External IP address + */ + public static function getExternalIP($deviceData) { + // Try to get from WAN IP Connection + if (isset($deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress']['value'][0])) { + return $deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress']['value'][0]; + } + + // Try alternative connection + if (isset($deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress']['value'][0])) { + return $deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress']['value'][0]; + } + + return null; + } + + /** + * Get management/local IP address from device data (private IP) + * @param array $deviceData Raw device data from API + * @return string|null Management IP address + */ + public static function getManagementIP($deviceData) { + // Check both WAN connections and return the one with a private IP + $ips = []; + + if (isset($deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress']['value'][0])) { + $ips[] = $deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress']['value'][0]; + } + + if (isset($deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress']['value'][0])) { + $ips[] = $deviceData['InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress']['value'][0]; + } + + // Return the first private IP found (10.x.x.x, 172.16-31.x.x, 192.168.x.x) + foreach ($ips as $ip) { + if (self::isPrivateIP($ip)) { + return $ip; + } + } + + // If no private IP found, return first IP + return $ips[0] ?? null; + } + + /** + * Check if an IP address is in a private range + * @param string $ip IP address + * @return bool True if private IP + */ + private static function isPrivateIP($ip) { + $parts = explode('.', $ip); + if (count($parts) !== 4) return false; + + $first = (int)$parts[0]; + $second = (int)$parts[1]; + + // 10.0.0.0 - 10.255.255.255 + if ($first === 10) return true; + + // 172.16.0.0 - 172.31.255.255 + if ($first === 172 && $second >= 16 && $second <= 31) return true; + + // 192.168.0.0 - 192.168.255.255 + if ($first === 192 && $second === 168) return true; + + return false; + } + + /** + * Get device manufacturer, model, and version info + * @param array $deviceData Raw device data from API + * @return array Device info + */ + public static function getDeviceInfo($deviceData) { + return [ + 'manufacturer' => $deviceData['DeviceID.Manufacturer']['value'][0] ?? null, + 'productClass' => $deviceData['DeviceID.ProductClass']['value'][0] ?? null, + 'oui' => $deviceData['DeviceID.OUI']['value'][0] ?? null, + 'serialNumber' => $deviceData['DeviceID.SerialNumber']['value'][0] ?? null, + 'hardwareVersion' => $deviceData['InternetGatewayDevice.DeviceInfo.HardwareVersion']['value'][0] ?? null, + 'softwareVersion' => $deviceData['InternetGatewayDevice.DeviceInfo.SoftwareVersion']['value'][0] ?? null, + ]; + } +} diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js index 0507dd526..0f995ff90 100644 --- a/public/js/pages/Radius/RadiusUsers.js +++ b/public/js/pages/Radius/RadiusUsers.js @@ -105,6 +105,8 @@ Vue.component('radius-users', { class="fa-duotone fa-circle-info"> + +