Completed
Push — master ( c080af...5d8bf0 )
by Mr
03:42
created

Client::wr()   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 2
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
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
     * Socket resource
22
     *
23
     * @var resource|null
24
     */
25
    private $_socket;
26
27
    /**
28
     * Code of error
29
     *
30
     * @var int
31
     */
32
    private $_socket_err_num;
33
34
    /**
35
     * Description of socket error
36
     *
37
     * @var string
38
     */
39
    private $_socket_err_str;
40
41
    /**
42
     * Configuration of connection
43
     *
44
     * @var \RouterOS\Config
45
     */
46
    private $_config;
47
48
    /**
49
     * Client constructor.
50
     *
51
     * @param   array|\RouterOS\Config $config
52
     * @throws  \RouterOS\Exceptions\ClientException
53
     * @throws  \RouterOS\Exceptions\ConfigException
54
     * @throws  \RouterOS\Exceptions\QueryException
55
     */
56 13
    public function __construct($config)
57
    {
58
        // If array then need create object
59 13
        if (\is_array($config)) {
60 3
            $config = new Config($config);
61
        }
62
63
        // Check for important keys
64 13
        if (true !== $key = ArrayHelper::checkIfKeysNotExist(['host', 'user', 'pass'], $config->getParameters())) {
65 1
            throw new ConfigException("One or few parameters '$key' of Config is not set or empty");
66
        }
67
68
        // Save config if everything is okay
69 12
        $this->setConfig($config);
70
71
        // Throw error if cannot to connect
72 12
        if (false === $this->connect()) {
73 1
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
74
        }
75 10
    }
76
77
    /**
78
     * Get some parameter from config
79
     *
80
     * @param   string $parameter Name of required parameter
81
     * @return  mixed
82
     * @throws  \RouterOS\Exceptions\ConfigException
83
     */
84 12
    private function config(string $parameter)
85
    {
86 12
        return $this->_config->get($parameter);
87
    }
88
89
    /**
90
     * Return socket resource if is exist
91
     *
92
     * @return  \RouterOS\Config
93
     * @since   0.6
94
     */
95 1
    public function getConfig(): Config
96
    {
97 1
        return $this->_config;
98
    }
99
100
    /**
101
     * Set configuration of client
102
     *
103
     * @param   \RouterOS\Config $config
104
     * @since   0.7
105
     */
106 12
    public function setConfig(Config $config)
107
    {
108 12
        $this->_config = $config;
109 12
    }
110
111
    /**
112
     * Encode given length in RouterOS format
113
     *
114
     * @param   string $string
115
     * @return  string Encoded length
116
     * @throws  \RouterOS\Exceptions\ClientException
117
     */
118 11
    private function encodeLength(string $string): string
119
    {
120 11
        $length = \strlen($string);
121
122 11
        if ($length < 128) {
123 11
            $orig_length = $length;
124 11
            $offset      = -1;
125
        } elseif ($length < 16384) {
126
            $orig_length = $length | 0x8000;
127
            $offset      = -2;
128
        } elseif ($length < 2097152) {
129
            $orig_length = $length | 0xC00000;
130
            $offset      = -3;
131
        } elseif ($length < 268435456) {
132
            $orig_length = $length | 0xE0000000;
133
            $offset      = -4;
134
        } else {
135
            throw new ClientException("Unable to encode length of '$string'");
136
        }
137
138
        // Pack string to binary format
139 11
        $result = pack('I*', $orig_length);
140
        // Parse binary string to array
141 11
        $result = str_split($result);
142
        // Reverse array
143 11
        $result = array_reverse($result);
144
        // Extract values from offset to end of array
145 11
        $result = \array_slice($result, $offset);
146
147
        // Sew items into one line
148 11
        $output = null;
149 11
        foreach ($result as $item) {
150 11
            $output .= $item;
151
        }
152
153 11
        return $output;
154
    }
155
156
    /**
157
     * Read length of line
158
     *
159
     * @param   int $byte
160
     * @return  int
161
     */
162 11
    private function getLength(int $byte): int
163
    {
164
        // If the first bit is set then we need to remove the first four bits, shift left 8
165
        // and then read another byte in.
166
        // We repeat this for the second and third bits.
167
        // If the fourth bit is set, we need to remove anything left in the first byte
168
        // and then read in yet another byte.
169 11
        if ($byte & 128) {
170
            if (($byte & 192) === 128) {
171
                $length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1));
172
            } elseif (($byte & 224) === 192) {
173
                $length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1));
174
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
175
            } elseif (($byte & 240) === 224) {
176
                $length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1));
177
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
178
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
179
            } else {
180
                $length = \ord(fread($this->_socket, 1));
181
                $length = ($length << 8) + \ord(fread($this->_socket, 1)) * 3;
182
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
183
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
184
            }
185
        } else {
186 11
            $length = $byte;
187
        }
188 11
        return $length;
189
    }
190
191
    /**
192
     * Send write query to RouterOS (with or without tag)
193
     *
194
     * @param   string|array|\RouterOS\Query $query
195
     * @return  \RouterOS\Client
196
     * @throws  \RouterOS\Exceptions\ClientException
197
     * @throws  \RouterOS\Exceptions\QueryException
198
     */
199 11
    public function write($query): Client
200
    {
201 11
        if (\is_string($query)) {
202 2
            $query = new Query($query);
203 11
        } elseif (\is_array($query)) {
204 1
            $endpoint = array_shift($query);
205 1
            $query    = new Query($endpoint, $query);
206
        }
207
208 11
        if (!$query instanceof Query) {
209 1
            throw new QueryException('Parameters cannot be processed');
210
        }
211
212
        // Send commands via loop to router
213 11
        foreach ($query->getQuery() as $command) {
214 11
            $command = trim($command);
215 11
            fwrite($this->_socket, $this->encodeLength($command) . $command);
216
        }
217
218
        // Write zero-terminator
219 11
        fwrite($this->_socket, \chr(0));
220
221 11
        return $this;
222
    }
223
224
    /**
225
     * Read answer from server after query was executed
226
     *
227
     * @param   bool $parse
228
     * @return  array
229
     */
230 11
    public function read(bool $parse = true): array
231
    {
232
        // By default response is empty
233 11
        $response = [];
234
235
        // Read answer from socket in loop
236 11
        while (true) {
237
            // Read the first byte of input which gives us some or all of the length
238
            // of the remaining reply.
239 11
            $byte   = fread($this->_socket, 1);
240 11
            $length = $this->getLength(\ord($byte));
241
242
            // Save only non empty strings
243 11
            if ($length > 0) {
244
                // Save output line to response array
245 11
                $response[] = stream_get_contents($this->_socket, $length);
246
            }
247
248
            // If we get a !done line in response, change state of $isDone variable
249 11
            $isDone = ('!done' === end($response));
250
251
            // Get status about latest operation
252 11
            $status = stream_get_meta_data($this->_socket);
253
254
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
255 11
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $isDone)) {
256 11
                break;
257
            }
258
        }
259
260
        // Parse results and return
261 11
        return $parse ? $this->parseResponse($response) : $response;
262
    }
263
264
    /**
265
     * Alias for ->write() method
266
     *
267
     * @param   string|array|\RouterOS\Query $query
268
     * @return  \RouterOS\Client
269
     * @throws  \RouterOS\Exceptions\ClientException
270
     * @throws  \RouterOS\Exceptions\QueryException
271
     */
272 1
    public function w($query): Client
273
    {
274 1
        return $this->write($query);
275
    }
276
277
    /**
278
     * Alias for ->read() method
279
     *
280
     * @param   bool $parse
281
     * @return  array
282
     * @since   0.7
283
     */
284 1
    public function r(bool $parse = true): array
285
    {
286 1
        return $this->read($parse);
287
    }
288
289
    /**
290
     * Alias for ->write()->read() combination of methods
291
     *
292
     * @param   string|array|\RouterOS\Query $query
293
     * @param   bool                         $parse
294
     * @return  array
295
     * @throws  \RouterOS\Exceptions\ClientException
296
     * @throws  \RouterOS\Exceptions\QueryException
297
     * @since   0.6
298
     */
299 4
    public function wr($query, bool $parse = true): array
300
    {
301 4
        return $this->write($query)->read($parse);
302
    }
303
304
    /**
305
     * Parse response from Router OS
306
     *
307
     * @param   array $response Response data
308
     * @return  array Array with parsed data
309
     */
310 3
    private function parseResponse(array $response): array
311
    {
312 3
        $result = [];
313 3
        $i      = -1;
314 3
        $lines  = \count($response);
315 3
        foreach ($response as $key => $value) {
316 3
            switch ($value) {
317 3
                case '!re':
318 1
                    $i++;
319 1
                    break;
320 3
                case '!fatal':
321 1
                    $result = $response;
322 1
                    break 2;
323 2
                case '!trap':
324 2
                case '!done':
325
                    // Check for =ret=, .tag and any other following messages
326 2
                    for ($j = $key + 1; $j <= $lines; $j++) {
327
                        // If we have lines after current one
328 2
                        if (isset($response[$j])) {
329 1
                            $this->pregResponse($response[$j], $matches);
330 1
                            if (!empty($matches)) {
331 1
                                $result['after'][$matches[1][0]] = $matches[2][0];
332
                            }
333
                        }
334
                    }
335 2
                    break 2;
336
                default:
337 1
                    $this->pregResponse($value, $matches);
338 1
                    if (!empty($matches)) {
339 1
                        $result[$i][$matches[1][0]] = $matches[2][0];
340
                    }
341 1
                    break;
342
            }
343
        }
344 3
        return $result;
345
    }
346
347
    /**
348
     * Parse result from RouterOS by regular expression
349
     *
350
     * @param   string $value
351
     * @param   array  $matches
352
     */
353 2
    private function pregResponse(string $value, &$matches)
354
    {
355 2
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
356 2
    }
357
358
    /**
359
     * Authorization logic
360
     *
361
     * @return  bool
362
     * @throws  \RouterOS\Exceptions\ClientException
363
     * @throws  \RouterOS\Exceptions\ConfigException
364
     * @throws  \RouterOS\Exceptions\QueryException
365
     */
366 11
    private function login(): bool
367
    {
368
        // If legacy login scheme is enabled
369 11
        if ($this->config('legacy')) {
370
            // For the first we need get hash with salt
371 1
            $query    = new Query('/login');
372 1
            $response = $this->write($query)->read();
373
374
            // Now need use this hash for authorization
375 1
            $query = (new Query('/login'))
376 1
                ->add('=name=' . $this->config('user'))
377 1
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])));
378
        } else {
379
            // Just login with our credentials
380 10
            $query = (new Query('/login'))
381 10
                ->add('=name=' . $this->config('user'))
382 10
                ->add('=password=' . $this->config('pass'));
383
        }
384
385
        // Execute query and get response
386 11
        $response = $this->write($query)->read(false);
387
388
        // Return true if we have only one line from server and this line is !done
389 11
        return isset($response[0]) && $response[0] === '!done';
390
    }
391
392
    /**
393
     * Connect to socket server
394
     *
395
     * @return  bool
396
     * @throws  \RouterOS\Exceptions\ClientException
397
     * @throws  \RouterOS\Exceptions\ConfigException
398
     * @throws  \RouterOS\Exceptions\QueryException
399
     */
400 12
    private function connect(): bool
401
    {
402
        // By default we not connected
403 12
        $connected = false;
404
405
        // Few attempts in loop
406 12
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
407
408
            // Initiate socket session
409 12
            $this->openSocket();
410
411
            // If socket is active
412 11
            if (null !== $this->getSocket()) {
413
414
                // If we logged in then exit from loop
415 11
                if (true === $this->login()) {
416 10
                    $connected = true;
417 10
                    break;
418
                }
419
420
                // Else close socket and start from begin
421 1
                $this->closeSocket();
422
            }
423
424
            // Sleep some time between tries
425 1
            sleep($this->config('delay'));
426
        }
427
428
        // Return status of connection
429 11
        return $connected;
430
    }
431
432
}
433