diff --git a/.gitignore b/.gitignore index 25aeec1..bbea472 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ /vendor/ /composer.lock /clover.xml -/.phpunit.result.cache \ No newline at end of file +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index dd98256..105c441 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ before_script: - ./preconf.tcl 12223 > /dev/null || true - ./preconf.tcl 22223 > /dev/null || true - composer self-update -- composer install --prefer-source --no-interaction --dev +- composer install --no-interaction --dev script: - vendor/bin/phpunit --coverage-clover=coverage.clover diff --git a/README.md b/README.md index 477f034..f20a256 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Code Coverage](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/EvilFreelancer/routeros-api-php/?branch=master) [![Scrutinizer CQ](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/evilfreelancer/routeros-api-php/) -# RouterOS PHP7 API Client +# RouterOS API Client composer require evilfreelancer/routeros-api-php @@ -17,6 +17,11 @@ to work with PHP7 in accordance with the PSR standards. You can use this library with pre-6.43 and post-6.43 versions of RouterOS firmware, it will be detected automatically on connection stage. +## Minimum requirements + +* `php` >= 7.2 +* `ext-sockets` + ## Laravel framework support RouterOS API client is optimized for usage as normal Laravel package, all functional is available via `\RouterOS` facade, @@ -32,10 +37,10 @@ $config = new \RouterOS\Config([ $client = new \RouterOS\Client($config); ``` -Call facade and pass array of parameters to `getClient` method: +Call facade and pass array of parameters to `client` method: ```php -$client = \RouterOS::getClient([ +$client = \RouterOS::client([ 'host' => '192.168.1.3', 'user' => 'admin', 'pass' => 'admin', @@ -43,26 +48,37 @@ $client = \RouterOS::getClient([ ]); ``` -### Laravel installation +You also may get array with all configs which was obtained from `routeros-api.php` file: + +```php +$config = \RouterOS::config([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin', + 'port' => 8728, +]); -Install the package via Composer: +dump($config); - composer require evilfreelancer/routeros-api-php +$client = \RouterOS::client($config); +``` -By default the package will automatically register its service provider, but -if you are a happy owner of Laravel version less than 5.3, then in a project, which is using your package +### Laravel installation + +By default, the package will automatically register its service provider, but +if you are a happy owner of Laravel version less than 5.5, then in a project, which is using your package (after composer require is done, of course), add into`providers` block of your `config/app.php`: ```php 'providers' => [ // ... - RouterOS\Laravel\ClientServiceProvider::class, + RouterOS\Laravel\ServiceProvider::class, ], ``` Optionally, publish the configuration file if you want to change any defaults: - php artisan vendor:publish --provider="RouterOS\\Laravel\\ClientServiceProvider" + php artisan vendor:publish --provider="RouterOS\\Laravel\\ServiceProvider" ## How to use @@ -88,18 +104,53 @@ $query = $response = $client->query($query)->read(); var_dump($response); +``` + +Basic example for update/create/delete types of queries: -// Send "equal" query +```php +use \RouterOS\Client; +use \RouterOS\Query; + +// Initiate client with config object +$client = new Client([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin' +]); + +// Send "equal" query with details about IP address which should be created $query = (new Query('/ip/hotspot/ip-binding/add')) ->equal('mac-address', '00:00:00:00:40:29') ->equal('type', 'bypassed') ->equal('comment', 'testcomment'); -// Send query and read response from RouterOS (ordinary answer to update/create/delete queries has empty body) +// Send query and read response from RouterOS (ordinary answer from update/create/delete queries has empty body) $response = $client->query($query)->read(); + +var_dump($response); ``` +If you need export all settings from router: + +```php +use \RouterOS\Client; + +// Initiate client with config object +$client = new Client([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin', + 'ssh_port' => 22222, +]); + +// Execute export command via ssh, because API /export method has a bug +$response = $client->export(); + +print_r($response); +``` + Examples with "where" conditions, "operations" and "tag": ```php @@ -257,10 +308,10 @@ need to create a "Query" object whose first argument is the required command, after this you can add the attributes of the command to "Query" object. -More about attributes and "words" from which this attributes +More about attributes and "words" from which these attributes should be created [here](https://wiki.mikrotik.com/wiki/Manual:API#Command_word). -More about "expressions", "where" and other filters/modificators +More about "expressions", "where", "equal" and other filters/modifications of your query you can find [here](https://wiki.mikrotik.com/wiki/Manual:API#Queries). Simple usage examples of Query class: @@ -271,6 +322,13 @@ use \RouterOS\Query; // Get all installed packages (it may be enabled or disabled) $query = new Query('/system/package/getall'); +// Send "equal" query with details about IP address which should be created +$query = + (new Query('/ip/hotspot/ip-binding/add')) + ->equal('mac-address', '00:00:00:00:40:29') + ->equal('type', 'bypassed') + ->equal('comment', 'testcomment'); + // Set where interface is disabled and ID is ether1 (with tag 4) $query = (new Query('/interface/set')) @@ -285,7 +343,7 @@ $query = ->where('type', 'vlan') ->operations('|'); -/// Get all routes that have non-empty comment +// Get all routes that have non-empty comment $query = (new Query('/ip/route/print')) ->where('comment', '>', null); @@ -357,8 +415,8 @@ $query->operations('|'); // Enable interface (tag is 4) $query = new Query('/interface/set'); -$query->where('disabled', 'no'); -$query->where('.id', 'ether1'); +$query->equal('disabled', 'no'); +$query->equal('.id', 'ether1'); $query->tag(4); // Or @@ -396,8 +454,8 @@ $response = $client->query($query)->read(); ## Read response as Iterator -By default original solution of this client is not optimized for -work with large amount of results, only for small count of lines +By default, original solution of this client is not optimized for +work with a large amount of results, only for small count of lines in response from RouterOS API. But some routers may have (for example) 30000+ records in @@ -468,6 +526,75 @@ $client->query($query1)->query($query2)->query($query3); $client->q($query1)->q($query2)->q($query3); ``` +## Known issues + +### Unable to establish socket session, Operation timed out + +This error means that the library cannot connect to your router, +it may mean router turned off (then need turn on), or the API service not enabled. + +Go to `Mikrotik Router OS -> IP -> Services` and enable `api` service. + +Or via command line: + +```shell script +/ip service enable api +``` + +### How to update/remove/create something via API? + +Instead of `->where()` method of `Query` class you need to +use `->equal()` method: + +```php +// Create query which should remove security profile +$query = new \RouterOS\Query('/interface/wireless/security-profiles/remove'); + +// It will generate queries, which stared from "?" symbol: +$query->where('.id', '*1'); + +/* +// Sample with ->where() method +RouterOS\Query Object +( + [_attributes:RouterOS\Query:private] => Array + ( + [0] => ?.id=*1 + ) + + [_operations:RouterOS\Query:private] => + [_tag:RouterOS\Query:private] => + [_endpoint:RouterOS\Query:private] => /interface/wireless/security-profiles/remove +) +*/ + +// So, as you can see, instead of `->where()` need to use `->equal()` +// It will generate queries, which stared from "=" symbol: +$query->equal('.id', '*1'); + +/* +// Sample with ->equal() method +RouterOS\Query Object +( + [_attributes:RouterOS\Query:private] => Array + ( + [0] => =.id=*1 + ) + + [_operations:RouterOS\Query:private] => + [_tag:RouterOS\Query:private] => + [_endpoint:RouterOS\Query:private] => /interface/wireless/security-profiles/remove +) +*/ +``` + +### Undefined character (any non-English languages) + +RouterOS does not support national languages, only English (and API of RouterOS too). + +You can try to reproduce it via web, for example add the comment to any +element of your system, then save and reload the page, you will see unreadable characters. + ## Testing You can use my [other project](https://github.com/EvilFreelancer/docker-routeros) diff --git a/composer.json b/composer.json index a70a207..e1db656 100644 --- a/composer.json +++ b/composer.json @@ -33,22 +33,25 @@ "extra": { "laravel": { "providers": [ - "RouterOS\\Laravel\\ClientServiceProvider" + "RouterOS\\Laravel\\ServiceProvider" ], "aliases": { - "RouterOS": "RouterOS\\Laravel\\ClientFacade" + "RouterOS": "RouterOS\\Laravel\\Facade" } } }, "require": { "php": "^7.2", - "ext-sockets": "*" + "ext-sockets": "*", + "divineomega/php-ssh-connection": "^2.2" }, "require-dev": { - "phpunit/phpunit": "^7.0", - "orchestra/testbench": "^3.0", + "limedeck/phpunit-detailed-printer": "^5.0", + "orchestra/testbench": "^4.0|^5.0", + "phpunit/phpunit": "^8.0", "roave/security-advisories": "dev-master", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^3.5", + "larapack/dd": "^1.1" }, "scripts": { "test": "phpunit --coverage-clover clover.xml", diff --git a/configs/routeros-api.php b/configs/routeros-api.php index 91a3359..8a91d23 100644 --- a/configs/routeros-api.php +++ b/configs/routeros-api.php @@ -1,13 +1,39 @@ null, - // 'user' => null, - // 'pass' => null, - // 'port' => null, - 'ssl' => false, - 'legacy' => false, - 'timeout' => 10, - 'attempts' => 10, - 'delay' => 1, + + /* + |-------------------------------------------------------------------------- + | Connection details + |-------------------------------------------------------------------------- + | + | Here you may specify different information about your router, like + | hostname (or ip-address), username, password, port and ssl mode. + | + | SSH port should be set if you want to use "/export" command. + | + */ + + 'host' => '192.168.88.1', // Address of Mikrotik RouterOS + 'user' => 'admin', // Username + 'pass' => null, // Password + 'port' => 8728, // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) + 'ssl' => false, // Enable ssl support (if port is not set this parameter must change default port to ssl port) + 'ssh_port' => 22, // Number of SSH port + + /* + |-------------------------------------------------------------------------- + | Optional connection settings of client + |-------------------------------------------------------------------------- + | + | Settings bellow need to advanced tune of your connection, for example + | you may enable legacy mode by default, or change timeout of connection. + | + */ + + 'legacy' => false, // Support of legacy login scheme (true - pre 6.43, false - post 6.43) + 'timeout' => 10, // Max timeout for answer from RouterOS + 'attempts' => 10, // Count of attempts to establish TCP session + 'delay' => 1, // Delay between attempts in seconds + ]; diff --git a/examples/bridge_hosts.php b/examples/bridge_hosts.php index 0cf2130..05edd53 100644 --- a/examples/bridge_hosts.php +++ b/examples/bridge_hosts.php @@ -22,5 +22,5 @@ $client = new Client($config); $query = new Query('/interface/bridge/host/print'); // Send query to RouterOS -$response = $client->write($query)->read(); +$response = $client->query($query)->read(); print_r($response); diff --git a/examples/different_queries.php b/examples/different_queries.php index 2ea45af..b8c49bd 100644 --- a/examples/different_queries.php +++ b/examples/different_queries.php @@ -14,12 +14,12 @@ $client = new Client([ ]); for ($i = 0; $i < 10; $i++) { - $response = $client->wr('/ip/address/print'); + $response = $client->qr('/ip/address/print'); print_r($response); - $response = $client->wr('/ip/arp/print'); + $response = $client->qr('/ip/arp/print'); print_r($response); - $response = $client->wr('/interface/print'); + $response = $client->qr('/interface/print'); print_r($response); } diff --git a/examples/export.php b/examples/export.php index c72d3ee..c2621fc 100644 --- a/examples/export.php +++ b/examples/export.php @@ -12,14 +12,34 @@ $config = (new Config()) ->set('host', '127.0.0.1') ->set('pass', 'admin') - ->set('user', 'admin'); + ->set('user', 'admin') + ->set('ssh_port', 22222); // Initiate client with config object $client = new Client($config); -// Build query +// Execute export command via ssh +$response = $client->export(); +dump($response); + +/* +// In results you will see something like this + +# jun/28/2020 16:31:21 by RouterOS 6.47 +# software id = +# +# +# +/interface wireless security-profiles +set [ find default=yes ] supplicant-identity=MikroTik +/ip dhcp-client +add disabled=no interface=ether1 + + */ + +// But here is another example $query = new Query('/export'); -// Send query and read answer from RouterOS -$response = $client->write($query)->read(false); -print_r($response); +// Execute export command via ssh but in style of library +$response = $client->query($query)->read(); +dump($response); diff --git a/examples/interface_print.php b/examples/interface_print.php index 355ab16..dca7d9f 100644 --- a/examples/interface_print.php +++ b/examples/interface_print.php @@ -21,7 +21,7 @@ $client = new Client($config); $query = new Query('/interface/getall'); // Send query to RouterOS -$request = $client->write($query); +$request = $client->query($query); // Read answer from RouterOS $response = $client->read(); diff --git a/examples/ip_address_print.php b/examples/ip_address_print.php index 50b9b6e..1e8dc40 100644 --- a/examples/ip_address_print.php +++ b/examples/ip_address_print.php @@ -18,5 +18,5 @@ $client = new Client([ $query = new Query('/ip/address/print'); // Send query to RouterOS -$response = $client->write($query)->read(); +$response = $client->query($query)->read(); print_r($response); diff --git a/examples/ip_filrewall_address-list_print.php b/examples/ip_filrewall_address-list_print.php index 6b17fe4..e8f7e76 100644 --- a/examples/ip_filrewall_address-list_print.php +++ b/examples/ip_filrewall_address-list_print.php @@ -14,7 +14,7 @@ $client = new Client([ ]); // Send query to RouterOS and parse response -$response = $client->write('/ip/firewall/address-list/print')->read(); +$response = $client->query('/ip/firewall/address-list/print')->read(); // You could treat response as an array except using array_* function diff --git a/examples/queue_simple_print.php b/examples/queue_simple_print.php index f827487..3c8ced6 100644 --- a/examples/queue_simple_print.php +++ b/examples/queue_simple_print.php @@ -25,6 +25,6 @@ $ips = [ foreach ($ips as $ip) { $query = new Query('/queue/simple/print', ['?target=' . $ip . '/32']); - $response = $client->wr($query); + $response = $client->qr($query); print_r($response); } diff --git a/examples/queue_simple_print_v2.php b/examples/queue_simple_print_v2.php index da396db..0e966a0 100644 --- a/examples/queue_simple_print_v2.php +++ b/examples/queue_simple_print_v2.php @@ -24,7 +24,7 @@ $ips = [ ]; foreach ($ips as $ip) { - $response = $client->wr([ + $response = $client->qr([ '/queue/simple/print', '?target=' . $ip . '/32' ]); diff --git a/examples/queue_simple_write.php b/examples/queue_simple_write.php index 62e23b8..8696a4c 100644 --- a/examples/queue_simple_write.php +++ b/examples/queue_simple_write.php @@ -12,8 +12,8 @@ $client = new Client([ 'pass' => 'admin' ]); -$out = $client->write(['/queue/simple/add', '=name=test'])->read(); +$out = $client->query(['/queue/simple/add', '=name=test'])->read(); print_r($out); -$out = $client->write(['/queue/simple/add', '=name=test'])->read(); +$out = $client->query(['/queue/simple/add', '=name=test'])->read(); print_r($out); diff --git a/examples/remove_security_profile.php b/examples/remove_security_profile.php new file mode 100644 index 0000000..f4e98d3 --- /dev/null +++ b/examples/remove_security_profile.php @@ -0,0 +1,14 @@ +where()` need to use `->equal()`, it will generate queries, +// which stared from "=" symbol: +$query->where('.id', '*1'); + +$client = new \RouterOS\Client(['host' => '192.168.88.1', 'user' => 'admin', 'pass' => 'password']); +$response = $client->query($query)->read(); diff --git a/examples/system_package_print.php b/examples/system_package_print.php index e19d507..8cd114c 100644 --- a/examples/system_package_print.php +++ b/examples/system_package_print.php @@ -21,7 +21,7 @@ $client = new Client($config); $query = new Query('/system/package/print'); // Send query to RouterOS -$request = $client->write($query); +$request = $client->query($query); // Read answer from RouterOS $response = $client->read(); diff --git a/examples/vlans_bridge.php b/examples/vlans_bridge.php index c1e014a..b22f912 100644 --- a/examples/vlans_bridge.php +++ b/examples/vlans_bridge.php @@ -35,14 +35,14 @@ foreach ($vlans as $vlanId => $ports) { // Add bridges $query = new Query('/interface/bridge/add'); $query->add("=name=vlan$vlanId-bridge")->add('vlan-filtering=no'); - $response = $client->write($query)->read(); + $response = $client->query($query)->read(); print_r($response); // Add ports to bridge foreach ($ports as $port) { $bridgePort = new Query('/interface/bridge/port/add'); $bridgePort->add("=bridge=vlan$vlanId-bridge")->add("=pvid=$vlanId")->add("=interface=ether$port"); - $response = $client->write($bridgePort)->read(); + $response = $client->query($bridgePort)->read(); print_r($response); } @@ -50,7 +50,7 @@ foreach ($vlans as $vlanId => $ports) { foreach ($ports as $port) { $vlan = new Query('/interface/bridge/vlan/add'); $vlan->add("=bridge=vlan$vlanId-bridge")->add("=untagged=ether$port")->add("=vlan-ids=$vlanId"); - $response = $client->write($vlan)->read(false); + $response = $client->query($vlan)->read(false); print_r($response); } diff --git a/examples/vlans_bridge_v2.php b/examples/vlans_bridge_v2.php index b6e5bdb..664cf7e 100644 --- a/examples/vlans_bridge_v2.php +++ b/examples/vlans_bridge_v2.php @@ -34,7 +34,7 @@ foreach ($vlans as $vlanId => $ports) { 'vlan-filtering=no' ]); - $response = $client->wr($query); + $response = $client->qr($query); print_r($response); // Add ports to bridge @@ -45,7 +45,7 @@ foreach ($vlans as $vlanId => $ports) { "=interface=ether$port" ]); - $response = $client->wr($bridgePort); + $response = $client->qr($bridgePort); print_r($response); } @@ -57,7 +57,7 @@ foreach ($vlans as $vlanId => $ports) { "=vlan-ids=$vlanId" ]); - $response = $client->wr($vlan); + $response = $client->qr($vlan); print_r($response); } diff --git a/examples/vlans_bridge_v3.php b/examples/vlans_bridge_v3.php index 3b01301..339537b 100644 --- a/examples/vlans_bridge_v3.php +++ b/examples/vlans_bridge_v3.php @@ -28,7 +28,7 @@ $vlans = [ foreach ($vlans as $vlanId => $ports) { // Add bridges - $response = $client->wr([ + $response = $client->qr([ '/interface/bridge/add', "=name=vlan$vlanId-bridge", 'vlan-filtering=no' @@ -37,7 +37,7 @@ foreach ($vlans as $vlanId => $ports) { // Add ports to bridge foreach ($ports as $port) { - $response = $client->wr([ + $response = $client->qr([ '/interface/bridge/port/add', "=bridge=vlan$vlanId-bridge", "=pvid=$vlanId", @@ -48,7 +48,7 @@ foreach ($vlans as $vlanId => $ports) { // Add untagged ports to bridge with tagging foreach ($ports as $port) { - $response = $client->wr([ + $response = $client->qr([ '/interface/bridge/vlan/add', "=bridge=vlan$vlanId-bridge", "=untagged=ether$port", diff --git a/phpunit.xml b/phpunit.xml index 1d3da08..809ef5f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,19 @@ - + ./src - ./tests + ./tests @@ -22,5 +31,6 @@ + diff --git a/preconf.tcl b/preconf.tcl index d0df49d..5c9c9f2 100755 --- a/preconf.tcl +++ b/preconf.tcl @@ -7,7 +7,7 @@ set port [lindex $argv 0] spawn telnet localhost $port expect "Login: " -send "admin+c\n" +send "admin+etc\n" expect "Password: " send "\n" expect "]:" diff --git a/src/APIConnector.php b/src/APIConnector.php index 9da509d..eed48b4 100644 --- a/src/APIConnector.php +++ b/src/APIConnector.php @@ -20,17 +20,26 @@ class APIConnector protected $stream; /** - * Constructor + * APIConnector constructor. * - * @param StreamInterface $stream + * @param \RouterOS\Interfaces\StreamInterface $stream */ - public function __construct(StreamInterface $stream) { $this->stream = $stream; } /** + * Close stream connection + * + * @return void + */ + public function close(): void + { + $this->stream->close(); + } + + /** * Reads a WORD from the stream * * WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content. diff --git a/src/APILengthCoDec.php b/src/APILengthCoDec.php index 5c9bda5..1ba8b41 100644 --- a/src/APILengthCoDec.php +++ b/src/APILengthCoDec.php @@ -3,8 +3,10 @@ namespace RouterOS; use DomainException; +use OverflowException; use RouterOS\Interfaces\StreamInterface; use RouterOS\Helpers\BinaryStringHelper; +use UnexpectedValueException; /** * class APILengthCoDec @@ -51,7 +53,7 @@ class APILengthCoDec * * @param int|float $length * - * @return string + * @return string */ public static function encodeLength($length): string { @@ -169,7 +171,7 @@ class APILengthCoDec // How can we test it ? // @codeCoverageIgnoreStart - throw new \OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system"); + throw new OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system"); // @codeCoverageIgnoreEnd } @@ -189,6 +191,6 @@ class APILengthCoDec // Now the only solution is 5 most significance bits are set to 1 (11111xxx) // This is a control word, not implemented by Mikrotik for the moment - throw new \UnexpectedValueException('Control Word found'); + throw new UnexpectedValueException('Control Word found'); } } diff --git a/src/Client.php b/src/Client.php index a6c8346..50508a2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,18 +2,17 @@ namespace RouterOS; +use DivineOmega\SSHConnection\SSHConnection; use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ConfigException; -use RouterOS\Exceptions\QueryException; -use RouterOS\Helpers\ArrayHelper; - +use RouterOS\Interfaces\ClientInterface; use RouterOS\Interfaces\QueryInterface; +use RouterOS\Helpers\ArrayHelper; use function array_keys; use function array_shift; use function chr; use function count; use function is_array; -use function is_string; use function md5; use function pack; use function preg_match_all; @@ -35,26 +34,33 @@ class Client implements Interfaces\ClientInterface * * @var \RouterOS\Config */ - private $_config; + private $config; /** * API communication object * * @var \RouterOS\APIConnector */ + private $connector; - private $_connector; + /** + * Some strings with custom output + * + * @var string + */ + private $customOutput; /** * Client constructor. * - * @param array|\RouterOS\Interfaces\ConfigInterface $config + * @param array|\RouterOS\Interfaces\ConfigInterface $config Array with configuration or Config object + * @param bool $autoConnect If false it will skip auto-connect stage if not need to instantiate connection * * @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException */ - public function __construct($config) + public function __construct($config, bool $autoConnect = true) { // If array then need create object if (is_array($config)) { @@ -67,7 +73,12 @@ class Client implements Interfaces\ClientInterface } // Save config if everything is okay - $this->_config = $config; + $this->config = $config; + + // Skip next step if not need to instantiate connection + if (false === $autoConnect) { + return; + } // Throw error if cannot to connect if (false === $this->connect()) { @@ -85,49 +96,24 @@ class Client implements Interfaces\ClientInterface */ private function config(string $parameter) { - return $this->_config->get($parameter); - } - - /** - * Send write query to RouterOS - * - * @param string|array|\RouterOS\Query $query - * - * @return \RouterOS\Client - * @throws \RouterOS\Exceptions\QueryException - * @deprecated - */ - public function write($query): Client - { - if (is_string($query)) { - $query = new Query($query); - } elseif (is_array($query)) { - $endpoint = array_shift($query); - $query = new Query($endpoint, $query); - } - - if (!$query instanceof Query) { - throw new QueryException('Parameters cannot be processed'); - } - - // Submit query to RouterOS - return $this->writeRAW($query); + return $this->config->get($parameter); } /** * Send write query to RouterOS (modern version of write) * - * @param string|\RouterOS\Query $endpoint Path of API query or Query object - * @param array|null $where List of where filters - * @param string|null $operations Some operations which need make on response - * @param string|null $tag Mark query with tag + * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object + * @param array|null $where List of where filters + * @param string|null $operations Some operations which need make on response + * @param string|null $tag Mark query with tag * - * @return \RouterOS\Client + * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException * @since 1.0.0 */ - public function query($endpoint, array $where = null, string $operations = null, string $tag = null): Client + public function query($endpoint, array $where = null, string $operations = null, string $tag = null): ClientInterface { // If endpoint is string then build Query object $query = ($endpoint instanceof Query) @@ -169,10 +155,10 @@ class Client implements Interfaces\ClientInterface * @param \RouterOS\Interfaces\QueryInterface $query * * @return \RouterOS\Query - * @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\QueryException + * @throws \RouterOS\Exceptions\ClientException */ - private function preQuery(array $item, Query $query): Query + private function preQuery(array $item, QueryInterface $query): QueryInterface { // Null by default $key = null; @@ -199,41 +185,66 @@ class Client implements Interfaces\ClientInterface /** * Send write query object to RouterOS * - * @param \RouterOS\Query $query + * @param \RouterOS\Interfaces\QueryInterface $query * - * @return \RouterOS\Client + * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException + * @throws \RouterOS\Exceptions\ConfigException * @since 1.0.0 */ - private function writeRAW(Query $query): Client + private function writeRAW(QueryInterface $query): ClientInterface { + $commands = $query->getQuery(); + + // Check if first command is export + if (strpos($commands[0], '/export') === 0) { + + // Convert export command with all arguments to valid SSH command + $arguments = explode('/', $commands[0]); + unset($arguments[1]); + $arguments = implode(' ', $arguments); + + // Call the router via ssh and store output of export + $this->customOutput = $this->export($arguments); + + // Return current object + return $this; + } + // Send commands via loop to router - foreach ($query->getQuery() as $command) { - $this->_connector->writeWord(trim($command)); + foreach ($commands as $command) { + $this->connector->writeWord(trim($command)); } // Write zero-terminator (empty string) - $this->_connector->writeWord(''); + $this->connector->writeWord(''); + // Return current object return $this; } /** - * Read RAW response from RouterOS + * Read RAW response from RouterOS, it can be /export command results also, not only array from API * - * @return array + * @return array|string * @since 1.0.0 */ - private function readRAW(): array + private function readRAW() { // By default response is empty $response = []; // We have to wait a !done or !fatal $lastReply = false; + // Convert strings to array and return results + if ($this->isCustomOutput()) { + // Return RAW configuration + return $this->customOutput; + } + // Read answer from socket in loop while (true) { - $word = $this->_connector->readWord(); + $word = $this->connector->readWord(); if ('' === $word) { if ($lastReply) { @@ -270,7 +281,7 @@ class Client implements Interfaces\ClientInterface * Reply ends with a complete !done or !fatal block (ended with 'empty line') * A !fatal block precedes TCP connexion close * - * @param bool $parse + * @param bool $parse If need parse output to array * * @return mixed */ @@ -279,6 +290,12 @@ class Client implements Interfaces\ClientInterface // Read RAW response $response = $this->readRAW(); + // Return RAW configuration if custom output is set + if ($this->isCustomOutput()) { + $this->customOutput = null; + return $response; + } + // Parse results and return return $parse ? $this->rosario($response) : $response; } @@ -447,7 +464,7 @@ class Client implements Interfaces\ClientInterface // => problem with legacy version, swap it and retry // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete? if ($legacyRetry && $this->isLegacy($response)) { - $this->_config->set('legacy', true); + $this->config->set('legacy', true); return $this->login(); } @@ -468,7 +485,7 @@ class Client implements Interfaces\ClientInterface * @return bool * @throws \RouterOS\Exceptions\ConfigException */ - private function isLegacy(array &$response): bool + private function isLegacy(array $response): bool { return count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); } @@ -481,7 +498,7 @@ class Client implements Interfaces\ClientInterface * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException */ - private function connect(): bool + public function connect(): bool { // By default we not connected $connected = false; @@ -494,7 +511,7 @@ class Client implements Interfaces\ClientInterface // If socket is active if (null !== $this->getSocket()) { - $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket())); + $this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket())); // If we logged in then exit from loop if (true === $this->login()) { $connected = true; @@ -512,4 +529,43 @@ class Client implements Interfaces\ClientInterface // Return status of connection return $connected; } + + /** + * Check if custom output is not empty + * + * @return bool + */ + private function isCustomOutput(): bool + { + return $this->customOutput !== null; + } + + /** + * Execute export command on remote host, it also will be used + * if "/export" command passed to query. + * + * @param string|null $arguments String with arguments which should be passed to export command + * + * @return string + * @throws \RouterOS\Exceptions\ConfigException + * @since 1.3.0 + */ + public function export(string $arguments = null): string + { + // Connect to remote host + $connection = + (new SSHConnection()) + ->timeout($this->config('timeout')) + ->to($this->config('host')) + ->onPort($this->config('ssh_port')) + ->as($this->config('user') . '+etc') + ->withPassword($this->config('pass')) + ->connect(); + + // Run export command + $command = $connection->run('/export' . ' ' . $arguments); + + // Return the output + return $command->getOutput(); + } } diff --git a/src/Config.php b/src/Config.php index c4ff807..1602a0d 100644 --- a/src/Config.php +++ b/src/Config.php @@ -6,6 +6,7 @@ use RouterOS\Exceptions\ConfigException; use RouterOS\Helpers\ArrayHelper; use RouterOS\Helpers\TypeHelper; use RouterOS\Interfaces\ConfigInterface; +use function gettype; /** * Class Config with array of parameters @@ -16,16 +17,73 @@ use RouterOS\Interfaces\ConfigInterface; class Config implements ConfigInterface { /** + * By default legacy login on RouterOS pre-6.43 is not supported + */ + public const LEGACY = false; + + /** + * Default port number + */ + public const PORT = 8728; + + /** + * Default ssl port number + */ + public const PORT_SSL = 8729; + + /** + * Do not use SSL by default + */ + public const SSL = false; + + /** + * Max timeout for answer from router + */ + public const TIMEOUT = 10; + + /** + * Count of reconnect attempts + */ + public const ATTEMPTS = 10; + + /** + * Delay between attempts in seconds + */ + public const ATTEMPTS_DELAY = 1; + + /** + * Delay between attempts in seconds + */ + public const SSH_PORT = 22; + + /** + * List of allowed parameters of config + */ + public const ALLOWED = [ + 'host' => 'string', // Address of Mikrotik RouterOS + 'user' => 'string', // Username + 'pass' => 'string', // Password + 'port' => 'integer', // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) + 'ssl' => 'boolean', // Enable ssl support (if port is not set this parameter must change default port to ssl port) + 'legacy' => 'boolean', // Support of legacy login scheme (true - pre 6.43, false - post 6.43) + 'timeout' => 'integer', // Max timeout for answer from RouterOS + 'attempts' => 'integer', // Count of attempts to establish TCP session + 'delay' => 'integer', // Delay between attempts in seconds + 'ssh_port' => 'integer', // Number of SSH port + ]; + + /** * Array of parameters (with some default values) * * @var array */ private $_parameters = [ - 'legacy' => Client::LEGACY, - 'ssl' => Client::SSL, - 'timeout' => Client::TIMEOUT, - 'attempts' => Client::ATTEMPTS, - 'delay' => Client::ATTEMPTS_DELAY + 'legacy' => self::LEGACY, + 'ssl' => self::SSL, + 'timeout' => self::TIMEOUT, + 'attempts' => self::ATTEMPTS, + 'delay' => self::ATTEMPTS_DELAY, + 'ssh_port' => self::SSH_PORT, ]; /** @@ -44,15 +102,11 @@ class Config implements ConfigInterface } /** - * Set parameter into array - * - * @param string $name - * @param mixed $value + * @inheritDoc * - * @return \RouterOS\Config - * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\ConfigException when name of configuration key is invalid or not allowed */ - public function set(string $name, $value): Config + public function set(string $name, $value): ConfigInterface { // Check of key in array if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) { @@ -60,8 +114,8 @@ class Config implements ConfigInterface } // Check what type has this value - if (TypeHelper::checkIfTypeMismatch(\gettype($value), self::ALLOWED[$name])) { - throw new ConfigException("Parameter '$name' has wrong type '" . \gettype($value) . "' but should be '" . self::ALLOWED[$name] . "'"); + if (TypeHelper::checkIfTypeMismatch(gettype($value), self::ALLOWED[$name])) { + throw new ConfigException("Parameter '$name' has wrong type '" . gettype($value) . "' but should be '" . self::ALLOWED[$name] . "'"); } // Save value to array @@ -83,21 +137,18 @@ class Config implements ConfigInterface if ($parameter === 'port' && (!isset($this->_parameters['port']) || null === $this->_parameters['port'])) { // then use default with or without ssl encryption return (isset($this->_parameters['ssl']) && $this->_parameters['ssl']) - ? Client::PORT_SSL - : Client::PORT; + ? self::PORT_SSL + : self::PORT; } return null; } /** - * Remove parameter from array by name - * - * @param string $name + * @inheritDoc * - * @return \RouterOS\Config - * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\ConfigException when parameter is not allowed */ - public function delete(string $name): Config + public function delete(string $name): ConfigInterface { // Check of key in array if (ArrayHelper::checkIfKeyNotExist($name, self::ALLOWED)) { @@ -111,12 +162,9 @@ class Config implements ConfigInterface } /** - * Return parameter of current config by name - * - * @param string $name + * @inheritDoc * - * @return mixed - * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\ConfigException when parameter is not allowed */ public function get(string $name) { @@ -129,9 +177,7 @@ class Config implements ConfigInterface } /** - * Return array with all parameters of configuration - * - * @return array + * @inheritDoc */ public function getParameters(): array { diff --git a/src/Helpers/ArrayHelper.php b/src/Helpers/ArrayHelper.php index 6be7090..b27908d 100644 --- a/src/Helpers/ArrayHelper.php +++ b/src/Helpers/ArrayHelper.php @@ -13,9 +13,10 @@ class ArrayHelper /** * Check if required single key in array of parameters * - * @param string $key - * @param array $array - * @return bool + * @param string $key + * @param array $array + * + * @return bool */ public static function checkIfKeyNotExist(string $key, array $array): bool { @@ -25,9 +26,10 @@ class ArrayHelper /** * Check if required keys in array of parameters * - * @param array $keys - * @param array $array - * @return array|bool Return true if all fine, and string with name of key which was not found + * @param array $keys + * @param array $array + * + * @return array|bool Return true if all fine, and string with name of key which was not found */ public static function checkIfKeysNotExist(array $keys, array $array) { diff --git a/src/Interfaces/ClientInterface.php b/src/Interfaces/ClientInterface.php index fad30ed..2b81e1e 100644 --- a/src/Interfaces/ClientInterface.php +++ b/src/Interfaces/ClientInterface.php @@ -2,9 +2,6 @@ namespace RouterOS\Interfaces; -use RouterOS\Client; -use RouterOS\Query; - /** * Interface ClientInterface * @@ -14,41 +11,6 @@ use RouterOS\Query; interface ClientInterface { /** - * By default legacy login on RouterOS pre-6.43 is not supported - */ - public const LEGACY = false; - - /** - * Default port number - */ - public const PORT = 8728; - - /** - * Default ssl port number - */ - public const PORT_SSL = 8729; - - /** - * Do not use SSL by default - */ - public const SSL = false; - - /** - * Max timeout for answer from router - */ - public const TIMEOUT = 10; - - /** - * Count of reconnect attempts - */ - public const ATTEMPTS = 10; - - /** - * Delay between attempts in seconds - */ - public const ATTEMPTS_DELAY = 1; - - /** * Return socket resource if is exist * * @return resource @@ -58,29 +20,42 @@ interface ClientInterface /** * Read answer from server after query was executed * - * @param bool $parse + * A Mikrotik reply is formed of blocks + * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal') + * Each block end with an zero byte (empty line) + * Reply ends with a complete !done or !fatal block (ended with 'empty line') + * A !fatal block precedes TCP connexion close + * + * @param bool $parse If need parse output to array + * * @return mixed */ - public function read(bool $parse); + public function read(bool $parse = true); /** - * Send write query to RouterOS + * Send write query to RouterOS (modern version of write) + * + * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object + * @param array|null $where List of where filters + * @param string|null $operations Some operations which need make on response + * @param string|null $tag Mark query with tag * - * @param string|array|\RouterOS\Query $query - * @return \RouterOS\Client + * @return \RouterOS\Interfaces\ClientInterface + * @throws \RouterOS\Exceptions\QueryException + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @since 1.0.0 */ - public function write($query): Client; + public function query($endpoint, array $where, string $operations, string $tag): ClientInterface; /** - * Send write query to RouterOS (modern version of write) + * Execute export command on remote host * - * @param string|Query $endpoint Path of API query or Query object - * @param array|null $where List of where filters - * @param string|null $operations Some operations which need make on response - * @param string|null $tag Mark query with tag - * @return \RouterOS\Client - * @throws \RouterOS\Exceptions\QueryException - * @since 1.0.0 + * @return string + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RuntimeException + * + * @since 1.3.0 */ - public function query($endpoint, array $where, string $operations, string $tag): Client; + public function export(): string; } diff --git a/src/Interfaces/ConfigInterface.php b/src/Interfaces/ConfigInterface.php index 463fc32..0ce8a29 100644 --- a/src/Interfaces/ConfigInterface.php +++ b/src/Interfaces/ConfigInterface.php @@ -2,8 +2,6 @@ namespace RouterOS\Interfaces; -use RouterOS\Config; - /** * Interface ConfigInterface * @@ -13,47 +11,23 @@ use RouterOS\Config; interface ConfigInterface { /** - * List of allowed parameters of config - */ - public const ALLOWED = [ - // Address of Mikrotik RouterOS - 'host' => 'string', - // Username - 'user' => 'string', - // Password - 'pass' => 'string', - // RouterOS API port number for access (if not set use default or default with SSL if SSL enabled) - 'port' => 'integer', - // Enable ssl support (if port is not set this parameter must change default port to ssl port) - 'ssl' => 'boolean', - // Support of legacy login scheme (true - pre 6.43, false - post 6.43) - 'legacy' => 'boolean', - // Max timeout for answer from RouterOS - 'timeout' => 'integer', - // Count of attempts to establish TCP session - 'attempts' => 'integer', - // Delay between attempts in seconds - 'delay' => 'integer', - ]; - - /** * Set parameter into array * * @param string $name * @param mixed $value * - * @return \RouterOS\Config + * @return \RouterOS\Interfaces\ConfigInterface */ - public function set(string $name, $value): Config; + public function set(string $name, $value): ConfigInterface; /** * Remove parameter from array by name * * @param string $parameter * - * @return \RouterOS\Config + * @return \RouterOS\Interfaces\ConfigInterface */ - public function delete(string $parameter): Config; + public function delete(string $parameter): ConfigInterface; /** * Return parameter of current config by name diff --git a/src/Interfaces/QueryInterface.php b/src/Interfaces/QueryInterface.php index cdd334f..295fe84 100644 --- a/src/Interfaces/QueryInterface.php +++ b/src/Interfaces/QueryInterface.php @@ -18,7 +18,7 @@ interface QueryInterface * @param bool|string|int $operator It may be one from list [-,=,>,<] * * @return \RouterOS\Interfaces\QueryInterface - * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\QueryException * @since 1.0.0 */ public function where(string $key, $operator = '=', $value = null): QueryInterface; diff --git a/src/Interfaces/StreamInterface.php b/src/Interfaces/StreamInterface.php index dd9a114..70cb7de 100644 --- a/src/Interfaces/StreamInterface.php +++ b/src/Interfaces/StreamInterface.php @@ -44,5 +44,5 @@ interface StreamInterface * * @return void */ - public function close(); + public function close(): void; } diff --git a/src/Laravel/ClientWrapper.php b/src/Laravel/ClientWrapper.php deleted file mode 100644 index 59cba9a..0000000 --- a/src/Laravel/ClientWrapper.php +++ /dev/null @@ -1,24 +0,0 @@ -app->bind(ClientWrapper::class); + $this->app->bind(Wrapper::class); } } diff --git a/src/Laravel/Wrapper.php b/src/Laravel/Wrapper.php new file mode 100644 index 0000000..a2be5f2 --- /dev/null +++ b/src/Laravel/Wrapper.php @@ -0,0 +1,63 @@ +client($params); + } + + /** + * Get configs of library + * + * @param array $params + * + * @return \RouterOS\Interfaces\ConfigInterface + * @throws \RouterOS\Exceptions\ConfigException + */ + public function config(array $params = []): ConfigInterface + { + $config = config('routeros-api'); + $config = array_merge($config, $params); + $config = new Config($config); + + return $config; + } + + /** + * Instantiate client object + * + * @param array $params + * @param bool $autoConnect + * + * @return \RouterOS\Interfaces\ClientInterface + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\QueryException + */ + public function client(array $params = [], bool $autoConnect = true): ClientInterface + { + $config = $this->config($params); + + return new Client($config, $autoConnect); + } +} diff --git a/src/Query.php b/src/Query.php index fe629f9..0754051 100644 --- a/src/Query.php +++ b/src/Query.php @@ -2,9 +2,11 @@ namespace RouterOS; -use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\QueryException; use RouterOS\Interfaces\QueryInterface; +use function in_array; +use function is_array; +use function is_string; /** * Class Query for building queries @@ -62,10 +64,10 @@ class Query implements QueryInterface */ public function __construct($endpoint, array $attributes = []) { - if (\is_string($endpoint)) { + if (is_string($endpoint)) { $this->setEndpoint($endpoint); $this->setAttributes($attributes); - } elseif (\is_array($endpoint)) { + } elseif (is_array($endpoint)) { $query = array_shift($endpoint); $this->setEndpoint($query); $this->setAttributes($endpoint); @@ -128,7 +130,7 @@ class Query implements QueryInterface if (null !== $operator && null !== $value) { // If operator is available in list - if (\in_array($operator, self::AVAILABLE_OPERATORS, true)) { + if (in_array($operator, self::AVAILABLE_OPERATORS, true)) { $key = $operator . $key; } else { throw new QueryException('Operator "' . $operator . '" in not in allowed list [' . implode(',', self::AVAILABLE_OPERATORS) . ']'); diff --git a/src/ResponseIterator.php b/src/ResponseIterator.php index 1498a60..2ed95be 100644 --- a/src/ResponseIterator.php +++ b/src/ResponseIterator.php @@ -2,10 +2,15 @@ namespace RouterOS; -use \Iterator, - \ArrayAccess, - \Countable, - \Serializable; +use Iterator; +use ArrayAccess; +use Countable; +use Serializable; +use function array_keys; +use function array_slice; +use function count; +use function serialize; +use function unserialize; /** * This class was created by memory save reasons, it convert response @@ -35,7 +40,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable * * @var array */ - private $raw = []; + private $raw; /** * Initial value of array position @@ -95,7 +100,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable /** * Move forward to next element */ - public function next() + public function next(): void { ++$this->current; } @@ -103,7 +108,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable /** * Previous value */ - public function prev() + public function prev(): void { --$this->current; } @@ -165,7 +170,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable /** * Rewind the Iterator to the first element */ - public function rewind() + public function rewind(): void { $this->current = 0; } @@ -176,7 +181,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable * @param mixed $offset * @param mixed $value */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->parsed[] = $value; @@ -202,7 +207,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable * * @param mixed $offset */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->parsed[$offset], $this->raw[$offset]); } @@ -245,7 +250,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable * * @param string $serialized */ - public function unserialize($serialized) + public function unserialize($serialized): void { $this->raw = unserialize($serialized, null); } diff --git a/src/ShortsTrait.php b/src/ShortsTrait.php index 92c2a3a..2287a41 100644 --- a/src/ShortsTrait.php +++ b/src/ShortsTrait.php @@ -14,30 +14,17 @@ namespace RouterOS; trait ShortsTrait { /** - * Alias for ->write() method - * - * @param string|array|\RouterOS\Query $query - * @return \RouterOS\Client - * @throws \RouterOS\Exceptions\QueryException - * @deprecated - */ - public function w($query): Client - { - return $this->write($query); - } - - /** * Alias for ->query() method * - * @param string $endpoint Path of API query - * @param array|null $where List of where filters - * @param string|null $operations Some operations which need make on response - * @param string|null $tag Mark query with tag + * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object + * @param array|null $where List of where filters + * @param string|null $operations Some operations which need make on response + * @param string|null $tag Mark query with tag + * * @return \RouterOS\Client - * @throws \RouterOS\Exceptions\QueryException * @since 1.0.0 */ - public function q(string $endpoint, array $where = null, string $operations = null, string $tag = null): Client + public function q($endpoint, array $where = null, string $operations = null, string $tag = null): Client { return $this->query($endpoint, $where, $operations, $tag); } @@ -45,7 +32,8 @@ trait ShortsTrait /** * Alias for ->read() method * - * @param bool $parse + * @param bool $parse If need parse output to array + * * @return mixed * @since 0.7 */ @@ -66,65 +54,34 @@ trait ShortsTrait } /** - * Alias for ->write()->read() combination of methods + * Alias for ->query()->read() combination of methods * - * @param string|array|\RouterOS\Query $query - * @param bool $parse - * @return array - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\QueryException - * @since 0.6 - * @deprecated - */ - public function wr($query, bool $parse = true): array - { - return $this->write($query)->read($parse); - } - - /** - * Alias for ->write()->read() combination of methods + * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object + * @param array|null $where List of where filters + * @param string|null $operations Some operations which need make on response + * @param string|null $tag Mark query with tag + * @param bool $parse If need parse output to array * - * @param string $endpoint Path of API query - * @param array|null $where List of where filters - * @param string|null $operations Some operations which need make on response - * @param string|null $tag Mark query with tag - * @param bool $parse - * @return \RouterOS\Client - * @throws \RouterOS\Exceptions\QueryException + * @return array * @since 1.0.0 */ - public function qr(string $endpoint, array $where = null, string $operations = null, string $tag = null, bool $parse = true): array + public function qr($endpoint, array $where = null, string $operations = null, string $tag = null, bool $parse = true): array { return $this->query($endpoint, $where, $operations, $tag)->read($parse); } /** - * Alias for ->write()->readAsIterator() combination of methods + * Alias for ->query()->readAsIterator() combination of methods * - * @param string|array|\RouterOS\Query $query - * @return \RouterOS\ResponseIterator - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\QueryException - * @since 1.0.0 - * @deprecated - */ - public function wri($query): ResponseIterator - { - return $this->write($query)->readAsIterator(); - } - - /** - * Alias for ->write()->read() combination of methods + * @param array|string|\RouterOS\Interfaces\QueryInterface $endpoint Path of API query or Query object + * @param array|null $where List of where filters + * @param string|null $operations Some operations which need make on response + * @param string|null $tag Mark query with tag * - * @param string $endpoint Path of API query - * @param array|null $where List of where filters - * @param string|null $operations Some operations which need make on response - * @param string|null $tag Mark query with tag * @return \RouterOS\ResponseIterator - * @throws \RouterOS\Exceptions\QueryException * @since 1.0.0 */ - public function qri(string $endpoint, array $where = null, string $operations = null, string $tag = null): ResponseIterator + public function qri($endpoint, array $where = null, string $operations = null, string $tag = null): ResponseIterator { return $this->query($endpoint, $where, $operations, $tag)->readAsIterator(); } diff --git a/src/SocketTrait.php b/src/SocketTrait.php index cfa2461..0161512 100644 --- a/src/SocketTrait.php +++ b/src/SocketTrait.php @@ -11,30 +11,30 @@ trait SocketTrait * * @var resource|null */ - private $_socket; + private $socket; /** * Code of error * * @var int */ - private $_socket_err_num; + private $socket_err_num; /** * Description of socket error * * @var string */ - private $_socket_err_str; + private $socket_err_str; /** * Initiate socket session * - * @return void - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\ConfigException + * @return void + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException */ - private function openSocket() + private function openSocket(): void { // Default: Context for ssl $context = stream_context_create([ @@ -51,8 +51,8 @@ trait SocketTrait // Initiate socket client $socket = @stream_socket_client( $proto . $this->config('host') . ':' . $this->config('port'), - $this->_socket_err_num, - $this->_socket_err_str, + $this->socket_err_num, + $this->socket_err_str, $this->config('timeout'), STREAM_CLIENT_CONNECT, $context @@ -60,12 +60,12 @@ trait SocketTrait // Throw error is socket is not initiated if (false === $socket) { - throw new ClientException('Unable to establish socket session, ' . $this->_socket_err_str); + throw new ClientException('Unable to establish socket session, ' . $this->socket_err_str); } - + //Timeout read stream_set_timeout($socket, $this->config('timeout')); - + // Save socket to static variable $this->setSocket($socket); } @@ -77,27 +77,28 @@ trait SocketTrait */ private function closeSocket(): bool { - return fclose($this->_socket); + return fclose($this->socket); } /** * Save socket resource to static variable * - * @param resource $socket + * @param resource $socket + * * @return void */ - private function setSocket($socket) + private function setSocket($socket): void { - $this->_socket = $socket; + $this->socket = $socket; } /** * Return socket resource if is exist * - * @return resource + * @return resource */ public function getSocket() { - return $this->_socket; + return $this->socket; } } diff --git a/src/Streams/ResourceStream.php b/src/Streams/ResourceStream.php index d8c143c..5139fc2 100644 --- a/src/Streams/ResourceStream.php +++ b/src/Streams/ResourceStream.php @@ -38,10 +38,10 @@ class ResourceStream implements StreamInterface } /** - * @param int $length - * @return string - * @throws \RouterOS\Exceptions\StreamException - * @throws \InvalidArgumentException + * @inheritDoc + * + * @throws \RouterOS\Exceptions\StreamException when length parameter is invalid + * @throws \InvalidArgumentException when the stream have been totally read and read method is called again */ public function read(int $length): string { @@ -60,17 +60,9 @@ class ResourceStream implements StreamInterface } /** - * Writes a string to a stream - * - * Write $length bytes of string, if not mentioned, write all the string - * Must be binary safe (as fread). - * if $length is greater than string length, write all string and return number of writen bytes - * if $length os smaller than string length, remaining bytes are losts. + * @inheritDoc * - * @param string $string - * @param int|null $length the numer of bytes to read - * @return int the number of written bytes - * @throws \RouterOS\Exceptions\StreamException + * @throws \RouterOS\Exceptions\StreamException when not possible to write bytes */ public function write(string $string, int $length = null): int { @@ -89,12 +81,11 @@ class ResourceStream implements StreamInterface } /** - * Close stream connection + * @inheritDoc * - * @return void - * @throws \RouterOS\Exceptions\StreamException + * @throws \RouterOS\Exceptions\StreamException when not possible to close the stream */ - public function close() + public function close(): void { $hasBeenClosed = false; diff --git a/src/Streams/StringStream.php b/src/Streams/StringStream.php index e845f32..a1af80e 100644 --- a/src/Streams/StringStream.php +++ b/src/Streams/StringStream.php @@ -2,6 +2,7 @@ namespace RouterOS\Streams; +use InvalidArgumentException; use RouterOS\Interfaces\StreamInterface; use RouterOS\Exceptions\StreamException; @@ -31,18 +32,18 @@ class StringStream implements StreamInterface $this->buffer = $string; } + /** - * {@inheritDoc} + * @inheritDoc * - * @throws \InvalidArgumentException when length parameter is invalid - * @throws StreamException when the stream have been tatly red and read methd is called again + * @throws \RouterOS\Exceptions\StreamException */ public function read(int $length): string { $remaining = strlen($this->buffer); if ($length < 0) { - throw new \InvalidArgumentException('Cannot read a negative count of bytes from a stream'); + throw new InvalidArgumentException('Cannot read a negative count of bytes from a stream'); } if (0 === $remaining) { @@ -65,12 +66,9 @@ class StringStream implements StreamInterface } /** - * Fake write method, do nothing except return the "writen" length + * @inheritDoc * - * @param string $string The string to write - * @param int|null $length the number of characters to write - * @return int number of "writen" bytes - * @throws \InvalidArgumentException on invalid length + * @throws \InvalidArgumentException on invalid length */ public function write(string $string, int $length = null): int { @@ -79,18 +77,16 @@ class StringStream implements StreamInterface } if ($length < 0) { - throw new \InvalidArgumentException('Cannot write a negative count of bytes'); + throw new InvalidArgumentException('Cannot write a negative count of bytes'); } return min($length, strlen($string)); } /** - * Close stream connection - * - * @return void + * @inheritDoc */ - public function close() + public function close(): void { $this->buffer = ''; } diff --git a/tests/APIConnectorTest.php b/tests/APIConnectorTest.php index b4784fe..7644495 100644 --- a/tests/APIConnectorTest.php +++ b/tests/APIConnectorTest.php @@ -25,7 +25,7 @@ class APIConnectorTest extends TestCase * @param StreamInterface $stream Cannot typehint, PHP refuse it * @param bool $closeResource shall we close the resource ? */ - public function test_construct(StreamInterface $stream, bool $closeResource = false) + public function testConstruct(StreamInterface $stream, bool $closeResource = false): void { $apiStream = new APIConnector($stream); $this->assertInstanceOf(APIConnector::class, $apiStream); @@ -38,7 +38,7 @@ class APIConnectorTest extends TestCase { return [ [new ResourceStream(fopen(__FILE__, 'rb')),], // Myself, sure I exists - [new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN'))),], // Socket + [new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN')))], // Socket [new ResourceStream(STDIN), false], // Try it, but do not close STDIN please !!! [new StringStream('Hello World !!!')], // Try it, but do not close STDIN please !!! [new StringStream('')], // Try it, but do not close STDIN please !!! @@ -53,7 +53,7 @@ class APIConnectorTest extends TestCase * @param APIConnector $connector * @param string $expected */ - public function test__readWord(APIConnector $connector, string $expected) + public function testReadWord(APIConnector $connector, string $expected): void { $this->assertSame($expected, $connector->readWord()); } @@ -79,7 +79,7 @@ class APIConnectorTest extends TestCase * @param string $toWrite * @param int $expected */ - public function test_writeWord(APIConnector $connector, string $toWrite, int $expected) + public function testWriteWord(APIConnector $connector, string $toWrite, int $expected): void { $this->assertEquals($expected, $connector->writeWord($toWrite)); } diff --git a/tests/APILengthCoDecTest.php b/tests/APILengthCoDecTest.php index 5b59514..74322e0 100644 --- a/tests/APILengthCoDecTest.php +++ b/tests/APILengthCoDecTest.php @@ -2,9 +2,8 @@ namespace RouterOS\Tests; +use DomainException; use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Constraint\IsType; - use RouterOS\APILengthCoDec; use RouterOS\Streams\StringStream; use RouterOS\Helpers\BinaryStringHelper; @@ -18,11 +17,13 @@ class APILengthCoDecTest extends TestCase { /** * @dataProvider encodeLengthNegativeProvider - * @expectedException \DomainException * @covers ::encodeLength + * + * @param $length */ - public function test__encodeLengthNegative($length) + public function testEncodeLengthNegative($length): void { + $this->expectException(DomainException::class); APILengthCoDec::encodeLength($length); } @@ -37,8 +38,11 @@ class APILengthCoDecTest extends TestCase /** * @dataProvider encodedLengthProvider * @covers ::encodeLength + * + * @param $expected + * @param $length */ - public function test__encodeLength($expected, $length) + public function testEncodeLength($expected, $length): void { $this->assertEquals(BinaryStringHelper::IntegerToNBOBinaryString((int) $expected), APILengthCoDec::encodeLength($length)); } @@ -76,8 +80,11 @@ class APILengthCoDecTest extends TestCase /** * @dataProvider encodedLengthProvider * @covers ::decodeLength + * + * @param $encodedLength + * @param $expected */ - public function test__decodeLength($encodedLength, $expected) + public function testDecodeLength($encodedLength, $expected): void { // We have to provide $encodedLength as a "bytes" stream $stream = new StringStream(BinaryStringHelper::IntegerToNBOBinaryString($encodedLength)); @@ -87,10 +94,12 @@ class APILengthCoDecTest extends TestCase /** * @dataProvider decodeLengthControlWordProvider * @covers ::decodeLength - * @expectedException \UnexpectedValueException + * + * @param string $encodedLength */ - public function test_decodeLengthControlWord(string $encodedLength) + public function testDecodeLengthControlWord(string $encodedLength): void { + $this->expectException(\UnexpectedValueException::class); APILengthCoDec::decodeLength(new StringStream($encodedLength)); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6dda7da..3841efd 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -2,6 +2,7 @@ namespace RouterOS\Tests; +use Exception; use PHPUnit\Framework\TestCase; use RouterOS\Client; use RouterOS\Exceptions\ConfigException; @@ -15,7 +16,12 @@ class ClientTest extends TestCase /** * @var array */ - public $router; + public $config; + + /** + * @var \RouterOS\Client + */ + public $client; /** * @var int @@ -27,86 +33,100 @@ class ClientTest extends TestCase */ public $port_legacy; - public function setUp() + public function setUp(): void { - parent::setUp(); - - $this->router = [ - 'user' => getenv('ROS_USER'), - 'pass' => getenv('ROS_PASS'), - 'host' => getenv('ROS_HOST'), + $this->config = [ + 'user' => getenv('ROS_USER'), + 'pass' => getenv('ROS_PASS'), + 'host' => getenv('ROS_HOST'), + 'ssh_port' => (int) getenv('ROS_SSH_PORT'), ]; + $this->client = new Client($this->config); + $this->port_modern = (int) getenv('ROS_PORT_MODERN'); $this->port_legacy = (int) getenv('ROS_PORT_LEGACY'); } - public function test__construct(): void + public function testConstruct(): void { try { $config = new Config(); $config - ->set('user', $this->router['user']) - ->set('pass', $this->router['pass']) - ->set('host', $this->router['host']); + ->set('user', $this->config['user']) + ->set('pass', $this->config['pass']) + ->set('host', $this->config['host']); $obj = new Client($config); $this->assertIsObject($obj); $socket = $obj->getSocket(); $this->assertIsResource($socket); - } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + } catch (Exception $e) { + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function test__construct2(): void + public function testConstruct2(): void { try { - $config = new Config($this->router); + $config = new Config($this->config); $obj = new Client($config); $this->assertIsObject($obj); $socket = $obj->getSocket(); $this->assertIsResource($socket); - } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + } catch (Exception $e) { + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function test__construct3(): void + public function testConstruct3(): void { try { - $obj = new Client($this->router); + $obj = new Client($this->config); $this->assertIsObject($obj); $socket = $obj->getSocket(); $this->assertIsResource($socket); - } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + } catch (Exception $e) { + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function test__constructEx(): void + public function testConstructException(): void { $this->expectException(ConfigException::class); - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], + new Client([ + 'user' => $this->config['user'], + 'pass' => $this->config['pass'], + ]); + } + + public function testConstructExceptionBadHost(): void + { + $this->expectException(ClientException::class); + + new Client([ + 'host' => '127.0.0.1', + 'port' => 123456, + 'attempts' => 0, + 'user' => $this->config['user'], + 'pass' => $this->config['pass'], ]); } - public function test__constructLegacy(): void + public function testConstructLegacy(): void { try { $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], + 'user' => $this->config['user'], + 'pass' => $this->config['pass'], + 'host' => $this->config['host'], 'port' => $this->port_legacy, 'legacy' => true ]); $this->assertIsObject($obj); - } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + } catch (Exception $e) { + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } @@ -115,46 +135,42 @@ class ClientTest extends TestCase * * login() method recognise legacy router response and swap to legacy mode */ - public function test__constructLegacy2(): void + public function testConstructLegacy2(): void { try { $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], + 'user' => $this->config['user'], + 'pass' => $this->config['pass'], + 'host' => $this->config['host'], 'port' => $this->port_legacy, 'legacy' => false ]); $this->assertIsObject($obj); - } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + } catch (Exception $e) { + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - - public function test__constructWrongPass(): void + public function testConstructWrongPass(): void { $this->expectException(ClientException::class); - $obj = new Client([ - 'user' => $this->router['user'], + new Client([ + 'user' => $this->config['user'], 'pass' => 'admin2', - 'host' => $this->router['host'], + 'host' => $this->config['host'], 'attempts' => 2 ]); } - /** - * @expectedException ClientException - */ - public function test__constructWrongNet(): void + public function testConstructWrongNet(): void { $this->expectException(ClientException::class); - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], + new Client([ + 'user' => $this->config['user'], + 'pass' => $this->config['pass'], + 'host' => $this->config['host'], 'port' => 11111, 'attempts' => 2 ]); @@ -162,41 +178,33 @@ class ClientTest extends TestCase public function testQueryRead(): void { - $config = new Config(); - $config - ->set('user', $this->router['user']) - ->set('pass', $this->router['pass']) - ->set('host', $this->router['host']); - - $obj = new Client($config); - /* * Build query with where */ - $read = $obj->query('/system/package/print', ['name'])->read(); - $this->assertCount(13, $read); + $read = $this->client->query('/system/package/print', ['name'])->read(); + $this->assertNotEmpty($read); - $read = $obj->query('/system/package/print', ['.id', '*1'])->read(); + $read = $this->client->query('/system/package/print', ['.id', '*1'])->read(); $this->assertCount(1, $read); - $read = $obj->query('/system/package/print', ['.id', '=', '*1'])->read(); + $read = $this->client->query('/system/package/print', ['.id', '=', '*1'])->read(); $this->assertCount(1, $read); - $read = $obj->query('/system/package/print', [['name']])->read(); - $this->assertCount(13, $read); + $read = $this->client->query('/system/package/print', [['name']])->read(); + $this->assertNotEmpty($read); - $read = $obj->query('/system/package/print', [['.id', '*1']])->read(); + $read = $this->client->query('/system/package/print', [['.id', '*1']])->read(); $this->assertCount(1, $read); - $read = $obj->query('/system/package/print', [['.id', '=', '*1']])->read(); + $read = $this->client->query('/system/package/print', [['.id', '=', '*1']])->read(); $this->assertCount(1, $read); /* * Build query with operations */ - $read = $obj->query('/interface/print', [ + $read = $this->client->query('/interface/print', [ ['type', 'ether'], ['type', 'vlan'] ], '|')->read(); @@ -207,68 +215,80 @@ class ClientTest extends TestCase * Build query with tag */ - $read = $obj->query('/system/package/print', null, null, 'zzzz')->read(); - $this->assertCount(13, $read); + $read = $this->client->query('/system/package/print', null, null, 'zzzz')->read(); + + // $this->assertCount(13, $read); $this->assertEquals('zzzz', $read[0]['tag']); } public function testReadAsIterator(): void { - $obj = new Client($this->router); - - $obj = $obj->write('/system/package/print')->readAsIterator(); - $this->assertIsObject($obj); + $result = $this->client->query('/system/package/print')->readAsIterator(); + $this->assertIsObject($result); } public function testWriteReadString(): void { - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], - ]); - - $readTrap = $obj->wr('/interface', false); + $readTrap = $this->client->query('/interface')->read(false); $this->assertCount(3, $readTrap); $this->assertEquals('!trap', $readTrap[0]); } public function testFatal(): void { - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], - ]); - - $readTrap = $obj->query('/quit')->read(); + $readTrap = $this->client->query('/quit')->read(); $this->assertCount(2, $readTrap); $this->assertEquals('!fatal', $readTrap[0]); } - public function testQueryEx1(): void + public function queryExceptionDataProvider(): array { - $this->expectException(ClientException::class); - - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], - ]); + return [ + // Wrong amount of parameters + ['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [[]]], + ['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [[], ['a', 'b', 'c']]], + ['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => ['a', 'b', 'c', 'd']], + ['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [['a', 'b', 'c', 'd']]], + ['exception' => ClientException::class, 'endpoint' => '/quiet', 'attributes' => [['a', 'b', 'c', 'd'], ['a', 'b', 'c']]], + // Wrong type of endpoint + ['exception' => QueryException::class, 'endpoint' => 1, 'attributes' => null], + ]; + } - $obj->query('/quiet', ['a', 'b', 'c', 'd']); + /** + * @dataProvider queryExceptionDataProvider + * + * @param string $exception + * @param mixed $endpoint + * @param mixed $attributes + * + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\QueryException + */ + public function testQueryException(string $exception, $endpoint, $attributes): void + { + $this->expectException($exception); + $this->client->query($endpoint, $attributes); } - public function testQueryEx2(): void + public function testExportMethod(): void { - $this->expectException(ClientException::class); + if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) { + $this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage'); + } - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], - ]); + $result = $this->client->export(); + $this->assertNotEmpty($result); + } + + public function testExportQuery(): void + { + if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) { + $this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage'); + } - $obj->query('/quiet', [[]]); + $result = $this->client->query('/export'); + $this->assertNotEmpty($result); } } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index d444888..2aa17cc 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -8,58 +8,58 @@ use RouterOS\Exceptions\ConfigException; class ConfigTest extends TestCase { - public function test__construct() + public function testConstruct(): void { try { $obj = new Config(); - $this->assertInternalType('object', $obj); + $this->assertIsObject($obj); } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function testGetParameters() + public function testGetParameters(): void { $obj = new Config(); $params = $obj->getParameters(); - $this->assertCount(5, $params); - $this->assertEquals($params['legacy'], false); - $this->assertEquals($params['ssl'], false); - $this->assertEquals($params['timeout'], 10); - $this->assertEquals($params['attempts'], 10); - $this->assertEquals($params['delay'], 1); + $this->assertCount(6, $params); + $this->assertEquals(false, $params['legacy']); + $this->assertEquals(false, $params['ssl']); + $this->assertEquals(10, $params['timeout']); + $this->assertEquals(10, $params['attempts']); + $this->assertEquals(1, $params['delay']); } - public function testGetParameters2() + public function testGetParameters2(): void { $obj = new Config(['timeout' => 100]); $params = $obj->getParameters(); - $this->assertCount(5, $params); - $this->assertEquals($params['timeout'], 100); + $this->assertCount(6, $params); + $this->assertEquals(100, $params['timeout']); } - public function testSet() + public function testSet(): void { $obj = new Config(); $obj->set('timeout', 111); $params = $obj->getParameters(); - $this->assertEquals($params['timeout'], 111); + $this->assertEquals(111, $params['timeout']); } - public function testSetArr() + public function testSetArr(): void { $obj = new Config([ 'timeout' => 111 ]); $params = $obj->getParameters(); - $this->assertEquals($params['timeout'], 111); + $this->assertEquals(111, $params['timeout']); } - public function testDelete() + public function testDelete(): void { $obj = new Config(); $obj->delete('timeout'); @@ -68,7 +68,7 @@ class ConfigTest extends TestCase $this->assertArrayNotHasKey('timeout', $params); } - public function testDeleteEx() + public function testDeleteEx(): void { $this->expectException(ConfigException::class); @@ -76,7 +76,7 @@ class ConfigTest extends TestCase $obj->delete('wrong'); } - public function testSetEx1() + public function testSetExceptionWrongType(): void { $this->expectException(ConfigException::class); @@ -84,7 +84,7 @@ class ConfigTest extends TestCase $obj->set('delay', 'some string'); } - public function testSetEx2() + public function testSetExceptionWrongKey(): void { $this->expectException(ConfigException::class); @@ -92,27 +92,26 @@ class ConfigTest extends TestCase $obj->set('wrong', 'some string'); } - public function testGet() + public function testGet(): void { $obj = new Config(); $test1 = $obj->get('legacy'); - $this->assertEquals($test1, false); + $this->assertEquals(false, $test1); $test2 = $obj->get('port'); - $this->assertEquals($test2, 8728); + $this->assertEquals(8728, $test2); $obj->set('port', 10000); $test3 = $obj->get('port'); - $this->assertEquals($test3, 10000); - + $this->assertEquals(10000, $test3); $obj->delete('port'); $obj->set('ssl', true); $test3 = $obj->get('port'); - $this->assertEquals($test3, 8729); + $this->assertEquals(8729, $test3); } - public function testGetEx() + public function testGetEx(): void { $this->expectException(ConfigException::class); diff --git a/tests/Helpers/ArrayHelperTest.php b/tests/Helpers/ArrayHelperTest.php index bb4a2dc..d980a40 100644 --- a/tests/Helpers/ArrayHelperTest.php +++ b/tests/Helpers/ArrayHelperTest.php @@ -7,7 +7,7 @@ use RouterOS\Helpers\ArrayHelper; class ArrayHelperTest extends TestCase { - public function testCheckIfKeyNotExist() + public function testCheckIfKeyNotExist(): void { $test1 = ArrayHelper::checkIfKeyNotExist(1, [0 => 'a', 1 => 'b', 2 => 'c']); $this->assertFalse($test1); @@ -16,7 +16,7 @@ class ArrayHelperTest extends TestCase $this->assertTrue($test2); } - public function testCheckIfKeysNotExist() + public function testCheckIfKeysNotExist(): void { $test1 = ArrayHelper::checkIfKeysNotExist([1, 2], [0 => 'a', 1 => 'b', 2 => 'c']); $this->assertTrue($test1); diff --git a/tests/Helpers/BinaryStringHelperTest.php b/tests/Helpers/BinaryStringHelperTest.php index 014f17a..716ebff 100644 --- a/tests/Helpers/BinaryStringHelperTest.php +++ b/tests/Helpers/BinaryStringHelperTest.php @@ -16,8 +16,11 @@ class BinaryStringHelperTest extends TestCase /** * @dataProvider IntegerToNBOBinaryStringProvider * @covers ::IntegerToNBOBinaryString + * + * @param $value + * @param $expected */ - public function test__IntegerToNBOBinaryString($value, $expected) + public function testIntegerToNBOBinaryString($value, $expected): void { $this->assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value)); } diff --git a/tests/Helpers/TypeHelperTest.php b/tests/Helpers/TypeHelperTest.php index ab08d2b..c00468e 100644 --- a/tests/Helpers/TypeHelperTest.php +++ b/tests/Helpers/TypeHelperTest.php @@ -7,7 +7,7 @@ use RouterOS\Helpers\TypeHelper; class TypeHelperTest extends TestCase { - public function testCheckIfTypeMismatch() + public function testCheckIfTypeMismatch(): void { $test1 = TypeHelper::checkIfTypeMismatch(gettype(true), gettype(false)); $this->assertFalse($test1); diff --git a/tests/Laravel/ServiceProviderTests.php b/tests/Laravel/ServiceProviderTests.php new file mode 100644 index 0000000..468fc2f --- /dev/null +++ b/tests/Laravel/ServiceProviderTests.php @@ -0,0 +1,62 @@ +assertInstanceOf(Wrapper::class, $manager); + } + + public function testConfig(): void + { + $config = \RouterOS::config([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin' + ]); + $this->assertInstanceOf(Config::class, $config); + + $params = $config->getParameters(); + $this->assertArrayHasKey('host', $params); + $this->assertArrayHasKey('user', $params); + $this->assertArrayHasKey('pass', $params); + $this->assertArrayHasKey('ssl', $params); + $this->assertArrayHasKey('legacy', $params); + $this->assertArrayHasKey('timeout', $params); + $this->assertArrayHasKey('attempts', $params); + $this->assertArrayHasKey('delay', $params); + } + + public function testClient(): void + { + $client = \RouterOS::client([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin' + ], false); + + $this->assertEquals(get_class_methods($client), $this->client); + } +} diff --git a/tests/Laravel/TestCase.php b/tests/Laravel/TestCase.php new file mode 100644 index 0000000..8faf6f6 --- /dev/null +++ b/tests/Laravel/TestCase.php @@ -0,0 +1,35 @@ + Facade::class, + ]; + } +} diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 2f5c701..4a91071 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -8,33 +8,33 @@ use RouterOS\Query; class QueryTest extends TestCase { - public function test__construct(): void + public function testConstruct(): void { try { $obj = new Query('test'); $this->assertIsObject($obj); } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function test__construct_arr(): void + public function testConstructArr(): void { try { $obj = new Query('test', ['line1', 'line2', 'line3']); $this->assertIsObject($obj); } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } - public function test__construct_arr2(): void + public function testConstructArr2(): void { try { $obj = new Query(['test', 'line1', 'line2', 'line3']); $this->assertIsObject($obj); } catch (\Exception $e) { - $this->assertContains('Must be initialized ', $e->getMessage()); + $this->assertStringContainsString('Must be initialized ', $e->getMessage()); } } @@ -42,14 +42,14 @@ class QueryTest extends TestCase { $obj = new Query('test'); $test = $obj->getEndpoint(); - $this->assertEquals($test, 'test'); + $this->assertEquals('test', $test); } public function testGetEndpoint2(): void { $obj = new Query(['zzz', 'line1', 'line2', 'line3']); $test = $obj->getEndpoint(); - $this->assertEquals($test, 'zzz'); + $this->assertEquals('zzz', $test); } public function testGetEndpointEx(): void @@ -65,7 +65,7 @@ class QueryTest extends TestCase $obj = new Query('test'); $obj->setEndpoint('zzz'); $test = $obj->getEndpoint(); - $this->assertEquals($test, 'zzz'); + $this->assertEquals('zzz', $test); } public function testGetAttributes(): void @@ -104,6 +104,18 @@ class QueryTest extends TestCase $this->assertEquals($attrs[1], '?key2=value2'); } + + public function testEqual(): void + { + $obj = new Query('test'); + $obj->equal('key1', 'value1'); + $obj->equal('key2', 'value2'); + + $attrs = $obj->getAttributes(); + $this->assertCount(2, $attrs); + $this->assertEquals($attrs[1], '=key2=value2'); + } + public function testTag(): void { $obj = new Query('/test/test'); diff --git a/tests/ResponseIteratorTest.php b/tests/ResponseIteratorTest.php index 33d2dbd..b01d3c8 100644 --- a/tests/ResponseIteratorTest.php +++ b/tests/ResponseIteratorTest.php @@ -4,38 +4,34 @@ namespace RouterOS\Tests; use PHPUnit\Framework\TestCase; use RouterOS\Client; +use RouterOS\ResponseIterator; class ResponseIteratorTest extends TestCase { - public function test__construct() + /** + * @var \RouterOS\Client + */ + private $client; + + public function setUp(): void { - $obj = new Client([ + $this->client = new Client([ 'user' => getenv('ROS_USER'), 'pass' => getenv('ROS_PASS'), 'host' => getenv('ROS_HOST'), ]); - - $obj = $obj->write('/system/package/print')->readAsIterator(); - $this->assertIsObject($obj); } - public function testReadWrite() + public function testReadWrite(): void { - $obj = new Client([ - 'user' => getenv('ROS_USER'), - 'pass' => getenv('ROS_PASS'), - 'host' => getenv('ROS_HOST'), - ]); - - $readTrap = $obj->write('/system/package/print')->readAsIterator(); - // Read from RAW - $this->assertCount(13, $readTrap); + $readTrap = $this->client->query('/system/logging/print')->readAsIterator(); + $this->assertNotEmpty($readTrap); - $readTrap = $obj->write('/ip/address/print')->readAsIterator(); + $readTrap = $this->client->query('/ip/address/print')->readAsIterator(); $this->assertCount(1, $readTrap); $this->assertEquals('ether1', $readTrap[0]['interface']); - $readTrap = $obj->write('/system/package/print')->readAsIterator(); + $readTrap = $this->client->query('/system/logging/print')->readAsIterator(); $key = $readTrap->key(); $this->assertEquals(0, $key); $current = $readTrap->current(); @@ -62,14 +58,9 @@ class ResponseIteratorTest extends TestCase public function testSerialize(): void { - $obj = new Client([ - 'user' => getenv('ROS_USER'), - 'pass' => getenv('ROS_PASS'), - 'host' => getenv('ROS_HOST'), - ]); - - $read = $obj->write('/queue/simple/print')->readAsIterator(); + $read = $this->client->query('/queue/simple/print')->readAsIterator(); $serialize = $read->serialize(); + $this->assertEquals('a:1:{i:0;a:1:{i:0;s:5:"!done";}}', $serialize); } diff --git a/tests/Streams/ResourceStreamTest.php b/tests/Streams/ResourceStreamTest.php index fedb316..8ce56d4 100644 --- a/tests/Streams/ResourceStreamTest.php +++ b/tests/Streams/ResourceStreamTest.php @@ -2,8 +2,10 @@ namespace RouterOS\Tests\Streams; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Constraint\IsType; +use RouterOS\Exceptions\StreamException; use RouterOS\Streams\ResourceStream; /** @@ -17,14 +19,14 @@ class ResourceStreamTest extends TestCase * Test that constructor throws an InvalidArgumentException on bad parameter type * * @covers ::__construct - * @expectedException \InvalidArgumentException * @dataProvider constructNotResourceProvider * * @param $notResource */ - public function test__constructNotResource($notResource) + public function testConstructNotResource($notResource): void { + $this->expectException(InvalidArgumentException::class); new ResourceStream($notResource); } @@ -56,12 +58,17 @@ class ResourceStreamTest extends TestCase * @param resource $resource Cannot typehint, PHP refuse it * @param bool $closeResource shall we close the resource ? */ - public function test_construct($resource, bool $closeResource = true) + public function testConstruct($resource, bool $closeResource = true): void { - $resourceStream = new ResourceStream($resource); + $resourceStream = new class($resource) extends ResourceStream { + public function getStream() + { + return $this->stream; + } + }; - $stream = $this->getObjectAttribute($resourceStream, 'stream'); - $this->assertInternalType(IsType::TYPE_RESOURCE, $stream); + $stream = $resourceStream->getStream(); + $this->assertIsResource($stream); if ($closeResource) { fclose($resource); @@ -89,12 +96,13 @@ class ResourceStreamTest extends TestCase * @covers ::read * @dataProvider readProvider * - * @param ResourceStream $stream Cannot typehint, PHP refuse it - * @param string $expected the result we should have - * @throws \RouterOS\Exceptions\StreamException - * @throws \InvalidArgumentException + * @param ResourceStream $stream Cannot typehint, PHP refuse it + * @param string $expected the result we should have + * + * @throws \RouterOS\Exceptions\StreamException + * @throws \InvalidArgumentException */ - public function test__read(ResourceStream $stream, string $expected) + public function testRead(ResourceStream $stream, string $expected): void { $this->assertSame($expected, $stream->read(strlen($expected))); } @@ -115,15 +123,16 @@ class ResourceStreamTest extends TestCase * * @covers ::read * @dataProvider readBadLengthProvider - * @expectedException \InvalidArgumentException * - * @param ResourceStream $stream Cannot typehint, PHP refuse it - * @param int $length + * @param ResourceStream $stream Cannot typehint, PHP refuse it + * @param int $length + * * @throws \RouterOS\Exceptions\StreamException * @throws \InvalidArgumentException */ - public function test__readBadLength(ResourceStream $stream, int $length) + public function testReadBadLength(ResourceStream $stream, int $length): void { + $this->expectException(InvalidArgumentException::class); $stream->read($length); } @@ -143,13 +152,13 @@ class ResourceStreamTest extends TestCase * * @covers ::read * @dataProvider readBadResourceProvider - * @expectedException \RouterOS\Exceptions\StreamException * - * @param ResourceStream $stream Cannot typehint, PHP refuse it - * @param int $length + * @param ResourceStream $stream Cannot typehint, PHP refuse it + * @param int $length */ - public function test__readBadResource(ResourceStream $stream, int $length) + public function testReadBadResource(ResourceStream $stream, int $length): void { + $this->expectException(StreamException::class); $stream->read($length); } @@ -169,11 +178,12 @@ class ResourceStreamTest extends TestCase * @covers ::write * @dataProvider writeProvider * - * @param ResourceStream $stream to test - * @param string $toWrite the writed string + * @param ResourceStream $stream to test + * @param string $toWrite the writed string + * * @throws \RouterOS\Exceptions\StreamException */ - public function test__write(ResourceStream $stream, string $toWrite) + public function testWrite(ResourceStream $stream, string $toWrite): void { $this->assertEquals(strlen($toWrite), $stream->write($toWrite)); } @@ -193,13 +203,13 @@ class ResourceStreamTest extends TestCase * * @covers ::write * @dataProvider writeBadResourceProvider - * @expectedException \RouterOS\Exceptions\StreamException * * @param ResourceStream $stream to test * @param string $toWrite the written string */ - public function test__writeBadResource(ResourceStream $stream, string $toWrite) + public function testWriteBadResource(ResourceStream $stream, string $toWrite): void { + $this->expectException(StreamException::class); $stream->write($toWrite); } @@ -219,12 +229,12 @@ class ResourceStreamTest extends TestCase * * @covers ::close * @dataProvider doubleCloseProvider - * @expectedException \RouterOS\Exceptions\StreamException * * @param ResourceStream $stream to test */ - public function test_doubleClose(ResourceStream $stream) + public function testDoubleClose(ResourceStream $stream): void { + $this->expectException(StreamException::class); $stream->close(); $stream->close(); } @@ -242,13 +252,13 @@ class ResourceStreamTest extends TestCase * @covers ::close * @covers ::write * @dataProvider writeClosedResourceProvider - * @expectedException \RouterOS\Exceptions\StreamException * * @param ResourceStream $stream to test * @param string $toWrite the written string */ - public function test_close(ResourceStream $stream, string $toWrite) + public function testClose(ResourceStream $stream, string $toWrite) { + $this->expectException(StreamException::class); $stream->close(); $stream->write($toWrite); } diff --git a/tests/Streams/StringStreamTest.php b/tests/Streams/StringStreamTest.php index 4fb4f1c..9943a5a 100644 --- a/tests/Streams/StringStreamTest.php +++ b/tests/Streams/StringStreamTest.php @@ -19,9 +19,9 @@ class StringStreamTest extends TestCase * @covers ::__construct * @dataProvider constructProvider * - * @param string $string + * @param string $string */ - public function test__construct(string $string) + public function testConstruct(string $string): void { $this->assertInstanceOf(StringStream::class, new StringStream($string)); } @@ -36,19 +36,18 @@ class StringStreamTest extends TestCase ]; } - /** * Test that write function returns the effective written bytes * * @covers ::write * @dataProvider writeProvider * - * @param string $string the string to write - * @param int|null $length the count if bytes to write - * @param int $expected the number of bytes that must be writen + * @param string $string the string to write + * @param int|null $length the count if bytes to write + * @param int $expected the number of bytes that must be writen */ - public function test__write(string $string, $length, int $expected) + public function testWrite(string $string, $length, int $expected): void { $stream = new StringStream('Does not matters'); if (null === $length) { @@ -79,10 +78,10 @@ class StringStreamTest extends TestCase /** * @covers ::write - * @expectedException \InvalidArgumentException */ - public function test__writeWithNegativeLength() + public function testWriteWithNegativeLength(): void { + $this->expectException(\InvalidArgumentException::class); $stream = new StringStream('Does not matters'); $stream->write('PLOP', -1); } @@ -92,7 +91,7 @@ class StringStreamTest extends TestCase * * @throws \RouterOS\Exceptions\StreamException */ - public function test__read() + public function testRead(): void { $stream = new StringStream('123456789'); @@ -105,12 +104,11 @@ class StringStreamTest extends TestCase } /** - * @expectedException \InvalidArgumentException - * * @throws \RouterOS\Exceptions\StreamException */ - public function test__readBadLength() + public function testReadBadLength(): void { + $this->expectException(\InvalidArgumentException::class); $stream = new StringStream('123456789'); $stream->read(-1); } @@ -118,14 +116,15 @@ class StringStreamTest extends TestCase /** * @covers ::read * @dataProvider readWhileEmptyProvider - * @expectedException \RouterOS\Exceptions\StreamException * - * @param StringStream $stream - * @param int $length - * @throws \RouterOS\Exceptions\StreamException + * @param StringStream $stream + * @param int $length + * + * @throws \RouterOS\Exceptions\StreamException */ - public function test__readWhileEmpty(StringStream $stream, int $length) + public function testReadWhileEmpty(StringStream $stream, int $length): void { + $this->expectException(\RouterOS\Exceptions\StreamException::class); $stream->read($length); } @@ -133,7 +132,7 @@ class StringStreamTest extends TestCase * @return \Generator * @throws StreamException */ - public function readWhileEmptyProvider() + public function readWhileEmptyProvider(): ?\Generator { $stream = new StringStream('123456789'); $stream->read(9); @@ -148,11 +147,9 @@ class StringStreamTest extends TestCase yield [$stream, 1]; } - /** - * @expectedException \RouterOS\Exceptions\StreamException - */ - public function testReadClosed() + public function testReadClosed(): void { + $this->expectException(\RouterOS\Exceptions\StreamException::class); $stream = new StringStream('123456789'); $stream->close(); $stream->read(1);