304 lines
12 KiB
PHP
304 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 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) {
|
|
$file = TEMP_DIR . "/RadiusCache/" . md5($key) . ".json";
|
|
if (file_exists($file)) {
|
|
if (filemtime($file) < (time() - 1800)) {
|
|
@unlink($file);
|
|
return null;
|
|
}
|
|
return json_decode(file_get_contents($file), true);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |