Completed
Push — master ( 07aad8...237a74 )
by Mr
09:22
created

Client::getConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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->_config = $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
     * Send write query to RouterOS (with or without tag)
79
     *
80
     * @param string|array|\RouterOS\Query $query
81
     * @return \RouterOS\Interfaces\ClientInterface
82
     * @throws \RouterOS\Exceptions\QueryException
83 1
     */
84
    public function write($query): self
85 1
    {
86
        if (\is_string($query)) {
87
            $query = new Query($query);
88
        } elseif (\is_array($query)) {
89
            $endpoint = array_shift($query);
90
            $query    = new Query($endpoint, $query);
91
        }
92
93
        if (!$query instanceof Query) {
94 13
            throw new QueryException('Parameters cannot be processed');
95
        }
96 13
97 13
        // Send commands via loop to router
98
        foreach ($query->getQuery() as $command) {
99
            $this->_connector->writeWord(trim($command));
100
        }
101
102
        // Write zero-terminator (empty string)
103
        $this->_connector->writeWord('');
104
105
        return $this;
106 12
    }
107
108 12
    /**
109 2
     * Read answer from server after query was executed
110 12
     *
111 1
     * A Mikrotik reply is formed of blocks
112 1
     * Each block starts with a word, one of ('!re', '!trap', '!done', '!fatal')
113
     * Each block end with an zero byte (empty line)
114
     * Reply ends with a complete !done or !fatal block (ended with 'empty line')
115 12
     * A !fatal block precedes TCP connexion close
116 1
     *
117
     * @param bool $parse
118
     * @return mixed
119
     */
120 12
    public function read(bool $parse = true)
121 12
    {
122
        // By default response is empty
123
        $response = [];
124
        // We have to wait a !done or !fatal
125 12
        $lastReply = false;
126
127 12
        // Read answer from socket in loop
128
        while (true) {
129
            $word = $this->_connector->readWord();
130
131
            if ('' === $word) {
132
                if ($lastReply) {
133
                    // We received a !done or !fatal message in a precedent loop
134
                    // response is complete
135
                    break;
136
                }
137
                // We did not receive the !done or !fatal message
138
                // This 0 length message is the end of a reply !re or !trap
139
                // We have to wait the router to send a !done or !fatal reply followed by optionals values and a 0 length message
140
                continue;
141
            }
142 12
143
            // Save output line to response array
144
            $response[] = $word;
145 12
146
            // If we get a !done or !fatal line in response, we are now ready to finish the read
147 12
            // but we need to wait a 0 length message, switch the flag
148
            if ('!done' === $word || '!fatal' === $word) {
149
                $lastReply = true;
150 12
            }
151 12
        }
152
153 12
        // Parse results and return
154 12
        return $parse ? $this->rosario($response) : $response;
155
    }
156
157 12
    /**
158
     * Read using Iterators to improve performance on large dataset
159
     *
160
     * @return \RouterOS\ResponseIterator
161
     */
162 4
    public function readAsIterator(): ResponseIterator
163
    {
164
        return new ResponseIterator($this);
165
    }
166 12
167
    /**
168
     * This method was created by memory save reasons, it convert response
169
     * from RouterOS to readable array in safe way.
170 12
     *
171 12
     * @param array $raw Array RAW response from server
172
     * @return mixed
173
     *
174
     * Based on RouterOSResponseArray solution by @arily
175
     *
176 12
     * @link    https://github.com/arily/RouterOSResponseArray
177
     * @since   1.0.0
178
     */
179
    private function rosario(array $raw): array
180
    {
181
        // This RAW should't be an error
182
        $positions = array_keys($raw, '!re');
183
        $count     = count($raw);
184
        $result    = [];
185
186
        if (isset($positions[1])) {
187
188
            foreach ($positions as $key => $position) {
189
                // Get length of future block
190
                $length = isset($positions[$key + 1])
191
                    ? $positions[$key + 1] - $position + 1
192
                    : $count - $position;
193
194
                // Convert array to simple items
195
                $item = [];
196
                for ($i = 1; $i < $length; $i++) {
197
                    $item[] = array_shift($raw);
198
                }
199
200
                // Save as result
201 4
                $result[] = $this->parseResponse($item)[0];
202
            }
203
204 4
        } else {
205 4
            $result = $this->parseResponse($raw);
206 4
        }
207
208 4
        return $result;
209
    }
210
211
    /**
212
     * Parse response from Router OS
213
     *
214
     * @param array $response Response data
215
     * @return array Array with parsed data
216
     */
217
    public function parseResponse(array $response): array
218
    {
219
        $result = [];
220
        $i      = -1;
221
        $lines  = \count($response);
222
        foreach ($response as $key => $value) {
223
            switch ($value) {
224
                case '!re':
225
                    $i++;
226
                    break;
227 4
                case '!fatal':
228
                    $result = $response;
229
                    break 2;
230 4
                case '!trap':
231
                case '!done':
232
                    // Check for =ret=, .tag and any other following messages
233
                    for ($j = $key + 1; $j <= $lines; $j++) {
234
                        // If we have lines after current one
235
                        if (isset($response[$j])) {
236
                            $this->pregResponse($response[$j], $matches);
237 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...
238
                                $result['after'][$matches[1][0]] = $matches[2][0];
239 4
                            }
240
                        }
241 4
                    }
242 4
                    break 2;
243 4
                default:
244 4
                    $this->pregResponse($value, $matches);
245 4 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...
246 4
                        $result[$i][$matches[1][0]] = $matches[2][0];
247 1
                    }
248 1
                    break;
249 4
            }
250 1
        }
251 1
        return $result;
252 3
    }
253 3
254
    /**
255 3
     * Parse result from RouterOS by regular expression
256
     *
257 3
     * @param string $value
258 2
     * @param array  $matches
259 2
     */
260 2
    private function pregResponse(string $value, &$matches)
261
    {
262
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
263
    }
264 3
265
    /**
266 1
     * Authorization logic
267 1
     *
268 1
     * @param bool $legacyRetry Retry login if we detect legacy version of RouterOS
269
     * @return bool
270 1
     * @throws \RouterOS\Exceptions\ClientException
271
     * @throws \RouterOS\Exceptions\ConfigException
272
     * @throws \RouterOS\Exceptions\QueryException
273 4
     */
274
    private function login(bool $legacyRetry = false): bool
275
    {
276
        // If legacy login scheme is enabled
277
        if ($this->config('legacy')) {
278
            // For the first we need get hash with salt
279
            $response = $this->write('/login')->read();
0 ignored issues
show
Bug introduced by
The call to read() misses a required argument $parse.

This check looks for function calls that miss required arguments.

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