From 3dc18f96f2439ef051e6724b5b892f8f8cdf77a7 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 22:16:10 +0300 Subject: [PATCH 01/38] Classes which required by Laravel was renamed --- src/Laravel/{ClientFacade.php => Facade.php} | 0 src/Laravel/{ClientServiceProvider.php => ServiceProvider.php} | 0 src/Laravel/{ClientWrapper.php => Wrapper.php} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/Laravel/{ClientFacade.php => Facade.php} (100%) rename src/Laravel/{ClientServiceProvider.php => ServiceProvider.php} (100%) rename src/Laravel/{ClientWrapper.php => Wrapper.php} (100%) diff --git a/src/Laravel/ClientFacade.php b/src/Laravel/Facade.php similarity index 100% rename from src/Laravel/ClientFacade.php rename to src/Laravel/Facade.php diff --git a/src/Laravel/ClientServiceProvider.php b/src/Laravel/ServiceProvider.php similarity index 100% rename from src/Laravel/ClientServiceProvider.php rename to src/Laravel/ServiceProvider.php diff --git a/src/Laravel/ClientWrapper.php b/src/Laravel/Wrapper.php similarity index 100% rename from src/Laravel/ClientWrapper.php rename to src/Laravel/Wrapper.php From 7d18e1bd54d556d7aae835e3331ccbc5584391db Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 22:48:00 +0300 Subject: [PATCH 02/38] Additional options added to phpunit.xml --- phpunit.xml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 1d3da08..b065af3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,18 @@ - + ./src - ./tests + ./tests From e572adb9848723fd3dafba93fcf918e00e728edb Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 22:49:18 +0300 Subject: [PATCH 03/38] Finetune of code --- src/APIConnector.php | 5 +++-- src/APILengthCoDec.php | 16 +++++++++------- src/Client.php | 34 +++++++++++++++++++--------------- src/Config.php | 5 +++-- src/Query.php | 21 ++++++++++++--------- src/ResponseIterator.php | 12 ++++++------ src/SocketTrait.php | 19 ++++++++++--------- 7 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/APIConnector.php b/src/APIConnector.php index f85c013..9da509d 100644 --- a/src/APIConnector.php +++ b/src/APIConnector.php @@ -48,8 +48,9 @@ class APIConnector /** * Write word to stream * - * @param string $word - * @return int return number of written bytes + * @param string $word + * + * @return int return number of written bytes */ public function writeWord(string $word): int { diff --git a/src/APILengthCoDec.php b/src/APILengthCoDec.php index 2f275bd..ad83c9c 100644 --- a/src/APILengthCoDec.php +++ b/src/APILengthCoDec.php @@ -2,6 +2,7 @@ namespace RouterOS; +use DomainException; use RouterOS\Interfaces\StreamInterface; use RouterOS\Helpers\BinaryStringHelper; @@ -18,7 +19,8 @@ class APILengthCoDec /** * Encode string to length of string * - * @param int|float $length + * @param int|float $length + * * @return string */ public static function encodeLength($length): string @@ -54,7 +56,7 @@ class APILengthCoDec // - length > 0x7FFFFFFFFF : not supported if ($length < 0) { - throw new \DomainException("Length of word could not to be negative ($length)"); + throw new DomainException("Length of word could not to be negative ($length)"); } if ($length <= 0x7F) { @@ -81,18 +83,18 @@ class APILengthCoDec // Decode length of data when reading : // The 5 firsts bits of the first byte specify how the length is encoded. // The position of the first 0 value bit, starting from the most significant postion. - // - 0xxxxxxx => The 7 remainings bits of the first byte is the length : + // - 0xxxxxxx => The 7 remaining bits of the first byte is the length : // => min value of length is 0x00 // => max value of length is 0x7F (127 bytes) - // - 10xxxxxx => The 6 remainings bits of the first byte plus the next byte represent the lenght + // - 10xxxxxx => The 6 remaining bits of the first byte plus the next byte represent the lenght // NOTE : the next byte MUST be at least 0x80 !! // => min value of length is 0x80 // => max value of length is 0x3FFF (16,383 bytes, near 16 KB) - // - 110xxxxx => The 5 remainings bits of th first byte and the two next bytes represent the length + // - 110xxxxx => The 5 remaining bits of th first byte and the two next bytes represent the length // => max value of length is 0x1FFFFF (2,097,151 bytes, near 2 MB) - // - 1110xxxx => The 4 remainings bits of the first byte and the three next bytes represent the length + // - 1110xxxx => The 4 remaining bits of the first byte and the three next bytes represent the length // => max value of length is 0xFFFFFFF (268,435,455 bytes, near 270 MB) - // - 11110xxx => The 3 remainings bits of the first byte and the four next bytes represent the length + // - 11110xxx => The 3 remaining bits of the first byte and the four next bytes represent the length // => max value of length is 0x7FFFFFFF (2,147,483,647 byes, 2GB) // - 11111xxx => This byte is not a length-encoded word but a control byte. // => Extracted from Mikrotik API doc : diff --git a/src/Client.php b/src/Client.php index be1469c..a4ae8c3 100644 --- a/src/Client.php +++ b/src/Client.php @@ -6,6 +6,10 @@ use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ConfigException; use RouterOS\Exceptions\QueryException; use RouterOS\Helpers\ArrayHelper; +use function chr; +use function count; +use function is_array; +use function is_string; /** * Class Client for RouterOS management @@ -44,7 +48,7 @@ class Client implements Interfaces\ClientInterface public function __construct($config) { // If array then need create object - if (\is_array($config)) { + if (is_array($config)) { $config = new Config($config); } @@ -87,9 +91,9 @@ class Client implements Interfaces\ClientInterface */ public function write($query): Client { - if (\is_string($query)) { + if (is_string($query)) { $query = new Query($query); - } elseif (\is_array($query)) { + } elseif (is_array($query)) { $endpoint = array_shift($query); $query = new Query($endpoint, $query); } @@ -134,15 +138,15 @@ class Client implements Interfaces\ClientInterface $operator = null; $value = null; - switch (\count($item)) { + switch (count($item)) { case 1: - list($key) = $item; + [$key] = $item; break; case 2: - list($key, $operator) = $item; + [$key, $operator] = $item; break; case 3: - list($key, $operator, $value) = $item; + [$key, $operator, $value] = $item; break; default: throw new ClientException('From 1 to 3 parameters of "where" condition is allowed'); @@ -155,15 +159,15 @@ class Client implements Interfaces\ClientInterface $operator = null; $value = null; - switch (\count($where)) { + switch (count($where)) { case 1: - list($key) = $where; + [$key] = $where; break; case 2: - list($key, $operator) = $where; + [$key, $operator] = $where; break; case 3: - list($key, $operator, $value) = $where; + [$key, $operator, $value] = $where; break; default: throw new ClientException('From 1 to 3 parameters of "where" condition is allowed'); @@ -342,7 +346,7 @@ class Client implements Interfaces\ClientInterface { $result = []; $i = -1; - $lines = \count($response); + $lines = count($response); foreach ($response as $key => $value) { switch ($value) { case '!re': @@ -383,7 +387,7 @@ class Client implements Interfaces\ClientInterface */ private function pregResponse(string $value, &$matches) { - preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); + preg_match_all('/^[=|.](.*)=(.*)/', $value, $matches); } /** @@ -406,7 +410,7 @@ class Client implements Interfaces\ClientInterface // Now need use this hash for authorization $query = new Query('/login', [ '=name=' . $this->config('user'), - '=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])) + '=response=00' . md5(chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])) ]); } else { // Just login with our credentials @@ -452,7 +456,7 @@ class Client implements Interfaces\ClientInterface */ private function isLegacy(array &$response): bool { - return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); + return count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); } /** diff --git a/src/Config.php b/src/Config.php index c4ff807..fbabf75 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 @@ -60,8 +61,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 diff --git a/src/Query.php b/src/Query.php index 9458d91..cfc868b 100644 --- a/src/Query.php +++ b/src/Query.php @@ -5,6 +5,9 @@ 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 +65,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); @@ -85,7 +88,7 @@ class Query implements QueryInterface * @throws \RouterOS\Exceptions\QueryException * @since 1.0.0 */ - public function where(string $key, $operator = null, $value = null): self + public function where(string $key, $operator = null, $value = null): Query { return $this->world('?' . $key, $operator, $value); } @@ -100,7 +103,7 @@ class Query implements QueryInterface * @throws \RouterOS\Exceptions\QueryException * @since 1.1 */ - public function equal(string $key, $value = null): self + public function equal(string $key, $value = null): Query { return $this->world('=' . $key, null, $value); } @@ -115,7 +118,7 @@ class Query implements QueryInterface * @return \RouterOS\Query * @throws \RouterOS\Exceptions\QueryException */ - private function world(string $key, $operator = null, $value = null): self + private function world(string $key, $operator = null, $value = null): Query { if (null !== $operator && null === $value) { @@ -128,7 +131,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) . ']'); @@ -151,7 +154,7 @@ class Query implements QueryInterface * @return \RouterOS\Query * @since 1.0.0 */ - public function operations(string $operations): self + public function operations(string $operations): Query { $this->_operations = '?#' . $operations; return $this; @@ -165,7 +168,7 @@ class Query implements QueryInterface * @return \RouterOS\Query * @since 1.0.0 */ - public function tag(string $name): self + public function tag(string $name): Query { $this->_tag = '.tag=' . $name; return $this; @@ -213,7 +216,7 @@ class Query implements QueryInterface * * @return string|null */ - public function getEndpoint() + public function getEndpoint(): ?string { return $this->_endpoint; } diff --git a/src/ResponseIterator.php b/src/ResponseIterator.php index 1498a60..4b742f5 100644 --- a/src/ResponseIterator.php +++ b/src/ResponseIterator.php @@ -95,7 +95,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable /** * Move forward to next element */ - public function next() + public function next(): void { ++$this->current; } @@ -103,7 +103,7 @@ class ResponseIterator implements Iterator, ArrayAccess, Countable, Serializable /** * Previous value */ - public function prev() + public function prev(): void { --$this->current; } @@ -165,7 +165,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 +176,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 +202,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 +245,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/SocketTrait.php b/src/SocketTrait.php index cfa2461..d0a748a 100644 --- a/src/SocketTrait.php +++ b/src/SocketTrait.php @@ -30,11 +30,11 @@ trait SocketTrait /** * 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([ @@ -62,10 +62,10 @@ trait SocketTrait if (false === $socket) { 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); } @@ -83,10 +83,11 @@ trait SocketTrait /** * 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; } @@ -94,7 +95,7 @@ trait SocketTrait /** * Return socket resource if is exist * - * @return resource + * @return resource */ public function getSocket() { From fd8ca3e356520505835c9642c6b4e0cffe77df18 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 23:17:02 +0300 Subject: [PATCH 04/38] Laravel classes renamed, tests of Laravel classes added --- src/Client.php | 16 ++++++--- src/Laravel/Facade.php | 6 ++-- src/Laravel/ServiceProvider.php | 4 +-- src/Laravel/Wrapper.php | 49 ++++++++++++++++++++++--- tests/Laravel/ServiceProviderTests.php | 65 ++++++++++++++++++++++++++++++++++ tests/Laravel/TestCase.php | 35 ++++++++++++++++++ 6 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 tests/Laravel/ServiceProviderTests.php create mode 100644 tests/Laravel/TestCase.php diff --git a/src/Client.php b/src/Client.php index a4ae8c3..0a5884c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -39,13 +39,14 @@ class Client implements Interfaces\ClientInterface /** * Client constructor. * - * @param array|\RouterOS\Config $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)) { @@ -60,6 +61,11 @@ class Client implements Interfaces\ClientInterface // Save config if everything is okay $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()) { throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port')); @@ -385,7 +391,7 @@ class Client implements Interfaces\ClientInterface * @param string $value * @param array $matches */ - private function pregResponse(string $value, &$matches) + private function pregResponse(string $value, &$matches): void { preg_match_all('/^[=|.](.*)=(.*)/', $value, $matches); } @@ -454,7 +460,7 @@ class Client implements Interfaces\ClientInterface * @return bool * @throws ConfigException */ - private function isLegacy(array &$response): bool + private function isLegacy(array $response): bool { return count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); } @@ -467,7 +473,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; diff --git a/src/Laravel/Facade.php b/src/Laravel/Facade.php index 1afcd3e..dc16db3 100644 --- a/src/Laravel/Facade.php +++ b/src/Laravel/Facade.php @@ -2,9 +2,9 @@ namespace RouterOS\Laravel; -use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Facades\Facade as BaseFacade; -class ClientFacade extends Facade +class Facade extends BaseFacade { /** * Get the registered name of the component. @@ -13,6 +13,6 @@ class ClientFacade extends Facade */ protected static function getFacadeAccessor(): string { - return ClientWrapper::class; + return Wrapper::class; } } diff --git a/src/Laravel/ServiceProvider.php b/src/Laravel/ServiceProvider.php index 9ce33ab..7bb6d49 100644 --- a/src/Laravel/ServiceProvider.php +++ b/src/Laravel/ServiceProvider.php @@ -4,7 +4,7 @@ namespace RouterOS\Laravel; use Illuminate\Support\ServiceProvider as BaseServiceProvider; -class ClientServiceProvider extends BaseServiceProvider +class ServiceProvider extends BaseServiceProvider { /** * Bootstrap any application services. @@ -29,6 +29,6 @@ class ClientServiceProvider extends BaseServiceProvider __DIR__ . '/../../configs/routeros-api.php', 'routeros-api' ); - $this->app->bind(ClientWrapper::class); + $this->app->bind(Wrapper::class); } } diff --git a/src/Laravel/Wrapper.php b/src/Laravel/Wrapper.php index 59cba9a..a2be5f2 100644 --- a/src/Laravel/Wrapper.php +++ b/src/Laravel/Wrapper.php @@ -3,22 +3,61 @@ namespace RouterOS\Laravel; use RouterOS\Client; +use RouterOS\Config; +use RouterOS\Interfaces\ClientInterface; +use RouterOS\Interfaces\ConfigInterface; -class ClientWrapper +class Wrapper { /** + * Alias for \RouterOS::client() method + * * @param array $params * * @return \RouterOS\Client * @throws \RouterOS\Exceptions\ClientException * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException + * @deprecated + * @codeCoverageIgnore + */ + public function getClient(array $params = []): ClientInterface + { + return $this->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 getClient(array $params = []): Client + public function client(array $params = [], bool $autoConnect = true): ClientInterface { - $configs = config('routeros-api'); - $configs = array_merge($configs, $params); + $config = $this->config($params); - return new Client($configs); + return new Client($config, $autoConnect); } } diff --git a/tests/Laravel/ServiceProviderTests.php b/tests/Laravel/ServiceProviderTests.php new file mode 100644 index 0000000..bff8ba4 --- /dev/null +++ b/tests/Laravel/ServiceProviderTests.php @@ -0,0 +1,65 @@ +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, + ]; + } +} From 6f14f684042146ce1862a2d95b3b0873745eec45 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 23:18:18 +0300 Subject: [PATCH 05/38] Additional notes about `equal` method of `Query` class added to readme, details about Laravel changed to new paths --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 477f034..04e8fe2 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,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 +43,37 @@ $client = \RouterOS::getClient([ ]); ``` -### Laravel installation +You also may get array with all configs which was obtained from `routeros-api.php` file: -Install the package via Composer: +```php +$config = \RouterOS::config([ + 'host' => '192.168.1.3', + 'user' => 'admin', + 'pass' => 'admin', + 'port' => 8728, +]); - composer require evilfreelancer/routeros-api-php +dump($config); + +$client = \RouterOS::client($config); +``` + +### 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.3, then in a project, which is using your package +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,16 +99,32 @@ $query = $response = $client->query($query)->read(); var_dump($response); +``` + +Basic example for update/create/delete types of queries: + +```php +use \RouterOS\Client; +use \RouterOS\Query; -// Send "equal" 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); ``` Examples with "where" conditions, "operations" and "tag": @@ -257,10 +284,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 +298,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 +319,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 +391,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 +430,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 From ef55a691b72c226379068f7b94a645b1fff15644 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Thu, 18 Jun 2020 23:49:27 +0300 Subject: [PATCH 06/38] PHPUnit package updated, some extended visualization of tests results enabled, paths to Laravel classes changed, small fixes for phpunit 8.0 added to tests --- .gitignore | 1 + composer.json | 9 +++-- phpunit.xml | 1 + tests/APIConnectorTest.php | 6 +-- tests/APILengthCoDecTest.php | 22 ++++++++--- tests/ClientTest.php | 47 +++++++++++------------ tests/ConfigTest.php | 51 ++++++++++++------------ tests/Helpers/ArrayHelperTest.php | 4 +- tests/Helpers/BinaryStringHelperTest.php | 5 ++- tests/Helpers/TypeHelperTest.php | 2 +- tests/QueryTest.php | 18 ++++----- tests/ResponseIteratorTest.php | 14 +++---- tests/Streams/ResourceStreamTest.php | 66 ++++++++++++++++++-------------- tests/Streams/StringStreamTest.php | 43 ++++++++++----------- 14 files changed, 154 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index f4686f8..24c6b28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.idea/ /vendor/ /composer.lock +/.phpunit.result.cache diff --git a/composer.json b/composer.json index 5a7af1b..57109fc 100644 --- a/composer.json +++ b/composer.json @@ -33,10 +33,10 @@ "extra": { "laravel": { "providers": [ - "RouterOS\\Laravel\\ClientServiceProvider" + "RouterOS\\Laravel\\ServiceProvider" ], "aliases": { - "RouterOS": "RouterOS\\Laravel\\ClientFacade" + "RouterOS": "RouterOS\\Laravel\\Facade" } } }, @@ -45,7 +45,8 @@ "ext-sockets": "*" }, "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" } } diff --git a/phpunit.xml b/phpunit.xml index b065af3..df8ac12 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + printerClass="LimeDeck\Testing\Printer" processIsolation="false" stopOnFailure="true"> diff --git a/tests/APIConnectorTest.php b/tests/APIConnectorTest.php index b4784fe..f5da3e5 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); @@ -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..5abd818 100644 --- a/tests/APILengthCoDecTest.php +++ b/tests/APILengthCoDecTest.php @@ -18,11 +18,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 +39,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 +81,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 +95,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..0417287 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; @@ -27,7 +28,7 @@ class ClientTest extends TestCase */ public $port_legacy; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -41,7 +42,7 @@ class ClientTest extends TestCase $this->port_legacy = (int) getenv('ROS_PORT_LEGACY'); } - public function test__construct(): void + public function testConstruct(): void { try { $config = new Config(); @@ -54,12 +55,12 @@ class ClientTest extends TestCase $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); @@ -67,24 +68,24 @@ class ClientTest extends TestCase $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); $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 testConstructEx(): void { $this->expectException(ConfigException::class); @@ -94,7 +95,7 @@ class ClientTest extends TestCase ]); } - public function test__constructLegacy(): void + public function testConstructLegacy(): void { try { $obj = new Client([ @@ -105,8 +106,8 @@ class ClientTest extends TestCase '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,7 +116,7 @@ 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([ @@ -126,13 +127,13 @@ class ClientTest extends TestCase '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); @@ -144,13 +145,9 @@ class ClientTest extends TestCase ]); } - /** - * @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'], @@ -216,7 +213,7 @@ class ClientTest extends TestCase { $obj = new Client($this->router); - $obj = $obj->write('/system/package/print')->readAsIterator(); + $obj = $obj->query('/system/package/print')->readAsIterator(); $this->assertIsObject($obj); } @@ -228,7 +225,7 @@ class ClientTest extends TestCase 'host' => $this->router['host'], ]); - $readTrap = $obj->wr('/interface', false); + $readTrap = $obj->query('/interface')->read(false); $this->assertCount(3, $readTrap); $this->assertEquals('!trap', $readTrap[0]); } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index d444888..f12459e 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->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->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 testSetEx1(): void { $this->expectException(ConfigException::class); @@ -84,7 +84,7 @@ class ConfigTest extends TestCase $obj->set('delay', 'some string'); } - public function testSetEx2() + public function testSetEx2(): 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/QueryTest.php b/tests/QueryTest.php index 2f5c701..0568d21 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 diff --git a/tests/ResponseIteratorTest.php b/tests/ResponseIteratorTest.php index 33d2dbd..3b3e027 100644 --- a/tests/ResponseIteratorTest.php +++ b/tests/ResponseIteratorTest.php @@ -7,7 +7,7 @@ use RouterOS\Client; class ResponseIteratorTest extends TestCase { - public function test__construct() + public function testConstruct(): void { $obj = new Client([ 'user' => getenv('ROS_USER'), @@ -15,11 +15,11 @@ class ResponseIteratorTest extends TestCase 'host' => getenv('ROS_HOST'), ]); - $obj = $obj->write('/system/package/print')->readAsIterator(); + $obj = $obj->query('/system/package/print')->readAsIterator(); $this->assertIsObject($obj); } - public function testReadWrite() + public function testReadWrite(): void { $obj = new Client([ 'user' => getenv('ROS_USER'), @@ -27,15 +27,15 @@ class ResponseIteratorTest extends TestCase 'host' => getenv('ROS_HOST'), ]); - $readTrap = $obj->write('/system/package/print')->readAsIterator(); + $readTrap = $obj->query('/system/package/print')->readAsIterator(); // Read from RAW $this->assertCount(13, $readTrap); - $readTrap = $obj->write('/ip/address/print')->readAsIterator(); + $readTrap = $obj->query('/ip/address/print')->readAsIterator(); $this->assertCount(1, $readTrap); $this->assertEquals('ether1', $readTrap[0]['interface']); - $readTrap = $obj->write('/system/package/print')->readAsIterator(); + $readTrap = $obj->query('/system/package/print')->readAsIterator(); $key = $readTrap->key(); $this->assertEquals(0, $key); $current = $readTrap->current(); @@ -68,7 +68,7 @@ class ResponseIteratorTest extends TestCase 'host' => getenv('ROS_HOST'), ]); - $read = $obj->write('/queue/simple/print')->readAsIterator(); + $read = $obj->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); From 0da8a1ffccb495656f5b49bcee2bfe81b7df59aa Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:34:10 +0300 Subject: [PATCH 07/38] Colors was disabled on preconf stage --- preconf.tcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 "]:" From 3b56dc6f5bb37bb6e2c747e6b1e69dd698a109d0 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:34:39 +0300 Subject: [PATCH 08/38] Library for work with SSH added to composer, it need for /export --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a85bc9c..d082987 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ }, "require": { "php": "^7.2", - "ext-sockets": "*" + "ext-sockets": "*", + "divineomega/php-ssh-connection": "^2.2" }, "require-dev": { "limedeck/phpunit-detailed-printer": "^5.0", From 951b78116385bf4cd5a597a52e010c269b1f7b79 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:35:35 +0300 Subject: [PATCH 09/38] New `ssh_port` option was added to confings, it need to executing direct commands on remote router --- configs/routeros-api.php | 1 + src/Config.php | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/configs/routeros-api.php b/configs/routeros-api.php index 91a3359..e049aff 100644 --- a/configs/routeros-api.php +++ b/configs/routeros-api.php @@ -6,6 +6,7 @@ return [ // 'pass' => null, // 'port' => null, 'ssl' => false, + 'ssh_port' => 22, 'legacy' => false, 'timeout' => 10, 'attempts' => 10, diff --git a/src/Config.php b/src/Config.php index fbabf75..f26828f 100644 --- a/src/Config.php +++ b/src/Config.php @@ -17,6 +17,32 @@ use function gettype; class Config implements 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', + // Number of SSH port + 'ssh_port' => 'integer', + ]; + + /** * Array of parameters (with some default values) * * @var array @@ -26,7 +52,8 @@ class Config implements ConfigInterface 'ssl' => Client::SSL, 'timeout' => Client::TIMEOUT, 'attempts' => Client::ATTEMPTS, - 'delay' => Client::ATTEMPTS_DELAY + 'delay' => Client::ATTEMPTS_DELAY, + 'ssh_port' => Client::SSH_PORT, ]; /** From 07ba9480b49d673c6fddba8cc8288847782e6190 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:36:08 +0300 Subject: [PATCH 10/38] ALLOWED const was moved from ConfigInterface to Config class --- src/Interfaces/ConfigInterface.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/Interfaces/ConfigInterface.php b/src/Interfaces/ConfigInterface.php index 463fc32..963d4bc 100644 --- a/src/Interfaces/ConfigInterface.php +++ b/src/Interfaces/ConfigInterface.php @@ -13,30 +13,6 @@ 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 From be80ff9bcf070afb41a8870c932586ff1da22990 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:37:16 +0300 Subject: [PATCH 11/38] To Client class was added ->export() method, it use divineomega/php-ssh-connection library for interact with remote hosts, example file updated --- examples/export.php | 9 ++++----- src/Client.php | 28 ++++++++++++++++++++++++++++ src/Interfaces/ClientInterface.php | 31 +++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/examples/export.php b/examples/export.php index c72d3ee..0ff790a 100644 --- a/examples/export.php +++ b/examples/export.php @@ -12,14 +12,13 @@ $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 -$query = new Query('/export'); +// Execute export command via ssh +$response = $client->export(); -// Send query and read answer from RouterOS -$response = $client->write($query)->read(false); print_r($response); diff --git a/src/Client.php b/src/Client.php index 13cccee..3638226 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,6 +2,7 @@ namespace RouterOS; +use DivineOmega\SSHConnection\SSHConnection; use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ConfigException; use RouterOS\Exceptions\QueryException; @@ -517,4 +518,31 @@ class Client implements Interfaces\ClientInterface // Return status of connection return $connected; } + + /** + * Execute export command on remote host + * + * @return string + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RuntimeException + * + * @since 1.3.0 + */ + public function export(): string + { + // Connect to remote host + $connection = + (new SSHConnection()) + ->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'); + + // Return the output + return $command->getOutput(); + } } diff --git a/src/Interfaces/ClientInterface.php b/src/Interfaces/ClientInterface.php index fad30ed..2cd9b4b 100644 --- a/src/Interfaces/ClientInterface.php +++ b/src/Interfaces/ClientInterface.php @@ -49,6 +49,11 @@ interface ClientInterface public const ATTEMPTS_DELAY = 1; /** + * Delay between attempts in seconds + */ + public const SSH_PORT = 22; + + /** * Return socket resource if is exist * * @return resource @@ -58,7 +63,14 @@ interface ClientInterface /** * Read answer from server after query was executed * + * 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 + * * @return mixed */ public function read(bool $parse); @@ -67,7 +79,10 @@ interface ClientInterface * Send write query to RouterOS * * @param string|array|\RouterOS\Query $query + * * @return \RouterOS\Client + * @throws \RouterOS\Exceptions\QueryException + * @deprecated */ public function write($query): Client; @@ -78,9 +93,21 @@ interface ClientInterface * @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 * @since 1.0.0 */ - public function query($endpoint, array $where, string $operations, string $tag): Client; + public function query($endpoint, array $where, string $operations, string $tag): ClientInterface; + + /** + * Execute export command on remote host + * + * @return string + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RuntimeException + * + * @since 1.3.0 + */ + public function export(): string; } From fed455cc7653509a74799b375823a943606ae059 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Fri, 19 Jun 2020 00:41:12 +0300 Subject: [PATCH 12/38] Note about export method added to readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 04e8fe2..a79d76a 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,25 @@ $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 From a9b18d40156f186d44de6e03e0f02842bb9aaca9 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 16:52:52 +0300 Subject: [PATCH 13/38] Note about minimal requirements added to readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a79d76a..f63ed1b 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, From 239e19168ba25d6ede3021bdd04bfe3873144fc0 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 17:52:58 +0300 Subject: [PATCH 14/38] Know issues block added to readme --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/README.md b/README.md index f63ed1b..f20a256 100644 --- a/README.md +++ b/README.md @@ -526,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) From df1c5d1ad4b5f6a97a0b062c0e2f8ea16c288222 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 17:53:30 +0300 Subject: [PATCH 15/38] larapack/dd added to require-dev packages --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d082987..e1db656 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "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", From 75a201e0f282ab06fb7ee7c046b31f4a01cf948b Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 17:55:12 +0300 Subject: [PATCH 16/38] Responses/parameters of some methods in Client class changed from other classes to interfaces, consts moved from ClientInterface to Config class --- src/Client.php | 18 ++++++------ src/Config.php | 56 +++++++++++++++++++++++++++++++------ src/Interfaces/ClientInterface.php | 57 +++++--------------------------------- src/Interfaces/QueryInterface.php | 2 +- 4 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/Client.php b/src/Client.php index 3638226..723b3a7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -6,6 +6,7 @@ use DivineOmega\SSHConnection\SSHConnection; use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ConfigException; use RouterOS\Exceptions\QueryException; +use RouterOS\Interfaces\ClientInterface; use RouterOS\Interfaces\QueryInterface; use RouterOS\Helpers\ArrayHelper; use function array_keys; @@ -97,13 +98,13 @@ class Client implements Interfaces\ClientInterface /** * Send write query to RouterOS * - * @param string|array|\RouterOS\Query $query + * @param string|array|\RouterOS\Interfaces\QueryInterface $query * - * @return \RouterOS\Client + * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException * @deprecated */ - public function write($query): Client + public function write($query): ClientInterface { if (is_string($query)) { $query = new Query($query); @@ -133,7 +134,7 @@ class Client implements Interfaces\ClientInterface * @throws \RouterOS\Exceptions\ClientException * @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) @@ -175,8 +176,8 @@ 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, QueryInterface $query): QueryInterface { @@ -197,6 +198,7 @@ class Client implements Interfaces\ClientInterface break; default: throw new ClientException('From 1 to 3 parameters of "where" condition is allowed'); + break; } return $query->where($key, $operator, $value); @@ -205,13 +207,13 @@ 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 * @since 1.0.0 */ - private function writeRAW(Query $query): Client + private function writeRAW(QueryInterface $query): ClientInterface { // Send commands via loop to router foreach ($query->getQuery() as $command) { diff --git a/src/Config.php b/src/Config.php index f26828f..5e83b7e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -17,6 +17,46 @@ use function gettype; 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 = [ @@ -48,12 +88,12 @@ class Config implements ConfigInterface * @var array */ private $_parameters = [ - 'legacy' => Client::LEGACY, - 'ssl' => Client::SSL, - 'timeout' => Client::TIMEOUT, - 'attempts' => Client::ATTEMPTS, - 'delay' => Client::ATTEMPTS_DELAY, - 'ssh_port' => Client::SSH_PORT, + 'legacy' => self::LEGACY, + 'ssl' => self::SSL, + 'timeout' => self::TIMEOUT, + 'attempts' => self::ATTEMPTS, + 'delay' => self::ATTEMPTS_DELAY, + 'ssh_port' => self::SSH_PORT, ]; /** @@ -111,8 +151,8 @@ 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; } diff --git a/src/Interfaces/ClientInterface.php b/src/Interfaces/ClientInterface.php index 2cd9b4b..fede7cd 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,46 +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; - - /** - * Delay between attempts in seconds - */ - public const SSH_PORT = 22; - - /** * Return socket resource if is exist * * @return resource @@ -78,21 +35,21 @@ interface ClientInterface /** * Send write query to RouterOS * - * @param string|array|\RouterOS\Query $query + * @param string|array|\RouterOS\Interfaces\QueryInterface $query * - * @return \RouterOS\Client + * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException * @deprecated */ - public function write($query): Client; + public function write($query): ClientInterface; /** * Send write query to RouterOS (modern version of write) * - * @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 + * @param 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\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException 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; From 0025b5115b2877794eb98d9762d5d17fa15fbd85 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 17:55:53 +0300 Subject: [PATCH 17/38] Simple example with ->equal method usage added --- examples/remove_security_profile.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 examples/remove_security_profile.php 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(); From 2cbd59c2d6171cecc05524e4733e77460cc5fdf1 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 17:56:25 +0300 Subject: [PATCH 18/38] Small fixes in tests --- tests/ConfigTest.php | 4 ++-- tests/Laravel/ServiceProviderTests.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index f12459e..6360202 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -23,7 +23,7 @@ class ConfigTest extends TestCase $obj = new Config(); $params = $obj->getParameters(); - $this->assertCount(5, $params); + $this->assertCount(6, $params); $this->assertEquals(false, $params['legacy']); $this->assertEquals(false, $params['ssl']); $this->assertEquals(10, $params['timeout']); @@ -36,7 +36,7 @@ class ConfigTest extends TestCase $obj = new Config(['timeout' => 100]); $params = $obj->getParameters(); - $this->assertCount(5, $params); + $this->assertCount(6, $params); $this->assertEquals(100, $params['timeout']); } diff --git a/tests/Laravel/ServiceProviderTests.php b/tests/Laravel/ServiceProviderTests.php index bff8ba4..9548b19 100644 --- a/tests/Laravel/ServiceProviderTests.php +++ b/tests/Laravel/ServiceProviderTests.php @@ -15,6 +15,7 @@ class ServiceProviderTests extends TestCase "readAsIterator", "parseResponse", "connect", + "export", "getSocket", "w", "q", From fee89f30ee3bfd694392bff8dfc1b9a75c28683b Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:28:25 +0300 Subject: [PATCH 19/38] Usage of legacy methods removed from all examples --- examples/bridge_hosts.php | 2 +- examples/different_queries.php | 6 +++--- examples/export.php | 23 ++++++++++++++++++++++- examples/interface_print.php | 2 +- examples/ip_address_print.php | 2 +- examples/ip_filrewall_address-list_print.php | 2 +- examples/queue_simple_print.php | 2 +- examples/queue_simple_print_v2.php | 2 +- examples/queue_simple_write.php | 4 ++-- examples/system_package_print.php | 2 +- examples/vlans_bridge.php | 6 +++--- examples/vlans_bridge_v2.php | 6 +++--- examples/vlans_bridge_v3.php | 6 +++--- 13 files changed, 43 insertions(+), 22 deletions(-) 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 0ff790a..c2621fc 100644 --- a/examples/export.php +++ b/examples/export.php @@ -20,5 +20,26 @@ $client = new Client($config); // Execute export command via ssh $response = $client->export(); +dump($response); -print_r($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'); + +// 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/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", From ab3acd4d56069f94ec908a2714f2923b1653b368 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:28:53 +0300 Subject: [PATCH 20/38] Additional information about parameters added to default Laravel config --- configs/routeros-api.php | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/configs/routeros-api.php b/configs/routeros-api.php index e049aff..8a91d23 100644 --- a/configs/routeros-api.php +++ b/configs/routeros-api.php @@ -1,14 +1,39 @@ null, - // 'user' => null, - // 'pass' => null, - // 'port' => null, - 'ssl' => false, - 'ssh_port' => 22, - '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 + ]; From e06a0078340d4eb259d8873d01536826aa0adcb5 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:29:29 +0300 Subject: [PATCH 21/38] Small rafactorigin in SocketTrait, provate variabled was renamed --- src/SocketTrait.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SocketTrait.php b/src/SocketTrait.php index d0a748a..0161512 100644 --- a/src/SocketTrait.php +++ b/src/SocketTrait.php @@ -11,21 +11,21 @@ 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 @@ -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,7 +60,7 @@ 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 @@ -77,7 +77,7 @@ trait SocketTrait */ private function closeSocket(): bool { - return fclose($this->_socket); + return fclose($this->socket); } /** @@ -89,7 +89,7 @@ trait SocketTrait */ private function setSocket($socket): void { - $this->_socket = $socket; + $this->socket = $socket; } /** @@ -99,6 +99,6 @@ trait SocketTrait */ public function getSocket() { - return $this->_socket; + return $this->socket; } } From f316c2d58dec6a425ff177a3871a9ace6ee4022b Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:30:02 +0300 Subject: [PATCH 22/38] Preloading feature added to APILengthCoDec class --- src/APILengthCoDec.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/APILengthCoDec.php b/src/APILengthCoDec.php index 19d7a41..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 @@ -19,8 +21,6 @@ class APILengthCoDec /** * Encode string to length of string * - * <<<<<<< HEAD - * ======= * Encode the length: * - if length <= 0x7F (binary : 01111111 => 7 bits set to 1) * - encode length with one byte @@ -51,10 +51,9 @@ class APILengthCoDec * - end * - length > 0x7FFFFFFFFF : not supported * - * >>>>>>> master * @param int|float $length * - * @return string + * @return string */ public static function encodeLength($length): string { @@ -172,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 } @@ -192,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'); } } From f88a4d1a1ddfff3562c15a8f117d38f0138674fa Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:31:15 +0300 Subject: [PATCH 23/38] PHPDoc fo Config class was updated, small tunes added to ConfigInterface --- src/Config.php | 60 ++++++++++++-------------------------- src/Interfaces/ConfigInterface.php | 10 +++---- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/src/Config.php b/src/Config.php index 5e83b7e..1602a0d 100644 --- a/src/Config.php +++ b/src/Config.php @@ -60,26 +60,16 @@ class Config implements 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', - // Number of SSH port - 'ssh_port' => 'integer', + '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 ]; /** @@ -112,15 +102,11 @@ class Config implements ConfigInterface } /** - * Set parameter into array + * @inheritDoc * - * @param string $name - * @param mixed $value - * - * @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)) { @@ -158,14 +144,11 @@ class Config implements ConfigInterface } /** - * Remove parameter from array by name + * @inheritDoc * - * @param string $name - * - * @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)) { @@ -179,12 +162,9 @@ class Config implements ConfigInterface } /** - * Return parameter of current config by name + * @inheritDoc * - * @param string $name - * - * @return mixed - * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\ConfigException when parameter is not allowed */ public function get(string $name) { @@ -197,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/Interfaces/ConfigInterface.php b/src/Interfaces/ConfigInterface.php index 963d4bc..0ce8a29 100644 --- a/src/Interfaces/ConfigInterface.php +++ b/src/Interfaces/ConfigInterface.php @@ -2,8 +2,6 @@ namespace RouterOS\Interfaces; -use RouterOS\Config; - /** * Interface ConfigInterface * @@ -18,18 +16,18 @@ interface ConfigInterface * @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 From 64d22d5eaa9e7e21b5cd6c15c7172fde4a729432 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:31:43 +0300 Subject: [PATCH 24/38] Prealoading added to ResponseIterator, some non-used code removed --- src/ResponseIterator.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ResponseIterator.php b/src/ResponseIterator.php index 4b742f5..a14c64d 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 From 049df4bb4cc7fdea3cee4c9c3882d1dc9f24c956 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:35:26 +0300 Subject: [PATCH 25/38] PHPDoc cleanup in ResourceStream and StringStream classes --- src/Streams/ResourceStream.php | 27 +++++++++------------------ src/Streams/StringStream.php | 24 ++++++++++-------------- 2 files changed, 19 insertions(+), 32 deletions(-) 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 = ''; } From a904982a5703fba3f543b46ec1fb1ee88de1bd40 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:36:25 +0300 Subject: [PATCH 26/38] Return type added to close method of StreamInterface --- src/Interfaces/StreamInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From e506768ecb3cf293409d17e46726ccf0ff859eb2 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:37:30 +0300 Subject: [PATCH 27/38] Deprecated method w, wr, wri was removed from ShortsTrait, tests fixed --- src/ShortsTrait.php | 87 +++++++++------------------------- tests/Laravel/ServiceProviderTests.php | 4 -- 2 files changed, 22 insertions(+), 69 deletions(-) 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/tests/Laravel/ServiceProviderTests.php b/tests/Laravel/ServiceProviderTests.php index 9548b19..468fc2f 100644 --- a/tests/Laravel/ServiceProviderTests.php +++ b/tests/Laravel/ServiceProviderTests.php @@ -9,7 +9,6 @@ class ServiceProviderTests extends TestCase { private $client = [ "__construct", - "write", "query", "read", "readAsIterator", @@ -17,13 +16,10 @@ class ServiceProviderTests extends TestCase "connect", "export", "getSocket", - "w", "q", "r", "ri", - "wr", "qr", - "wri", "qri", ]; From e921c4df25a36ff72bea13056ca90e48a87d8f30 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Sun, 28 Jun 2020 20:39:01 +0300 Subject: [PATCH 28/38] Deprecated write method was removed from Client class, export method now support additional arguments, detector of /export command which exec ssh was added --- src/Client.php | 126 ++++++++++++++++++++++--------------- src/Interfaces/ClientInterface.php | 25 +++----- 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/src/Client.php b/src/Client.php index 723b3a7..478f8af 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,7 +5,6 @@ namespace RouterOS; use DivineOmega\SSHConnection\SSHConnection; use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\ConfigException; -use RouterOS\Exceptions\QueryException; use RouterOS\Interfaces\ClientInterface; use RouterOS\Interfaces\QueryInterface; use RouterOS\Helpers\ArrayHelper; @@ -14,7 +13,6 @@ 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; @@ -36,15 +34,21 @@ 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. @@ -69,7 +73,7 @@ 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) { @@ -92,46 +96,21 @@ class Client implements Interfaces\ClientInterface */ private function config(string $parameter) { - return $this->_config->get($parameter); - } - - /** - * Send write query to RouterOS - * - * @param string|array|\RouterOS\Interfaces\QueryInterface $query - * - * @return \RouterOS\Interfaces\ClientInterface - * @throws \RouterOS\Exceptions\QueryException - * @deprecated - */ - public function write($query): ClientInterface - { - 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): ClientInterface @@ -211,37 +190,62 @@ class Client implements Interfaces\ClientInterface * * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException + * @throws \RouterOS\Exceptions\ConfigException * @since 1.0.0 */ 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) { @@ -278,7 +282,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 */ @@ -287,6 +291,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; } @@ -455,7 +465,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(); } @@ -502,7 +512,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; @@ -522,19 +532,31 @@ class Client implements Interfaces\ClientInterface } /** - * Execute export command on remote host + * 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 - * @throws \RuntimeException - * * @since 1.3.0 */ - public function export(): string + 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') @@ -542,7 +564,7 @@ class Client implements Interfaces\ClientInterface ->connect(); // Run export command - $command = $connection->run('/export'); + $command = $connection->run('/export' . ' ' . $arguments); // Return the output return $command->getOutput(); diff --git a/src/Interfaces/ClientInterface.php b/src/Interfaces/ClientInterface.php index fede7cd..2b81e1e 100644 --- a/src/Interfaces/ClientInterface.php +++ b/src/Interfaces/ClientInterface.php @@ -26,33 +26,24 @@ interface 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 */ - public function read(bool $parse); - - /** - * Send write query to RouterOS - * - * @param string|array|\RouterOS\Interfaces\QueryInterface $query - * - * @return \RouterOS\Interfaces\ClientInterface - * @throws \RouterOS\Exceptions\QueryException - * @deprecated - */ - public function write($query): ClientInterface; + public function read(bool $parse = true); /** * Send write query to RouterOS (modern version of write) * - * @param 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 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\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException * @since 1.0.0 */ public function query($endpoint, array $where, string $operations, string $tag): ClientInterface; From 60f83aa7569198bc653a3837043668c612099462 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:05:34 +0300 Subject: [PATCH 29/38] close method added to APIConnector class --- src/APIConnector.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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. From e5498b69717ef63e78fe61e2994288ffab2f56c9 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:07:24 +0300 Subject: [PATCH 30/38] non-reachable break removed --- src/Client.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 478f8af..50508a2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -177,7 +177,6 @@ class Client implements Interfaces\ClientInterface break; default: throw new ClientException('From 1 to 3 parameters of "where" condition is allowed'); - break; } return $query->where($key, $operator, $value); From 9986867d0fe72b683ba15bb168613200682a1f37 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:16:42 +0300 Subject: [PATCH 31/38] phpDoc update in ArrayHelper --- src/Helpers/ArrayHelper.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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) { From f578750e50ea222fe8892d445800edb29ed040f3 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:17:25 +0300 Subject: [PATCH 32/38] non-used use removed from Query class --- src/Query.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Query.php b/src/Query.php index f436469..0754051 100644 --- a/src/Query.php +++ b/src/Query.php @@ -2,7 +2,6 @@ namespace RouterOS; -use RouterOS\Exceptions\ClientException; use RouterOS\Exceptions\QueryException; use RouterOS\Interfaces\QueryInterface; use function in_array; From 70e75ff9c3f048198b084d9b687750692c47a4ba Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:17:45 +0300 Subject: [PATCH 33/38] code style tune in ResponseIterator --- src/ResponseIterator.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ResponseIterator.php b/src/ResponseIterator.php index a14c64d..2ed95be 100644 --- a/src/ResponseIterator.php +++ b/src/ResponseIterator.php @@ -2,10 +2,10 @@ namespace RouterOS; -use \Iterator; -use \ArrayAccess; -use \Countable; -use \Serializable; +use Iterator; +use ArrayAccess; +use Countable; +use Serializable; use function array_keys; use function array_slice; use function count; From ab632d79cb332a7c7229c86a2cc9eef51b82a7bc Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:18:25 +0300 Subject: [PATCH 34/38] tests cleanup and update to level of logic, coverage increased --- tests/APIConnectorTest.php | 2 +- tests/APILengthCoDecTest.php | 5 +- tests/ClientTest.php | 185 ++++++++++++++++++++++------------------- tests/ConfigTest.php | 4 +- tests/QueryTest.php | 12 +++ tests/ResponseIteratorTest.php | 37 ++++----- 6 files changed, 131 insertions(+), 114 deletions(-) diff --git a/tests/APIConnectorTest.php b/tests/APIConnectorTest.php index f5da3e5..7644495 100644 --- a/tests/APIConnectorTest.php +++ b/tests/APIConnectorTest.php @@ -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 !!! diff --git a/tests/APILengthCoDecTest.php b/tests/APILengthCoDecTest.php index 5abd818..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; @@ -24,7 +23,7 @@ class APILengthCoDecTest extends TestCase */ public function testEncodeLengthNegative($length): void { - $this->expectException(\DomainException::class); + $this->expectException(DomainException::class); APILengthCoDec::encodeLength($length); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 0417287..cc36d16 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -16,7 +16,12 @@ class ClientTest extends TestCase /** * @var array */ - public $router; + public $config; + + /** + * @var \RouterOS\Client + */ + public $client; /** * @var int @@ -30,14 +35,15 @@ class ClientTest extends TestCase 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'); } @@ -47,9 +53,9 @@ class ClientTest extends TestCase 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); @@ -63,7 +69,7 @@ class ClientTest extends TestCase 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(); @@ -76,7 +82,7 @@ class ClientTest extends TestCase public function testConstruct3(): void { try { - $obj = new Client($this->router); + $obj = new Client($this->config); $this->assertIsObject($obj); $socket = $obj->getSocket(); $this->assertIsResource($socket); @@ -85,13 +91,26 @@ class ClientTest extends TestCase } } - public function testConstructEx(): 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'], ]); } @@ -99,9 +118,9 @@ class ClientTest extends TestCase { 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 ]); @@ -120,9 +139,9 @@ class ClientTest extends TestCase { 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 ]); @@ -132,15 +151,14 @@ class ClientTest extends TestCase } } - 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 ]); } @@ -148,10 +166,11 @@ class ClientTest extends TestCase 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 ]); @@ -159,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(); @@ -204,68 +215,72 @@ 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->query('/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->query('/interface')->read(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'], - ]); - - $obj->query('/quiet', ['a', 'b', 'c', 'd']); + 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], + ]; } - public function testQueryEx2(): void + /** + * @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(ClientException::class); + $this->expectException($exception); + $this->client->query($endpoint, $attributes); + } - $obj = new Client([ - 'user' => $this->router['user'], - 'pass' => $this->router['pass'], - 'host' => $this->router['host'], - ]); + public function testExportMethod(): void + { + $result = $this->client->export(); + $this->assertNotEmpty($result); + } - $obj->query('/quiet', [[]]); + public function testExportQuery(): void + { + $result = $this->client->query('/export'); + $this->assertNotEmpty($result); } } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 6360202..2aa17cc 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -76,7 +76,7 @@ class ConfigTest extends TestCase $obj->delete('wrong'); } - public function testSetEx1(): void + public function testSetExceptionWrongType(): void { $this->expectException(ConfigException::class); @@ -84,7 +84,7 @@ class ConfigTest extends TestCase $obj->set('delay', 'some string'); } - public function testSetEx2(): void + public function testSetExceptionWrongKey(): void { $this->expectException(ConfigException::class); diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 0568d21..4a91071 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -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 3b3e027..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 testConstruct(): void + /** + * @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->query('/system/package/print')->readAsIterator(); - $this->assertIsObject($obj); } public function testReadWrite(): void { - $obj = new Client([ - 'user' => getenv('ROS_USER'), - 'pass' => getenv('ROS_PASS'), - 'host' => getenv('ROS_HOST'), - ]); - - $readTrap = $obj->query('/system/package/print')->readAsIterator(); - // Read from RAW - $this->assertCount(13, $readTrap); + $readTrap = $this->client->query('/system/logging/print')->readAsIterator(); + $this->assertNotEmpty($readTrap); - $readTrap = $obj->query('/ip/address/print')->readAsIterator(); + $readTrap = $this->client->query('/ip/address/print')->readAsIterator(); $this->assertCount(1, $readTrap); $this->assertEquals('ether1', $readTrap[0]['interface']); - $readTrap = $obj->query('/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->query('/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); } From 98bf022c8b0b6b7ba06c864b76dde412e8c8940f Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:18:51 +0300 Subject: [PATCH 35/38] ROS_SSH_PORT param added to phpunit.xml --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index df8ac12..809ef5f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,5 +31,6 @@ + From c838edc064777ed9afa46d6ff2aec1ee3031a21b Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:37:25 +0300 Subject: [PATCH 36/38] skip state if openssl extention is not available added to ClientTest of export methods --- tests/ClientTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/ClientTest.php b/tests/ClientTest.php index cc36d16..c2e032c 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -274,12 +274,20 @@ class ClientTest extends TestCase public function testExportMethod(): void { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('The OpenSSL extension is not available.'); + } + $result = $this->client->export(); $this->assertNotEmpty($result); } public function testExportQuery(): void { + if (!extension_loaded('openssl')) { + $this->markTestSkipped('The OpenSSL extension is not available.'); + } + $result = $this->client->query('/export'); $this->assertNotEmpty($result); } From 0b117accd58a01699c587e4492bf12e49fc04a0e Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:41:21 +0300 Subject: [PATCH 37/38] --prefer-source parameter removed from travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 71f5bf7bf5e6f9c3f3c3ff011a18793094f81f25 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Wed, 1 Jul 2020 22:56:42 +0300 Subject: [PATCH 38/38] travis does not allow to use ssh on testing stage --- tests/ClientTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ClientTest.php b/tests/ClientTest.php index c2e032c..3841efd 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -274,8 +274,8 @@ class ClientTest extends TestCase public function testExportMethod(): void { - if (!extension_loaded('openssl')) { - $this->markTestSkipped('The OpenSSL extension is not available.'); + if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) { + $this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage'); } $result = $this->client->export(); @@ -284,8 +284,8 @@ class ClientTest extends TestCase public function testExportQuery(): void { - if (!extension_loaded('openssl')) { - $this->markTestSkipped('The OpenSSL extension is not available.'); + if (!in_array(gethostname(), ['pasha-lt', 'pasha-pc'])) { + $this->markTestSkipped('Travis does not allow to use SSH protocol on testing stage'); } $result = $this->client->query('/export');