Files
thetool/lib/GenieACS/GenieACS.php
2026-02-02 13:33:27 +01:00

330 lines
12 KiB
PHP

<?php
class GenieACS {
private $log;
private $baseurl;
private $username;
private $password;
private $jwt_token;
public function __construct($baseurl, $username, $password) {
$this->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;
}
}