Completed
Push — master ( 87995b...abed83 )
by Mr
03:29
created

Client::isLegacy()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
1
<?php
2
3
namespace RouterOS;
4
5
use RouterOS\Exceptions\ClientException;
6
use RouterOS\Exceptions\ConfigException;
7
use RouterOS\Exceptions\QueryException;
8
use RouterOS\Helpers\ArrayHelper;
9
use RouterOS\Interfaces\ClientInterface;
10
11
/**
12
 * Class Client for RouterOS management
13
 *
14
 * @package RouterOS
15
 * @since   0.1
16
 */
17
class Client implements Interfaces\ClientInterface
18
{
19
    use SocketTrait, ShortsTrait;
20
21
    /**
22
     * Configuration of connection
23
     *
24
     * @var \RouterOS\Config
25
     */
26
    private $_config;
27
28
    /**
29
     * API communication object
30
     *
31
     * @var \RouterOS\APIConnector
32
     */
33
34
    private $_connector;
35
36
    /**
37
     * Client constructor.
38
     *
39
     * @param array|\RouterOS\Config $config
40
     * @throws \RouterOS\Exceptions\ClientException
41
     * @throws \RouterOS\Exceptions\ConfigException
42
     * @throws \RouterOS\Exceptions\QueryException
43
     */
44 14
    public function __construct($config)
45
    {
46
        // If array then need create object
47 14
        if (\is_array($config)) {
48 3
            $config = new Config($config);
49
        }
50
51
        // Check for important keys
52 14
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
53 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
54
        }
55
56
        // Save config if everything is okay
57 13
        $this->setConfig($config);
58
59
        // Throw error if cannot to connect
60 13
        if (false === $this->connect()) {
61 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
62
        }
63 11
    }
64
65
    /**
66
     * Get some parameter from config
67
     *
68
     * @param string $parameter Name of required parameter
69
     * @return mixed
70
     * @throws \RouterOS\Exceptions\ConfigException
71
     */
72 13
    private function config(string $parameter)
73
    {
74 13
        return $this->_config->get($parameter);
75
    }
76
77
    /**
78
     * Return socket resource if is exist
79
     *
80
     * @return \RouterOS\Config
81
     * @since 0.6
82
     */
83 1
    public function getConfig(): Config
84
    {
85 1
        return $this->_config;
86
    }
87
88
    /**
89
     * Set configuration of client
90
     *
91
     * @param \RouterOS\Config $config
92
     * @since 0.7
93
     */
94 13
    public function setConfig(Config $config)
95
    {
96 13
        $this->_config = $config;
97 13
    }
98
99
    /**
100
     * Send write query to RouterOS (with or without tag)
101
     *
102
     * @param string|array|\RouterOS\Query $query
103
     * @return \RouterOS\Interfaces\ClientInterface
104
     * @throws \RouterOS\Exceptions\QueryException
105
     */
106 12
    public function write($query): ClientInterface
107
    {
108 12
        if (\is_string($query)) {
109 2
            $query = new Query($query);
110 12
        } elseif (\is_array($query)) {
111 1
            $endpoint = array_shift($query);
112 1
            $query    = new Query($endpoint, $query);
113
        }
114
115 12
        if (!$query instanceof Query) {
116 1
            throw new QueryException('Parameters cannot be processed');
117
        }
118
119
        // Send commands via loop to router
120 12
        foreach ($query->getQuery() as $command) {
121 12
            $this->_connector->writeWord(trim($command));
122
        }
123
124
        // Write zero-terminator (empty string)
125 12
        $this->_connector->writeWord('');
126
127 12
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (RouterOS\Client) is incompatible with the return type declared by the interface RouterOS\Interfaces\ClientInterface::write of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
128
    }
129
130
    /**
131
     * Read answer from server after query was executed
132
     *
133
     * A Mikrotik reply is formed of blocks
134
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
135
     * Each block end with an zero byte (empty line)
136
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
137
     * A !fatal block precedes TCP connexion close
138
     *
139
     * @param bool $parse
140
     * @return mixed
141
     */
142 12
    public function read(bool $parse = true)
143
    {
144
        // By default response is empty
145 12
        $response = [];
146
        // We have to wait a !done or !fatal 
147 12
        $lastReply = false;
148
149
        // Read answer from socket in loop
150 12
        while (true) {
151 12
            $word = $this->_connector->readWord();
152
153 12
            if ('' === $word) {
154 12
                if ($lastReply) {
155
                    // We received a !done or !fatal message in a precedent loop
156
                    // response is complete
157 12
                    break;
158
                }
159
                // We did not receive the !done or !fatal message
160
                // This 0 length message is the end of a reply !re or !trap
161
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
162 4
                continue;
163
            }
164
165
            // Save output line to response array
166 12
            $response[] = $word;
167
168
            // If we get a !done or !fatal line in response, we are now ready to finish the read
169
            // but we need to wait a 0 length message, switch the flag
170 12
            if ('!done' === $word || '!fatal' === $word) {
171 12
                $lastReply = true;
172
            }
173
        }
174
175
        // Parse results and return
176 12
        return $parse ? $this->rosario($response) : $response;
177
    }
178
179
    /**
180
     * Class Rosario, created for work with response array from RouterOS
181
     * as with multiple chunks of values, this class was created by memory save reasons.
182
     *
183
     * Based on RouterOSResponseArray solution by @arily
184
     *
185
     * @link    https://github.com/arily/RouterOSResponseArray
186
     * @package RouterOS
187
     * @since   0.10
188
     */
189
190
    /**
191
     * This method was created by memory save reasons, it convert response
192
     * from RouterOS to readable array in safe way.
193
     *
194
     * @param array $raw Array RAW response from server
195
     * @return mixed
196
     *
197
     * Based on RouterOSResponseArray solution by @arily
198
     *
199
     * @link    https://github.com/arily/RouterOSResponseArray
200
     * @since   0.10
201
     */
202 4
    private function rosario(array $raw): array
203
    {
204
        // This RAW should't be an error
205 4
        $positions = array_keys($raw, '!re');
206 4
        $count     = count($raw);
207 4
        $result    = [];
208
209 4
        if (isset($positions[1])) {
210
211
            foreach ($positions as $key => $position) {
212
                // Get length of future block
213
                $length = isset($positions[$key + 1])
214
                    ? $positions[$key + 1] - $position + 1
215
                    : $count - $position;
216
217
                // Convert array to simple items
218
                $item = [];
219
                for ($i = 1; $i < $length; $i++) {
220
                    $item[] = array_shift($raw);
221
                }
222
223
                // Save as result
224
                $result[] = $this->parseResponse($item)[0];
225
            }
226
227
        } else {
228 4
            $result = $this->parseResponse($raw);
229
        }
230
231 4
        return $result;
232
    }
233
234
    /**
235
     * Parse response from Router OS
236
     *
237
     * @param array $response Response data
238
     * @return array Array with parsed data
239
     */
240 4
    protected function parseResponse(array $response): array
241
    {
242 4
        $result = [];
243 4
        $i      = -1;
244 4
        $lines  = \count($response);
245 4
        foreach ($response as $key => $value) {
246
            switch ($value) {
247 4
                case '!re':
248 1
                    $i++;
249 1
                    break;
250 4
                case '!fatal':
251 1
                    $result = $response;
252 1
                    break 2;
253 3
                case '!trap':
254 3
                case '!done':
255
                    // Check for =ret=, .tag and any other following messages
256 3
                    for ($j = $key + 1; $j <= $lines; $j++) {
257
                        // If we have lines after current one
258 3
                        if (isset($response[$j])) {
259 2
                            $this->pregResponse($response[$j], $matches);
260 2 View Code Duplication
                            if (isset($matches[1][0], $matches[2][0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
261 2
                                $result['after'][$matches[1][0]] = $matches[2][0];
262
                            }
263
                        }
264
                    }
265 3
                    break 2;
266
                default:
267 1
                    $this->pregResponse($value, $matches);
268 1 View Code Duplication
                    if (isset($matches[1][0], $matches[2][0])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
269 1
                        $result[$i][$matches[1][0]] = $matches[2][0];
270
                    }
271 1
                    break;
272
            }
273
        }
274 4
        return $result;
275
    }
276
277
    /**
278
     * Parse result from RouterOS by regular expression
279
     *
280
     * @param string $value
281
     * @param array  $matches
282
     */
283 3
    private function pregResponse(string $value, &$matches)
284
    {
285 3
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
286 3
    }
287
288
    /**
289
     * Authorization logic
290
     *
291
     * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS
292
     * @return bool
293
     * @throws \RouterOS\Exceptions\ClientException
294
     * @throws \RouterOS\Exceptions\ConfigException
295
     * @throws \RouterOS\Exceptions\QueryException
296
     */
297 12
    private function login(bool $legacyRetry = false): bool
298
    {
299
        // If legacy login scheme is enabled
300 12
        if ($this->config('legacy')) {
301
            // For the first we need get hash with salt
302 2
            $query    = new Query('/login');
303 2
            $response = $this->write($query)->read();
304
305
            // Now need use this hash for authorization
306 2
            $query = (new Query('/login'))
307 2
                ->add('=name=' . $this->config('user'))
308 2
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])));
309
        } else {
310
            // Just login with our credentials
311 11
            $query = (new Query('/login'))
312 11
                ->add('=name=' . $this->config('user'))
313 11
                ->add('=password=' . $this->config('pass'));
314
315
            // If we set modern auth scheme but router with legacy firmware then need to retry query,
316
            // but need to prevent endless loop
317 11
            $legacyRetry = true;
318
        }
319
320
        // Execute query and get response
321 12
        $response = $this->write($query)->read(false);
322
323
        // if:
324
        //  - we have more than one response
325
        //  - response is '!done'
326
        // => problem with legacy version, swap it and retry
327
        // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
328 12
        if ($legacyRetry && $this->isLegacy($response)) {
329 1
            $this->_config->set('legacy', true);
330 1
            return $this->login();
331
        }
332
333
        // Return true if we have only one line from server and this line is !done
334 12
        return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done');
335
    }
336
337
    /**
338
     * Detect by login request if firmware is legacy
339
     *
340
     * @param array $response
341
     * @return bool
342
     * @throws ConfigException
343
     */
344 11
    private function isLegacy(array &$response): bool
345
    {
346 11
        return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
347
    }
348
349
    /**
350
     * Connect to socket server
351
     *
352
     * @return bool
353
     * @throws \RouterOS\Exceptions\ClientException
354
     * @throws \RouterOS\Exceptions\ConfigException
355
     * @throws \RouterOS\Exceptions\QueryException
356
     */
357 13
    private function connect(): bool
358
    {
359
        // By default we not connected
360 13
        $connected = false;
361
362
        // Few attempts in loop
363 13
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
364
365
            // Initiate socket session
366 13
            $this->openSocket();
367
368
            // If socket is active
369 12
            if (null !== $this->getSocket()) {
370 12
                $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
371
                // If we logged in then exit from loop
372 12
                if (true === $this->login()) {
373 11
                    $connected = true;
374 11
                    break;
375
                }
376
377
                // Else close socket and start from begin
378 1
                $this->closeSocket();
379
            }
380
381
            // Sleep some time between tries
382 1
            sleep($this->config('delay'));
383
        }
384
385
        // Return status of connection
386 12
        return $connected;
387
    }
388
}
389