Completed
Push — master ( 293aef...20e4ee )
by Mr
01:13
created

Client::config()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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)
79
                    . \chr($string & 0xFF);
80
                break;
81
            case ($string < 0x200000):
82
                $string |= 0xC00000;
83
                $string = \chr(($string >> 16) & 0xFF)
84
                    . \chr(($string >> 8) & 0xFF)
85
                    . \chr($string & 0xFF);
86
                break;
87 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...
88
                $string |= 0xE0000000;
89
                $string = \chr(($string >> 24) & 0xFF)
90
                    . \chr(($string >> 16) & 0xFF)
91
                    . \chr(($string >> 8) & 0xFF)
92
                    . \chr($string & 0xFF);
93
                break;
94 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...
Coding Style introduced by
As per coding-style, case should be followed by a single space.

As per the PSR-2 coding standard, there must be a space after the case keyword, instead of the test immediately following it.

switch (true) {
    case!isset($a):  //wrong
        doSomething();
        break;
    case !isset($b):  //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

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