Compare commits

...

6 Commits

Author SHA1 Message Date
malle-pietje
17d895076f API client class v1.1.47
- applied patches to update_switch_poe-mode.php as suggested by scrutinizer-ci.com
- code clean up and improvements in preparation of support for UniFi OS-based controllers
- added support for UniFi OS-based controllers, thanks to @Scyto for providing access
- adapt login route and method, and base URL for UniFi OS-based controllers
- automatically identify UniFi OS-based controllers, thanks to @TwitchCaptain for this and several other suggestions
- relaxed URL validation to allow UniFi OS-based controllers to pass
- changed default HTTP method to GET
- many improvements throughout the code
2020-02-06 08:52:49 +01:00
malle-pietje
961d692125 - added support for UniFi OS-based controllers 2020-02-04 14:42:03 +01:00
malle-pietje
796ad9a82d code cleanup and improvements in preparation of support for UniFi OS-based controllers 2020-02-04 07:41:43 +01:00
malle-pietje
a3fc0732e2 API client class v1.1.46
- applied patches to update_switch_poe-mode.php as suggested by scrutinizer-ci.com
- placed warning that UniFI OS is not (yet) supported
2020-01-31 12:14:52 +01:00
malle-pietje
ca25c8ab52 re-added PHP_EOL in several places 2020-01-30 10:14:55 +01:00
malle-pietje
6754eb5041 API client class v1.1.45
- added function/method force_provision(), contributed by @VWT-Dan
- added example update_switch_poe-mode.php, contributed by @Kaltt
2020-01-30 10:09:34 +01:00
4 changed files with 438 additions and 132 deletions

View File

@@ -1,15 +1,21 @@
## UniFi Controller API client class ## UniFi Controller API client class
A PHP class which provides access to Ubiquiti's [**UniFi SDN Controller**](https://unifi-sdn.ui.com/) API, versions 4.X.X and 5.X.X of the UniFi SDN Controller software are supported (version 5.12.35 has been confirmed to work). It's a standalone version of the class which is used in our API browser tool which can be found [here](https://github.com/Art-of-WiFi/UniFi-API-browser). A PHP class that provides access to Ubiquiti's [**UniFi SDN Controller**](https://unifi-sdn.ui.com/) API, versions 4.X.X and 5.X.X of the UniFi SDN Controller software are supported (version 5.12.35 has been confirmed to work) as well as UniFi OS-based controllers (version 5.12.59 has been confirmed to work). This class is used in our API browser tool which can be found [here](https://github.com/Art-of-WiFi/UniFi-API-browser).
This class can be installed manually or using composer/[packagist](https://packagist.org/packages/art-of-wifi/unifi-api-client) for easy inclusion in your projects. The package can be installed manually or using composer/[packagist](https://packagist.org/packages/art-of-wifi/unifi-api-client) for easy inclusion in your projects.
## Requirements ## Requirements
- a web server with PHP and cURL modules installed (tested on Apache 2.4 with PHP Version 5.6.1 and cURL 7.42.1 and with PHP 7.2.24 and cURL 7.58.0) - a web server with PHP and cURL modules installed (tested on Apache 2.4 with PHP Version 5.6.1 and cURL 7.42.1 and with PHP 7.2.24 and cURL 7.58.0)
- network connectivity between this web server and the server and port (normally TCP port 8443) where the UniFi Controller is running - network connectivity between this web server and the server and port (normally TCP port 8443) where the UniFi Controller is running
## Installation ## ## UniFi OS Support
Support for UniFi OS-based controllers (UniFi Dream Machine Pro) has been added as of version 1.1.47. The class automatically detects UniFi OS devices and adjusts URLs and several functions/methods accordingly. If your own code applies strict validation of the URL that is passed to the constructor, please adapt your logic to allow URLs without a port suffix when dealing with a UniFi OS-based controller.
Please test all methods you plan on using thoroughly before using the API Client with UniFi OS devices in a production environment.
## Installation
You can use [Composer](#composer), [Git](#git) or simply [Download the Release](#download-the-release) to install the API client class. You can use [Composer](#composer), [Git](#git) or simply [Download the Release](#download-the-release) to install the API client class.

View File

@@ -15,8 +15,9 @@
*/ */
$controlleruser = ''; // the user name for access to the UniFi Controller $controlleruser = ''; // the user name for access to the UniFi Controller
$controllerpassword = ''; // the password for access to the UniFi Controller $controllerpassword = ''; // the password for access to the UniFi Controller
$controllerurl = ''; // full url to the UniFi Controller, eg. 'https://22.22.11.11:8443' $controllerurl = ''; // full url to the UniFi Controller, eg. 'https://22.22.11.11:8443', for UniFi OS-based
$controllerversion = ''; // the version of the Controller software, eg. '4.6.6' (must be at least 4.0.0) // controllers a port suffix isn't required, no trailing slashes should be added
$controllerversion = ''; // the version of the Controller software, e.g. '4.6.6' (must be at least 4.0.0)
/** /**
* set to true (without quotes) to enable debug output to the browser and the PHP error log * set to true (without quotes) to enable debug output to the browser and the PHP error log

View File

@@ -0,0 +1,94 @@
<?php
/**
* PHP API usage example to turn the PoE of the selected switch ports to "off" or "auto"
*
* contributed by: @Kaltt
* description: A use case for this script is to turn off the PoE of the port where a camera is connected in order to turn off the camera
*
* usage: If the file is called via a web URL, it should be called like: update_switch_poe-mode.php?poe_mode=off
* If the file is called via the command line, it should be called like: php update_switch_poe-mode.php off
* The values can be "off" or "auto"
*/
/**
* using the composer autoloader
*/
require_once('vendor/autoload.php');
/**
* include the config file (place your credentials etc. there if not already present)
* see the config.template.php file for an example
*/
require_once('config.php');
/**
* the site to use to log in to the controller
*/
$site_id = '<short site name of a site the credentials used have access to>';
/**
* the MAC address of the AC-IW device to re-configure
*/
$device_mac = '<enter MAC address>';
/**
* $lanports is an array that defines which ports should be changed
*/
$lanports = [6];
/**
* This is the function that reads out the current port configuration and changes the value for the poe_mode for the ports defined in $lanports
*/
function update_ports($running_config, $ports, $poe_mode){
/**
* Update already non-default ports
*/
$running_config_count = count($running_config);
for($i = 0; $i < $running_config_count; $i++){
if(in_array($running_config[$i]->port_idx, $ports)){
$running_config[$i]->poe_mode = $poe_mode;
unset($ports[array_search($running_config[$i]->port_idx, $ports)]);
}
}
$add_conf = [];
foreach($ports as $port){
$add_conf[] = [
'port_idx' => $port,
'poe_mode' => $poe_mode
];
}
return array_merge($running_config, $add_conf);
}
$unifi_connection = new UniFi_API\Client($controlleruser, $controllerpassword, $controllerurl, $site_id, $controllerversion, false);
$set_debug_mode = $unifi_connection->set_debug(false);
$loginresults = $unifi_connection->login();
$data = $unifi_connection->list_devices($device_mac);
$device_id = $data[0]->device_id;
$current_conf = $data[0]->port_overrides;
/**
* This reads in the values provided via URL or in the command line, if nothing is set than it will poe_mode will be set to "auto"
*/
if (isset($_GET['poe_mode'])) {
$poe_mode = $_GET['poe_mode'];
} elseif (isset($argv[1])) {
$poe_mode = $argv[1];
} else {
$poe_mode = 'auto';
}
$new_ports_config = [
'port_overrides' => update_ports($current_conf, $lanports, $poe_mode)
];
$update_device = $unifi_connection->set_device_settings_base($device_id, $new_ports_config);
if (!$update_device) {
$error = $unifi_connection->get_last_results_raw();
echo json_encode($error, JSON_PRETTY_PRINT);
}
echo json_encode($update_device, JSON_PRETTY_PRINT);

View File

@@ -2,7 +2,7 @@
/** /**
* This file is part of the art-of-wifi/unifi-api-client package * This file is part of the art-of-wifi/unifi-api-client package
* *
* This UniFi API client is based on the work done by the following developers: * This UniFi API client Class is based on the work done by the following developers:
* domwo: http://community.ubnt.com/t5/UniFi-Wireless/little-php-class-for-unifi-api/m-p/603051 * domwo: http://community.ubnt.com/t5/UniFi-Wireless/little-php-class-for-unifi-api/m-p/603051
* fbagnol: https://github.com/fbagnol/class.unifi.php * fbagnol: https://github.com/fbagnol/class.unifi.php
* and the API as published by Ubiquiti: * and the API as published by Ubiquiti:
@@ -17,40 +17,44 @@
namespace UniFi_API; namespace UniFi_API;
/** /**
* the UniFi API client class * the UniFi API client Class
*/ */
class Client class Client
{ {
/** /**
* private properties * private and protected properties
*/ */
protected $baseurl = 'https://127.0.0.1:8443'; protected $baseurl = 'https://127.0.0.1:8443';
protected $user = ''; protected $user = '';
protected $password = ''; protected $password = '';
protected $site = 'default'; protected $site = 'default';
protected $version = '5.6.39'; protected $version = '5.6.39';
protected $debug = false; protected $debug = false;
protected $is_loggedin = false; protected $is_loggedin = false;
private $cookies = ''; protected $is_unifi_os = false;
private $request_type = 'POST'; private $cookies = '';
private $connect_timeout = 10; private $request_type = 'GET';
private $last_results_raw = null; private $request_types_allowed = ['GET', 'POST', 'PUT', 'DELETE'];
private $last_error_message = null; private $connect_timeout = 10;
private $curl_ssl_verify_peer = false; private $last_results_raw = null;
private $curl_ssl_verify_host = false; private $last_error_message = null;
private $curl_ssl_verify_peer = false;
private $curl_ssl_verify_host = false;
/** /**
* Construct an instance of the UniFi API client class * Construct an instance of the UniFi API client Class
* --------------------------------------------------- * ---------------------------------------------------
* return a new class instance * return a new class instance
* required parameter <user> = string; user name to use when connecting to the UniFi controller * required parameter <user> = string; user name to use when connecting to the UniFi controller
* required parameter <password> = string; password to use when connecting to the UniFi controller * required parameter <password> = string; password to use when connecting to the UniFi controller
* optional parameter <baseurl> = string; base URL of the UniFi controller, *must* include "https://" prefix and port suffix (:8443) * optional parameter <baseurl> = string; base URL of the UniFi controller which *must* include "https://" prefix,
* a port suffix (e.g. :8443) is required for non-UniFi OS controllers,
* do not add trailing slashes
* optional parameter <site> = string; short site name to access, defaults to "default" * optional parameter <site> = string; short site name to access, defaults to "default"
* optional parameter <version> = string; the version number of the controller, defaults to "5.4.16" * optional parameter <version> = string; the version number of the controller, defaults to "5.4.16"
* optional parameter <ssl_verify> = boolean; whether to validate the controller's SSL certificate or not, a value of true is * optional parameter <ssl_verify> = boolean; whether to validate the controller's SSL certificate or not, a value of true is
* recommended for production environments to prevent potential MitM attacks, default value (false) * recommended for production environments to prevent potential MitM attacks, default value (false)
* is to not validate the controller certificate * disables validation of the controller certificate
*/ */
public function __construct($user, $password, $baseurl = '', $site = '', $version = '', $ssl_verify = false) public function __construct($user, $password, $baseurl = '', $site = '', $version = '', $ssl_verify = false)
{ {
@@ -73,7 +77,7 @@ class Client
$this->version = trim($version); $this->version = trim($version);
} }
if ($ssl_verify === true) { if ((boolean)$ssl_verify === true) {
$this->curl_ssl_verify_peer = true; $this->curl_ssl_verify_peer = true;
$this->curl_ssl_verify_host = 2; $this->curl_ssl_verify_host = 2;
} }
@@ -83,10 +87,18 @@ class Client
$this->update_unificookie(); $this->update_unificookie();
} }
/**
* This method is be called as soon as there are no other references to the Class instance
* https://www.php.net/manual/en/language.oop5.decon.php
*
* NOTES:
* to force the Class instance to log out automatically upon destruct, simply call logout() or unset
* $_SESSION['unificookie'] at the end of your code
*/
public function __destruct() public function __destruct()
{ {
/** /**
* if user has $_SESSION['unificookie'] set, do not logout here * if $_SESSION['unificookie'] is set, do not logout here
*/ */
if (isset($_SESSION['unificookie'])) { if (isset($_SESSION['unificookie'])) {
return; return;
@@ -101,70 +113,102 @@ class Client
} }
/** /**
* Login to UniFi Controller * Login to the UniFi controller
* ------------------------- * -----------------------------
* returns true upon success * returns true upon success
*/ */
public function login() public function login()
{ {
/** /**
* if $_SESSION['unificookie'] is set, skip the login * if already logged in we skip the login process
*/ */
if (isset($_SESSION['unificookie'])) { if ($this->is_loggedin === true) {
return $this->is_loggedin = true; return true;
} }
/**
* check we have a "regular" controller or one based on UniFi OS
*/
if (!is_resource($ch = $this->get_curl_resource())) { if (!is_resource($ch = $this->get_curl_resource())) {
trigger_error('$ch as returned by get_curl_resource() is not a resource'); trigger_error('$ch as returned by get_curl_resource() is not a resource');
} else { } else {
curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_REFERER, $this->baseurl . '/login'); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/api/login'); curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['username' => $this->user, 'password' => $this->password])); curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/');
/** /**
* execute the cURL request * execute the cURL request and get the HTTP response code
*/ */
$content = curl_exec($ch); $content = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
trigger_error('cURL error: ' . curl_error($ch));
}
if ($http_code === 200) {
$this->is_unifi_os = true;
}
if ($this->is_unifi_os) {
curl_setopt($ch, CURLOPT_REFERER, $this->baseurl . '/login');
curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/api/auth/login');
} else {
curl_setopt($ch, CURLOPT_REFERER, $this->baseurl . '/login');
curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/api/login');
}
curl_setopt($ch, CURLOPT_NOBODY, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['username' => $this->user, 'password' => $this->password]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['content-type: application/json; charset=utf-8']);
/**
* execute the cURL request and get the HTTP response code
*/
$content = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) { if (curl_errno($ch)) {
trigger_error('cURL error: ' . curl_error($ch)); trigger_error('cURL error: ' . curl_error($ch));
} }
if ($this->debug) { if ($this->debug) {
curl_setopt($ch, CURLOPT_VERBOSE, true); print PHP_EOL . '<pre>';
print '<pre>';
print PHP_EOL . '-----------LOGIN-------------' . PHP_EOL; print PHP_EOL . '-----------LOGIN-------------' . PHP_EOL;
print_r(curl_getinfo($ch)); print_r(curl_getinfo($ch));
print PHP_EOL . '----------RESPONSE-----------' . PHP_EOL; print PHP_EOL . '----------RESPONSE-----------' . PHP_EOL;
print $content; print $content;
print PHP_EOL . '-----------------------------' . PHP_EOL; print PHP_EOL . '-----------------------------' . PHP_EOL;
print '</pre>'; print '</pre>' . PHP_EOL;
}
/**
* based on the HTTP response code we either trigger an error or
* extract the cookie from the headers
*/
if ($http_code === 400) {
trigger_error('We received an HTTP response status: 400. Probably a controller login failure');
return $http_code;
} }
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($content, 0, $header_size); $headers = substr($content, 0, $header_size);
$body = trim(substr($content, $header_size)); $body = trim(substr($content, $header_size));
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
preg_match_all('|Set-Cookie: (.*);|Ui', $headers, $results); if ($http_code >= 200 && $http_code < 400 && !empty($body)) {
preg_match_all('|Set-Cookie: (.*);|Ui', $headers, $results);
if (isset($results[1])) {
$this->cookies = implode(';', $results[1]);
if (isset($results[1])) { /**
$this->cookies = implode(';', $results[1]); * accept cookies from UniFi OS or from regular UNiFI controllers
if (!empty($body)) { */
if (($http_code >= 200) && ($http_code < 400)) { if (strpos($this->cookies, 'unifises') !== false || strpos($this->cookies, 'TOKEN') !== false) {
if (strpos($this->cookies, 'unifises') !== false) { return $this->is_loggedin = true;
return $this->is_loggedin = true;
}
}
if ($http_code === 400) {
trigger_error('We have received an HTTP response status: 400. Probably a controller login failure');
return $http_code;
} }
} }
} }
@@ -174,8 +218,8 @@ class Client
} }
/** /**
* Logout from UniFi Controller * Logout from the UniFi controller
* ---------------------------- * --------------------------------
* returns true upon success * returns true upon success
*/ */
public function logout() public function logout()
@@ -184,7 +228,14 @@ class Client
return false; return false;
} }
$this->exec_curl('/logout'); if ($this->is_unifi_os) {
$logout_url = '/api/auth/logout';
} else {
$logout_url = '/logout';
}
$this->exec_curl($logout_url, []);
$this->is_loggedin = false; $this->is_loggedin = false;
$this->cookies = ''; $this->cookies = '';
@@ -317,7 +368,8 @@ class Client
* required parameter <macs> = array of client MAC addresses * required parameter <macs> = array of client MAC addresses
* *
* NOTE: * NOTE:
* only supported with controller versions 5.9.X and higher * only supported with controller versions 5.9.X and higher, can be
* slow (up to 5 minutes) on larger controllers
*/ */
public function forget_sta($macs) public function forget_sta($macs)
{ {
@@ -348,7 +400,6 @@ class Client
return false; return false;
} }
$this->request_type = 'POST';
$new_user = ['mac' => strtolower($mac), 'usergroup_id' => $user_group_id]; $new_user = ['mac' => strtolower($mac), 'usergroup_id' => $user_group_id];
if (!is_null($name)) { if (!is_null($name)) {
$new_user['name'] = $name; $new_user['name'] = $name;
@@ -1091,9 +1142,8 @@ class Client
return false; return false;
} }
$this->request_type = 'POST'; $payload = ['name' => $group_name, 'qos_rate_max_down' => intval($group_dn), 'qos_rate_max_up' => intval($group_up)];
$payload = ['name' => $group_name, 'qos_rate_max_down' => intval($group_dn), 'qos_rate_max_up' => intval($group_up)]; $response = $this->exec_curl('/api/s/' . $this->site . '/rest/usergroup', $payload);
$response = $this->exec_curl('/api/s/' . $this->site . '/rest/usergroup', $payload);
return $this->process_response($response); return $this->process_response($response);
} }
@@ -1182,7 +1232,6 @@ class Client
return false; return false;
} }
$this->request_type = 'POST';
$payload = ['name' => $group_name, 'group_type' => $group_type, 'group_members' => $group_members]; $payload = ['name' => $group_name, 'group_type' => $group_type, 'group_members' => $group_members];
$response = $this->exec_curl('/api/s/' . $this->site . '/rest/firewallgroup', $payload); $response = $this->exec_curl('/api/s/' . $this->site . '/rest/firewallgroup', $payload);
@@ -1381,6 +1430,27 @@ class Client
return $this->process_response($response); return $this->process_response($response);
} }
/**
* Generate backup
* ---------------------------
* returns a URL from where the backup file can be downloaded once generated
*
* NOTES:
* this is an experimental function, please do not use unless you know exactly
* what you're doing
*/
public function generate_backup()
{
if (!$this->is_loggedin) {
return false;
}
$payload = ['cmd' => 'backup'];
$response = $this->exec_curl('/api/s/' . $this->site . '/cmd/backup', $payload);
return $this->process_response($response);
}
/** /**
* List auto backups * List auto backups
* --------------------------- * ---------------------------
@@ -1742,12 +1812,8 @@ class Client
* restart devices, default value is false. With versions < 5.9.X this only applies * restart devices, default value is false. With versions < 5.9.X this only applies
* when readonly is true. * when readonly is true.
*/ */
public function assign_existing_admin( public function assign_existing_admin($admin_id, $readonly = false, $device_adopt = false, $device_restart = false)
$admin_id, {
$readonly = false,
$device_adopt = false,
$device_restart = false
) {
if (!$this->is_loggedin) { if (!$this->is_loggedin) {
return false; return false;
} }
@@ -1908,8 +1974,7 @@ class Client
return false; return false;
} }
$this->request_type = 'POST'; $payload = ['name' => $name, 'x_password' => $x_password];
$payload = ['name' => $name, 'x_password' => $x_password];
if (isset($note)) { if (isset($note)) {
$payload['note'] = trim($note); $payload['note'] = trim($note);
} }
@@ -2058,6 +2123,32 @@ class Client
return $this->process_response($response); return $this->process_response($response);
} }
/**
* List filtered DPI stats
* -----------------------
* returns an array of fileterd DPI stats
* optional parameter <type> = whether to returns stats by app or by category, valid values:
* 'by_cat' or 'by_app'
* optional parameter <cat_filter> = an array containing numeric category ids to filter by,
* only to be combined with a "by_app" value for $type
*/
public function list_dpi_stats_filtered($type = 'by_cat', $cat_filter = null)
{
if (!$this->is_loggedin || !in_array($type, ['by_cat', 'by_app'])) {
return false;
}
$payload = ['type' => $type];
if ($cat_filter !== null && $type == 'by_app' && is_array($cat_filter)) {
$payload['cats'] = $cat_filter;
}
$response = $this->exec_curl('/api/s/' . $this->site . '/stat/sitedpi', $payload);
return $this->process_response($response);
}
/** /**
* List current channels * List current channels
* --------------------- * ---------------------
@@ -2219,6 +2310,25 @@ class Client
return $this->process_response_boolean($response); return $this->process_response_boolean($response);
} }
/**
* Force provision of a device
* ---------------------------
* return true on success
* required parameter <mac> = device MAC address
*/
public function force_provision($mac)
{
if (!$this->is_loggedin) {
return false;
}
$payload = ['mac' => strtolower($mac), 'cmd' => 'force-provision'];
$response = $this->exec_curl('/api/s/' . $this->site . '/cmd/devmgr', $payload);
return $this->process_response_boolean($response);
}
/** /**
* Reboot a UniFi CloudKey * Reboot a UniFi CloudKey
* ----------------------- * -----------------------
@@ -2384,23 +2494,23 @@ class Client
* Assign access point to another WLAN group * Assign access point to another WLAN group
* ----------------------------------------- * -----------------------------------------
* return true on success * return true on success
* required parameter <wlantype_id> = string; WLAN type, can be either 'ng' (for WLANs 2G (11n/b/g)) or 'na' (WLANs 5G (11n/a/ac)) * required parameter <type_id> = string; WLAN type, can be either 'ng' (for WLANs 2G (11n/b/g)) or 'na' (WLANs 5G (11n/a/ac))
* required parameter <device_id> = string; _id value of the access point to be modified * required parameter <device_id> = string; _id value of the access point to be modified
* required parameter <wlangroup_id> = string; _id value of the WLAN group to assign device to * required parameter <group_id> = string; _id value of the WLAN group to assign device to
*/ */
public function set_ap_wlangroup($wlantype_id, $device_id, $wlangroup_id) public function set_ap_wlangroup($type_id, $device_id, $group_id)
{ {
if (!$this->is_loggedin) { if (!$this->is_loggedin) {
return false; return false;
} }
if (!in_array($wlantype_id, ['ng', 'na'])) { if (!in_array($type_id, ['ng', 'na'])) {
return false; return false;
} }
$payload = [ $payload = [
'wlan_overrides' => [], 'wlan_overrides' => [],
'wlangroup_id_' . $wlantype_id => $wlangroup_id 'wlangroup_id_' . $type_id => $group_id
]; ];
$response = $this->exec_curl('/api/s/' . $this->site . '/upd/device/' . trim($device_id), $payload); $response = $this->exec_curl('/api/s/' . $this->site . '/upd/device/' . trim($device_id), $payload);
@@ -2434,7 +2544,8 @@ class Client
$expire_number, $expire_number,
$expire_unit, $expire_unit,
$section_id $section_id
) { )
{
if (!$this->is_loggedin) { if (!$this->is_loggedin) {
return false; return false;
} }
@@ -2638,8 +2749,7 @@ class Client
return false; return false;
} }
$this->request_type = 'POST'; $response = $this->exec_curl('/api/s/' . $this->site . '/rest/networkconf', $payload);
$response = $this->exec_curl('/api/s/' . $this->site . '/rest/networkconf', $payload);
return $this->process_response($response); return $this->process_response($response);
} }
@@ -2739,7 +2849,8 @@ class Client
$uapsd_enabled = false, $uapsd_enabled = false,
$schedule_enabled = false, $schedule_enabled = false,
$schedule = [] $schedule = []
) { )
{
if (!$this->is_loggedin) { if (!$this->is_loggedin) {
return false; return false;
} }
@@ -2958,8 +3069,7 @@ class Client
return false; return false;
} }
$this->request_type = 'POST'; $payload = ['cmd' => 'archive-all-alarms'];
$payload = ['cmd' => 'archive-all-alarms'];
if (!is_null($alarm_id)) { if (!is_null($alarm_id)) {
$payload = ['_id' => $alarm_id, 'cmd' => 'archive-alarm']; $payload = ['_id' => $alarm_id, 'cmd' => 'archive-alarm'];
} }
@@ -3219,7 +3329,6 @@ class Client
return false; return false;
} }
$this->request_type = 'POST';
$payload = [ $payload = [
'name' => $name, 'name' => $name,
'x_password' => $x_password, 'x_password' => $x_password,
@@ -3281,14 +3390,11 @@ class Client
} }
/** /**
* Execute specific command * Execute specific stats command
* ------------------------ * ------------------------------
* return true on success * return true on success
* required parameter <command> = string; command to execute, known valid values * required parameter <command> = string; command to execute, known valid values
* 'reset-dpi': reset all DPI counters for the current site * 'reset-dpi': reset all DPI counters for the current site
*
* NOTE:
* the provided <command> parameter isn't validated so make sure you're using a correct value
*/ */
public function cmd_stat($command) public function cmd_stat($command)
{ {
@@ -3296,6 +3402,10 @@ class Client
return false; return false;
} }
if (!in_array($command, ['reset-dpi'])) {
return false;
}
$payload = ['cmd' => trim($command)]; $payload = ['cmd' => trim($command)];
$response = $this->exec_curl('/api/s/' . $this->site . '/cmd/stat', $payload); $response = $this->exec_curl('/api/s/' . $this->site . '/cmd/stat', $payload);
@@ -3424,6 +3534,10 @@ class Client
return false; return false;
} }
if (!in_array($request_type, $this->request_types_allowed)) {
return false;
}
$this->request_type = $request_type; $this->request_type = $request_type;
$response = $this->exec_curl($path, $payload); $response = $this->exec_curl($path, $payload);
@@ -3502,7 +3616,7 @@ class Client
* -------------------- * --------------------
* returns the raw results of the last method called, returns false if unavailable * returns the raw results of the last method called, returns false if unavailable
* optional parameter <return_json> = boolean; true will return the results in "pretty printed" json format, * optional parameter <return_json> = boolean; true will return the results in "pretty printed" json format,
* PHP stdClass Object format is returned by default * false returns PHP stdClass Object format (default)
*/ */
public function get_last_results_raw($return_json = false) public function get_last_results_raw($return_json = false)
{ {
@@ -3532,15 +3646,17 @@ class Client
} }
/** /**
* Get Cookie from UniFi Controller * Get Cookie from UniFi controller
* -------------------------------- * --------------------------------
* returns the UniFi controller cookie * returns the UniFi controller cookie
* *
* NOTES: * NOTES:
* - when the results from this method are stored in $_SESSION['unificookie'], the class will initially not * - when the results from this method are stored in $_SESSION['unificookie'], the Class will initially not
* log in to the controller when a subsequent request is made using a new instance. This speeds up the * log in to the controller when a subsequent request is made using a new instance. This speeds up the
* overall request considerably. If that subsequent request fails (e.g. cookies have expired), a new login * overall request considerably. Only when a subsequent request fails (e.g. cookies have expired) is a new login
* is executed automatically and the value of $_SESSION['unificookie'] is updated. * executed and the value of $_SESSION['unificookie'] updated.
* - to force the Class instance to log out automatically upon destruct, simply call logout() or unset
* $_SESSION['unificookie'] at the end of your code
*/ */
public function get_cookie() public function get_cookie()
{ {
@@ -3581,7 +3697,14 @@ class Client
public function set_request_type($request_type) public function set_request_type($request_type)
{ {
if (!in_array($request_type, $this->request_types_allowed)) {
return false;
}
$this->request_type = $request_type; $this->request_type = $request_type;
return true;
} }
public function set_connection_timeout($timeout) public function set_connection_timeout($timeout)
@@ -3599,17 +3722,34 @@ class Client
$this->last_error_message = $last_error_message; $this->last_error_message = $last_error_message;
} }
/**
* set the value for cURL option CURLOPT_SSL_VERIFYPEER, should be 0/false or 1/true
* https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html
*/
public function set_ssl_verify_peer($ssl_verify_peer) public function set_ssl_verify_peer($ssl_verify_peer)
{ {
if (!in_array($ssl_verify_peer, [0, false, 1, true])) {
return false;
}
$this->curl_ssl_verify_peer = $ssl_verify_peer; $this->curl_ssl_verify_peer = $ssl_verify_peer;
return true;
} }
/** /**
* set the value for cURL option CURLOPT_SSL_VERIFYHOST, should be 0/false or 2 * set the value for cURL option CURLOPT_SSL_VERIFYHOST, should be 0/false or 2
* https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
*/ */
public function set_ssl_verify_host($ssl_verify_host) public function set_ssl_verify_host($ssl_verify_host)
{ {
if (!in_array($ssl_verify_host, [0, false, 2])) {
return false;
}
$this->curl_ssl_verify_host = $ssl_verify_host; $this->curl_ssl_verify_host = $ssl_verify_host;
return true;
} }
/**************************************************************** /****************************************************************
@@ -3694,13 +3834,13 @@ class Client
$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
@@ -3727,7 +3867,7 @@ class Client
break; break;
default: default:
// we have an unknown error // we have an unknown error
$error = 'Unknown JSON error occured.'; $error = 'Unknown JSON error occured';
break; break;
} }
@@ -3742,7 +3882,7 @@ class Client
} }
/** /**
* Check the submitted base URL * validate the submitted base URL
*/ */
private function check_base_url() private function check_base_url()
{ {
@@ -3753,13 +3893,6 @@ class Client
return false; return false;
} }
$base_url_components = parse_url($this->baseurl);
if (empty($base_url_components['port'])) {
trigger_error('The URL provided does not have a port suffix, normally this is :8443');
return false;
}
return true; return true;
} }
@@ -3768,9 +3901,13 @@ class Client
*/ */
private function check_site($site) private function check_site($site)
{ {
if ($this->debug && strlen($site) !== 8 && $site !== 'default') { if ($this->debug && preg_match("/\s/", $site)) {
error_log('The provided (short) site name is probably incorrect'); trigger_error('The provided (short) site name may not contain any spaces');
return false;
} }
return true;
} }
/** /**
@@ -3778,42 +3915,112 @@ class Client
*/ */
private function update_unificookie() private function update_unificookie()
{ {
if (isset($_SESSION['unificookie'])) { if (isset($_SESSION['unificookie']) && !empty($_SESSION['unificookie'])) {
$this->cookies = $_SESSION['unificookie']; $this->cookies = $_SESSION['unificookie'];
$this->is_loggedin = true;
/**
* if we have a JWT in our cookie we know we're dealing with a UniFi OS controller
*/
if (strpos($this->cookies, 'TOKEN') !== false) {
$this->is_unifi_os = true;
}
return true;
} }
return false;
}
/**
* Extract the CSRF token from our Cookie string
*/
private function extract_csrf_token_from_cookie()
{
if ($this->cookies !== '') {
$cookie_bits = explode('=', $this->cookies);
if (!empty($cookie_bits) && array_key_exists(1, $cookie_bits)) {
$jwt = $cookie_bits[1];
} else {
return false;
}
$jwt_components = explode('.', $jwt);
if (!empty($jwt_components) && array_key_exists(1, $jwt_components)) {
$jwt_payload = $jwt_components[1];
} else {
return false;
}
return json_decode(base64_decode($jwt_payload))->csrfToken;
}
return false;
} }
/** /**
* Execute the cURL request * Execute the cURL request
*/ */
protected function exec_curl($path, $payload = '') protected function exec_curl($path, $payload = null)
{ {
if (!in_array($this->request_type, $this->request_types_allowed)) {
trigger_error('an invalid HTTP request type was used: ' . $this->request_type);
}
if (!is_resource($ch = $this->get_curl_resource())) { if (!is_resource($ch = $this->get_curl_resource())) {
trigger_error('$ch as returned by get_curl_resource() is not a resource'); trigger_error('$ch as returned by get_curl_resource() is not a resource');
} else { } else {
$json_payload = ''; $json_payload = [];
$url = $this->baseurl . $path;
if ($this->is_unifi_os) {
$url = $this->baseurl . '/proxy/network' . $path;
} else {
$url = $this->baseurl . $path;
}
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
if (!empty($payload)) { if ($payload !== null) {
curl_setopt($ch, CURLOPT_POST, true);
$json_payload = json_encode($payload, JSON_UNESCAPED_SLASHES); $json_payload = json_encode($payload, JSON_UNESCAPED_SLASHES);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload); curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
$headers = [
'Content-Type: application/json',
'Content-Length: ' . strlen($json_payload),
'Accept: application/json'
];
if ($this->is_unifi_os) {
$csrf_token = $this->extract_csrf_token_from_cookie();
if ($csrf_token) {
$headers[] = 'x-csrf-token: ' . $csrf_token;
}
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
/**
* we shouldn't be using GET (the default request type) or DELETE when passing a payload,
* we switch to POST instead
*/
if ($this->request_type === 'GET' || $this->request_type === 'DELETE') {
$this->request_type = 'POST';
}
if ($this->request_type === 'PUT') { if ($this->request_type === 'PUT') {
curl_setopt($ch, CURLOPT_HTTPHEADER,
['Content-Type: application/json', 'Content-Length: ' . strlen($json_payload)]);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
} else {
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
} }
} else {
curl_setopt($ch, CURLOPT_POST, false); if ($this->request_type === 'POST') {
if ($this->request_type === 'DELETE') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
} }
} }
if ($this->request_type === 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}
/** /**
* execute the cURL request * execute the cURL request
*/ */
@@ -3823,7 +4030,7 @@ class Client
} }
/** /**
* has the session timed out? If so, we need to login again. * has the Cookie/Token expired? If so, we need to login again.
*/ */
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -3832,14 +4039,14 @@ class Client
if (isset($json_decoded_content['meta']['msg']) && $json_decoded_content['meta']['msg'] === 'api.err.LoginRequired') { if (isset($json_decoded_content['meta']['msg']) && $json_decoded_content['meta']['msg'] === 'api.err.LoginRequired') {
if ($this->debug) { if ($this->debug) {
error_log('cURL debug: Needed to reconnect to UniFi Controller'); error_log('cURL debug: needed to reconnect to UniFi controller');
} }
/** /**
* explicitly unset the old cookie now * explicitly clear the expired Cookie/Token now
*/ */
if (isset($_SESSION['unificookie'])) { if (isset($_SESSION['unificookie'])) {
unset($_SESSION['unificookie']); $_SESSION['unificookie'] = '';
} }
/** /**
@@ -3890,7 +4097,7 @@ class Client
/** /**
* set request_type value back to default, just in case * set request_type value back to default, just in case
*/ */
$this->request_type = 'POST'; $this->request_type = 'GET';
return $content; return $content;
} }
@@ -3905,12 +4112,10 @@ class Client
protected function get_curl_resource() protected function get_curl_resource()
{ {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->curl_ssl_verify_peer); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->curl_ssl_verify_peer);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->curl_ssl_verify_host); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->curl_ssl_verify_host);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
if ($this->debug) { if ($this->debug) {
curl_setopt($ch, CURLOPT_VERBOSE, true); curl_setopt($ch, CURLOPT_VERBOSE, true);