Completed
Push — master ( 72d645...a1ccb9 )
by Mr
03:56 queued 41s
created

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