This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Client.php |
||
4 | * |
||
5 | * @copyright More in license.md |
||
6 | * @license https://www.ipublikuj.eu |
||
7 | * @author Adam Kadlec <[email protected]> |
||
8 | * @package iPublikuj:WebSocketsWAMPClient! |
||
9 | * @subpackage Client |
||
10 | * @since 1.0.0 |
||
11 | * |
||
12 | * @date 11.05.18 |
||
13 | */ |
||
14 | |||
15 | declare(strict_types = 1); |
||
16 | |||
17 | namespace IPub\WebSocketsWAMPClient\Client; |
||
18 | |||
19 | use Nette; |
||
20 | use Nette\Utils; |
||
21 | |||
22 | use React\EventLoop; |
||
23 | use React\Stream; |
||
24 | |||
25 | use IPub\WebSocketsWAMPClient\Exceptions; |
||
26 | |||
27 | /** |
||
28 | * @method void onConnect() |
||
29 | * @method void onOpen(array $data) |
||
30 | * @method void onEvent(string $topic, string $event) |
||
31 | */ |
||
32 | 1 | class Client implements IClient |
|
33 | { |
||
34 | /** |
||
35 | * Implement nette smart magic |
||
36 | */ |
||
37 | 1 | use Nette\SmartObject; |
|
38 | |||
39 | const VERSION = '0.1.0'; |
||
40 | |||
41 | const TYPE_ID_WELCOME = 0; |
||
42 | const TYPE_ID_PREFIX = 1; |
||
43 | const TYPE_ID_CALL = 2; |
||
44 | const TYPE_ID_CALL_RESULT = 3; |
||
45 | const TYPE_ID_CALL_ERROR = 4; |
||
46 | const TYPE_ID_SUBSCRIBE = 5; |
||
47 | const TYPE_ID_UNSUBSCRIBE = 6; |
||
48 | const TYPE_ID_PUBLISH = 7; |
||
49 | const TYPE_ID_EVENT = 8; |
||
50 | |||
51 | /** |
||
52 | * @var \Closure |
||
53 | */ |
||
54 | public $onConnect = []; |
||
55 | |||
56 | /** |
||
57 | * @var \Closure |
||
58 | */ |
||
59 | public $onOpen = []; |
||
60 | |||
61 | /** |
||
62 | * @var \Closure |
||
63 | */ |
||
64 | public $onEvent = []; |
||
65 | |||
66 | /** |
||
67 | * @var EventLoop\LoopInterface |
||
68 | */ |
||
69 | private $loop; |
||
70 | |||
71 | /** |
||
72 | * @var Configuration |
||
73 | */ |
||
74 | private $configuration; |
||
75 | |||
76 | /** |
||
77 | * @var Stream\DuplexResourceStream |
||
78 | */ |
||
79 | private $stream; |
||
80 | |||
81 | /** |
||
82 | * @var bool |
||
83 | */ |
||
84 | private $connected = FALSE; |
||
85 | |||
86 | /** |
||
87 | * @var callable[] |
||
88 | */ |
||
89 | private $sucessCallbacks = []; |
||
90 | |||
91 | /** |
||
92 | * @var callable[] |
||
93 | */ |
||
94 | private $errorCallbacks = []; |
||
95 | |||
96 | /** |
||
97 | * @param EventLoop\LoopInterface $loop |
||
98 | * @param Configuration $configuration |
||
99 | */ |
||
100 | function __construct(EventLoop\LoopInterface $loop, Configuration $configuration) |
||
0 ignored issues
–
show
|
|||
101 | { |
||
102 | 1 | $this->loop = $loop; |
|
103 | 1 | $this->configuration = $configuration; |
|
104 | 1 | } |
|
105 | |||
106 | /** |
||
107 | * Disconnect on destruct |
||
108 | */ |
||
109 | function __destruct() |
||
0 ignored issues
–
show
|
|||
110 | { |
||
111 | 1 | $this->disconnect(); |
|
112 | 1 | } |
|
113 | |||
114 | /** |
||
115 | * {@inheritdoc} |
||
116 | */ |
||
117 | public function setLoop(EventLoop\LoopInterface $loop) : void |
||
118 | { |
||
119 | $this->loop = $loop; |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * {@inheritdoc} |
||
124 | */ |
||
125 | public function getLoop() : EventLoop\LoopInterface |
||
126 | { |
||
127 | return $this->loop; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * {@inheritdoc} |
||
132 | */ |
||
133 | public function connect() : void |
||
134 | { |
||
135 | $resource = @stream_socket_client('tcp://' . $this->configuration->getHost() . ':' . $this->configuration->getPort()); |
||
136 | |||
137 | if (!$resource) { |
||
138 | throw new Exceptions\ConnectionException('Opening socket failed.'); |
||
139 | } |
||
140 | |||
141 | $this->stream = new Stream\DuplexResourceStream($resource, $this->getLoop()); |
||
142 | |||
143 | $this->stream->on('data', function ($chunk) : void { |
||
144 | $data = $this->parseChunk($chunk); |
||
145 | |||
146 | $this->parseData($data); |
||
147 | }); |
||
148 | |||
149 | $this->stream->on('close', function () : void { |
||
150 | // When connection is closed, stop loop & end running script |
||
151 | $this->loop->stop(); |
||
152 | }); |
||
153 | |||
154 | $this->stream->write($this->createHeader()); |
||
155 | |||
156 | $this->onConnect(); |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * {@inheritdoc} |
||
161 | */ |
||
162 | public function disconnect() : void |
||
163 | { |
||
164 | 1 | $this->connected = FALSE; |
|
165 | |||
166 | 1 | if ($this->stream instanceof Stream\DuplexStreamInterface) { |
|
0 ignored issues
–
show
The class
React\Stream\DuplexStreamInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?
This error could be the result of: 1. Missing dependenciesPHP Analyzer uses your Are you sure this class is defined by one of your dependencies, or did you maybe
not list a dependency in either the 2. Missing use statementPHP does not complain about undefined classes in if ($x instanceof DoesNotExist) {
// Do something.
}
If you have not tested against this specific condition, such errors might go unnoticed.
Loading history...
|
|||
167 | $this->stream->close(); |
||
168 | } |
||
169 | 1 | } |
|
170 | |||
171 | /** |
||
172 | * {@inheritdoc} |
||
173 | */ |
||
174 | public function isConnected() : bool |
||
175 | { |
||
176 | return $this->connected; |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * {@inheritdoc} |
||
181 | */ |
||
182 | public function publish(string $topicUri, string $event, array $exclude = [], array $eligible = []) : void |
||
183 | { |
||
184 | $this->sendData([ |
||
185 | self::TYPE_ID_PUBLISH, |
||
186 | $topicUri, |
||
187 | $event, |
||
188 | $exclude, |
||
189 | $eligible |
||
190 | ]); |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * {@inheritdoc} |
||
195 | */ |
||
196 | public function subscribe(string $topicUri) : void |
||
197 | { |
||
198 | $this->sendData([ |
||
199 | self::TYPE_ID_SUBSCRIBE, |
||
200 | $topicUri |
||
201 | ]); |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * {@inheritdoc} |
||
206 | */ |
||
207 | public function unsubscribe(string $topicUri) : void |
||
208 | { |
||
209 | $this->sendData([ |
||
210 | self::TYPE_ID_UNSUBSCRIBE, |
||
211 | $topicUri |
||
212 | ]); |
||
213 | } |
||
214 | |||
215 | /** |
||
216 | * {@inheritdoc} |
||
217 | */ |
||
218 | public function call(string $processUri, array $args, callable $successCallback = NULL, callable $errorCallback = NULL) : void |
||
219 | { |
||
220 | $callId = $this->generateAlphaNumToken(16); |
||
221 | |||
222 | $this->sucessCallbacks[$callId] = $successCallback; |
||
223 | $this->errorCallbacks[$callId] = $errorCallback; |
||
224 | |||
225 | $data = [ |
||
226 | self::TYPE_ID_CALL, |
||
227 | $callId, |
||
228 | $processUri, |
||
229 | $args, |
||
230 | ]; |
||
231 | |||
232 | $this->sendData($data); |
||
233 | } |
||
234 | |||
235 | /** |
||
236 | * @param mixed $data |
||
237 | * @param array $header |
||
238 | * |
||
239 | * @return void |
||
240 | */ |
||
241 | private function receiveData($data, array $header) : void |
||
242 | { |
||
243 | if (!$this->isConnected()) { |
||
244 | $this->disconnect(); |
||
245 | |||
246 | return; |
||
247 | } |
||
248 | |||
249 | if (isset($data[0])) { |
||
250 | switch ($data[0]) { |
||
251 | case self::TYPE_ID_WELCOME: |
||
252 | $this->onOpen($data); |
||
253 | break; |
||
254 | |||
255 | case self::TYPE_ID_CALL_RESULT: |
||
256 | if (isset($data[1])) { |
||
257 | $id = $data[1]; |
||
258 | |||
259 | if (isset($this->sucessCallbacks[$id])) { |
||
260 | $callback = $this->sucessCallbacks[$id]; |
||
261 | $callback( |
||
262 | Utils\ArrayHash::from(isset($data[2]) ? (is_array($data[2]) ? $data[2] : [$data[2]]) : []) |
||
263 | ); |
||
264 | |||
265 | unset($this->sucessCallbacks[$id]); |
||
266 | } |
||
267 | } |
||
268 | break; |
||
269 | |||
270 | case self::TYPE_ID_CALL_ERROR: |
||
271 | if (isset($data[1])) { |
||
272 | $id = $data[1]; |
||
273 | |||
274 | if (isset($this->errorCallbacks[$id])) { |
||
275 | $callback = $this->errorCallbacks[$id]; |
||
276 | $callback( |
||
277 | // Topic |
||
278 | (isset($data[2]) ? (string) $data[2] : NULL), |
||
279 | |||
280 | // Error exception message |
||
281 | (isset($data[3]) ? (string) $data[3] : NULL), |
||
282 | |||
283 | // Additional error data |
||
284 | Utils\ArrayHash::from(isset($data[4]) ? (is_array($data[4]) ? $data[4] : [$data[4]]) : []) |
||
285 | ); |
||
286 | |||
287 | unset($this->errorCallbacks[$id]); |
||
288 | } |
||
289 | } |
||
290 | break; |
||
291 | |||
292 | case self::TYPE_ID_EVENT: |
||
293 | if (isset($data[1]) && isset($data[2])) { |
||
294 | $this->onEvent($data[1], $data[2]); |
||
295 | } |
||
296 | break; |
||
297 | } |
||
298 | } |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param array $data |
||
303 | * @param string $type |
||
304 | * @param bool $masked |
||
305 | * |
||
306 | * @return void |
||
307 | * |
||
308 | * @throws Utils\JsonException |
||
309 | */ |
||
310 | private function sendData(array $data, string $type = 'text', bool $masked = TRUE) : void |
||
311 | { |
||
312 | if (!$this->isConnected()) { |
||
313 | $this->disconnect(); |
||
314 | |||
315 | return; |
||
316 | } |
||
317 | |||
318 | $this->stream->write($this->hybi10Encode(Utils\Json::encode($data), $type, $masked)); |
||
319 | } |
||
320 | |||
321 | /** |
||
322 | * Parse received data |
||
323 | * |
||
324 | * @param array $response |
||
325 | * |
||
326 | * @return void |
||
327 | * |
||
328 | * @throws Utils\JsonException |
||
329 | */ |
||
330 | private function parseData(array $response) : void |
||
331 | { |
||
332 | if (!$this->connected && isset($response['Sec-Websocket-Accept'])) { |
||
333 | if (base64_encode(pack('H*', sha1($this->configuration->getKey() . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))) === $response['Sec-Websocket-Accept']) { |
||
334 | $this->connected = TRUE; |
||
335 | } |
||
336 | } |
||
337 | |||
338 | if ($this->connected && !empty($response['content'])) { |
||
339 | $content = str_replace("\r\n", '', trim($response['content'])); |
||
340 | |||
341 | if (preg_match('/(\[[^\]]+\])/', $content, $match)) { |
||
342 | try { |
||
343 | $parsedContent = Utils\Json::decode($match[1], Utils\Json::FORCE_ARRAY); |
||
344 | |||
345 | } catch (Utils\JsonException $ex) { |
||
0 ignored issues
–
show
The class
Nette\Utils\JsonException does not exist. Did you forget a USE statement, or did you not list all dependencies?
Scrutinizer analyzes your It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.
Loading history...
|
|||
346 | $parsedContent = NULL; |
||
347 | } |
||
348 | |||
349 | if (is_array($parsedContent)) { |
||
350 | unset($response['status']); |
||
351 | unset($response['content']); |
||
352 | |||
353 | $this->receiveData($parsedContent, $response); |
||
354 | } |
||
355 | } |
||
356 | } |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * Create header for web socket client |
||
361 | * |
||
362 | * @return string |
||
363 | */ |
||
364 | private function createHeader() : string |
||
365 | { |
||
366 | $host = $this->configuration->getHost(); |
||
367 | |||
368 | if ($host === '127.0.0.1' || $host === '0.0.0.0') { |
||
369 | $host = 'localhost'; |
||
370 | } |
||
371 | |||
372 | $origin = $this->configuration->getOrigin() ? $this->configuration->getOrigin() : 'null'; |
||
373 | |||
374 | return |
||
375 | "GET {$this->configuration->getPath()} HTTP/1.1" . "\r\n" . |
||
376 | "Origin: {$origin}" . "\r\n" . |
||
377 | "Host: {$host}:{$this->configuration->getPort()}" . "\r\n" . |
||
378 | "Sec-WebSocket-Key: {$this->configuration->getKey()}" . "\r\n" . |
||
379 | "User-Agent: IPubWebSocketClient/" . self::VERSION . "\r\n" . |
||
380 | "Upgrade: websocket" . "\r\n" . |
||
381 | "Connection: Upgrade" . "\r\n" . |
||
382 | "Sec-WebSocket-Protocol: wamp" . "\r\n" . |
||
383 | "Sec-WebSocket-Version: 13" . "\r\n" . "\r\n"; |
||
384 | } |
||
385 | |||
386 | /** |
||
387 | * Parse raw incoming data |
||
388 | * |
||
389 | * @param string $header |
||
390 | * |
||
391 | * @return mixed[] |
||
392 | */ |
||
393 | private function parseChunk(string $header) : array |
||
394 | { |
||
395 | $parsed = []; |
||
396 | |||
397 | $content = ''; |
||
398 | |||
399 | $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $header)); |
||
400 | |||
401 | foreach ($fields as $field) { |
||
402 | if (preg_match('/([^:]+): (.+)/m', $field, $match)) { |
||
403 | $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function ($matches) { |
||
404 | return strtoupper($matches[0]); |
||
405 | }, strtolower(trim($match[1]))); |
||
406 | |||
407 | if (isset($parsed[$match[1]])) { |
||
408 | $parsed[$match[1]] = [$parsed[$match[1]], $match[2]]; |
||
409 | |||
410 | } else { |
||
411 | $parsed[$match[1]] = trim($match[2]); |
||
412 | } |
||
413 | |||
414 | } elseif (preg_match('!HTTP/1\.\d (\d)* .!', $field)) { |
||
415 | $parsed['status'] = $field; |
||
416 | |||
417 | } else { |
||
418 | $content .= $field . "\r\n"; |
||
419 | } |
||
420 | } |
||
421 | |||
422 | $parsed['content'] = $content; |
||
423 | |||
424 | return $parsed; |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * Generate token |
||
429 | * |
||
430 | * @param int $length |
||
431 | * |
||
432 | * @return string |
||
433 | */ |
||
434 | private function generateAlphaNumToken(int $length) : string |
||
435 | { |
||
436 | $characters = str_split('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); |
||
437 | |||
438 | srand((int) microtime() * 1000000); |
||
439 | |||
440 | $token = ''; |
||
441 | |||
442 | do { |
||
443 | shuffle($characters); |
||
444 | $token .= $characters[mt_rand(0, (count($characters) - 1))]; |
||
445 | } while (strlen($token) < $length); |
||
446 | |||
447 | return $token; |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * @param string $payload |
||
452 | * @param string $type |
||
453 | * @param bool $masked |
||
454 | * |
||
455 | * @return bool|string |
||
456 | */ |
||
457 | private function hybi10Encode(string $payload, string $type = 'text', bool $masked = TRUE) |
||
458 | { |
||
459 | $frameHead = []; |
||
460 | |||
461 | $payloadLength = strlen($payload); |
||
462 | |||
463 | switch ($type) { |
||
464 | case 'text': |
||
465 | // First byte indicates FIN, Text-Frame (10000001): |
||
466 | $frameHead[0] = 129; |
||
467 | break; |
||
468 | |||
469 | case 'close': |
||
470 | // First byte indicates FIN, Close Frame(10001000): |
||
471 | $frameHead[0] = 136; |
||
472 | break; |
||
473 | |||
474 | case 'ping': |
||
475 | // First byte indicates FIN, Ping frame (10001001): |
||
476 | $frameHead[0] = 137; |
||
477 | break; |
||
478 | |||
479 | case 'pong': |
||
480 | // First byte indicates FIN, Pong frame (10001010): |
||
481 | $frameHead[0] = 138; |
||
482 | break; |
||
483 | } |
||
484 | |||
485 | // Set mask and payload length (using 1, 3 or 9 bytes) |
||
486 | if ($payloadLength > 65535) { |
||
487 | $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); |
||
488 | $frameHead[1] = ($masked === TRUE) ? 255 : 127; |
||
489 | |||
490 | for ($i = 0; $i < 8; $i++) { |
||
491 | $frameHead[$i + 2] = bindec($payloadLengthBin[$i]); |
||
492 | } |
||
493 | |||
494 | // Most significant bit MUST be 0 (close connection if frame too big) |
||
495 | if ($frameHead[2] > 127) { |
||
496 | $this->close(1004); |
||
497 | |||
498 | return FALSE; |
||
499 | } |
||
500 | |||
501 | } elseif ($payloadLength > 125) { |
||
502 | $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); |
||
503 | |||
504 | $frameHead[1] = ($masked === TRUE) ? 254 : 126; |
||
505 | $frameHead[2] = bindec($payloadLengthBin[0]); |
||
506 | $frameHead[3] = bindec($payloadLengthBin[1]); |
||
507 | |||
508 | } else { |
||
509 | $frameHead[1] = ($masked === TRUE) ? $payloadLength + 128 : $payloadLength; |
||
510 | } |
||
511 | |||
512 | // Convert frame-head to string: |
||
513 | foreach (array_keys($frameHead) as $i) { |
||
514 | $frameHead[$i] = chr($frameHead[$i]); |
||
515 | } |
||
516 | |||
517 | if ($masked === TRUE) { |
||
518 | // Generate a random mask: |
||
519 | $mask = []; |
||
520 | |||
521 | for ($i = 0; $i < 4; $i++) { |
||
522 | $mask[$i] = chr(rand(0, 255)); |
||
523 | } |
||
524 | |||
525 | $frameHead = array_merge($frameHead, $mask); |
||
526 | } |
||
527 | |||
528 | $frame = implode('', $frameHead); |
||
529 | |||
530 | // Append payload to frame: |
||
531 | for ($i = 0; $i < $payloadLength; $i++) { |
||
532 | $frame .= ($masked === TRUE) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; |
||
0 ignored issues
–
show
The variable
$mask 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
Loading history...
|
|||
533 | } |
||
534 | |||
535 | return $frame; |
||
536 | } |
||
537 | |||
538 | /** |
||
539 | * @param $data |
||
540 | * |
||
541 | * @return string|NULL |
||
542 | */ |
||
543 | private function hybi10Decode($data) : ?string |
||
0 ignored issues
–
show
|
|||
544 | { |
||
545 | if (empty($data)) { |
||
546 | return NULL; |
||
547 | } |
||
548 | |||
549 | $bytes = $data; |
||
550 | $decodedData = ''; |
||
551 | $secondByte = sprintf('%08b', ord($bytes[1])); |
||
552 | $masked = ($secondByte[0] == '1') ? TRUE : FALSE; |
||
553 | $dataLength = ($masked === TRUE) ? ord($bytes[1]) & 127 : ord($bytes[1]); |
||
554 | |||
555 | if ($masked === TRUE) { |
||
556 | if ($dataLength === 126) { |
||
557 | $mask = substr($bytes, 4, 4); |
||
558 | $coded_data = substr($bytes, 8); |
||
559 | |||
560 | } elseif ($dataLength === 127) { |
||
561 | $mask = substr($bytes, 10, 4); |
||
562 | $coded_data = substr($bytes, 14); |
||
563 | |||
564 | } else { |
||
565 | $mask = substr($bytes, 2, 4); |
||
566 | $coded_data = substr($bytes, 6); |
||
567 | } |
||
568 | |||
569 | for ($i = 0; $i < strlen($coded_data); $i++) { |
||
570 | $decodedData .= $coded_data[$i] ^ $mask[$i % 4]; |
||
571 | } |
||
572 | |||
573 | } else { |
||
574 | if ($dataLength === 126) { |
||
575 | $decodedData = substr($bytes, 4); |
||
576 | |||
577 | } elseif ($dataLength === 127) { |
||
578 | $decodedData = substr($bytes, 10); |
||
579 | |||
580 | } else { |
||
581 | $decodedData = substr($bytes, 2); |
||
582 | } |
||
583 | } |
||
584 | |||
585 | return $decodedData; |
||
586 | } |
||
587 | } |
||
588 |
Adding explicit visibility (
private
,protected
, orpublic
) is generally recommend to communicate to other developers how, and from where this method is intended to be used.