Completed
Push — master ( 55cea2...4dcdcf )
by Mr
04:08
created

Client   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 453
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 80%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 4
dl 0
loc 453
ccs 120
cts 150
cp 0.8
rs 6.4799
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 3
A exceptionIfKeyNotExist() 0 9 4
A config() 0 4 1
A getConfig() 0 4 1
B encodeLength() 0 37 6
A getLength() 0 32 5
A wr() 0 4 1
A write() 0 13 2
B read() 0 36 7
B parseResponse() 0 36 10
A pregResponse() 0 4 1
A login() 0 25 3
A connect() 0 31 4
A setSocket() 0 4 1
A getSocket() 0 4 1
A openSocket() 0 32 3
A closeSocket() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

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