diff --git a/README.md b/README.md index 1ee1946..7634639 100755 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@ This tool is for browsing data that is exposed through Ubiquiti's UniFi Controll It comes bundled with a **PHP class for access to the UniFi Controller API**, which supports [more API endpoints](https://github.com/Art-of-WiFi/UniFi-API-client#methods-and-functions-supported) than the UniFi API browser tool does. -If you plan to create your own PHP code leveraging the UniFi controller API, it is recommended to use the standalone version of the API client class which can be found here: https://github.com/Art-of-WiFi/UniFi-API-client +If you plan on creating your own PHP code to leverage the UniFi controller API, it is recommended to use the standalone version of the API client class which can be found here: https://github.com/Art-of-WiFi/UniFi-API-client You will find examples and detailed instructions there. Please keep the following in mind: - the API browser tool doesn't support all available data collections/API endpoints, see the list below of those that are currently supported -- currently, versions 4.x.x and 5.x.x of the UniFi Controller software are supported (version 5.11.50 has been confirmed to work) -- there is still work to be done to add/improve functionality and usability of this tool so suggestions/comments are welcome. Please use the github [issue](https://github.com/Art-of-WiFi/UniFi-API-browser/issues) list or the Ubiquiti Community forums (https://community.ubnt.com/t5/UniFi-Wireless/UniFi-API-browser-tool-released/m-p/1392651) to share your ideas/questions. +- currently, versions 4.x.x and 5.x.x of the UniFi 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) +- when accessing UniFi OS-based controllers (e.g. UDM PRO) through this tool, please read the remarks below regarding UniFi OS support +- there is still work to be done to add/improve functionality and usability of this tool so suggestions/comments are welcome. Please use the GitHub [issue](https://github.com/Art-of-WiFi/UniFi-API-browser/issues) list or the Ubiquiti Community forums (https://community.ubnt.com/t5/UniFi-Wireless/UniFi-API-browser-tool-released/m-p/1392651) to share your ideas/questions. - please read the Security Notice below before installing this tool! @@ -129,6 +130,11 @@ Alternatively you may choose to download the zip file and unzip it in your direc - after following these steps, you can open the tool in your browser (assuming you installed it in the root folder of your web server as suggested above) by going to this url: `http(s):///UniFi-API-browser/` +### UniFi OS support + +Support for UniFi OS-based controllers (UniFi Dream Machine Pro) has been added with version 2.0.7. When adding the details for a UniFi OS device to the `config/config.php` file, please make sure not to add a port suffix or trailing slashes to the URL. + + ### Extending the dropdown menu Since version 2.0.0 you can extend the dropdown menu with your own options by adding them to the `config.php` file. Here's an example: diff --git a/ajax/fetch_collection.php b/ajax/fetch_collection.php index 878d748..1da0a5e 100755 --- a/ajax/fetch_collection.php +++ b/ajax/fetch_collection.php @@ -46,7 +46,14 @@ 'lan-rx_packets', 'lan-tx_packets', 'lan-rx_dropped', - 'lan-tx_dropped' + 'lan-tx_dropped', + 'wan-tx_bytes', + 'wan-rx_bytes', + 'wan2-tx_bytes', + 'wan2-rx_bytes', + 'latency_min', + 'latency_avg', + 'latency_max', ]; /** diff --git a/ajax/fetch_sites.php b/ajax/fetch_sites.php index efbbdbe..c701e1c 100755 --- a/ajax/fetch_sites.php +++ b/ajax/fetch_sites.php @@ -48,6 +48,7 @@ */ $host = parse_url($controller['url'], PHP_URL_HOST); $port = parse_url($controller['url'], PHP_URL_PORT); + $port = $port ?: 443; if (!empty($host) && !empty($port)) { $fp = @fsockopen($host, $port, $errno, $errstr, 2); diff --git a/ajax/show_api_debug.php b/ajax/show_api_debug.php index f4b5072..74da376 100755 --- a/ajax/show_api_debug.php +++ b/ajax/show_api_debug.php @@ -39,6 +39,7 @@ */ $host = parse_url($controller['url'], PHP_URL_HOST); $port = parse_url($controller['url'], PHP_URL_PORT); + $port = $port ?: 443; if (!empty($host) && !empty($port)) { $fp = @fsockopen($host, $port, $errno, $errstr, 2); diff --git a/common.php b/common.php index 25fbc8b..e12f2e5 100755 --- a/common.php +++ b/common.php @@ -7,7 +7,7 @@ * with this package in the file LICENSE.md * */ -define('TOOL_VERSION', '2.0.6'); +define('TOOL_VERSION', '2.0.7'); /** * gather some basic information for the About modal diff --git a/composer.lock b/composer.lock index 27c5100..3e2c195 100755 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "art-of-wifi/unifi-api-client", - "version": "v1.1.42", + "version": "v1.1.47", "source": { "type": "git", "url": "https://github.com/Art-of-WiFi/UniFi-API-client.git", - "reference": "c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a" + "reference": "17d895076fc8b8f1d0fa26c8f9d71c107360c884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a", - "reference": "c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a", + "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/17d895076fc8b8f1d0fa26c8f9d71c107360c884", + "reference": "17d895076fc8b8f1d0fa26c8f9d71c107360c884", "shasum": "" }, "require": { @@ -51,7 +51,7 @@ "ubiquiti", "unifi" ], - "time": "2019-10-15T12:41:16+00:00" + "time": "2020-02-06T07:52:49+00:00" }, { "name": "kint-php/kint", @@ -113,7 +113,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.13.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", diff --git a/js/custom.js b/js/custom.js index 9435892..8d49e6f 100755 --- a/js/custom.js +++ b/js/custom.js @@ -325,7 +325,8 @@ function fetchDebugDetails() { } } }); - }} + } +} /** * function to fetch a collection diff --git a/vendor/art-of-wifi/unifi-api-client/README.md b/vendor/art-of-wifi/unifi-api-client/README.md index bf6ac35..7b1efec 100755 --- a/vendor/art-of-wifi/unifi-api-client/README.md +++ b/vendor/art-of-wifi/unifi-api-client/README.md @@ -1,15 +1,21 @@ ## UniFi Controller API client class -A PHP class which provides access to Ubiquiti's [**UniFi SDN Controller API**](https://unifi-sdn.ui.com/), versions 4.X.X and 5.X.X of the UniFi SDN Controller software are supported (version 5.11.39 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 -- 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.10 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 -## 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. @@ -169,7 +175,9 @@ The class currently supports the following functions/methods to GET/POST/PUT/DEL - power_cycle_switch_port() - reconnect_sta() - rename_ap() -- restart_ap() +- restart_ap() (deprecated but still available as alias) +- restart_device() +- reboot_cloudkey() - revoke_voucher() - set_ap_radiosettings() - set_device_settings_base() @@ -261,7 +269,7 @@ This class is based on the initial work by the following developers: and the API as published by Ubiquiti: -- https://dl.ubnt.com/unifi/5.10.19/unifi_sh_api +- https://dl.ui.com/unifi/5.12.35/unifi_sh_api ## Important Disclaimer diff --git a/vendor/art-of-wifi/unifi-api-client/examples/ap_upgrade_firmware.php b/vendor/art-of-wifi/unifi-api-client/examples/ap_upgrade_firmware.php index 78dacaf..9989460 100755 --- a/vendor/art-of-wifi/unifi-api-client/examples/ap_upgrade_firmware.php +++ b/vendor/art-of-wifi/unifi-api-client/examples/ap_upgrade_firmware.php @@ -3,7 +3,7 @@ * PHP API usage example * * contributed by: @4oo4 - * description: example script to check and upgrade device firmware (can be scheduled with systemd/cron) + * description: example script to upgrade device firmware (can be scheduled with systemd/cron) * to the most current version */ require_once('vendor/autoload.php'); diff --git a/vendor/art-of-wifi/unifi-api-client/examples/config.template.php b/vendor/art-of-wifi/unifi-api-client/examples/config.template.php index 1052620..f789bd6 100755 --- a/vendor/art-of-wifi/unifi-api-client/examples/config.template.php +++ b/vendor/art-of-wifi/unifi-api-client/examples/config.template.php @@ -15,8 +15,9 @@ */ $controlleruser = ''; // the user name 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' -$controllerversion = ''; // the version of the Controller software, eg. '4.6.6' (must be at least 4.0.0) +$controllerurl = ''; // full url to the UniFi Controller, eg. 'https://22.22.11.11:8443', for UniFi OS-based + // 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 diff --git a/vendor/art-of-wifi/unifi-api-client/examples/list_connected_users.php b/vendor/art-of-wifi/unifi-api-client/examples/list_connected_users.php new file mode 100755 index 0000000..0cc2c68 --- /dev/null +++ b/vendor/art-of-wifi/unifi-api-client/examples/list_connected_users.php @@ -0,0 +1,37 @@ +'; + +/** + * initialize the UniFi API connection class and log in to the controller and pull the requested data + */ +$unifi_connection = new UniFi_API\Client($controlleruser, $controllerpassword, $controllerurl, $site_id, $controllerversion); +$set_debug_mode = $unifi_connection->set_debug($debug); +$loginresults = $unifi_connection->login(); +$clients_array = $unifi_connection->list_clients(); + +/** + * output the results in JSON format + */ +header('Content-Type: application/json; charset=utf-8'); +echo json_encode($clients_array); \ No newline at end of file diff --git a/vendor/art-of-wifi/unifi-api-client/examples/reconnect_client.php b/vendor/art-of-wifi/unifi-api-client/examples/reconnect_client.php new file mode 100755 index 0000000..bd8e32e --- /dev/null +++ b/vendor/art-of-wifi/unifi-api-client/examples/reconnect_client.php @@ -0,0 +1,45 @@ +'; + +/** + * site where the above MAC address is connected + */ +$site_id = ''; + +/** + * initialize the UniFi API connection class and log in to the controller + */ +$unifi_connection = new UniFi_API\Client($controlleruser, $controllerpassword, $controllerurl, $site_id, $controllerversion); +$set_debug_mode = $unifi_connection->set_debug($debug); +$loginresults = $unifi_connection->login(); + +/** + * then we force the device to reconnect + */ +$reconnect_result = $unifi_connection->reconnect_sta($mac_to_reconnect); + +/** + * provide feedback in json format + */ +echo json_encode($reconnect_result, JSON_PRETTY_PRINT); diff --git a/vendor/art-of-wifi/unifi-api-client/examples/update_switch_poe-mode.php b/vendor/art-of-wifi/unifi-api-client/examples/update_switch_poe-mode.php new file mode 100755 index 0000000..83186b9 --- /dev/null +++ b/vendor/art-of-wifi/unifi-api-client/examples/update_switch_poe-mode.php @@ -0,0 +1,94 @@ +'; + +/** + * the MAC address of the AC-IW device to re-configure + */ +$device_mac = ''; + +/** + * $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); \ No newline at end of file diff --git a/vendor/art-of-wifi/unifi-api-client/src/Client.php b/vendor/art-of-wifi/unifi-api-client/src/Client.php index c80419f..93e9ef9 100755 --- a/vendor/art-of-wifi/unifi-api-client/src/Client.php +++ b/vendor/art-of-wifi/unifi-api-client/src/Client.php @@ -2,7 +2,7 @@ /** * 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 * fbagnol: https://github.com/fbagnol/class.unifi.php * and the API as published by Ubiquiti: @@ -17,40 +17,44 @@ namespace UniFi_API; /** - * the UniFi API client class + * the UniFi API client Class */ class Client { /** - * private properties - */ - protected $baseurl = 'https://127.0.0.1:8443'; - protected $user = ''; - protected $password = ''; - protected $site = 'default'; - protected $version = '5.6.39'; - protected $debug = false; - protected $is_loggedin = false; - private $cookies = ''; - private $request_type = 'POST'; - private $connect_timeout = 10; - private $last_results_raw = null; - 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 + * private and protected properties + */ + protected $baseurl = 'https://127.0.0.1:8443'; + protected $user = ''; + protected $password = ''; + protected $site = 'default'; + protected $version = '5.6.39'; + protected $debug = false; + protected $is_loggedin = false; + protected $is_unifi_os = false; + private $cookies = ''; + private $request_type = 'GET'; + private $request_types_allowed = ['GET', 'POST', 'PUT', 'DELETE']; + private $connect_timeout = 10; + private $last_results_raw = null; + 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 * --------------------------------------------------- * return a new class instance * required parameter = string; user name to use when connecting to the UniFi controller * required parameter = string; password to use when connecting to the UniFi controller - * optional parameter = string; base URL of the UniFi controller, *must* include "https://" prefix and port suffix (:8443) + * optional parameter = 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 = string; short site name to access, defaults to "default" * optional parameter = string; the version number of the controller, defaults to "5.4.16" * optional parameter = 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) - * is to not validate the controller certificate + * disables validation of the controller certificate */ public function __construct($user, $password, $baseurl = '', $site = '', $version = '', $ssl_verify = false) { @@ -73,7 +77,7 @@ public function __construct($user, $password, $baseurl = '', $site = '', $versio $this->version = trim($version); } - if ($ssl_verify === true) { + if ((boolean)$ssl_verify === true) { $this->curl_ssl_verify_peer = true; $this->curl_ssl_verify_host = 2; } @@ -83,10 +87,18 @@ public function __construct($user, $password, $baseurl = '', $site = '', $versio $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() { /** - * if user has $_SESSION['unificookie'] set, do not logout here + * if $_SESSION['unificookie'] is set, do not logout here */ if (isset($_SESSION['unificookie'])) { return; @@ -101,70 +113,102 @@ public function __destruct() } /** - * Login to UniFi Controller - * ------------------------- + * Login to the UniFi controller + * ----------------------------- * returns true upon success */ public function login() { /** - * if $_SESSION['unificookie'] is set, skip the login + * if already logged in we skip the login process */ - if (isset($_SESSION['unificookie'])) { - return $this->is_loggedin = true; + if ($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())) { trigger_error('$ch as returned by get_curl_resource() is not a resource'); } else { - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_REFERER, $this->baseurl . '/login'); - curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/api/login'); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_URL, $this->baseurl . '/'); + + /** + * 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)) { + 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 + * 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 ($this->debug) { - curl_setopt($ch, CURLOPT_VERBOSE, true); - - print '
';
+                print PHP_EOL . '
';
                 print PHP_EOL . '-----------LOGIN-------------' . PHP_EOL;
                 print_r(curl_getinfo($ch));
                 print PHP_EOL . '----------RESPONSE-----------' . PHP_EOL;
                 print $content;
                 print PHP_EOL . '-----------------------------' . PHP_EOL;
-                print '
'; + print '
' . 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); $headers = substr($content, 0, $header_size); $body = trim(substr($content, $header_size)); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); - preg_match_all('|Set-Cookie: (.*);|Ui', $headers, $results); - - if (isset($results[1])) { - $this->cookies = implode(';', $results[1]); - if (!empty($body)) { - if (($http_code >= 200) && ($http_code < 400)) { - if (strpos($this->cookies, 'unifises') !== false) { - return $this->is_loggedin = true; - } - } + 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 ($http_code === 400) { - trigger_error('We have received an HTTP response status: 400. Probably a controller login failure'); - - return $http_code; + /** + * accept cookies from UniFi OS or from regular UNiFI controllers + */ + if (strpos($this->cookies, 'unifises') !== false || strpos($this->cookies, 'TOKEN') !== false) { + return $this->is_loggedin = true; } } } @@ -174,8 +218,8 @@ public function login() } /** - * Logout from UniFi Controller - * ---------------------------- + * Logout from the UniFi controller + * -------------------------------- * returns true upon success */ public function logout() @@ -184,7 +228,14 @@ public function logout() 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->cookies = ''; @@ -317,7 +368,8 @@ public function unblock_sta($mac) * required parameter = array of client MAC addresses * * 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) { @@ -348,7 +400,6 @@ public function create_user($mac, $user_group_id, $name = null, $note = null) return false; } - $this->request_type = 'POST'; $new_user = ['mac' => strtolower($mac), 'usergroup_id' => $user_group_id]; if (!is_null($name)) { $new_user['name'] = $name; @@ -537,9 +588,10 @@ public function stat_5minutes_aps($start = null, $end = null, $mac = null) return false; } - $end = is_null($end) ? (time() * 1000) : intval($end); - $start = is_null($start) ? $end - (12 * 3600 * 1000) : intval($start); - $payload = ['attrs' => ['bytes', 'num_sta', 'time'], 'start' => $start, 'end' => $end]; + $end = is_null($end) ? (time() * 1000) : intval($end); + $start = is_null($start) ? $end - (12 * 3600 * 1000) : intval($start); + $attributes = ['bytes', 'num_sta', 'time']; + $payload = ['attrs' => $attributes, 'start' => $start, 'end' => $end]; if (!is_null($mac)) { $payload['mac'] = strtolower($mac); } @@ -567,9 +619,10 @@ public function stat_hourly_aps($start = null, $end = null, $mac = null) return false; } - $end = is_null($end) ? (time() * 1000) : intval($end); - $start = is_null($start) ? $end - (7 * 24 * 3600 * 1000) : intval($start); - $payload = ['attrs' => ['bytes', 'num_sta', 'time'], 'start' => $start, 'end' => $end]; + $end = is_null($end) ? (time() * 1000) : intval($end); + $start = is_null($start) ? $end - (7 * 24 * 3600 * 1000) : intval($start); + $attributes = ['bytes', 'num_sta', 'time']; + $payload = ['attrs' => $attributes, 'start' => $start, 'end' => $end]; if (!is_null($mac)) { $payload['mac'] = strtolower($mac); } @@ -597,9 +650,10 @@ public function stat_daily_aps($start = null, $end = null, $mac = null) return false; } - $end = is_null($end) ? (time() * 1000) : intval($end); - $start = is_null($start) ? $end - (7 * 24 * 3600 * 1000) : intval($start); - $payload = ['attrs' => ['bytes', 'num_sta', 'time'], 'start' => $start, 'end' => $end]; + $end = is_null($end) ? (time() * 1000) : intval($end); + $start = is_null($start) ? $end - (7 * 24 * 3600 * 1000) : intval($start); + $attributes = ['bytes', 'num_sta', 'time']; + $payload = ['attrs' => $attributes, 'start' => $start, 'end' => $end]; if (!is_null($mac)) { $payload['mac'] = strtolower($mac); } @@ -1088,9 +1142,8 @@ public function create_usergroup($group_name, $group_dn = -1, $group_up = -1) return false; } - $this->request_type = 'POST'; - $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); + $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); return $this->process_response($response); } @@ -1179,7 +1232,6 @@ public function create_firewallgroup($group_name, $group_type, $group_members = return false; } - $this->request_type = 'POST'; $payload = ['name' => $group_name, 'group_type' => $group_type, 'group_members' => $group_members]; $response = $this->exec_curl('/api/s/' . $this->site . '/rest/firewallgroup', $payload); @@ -1378,6 +1430,27 @@ public function list_known_rogueaps() 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 * --------------------------- @@ -1739,12 +1812,8 @@ public function invite_admin( * restart devices, default value is false. With versions < 5.9.X this only applies * when readonly is true. */ - public function assign_existing_admin( - $admin_id, - $readonly = false, - $device_adopt = false, - $device_restart = false - ) { + public function assign_existing_admin($admin_id, $readonly = false, $device_adopt = false, $device_restart = false) + { if (!$this->is_loggedin) { return false; } @@ -1905,8 +1974,7 @@ public function create_hotspotop($name, $x_password, $note = null) return false; } - $this->request_type = 'POST'; - $payload = ['name' => $name, 'x_password' => $x_password]; + $payload = ['name' => $name, 'x_password' => $x_password]; if (isset($note)) { $payload['note'] = trim($note); } @@ -2055,6 +2123,32 @@ public function list_dpi_stats() return $this->process_response($response); } + /** + * List filtered DPI stats + * ----------------------- + * returns an array of fileterd DPI stats + * optional parameter = whether to returns stats by app or by category, valid values: + * 'by_cat' or 'by_app' + * optional parameter = 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 * --------------------- @@ -2190,20 +2284,67 @@ public function adopt_device($mac) } /** - * Reboot an access point + * Reboot a device * ---------------------- * return true on success + * required parameter = device MAC address + * optional parameter = string; two options: 'soft' or 'hard', defaults to soft + * soft can be used for all devices, requests a plain restart of that device + * hard is special for PoE switches and besides the restart also requests a + * power cycle on all PoE capable ports. Keep in mind that a 'hard' reboot + * does *NOT* trigger a factory-reset, as it somehow could suggest. + */ + public function restart_device($mac, $type = 'soft') + { + if (!$this->is_loggedin) { + return false; + } + + $payload = ['cmd' => 'restart', 'mac' => strtolower($mac)]; + if (!is_null($type) && in_array($type, ['soft', 'hard'])) { + $payload['type'] = strtolower($type); + } + + $response = $this->exec_curl('/api/s/' . $this->site . '/cmd/devmgr', $payload); + + return $this->process_response_boolean($response); + } + + /** + * Force provision of a device + * --------------------------- + * return true on success * required parameter = device MAC address */ - public function restart_ap($mac) + public function force_provision($mac) { if (!$this->is_loggedin) { return false; } - $payload = ['cmd' => 'restart', 'mac' => strtolower($mac)]; + $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 + * ----------------------- + * return true on success + * + * This API call does nothing on UniFi controllers *not* running on a UniFi CloudKey device + */ + public function reboot_cloudkey() + { + if (!$this->is_loggedin) { + return false; + } + + $payload = ['cmd' => 'reboot']; + $response = $this->exec_curl('/api/s/' . $this->site . '/cmd/system', $payload); + return $this->process_response_boolean($response); } @@ -2353,24 +2494,25 @@ public function set_ap_radiosettings($ap_id, $radio, $channel, $ht, $tx_power_mo * Assign access point to another WLAN group * ----------------------------------------- * return true on success - * required parameter = string; WLAN type, can be either 'ng' (for WLANs 2G (11n/b/g)) or 'na' (WLANs 5G (11n/a/ac)) - * required parameter = string; _id value of the access point to be modified - * required parameter = string; _id value of the WLAN group to assign device to - * - * NOTES: - * - can for example be used to turn WiFi off + * required parameter = string; WLAN type, can be either 'ng' (for WLANs 2G (11n/b/g)) or 'na' (WLANs 5G (11n/a/ac)) + * required parameter = string; _id value of the access point to be modified + * required parameter = 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) { return false; } - if (!in_array($wlantype_id, ['ng', 'na'])) { + if (!in_array($type_id, ['ng', 'na'])) { return false; } - $payload = ['wlan_overrides' => [], 'wlangroup_id_' . $wlantype_id => $wlangroup_id]; + $payload = [ + 'wlan_overrides' => [], + 'wlangroup_id_' . $type_id => $group_id + ]; + $response = $this->exec_curl('/api/s/' . $this->site . '/upd/device/' . trim($device_id), $payload); return $this->process_response_boolean($response); @@ -2380,14 +2522,15 @@ public function set_ap_wlangroup($wlantype_id, $device_id, $wlangroup_id) * Update guest login settings * --------------------------- * return true on success - * required parameter - * required parameter - * required parameter - * required parameter - * required parameter - * required parameter - * required parameter - * required parameter + * required parameter = boolean; enable/disable the captive portal + * required parameter = boolean; enable/disable captive portal customizations + * required parameter = boolean; enable/disable captive portal redirect + * required parameter = string; url to redirect to, must include the http/https prefix, no trailing slashes + * required parameter = string; the captive portal (simple) password + * required parameter = numeric; number of units for the authorization expiry + * required parameter = numeric; number of minutes within a unit (a value 60 is required for hours) + * required parameter = 24 char string; value of _id for the site settings section where key = "guest_access", settings can be obtained + * using the list_settings() function * * NOTES: * - both portal parameters are set to the same value! @@ -2400,8 +2543,9 @@ public function set_guestlogin_settings( $x_password, $expire_number, $expire_unit, - $site_id - ) { + $section_id + ) + { if (!$this->is_loggedin) { return false; } @@ -2414,7 +2558,7 @@ public function set_guestlogin_settings( 'x_password' => $x_password, 'expire_number' => $expire_number, 'expire_unit' => $expire_unit, - 'site_id' => $site_id + '_id' => $section_id ]; $response = $this->exec_curl('/api/s/' . $this->site . '/set/setting/guest_access', $payload); @@ -2605,8 +2749,7 @@ public function create_network($payload) 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); } @@ -2706,7 +2849,8 @@ public function create_wlan( $uapsd_enabled = false, $schedule_enabled = false, $schedule = [] - ) { + ) + { if (!$this->is_loggedin) { return false; } @@ -2925,8 +3069,7 @@ public function archive_alarm($alarm_id = null) return false; } - $this->request_type = 'POST'; - $payload = ['cmd' => 'archive-all-alarms']; + $payload = ['cmd' => 'archive-all-alarms']; if (!is_null($alarm_id)) { $payload = ['_id' => $alarm_id, 'cmd' => 'archive-alarm']; } @@ -3186,7 +3329,6 @@ public function create_radius_account($name, $x_password, $tunnel_type, $tunnel_ return false; } - $this->request_type = 'POST'; $payload = [ 'name' => $name, 'x_password' => $x_password, @@ -3248,14 +3390,11 @@ public function delete_radius_account($account_id) } /** - * Execute specific command - * ------------------------ + * Execute specific stats command + * ------------------------------ * return true on success * required parameter = string; command to execute, known valid values * 'reset-dpi': reset all DPI counters for the current site - * - * NOTE: - * the provided parameter isn't validated so make sure you're using a correct value */ public function cmd_stat($command) { @@ -3263,6 +3402,10 @@ public function cmd_stat($command) return false; } + if (!in_array($command, ['reset-dpi'])) { + return false; + } + $payload = ['cmd' => trim($command)]; $response = $this->exec_curl('/api/s/' . $this->site . '/cmd/stat', $payload); @@ -3355,6 +3498,23 @@ public function site_ledsoff() return $this->site_leds(false); } + /** + * Reboot an access point + * ---------------------- + * return true on success + * required parameter = device MAC address + */ + public function restart_ap($mac) + { + trigger_error( + 'Function restart_ap() has been deprecated, use restart_device() instead.', + E_USER_DEPRECATED + ); + + return $this->restart_device($mac); + } + + /** * Custom API request * ------------------ @@ -3374,6 +3534,10 @@ public function custom_api_request($path, $request_type = 'GET', $payload = null return false; } + if (!in_array($request_type, $this->request_types_allowed)) { + return false; + } + $this->request_type = $request_type; $response = $this->exec_curl($path, $payload); @@ -3452,7 +3616,7 @@ public function get_debug() * -------------------- * returns the raw results of the last method called, returns false if unavailable * optional parameter = 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) { @@ -3482,15 +3646,17 @@ public function get_last_error_message() } /** - * Get Cookie from UniFi Controller + * Get Cookie from UniFi controller * -------------------------------- * returns the UniFi controller cookie * * 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 - * overall request considerably. If that subsequent request fails (e.g. cookies have expired), a new login - * is executed automatically and the value of $_SESSION['unificookie'] is updated. + * overall request considerably. Only when a subsequent request fails (e.g. cookies have expired) is a new login + * 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() { @@ -3531,7 +3697,14 @@ public function set_cookies($cookies_value) public function set_request_type($request_type) { + + if (!in_array($request_type, $this->request_types_allowed)) { + return false; + } + $this->request_type = $request_type; + + return true; } public function set_connection_timeout($timeout) @@ -3549,17 +3722,34 @@ public function set_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) { + if (!in_array($ssl_verify_peer, [0, false, 1, true])) { + return false; + } + $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 + * https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html */ 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; + + return true; } /**************************************************************** @@ -3644,13 +3834,13 @@ private function catch_json_last_error() $error = 'The maximum stack depth has been exceeded'; break; case JSON_ERROR_STATE_MISMATCH: - $error = 'Invalid or malformed JSON.'; + $error = 'Invalid or malformed JSON'; break; case JSON_ERROR_CTRL_CHAR: $error = 'Control character error, possibly incorrectly encoded'; break; case JSON_ERROR_SYNTAX: - $error = 'Syntax error, malformed JSON.'; + $error = 'Syntax error, malformed JSON'; break; case JSON_ERROR_UTF8: // PHP >= 5.3.3 @@ -3677,7 +3867,7 @@ private function catch_json_last_error() break; default: // we have an unknown error - $error = 'Unknown JSON error occured.'; + $error = 'Unknown JSON error occured'; break; } @@ -3692,7 +3882,7 @@ private function catch_json_last_error() } /** - * Check the submitted base URL + * validate the submitted base URL */ private function check_base_url() { @@ -3703,13 +3893,6 @@ private function check_base_url() 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; } @@ -3718,9 +3901,13 @@ private function check_base_url() */ private function check_site($site) { - if ($this->debug && strlen($site) !== 8 && $site !== 'default') { - error_log('The provided (short) site name is probably incorrect'); + if ($this->debug && preg_match("/\s/", $site)) { + trigger_error('The provided (short) site name may not contain any spaces'); + + return false; } + + return true; } /** @@ -3728,42 +3915,112 @@ private function check_site($site) */ private function update_unificookie() { - if (isset($_SESSION['unificookie'])) { + if (isset($_SESSION['unificookie']) && !empty($_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 */ - 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())) { trigger_error('$ch as returned by get_curl_resource() is not a resource'); } else { - $json_payload = ''; - $url = $this->baseurl . $path; + $json_payload = []; + + if ($this->is_unifi_os) { + $url = $this->baseurl . '/proxy/network' . $path; + } else { + $url = $this->baseurl . $path; + } + 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); - 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') { - curl_setopt($ch, CURLOPT_HTTPHEADER, - ['Content-Type: application/json', 'Content-Length: ' . strlen($json_payload)]); 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 === 'DELETE') { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + + if ($this->request_type === 'POST') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); } } + if ($this->request_type === 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } + /** * execute the cURL request */ @@ -3773,7 +4030,7 @@ protected function exec_curl($path, $payload = '') } /** - * 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); @@ -3782,14 +4039,14 @@ protected function exec_curl($path, $payload = '') if (isset($json_decoded_content['meta']['msg']) && $json_decoded_content['meta']['msg'] === 'api.err.LoginRequired') { 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'])) { - unset($_SESSION['unificookie']); + $_SESSION['unificookie'] = ''; } /** @@ -3840,7 +4097,7 @@ protected function exec_curl($path, $payload = '') /** * set request_type value back to default, just in case */ - $this->request_type = 'POST'; + $this->request_type = 'GET'; return $content; } @@ -3855,12 +4112,10 @@ protected function exec_curl($path, $payload = '') protected function get_curl_resource() { $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_VERIFYHOST, $this->curl_ssl_verify_host); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout); - curl_setopt($ch, CURLINFO_HEADER_OUT, true); if ($this->debug) { curl_setopt($ch, CURLOPT_VERBOSE, true); diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 0879324..8412f2d 100755 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -1,24 +1,24 @@ [ { "name": "art-of-wifi/unifi-api-client", - "version": "v1.1.42", - "version_normalized": "1.1.42.0", + "version": "v1.1.47", + "version_normalized": "1.1.47.0", "source": { "type": "git", "url": "https://github.com/Art-of-WiFi/UniFi-API-client.git", - "reference": "c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a" + "reference": "17d895076fc8b8f1d0fa26c8f9d71c107360c884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a", - "reference": "c7d1eee3e3ae9da2bd4f67994a95ff571fdfb89a", + "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/17d895076fc8b8f1d0fa26c8f9d71c107360c884", + "reference": "17d895076fc8b8f1d0fa26c8f9d71c107360c884", "shasum": "" }, "require": { "ext-curl": "*", "php": ">=5.4.0" }, - "time": "2019-10-15T12:41:16+00:00", + "time": "2020-02-06T07:52:49+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -110,8 +110,8 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.13.0", - "version_normalized": "1.13.0.0", + "version": "v1.13.1", + "version_normalized": "1.13.1.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git",