Compare commits

...

7 Commits

Author SHA1 Message Date
malle-pietje
665fed93e2 shortened several property names 2021-03-22 11:58:20 +01:00
malle-pietje
36c0fecaff minor improvements 2021-03-22 11:52:44 +01:00
malle-pietje
474578a9d5 removed non-compliant header suffix 2021-03-21 16:08:55 +01:00
malle-pietje
5281db56de API client class v1.1.69
- added list_device_states() function/method, as suggested by @hoerter
- implemented fix to prevent cURL from sending an `Expect: 100-continue` header with each POST request
- implemented a callback function with the CURLOPT_HEADERFUNCTION option to process the response headers after each request and extract the Cookie contents
- general cleanup
2021-03-21 16:03:05 +01:00
malle-pietje
021d01ba86 API client class v1.1.68
- fixed a bug that was introduced with 1.1.67 and would only occur in certain corner cases
2021-01-24 18:09:38 +01:00
malle-pietje
caf838abb9 API client class v1.1.67
- fixed a bug where the request headers for subsequent function calls within the same Client instance would not always be cleared
2021-01-24 17:58:23 +01:00
malle-pietje
aa778c9b7b API client class v1.1.66
- simplified code based on Scrutinizer reports
2021-01-24 14:21:26 +01:00

View File

@@ -12,7 +12,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.65 * @version Release: 1.1.70
* @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
@@ -20,27 +20,27 @@ namespace UniFi_API;
class Client class Client
{ {
/** /**
* protected and private properties * private and protected properties
*/ */
protected $baseurl = 'https://127.0.0.1:8443'; private $class_version = '1.1.70';
protected $user = ''; protected $baseurl = 'https://127.0.0.1:8443';
protected $password = ''; protected $user = '';
protected $site = 'default'; protected $password = '';
protected $version = '6.0.43'; protected $site = 'default';
protected $debug = false; protected $version = '6.0.43';
protected $is_loggedin = false; protected $debug = false;
protected $is_unifi_os = false; protected $ssl_verify_peer = false;
protected $exec_retries = 0; protected $ssl_verify_host = false;
protected $class_version = '1.1.65'; protected $is_loggedin = false;
private $cookies = ''; protected $is_unifi_os = false;
private $headers = []; protected $exec_retries = 0;
private $request_method = 'GET'; protected $cookies = '';
private $request_methods_allowed = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; protected $headers = [];
private $connect_timeout = 10; protected $method = 'GET';
private $last_results_raw = null; protected $methods_allowed = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
private $last_error_message = null; protected $connect_timeout = 10;
private $curl_ssl_verify_peer = false; protected $last_results_raw = null;
private $curl_ssl_verify_host = false; protected $last_error_message = null;
/** /**
* Construct an instance of the UniFi API client class * Construct an instance of the UniFi API client class
@@ -80,8 +80,8 @@ class Client
} }
if ((boolean) $ssl_verify === true) { if ((boolean) $ssl_verify === true) {
$this->curl_ssl_verify_peer = true; $this->ssl_verify_peer = true;
$this->curl_ssl_verify_host = 2; $this->ssl_verify_host = 2;
} }
} }
@@ -116,7 +116,7 @@ class Client
public function login() public function login()
{ {
/** /**
* if already logged in we skip the login process * skip the login process if already logged in
*/ */
if ($this->is_loggedin === true) { if ($this->is_loggedin === true) {
return true; return true;
@@ -129,7 +129,7 @@ class Client
} }
/** /**
* first we check whether we have a "regular" controller or one based on UniFi OS, * check whether this is a "regular" controller or one based on UniFi OS,
* prepare cURL and options * prepare cURL and options
*/ */
if (!($ch = $this->get_curl_resource())) { if (!($ch = $this->get_curl_resource())) {
@@ -156,14 +156,17 @@ class Client
} }
/** /**
* we now proceed with the actual login * prepare the actual login
*/ */
$curl_options = [ $curl_options = [
CURLOPT_NOBODY => false, CURLOPT_NOBODY => false,
CURLOPT_POSTFIELDS => json_encode(['username' => $this->user, 'password' => $this->password]), CURLOPT_POSTFIELDS => json_encode(['username' => $this->user, 'password' => $this->password]),
CURLOPT_HTTPHEADER => ['content-type: application/json'], CURLOPT_HTTPHEADER => [
'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',
]; ];
/** /**
@@ -197,43 +200,21 @@ class Client
} }
/** /**
* based on the HTTP response code we either trigger an error or * based on the HTTP response code trigger an error
* extract the cookie from the headers
*/ */
if ($http_code === 400 || $http_code === 401) { if ($http_code === 400 || $http_code === 401) {
trigger_error("We received the following HTTP response status: $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;
} }
$response_header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$response_headers = substr($response, 0, $response_header_size);
$response_body = trim(substr($response, $response_header_size));
curl_close($ch); curl_close($ch);
/** /**
* we are good to extract the cookies * extract the cookies
*/ */
if ($http_code >= 200 && $http_code < 400 && !empty($response_body)) { if ($http_code >= 200 && $http_code < 400) {
preg_match_all('|Set-Cookie: (.*);|Ui', $response_headers, $results); return $this->is_loggedin;
if (array_key_exists(1, $results)) {
$this->cookies = implode(';', $results[1]);
/**
* accept cookies from regular UniFi controllers or from UniFi OS
*/
if (strpos($this->cookies, 'unifises') !== false || strpos($this->cookies, 'TOKEN') !== false) {
/**
* update the cookie value in $_SESSION['unificookie'], if it exists
*/
if (isset($_SESSION['unificookie'])) {
$_SESSION['unificookie'] = $this->cookies;
}
return $this->is_loggedin = true;
}
}
} }
return false; return false;
@@ -261,7 +242,11 @@ class Client
/** /**
* constuct HTTP request headers as required * constuct HTTP request headers as required
*/ */
$this->headers = ['content-length: 0']; $this->headers = [
'content-length: 0',
'Expect:'
];
$logout_path = '/logout'; $logout_path = '/logout';
if ($this->is_unifi_os) { if ($this->is_unifi_os) {
$logout_path = '/api/auth/logout'; $logout_path = '/api/auth/logout';
@@ -312,7 +297,7 @@ class Client
$payload = ['cmd' => 'authorize-guest', 'mac' => strtolower($mac), 'minutes' => intval($minutes)]; $payload = ['cmd' => 'authorize-guest', 'mac' => strtolower($mac), 'minutes' => intval($minutes)];
/** /**
* if we have received values for up/down/megabytes/ap_mac we append them to the payload array to be submitted * append received values for up/down/megabytes/ap_mac to the payload array to be submitted
*/ */
if (!empty($up)) { if (!empty($up)) {
$payload['up'] = intval($up); $payload['up'] = intval($up);
@@ -423,7 +408,6 @@ class Client
if (!empty($note)) { if (!empty($note)) {
$new_user['note'] = $note; $new_user['note'] = $note;
$new_user['noted'] = true;
} }
if (!empty($is_guest) && is_bool($is_guest)) { if (!empty($is_guest) && is_bool($is_guest)) {
@@ -449,8 +433,8 @@ class Client
*/ */
public function set_sta_note($user_id, $note = null) public function set_sta_note($user_id, $note = null)
{ {
$noted = empty($note) ? false : true; //$noted = empty($note) ? false : true;
$payload = ['note' => $note, 'noted' => $noted]; $payload = ['note' => $note];
return $this->fetch_results_boolean('/api/s/' . $this->site . '/upd/user/' . trim($user_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/upd/user/' . trim($user_id), $payload);
} }
@@ -1109,7 +1093,7 @@ class Client
return false; return false;
} }
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = [ $payload = [
'_id' => $client_id, '_id' => $client_id,
'use_fixedip' => $use_fixedip 'use_fixedip' => $use_fixedip
@@ -1165,7 +1149,7 @@ class Client
*/ */
public function edit_usergroup($group_id, $site_id, $group_name, $group_dn = -1, $group_up = -1) public function edit_usergroup($group_id, $site_id, $group_name, $group_dn = -1, $group_up = -1)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = [ $payload = [
'_id' => $group_id, '_id' => $group_id,
'name' => $group_name, 'name' => $group_name,
@@ -1185,7 +1169,7 @@ class Client
*/ */
public function delete_usergroup($group_id) public function delete_usergroup($group_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/usergroup/' . trim($group_id)); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/usergroup/' . trim($group_id));
} }
@@ -1225,7 +1209,7 @@ class Client
*/ */
public function edit_apgroup($group_id, $group_name, $device_macs) public function edit_apgroup($group_id, $group_name, $device_macs)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = [ $payload = [
'_id' => $group_id, '_id' => $group_id,
'attr_no_delete' => false, 'attr_no_delete' => false,
@@ -1244,7 +1228,7 @@ class Client
*/ */
public function delete_apgroup($group_id) public function delete_apgroup($group_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/v2/api/site/' . $this->site . '/apgroups/' . trim($group_id)); return $this->fetch_results_boolean('/v2/api/site/' . $this->site . '/apgroups/' . trim($group_id));
} }
@@ -1298,7 +1282,7 @@ class Client
return false; return false;
} }
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = [ $payload = [
'_id' => $group_id, '_id' => $group_id,
'name' => $group_name, 'name' => $group_name,
@@ -1318,7 +1302,7 @@ class Client
*/ */
public function delete_firewallgroup($group_id) public function delete_firewallgroup($group_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/firewallgroup/' . trim($group_id)); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/firewallgroup/' . trim($group_id));
} }
@@ -1525,7 +1509,7 @@ class Client
*/ */
public function set_site_country($country_id, $payload) public function set_site_country($country_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/country/' . trim($country_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/country/' . trim($country_id), $payload);
} }
@@ -1544,7 +1528,7 @@ class Client
*/ */
public function set_site_locale($locale_id, $payload) public function set_site_locale($locale_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/locale/' . trim($locale_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/locale/' . trim($locale_id), $payload);
} }
@@ -1560,7 +1544,7 @@ class Client
*/ */
public function set_site_snmp($snmp_id, $payload) public function set_site_snmp($snmp_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/snmp/' . trim($snmp_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/snmp/' . trim($snmp_id), $payload);
} }
@@ -1576,7 +1560,7 @@ class Client
*/ */
public function set_site_mgmt($mgmt_id, $payload) public function set_site_mgmt($mgmt_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/mgmt/' . trim($mgmt_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/mgmt/' . trim($mgmt_id), $payload);
} }
@@ -1592,7 +1576,7 @@ class Client
*/ */
public function set_site_guest_access($guest_access_id, $payload) public function set_site_guest_access($guest_access_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/guest_access/' . trim($guest_access_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/guest_access/' . trim($guest_access_id), $payload);
} }
@@ -1608,7 +1592,7 @@ class Client
*/ */
public function set_site_ntp($ntp_id, $payload) public function set_site_ntp($ntp_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/ntp/' . trim($ntp_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/ntp/' . trim($ntp_id), $payload);
} }
@@ -1624,7 +1608,7 @@ class Client
*/ */
public function set_site_connectivity($connectivity_id, $payload) public function set_site_connectivity($connectivity_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/connectivity/' . trim($connectivity_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/setting/connectivity/' . trim($connectivity_id), $payload);
} }
@@ -1812,10 +1796,9 @@ class Client
*/ */
public function stat_full_status() public function stat_full_status()
{ {
$initial_fetch = $this->fetch_results_boolean('/status', null, false); $this->fetch_results_boolean('/status', null, false);
$mappings = $this->get_last_results_raw();
return json_decode($mappings); return json_decode($this->get_last_results_raw());
} }
/** /**
@@ -1828,10 +1811,9 @@ class Client
*/ */
public function list_device_name_mappings() public function list_device_name_mappings()
{ {
$initial_fetch = $this->fetch_results_boolean('/dl/firmware/bundles.json', null, false); $this->fetch_results_boolean('/dl/firmware/bundles.json', null, false);
$mappings = $this->get_last_results_raw();
return json_decode($mappings); return json_decode($this->get_last_results_raw());
} }
/** /**
@@ -1906,7 +1888,7 @@ class Client
* @param int $minutes minutes the voucher is valid after activation (expiration time) * @param int $minutes minutes the voucher is valid after activation (expiration time)
* @param int $count number of vouchers to create, default value is 1 * @param int $count number of vouchers to create, default value is 1
* @param int $quota single-use or multi-use vouchers, value '0' is for multi-use, '1' is for single-use, * @param int $quota single-use or multi-use vouchers, value '0' is for multi-use, '1' is for single-use,
* 'n' is for multi-use n times * 'n' is for multi-use n times
* @param string $note note text to add to voucher when printing * @param string $note note text to add to voucher when printing
* @param int $up upload speed limit in kbps * @param int $up upload speed limit in kbps
* @param int $down download speed limit in kbps * @param int $down download speed limit in kbps
@@ -2162,8 +2144,8 @@ class Client
return false; return false;
} }
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = ['disabled' => $disable]; $payload = ['disabled' => $disable];
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($ap_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($ap_id), $payload);
} }
@@ -2186,8 +2168,8 @@ class Client
return false; return false;
} }
$this->request_method = 'PUT'; $this->method = 'PUT';
$payload = ['led_override' => $override_mode]; $payload = ['led_override' => $override_mode];
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($device_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($device_id), $payload);
} }
@@ -2320,7 +2302,7 @@ class Client
'_id' => $section_id '_id' => $section_id
]; ];
return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/guest_access', $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/guest_access/' . $section_id, $payload);
} }
/** /**
@@ -2330,9 +2312,13 @@ class Client
* object/array structured in the same manner as is returned by list_settings() for the "guest_access" section. * object/array structured in the same manner as is returned by list_settings() for the "guest_access" section.
* @return bool true on success * @return bool true on success
*/ */
public function set_guestlogin_settings_base($payload) public function set_guestlogin_settings_base($payload, $section_id = '')
{ {
return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/guest_access', $payload); if (!empty($section_id)) {
$section_id = '/' . $section_id;
}
return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/guest_access' . $section_id, $payload);
} }
/** /**
@@ -2462,7 +2448,7 @@ class Client
*/ */
public function set_dynamicdns($dynamicdns_id, $payload) public function set_dynamicdns($dynamicdns_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/dynamicdns/' . trim($dynamicdns_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/dynamicdns/' . trim($dynamicdns_id), $payload);
} }
@@ -2500,7 +2486,7 @@ class Client
*/ */
public function set_networksettings_base($network_id, $payload) public function set_networksettings_base($network_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/networkconf/' . trim($network_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/networkconf/' . trim($network_id), $payload);
} }
@@ -2513,7 +2499,7 @@ class Client
*/ */
public function delete_network($network_id) public function delete_network($network_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/networkconf/' . trim($network_id)); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/networkconf/' . trim($network_id));
} }
@@ -2611,7 +2597,7 @@ class Client
*/ */
public function set_wlansettings_base($wlan_id, $payload) public function set_wlansettings_base($wlan_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/wlanconf/' . trim($wlan_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/wlanconf/' . trim($wlan_id), $payload);
} }
@@ -2664,7 +2650,7 @@ class Client
*/ */
public function delete_wlan($wlan_id) public function delete_wlan($wlan_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/wlanconf/' . trim($wlan_id)); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/wlanconf/' . trim($wlan_id));
} }
@@ -2926,7 +2912,7 @@ class Client
*/ */
public function set_device_settings_base($device_id, $payload) public function set_device_settings_base($device_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($device_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/device/' . trim($device_id), $payload);
} }
@@ -3033,7 +3019,7 @@ class Client
*/ */
public function set_radius_account_base($account_id, $payload) public function set_radius_account_base($account_id, $payload)
{ {
$this->request_method = 'PUT'; $this->method = 'PUT';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/account/' . trim($account_id), $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/account/' . trim($account_id), $payload);
} }
@@ -3049,7 +3035,7 @@ class Client
*/ */
public function delete_radius_account($account_id) public function delete_radius_account($account_id)
{ {
$this->request_method = 'DELETE'; $this->method = 'DELETE';
return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/account/' . trim($account_id)); return $this->fetch_results_boolean('/api/s/' . $this->site . '/rest/account/' . trim($account_id));
} }
@@ -3073,7 +3059,7 @@ class Client
} }
/** /**
* Toggle Element Adoption ON or OFF * Toggle Element Adoption ON or OFF
* *
* @param bool $enable true enables Element Adoption, false disables Element Adoption * @param bool $enable true enables Element Adoption, false disables Element Adoption
* @return bool true on success * @return bool true on success
@@ -3089,6 +3075,32 @@ class Client
return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/element_adopt', $payload); return $this->fetch_results_boolean('/api/s/' . $this->site . '/set/setting/element_adopt', $payload);
} }
/**
* List device states
*
* NOTE:
* this function returns a partial implementation of the codes listed here
* https://help.ui.com/hc/en-us/articles/205231710-UniFi-UAP-Status-Meaning-Definitions
*
* @return array containing translations of UniFi device "state" values to humanized form
*/
public function list_device_states()
{
$device_states = [
0 => 'offline',
1 => 'connected',
2 => 'pending adoption',
4 => 'updating',
5 => 'provisioning',
6 => 'unreachable',
7 => 'adopting',
9 => 'adoption error',
11 => 'isolated'
];
return $device_states;
}
/** /**
* Custom API request * Custom API request
* *
@@ -3096,15 +3108,15 @@ class Client
* Only use this method when you fully understand the behavior of the UniFi controller API. No input validation is performed, to be used with care! * Only use this method when you fully understand the behavior of the UniFi controller API. No input validation is performed, to be used with care!
* *
* @param string $path suffix of the URL (following the port number) to pass request to, *must* start with a "/" character * @param string $path suffix of the URL (following the port number) to pass request to, *must* start with a "/" character
* @param string $request_method optional, HTTP request type, can be GET (default), POST, PUT, PATCH, or DELETE * @param string $method optional, HTTP request type, can be GET (default), POST, PUT, PATCH, or DELETE
* @param object|array $payload optional, stdClass object or associative array containing the payload to pass * @param object|array $payload optional, stdClass object or associative array containing the payload to pass
* @param string $return optional, string; determines how to return results, when "boolean" the method must return a * @param string $return optional, string; determines how to return results, when "boolean" the method must return a
* boolean result (true/false) or "array" when the method must return an array * boolean result (true/false) or "array" when the method must return an array
* @return bool|array returns results as requested, returns false on incorrect parameters * @return bool|array returns results as requested, returns false on incorrect parameters
*/ */
public function custom_api_request($path, $request_method = 'GET', $payload = null, $return = 'array') public function custom_api_request($path, $method = 'GET', $payload = null, $return = 'array')
{ {
if (!in_array($request_method, $this->request_methods_allowed)) { if (!in_array($method, $this->methods_allowed)) {
return false; return false;
} }
@@ -3112,7 +3124,7 @@ class Client
return false; return false;
} }
$this->request_method = $request_method; $this->method = $method;
if ($return === 'array') { if ($return === 'array') {
return $this->fetch_results($path, $payload); return $this->fetch_results($path, $payload);
@@ -3354,10 +3366,6 @@ class Client
return $this->class_version; return $this->class_version;
} }
/******************************************************************
* other getter/setter functions/methods from here, use with care!
******************************************************************/
/** /**
* Set value for the private property $cookies * Set value for the private property $cookies
* *
@@ -3373,25 +3381,25 @@ class Client
* *
* @return string request type * @return string request type
*/ */
public function get_request_method() public function get_method()
{ {
return $this->request_method; return $this->method;
} }
/** /**
* Set request method * Set request method
* *
* @param string $request_method a valid HTTP request method * @param string $method a valid HTTP request method
* @return bool whether request was successful or not * @return bool whether request was successful or not
*/ */
public function set_request_method($request_method) public function set_method($method)
{ {
if (!in_array($request_method, $this->request_methods_allowed)) { if (!in_array($method, $this->methods_allowed)) {
return false; return false;
} }
$this->request_method = $request_method; $this->method = $method;
return true; return true;
} }
@@ -3401,11 +3409,11 @@ class Client
* *
* https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html * https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html
* *
* @return bool value of private property $curl_ssl_verify_peer (cURL option CURLOPT_SSL_VERIFYPEER) * @return bool value of private property $ssl_verify_peer (cURL option CURLOPT_SSL_VERIFYPEER)
*/ */
public function get_ssl_verify_peer() public function get_ssl_verify_peer()
{ {
return $this->curl_ssl_verify_peer; return $this->ssl_verify_peer;
} }
/** /**
@@ -3421,7 +3429,7 @@ class Client
return false; return false;
} }
$this->curl_ssl_verify_peer = $ssl_verify_peer; $this->ssl_verify_peer = $ssl_verify_peer;
return true; return true;
} }
@@ -3431,11 +3439,11 @@ class Client
* *
* https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html * https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
* *
* @return bool value of private property $curl_ssl_verify_peer (cURL option CURLOPT_SSL_VERIFYHOST) * @return bool value of private property $ssl_verify_peer (cURL option CURLOPT_SSL_VERIFYHOST)
*/ */
public function get_ssl_verify_host() public function get_ssl_verify_host()
{ {
return $this->curl_ssl_verify_host; return $this->ssl_verify_host;
} }
/** /**
@@ -3451,7 +3459,7 @@ class Client
return false; return false;
} }
$this->curl_ssl_verify_host = $ssl_verify_host; $this->ssl_verify_host = $ssl_verify_host;
return true; return true;
} }
@@ -3504,7 +3512,7 @@ class Client
} }
/**************************************************************** /****************************************************************
* internal (private and protected) functions from here: * private and protected functions from here:
****************************************************************/ ****************************************************************/
/** /**
@@ -3522,7 +3530,7 @@ class Client
protected function fetch_results($path, $payload = null, $boolean = false, $login_required = true) protected function fetch_results($path, $payload = null, $boolean = false, $login_required = true)
{ {
/** /**
* guard clause to check if we are logged in when needed * guard clause to check if logged in when needed
*/ */
if ($login_required && !$this->is_loggedin) { if ($login_required && !$this->is_loggedin) {
return false; return false;
@@ -3544,7 +3552,7 @@ class Client
return true; return true;
} elseif ($response->meta->rc === 'error') { } elseif ($response->meta->rc === 'error') {
/** /**
* we have an error: * 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
*/ */
if (isset($response->meta->msg)) { if (isset($response->meta->msg)) {
@@ -3567,9 +3575,11 @@ class Client
trigger_error('Debug: Last error message: ' . $this->last_error_message); trigger_error('Debug: Last error message: ' . $this->last_error_message);
} }
} }
} else {
return $response; return false;
} }
return $response;
} }
} }
@@ -3596,12 +3606,12 @@ class Client
* *
* @return bool returns true upon success, false upon failure * @return bool returns true upon success, false upon failure
*/ */
private function catch_json_last_error() protected function catch_json_last_error()
{ {
if ($this->debug) { if ($this->debug) {
switch (json_last_error()) { switch (json_last_error()) {
case JSON_ERROR_NONE: case JSON_ERROR_NONE:
// JSON is valid, no error has occurred and we return true early // JSON is valid, no error has occurred and return true early
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';
@@ -3639,7 +3649,7 @@ class Client
$error = 'Malformed UTF-16 characters, possibly incorrectly encoded'; $error = 'Malformed UTF-16 characters, possibly incorrectly encoded';
break; break;
default: default:
// we have an unknown error // an unknown error occurred
$error = 'Unknown JSON error occurred'; $error = 'Unknown JSON error occurred';
break; break;
} }
@@ -3658,7 +3668,7 @@ class Client
* @param string $baseurl the base URL to validate * @param string $baseurl the base URL to validate
* @return bool true if base URL is a valid URL, else returns false * @return bool true if base URL is a valid URL, else returns false
*/ */
private function check_base_url($baseurl) protected function check_base_url($baseurl)
{ {
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!');
@@ -3675,7 +3685,7 @@ class Client
* @param string $site the (short) site name to check * @param string $site the (short) site name to check
* @return bool true if (short) site name is valid, else returns false * @return bool true if (short) site name is valid, else returns false
*/ */
private function check_site($site) protected function check_site($site)
{ {
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');
@@ -3686,21 +3696,18 @@ class Client
return true; return true;
} }
/**
*/
/** /**
* Update the unificookie if sessions are enabled * Update the unificookie if sessions are enabled
* *
* @return bool true when unificookie was updated, else returns false * @return bool true when unificookie was updated, else returns false
*/ */
private function update_unificookie() protected function update_unificookie()
{ {
if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['unificookie']) && !empty($_SESSION['unificookie'])) { if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION['unificookie']) && !empty($_SESSION['unificookie'])) {
$this->cookies = $_SESSION['unificookie']; $this->cookies = $_SESSION['unificookie'];
/** /**
* if we have a JWT in our cookie we know we're dealing with a UniFi OS controller * if the cookie contains a JWT this is a UniFi OS controller
*/ */
if (strpos($this->cookies, 'TOKEN') !== false) { if (strpos($this->cookies, 'TOKEN') !== false) {
$this->is_unifi_os = true; $this->is_unifi_os = true;
@@ -3713,46 +3720,76 @@ class Client
} }
/** /**
* Add a header containing the CSRF token from our Cookie string * Add a cURL header containing the CSRF token from the TOKEN in our Cookie string
* *
* @return bool true upon success or false when unable to extract the CSRF token * @return bool true upon success or false when unable to extract the CSRF token
*/ */
private function create_x_csrf_token_header() protected function create_x_csrf_token_header()
{ {
if (!empty($this->cookies)) { if (!empty($this->cookies) && strpos($this->cookies, 'TOKEN') !== false) {
$cookie_bits = explode('=', $this->cookies); $cookie_bits = explode('=', $this->cookies);
if (!empty($cookie_bits) && array_key_exists(1, $cookie_bits)) { if (empty($cookie_bits) || !array_key_exists(1, $cookie_bits)) {
$jwt = $cookie_bits[1]; return;
} else {
return false;
} }
$jwt_components = explode('.', $jwt); $jwt_components = explode('.', $cookie_bits[1]);
if (!empty($jwt_components) && array_key_exists(1, $jwt_components)) { if (empty($jwt_components) || !array_key_exists(1, $jwt_components)) {
$jwt_payload = $jwt_components[1]; return;
} else {
return false;
} }
$this->headers[] = 'x-csrf-token: ' . json_decode(base64_decode($jwt_payload))->csrfToken; $this->headers[] = 'x-csrf-token: ' . json_decode(base64_decode($jwt_components[1]))->csrfToken;
}
}
return true; /**
* Callback function for cURL to extract and store cookies as needed
*
* @param object|resource $ch the cURL instance
* @param int $header_line the response header line number
* @return int length of the header line
*/
protected function response_header_callback($ch, $header_line) {
if (strpos($header_line, 'unifises') !== false || strpos($header_line, 'TOKEN') !== false) {
$cookie = trim(str_replace(['set-cookie: ', 'Set-Cookie: '], '', $header_line));
if (!empty($cookie)) {
$cookie_crumbs = explode(';', $cookie);
foreach ($cookie_crumbs as $cookie_crumb) {
if (strpos($cookie_crumb, 'unifises') !== false) {
$this->cookies = $cookie_crumb;
$this->is_loggedin = true;
$this->is_unifi_os = false;
break;
}
if (strpos($cookie_crumb, 'TOKEN') !== false) {
$this->cookies = $cookie_crumb;
$this->is_loggedin = true;
$this->is_unifi_os = true;
break;
}
}
}
} }
return false; return strlen($header_line);
} }
/** /**
* Execute the cURL request * Execute the cURL request
* *
* @param string $path path for the request * @param string $path path for the request
* @param object|array $payload optional, payload to pass with the request * @param object|array $payload optional, payload to pass with the request
* @return bool|array|string response returned by the controller API, false upon error * @return bool|array|string response returned by the controller API, false upon error
*/ */
protected function exec_curl($path, $payload = null) protected function exec_curl($path, $payload = null)
{ {
if (!in_array($this->request_method, $this->request_methods_allowed)) { if (!in_array($this->method, $this->methods_allowed)) {
trigger_error('an invalid HTTP request type was used: ' . $this->request_method); trigger_error('an invalid HTTP request type was used: ' . $this->method);
return false;
} }
if (!($ch = $this->get_curl_resource())) { if (!($ch = $this->get_curl_resource())) {
@@ -3761,46 +3798,45 @@ class Client
return false; return false;
} }
$json_payload = ''; $this->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;
} else {
$url = $this->baseurl . $path;
} }
/**
* prepare cURL options
*/
$curl_options = [ $curl_options = [
CURLOPT_URL => $url CURLOPT_URL => $url
]; ];
/** /**
* what we do when a payload is passed * when a payload is passed
*/ */
if (!is_null($payload)) { $json_payload = '';
$json_payload = json_encode($payload, JSON_UNESCAPED_SLASHES); if (!empty($payload)) {
$curl_options[CURLOPT_POST] = true; $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->headers = [ $this->headers = [
'content-type: application/json', 'content-type: application/json',
'content-length: ' . strlen($json_payload) 'Expect:'
]; ];
/** /**
* we shouldn't be using 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,
* switch to POST instead * switch to POST instead
*/ */
if ($this->request_method === 'GET' || $this->request_method === 'DELETE') { if ($this->method === 'GET' || $this->method === 'DELETE') {
$this->request_method = 'POST'; $this->method = 'POST';
} }
} }
switch ($this->request_method) { switch ($this->method) {
case 'POST': case 'POST':
$curl_options[CURLOPT_CUSTOMREQUEST] = 'POST'; $curl_options[CURLOPT_POST] = true;
break; break;
case 'DELETE': case 'DELETE':
$curl_options[CURLOPT_CUSTOMREQUEST] = 'DELETE'; $curl_options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
@@ -3813,7 +3849,7 @@ class Client
break; break;
} }
if ($this->is_unifi_os && $this->request_method !== 'GET') { if ($this->is_unifi_os && $this->method !== 'GET') {
$this->create_x_csrf_token_header(); $this->create_x_csrf_token_header();
} }
@@ -3838,7 +3874,7 @@ class Client
/** /**
* an HTTP response code 401 (Unauthorized) indicates the Cookie/Token has expired in which case * an HTTP response code 401 (Unauthorized) indicates the Cookie/Token has expired in which case
* we need to login again. * re-login is required
*/ */
if ($http_code == 401) { if ($http_code == 401) {
if ($this->debug) { if ($this->debug) {
@@ -3854,6 +3890,7 @@ class Client
} }
$this->is_loggedin = false; $this->is_loggedin = false;
$this->cookies = '';
$this->exec_retries++; $this->exec_retries++;
curl_close($ch); curl_close($ch);
@@ -3889,10 +3926,9 @@ class Client
print $url . PHP_EOL; print $url . PHP_EOL;
if (empty($json_payload)) { if (empty($json_payload)) {
print 'empty payload'; print 'empty payload';
} else {
print $json_payload;
} }
print $json_payload;
print PHP_EOL . '----------RESPONSE-----------' . PHP_EOL; print PHP_EOL . '----------RESPONSE-----------' . PHP_EOL;
print $response; print $response;
print PHP_EOL . '-----------------------------' . PHP_EOL; print PHP_EOL . '-----------------------------' . PHP_EOL;
@@ -3902,9 +3938,9 @@ class Client
curl_close($ch); curl_close($ch);
/** /**
* set request_method value back to default, just in case * set method back to default value, just in case
*/ */
$this->request_method = 'GET'; $this->method = 'GET';
return $response; return $response;
} }
@@ -3920,11 +3956,12 @@ class Client
if (is_object($ch) || is_resource($ch)) { if (is_object($ch) || is_resource($ch)) {
$curl_options = [ $curl_options = [
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
CURLOPT_SSL_VERIFYPEER => $this->curl_ssl_verify_peer, CURLOPT_SSL_VERIFYPEER => $this->ssl_verify_peer,
CURLOPT_SSL_VERIFYHOST => $this->curl_ssl_verify_host, CURLOPT_SSL_VERIFYHOST => $this->ssl_verify_host,
CURLOPT_CONNECTTIMEOUT => $this->connect_timeout, CURLOPT_CONNECTTIMEOUT => $this->connect_timeout,
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '', CURLOPT_ENCODING => '',
CURLOPT_HEADERFUNCTION => [$this, 'response_header_callback'],
]; ];
if ($this->debug) { if ($this->debug) {