log = mfLoghandler::singleton(); $this->baseurl = rtrim($baseurl, '/'); $this->username = $username; $this->password = $password; } private function _authenticate() { $session_key = "genieacs.{$this->baseurl}.jwt"; $session = new mfConfig($session_key); if ($session->value() && (time() - $session->edit) < 3600) { $this->jwt_token = $session->value(); $this->log->debug("GenieACS: Using cached JWT token."); return true; } $this->log->debug("GenieACS: Authenticating to get new JWT token."); $ctx = stream_context_create([ "http" => [ "ignore_errors" => true, "method" => "POST", "header" => ["Content-Type: application/json"], "content" => json_encode(["username" => $this->username, "password" => $this->password]), ] ]); $response = file_get_contents($this->baseurl . '/login', false, $ctx); if (isset($http_response_header)) { foreach ($http_response_header as $header) { if (preg_match('/genieacs-ui-jwt=([^;]+)/', $header, $matches)) { $this->jwt_token = $matches[1]; $session->value($this->jwt_token); $session->save(); $this->log->debug("GenieACS: Successfully retrieved and cached new JWT token."); return true; } } } $this->log->debug("GenieACS: Failed to retrieve JWT token."); return false; } private function _request($method, $endpoint, $data = null) { if (!$this->jwt_token && !$this->_authenticate()) { throw new Exception("GenieACS Authentication failed."); } $this->log->debug("GenieACS: Making API request", ['method' => $method, 'endpoint' => $endpoint]); $opts = [ 'http' => [ 'ignore_errors' => true, 'method' => $method, 'header' => ['Cookie: genieacs-ui-jwt=' . $this->jwt_token, 'Content-Type: application/json'], ] ]; if ($data) $opts['http']['content'] = json_encode($data); $ctx = stream_context_create($opts); $response = @file_get_contents($this->baseurl . $endpoint, false, $ctx); // Re-auth on 401 if (isset($http_response_header)) { foreach ($http_response_header as $header) { if (strpos($header, '401') !== false) { $this->log->debug("GenieACS: 401 Unauthorized, re-authenticating."); $this->jwt_token = null; if ($this->_authenticate()) { return $this->_request($method, $endpoint, $data); } else { throw new Exception("GenieACS Re-authentication failed."); } } } } if ($response === false || $response === '') { // 200-204 check if (isset($http_response_header)) { foreach ($http_response_header as $header) { if (strpos($header, 'HTTP/') === 0 && (strpos($header, '200') !== false || strpos($header, '202') !== false || strpos($header, '204') !== false)) { return ['success' => true]; } } } } $decoded = json_decode($response, true); // If request was GET /devices/ID, the response IS the device object. // If request was GET /devices, it is an array of objects. return $decoded; } public function getDevices() { return $this->_request('GET', '/api/devices'); } public function getDeviceByMac($mac) { $mac = strtolower(preg_replace('/[^A-Fa-f0-9]/', '', $mac)); $mac = implode(':', str_split($mac, 2)); $paths = [ 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress', 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.MACAddress', ]; foreach ($paths as $path) { $filter = urlencode($path . ' = "' . $mac . '"'); $result = $this->_request('GET', '/api/devices/?filter=' . $filter . '&limit=1'); if ($result && is_array($result) && count($result) > 0) return $result[0]; } return null; } public function getDevice($deviceId) { return $this->_request('GET', '/api/devices/' . rawurlencode($deviceId)); } public function rebootDevice($deviceId) { return $this->_request('POST', '/api/devices/' . rawurlencode($deviceId) . '/tasks', [['name' => 'reboot']]); } public function ping($ip) { return $this->_request('GET', '/api/ping/' . $ip); } public function getParameterValues($deviceId, $parameterNames) { return $this->_request('POST', '/api/devices/' . rawurlencode($deviceId) . '/tasks', [[ "name" => "getParameterValues", "parameterNames" => $parameterNames ]]); } public function setParameterValues($deviceId, $parameterValues) { $formattedParams = []; foreach ($parameterValues as $name => $value) { $type = 'xsd:string'; if (is_bool($value)) { $type = 'xsd:boolean'; $value = $value ? true : false; } elseif (is_int($value)) $type = 'xsd:int'; elseif (is_float($value)) $type = 'xsd:double'; $formattedParams[] = [$name, $value, $type]; } return $this->_request('POST', '/api/devices/' . rawurlencode($deviceId) . '/tasks', [[ "name" => "setParameterValues", "parameterValues" => $formattedParams ]]); } public function getSpeedtestResult($deviceId) { $param = 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result'; $this->getParameterValues($deviceId, [$param]); usleep(500000); $device = $this->getDevice($deviceId); return self::getParam($device, $param); } public function createRemoteUser($deviceId, $forceRecreate = false) { $this->log->debug("GenieACS: createRemoteUser called", ['deviceId' => $deviceId, 'forceRecreate' => $forceRecreate]); $cacheKey = "remote_user_" . $deviceId; if (!$forceRecreate && $cached = $this->getCache($cacheKey)) { $this->log->debug("GenieACS: Using cached credentials"); return $cached; } $password = $this->generatePassword(12); $timestamp = (string)time(); $userParamsToRefresh = [ 'InternetGatewayDevice.User.1.Enable', 'InternetGatewayDevice.User.1.Password', 'InternetGatewayDevice.User.1.RemoteAccessCapable', 'InternetGatewayDevice.User.1.Username' ]; $this->getParameterValues($deviceId, $userParamsToRefresh); sleep(2); $this->setParameterValues($deviceId, [ 'InternetGatewayDevice.User.1.Enable' => true, 'InternetGatewayDevice.User.1.Password' => $password, 'InternetGatewayDevice.User.1.RemoteAccessCapable' => true, 'InternetGatewayDevice.User.1.Username' => $timestamp ]); // Poll for Username $username = null; $maxAttempts = 15; $paramName = 'InternetGatewayDevice.User.1.Username'; for ($i = 0; $i < $maxAttempts; $i++) { sleep(1); $this->getParameterValues($deviceId, [$paramName]); usleep(500000); $device = $this->getDevice($deviceId); // Access property using flat dot-notation key $val = self::getParam($device, $paramName); $this->log->debug("GenieACS: Poll attempt " . ($i + 1) . " value: " . json_encode($val)); if ($val && strpos($val, 'TR069-') === 0) { $username = $val; break; } } if (!$username) { $this->log->debug("GenieACS: Failed to retrieve TR069 username."); return null; } $ip = self::getExternalIP($this->getDevice($deviceId)); if (!$ip) { $this->log->debug("GenieACS: Could not get external IP."); return null; } $result = [ 'username' => $username, 'password' => $password, 'ip' => $ip, 'link' => "https://" . $ip . ":9090" ]; $this->setCache($cacheKey, $result); return $result; } private function getCache($key, $ttl = 3300) { $file = TEMP_DIR . "/RadiusCache/" . md5($key) . ".json"; if (file_exists($file)) { if (filemtime($file) < (time() - $ttl)) { @unlink($file); return null; } return json_decode(file_get_contents($file), true); } return null; } public function getCachePublic($key, $ttl = 3300) { return $this->getCache($key, $ttl); } public function setCachePublic($key, $data) { $this->setCache($key, $data); } private function setCache($key, $data) { $dir = TEMP_DIR . "/RadiusCache/"; if (!is_dir($dir)) @mkdir($dir, 0777, true); file_put_contents($dir . md5($key) . ".json", json_encode($data)); } private function generatePassword($length) { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return substr(str_shuffle(str_repeat($chars, ceil($length/strlen($chars)))), 1, $length); } // Helpers to safely access device parameters from flat JSON structure private static function getParam($deviceData, $key) { if (!is_array($deviceData)) return null; if (isset($deviceData[$key]['value'][0])) { return $deviceData[$key]['value'][0]; } return null; } public static function getExternalIP($deviceData) { // Try typical WAN paths $paths = [ 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress', 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress', 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress' ]; foreach ($paths as $path) { $val = self::getParam($deviceData, $path); if ($val) return $val; } return null; } public static function getDeviceId($deviceData) { return self::getParam($deviceData, 'DeviceID.ID'); } public static function getDeviceInfo($deviceData) { return [ 'serialNumber' => self::getParam($deviceData, 'DeviceID.SerialNumber'), 'hardwareVersion' => self::getParam($deviceData, 'InternetGatewayDevice.DeviceInfo.HardwareVersion'), 'softwareVersion' => self::getParam($deviceData, 'InternetGatewayDevice.DeviceInfo.SoftwareVersion'), ]; } public static function getManagementIP($deviceData) { // Return any valid IP found, prioritizing private IPs if possible $ip1 = self::getParam($deviceData, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress'); $ip2 = self::getParam($deviceData, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.ExternalIPAddress'); if ($ip1 && self::isPrivateIP($ip1)) return $ip1; if ($ip2 && self::isPrivateIP($ip2)) return $ip2; return $ip1 ?: $ip2; } public static function getMacAddress($deviceData) { $mac1 = self::getParam($deviceData, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress'); if ($mac1) return $mac1; return self::getParam($deviceData, 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.2.MACAddress'); } private static function isPrivateIP($ip) { $parts = explode('.', $ip); if (count($parts) !== 4) return false; $first = (int)$parts[0]; $second = (int)$parts[1]; if ($first === 10) return true; if ($first === 172 && $second >= 16 && $second <= 31) return true; if ($first === 192 && $second === 168) return true; return false; } }