Completed
Push — master ( 52e6bb...f12d15 )
by Mr
02:17
created

Client::read2()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 0
cts 12
cp 0
rs 9.6666
c 0
b 0
f 0
cc 4
nc 2
nop 1
crap 20
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 output line to response array
226
            $response[] = stream_get_contents($this->_socket, $length);
227
228
            // If we get a !done line in response, change state of $isDone variable
229
            $isDone = ('!done' === end($response));
230
231
            // Get status about latest operation
232
            $status = stream_get_meta_data($this->_socket);
233
234
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
235
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $isDone)) {
236
                break;
237
            }
238
        }
239
240
        // Parse results and return
241
        return $parse ? $this->parseResponse($response) : $response;
242
    }
243
244
    /**
245
     * Parse response from Router OS
246
     *
247
     * @param   array $response Response data
248
     * @return  array Array with parsed data
249
     */
250
    private function parseResponse(array $response): array
251
    {
252
        $result = [];
253
        $i = -1;
254
        foreach ($response as $value) {
255
            switch ($value) {
256
                case '!re':
257
                    $i++;
258
                    break;
259
                case '!fatal':
260
                case '!trap':
261
                case '!done':
262
                    break 2;
263
                default:
264
                    if (preg_match_all('/^=(.*)=(.*)/', $value, $matches)) {
265
                        $result[$i][$matches[1][0]] = $matches[2][0];
266
                    }
267
                    break;
268
            }
269
        }
270
        return $result;
271
    }
272
273
    /**
274
     * Authorization logic
275
     *
276
     * @return  bool
277
     */
278
    private function login(): bool
279
    {
280
        // If legacy login scheme is enabled
281
        if ($this->config('legacy')) {
282
            // For the first we need get hash with salt
283
            $query = new Query('/login');
284
            $response = $this->write($query)->read(false);
285
286
            // Now need use this hash for authorization
287
            $query = (new Query('/login'))
288
                ->add('=name=' . $this->config('user'))
289
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response[1])));
290
        } else {
291
            // Just login with our credentials
292
            $query = (new Query('/login'))
293
                ->add('=name=' . $this->config('user'))
294
                ->add('=password=' . $this->config('pass'));
295
        }
296
297
        // Execute query and get response
298
        $response = $this->write($query)->read(false);
299
300
        // Return true if we have only one line from server and this line is !done
301
        return isset($response[0]) && $response[0] === '!done';
302
    }
303
304
    /**
305
     * Connect to socket server
306
     *
307
     * @return  bool
308
     */
309
    public function connect(): bool
310
    {
311
        // By default we not connected
312
        $connected = false;
313
314
        // Few attempts in loop
315
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
316
317
            // Initiate socket session
318
            $this->openSocket();
319
320
            // If socket is active
321
            if ($this->getSocket()) {
322
323
                // If we logged in then exit from loop
324
                if (true === $this->login()) {
325
                    $connected = true;
326
                    break;
327
                }
328
329
                // Else close socket and start from begin
330
                $this->closeSocket();
331
            }
332
333
            // Sleep some time between tries
334
            sleep($this->config('delay'));
335
        }
336
337
        // Return status of connection
338
        return $connected;
339
    }
340
341
    /**
342
     * Save socket resource to static variable
343
     *
344
     * @param   resource|null $socket
345
     * @return  bool
346
     */
347
    private function setSocket($socket): bool
348
    {
349
        if (\is_resource($socket)) {
350
            $this->_socket = $socket;
351
            return true;
352
        }
353
        return false;
354
    }
355
356
    /**
357
     * Return socket resource if is exist
358
     *
359
     * @return  bool|resource
360
     */
361
    public function getSocket()
362
    {
363
        return \is_resource($this->_socket)
364
            ? $this->_socket
365
            : false;
366
    }
367
368
    /**
369
     * Initiate socket session
370
     *
371
     * @return  bool
372
     * @throws  ClientException
373
     */
374
    private function openSocket(): bool
375
    {
376
        // Connect to server
377
        $socket = false;
0 ignored issues
show
Unused Code introduced by
$socket 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...
378
379
        // Default: Context for ssl
380
        $context = stream_context_create([
381
            'ssl' => [
382
                'ciphers' => 'ADH:ALL',
383
                'verify_peer' => false,
384
                'verify_peer_name' => false
385
            ]
386
        ]);
387
388
        // Default: Proto tcp:// but for ssl we need ssl://
389
        $proto = $this->config('ssl') ? 'ssl://' : '';
390
391
        // Initiate socket client
392
        $socket = stream_socket_client(
393
            $proto . $this->config('host') . ':' . $this->config('port'),
394
            $this->_socket_err_num,
395
            $this->_socket_err_str,
396
            $this->config('timeout'),
397
            STREAM_CLIENT_CONNECT,
398
            $context
399
        );
400
401
        // Throw error is socket is not initiated
402
        if (false === $socket) {
403
            throw new ClientException('stream_socket_client() failed: code: ' . $this->_socket_err_num . ' reason: ' . $this->_socket_err_str);
404
        }
405
406
        // Save socket to static variable
407
        return $this->setSocket($socket);
408
    }
409
410
    /**
411
     * Close socket session
412
     *
413
     * @return bool
414
     */
415
    private function closeSocket(): bool
416
    {
417
        return fclose($this->_socket);
418
    }
419
}
420