Test Failed
Push — master ( aa1a69...bd3168 )
by Mr
03:21
created

Client::keysCheck()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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