Completed
Pull Request — master (#8)
by Mr
03:44
created

Client::parseResponse()   B

Complexity

Conditions 10
Paths 5

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 24
cts 24
cp 1
rs 7.6666
c 0
b 0
f 0
cc 10
nc 5
nop 1
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
10
/**
11
 * Class Client for RouterOS management
12
 *
13
 * @package RouterOS
14
 * @since   0.1
15
 */
16
class Client implements Interfaces\ClientInterface
17
{
18
    use SocketTrait;
19
20
    /**
21
     * Configuration of connection
22
     *
23
     * @var \RouterOS\Config
24
     */
25
    private $_config;
26
27
    /**
28
     * API communication object
29
     *
30
     * @var \RouterOS\APIConnector
31
     */
32
33
    private $_connector;
34
35
    /**
36
     * Client constructor.
37
     *
38
     * @param   array|\RouterOS\Config $config
39
     * @throws  \RouterOS\Exceptions\ClientException
40
     * @throws  \RouterOS\Exceptions\ConfigException
41
     * @throws  \RouterOS\Exceptions\QueryException
42
     */
43 14
    public function __construct($config)
44
    {
45
        // If array then need create object
46 14
        if (\is_array($config)) {
47 3
            $config = new Config($config);
48
        }
49
50
        // Check for important keys
51 14
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
52 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
53
        }
54
55
        // Save config if everything is okay
56 13
        $this->setConfig($config);
57
58
        // Throw error if cannot to connect
59 13
        if (false === $this->connect()) {
60 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
61
        }
62 11
    }
63
64
    /**
65
     * Get some parameter from config
66
     *
67
     * @param   string $parameter Name of required parameter
68
     * @return  mixed
69
     * @throws  \RouterOS\Exceptions\ConfigException
70
     */
71 13
    private function config(string $parameter)
72
    {
73 13
        return $this->_config->get($parameter);
74
    }
75
76
    /**
77
     * Return socket resource if is exist
78
     *
79
     * @return  \RouterOS\Config
80
     * @since   0.6
81
     */
82 1
    public function getConfig(): Config
83
    {
84 1
        return $this->_config;
85
    }
86
87
    /**
88
     * Set configuration of client
89
     *
90
     * @param   \RouterOS\Config $config
91
     * @since   0.7
92
     */
93 13
    public function setConfig(Config $config)
94
    {
95 13
        $this->_config = $config;
96 13
    }
97
98
    /**
99
     * Send write query to RouterOS (with or without tag)
100
     *
101
     * @param   string|array|\RouterOS\Query $query
102
     * @return  \RouterOS\Client
103
     * @throws  \RouterOS\Exceptions\QueryException
104
     */
105 12
    public function write($query): Client
106
    {
107 12
        if (\is_string($query)) {
108 2
            $query = new Query($query);
109 12
        } elseif (\is_array($query)) {
110 1
            $endpoint = array_shift($query);
111 1
            $query    = new Query($endpoint, $query);
112
        }
113
114 12
        if (!$query instanceof Query) {
115 1
            throw new QueryException('Parameters cannot be processed');
116
        }
117
118
        // Send commands via loop to router
119 12
        foreach ($query->getQuery() as $command) {
120 12
            $this->_connector->writeWord(trim($command));
121
        }
122
123
        // Write zero-terminator (empty string)
124 12
        $this->_connector->writeWord('');
125
126 12
        return $this;
127
    }
128
129
    /**
130
     * Read answer from server after query was executed
131
     *
132
     * A Mikrotik reply is formed of blocks
133
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
134
     * Each block end with an zero byte (empty line)
135
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
136
     * A !fatal block precedes TCP connexion close
137
     *
138
     * @param   bool $parse
139
     * @return  array
140
     */
141 12
    public function read(bool $parse = true): array
142
    {
143
        // By default response is empty
144 12
        $response = [];
145
        // We have to wait a !done or !fatal 
146 12
        $lastReply = false;
147
148
        // Read answer from socket in loop
149 12
        while (true) {
150 12
            $word = $this->_connector->readWord();
151
152 12
            if ('' === $word) {
153 12
                if ($lastReply) {
154
                    // We received a !done or !fatal message in a precedent loop
155
                    // response is complete
156 12
                    break;
157
                }
158
                // We did not receive the !done or !fatal message
159
                // This 0 length message is the end of a reply !re or !trap
160
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
161 4
                continue;
162
            }
163
164
            // Save output line to response array
165 12
            $response[] = $word;
166
167
            // If we get a !done or !fatal line in response, we are now ready to finish the read
168
            // but we need to wait a 0 length message, switch the flag
169 12
            if ('!done' === $word || '!fatal' === $word) {
170 12
                $lastReply = true;
171
            }
172
        }
173
174
        // Parse results and return
175 12
        return $parse ? $this->parseResponse($response) : $response;
176
    }
177
178
    /**
179
     * Alias for ->write() method
180
     *
181
     * @param   string|array|\RouterOS\Query $query
182
     * @return  \RouterOS\Client
183
     * @throws  \RouterOS\Exceptions\QueryException
184
     */
185 1
    public function w($query): Client
186
    {
187 1
        return $this->write($query);
188
    }
189
190
    /**
191
     * Alias for ->read() method
192
     *
193
     * @param   bool $parse
194
     * @return  array
195
     * @since   0.7
196
     */
197 1
    public function r(bool $parse = true): array
198
    {
199 1
        return $this->read($parse);
200
    }
201
202
    /**
203
     * Alias for ->write()->read() combination of methods
204
     *
205
     * @param   string|array|\RouterOS\Query $query
206
     * @param   bool                         $parse
207
     * @return  array
208
     * @throws  \RouterOS\Exceptions\ClientException
209
     * @throws  \RouterOS\Exceptions\QueryException
210
     * @since   0.6
211
     */
212 4
    public function wr($query, bool $parse = true): array
213
    {
214 4
        return $this->write($query)->read($parse);
215
    }
216
217
    /**
218
     * Parse response from Router OS
219
     *
220
     * @param   array $response Response data
221
     * @return  array Array with parsed data
222
     */
223 4
    private function parseResponse(array $response): array
224
    {
225 4
        $result = [];
226 4
        $i      = -1;
227 4
        $lines  = \count($response);
228 4
        foreach ($response as $key => $value) {
229
            switch ($value) {
230 4
                case '!re':
231 1
                    $i++;
232 1
                    break;
233 4
                case '!fatal':
234 1
                    $result = $response;
235 1
                    break 2;
236 3
                case '!trap':
237 3
                case '!done':
238
                    // Check for =ret=, .tag and any other following messages
239 3
                    for ($j = $key + 1; $j <= $lines; $j++) {
240
                        // If we have lines after current one
241 3
                        if (isset($response[$j])) {
242 2
                            $this->pregResponse($response[$j], $matches);
243 2
                            if (!empty($matches)) {
244 2
                                $result['after'][$matches[1][0]] = $matches[2][0];
245
                            }
246
                        }
247
                    }
248 3
                    break 2;
249
                default:
250 1
                    $this->pregResponse($value, $matches);
251 1
                    if (!empty($matches)) {
252 1
                        $result[$i][$matches[1][0]] = $matches[2][0];
253
                    }
254 1
                    break;
255
            }
256
        }
257 4
        return $result;
258
    }
259
260
    /**
261
     * Parse result from RouterOS by regular expression
262
     *
263
     * @param   string $value
264
     * @param   array  $matches
265
     */
266 3
    private function pregResponse(string $value, &$matches)
267
    {
268 3
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
269 3
    }
270
271
    /**
272
     * Authorization logic
273
     *
274
     * @param   bool $legacyRetry Retry login if we detect legacy version of RouterOS
275
     * @return  bool
276
     * @throws  \RouterOS\Exceptions\ClientException
277
     * @throws  \RouterOS\Exceptions\ConfigException
278
     * @throws  \RouterOS\Exceptions\QueryException
279
     */
280 12
    private function login(bool $legacyRetry = false): bool
281
    {
282
        // If legacy login scheme is enabled
283 12
        if ($this->config('legacy')) {
284
            // For the first we need get hash with salt
285 2
            $query    = new Query('/login');
286 2
            $response = $this->write($query)->read();
287
288
            // Now need use this hash for authorization
289 2
            $query = (new Query('/login'))
290 2
                ->add('=name=' . $this->config('user'))
291 2
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])));
292
        } else {
293
            // Just login with our credentials
294 11
            $query = (new Query('/login'))
295 11
                ->add('=name=' . $this->config('user'))
296 11
                ->add('=password=' . $this->config('pass'));
297
298
            // If we set modern auth scheme but router with legacy firmware then need to retry query,
299
            // but need to prevent endless loop
300 11
            $legacyRetry = true;
301
        }
302
303
        // Execute query and get response
304 12
        $response = $this->write($query)->read(false);
305
306
        // if:
307
        //  - we have more than one response
308
        //  - response is '!done'
309
        // => problem with legacy version, swap it and retry
310
        // Only tested with ROS pre 6.43, will test with post 6.43 => this could make legacy parameter obsolete?
311 12
        if ($legacyRetry && $this->isLegacy($response)) {
312 1
            $this->_config->set('legacy', true);
313 1
            return $this->login();
314
        }
315
316
        // Return true if we have only one line from server and this line is !done
317 12
        return (1 === count($response)) && isset($response[0]) && ($response[0] === '!done');
318
    }
319
320
    /**
321
     * Detect by login request if firmware is legacy
322
     *
323
     * @param   array $response
324
     * @return  bool
325
     * @throws  ConfigException
326
     */
327 11
    private function isLegacy(array &$response): bool
328
    {
329 11
        return \count($response) > 1 && $response[0] === '!done' && !$this->config('legacy');
330
    }
331
332
    /**
333
     * Connect to socket server
334
     *
335
     * @return  bool
336
     * @throws  \RouterOS\Exceptions\ClientException
337
     * @throws  \RouterOS\Exceptions\ConfigException
338
     * @throws  \RouterOS\Exceptions\QueryException
339
     */
340 13
    private function connect(): bool
341
    {
342
        // By default we not connected
343 13
        $connected = false;
344
345
        // Few attempts in loop
346 13
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
347
348
            // Initiate socket session
349 13
            $this->openSocket();
350
351
            // If socket is active
352 12
            if (null !== $this->getSocket()) {
353 12
                $this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
354
                // If we logged in then exit from loop
355 12
                if (true === $this->login()) {
356 11
                    $connected = true;
357 11
                    break;
358
                }
359
360
                // Else close socket and start from begin
361 1
                $this->closeSocket();
362
            }
363
364
            // Sleep some time between tries
365 1
            sleep($this->config('delay'));
366
        }
367
368
        // Return status of connection
369 12
        return $connected;
370
    }
371
372
}
373