Completed
Push — master ( bc728c...55b5f6 )
by Mr
01:19
created

Client::getLength()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 1
1
<?php
2
3
namespace RouterOS;
4
5
use RouterOS\Exceptions\Exception;
6
use RouterOS\Interfaces\ClientInterface;
7
use RouterOS\Interfaces\ConfigInterface;
8
use RouterOS\Interfaces\QueryInterface;
9
10
/**
11
 * Class Client
12
 * @package RouterOS
13
 * @since 0.1
14
 */
15
class Client implements Interfaces\ClientInterface
16
{
17
    /**
18
     * Socket resource
19
     * @var resource|null
20
     */
21
    private $_socket;
22
23
    /**
24
     * Code of error
25
     * @var int
26
     */
27
    private $_socket_err_num;
28
29
    /**
30
     * Description of socket error
31
     * @var string
32
     */
33
    private $_socket_err_str;
34
35
    /**
36
     * Configuration of connection
37
     * @var Config
38
     */
39
    private $_config;
40
41
    /**
42
     * Client constructor.
43
     * @param   ConfigInterface $config
44
     */
45
    public function __construct(ConfigInterface $config)
46
    {
47
        $this->_config = $config;
0 ignored issues
show
Documentation Bug introduced by
$config is of type object<RouterOS\Interfaces\ConfigInterface>, but the property $_config was declared to be of type object<RouterOS\Config>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
48
        $this->connect();
49
    }
50
51
    /**
52
     * Get some parameter from config
53
     *
54
     * @param   string $parameter
55
     * @return  mixed
56
     */
57
    private function config(string $parameter)
58
    {
59
        return $this->_config->get($parameter);
60
    }
61
62
    /**
63
     * Convert ordinary string to hex string
64
     *
65
     * @param   string $string
66
     * @return  string
67
     */
68
    private function encodeLength(string $string): string
69
    {
70
        // Yeah, that's insane, but was more ugly, you need read this post if you interesting a details:
71
        // https://wiki.mikrotik.com/wiki/Manual:API#API_words
72
        switch (true) {
73
            case ($string < 0x80):
74
                $string = \chr($string);
75
                break;
76
            case ($string < 0x4000):
77
                $string |= 0x8000;
78
                $string = \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
79
                break;
80
            case ($string < 0x200000):
81
                $string |= 0xC00000;
82
                $string = \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
83
                break;
84 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...
85
                $string |= 0xE0000000;
86
                $string = \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
87
                break;
88 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...
89
                $string = \chr(0xF0) . \chr(($string >> 24) & 0xFF) . \chr(($string >> 16) & 0xFF) . \chr(($string >> 8) & 0xFF) . \chr($string & 0xFF);
90
                break;
91
        }
92
93
        return $string;
94
    }
95
96
    /**
97
     * Send write query to RouterOS (with or without tag)
98
     *
99
     * @param   QueryInterface $query
100
     * @return  ClientInterface
101
     */
102
    public function write(QueryInterface $query): ClientInterface
103
    {
104
        // Send commands via loop to router
105
        foreach ($query->getQuery() as $command) {
106
            $command = trim($command);
107
            fwrite($this->_socket, $this->encodeLength(\strlen($command)) . $command);
108
        }
109
110
        // Write zero-terminator
111
        fwrite($this->_socket, \chr(0));
112
113
        return $this;
114
    }
115
116
    /**
117
     * Read length of line
118
     *
119
     * @param   int $byte
120
     * @return  int
121
     */
122
    private function getLength(int $byte): int
123
    {
124
        // If the first bit is set then we need to remove the first four bits, shift
125
        // left 8 and then read another byte in.
126
        //
127
        // We repeat this for the second and third bits.
128
        //
129
        // If the fourth bit is set, we need to remove anything left in the first byte
130
        // and then read in yet another byte.
131
        switch (true) {
132
            case ($byte & 128) && (($byte & 192) === 128):
133
                $length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1));
134
                break;
135
            case ($byte & 128) && (($byte & 224) === 192):
136
                $length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1));
137
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
138
                break;
139
            case ($byte & 128) && (($byte & 240) === 224):
140
                $length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1));
141
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
142
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
143
                break;
144
            case ($byte & 128) && (($byte & 256) === 240):
145
                $length = \ord(fread($this->_socket, 1));
146
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
147
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
148
                $length = ($length << 8) + \ord(fread($this->_socket, 1));
149
                break;
150
            default:
151
                $length = $byte;
152
        }
153
154
        return $length;
155
    }
156
157
    /**
158
     * Read answer from server after query was executed
159
     *
160
     * @param   bool $parse
161
     * @return  array
162
     */
163
    public function read(bool $parse = true): array
164
    {
165
        // By default response is empty
166
        $response = [];
167
168
        // Read answer from socket in loop
169
        while (true) {
170
            // Read the first byte of input which gives us some or all of the length
171
            // of the remaining reply.
172
            $byte = \ord(fread($this->_socket, 1));
173
174
            // Read length of line
175
            $length = $this->getLength($byte);
176
177
            // By default line is empty
178
            $line = '';
179
180
            // If we have got more characters to read, read them in.
181
            if ($length > 0) {
182
                $line = '';
183
                $retLen = 0;
184
                while ($retLen < $length) {
185
                    $toRead = $length - $retLen;
186
                    $line .= fread($this->_socket, $toRead);
187
                    $retLen = \strlen($line);
188
                }
189
                $response[] = $line;
190
            }
191
192
            // If we get a !done, change state of done variable
193
            $done = ($line === '!done');
194
195
            // Get status about latest operation
196
            $status = stream_get_meta_data($this->_socket);
197
198
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
199
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $done)) {
200
                break;
201
            }
202
        }
203
204
        // Parse results and return
205
        return $parse ? $this->parseResponse($response) : $response;
206
    }
207
208
    /**
209
     * Parse response from Router OS
210
     *
211
     * @param   array $response Response data
212
     * @return  array Array with parsed data
213
     */
214
    private function parseResponse(array $response): array
215
    {
216
        $parsed = [];
217
        $current = null;
218
        $single = null;
219
        foreach ($response as $x) {
220
            if (\in_array($x, ['!fatal', '!re', '!trap'])) {
221
                if ($x === '!re') {
222
                    $current =& $parsed[];
223
                } else {
224
                    $current =& $parsed[$x][];
225
                }
226
            } elseif ($x !== '!done') {
227
                $matches = [];
228
                if (preg_match_all('/[^=]+/', $x, $matches)) {
229
                    if ($matches[0][0] === 'ret') {
230
                        $single = $matches[0][1];
231
                    }
232
                    $current[$matches[0][0]] = $matches[0][1] ?? '';
233
                }
234
            }
235
        }
236
237
        if (empty($parsed) && null !== $single) {
238
            $parsed[] = $single;
239
        }
240
241
        return $parsed;
242
    }
243
244
    /**
245
     * Authorization logic
246
     *
247
     * @return  bool
248
     */
249
    private function login(): bool
250
    {
251
        // If legacy login scheme is enabled
252
        if ($this->config('legacy')) {
253
            // For the first we need get hash with salt
254
            $query = new Query('/login');
255
            $response = $this->write($query)->read();
256
257
            // Now need use this hash for authorization
258
            $query = (new Query('/login'))
259
                ->add('=name=' . $this->config('user'))
260
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response[0])));
261
        } else {
262
            // Just login with our credentials
263
            $query = (new Query('/login'))
264
                ->add('=name=' . $this->config('user'))
265
                ->add('=password=' . $this->config('pass'));
266
        }
267
268
        // Execute query and get response
269
        $response = $this->write($query)->read(false);
270
271
        // Return true if we have only one line from server and this line is !done
272
        return isset($response[0]) && $response[0] === '!done';
273
    }
274
275
    /**
276
     * Connect to socket server
277
     *
278
     * @return  bool
279
     */
280
    public function connect(): bool
281
    {
282
        // Few attempts in loop
283
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
284
285
            // Initiate socket session
286
            $this->openSocket();
287
288
            // If socket is active
289
            if ($this->getSocket()) {
290
291
                // If we logged in then exit from loop
292
                if (true === $this->login()) {
293
                    break;
294
                }
295
296
                // Else close socket and start from begin
297
                $this->closeSocket();
298
            }
299
300
            // Sleep some time between tries
301
            sleep($this->config('delay'));
302
        }
303
304
        // Return status of connection
305
        return true;
306
    }
307
308
    /**
309
     * Save socket resource to static variable
310
     *
311
     * @param   resource|null $socket
312
     * @return  bool
313
     */
314
    private function setSocket($socket): bool
315
    {
316
        if (\is_resource($socket)) {
317
            $this->_socket = $socket;
318
            return true;
319
        }
320
        return false;
321
    }
322
323
    /**
324
     * Return socket resource if is exist
325
     *
326
     * @return  bool|resource
327
     */
328
    public function getSocket()
329
    {
330
        return \is_resource($this->_socket)
331
            ? $this->_socket
332
            : false;
333
    }
334
335
    /**
336
     * Initiate socket session
337
     *
338
     * @return  bool
339
     */
340
    private function openSocket(): bool
341
    {
342
        // Connect to server
343
        $socket = false;
344
345
        // Default: Context for ssl
346
        $context = stream_context_create([
347
            'ssl' => [
348
                'ciphers' => 'ADH:ALL',
349
                'verify_peer' => false,
350
                'verify_peer_name' => false
351
            ]
352
        ]);
353
354
        // Default: Proto tcp:// but for ssl we need ssl://
355
        $proto = $this->config('ssl') ? 'ssl://' : '';
356
357
        try {
358
            // Initiate socket client
359
            $socket = stream_socket_client(
360
                $proto . $this->config('host') . ':' . $this->config('port'),
361
                $this->_socket_err_num,
362
                $this->_socket_err_str,
363
                $this->config('timeout'),
364
                STREAM_CLIENT_CONNECT,
365
                $context
366
            );
367
            // Throw error is socket is not initiated
368
            if (false === $socket) {
369
                throw new Exception('stream_socket_client() failed: code: ' . $this->_socket_err_num . ' reason:' . $this->_socket_err_str);
370
            }
371
372
        } catch (Exception $e) {
373
            // __construct
374
        }
375
376
        // Save socket to static variable
377
        return $this->setSocket($socket);
378
    }
379
380
    /**
381
     * Close socket session
382
     *
383
     * @return bool
384
     */
385
    private function closeSocket(): bool
386
    {
387
        fclose($this->_socket);
388
        return true;
389
    }
390
}
391