Passed
Push — master ( 55de2c...13daca )
by Mr
02:58
created

Client::pregResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
namespace RouterOS;
4
5
use RouterOS\Exceptions\ClientException;
6
use RouterOS\Exceptions\ConfigException;
7
use RouterOS\Exceptions\Exception;
8
use RouterOS\Interfaces\ClientInterface;
9
use RouterOS\Interfaces\ConfigInterface;
10
use RouterOS\Interfaces\QueryInterface;
11
12
/**
13
 * Class Client for RouterOS management
14
 * @package RouterOS
15
 * @since 0.1
16
 */
17
class Client implements Interfaces\ClientInterface
18
{
19
    /**
20
     * Socket resource
21
     * @var resource|null
22
     */
23
    private $_socket;
24
25
    /**
26
     * Code of error
27
     * @var int
28
     */
29
    private $_socket_err_num;
30
31
    /**
32
     * Description of socket error
33
     * @var string
34
     */
35
    private $_socket_err_str;
36
37
    /**
38
     * Configuration of connection
39
     * @var ConfigInterface
40
     */
41
    private $_config;
42
43
    /**
44
     * Client constructor.
45
     *
46
     * @param   ConfigInterface $config
47
     * @throws  ConfigException
48
     * @throws  ClientException
49
     */
50
    public function __construct(ConfigInterface $config)
51
    {
52
        // Check for important keys
53
        $this->keysCheck(['host', 'user', 'pass'], $config);
54
55
        // Save config if everything is okay
56
        $this->_config = $config;
57
58
        // Throw error if cannot to connect
59
        if (false === $this->connect()) {
60
            throw new ClientException('Unable to connect to ' . $config->get('host') . ':' . $config->get('port'));
61
        }
62
    }
63
64
    /**
65
     * Check for important keys
66
     *
67
     * @param   array $keys
68
     * @param   ConfigInterface $config
69
     * @throws  ConfigException
70
     */
71
    private function keysCheck(array $keys, ConfigInterface $config)
72
    {
73
        $parameters = $config->getParameters();
74
        foreach ($keys as $key) {
75
            if (false === (array_key_exists($key, $parameters) && isset($parameters[$key]))) {
76
                throw new ConfigException("Parameter '$key' of Config is not set or empty");
77
            }
78
        }
79
    }
80
81
    /**
82
     * Get some parameter from config
83
     *
84
     * @param   string $parameter
85
     * @return  mixed
86
     */
87
    private function config(string $parameter)
88
    {
89
        return $this->_config->get($parameter);
90
    }
91
92
    /**
93
     * Convert ordinary string to hex string
94
     *
95
     * @param   string $string
96
     * @return  string
97
     */
98
    private function encodeLength(string $string): string
99
    {
100
        // Yeah, that's insane, but was more ugly, you need read this post if you interesting a details:
101
        // https://wiki.mikrotik.com/wiki/Manual:API#API_words
102
        switch (true) {
103
            case ($string < 0x80):
104
                $string = \chr($string);
105
                break;
106
            case ($string < 0x4000):
107
                $string |= 0x8000;
108
                $string = \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
109
                break;
110
            case ($string < 0x200000):
111
                $string |= 0xC00000;
112
                $string = \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
113
                break;
114 View Code Duplication
            case ($string < 0x10000000):
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...
115
                $string |= 0xE0000000;
116
                $string = \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
117
                break;
118 View Code Duplication
            case ($string >= 0x10000000):
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...
119
                $string = \chr(0xF0) . \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
120
                break;
121
        }
122
123
        return $string;
124
    }
125
126
    /**
127
     * Read length of line
128
     *
129
     * @param   int $byte
130
     * @return  int
131
     */
132
    private function getLength(int $byte): int
133
    {
134
        // If the first bit is set then we need to remove the first four bits, shift left 8
135
        // and then read another byte in.
136
        // We repeat this for the second and third bits.
137
        // If the fourth bit is set, we need to remove anything left in the first byte
138
        // and then read in yet another byte.
139
        $length = 0;
0 ignored issues
show
Unused Code introduced by
$length is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
140
        if ($byte & 128) {
141
            if (($byte & 192) === 128) {
142
                $length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1));
143
            } else {
144
                if (($byte & 224) === 192) {
145
                    $length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1));
146
                    $length = ($length << 8) + \ord(fread($this->_socket, 1));
147
                } else {
148
                    if (($byte & 240) === 224) {
149
                        $length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1));
150
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
151
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
152
                    } else {
153
                        $length = \ord(fread($this->_socket, 1));
154
                        $length = ($length << 8) + \ord(fread($this->_socket, 1)) * 3;
155
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
156
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
157
                    }
158
                }
159
            }
160
        } else {
161
            $length = $byte;
162
        }
163
        return $length;
164
    }
165
166
    /**
167
     * Send write query to RouterOS (with or without tag)
168
     *
169
     * @param   QueryInterface $query
170
     * @return  ClientInterface
171
     */
172
    public function write(QueryInterface $query): ClientInterface
173
    {
174
        // Send commands via loop to router
175
        foreach ($query->getQuery() as $command) {
176
            $command = trim($command);
177
            fwrite($this->_socket, $this->encodeLength(\strlen($command)) . $command);
178
        }
179
180
        // Write zero-terminator
181
        fwrite($this->_socket, \chr(0));
182
183
        return $this;
184
    }
185
186
//    public function read2(bool $parse = true): array
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
187
//    {
188
//        while (true) {
189
//
190
//            $res = '';
191
//            while ($buf = fread($this->_socket, 1)) {
192
//                if (substr($res, -5) === '!done') {
193
//                    echo 'done';
194
//                    break 2;
195
//                }
196
//                echo "$buf\n";
197
//                $res .= $buf;
198
//            }
199
//            $result[] = $res;
200
//        }
201
//        print_r($result);
202
//        die();
203
//    }
204
205
    /**
206
     * Read answer from server after query was executed
207
     *
208
     * @param   bool $parse
209
     * @return  array
210
     */
211
    public function read(bool $parse = true): array
212
    {
213
        // By default response is empty
214
        $response = [];
215
216
        // Read answer from socket in loop
217
        while (true) {
218
            // Read the first byte of input which gives us some or all of the length
219
            // of the remaining reply.
220
            $byte = \ord(fread($this->_socket, 1));
221
222
            // Read length of line
223
            $length = $this->getLength($byte);
224
225
            // Save only non empty strings
226
            if ($length > 0) {
227
                // Save output line to response array
228
                $response[] = stream_get_contents($this->_socket, $length);
229
            } else {
230
                // Read next line
231
                stream_get_contents($this->_socket, $length);
232
            }
233
234
            // If we get a !done line in response, change state of $isDone variable
235
            $isDone = ('!done' === end($response));
236
237
            // Get status about latest operation
238
            $status = stream_get_meta_data($this->_socket);
239
240
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
241
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $isDone)) {
242
                break;
243
            }
244
        }
245
246
        // Parse results and return
247
        return $parse ? $this->parseResponse($response) : $response;
248
    }
249
250
    /**
251
     * Parse response from Router OS
252
     *
253
     * @param   array $response Response data
254
     * @return  array Array with parsed data
255
     */
256
    private function parseResponse(array $response): array
257
    {
258
        $result = [];
259
        $i = -1;
260
        $lines = \count($response);
261
        foreach ($response as $key => $value) {
262
            switch ($value) {
263
                case '!re':
264
                    $i++;
265
                    break;
266
                case '!fatal':
267
                case '!trap':
268
                case '!done':
269
                    // Check for =ret=, .tag and any other following messages
270
                    for ($j = $key + 1; $j <= $lines; $j++) {
271
                        // If we have lines after current one
272
                        if (isset($response[$j])) {
273
                            $this->pregResponse($response[$j], $matches);
274
                            if (!empty($matches)) {
275
                                $result['after'][$matches[1][0]] = $matches[2][0];
276
                            }
277
                        }
278
                    }
279
                    break 2;
280
                default:
281
                    $this->pregResponse($value, $matches);
282
                    if (!empty($matches)) {
283
                        $result[$i][$matches[1][0]] = $matches[2][0];
284
                    }
285
                    break;
286
            }
287
        }
288
        return $result;
289
    }
290
291
    private function pregResponse(string $value, &$matches)
292
    {
293
        preg_match_all('/^[=|\.](.*)=(.*)/', $value, $matches);
294
    }
295
296
    /**
297
     * Authorization logic
298
     *
299
     * @return  bool
300
     */
301
    private function login(): bool
302
    {
303
        // If legacy login scheme is enabled
304
        if ($this->config('legacy')) {
305
            // For the first we need get hash with salt
306
            $query = new Query('/login');
307
            $response = $this->write($query)->read();
308
309
            // Now need use this hash for authorization
310
            $query = (new Query('/login'))
311
                ->add('=name=' . $this->config('user'))
312
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response['after']['ret'])));
313
        } else {
314
            // Just login with our credentials
315
            $query = (new Query('/login'))
316
                ->add('=name=' . $this->config('user'))
317
                ->add('=password=' . $this->config('pass'));
318
        }
319
320
        // Execute query and get response
321
        $response = $this->write($query)->read(false);
322
323
        // Return true if we have only one line from server and this line is !done
324
        return isset($response[0]) && $response[0] === '!done';
325
    }
326
327
    /**
328
     * Connect to socket server
329
     *
330
     * @return  bool
331
     * @throws  ClientException
332
     */
333
    public function connect(): bool
334
    {
335
        // By default we not connected
336
        $connected = false;
337
338
        // Few attempts in loop
339
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
340
341
            // Initiate socket session
342
            $this->openSocket();
343
344
            // If socket is active
345
            if ($this->getSocket()) {
346
347
                // If we logged in then exit from loop
348
                if (true === $this->login()) {
349
                    $connected = true;
350
                    break;
351
                }
352
353
                // Else close socket and start from begin
354
                $this->closeSocket();
355
            }
356
357
            // Sleep some time between tries
358
            sleep($this->config('delay'));
359
        }
360
361
        // Return status of connection
362
        return $connected;
363
    }
364
365
    /**
366
     * Save socket resource to static variable
367
     *
368
     * @param   resource|null $socket
369
     * @return  bool
370
     */
371
    private function setSocket($socket): bool
372
    {
373
        if (\is_resource($socket)) {
374
            $this->_socket = $socket;
375
            return true;
376
        }
377
        return false;
378
    }
379
380
    /**
381
     * Return socket resource if is exist
382
     *
383
     * @return  bool|resource
384
     */
385
    public function getSocket()
386
    {
387
        return \is_resource($this->_socket)
388
            ? $this->_socket
389
            : false;
390
    }
391
392
    /**
393
     * Initiate socket session
394
     *
395
     * @return  bool
396
     * @throws  ClientException
397
     */
398
    private function openSocket(): bool
399
    {
400
        // Default: Context for ssl
401
        $context = stream_context_create([
402
            'ssl' => [
403
                'ciphers' => 'ADH:ALL',
404
                'verify_peer' => false,
405
                'verify_peer_name' => false
406
            ]
407
        ]);
408
409
        // Default: Proto tcp:// but for ssl we need ssl://
410
        $proto = $this->config('ssl') ? 'ssl://' : '';
411
412
        // Initiate socket client
413
        $socket = stream_socket_client(
414
            $proto . $this->config('host') . ':' . $this->config('port'),
415
            $this->_socket_err_num,
416
            $this->_socket_err_str,
417
            $this->config('timeout'),
418
            STREAM_CLIENT_CONNECT,
419
            $context
420
        );
421
422
        // Throw error is socket is not initiated
423
        if (false === $socket) {
424
            throw new ClientException('stream_socket_client() failed: code: ' . $this->_socket_err_num . ' reason: ' . $this->_socket_err_str);
425
        }
426
427
        // Save socket to static variable
428
        return $this->setSocket($socket);
429
    }
430
431
    /**
432
     * Close socket session
433
     *
434
     * @return bool
435
     */
436
    private function closeSocket(): bool
437
    {
438
        return fclose($this->_socket);
439
    }
440
}
441