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): |
|
|
|
|
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): |
|
|
|
|
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; |
|
|
|
|
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 |
|
|
|
|
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; |
|
|
|
|
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
|
|
|
|
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.