From ccd0d5054aa39ec1c1fa9fdfa26988f4f0d53053 Mon Sep 17 00:00:00 2001 From: arily Date: Mon, 22 Jul 2019 19:30:40 +0900 Subject: [PATCH 1/7] Iterator --- src/Client.php | 7 ++ src/Iterators/ResponseIterator.php | 143 +++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/Iterators/ResponseIterator.php diff --git a/src/Client.php b/src/Client.php index ecb9d31..c05cc48 100644 --- a/src/Client.php +++ b/src/Client.php @@ -176,6 +176,12 @@ class Client implements Interfaces\ClientInterface return $parse ? $this->rosario($response) : $response; } + + public function readAsIterators() + { + return new Iterators\ResponseIterator($this->rosario($this->read(false))); + } + /** * This method was created by memory save reasons, it convert response * from RouterOS to readable array in safe way. @@ -220,6 +226,7 @@ class Client implements Interfaces\ClientInterface return $result; } + /** * Parse response from Router OS * diff --git a/src/Iterators/ResponseIterator.php b/src/Iterators/ResponseIterator.php new file mode 100644 index 0000000..d232f37 --- /dev/null +++ b/src/Iterators/ResponseIterator.php @@ -0,0 +1,143 @@ +current = 0; + // This RAW should't be an error + $positions = array_keys($raw, '!re'); + $count = count($raw); + $result = []; + + if (isset($positions[1])) { + + foreach ($positions as $key => $position) { + // Get length of future block + $length = isset($positions[$key + 1]) + ? $positions[$key + 1] - $position + 1 + : $count - $position; + + // Convert array to simple items + $item = []; + for ($i = 1; $i < $length; $i++) { + $item[] = array_shift($raw); + } + + // Save as result + $result[] = $item; + } + + } else { + $result = [$raw]; + } + + $this->raw = $result; + } + public function next(){ + ++$this->current; + } + public function current() { + if (isset($this->parsed[$this->current])){ + return $this->parsed[$this->current]; + } elseif (isset($this->raw[$this->current])){ + return $this->parseResponse($this->raw[$this->current])[0]; + } else { + return FALSE; + } + } + public function key() { + return $this->current; + } + public function valid() { + return isset($this->raw[$this->current]); + } + public function count() { + return count($this->raw); + } + public function rewind() { + $this->current = 0; + } + public function offsetSet($offset, $value) { + if (is_null($offset)) { + $this->parsed[] = $value; + throw new \RuntimeException('don\'t append to me It will cause a Bug sometime I PROMISE'); + } else { + $this->parsed[$offset] = $value; + } + } + public function offsetExists($offset) { + return isset($this->raw[$offset]) && $this->raw[$offset] !== ['!re']; + } + public function offsetUnset($offset) { + unset($this->parsed[$offset]); + unset($this->raw[$offset]); + } + public function offsetGet($offset) { + if (isset($this->parsed[$offset])){ + return $this->parsed[$offset]; + } elseif(isset($this->raw[$offset]) && $this->raw[$offset] !== NULL) { + $f = $this->parseResponse($this->raw[$offset]); + if ($f !==[]){ + $r = $this->parsed[$offset] = $f[0]; + return $r; + } + } else { + return FALSE; + } + } + public function flush(){ + $this->raw = []; + $this->parsed = []; + } + private function parseResponse(array $response): array + { + $result = []; + $i = -1; + $lines = \count($response); + foreach ($response as $key => $value) { + switch ($value) { + case '!re': + $i++; + break; + case '!fatal': + $result = $response; + break 2; + case '!trap': + case '!done': + // Check for =ret=, .tag and any other following messages + for ($j = $key + 1; $j <= $lines; $j++) { + // If we have lines after current one + if (isset($response[$j])) { + $this->pregResponse($response[$j], $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result['after'][$matches[1][0]] = $matches[2][0]; + } + } + } + break 2; + default: + $this->pregResponse($value, $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result[$i][$matches[1][0]] = $matches[2][0]; + } + break; + } + } + return $result; + } + private function pregResponse(string $value, &$matches) + { + preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); + } +} From e67b62b58f3f79152a73b49ca03f5b16181516fb Mon Sep 17 00:00:00 2001 From: arily Date: Mon, 22 Jul 2019 19:36:10 +0900 Subject: [PATCH 2/7] Iterator --- src/Client.php | 12 +++ src/Iterators/ResponseIterator.php | 146 +++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/Iterators/ResponseIterator.php diff --git a/src/Client.php b/src/Client.php index ecb9d31..2125fce 100644 --- a/src/Client.php +++ b/src/Client.php @@ -176,6 +176,17 @@ class Client implements Interfaces\ClientInterface return $parse ? $this->rosario($response) : $response; } + + /** + * Read using Iterators to improve performance on large dataset + * + * @return Iterators\ResponseIterator + */ + public function readAsIterator() + { + return new Iterators\ResponseIterator($this->read(false)); + } + /** * This method was created by memory save reasons, it convert response * from RouterOS to readable array in safe way. @@ -220,6 +231,7 @@ class Client implements Interfaces\ClientInterface return $result; } + /** * Parse response from Router OS * diff --git a/src/Iterators/ResponseIterator.php b/src/Iterators/ResponseIterator.php new file mode 100644 index 0000000..3fba772 --- /dev/null +++ b/src/Iterators/ResponseIterator.php @@ -0,0 +1,146 @@ +current = 0; + // This RAW should't be an error + $positions = array_keys($raw, '!re'); + $this->length = count($positions); + $count = count($raw); + $result = []; + + if (isset($positions[1])) { + + foreach ($positions as $key => $position) { + // Get length of future block + $length = isset($positions[$key + 1]) + ? $positions[$key + 1] - $position + 1 + : $count - $position; + + // Convert array to simple items + $item = array_slice($raw,$position,$length); + + // Save as result + $result[] = $item; + } + + } else { + $result = [$raw]; + } + + $this->raw = $result; + } + public function next(){ + ++$this->current; + } + public function current() { + if (isset($this->parsed[$this->current])){ + return $this->parsed[$this->current]; + } elseif (isset($this->raw[$this->current])){ + return $this->parseResponse($this->raw[$this->current])[0]; + } else { + return FALSE; + } + } + public function key() { + return $this->current; + } + public function valid() { + return isset($this->raw[$this->current]); + } + public function count() { + return count($this->raw); + } + public function rewind() { + $this->current = 0; + } + public function offsetSet($offset, $value) { + if (is_null($offset)) { + $this->parsed[] = $value; + throw new \RuntimeException('don\'t append to me It will cause a Bug sometime I PROMISE'); + } else { + $this->parsed[$offset] = $value; + } + } + public function offsetExists($offset) { + return isset($this->raw[$offset]) && $this->raw[$offset] !== ['!re']; + } + public function offsetUnset($offset) { + unset($this->parsed[$offset]); + unset($this->raw[$offset]); + } + public function offsetGet($offset) { + if (isset($this->parsed[$offset])){ + return $this->parsed[$offset]; + } elseif(isset($this->raw[$offset]) && $this->raw[$offset] !== NULL) { + $f = $this->parseResponse($this->raw[$offset]); + if ($f !==[]){ + $r = $this->parsed[$offset] = $f[0]; + return $r; + } + } else { + return FALSE; + } + } + public function flush(){ + $this->raw = []; + $this->parsed = []; + } + private function parseResponse(array $response): array + { + $result = []; + $i = -1; + $lines = \count($response); + foreach ($response as $key => $value) { + switch ($value) { + case '!re': + $i++; + break; + case '!fatal': + $result = $response; + break 2; + case '!trap': + case '!done': + // Check for =ret=, .tag and any other following messages + for ($j = $key + 1; $j <= $lines; $j++) { + // If we have lines after current one + if (isset($response[$j])) { + $this->pregResponse($response[$j], $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result['after'][$matches[1][0]] = $matches[2][0]; + } + } + } + break 2; + default: + $this->pregResponse($value, $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result[$i][$matches[1][0]] = $matches[2][0]; + } + break; + } + } + return $result; + } + private function pregResponse(string $value, &$matches) + { + preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); + } +} From 390fb733b3ab3befaa76d3be7ed47f292f3ab64b Mon Sep 17 00:00:00 2001 From: arily Date: Mon, 22 Jul 2019 19:38:32 +0900 Subject: [PATCH 3/7] Iterator --- src/Client.php | 12 +++ src/Iterators/ResponseIterator.php | 146 +++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/Iterators/ResponseIterator.php diff --git a/src/Client.php b/src/Client.php index ecb9d31..2125fce 100644 --- a/src/Client.php +++ b/src/Client.php @@ -176,6 +176,17 @@ class Client implements Interfaces\ClientInterface return $parse ? $this->rosario($response) : $response; } + + /** + * Read using Iterators to improve performance on large dataset + * + * @return Iterators\ResponseIterator + */ + public function readAsIterator() + { + return new Iterators\ResponseIterator($this->read(false)); + } + /** * This method was created by memory save reasons, it convert response * from RouterOS to readable array in safe way. @@ -220,6 +231,7 @@ class Client implements Interfaces\ClientInterface return $result; } + /** * Parse response from Router OS * diff --git a/src/Iterators/ResponseIterator.php b/src/Iterators/ResponseIterator.php new file mode 100644 index 0000000..3fba772 --- /dev/null +++ b/src/Iterators/ResponseIterator.php @@ -0,0 +1,146 @@ +current = 0; + // This RAW should't be an error + $positions = array_keys($raw, '!re'); + $this->length = count($positions); + $count = count($raw); + $result = []; + + if (isset($positions[1])) { + + foreach ($positions as $key => $position) { + // Get length of future block + $length = isset($positions[$key + 1]) + ? $positions[$key + 1] - $position + 1 + : $count - $position; + + // Convert array to simple items + $item = array_slice($raw,$position,$length); + + // Save as result + $result[] = $item; + } + + } else { + $result = [$raw]; + } + + $this->raw = $result; + } + public function next(){ + ++$this->current; + } + public function current() { + if (isset($this->parsed[$this->current])){ + return $this->parsed[$this->current]; + } elseif (isset($this->raw[$this->current])){ + return $this->parseResponse($this->raw[$this->current])[0]; + } else { + return FALSE; + } + } + public function key() { + return $this->current; + } + public function valid() { + return isset($this->raw[$this->current]); + } + public function count() { + return count($this->raw); + } + public function rewind() { + $this->current = 0; + } + public function offsetSet($offset, $value) { + if (is_null($offset)) { + $this->parsed[] = $value; + throw new \RuntimeException('don\'t append to me It will cause a Bug sometime I PROMISE'); + } else { + $this->parsed[$offset] = $value; + } + } + public function offsetExists($offset) { + return isset($this->raw[$offset]) && $this->raw[$offset] !== ['!re']; + } + public function offsetUnset($offset) { + unset($this->parsed[$offset]); + unset($this->raw[$offset]); + } + public function offsetGet($offset) { + if (isset($this->parsed[$offset])){ + return $this->parsed[$offset]; + } elseif(isset($this->raw[$offset]) && $this->raw[$offset] !== NULL) { + $f = $this->parseResponse($this->raw[$offset]); + if ($f !==[]){ + $r = $this->parsed[$offset] = $f[0]; + return $r; + } + } else { + return FALSE; + } + } + public function flush(){ + $this->raw = []; + $this->parsed = []; + } + private function parseResponse(array $response): array + { + $result = []; + $i = -1; + $lines = \count($response); + foreach ($response as $key => $value) { + switch ($value) { + case '!re': + $i++; + break; + case '!fatal': + $result = $response; + break 2; + case '!trap': + case '!done': + // Check for =ret=, .tag and any other following messages + for ($j = $key + 1; $j <= $lines; $j++) { + // If we have lines after current one + if (isset($response[$j])) { + $this->pregResponse($response[$j], $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result['after'][$matches[1][0]] = $matches[2][0]; + } + } + } + break 2; + default: + $this->pregResponse($value, $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result[$i][$matches[1][0]] = $matches[2][0]; + } + break; + } + } + return $result; + } + private function pregResponse(string $value, &$matches) + { + preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); + } +} From ba6dd116972026d34c55a01cdc35f96b63e358df Mon Sep 17 00:00:00 2001 From: arily Date: Mon, 22 Jul 2019 19:45:38 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EF=BD=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Client.php | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Client.php b/src/Client.php index fced313..f84ebc4 100644 --- a/src/Client.php +++ b/src/Client.php @@ -14,7 +14,8 @@ use RouterOS\Interfaces\ClientInterface; * @package RouterOS * @since 0.1 */ -class Client implements Interfaces\ClientInterface { +class Client implements Interfaces\ClientInterface +{ use SocketTrait, ShortsTrait; /** @@ -40,7 +41,8 @@ class Client implements Interfaces\ClientInterface { * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException */ - public function __construct($config) { + public function __construct($config) + { // If array then need create object if (\is_array($config)) { $config = new Config($config); @@ -67,7 +69,8 @@ class Client implements Interfaces\ClientInterface { * @return mixed * @throws \RouterOS\Exceptions\ConfigException */ - private function config(string $parameter) { + private function config(string $parameter) + { return $this->_config->get($parameter); } @@ -77,7 +80,8 @@ class Client implements Interfaces\ClientInterface { * @return \RouterOS\Config * @since 0.6 */ - public function getConfig(): Config { + public function getConfig(): Config + { return $this->_config; } @@ -87,7 +91,8 @@ class Client implements Interfaces\ClientInterface { * @param \RouterOS\Config $config * @since 0.7 */ - public function setConfig(Config $config) { + public function setConfig(Config $config) + { $this->_config = $config; } @@ -98,7 +103,8 @@ class Client implements Interfaces\ClientInterface { * @return \RouterOS\Interfaces\ClientInterface * @throws \RouterOS\Exceptions\QueryException */ - public function write($query): ClientInterface { + public function write($query): ClientInterface + { if (\is_string($query)) { $query = new Query($query); } elseif (\is_array($query)) { @@ -133,7 +139,8 @@ class Client implements Interfaces\ClientInterface { * @param bool $parse * @return mixed */ - public function read(bool $parse = true) { + public function read(bool $parse = true) + { // By default response is empty $response = []; // We have to wait a !done or !fatal @@ -174,7 +181,8 @@ class Client implements Interfaces\ClientInterface { * * @return Iterators\ResponseIterator */ - public function readAsIterator() { + public function readAsIterator() + { return new Iterators\ResponseIterator($this->read(false)); } @@ -271,7 +279,8 @@ class Client implements Interfaces\ClientInterface { * @param string $value * @param array $matches */ - private function pregResponse(string $value, &$matches) { + private function pregResponse(string $value, &$matches) + { preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); } @@ -284,7 +293,8 @@ class Client implements Interfaces\ClientInterface { * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException */ - private function login(bool $legacyRetry = false): bool { + private function login(bool $legacyRetry = false): bool + { // If legacy login scheme is enabled if ($this->config('legacy')) { // For the first we need get hash with salt @@ -330,7 +340,8 @@ 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'); } @@ -342,7 +353,8 @@ class Client implements Interfaces\ClientInterface { * @throws \RouterOS\Exceptions\ConfigException * @throws \RouterOS\Exceptions\QueryException */ - private function connect(): bool{ + private function connect(): bool + { // By default we not connected $connected = false; From 9a267d0710550e1407b00c8b1fa5384861676cc5 Mon Sep 17 00:00:00 2001 From: arily Date: Mon, 22 Jul 2019 19:46:33 +0900 Subject: [PATCH 5/7] Delete .DS_Store --- src/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index f1cb65b142bbfe97aa257d8f37a49639ee293542..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyK2Kg5ZrZC$hdLo@_r$Ia0sVJ?H_Q!!A+3CiCwGmcjePE`yfO}X$)z?EbPwR z-p&b4q1B3rc9+AW$XY~NxS`xEOwG>CC$`9p0^zvhMLy&tXL-NwCe`x^<1XbHyjSpk z@X!6*sXxZe^HgT302QDDRDcRlfjbni-V1A2fs9mu3Q&P>1?>A!;D$AE2=q?}f{y^e z25C2}eU<86Nf;jBkyz|e+En!8Ws3!1-<}Js}-OC From 19aefc21003e869cb4501c4d72566ac225392a37 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Mon, 22 Jul 2019 15:01:37 +0300 Subject: [PATCH 6/7] ResponseIterator moved to room of sources, small tune of codebase in according with PSR standars, flush method removed --- src/Iterators/ResponseIterator.php | 145 ------------------------- src/ResponseIterator.php | 210 +++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 145 deletions(-) delete mode 100644 src/Iterators/ResponseIterator.php create mode 100644 src/ResponseIterator.php diff --git a/src/Iterators/ResponseIterator.php b/src/Iterators/ResponseIterator.php deleted file mode 100644 index 336a4f5..0000000 --- a/src/Iterators/ResponseIterator.php +++ /dev/null @@ -1,145 +0,0 @@ -current = 0; - // This RAW should't be an error - $positions = array_keys($raw, '!re'); - $this->length = count($positions); - $count = count($raw); - $result = []; - - if (isset($positions[1])) { - - foreach ($positions as $key => $position) { - // Get length of future block - $length = isset($positions[$key + 1]) - ? $positions[$key + 1] - $position + 1 - : $count - $position; - - // Convert array to simple items - // Save as result - $result[] = array_slice($raw, $position, $length); - } - - } else { - $result = [$raw]; - } - - $this->raw = $result; - } - public function next() { - ++$this->current; - } - public function current() { - if (isset($this->parsed[$this->current])) { - return $this->parsed[$this->current]; - } elseif (isset($this->raw[$this->current])) { - return $this->parseResponse($this->raw[$this->current])[0]; - } else { - return FALSE; - } - } - public function key() { - return $this->current; - } - public function valid() { - return isset($this->raw[$this->current]); - } - public function count() { - return count($this->raw); - } - public function rewind() { - $this->current = 0; - } - public function offsetSet($offset, $value) { - if (is_null($offset)) { - $this->parsed[] = $value; - throw new \RuntimeException('don\'t append to me It will cause a Bug sometime I PROMISE'); - } else { - $this->parsed[$offset] = $value; - } - } - public function offsetExists($offset) { - return isset($this->raw[$offset]) && $this->raw[$offset] !== ['!re']; - } - public function offsetUnset($offset) { - unset($this->parsed[$offset]); - unset($this->raw[$offset]); - } - public function offsetGet($offset) { - if (isset($this->parsed[$offset])) { - return $this->parsed[$offset]; - } elseif (isset($this->raw[$offset]) && $this->raw[$offset] !== NULL) { - $f = $this->parseResponse($this->raw[$offset]); - if ($f !== []) { - $r = $this->parsed[$offset] = $f[0]; - return $r; - } - } else { - return FALSE; - } - } - public function flush() { - $this->raw = []; - $this->parsed = []; - } - private function parseResponse(array $response): array - { - $result = []; - $i = -1; - $lines = \count($response); - foreach ($response as $key => $value) { - switch ($value) { - case '!re': - $i++; - break; - case '!fatal': - $result = $response; - break 2; - case '!trap': - case '!done': - // Check for =ret=, .tag and any other following messages - for ($j = $key + 1; $j <= $lines; $j++) { - // If we have lines after current one - if (isset($response[$j])) { - $this->pregResponse($response[$j], $matches); - if (isset($matches[1][0], $matches[2][0])) { - $result['after'][$matches[1][0]] = $matches[2][0]; - } - } - } - break 2; - default: - $this->pregResponse($value, $matches); - if (isset($matches[1][0], $matches[2][0])) { - $result[$i][$matches[1][0]] = $matches[2][0]; - } - break; - } - } - return $result; - } - private function pregResponse(string $value, &$matches) { - preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); - } -} diff --git a/src/ResponseIterator.php b/src/ResponseIterator.php new file mode 100644 index 0000000..6654d50 --- /dev/null +++ b/src/ResponseIterator.php @@ -0,0 +1,210 @@ +rewind(); + + // Save client as parameter of object + $this->client = $client; + + // Read RAW data from client + $raw = $client->read(false); + + // This RAW should't be an error + $positions = array_keys($raw, '!re'); + $count = count($raw); + $result = []; + + if (isset($positions[1])) { + + foreach ($positions as $key => $position) { + + // Get length of future block + $length = isset($positions[$key + 1]) + ? $positions[$key + 1] - $position + 1 + : $count - $position; + + // Convert array to simple items, save as result + $result[] = array_slice($raw, $position, $length); + } + + } else { + $result = [$raw]; + } + + $this->raw = $result; + } + + /** + * Move forward to next element + */ + public function next() + { + ++$this->current; + } + + /** + * Return the current element + * + * @return mixed + */ + public function current() + { + if (isset($this->parsed[$this->current])) { + return $this->parsed[$this->current]; + } + + if (isset($this->raw[$this->current])) { + return $this->client->parseResponse($this->raw[$this->current])[0]; + } + + return null; + } + + /** + * Return the key of the current element + * + * @return mixed + */ + public function key() + { + return $this->current; + } + + /** + * Checks if current position is valid + * + * @return bool + */ + public function valid(): bool + { + return isset($this->raw[$this->current]); + } + + /** + * Count elements of an object + * + * @return int + */ + public function count(): int + { + return count($this->raw); + } + + /** + * Rewind the Iterator to the first element + */ + public function rewind() + { + $this->current = 0; + } + + /** + * Offset to set + * + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->parsed[] = $value; + } else { + $this->parsed[$offset] = $value; + } + } + + /** + * Whether a offset exists + * + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset): bool + { + return isset($this->raw[$offset]) && $this->raw[$offset] !== ['!re']; + } + + /** + * Offset to unset + * + * @param mixed $offset + */ + public function offsetUnset($offset) + { + unset($this->parsed[$offset], $this->raw[$offset]); + } + + /** + * Offset to retrieve + * + * @param mixed $offset + * @return bool|mixed + */ + public function offsetGet($offset) + { + if (isset($this->parsed[$offset])) { + return $this->parsed[$offset]; + } + + if (isset($this->raw[$offset]) && $this->raw[$offset] !== null) { + $f = $this->client->parseResponse($this->raw[$offset]); + if ($f !== []) { + return $this->parsed[$offset] = $f[0]; + } + } + + return false; + } +} From ca2646b2be7e220247b813041930610b26937799 Mon Sep 17 00:00:00 2001 From: Paul Rock Date: Mon, 22 Jul 2019 15:02:36 +0300 Subject: [PATCH 7/7] tune of code with according to PSR standards, readLiterator fix, now Client object will source of new object --- src/Client.php | 738 ++++++++++++++++++------------------- src/Interfaces/ClientInterface.php | 2 +- 2 files changed, 370 insertions(+), 370 deletions(-) diff --git a/src/Client.php b/src/Client.php index f84ebc4..df7c434 100644 --- a/src/Client.php +++ b/src/Client.php @@ -14,374 +14,374 @@ use RouterOS\Interfaces\ClientInterface; * @package RouterOS * @since 0.1 */ -class Client implements Interfaces\ClientInterface +class Client implements Interfaces\ClientInterface { - use SocketTrait, ShortsTrait; - - /** - * Configuration of connection - * - * @var \RouterOS\Config - */ - private $_config; - - /** - * API communication object - * - * @var \RouterOS\APIConnector - */ - - private $_connector; - - /** - * Client constructor. - * - * @param array|\RouterOS\Config $config - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\ConfigException - * @throws \RouterOS\Exceptions\QueryException - */ - public function __construct($config) - { - // If array then need create object - if (\is_array($config)) { - $config = new Config($config); - } - - // Check for important keys - if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) { - throw new ConfigException("One or few parameters '$key' of Config is not set or empty"); - } - - // Save config if everything is okay - $this->setConfig($config); - - // Throw error if cannot to connect - if (false === $this->connect()) { - throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port')); - } - } - - /** - * Get some parameter from config - * - * @param string $parameter Name of required parameter - * @return mixed - * @throws \RouterOS\Exceptions\ConfigException - */ - private function config(string $parameter) - { - return $this->_config->get($parameter); - } - - /** - * Return socket resource if is exist - * - * @return \RouterOS\Config - * @since 0.6 - */ - public function getConfig(): Config - { - return $this->_config; - } - - /** - * Set configuration of client - * - * @param \RouterOS\Config $config - * @since 0.7 - */ - public function setConfig(Config $config) - { - $this->_config = $config; - } - - /** - * Send write query to RouterOS (with or without tag) - * - * @param string|array|\RouterOS\Query $query - * @return \RouterOS\Interfaces\ClientInterface - * @throws \RouterOS\Exceptions\QueryException - */ - 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'); - } - - // Send commands via loop to router - foreach ($query->getQuery() as $command) { - $this->_connector->writeWord(trim($command)); - } - - // Write zero-terminator (empty string) - $this->_connector->writeWord(''); - - return $this; - } - - /** - * 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 = true) - { - // By default response is empty - $response = []; - // We have to wait a !done or !fatal - $lastReply = false; - - // Read answer from socket in loop - while (true) { - $word = $this->_connector->readWord(); - - if ('' === $word) { - if ($lastReply) { - // We received a !done or !fatal message in a precedent loop - // response is complete - break; - } - // We did not receive the !done or !fatal message - // This 0 length message is the end of a reply !re or !trap - // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message - continue; - } - - // Save output line to response array - $response[] = $word; - - // If we get a !done or !fatal line in response, we are now ready to finish the read - // but we need to wait a 0 length message, switch the flag - if ('!done' === $word || '!fatal' === $word) { - $lastReply = true; - } - } - - // Parse results and return - return $parse ? $this->rosario($response) : $response; - } - - /** - * Read using Iterators to improve performance on large dataset - * - * @return Iterators\ResponseIterator - */ - public function readAsIterator() - { - return new Iterators\ResponseIterator($this->read(false)); - } - - /** - * This method was created by memory save reasons, it convert response - * from RouterOS to readable array in safe way. - * - * @param array $raw Array RAW response from server - * @return mixed - * - * Based on RouterOSResponseArray solution by @arily - * - * @link https://github.com/arily/RouterOSResponseArray - * @since 0.10 - */ - private function rosario(array $raw): array - { - // This RAW should't be an error - $positions = array_keys($raw, '!re'); - $count = count($raw); - $result = []; - - if (isset($positions[1])) { - - foreach ($positions as $key => $position) { - // Get length of future block - $length = isset($positions[$key + 1]) - ? $positions[$key + 1] - $position + 1 - : $count - $position; - - // Convert array to simple items - $item = []; - for ($i = 1; $i < $length; $i++) { - $item[] = array_shift($raw); - } - - // Save as result - $result[] = $this->parseResponse($item)[0]; - } - - } else { - $result = $this->parseResponse($raw); - } - - return $result; - } - - /** - * Parse response from Router OS - * - * @param array $response Response data - * @return array Array with parsed data - */ - protected function parseResponse(array $response): array - { - $result = []; - $i = -1; - $lines = \count($response); - foreach ($response as $key => $value) { - switch ($value) { - case '!re': - $i++; - break; - case '!fatal': - $result = $response; - break 2; - case '!trap': - case '!done': - // Check for =ret=, .tag and any other following messages - for ($j = $key + 1; $j <= $lines; $j++) { - // If we have lines after current one - if (isset($response[$j])) { - $this->pregResponse($response[$j], $matches); - if (isset($matches[1][0], $matches[2][0])) { - $result['after'][$matches[1][0]] = $matches[2][0]; - } - } - } - break 2; - default: - $this->pregResponse($value, $matches); - if (isset($matches[1][0], $matches[2][0])) { - $result[$i][$matches[1][0]] = $matches[2][0]; - } - break; - } - } - return $result; - } - - /** - * Parse result from RouterOS by regular expression - * - * @param string $value - * @param array $matches - */ - private function pregResponse(string $value, &$matches) - { - preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); - } - - /** - * Authorization logic - * - * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS - * @return bool - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\ConfigException - * @throws \RouterOS\Exceptions\QueryException - */ - private function login(bool $legacyRetry = false): bool - { - // If legacy login scheme is enabled - if ($this->config('legacy')) { - // For the first we need get hash with salt - $query = new Query('/login'); - $response = $this->write($query)->read(); - - // Now need use this hash for authorization - $query = (new Query('/login')) - ->add('=name=' . $this->config('user')) - ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret']))); - } else { - // Just login with our credentials - $query = (new Query('/login')) - ->add('=name=' . $this->config('user')) - ->add('=password=' . $this->config('pass')); - - // If we set modern auth scheme but router with legacy firmware then need to retry query, - // but need to prevent endless loop - $legacyRetry = true; - } - - // Execute query and get response - $response = $this->write($query)->read(false); - - // if: - // - we have more than one response - // - response is '!done' - // => 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); - return $this->login(); - } - - // Return true if we have only one line from server and this line is !done - return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done'); - } - - /** - * Detect by login request if firmware is legacy - * - * @param array $response - * @return bool - * @throws ConfigException - */ - private function isLegacy(array &$response): bool - { - return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); - } - - /** - * Connect to socket server - * - * @return bool - * @throws \RouterOS\Exceptions\ClientException - * @throws \RouterOS\Exceptions\ConfigException - * @throws \RouterOS\Exceptions\QueryException - */ - private function connect(): bool - { - // By default we not connected - $connected = false; - - // Few attempts in loop - for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) { - - // Initiate socket session - $this->openSocket(); - - // If socket is active - if (null !== $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; - break; - } - - // Else close socket and start from begin - $this->closeSocket(); - } - - // Sleep some time between tries - sleep($this->config('delay')); - } - - // Return status of connection - return $connected; - } + use SocketTrait, ShortsTrait; + + /** + * Configuration of connection + * + * @var \RouterOS\Config + */ + private $_config; + + /** + * API communication object + * + * @var \RouterOS\APIConnector + */ + + private $_connector; + + /** + * Client constructor. + * + * @param array|\RouterOS\Config $config + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\QueryException + */ + public function __construct($config) + { + // If array then need create object + if (\is_array($config)) { + $config = new Config($config); + } + + // Check for important keys + if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) { + throw new ConfigException("One or few parameters '$key' of Config is not set or empty"); + } + + // Save config if everything is okay + $this->setConfig($config); + + // Throw error if cannot to connect + if (false === $this->connect()) { + throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port')); + } + } + + /** + * Get some parameter from config + * + * @param string $parameter Name of required parameter + * @return mixed + * @throws \RouterOS\Exceptions\ConfigException + */ + private function config(string $parameter) + { + return $this->_config->get($parameter); + } + + /** + * Return socket resource if is exist + * + * @return \RouterOS\Config + * @since 0.6 + */ + public function getConfig(): Config + { + return $this->_config; + } + + /** + * Set configuration of client + * + * @param \RouterOS\Config $config + * @since 0.7 + */ + public function setConfig(Config $config) + { + $this->_config = $config; + } + + /** + * Send write query to RouterOS (with or without tag) + * + * @param string|array|\RouterOS\Query $query + * @return \RouterOS\Interfaces\ClientInterface + * @throws \RouterOS\Exceptions\QueryException + */ + 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'); + } + + // Send commands via loop to router + foreach ($query->getQuery() as $command) { + $this->_connector->writeWord(trim($command)); + } + + // Write zero-terminator (empty string) + $this->_connector->writeWord(''); + + return $this; + } + + /** + * 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 = true) + { + // By default response is empty + $response = []; + // We have to wait a !done or !fatal + $lastReply = false; + + // Read answer from socket in loop + while (true) { + $word = $this->_connector->readWord(); + + if ('' === $word) { + if ($lastReply) { + // We received a !done or !fatal message in a precedent loop + // response is complete + break; + } + // We did not receive the !done or !fatal message + // This 0 length message is the end of a reply !re or !trap + // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message + continue; + } + + // Save output line to response array + $response[] = $word; + + // If we get a !done or !fatal line in response, we are now ready to finish the read + // but we need to wait a 0 length message, switch the flag + if ('!done' === $word || '!fatal' === $word) { + $lastReply = true; + } + } + + // Parse results and return + return $parse ? $this->rosario($response) : $response; + } + + /** + * Read using Iterators to improve performance on large dataset + * + * @return \RouterOS\ResponseIterator + */ + public function readAsIterator(): ResponseIterator + { + return new ResponseIterator($this); + } + + /** + * This method was created by memory save reasons, it convert response + * from RouterOS to readable array in safe way. + * + * @param array $raw Array RAW response from server + * @return mixed + * + * Based on RouterOSResponseArray solution by @arily + * + * @link https://github.com/arily/RouterOSResponseArray + * @since 0.10 + */ + private function rosario(array $raw): array + { + // This RAW should't be an error + $positions = array_keys($raw, '!re'); + $count = count($raw); + $result = []; + + if (isset($positions[1])) { + + foreach ($positions as $key => $position) { + // Get length of future block + $length = isset($positions[$key + 1]) + ? $positions[$key + 1] - $position + 1 + : $count - $position; + + // Convert array to simple items + $item = []; + for ($i = 1; $i < $length; $i++) { + $item[] = array_shift($raw); + } + + // Save as result + $result[] = $this->parseResponse($item)[0]; + } + + } else { + $result = $this->parseResponse($raw); + } + + return $result; + } + + /** + * Parse response from Router OS + * + * @param array $response Response data + * @return array Array with parsed data + */ + public function parseResponse(array $response): array + { + $result = []; + $i = -1; + $lines = \count($response); + foreach ($response as $key => $value) { + switch ($value) { + case '!re': + $i++; + break; + case '!fatal': + $result = $response; + break 2; + case '!trap': + case '!done': + // Check for =ret=, .tag and any other following messages + for ($j = $key + 1; $j <= $lines; $j++) { + // If we have lines after current one + if (isset($response[$j])) { + $this->pregResponse($response[$j], $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result['after'][$matches[1][0]] = $matches[2][0]; + } + } + } + break 2; + default: + $this->pregResponse($value, $matches); + if (isset($matches[1][0], $matches[2][0])) { + $result[$i][$matches[1][0]] = $matches[2][0]; + } + break; + } + } + return $result; + } + + /** + * Parse result from RouterOS by regular expression + * + * @param string $value + * @param array $matches + */ + private function pregResponse(string $value, &$matches) + { + preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches); + } + + /** + * Authorization logic + * + * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS + * @return bool + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\QueryException + */ + private function login(bool $legacyRetry = false): bool + { + // If legacy login scheme is enabled + if ($this->config('legacy')) { + // For the first we need get hash with salt + $query = new Query('/login'); + $response = $this->write($query)->read(); + + // Now need use this hash for authorization + $query = (new Query('/login')) + ->add('=name=' . $this->config('user')) + ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret']))); + } else { + // Just login with our credentials + $query = (new Query('/login')) + ->add('=name=' . $this->config('user')) + ->add('=password=' . $this->config('pass')); + + // If we set modern auth scheme but router with legacy firmware then need to retry query, + // but need to prevent endless loop + $legacyRetry = true; + } + + // Execute query and get response + $response = $this->write($query)->read(false); + + // if: + // - we have more than one response + // - response is '!done' + // => 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); + return $this->login(); + } + + // Return true if we have only one line from server and this line is !done + return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done'); + } + + /** + * Detect by login request if firmware is legacy + * + * @param array $response + * @return bool + * @throws ConfigException + */ + private function isLegacy(array &$response): bool + { + return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy'); + } + + /** + * Connect to socket server + * + * @return bool + * @throws \RouterOS\Exceptions\ClientException + * @throws \RouterOS\Exceptions\ConfigException + * @throws \RouterOS\Exceptions\QueryException + */ + private function connect(): bool + { + // By default we not connected + $connected = false; + + // Few attempts in loop + for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) { + + // Initiate socket session + $this->openSocket(); + + // If socket is active + if (null !== $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; + break; + } + + // Else close socket and start from begin + $this->closeSocket(); + } + + // Sleep some time between tries + sleep($this->config('delay')); + } + + // Return status of connection + return $connected; + } } diff --git a/src/Interfaces/ClientInterface.php b/src/Interfaces/ClientInterface.php index a87e4c6..c6e4dba 100644 --- a/src/Interfaces/ClientInterface.php +++ b/src/Interfaces/ClientInterface.php @@ -58,7 +58,7 @@ interface ClientInterface * @param bool $parse * @return mixed */ - public function read(bool $parse = true); + public function read(bool $parse); /** * Send write query to RouterOS (with or without tag)