Completed
Push — master ( 797114...47ec8a )
by Mr
01:16
created

Client::parseResponse()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.6026
c 0
b 0
f 0
cc 7
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 ConfigInterface
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;
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
     * Read length of line
98
     *
99
     * @param   int $byte
100
     * @return  int
101
     */
102
    private function getLength(int $byte): int
103
    {
104
        // If the first bit is set then we need to remove the first four bits, shift left 8
105
        // and then read another byte in.
106
        // We repeat this for the second and third bits.
107
        // If the fourth bit is set, we need to remove anything left in the first byte
108
        // and then read in yet another byte.
109
        $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...
110
        if ($byte & 128) {
111
            if (($byte & 192) === 128) {
112
                $length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1));
113
            } else {
114
                if (($byte & 224) === 192) {
115
                    $length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1));
116
                    $length = ($length << 8) + \ord(fread($this->_socket, 1));
117
                } else {
118
                    if (($byte & 240) === 224) {
119
                        $length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1));
120
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
121
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
122
                    } else {
123
                        $length = \ord(fread($this->_socket, 1));
124
                        $length = ($length << 8) + \ord(fread($this->_socket, 1)) * 3;
125
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
126
                        $length = ($length << 8) + \ord(fread($this->_socket, 1));
127
                    }
128
                }
129
            }
130
        } else {
131
            $length = $byte;
132
        }
133
        return $length;
134
    }
135
136
    /**
137
     * Send write query to RouterOS (with or without tag)
138
     *
139
     * @param   QueryInterface $query
140
     * @return  ClientInterface
141
     */
142
    public function write(QueryInterface $query): ClientInterface
143
    {
144
        // Send commands via loop to router
145
        foreach ($query->getQuery() as $command) {
146
            $command = trim($command);
147
            fwrite($this->_socket, $this->encodeLength(\strlen($command)) . $command);
148
        }
149
150
        // Write zero-terminator
151
        fwrite($this->_socket, \chr(0));
152
153
        return $this;
154
    }
155
156
    public function read2(bool $parse = true): array
0 ignored issues
show
Unused Code introduced by
The parameter $parse is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
157
    {
158
        while (true) {
159
160
            $res = '';
161
            while ($buf = fread($this->_socket, 1)) {
162
                if (substr($res, -5) === '!done') {
163
                    echo 'done';
164
                    break 2;
165
                }
166
                echo "$buf\n";
167
                $res .= $buf;
168
            }
169
            $result[] = $res;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
170
        }
171
        print_r($result);
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
172
        die();
173
    }
174
175
    /**
176
     * Read answer from server after query was executed
177
     *
178
     * @param   bool $parse
179
     * @return  array
180
     */
181
    public function read(bool $parse = true): array
182
    {
183
        // By default response is empty
184
        $response = [];
185
186
        // Read answer from socket in loop
187
        while (true) {
188
            // Read the first byte of input which gives us some or all of the length
189
            // of the remaining reply.
190
            $byte = \ord(fread($this->_socket, 1));
191
192
            // Read length of line
193
            $length = $this->getLength($byte);
194
195
            // Save output line to response array
196
            $response[] = stream_get_contents($this->_socket, $length);
197
198
            // If we get a !done line in response, change state of $isDone variable
199
            $isDone = ('!done' === end($response));
200
201
            // Get status about latest operation
202
            $status = stream_get_meta_data($this->_socket);
203
204
            // If we do not have unread bytes from socket or <-same and if done, then exit from loop
205
            if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $isDone)) {
206
                break;
207
            }
208
        }
209
210
        // Parse results and return
211
        return $parse ? $this->parseResponse($response) : $response;
212
    }
213
214
    /**
215
     * Parse response from Router OS
216
     *
217
     * @param   array $response Response data
218
     * @return  array Array with parsed data
219
     */
220
    private function parseResponse(array $response): array
221
    {
222
        print_r($response);
223
224
        $result = [];
225
        $i = -1;
226
        foreach ($response as $value) {
227
            switch ($value) {
228
                case '!re':
229
                    $i++;
230
                    break;
231
                case '!fatal':
232
                case '!trap':
233
                case '!done':
234
                    break 2;
235
                default:
236
                    if (preg_match_all('/^=(.*)=(.*)/', $value, $matches)) {
237
                        $result[$i][$matches[1][0]] = $matches[2][0];
238
                    }
239
                    break;
240
            }
241
        }
242
        return $result;
243
    }
244
245
    /**
246
     * Authorization logic
247
     *
248
     * @return  bool
249
     */
250
    private function login(): bool
251
    {
252
        // If legacy login scheme is enabled
253
        if ($this->config('legacy')) {
254
            // For the first we need get hash with salt
255
            $query = new Query('/login');
256
            $response = $this->write($query)->read();
257
258
            // Now need use this hash for authorization
259
            $query = (new Query('/login'))
260
                ->add('=name=' . $this->config('user'))
261
                ->add('=response=00' . md5(\chr(0) . $this->config('pass') . pack('H*', $response[0])));
262
        } else {
263
            // Just login with our credentials
264
            $query = (new Query('/login'))
265
                ->add('=name=' . $this->config('user'))
266
                ->add('=password=' . $this->config('pass'));
267
        }
268
269
        // Execute query and get response
270
        $response = $this->write($query)->read(false);
271
272
        // Return true if we have only one line from server and this line is !done
273
        return isset($response[0]) && $response[0] === '!done';
274
    }
275
276
    /**
277
     * Connect to socket server
278
     *
279
     * @return  bool
280
     */
281
    public function connect(): bool
282
    {
283
        // Few attempts in loop
284
        for ($attempt = 1; $attempt <= $this->config('attempts'); $attempt++) {
285
286
            // Initiate socket session
287
            $this->openSocket();
288
289
            // If socket is active
290
            if ($this->getSocket()) {
291
292
                // If we logged in then exit from loop
293
                if (true === $this->login()) {
294
                    break;
295
                }
296
297
                // Else close socket and start from begin
298
                $this->closeSocket();
299
            }
300
301
            // Sleep some time between tries
302
            sleep($this->config('delay'));
303
        }
304
305
        // Return status of connection
306
        return true;
307
    }
308
309
    /**
310
     * Save socket resource to static variable
311
     *
312
     * @param   resource|null $socket
313
     * @return  bool
314
     */
315
    private function setSocket($socket): bool
316
    {
317
        if (\is_resource($socket)) {
318
            $this->_socket = $socket;
319
            return true;
320
        }
321
        return false;
322
    }
323
324
    /**
325
     * Return socket resource if is exist
326
     *
327
     * @return  bool|resource
328
     */
329
    public function getSocket()
330
    {
331
        return \is_resource($this->_socket)
332
            ? $this->_socket
333
            : false;
334
    }
335
336
    /**
337
     * Initiate socket session
338
     *
339
     * @return  bool
340
     */
341
    private function openSocket(): bool
342
    {
343
        // Connect to server
344
        $socket = false;
345
346
        // Default: Context for ssl
347
        $context = stream_context_create([
348
            'ssl' => [
349
                'ciphers' => 'ADH:ALL',
350
                'verify_peer' => false,
351
                'verify_peer_name' => false
352
            ]
353
        ]);
354
355
        // Default: Proto tcp:// but for ssl we need ssl://
356
        $proto = $this->config('ssl') ? 'ssl://' : '';
357
358
        try {
359
            // Initiate socket client
360
            $socket = stream_socket_client(
361
                $proto . $this->config('host') . ':' . $this->config('port'),
362
                $this->_socket_err_num,
363
                $this->_socket_err_str,
364
                $this->config('timeout'),
365
                STREAM_CLIENT_CONNECT,
366
                $context
367
            );
368
            // Throw error is socket is not initiated
369
            if (false === $socket) {
370
                throw new Exception('stream_socket_client() failed: code: ' . $this->_socket_err_num . ' reason:' . $this->_socket_err_str);
371
            }
372
373
        } catch (Exception $e) {
374
            // __construct
375
        }
376
377
        // Save socket to static variable
378
        return $this->setSocket($socket);
379
    }
380
381
    /**
382
     * Close socket session
383
     *
384
     * @return bool
385
     */
386
    private function closeSocket(): bool
387
    {
388
        return fclose($this->_socket);
389
    }
390
}
391