Compare commits

...

4 Commits

Author SHA1 Message Date
malle-pietje
d36a088101 API client class v1.1.90
- remove existing x-csrf-token headers before adding a new one, UniFi OS does not like multiple x-csrf-tokens in the same request
2024-02-27 18:48:41 +01:00
malle-pietje
007117cbfc API client class v1.1.89
- added `create_tag()`, `set_tagged_devices()`, `get_tag()`, and `delete_tag()` methods to manage tags, contributed by @brenard, #205
2024-02-11 17:24:31 +01:00
malle-pietje
7e60ce3e87 API client class v1.1.88
- added list_fingerprint_devices() method to list fingerprints for clients devices, contributed by @dream-rhythm, #213
- minor code cleanup
2024-02-11 14:46:30 +01:00
malle-pietje
83d4f121ed API client class v1.1.87
- further code cleanup and refactoring to pass more phpstan tests
2024-02-11 12:33:37 +01:00

View File

@@ -13,7 +13,7 @@ namespace UniFi_API;
* *
* @package UniFi_Controller_API_Client_Class * @package UniFi_Controller_API_Client_Class
* @author Art of WiFi <info@artofwifi.net> * @author Art of WiFi <info@artofwifi.net>
* @version Release: 1.1.86 * @version Release: 1.1.90
* @license This class is subject to the MIT license that is bundled with this package in the file LICENSE.md * @license This class is subject to the MIT license that is bundled with this package in the file LICENSE.md
* @example This directory in the package repository contains a collection of examples: * @example This directory in the package repository contains a collection of examples:
* https://github.com/Art-of-WiFi/UniFi-API-client/tree/master/examples * https://github.com/Art-of-WiFi/UniFi-API-client/tree/master/examples
@@ -21,11 +21,11 @@ namespace UniFi_API;
class Client class Client
{ {
/** /**
* private and protected properties * protected properties
* *
* NOTE: do **not** modify the values here, instead use the constructor or the getter and setter functions/methods * NOTE: do **not** modify the values below, instead use the constructor or the getter and setter functions/methods
*/ */
const CLASS_VERSION = '1.1.86'; const CLASS_VERSION = '1.1.90';
protected string $baseurl = 'https://127.0.0.1:8443'; protected string $baseurl = 'https://127.0.0.1:8443';
protected string $user = ''; protected string $user = '';
protected string $password = ''; protected string $password = '';
@@ -41,12 +41,16 @@ class Client
protected bool $curl_ssl_verify_peer = false; protected bool $curl_ssl_verify_peer = false;
protected int $curl_ssl_verify_host = 0; protected int $curl_ssl_verify_host = 0;
protected int $curl_http_version = CURL_HTTP_VERSION_NONE; protected int $curl_http_version = CURL_HTTP_VERSION_NONE;
protected array $curl_headers = [];
protected string $curl_method = 'GET'; protected string $curl_method = 'GET';
protected array $curl_methods_allowed = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; protected array $curl_methods_allowed = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
protected int $curl_request_timeout = 30; protected int $curl_request_timeout = 30;
protected int $curl_connect_timeout = 10; protected int $curl_connect_timeout = 10;
protected string $unificookie_name; protected string $unificookie_name = 'unificookie';
protected array $curl_headers = [
'accept: application/json',
'content-type: application/json',
'Expect:',
];
/** /**
* Construct an instance of the UniFi API client class * Construct an instance of the UniFi API client class
@@ -65,7 +69,15 @@ class Client
* This is only needed when you have multiple apps using the API on the same web * This is only needed when you have multiple apps using the API on the same web
* server. * server.
*/ */
public function __construct(string $user, string $password, string $baseurl = '', string $site = null, string $version = null, bool $ssl_verify = false, string $unificookie_name = 'unificookie') public function __construct(
string $user,
string $password,
string $baseurl = '',
string $site = null,
string $version = null,
bool $ssl_verify = false,
string $unificookie_name = 'unificookie'
)
{ {
if (!extension_loaded('curl')) { if (!extension_loaded('curl')) {
trigger_error('The PHP curl extension is not loaded. Please correct this before proceeding!'); trigger_error('The PHP curl extension is not loaded. Please correct this before proceeding!');
@@ -104,7 +116,7 @@ class Client
public function __destruct() public function __destruct()
{ {
/** /**
* if $_SESSION[$this->unificookie_name] is set, do not log out here except when this is a UniFi OS-based controller * if $_SESSION[$this->unificookie_name] is set, do not log out here
*/ */
if (isset($_SESSION[$this->unificookie_name])) { if (isset($_SESSION[$this->unificookie_name])) {
return; return;
@@ -121,7 +133,9 @@ class Client
/** /**
* Login to the UniFi controller * Login to the UniFi controller
* *
* @return bool|int returns true upon success, false or HTTP status upon error * @return bool|int returns true upon success, false or the HTTP response code (typically 400, 401, or 403) upon
* error
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses
*/ */
public function login() public function login()
{ {
@@ -139,9 +153,7 @@ class Client
/** /**
* prepare cURL and options to check whether this is a "regular" controller or one based on UniFi OS * prepare cURL and options to check whether this is a "regular" controller or one based on UniFi OS
*/ */
if (!($ch = $this->get_curl_handle())) { $ch = $this->get_curl_handle();
return false;
}
$curl_options = [ $curl_options = [
CURLOPT_URL => $this->baseurl . '/', CURLOPT_URL => $this->baseurl . '/',
@@ -153,6 +165,7 @@ class Client
* execute the cURL request and get the HTTP response code * execute the cURL request and get the HTTP response code
*/ */
curl_exec($ch); curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) { if (curl_errno($ch)) {
@@ -165,11 +178,7 @@ class Client
$curl_options = [ $curl_options = [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['username' => $this->user, 'password' => $this->password]), CURLOPT_POSTFIELDS => json_encode(['username' => $this->user, 'password' => $this->password]),
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => $this->curl_headers,
'accept: application/json',
'content-type: application/json',
'Expect:',
],
CURLOPT_REFERER => $this->baseurl . '/login', CURLOPT_REFERER => $this->baseurl . '/login',
CURLOPT_URL => $this->baseurl . '/api/login', CURLOPT_URL => $this->baseurl . '/api/login',
]; ];
@@ -207,7 +216,7 @@ class Client
/** /**
* based on the HTTP response code trigger an error * based on the HTTP response code trigger an error
*/ */
if ($http_code === 400 || $http_code === 401) { if ($http_code >= 400) {
trigger_error("HTTP response status received: $http_code. Probably a controller login failure"); trigger_error("HTTP response status received: $http_code. Probably a controller login failure");
return $http_code; return $http_code;
@@ -218,8 +227,9 @@ class Client
/** /**
* check the HTTP response code * check the HTTP response code
*/ */
if ($http_code >= 200 && $http_code < 400) { if ($http_code >= 200) {
$this->is_logged_in = true; $this->is_logged_in = true;
return $this->is_logged_in; return $this->is_logged_in;
} }
@@ -236,22 +246,13 @@ class Client
/** /**
* prepare cURL and options * prepare cURL and options
*/ */
if (!($ch = $this->get_curl_handle())) { $ch = $this->get_curl_handle();
return false;
}
$curl_options = [ $curl_options = [
CURLOPT_HEADER => true, CURLOPT_HEADER => true,
CURLOPT_POST => true, CURLOPT_POST => true,
]; ];
/**
* construct the HTTP request headers as required
*/
$this->curl_headers = [
'Expect:',
];
$logout_path = '/logout'; $logout_path = '/logout';
if ($this->is_unifi_os) { if ($this->is_unifi_os) {
@@ -407,9 +408,17 @@ class Client
* @return array|bool returns an array with a single object containing details of the new user/client-device on * @return array|bool returns an array with a single object containing details of the new user/client-device on
* success, else returns false * success, else returns false
*/ */
public function create_user(string $mac, string $user_group_id, string $name = null, string $note = null, bool $is_guest = null, bool $is_wired = null) public function create_user(
string $mac,
string $user_group_id,
string $name = null,
string $note = null,
bool $is_guest = null,
bool $is_wired = null
)
{ {
$new_user = ['mac' => strtolower($mac), 'usergroup_id' => $user_group_id]; $new_user = ['mac' => strtolower($mac), 'usergroup_id' => $user_group_id];
if (!empty($name)) { if (!empty($name)) {
$new_user['name'] = $name; $new_user['name'] = $name;
} }
@@ -1123,6 +1132,19 @@ class Client
return $this->fetch_results('/api/s/' . $this->site . '/stat/user/' . strtolower(trim($client_mac))); return $this->fetch_results('/api/s/' . $this->site . '/stat/user/' . strtolower(trim($client_mac)));
} }
/**
* Fetch fingerprints for client devices
*
* @param int $fingerprint_source the id of the client fingerprint_source, starts from 0, this matches the
* fingerprint_source in the client device objects, the default value is 0
* @return array|bool an array of fingerprints, contain dev_ids, dev_type_ids, family_ids, os_name_ids, os_class_ids
* and vendor_ids, false upon error
*/
public function list_fingerprint_devices(int $fingerprint_source = 0)
{
return $this->fetch_results('/v2/api/fingerprint_devices/' . $fingerprint_source);
}
/** /**
* Assign client device to another group * Assign client device to another group
* *
@@ -1489,6 +1511,74 @@ class Client
return $this->fetch_results('/api/s/' . $this->site . '/rest/tag'); return $this->fetch_results('/api/s/' . $this->site . '/rest/tag');
} }
/**
* Create (device) tag (using REST)
*
* NOTES: this endpoint was introduced with controller versions 5.5.X
*
* @param string $name required, the tag name to add
* @param array|null $devices_macs optional, array of the MAC address(es) of the device(s) to tag with the new tag
* @return bool return true on success
*/
public function create_tag(string $name, array $devices_macs = null): bool
{
$payload = ['name' => $name];
if (is_array($devices_macs)) {
$payload['member_table'] = $devices_macs;
}
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/tag', $payload);
}
/**
* Set tagged devices (using REST)
*
* NOTES: this endpoint was introduced with controller versions 5.5.X
*
* @param array $devices_macs required, array of the MAC address(es) of the device(s) to tag
* @param string $tag_id required, the _id value of the tag to set
* @return bool return true on success
*/
public function set_tagged_devices(array $devices_macs, string $tag_id): bool
{
$this->curl_method = 'PUT';
$payload = ['member_table' => $devices_macs];
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/tag/' . $tag_id, $payload);
}
/**
* Get (device) tag (using REST)
*
* NOTES: this endpoint was introduced with controller versions 5.5.X
*
* @param string $tag_id required, the _id value of the tag to retrieve
* @return array|bool containing matching tag objects
*/
public function get_tag(string $tag_id)
{
$this->curl_method = 'GET';
return $this->fetch_results('/api/s/' . $this->site . '/rest/tag/' . $tag_id);
}
/**
* Delete (device) tag (using REST)
*
* NOTES: this endpoint was introduced with controller versions 5.5.X
*
* @param string $tag_id required, the _id value of the tag to set
* @return bool return true on success
*/
public function delete_tag(string $tag_id): bool
{
$this->curl_method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/tag/' . $tag_id);
}
/** /**
* Fetch rogue/neighboring access points * Fetch rogue/neighboring access points
* *
@@ -1498,6 +1588,7 @@ class Client
public function list_rogueaps(int $within = 24) public function list_rogueaps(int $within = 24)
{ {
$payload = ['within' => $within]; $payload = ['within' => $within];
return $this->fetch_results('/api/s/' . $this->site . '/stat/rogueap', $payload); return $this->fetch_results('/api/s/' . $this->site . '/stat/rogueap', $payload);
} }
@@ -3093,9 +3184,8 @@ class Client
* *
* @param string $device_id _id of the device which can be found with the list_devices() function * @param string $device_id _id of the device which can be found with the list_devices() function
* @param object|array $payload stdClass object or associative array containing the configuration to apply to the * @param object|array $payload stdClass object or associative array containing the configuration to apply to the
* device, must be a * device, must be a (partial) object/array structured in the same manner as is returned
* (partial) object/array structured in the same manner as is returned by * by list_devices() for the device.
* list_devices() for the device.
* @return bool true on success * @return bool true on success
*/ */
public function set_device_settings_base(string $device_id, $payload): bool public function set_device_settings_base(string $device_id, $payload): bool
@@ -3813,7 +3903,9 @@ class Client
} }
return true; return true;
} elseif ($response->meta->rc === 'error') { }
if ($response->meta->rc === 'error') {
/** /**
* an error occurred: * an error occurred:
* set $this->set last_error_message if the returned error message is available * set $this->set last_error_message if the returned error message is available
@@ -3880,30 +3972,38 @@ class Client
return true; return true;
case JSON_ERROR_DEPTH: case JSON_ERROR_DEPTH:
$error = 'The maximum stack depth has been exceeded'; $error = 'The maximum stack depth has been exceeded';
break; break;
case JSON_ERROR_STATE_MISMATCH: case JSON_ERROR_STATE_MISMATCH:
$error = 'Invalid or malformed JSON'; $error = 'Invalid or malformed JSON';
break; break;
case JSON_ERROR_CTRL_CHAR: case JSON_ERROR_CTRL_CHAR:
$error = 'Control character error, possibly incorrectly encoded'; $error = 'Control character error, possibly incorrectly encoded';
break; break;
case JSON_ERROR_SYNTAX: case JSON_ERROR_SYNTAX:
$error = 'Syntax error, malformed JSON'; $error = 'Syntax error, malformed JSON';
break; break;
case JSON_ERROR_UTF8: case JSON_ERROR_UTF8:
// PHP >= 5.3.3 // PHP >= 5.3.3
$error = 'Malformed UTF-8 characters, possibly incorrectly encoded'; $error = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break; break;
case JSON_ERROR_RECURSION: case JSON_ERROR_RECURSION:
// PHP >= 5.5.0 // PHP >= 5.5.0
$error = 'One or more recursive references in the value to be encoded'; $error = 'One or more recursive references in the value to be encoded';
break; break;
case JSON_ERROR_INF_OR_NAN: case JSON_ERROR_INF_OR_NAN:
// PHP >= 5.5.0 // PHP >= 5.5.0
$error = 'One or more NAN or INF values in the value to be encoded'; $error = 'One or more NAN or INF values in the value to be encoded';
break; break;
case JSON_ERROR_UNSUPPORTED_TYPE: case JSON_ERROR_UNSUPPORTED_TYPE:
$error = 'A value of a type that cannot be encoded was given'; $error = 'A value of a type that cannot be encoded was given';
break; break;
} }
@@ -3914,9 +4014,11 @@ class Client
switch (json_last_error()) { switch (json_last_error()) {
case JSON_ERROR_INVALID_PROPERTY_NAME: case JSON_ERROR_INVALID_PROPERTY_NAME:
$error = 'A property name that cannot be encoded was given'; $error = 'A property name that cannot be encoded was given';
break; break;
case JSON_ERROR_UTF16: case JSON_ERROR_UTF16:
$error = 'Malformed UTF-16 characters, possibly incorrectly encoded'; $error = 'Malformed UTF-16 characters, possibly incorrectly encoded';
break; break;
} }
} }
@@ -3939,6 +4041,7 @@ class Client
{ {
if (!filter_var($baseurl, FILTER_VALIDATE_URL) || substr($baseurl, -1) === '/') { if (!filter_var($baseurl, FILTER_VALIDATE_URL) || substr($baseurl, -1) === '/') {
trigger_error('The URL provided is incomplete, invalid or ends with a / character!'); trigger_error('The URL provided is incomplete, invalid or ends with a / character!');
return false; return false;
} }
@@ -3955,6 +4058,7 @@ class Client
{ {
if ($this->debug && preg_match('/\s/', $site)) { if ($this->debug && preg_match('/\s/', $site)) {
trigger_error('The provided (short) site name may not contain any spaces'); trigger_error('The provided (short) site name may not contain any spaces');
return false; return false;
} }
@@ -3993,15 +4097,26 @@ class Client
{ {
if (!empty($this->cookies) && strpos($this->cookies, 'TOKEN') !== false) { if (!empty($this->cookies) && strpos($this->cookies, 'TOKEN') !== false) {
$cookie_bits = explode('=', $this->cookies); $cookie_bits = explode('=', $this->cookies);
if (!array_key_exists(1, $cookie_bits)) { if (!array_key_exists(1, $cookie_bits)) {
return; return;
} }
$jwt_components = explode('.', $cookie_bits[1]); $jwt_components = explode('.', $cookie_bits[1]);
if (!array_key_exists(1, $jwt_components)) { if (!array_key_exists(1, $jwt_components)) {
return; return;
} }
/**
* remove any existing x-csrf-token headers first
*/
foreach ($this->curl_headers as $index => $header) {
if (strpos(strtolower($header), strtolower('x-csrf-token:')) !== false) {
unset($this->curl_headers[$index]);
}
}
$this->curl_headers[] = 'x-csrf-token: ' . json_decode(base64_decode($jwt_components[1]))->csrfToken; $this->curl_headers[] = 'x-csrf-token: ' . json_decode(base64_decode($jwt_components[1]))->csrfToken;
} }
} }
@@ -4057,18 +4172,14 @@ class Client
return false; return false;
} }
if (!($ch = $this->get_curl_handle())) { $url = $this->baseurl . $path;
trigger_error('get_curl_handle() did not return a resource');
return false;
}
$this->curl_headers = [];
$url = $this->baseurl . $path;
if ($this->is_unifi_os) { if ($this->is_unifi_os) {
$url = $this->baseurl . '/proxy/network' . $path; $url = $this->baseurl . '/proxy/network' . $path;
} }
$ch = $this->get_curl_handle();
$curl_options = [ $curl_options = [
CURLOPT_URL => $url, CURLOPT_URL => $url,
]; ];
@@ -4081,14 +4192,6 @@ class Client
$json_payload = json_encode($payload, JSON_UNESCAPED_SLASHES); $json_payload = json_encode($payload, JSON_UNESCAPED_SLASHES);
$curl_options[CURLOPT_POSTFIELDS] = $json_payload; $curl_options[CURLOPT_POSTFIELDS] = $json_payload;
/**
* add empty Expect header to prevent cURL from injecting an "Expect: 100-continue" header
*/
$this->curl_headers = [
'accept: application/json',
'content-type: application/json',
'Expect:',
];
/** /**
* should not use GET (the default request type) or DELETE when passing a payload, * should not use GET (the default request type) or DELETE when passing a payload,
@@ -4118,9 +4221,7 @@ class Client
$this->create_x_csrf_token_header(); $this->create_x_csrf_token_header();
} }
if (count($this->curl_headers) > 0) { $curl_options[CURLOPT_HTTPHEADER] = $this->curl_headers;
$curl_options[CURLOPT_HTTPHEADER] = $this->curl_headers;
}
curl_setopt_array($ch, $curl_options); curl_setopt_array($ch, $curl_options);
@@ -4128,6 +4229,7 @@ class Client
* execute the cURL request * execute the cURL request
*/ */
$response = curl_exec($ch); $response = curl_exec($ch);
if (curl_errno($ch)) { if (curl_errno($ch)) {
trigger_error('cURL error: ' . curl_error($ch)); trigger_error('cURL error: ' . curl_error($ch));
} }
@@ -4157,6 +4259,7 @@ class Client
$this->is_logged_in = false; $this->is_logged_in = false;
$this->cookies = ''; $this->cookies = '';
$this->exec_retries++; $this->exec_retries++;
curl_close($ch); curl_close($ch);
/** /**
@@ -4189,6 +4292,7 @@ class Client
print_r(curl_getinfo($ch)); print_r(curl_getinfo($ch));
print PHP_EOL . '-------URL & PAYLOAD---------' . PHP_EOL; print PHP_EOL . '-------URL & PAYLOAD---------' . PHP_EOL;
print $url . PHP_EOL; print $url . PHP_EOL;
if (empty($json_payload)) { if (empty($json_payload)) {
print 'empty payload'; print 'empty payload';
} }
@@ -4206,23 +4310,19 @@ class Client
* set method back to default value, just in case * set method back to default value, just in case
*/ */
$this->curl_method = 'GET'; $this->curl_method = 'GET';
return $response; return $response;
} }
/** /**
* Create and return a new cURL handle * Create and return a new cURL handle
* *
* @return object|resource|bool CurlHandle object with PHP 8, or a resource for lower PHP versions upon * @return object|resource CurlHandle object with PHP 8, or a resource for lower PHP versions
* success, false upon failure
*/ */
protected function get_curl_handle() protected function get_curl_handle()
{ {
$ch = curl_init(); $ch = curl_init();
if (!is_resource($ch) && !is_object($ch)) {
return false;
}
$curl_options = [ $curl_options = [
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_HTTP_VERSION => $this->curl_http_version, CURLOPT_HTTP_VERSION => $this->curl_http_version,