These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * This file is a part of Woketo package. |
||
4 | * |
||
5 | * (c) Nekland <[email protected]> |
||
6 | * |
||
7 | * For the full license, take a look to the LICENSE file |
||
8 | * on the root directory of this project |
||
9 | */ |
||
10 | |||
11 | declare(strict_types=1); |
||
12 | namespace Nekland\Woketo\Rfc6455; |
||
13 | |||
14 | use Nekland\Woketo\Exception\Frame\ControlFrameException; |
||
15 | use Nekland\Woketo\Exception\Frame\IncompleteFrameException; |
||
16 | use Nekland\Woketo\Exception\Frame\InvalidFrameException; |
||
17 | use Nekland\Woketo\Exception\Frame\ProtocolErrorException; |
||
18 | use Nekland\Woketo\Exception\Frame\TooBigControlFrameException; |
||
19 | use Nekland\Woketo\Exception\Frame\TooBigFrameException; |
||
20 | use Nekland\Woketo\Utils\BitManipulation; |
||
21 | |||
22 | /** |
||
23 | * Class Frame |
||
24 | * |
||
25 | * @TODO: add support for extensions. |
||
26 | */ |
||
27 | class Frame |
||
28 | { |
||
29 | const OP_CONTINUE = 0; |
||
30 | const OP_TEXT = 1; |
||
31 | const OP_BINARY = 2; |
||
32 | const OP_CLOSE = 8; |
||
33 | const OP_PING = 9; |
||
34 | const OP_PONG = 10; |
||
35 | |||
36 | // To understand codes, please refer to RFC: |
||
37 | // https://tools.ietf.org/html/rfc6455#section-7.4 |
||
38 | const CLOSE_NORMAL = 1000; |
||
39 | const CLOSE_GOING_AWAY = 1001; |
||
40 | const CLOSE_PROTOCOL_ERROR = 1002; |
||
41 | const CLOSE_WRONG_DATA = 1003; |
||
42 | // 1004-1006 are reserved |
||
43 | const CLOSE_INCOHERENT_DATA = 1007; |
||
44 | const CLOSE_POLICY_VIOLATION = 1008; |
||
45 | const CLOSE_TOO_BIG_TO_PROCESS = 1009; |
||
46 | const CLOSE_MISSING_EXTENSION = 1010; // In this case you should precise a reason |
||
47 | const CLOSE_UNEXPECTING_CONDITION = 1011; |
||
48 | // 1015 is reserved |
||
49 | |||
50 | /** |
||
51 | * @see https://tools.ietf.org/html/rfc6455#section-5.5 |
||
52 | */ |
||
53 | const MAX_CONTROL_FRAME_SIZE = 125; |
||
54 | |||
55 | /** |
||
56 | * The payload size can be specified on 64b unsigned int according to the RFC. That means that maximum data |
||
57 | * inside the payload is 0b1111111111111111111111111111111111111111111111111111111111111111 bits. In |
||
58 | * decimal and GB, that means 2147483647 GB. As this is a bit too much for the memory of your computer or |
||
59 | * server, we specified a max size to. |
||
60 | * |
||
61 | * Notice that to support larger transfer we need to implemente a cache strategy on the harddrive. It also suggest |
||
62 | * to have a threaded environment as the task of retrieving the data and treat it will be long. |
||
63 | * |
||
64 | * This value is in bytes. Here we allow 0.5 MiB. |
||
65 | * |
||
66 | * @var int |
||
67 | */ |
||
68 | private static $defaultMaxPayloadSize = 524288; |
||
69 | |||
70 | /** |
||
71 | * Complete string representing data collected from socket |
||
72 | * |
||
73 | * @var string |
||
74 | */ |
||
75 | private $rawData; |
||
76 | |||
77 | /** |
||
78 | * @var int |
||
79 | */ |
||
80 | private $frameSize; |
||
81 | |||
82 | // In case of enter request the following data is cache. |
||
83 | // Otherwise it's data used to generate "rawData". |
||
84 | |||
85 | /** |
||
86 | * @var int |
||
87 | */ |
||
88 | private $firstByte; |
||
89 | |||
90 | /** |
||
91 | * @var int |
||
92 | */ |
||
93 | private $secondByte; |
||
94 | |||
95 | /** |
||
96 | * @var bool |
||
97 | */ |
||
98 | private $final; |
||
99 | |||
100 | /** |
||
101 | * @var int |
||
102 | */ |
||
103 | private $payloadLen; |
||
104 | |||
105 | /** |
||
106 | * Number of bits representing the payload length in the current frame. |
||
107 | * |
||
108 | * @var int |
||
109 | */ |
||
110 | private $payloadLenSize; |
||
111 | |||
112 | /** |
||
113 | * Cache variable for the payload. |
||
114 | * |
||
115 | * @var string |
||
116 | */ |
||
117 | private $payload; |
||
118 | |||
119 | /** |
||
120 | * @var |
||
121 | */ |
||
122 | private $mask; |
||
123 | |||
124 | /** |
||
125 | * @var int |
||
126 | */ |
||
127 | private $opcode; |
||
128 | |||
129 | /** |
||
130 | * @var int |
||
131 | */ |
||
132 | private $infoBytesLen; |
||
133 | |||
134 | /** |
||
135 | * @see Frame::setConfig() for full default configuration. |
||
136 | * |
||
137 | * @var array |
||
138 | */ |
||
139 | private $config; |
||
140 | |||
141 | /** |
||
142 | * You should construct this object by using the FrameFactory class instead of this direct constructor. |
||
143 | * |
||
144 | * @param string|null $data |
||
145 | * @param array $config |
||
146 | * @internal |
||
147 | */ |
||
148 | 81 | public function __construct(string $data = null, array $config = []) |
|
149 | { |
||
150 | 81 | $this->setConfig($config); |
|
151 | 81 | if (null !== $data) { |
|
152 | 74 | $this->setRawData($data); |
|
153 | } |
||
154 | 75 | } |
|
155 | |||
156 | /** |
||
157 | * It also run checks on data. |
||
158 | * |
||
159 | * @param string|int $rawData Probably more likely a string than an int, but well... why not. |
||
160 | * @return self |
||
161 | * @throws IncompleteFrameException |
||
162 | */ |
||
163 | 74 | public function setRawData(string $rawData) |
|
164 | { |
||
165 | 74 | $this->rawData = $rawData; |
|
0 ignored issues
–
show
|
|||
166 | 74 | $this->frameSize = BitManipulation::frameSize($rawData); |
|
167 | |||
168 | 74 | if ($this->frameSize < 2) { |
|
169 | 1 | throw new IncompleteFrameException('Not enough data to be a frame.'); |
|
170 | } |
||
171 | 73 | $this->getInformationFromRawData(); |
|
172 | |||
173 | try { |
||
174 | 72 | $this->checkFrameSize(); |
|
175 | 12 | } catch (TooBigFrameException $e) { |
|
176 | 10 | $this->frameSize = $e->getMaxLength(); |
|
177 | 10 | $this->rawData = BitManipulation::bytesFromToString($this->rawData, 0, $this->frameSize, BitManipulation::MODE_PHP); |
|
178 | } |
||
179 | |||
180 | 70 | Frame::checkFrame($this); |
|
181 | |||
182 | 68 | return $this; |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * Return cached raw data or generate it from data (payload/frame type). |
||
187 | * |
||
188 | * @return string |
||
189 | * @throws InvalidFrameException |
||
190 | */ |
||
191 | 22 | public function getRawData() : string |
|
192 | { |
||
193 | 22 | if (null !== $this->rawData) { |
|
194 | 17 | return $this->rawData; |
|
195 | } |
||
196 | |||
197 | 7 | if (!$this->isValid()) { |
|
198 | throw new InvalidFrameException('The frame you composed is not valid !'); |
||
199 | } |
||
200 | 7 | $data = ''; |
|
201 | |||
202 | 7 | $secondLen = null; |
|
203 | 7 | if ($this->payloadLen < 126) { |
|
204 | 6 | $firstLen = $this->payloadLen; |
|
205 | |||
206 | } else { |
||
207 | 1 | if ($this->payloadLen < 65536) { |
|
208 | $firstLen = 126; |
||
209 | } else { |
||
210 | 1 | $firstLen = 127; |
|
211 | } |
||
212 | 1 | $secondLen = $this->payloadLen; |
|
213 | } |
||
214 | |||
215 | 7 | $data .= BitManipulation::intToString( |
|
216 | 7 | ((((null === $this->final ? 1 : (int) $this->final) << 7) + $this->opcode) << 8) |
|
217 | 7 | + ($this->isMasked() << 7) + $firstLen |
|
218 | ); |
||
219 | 7 | if (null !== $secondLen) { |
|
220 | 1 | $data .= BitManipulation::intToString($secondLen, $firstLen === 126 ? 2 : 8); |
|
221 | } |
||
222 | 7 | if ($this->isMasked()) { |
|
223 | 1 | $data .= $this->getMaskingKey(); |
|
224 | 1 | $data .= $this->applyMask(); |
|
225 | |||
226 | 1 | return $this->rawData = $data; |
|
227 | } |
||
228 | |||
229 | 6 | return $this->rawData = $data . $this->getPayload(); |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * As a message is composed by many frames, the frame have the information of "last" or not. |
||
234 | * The frame is final if the first bit is 0. |
||
235 | */ |
||
236 | 62 | public function isFinal() : bool |
|
237 | { |
||
238 | 62 | return $this->final; |
|
239 | } |
||
240 | |||
241 | /** |
||
242 | * @param bool $final |
||
243 | * @return Frame |
||
244 | */ |
||
245 | 3 | public function setFinal(bool $final) : Frame |
|
246 | { |
||
247 | 3 | $this->final = $final; |
|
248 | |||
249 | 3 | return $this; |
|
250 | } |
||
251 | |||
252 | /** |
||
253 | * @return boolean |
||
254 | */ |
||
255 | 10 | public function getRsv1() : bool |
|
256 | { |
||
257 | 10 | return (bool) BitManipulation::nthBit($this->firstByte, 2); |
|
258 | } |
||
259 | |||
260 | /** |
||
261 | * @return boolean |
||
262 | */ |
||
263 | 9 | public function getRsv2() : bool |
|
264 | { |
||
265 | 9 | return (bool) BitManipulation::nthBit($this->firstByte, 3); |
|
266 | } |
||
267 | |||
268 | /** |
||
269 | * @return boolean |
||
270 | */ |
||
271 | 9 | public function getRsv3() : bool |
|
272 | { |
||
273 | 9 | return (bool) BitManipulation::nthBit($this->firstByte, 4); |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * @return int |
||
278 | */ |
||
279 | 71 | public function getOpcode() : int |
|
280 | { |
||
281 | 71 | return BitManipulation::partOfByte($this->firstByte, 2); |
|
282 | } |
||
283 | |||
284 | /** |
||
285 | * Set the opcode of the frame. |
||
286 | * |
||
287 | * @param int $opcode |
||
288 | * @return Frame |
||
289 | */ |
||
290 | 8 | public function setOpcode(int $opcode) : Frame |
|
291 | { |
||
292 | 8 | if (!\in_array($opcode, [Frame::OP_TEXT, Frame::OP_BINARY, Frame::OP_CLOSE, Frame::OP_CONTINUE, Frame::OP_PING, Frame::OP_PONG])) { |
|
293 | throw new \InvalidArgumentException('Wrong opcode !'); |
||
294 | } |
||
295 | |||
296 | 8 | $this->rawData = null; |
|
297 | 8 | $this->opcode = $opcode; |
|
298 | |||
299 | 8 | return $this; |
|
300 | } |
||
301 | |||
302 | /** |
||
303 | * Set the masking key of the frame. As a consequence the frame is now considered as masked. |
||
304 | * |
||
305 | * @param string $mask |
||
306 | * @return Frame |
||
307 | */ |
||
308 | 1 | public function setMaskingKey(string $mask) : Frame |
|
309 | { |
||
310 | 1 | if (null === $mask) { |
|
311 | $this->isMasked(); |
||
312 | } |
||
313 | 1 | $this->mask = $mask; |
|
314 | 1 | $this->rawData = null; |
|
315 | |||
316 | 1 | return $this; |
|
317 | } |
||
318 | |||
319 | /** |
||
320 | * Get the masking key (from cache if possible). |
||
321 | * |
||
322 | * @return string |
||
323 | */ |
||
324 | 10 | public function getMaskingKey() : string |
|
325 | { |
||
326 | 10 | if (null !== $this->mask) { |
|
327 | 2 | return $this->mask; |
|
328 | } |
||
329 | 9 | if (!$this->isMasked()) { |
|
330 | return ''; |
||
331 | } |
||
332 | |||
333 | 9 | if (null === $this->payloadLenSize) { |
|
334 | throw new \LogicException('The payload length size must be load before anything.'); |
||
335 | } |
||
336 | |||
337 | // 8 is the numbers of bits before the payload len. |
||
338 | 9 | $start = ((9 + $this->payloadLenSize) / 8); |
|
339 | |||
340 | 9 | $value = BitManipulation::bytesFromTo($this->rawData, $start, $start + 3); |
|
341 | |||
342 | 9 | return $this->mask = BitManipulation::intToString($value, 4); |
|
343 | } |
||
344 | |||
345 | /** |
||
346 | * Get payload from cache or generate it from raw data. |
||
347 | * |
||
348 | * @return string |
||
349 | */ |
||
350 | 36 | public function getPayload() |
|
351 | { |
||
352 | 36 | if ($this->payload !== null) { |
|
353 | 20 | return $this->payload; |
|
354 | } |
||
355 | |||
356 | 32 | $infoBytesLen = $this->getInfoBytesLen(); |
|
357 | 32 | $payload = (string) BitManipulation::bytesFromToString($this->rawData, $infoBytesLen, $this->payloadLen, BitManipulation::MODE_PHP); |
|
358 | |||
359 | 32 | if ($this->isMasked()) { |
|
360 | 9 | $this->payload = $payload; |
|
361 | |||
362 | 9 | return $this->payload = $this->applyMask(); |
|
363 | } |
||
364 | |||
365 | 25 | return $this->payload = $payload; |
|
366 | } |
||
367 | |||
368 | /** |
||
369 | * Returns the content and not potential metadata of the body. |
||
370 | * If you want to get the real body you will prefer using `getPayload` |
||
371 | * |
||
372 | * @return string |
||
373 | */ |
||
374 | 26 | public function getContent() |
|
375 | { |
||
376 | 26 | $payload = $this->getPayload(); |
|
377 | 26 | if ($this->getOpcode() === Frame::OP_TEXT || $this->getOpcode() === Frame::OP_BINARY) { |
|
378 | 12 | return $payload; |
|
379 | } |
||
380 | |||
381 | 21 | $len = BitManipulation::frameSize($payload); |
|
382 | 21 | if ($len !== 0 && $this->getOpcode() === Frame::OP_CLOSE) { |
|
383 | 10 | return BitManipulation::bytesFromToString($payload, 2, $len); |
|
384 | } |
||
385 | |||
386 | 11 | return $payload; |
|
387 | } |
||
388 | |||
389 | /** |
||
390 | * Get length of meta data of the frame. |
||
391 | * Metadata contains type of frame, length, masking key and rsv data. |
||
392 | * |
||
393 | * @return int |
||
394 | */ |
||
395 | 72 | public function getInfoBytesLen() |
|
396 | { |
||
397 | 72 | if ($this->infoBytesLen) { |
|
398 | 32 | return $this->infoBytesLen; |
|
399 | } |
||
400 | |||
401 | // Calculate headers (infos) length |
||
402 | // which can depend on mask and payload length information size |
||
403 | 72 | return $this->infoBytesLen = (9 + $this->payloadLenSize) / 8 + ($this->isMasked() ? 4 : 0); |
|
404 | } |
||
405 | |||
406 | /** |
||
407 | * Set the payload. |
||
408 | * The raw data is reset. |
||
409 | * |
||
410 | * @param string $payload |
||
411 | * @return Frame |
||
412 | */ |
||
413 | 8 | public function setPayload(string $payload) : Frame |
|
414 | { |
||
415 | 8 | $this->rawData = null; |
|
416 | 8 | $this->payload = $payload; |
|
417 | 8 | $this->payloadLen = BitManipulation::frameSize($this->payload); |
|
418 | 8 | $this->payloadLenSize = 7; |
|
419 | |||
420 | 8 | if ($this->payloadLen > 126 && $this->payloadLen < 65536) { |
|
421 | $this->payloadLenSize += 16; |
||
422 | 8 | } else if ($this->payloadLen > 126) { |
|
423 | 1 | $this->payloadLenSize += 64; |
|
424 | } |
||
425 | |||
426 | 8 | return $this; |
|
427 | } |
||
428 | |||
429 | /** |
||
430 | * @return int |
||
431 | * @throws TooBigFrameException |
||
432 | */ |
||
433 | 73 | public function getPayloadLength() : int |
|
434 | { |
||
435 | 73 | if (null !== $this->payloadLen) { |
|
436 | 42 | return $this->payloadLen; |
|
437 | } |
||
438 | |||
439 | 73 | if ($this->secondByte === null) { |
|
440 | throw new \RuntimeException('Impossible to get the payload length at this state of the frame, there is no data.'); |
||
441 | } |
||
442 | |||
443 | // Get the first part of the payload length by removing mask information from the second byte |
||
444 | 73 | $payloadLen = $this->secondByte & 127; |
|
445 | 73 | $this->payloadLenSize = 7; |
|
446 | |||
447 | 73 | if ($payloadLen === 126) { |
|
448 | 2 | $this->payloadLenSize += 16; |
|
449 | 2 | $payloadLen = BitManipulation::bytesFromTo($this->rawData, 2, 3); |
|
450 | 72 | } else if ($payloadLen === 127) { |
|
451 | 6 | $this->payloadLenSize += 64; |
|
452 | |||
453 | 6 | $payloadLen = BitManipulation::bytesFromTo($this->rawData, 2, 9, true); |
|
454 | } |
||
455 | |||
456 | // Check < 0 because 64th bit is the negative one in PHP. |
||
457 | 73 | if ($payloadLen < 0 || $payloadLen > $this->config['maxPayloadSize']) { |
|
458 | 3 | throw new TooBigFrameException($this->config['maxPayloadSize']); |
|
459 | } |
||
460 | |||
461 | 72 | return $this->payloadLen = $payloadLen; |
|
462 | } |
||
463 | |||
464 | /** |
||
465 | * If there is a mask in the raw data or if the mask was set, the frame is masked and this method gets the result |
||
466 | * for you. |
||
467 | * |
||
468 | * @return bool |
||
469 | */ |
||
470 | 77 | public function isMasked() : bool |
|
471 | { |
||
472 | 77 | if ($this->mask !== null) { |
|
473 | 2 | return true; |
|
474 | } |
||
475 | |||
476 | 76 | if ($this->rawData !== null) { |
|
477 | 72 | return (bool) BitManipulation::nthBit($this->secondByte, 1); |
|
478 | } |
||
479 | |||
480 | 6 | return false; |
|
481 | } |
||
482 | |||
483 | /** |
||
484 | * This method works for mask and unmask (it's the same operation) |
||
485 | * |
||
486 | * @return string |
||
487 | */ |
||
488 | 10 | public function applyMask() : string |
|
489 | { |
||
490 | 10 | $res = ''; |
|
491 | 10 | $mask = $this->getMaskingKey(); |
|
492 | |||
493 | 10 | for ($i = 0; $i < $this->payloadLen; $i++) { |
|
494 | 10 | $payloadByte = $this->payload[$i]; |
|
495 | 10 | $res .= $payloadByte ^ $mask[$i % 4]; |
|
496 | } |
||
497 | |||
498 | 10 | return $res; |
|
499 | } |
||
500 | |||
501 | /** |
||
502 | * Fill metadata of the frame from the raw data. |
||
503 | */ |
||
504 | 73 | private function getInformationFromRawData() |
|
505 | { |
||
506 | 73 | $this->firstByte = BitManipulation::nthByte($this->rawData, 0); |
|
507 | 73 | $this->secondByte = BitManipulation::nthByte($this->rawData, 1); |
|
508 | |||
509 | 73 | $this->final = (bool) BitManipulation::nthBit($this->firstByte, 1); |
|
510 | 73 | $this->payloadLen = $this->getPayloadLength(); |
|
511 | 72 | } |
|
512 | |||
513 | /** |
||
514 | * Check if the frame have the good size based on payload size. |
||
515 | * |
||
516 | * @throws IncompleteFrameException |
||
517 | * @throws TooBigFrameException |
||
518 | */ |
||
519 | 72 | public function checkFrameSize() |
|
520 | { |
||
521 | 72 | $infoBytesLen = $this->getInfoBytesLen(); |
|
522 | 72 | $this->frameSize = BitManipulation::frameSize($this->rawData); |
|
523 | 72 | $theoricDataLength = $infoBytesLen + $this->payloadLen; |
|
524 | |||
525 | 72 | if ($this->frameSize < $theoricDataLength) { |
|
526 | 1 | throw new IncompleteFrameException( |
|
527 | 1 | sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $this->frameSize) |
|
528 | ); |
||
529 | } |
||
530 | |||
531 | 71 | if ($this->frameSize > $theoricDataLength) { |
|
532 | 10 | throw new TooBigFrameException($theoricDataLength); |
|
533 | } |
||
534 | |||
535 | 68 | if ($this->getOpcode() === Frame::OP_CLOSE && $this->payloadLen === 1) { |
|
536 | 1 | throw new ProtocolErrorException('The close frame cannot be only 1 bytes as the close code MUST be send as 2 bytes unsigned int.'); |
|
537 | } |
||
538 | 67 | } |
|
539 | |||
540 | |||
541 | /** |
||
542 | * Validate a frame with RFC criteria |
||
543 | * |
||
544 | * @param Frame $frame |
||
545 | * |
||
546 | * @throws ControlFrameException |
||
547 | * @throws TooBigControlFrameException |
||
548 | */ |
||
549 | 70 | public static function checkFrame(Frame $frame) |
|
550 | { |
||
551 | 70 | if ($frame->isControlFrame()) { |
|
552 | 41 | if (!$frame->isFinal()) { |
|
553 | 1 | throw new ControlFrameException('The frame cannot be fragmented'); |
|
554 | } |
||
555 | |||
556 | 40 | if ($frame->getPayloadLength() > Frame::MAX_CONTROL_FRAME_SIZE) { |
|
557 | 2 | throw new TooBigControlFrameException('A control frame cannot be larger than 125 bytes.'); |
|
558 | } |
||
559 | } |
||
560 | 68 | } |
|
561 | |||
562 | /** |
||
563 | * You can call this method to be sure your frame is valid before trying to get the raw data. |
||
564 | * |
||
565 | * @return bool |
||
566 | */ |
||
567 | 7 | public function isValid() : bool |
|
568 | { |
||
569 | 7 | return !empty($this->opcode); |
|
570 | } |
||
571 | |||
572 | /** |
||
573 | * The Control Frame is a pong, ping, close frame or a reserved frame between 0xB-0xF. |
||
574 | * @see https://tools.ietf.org/html/rfc6455#section-5.5 |
||
575 | * |
||
576 | * @return bool |
||
577 | */ |
||
578 | 70 | public function isControlFrame() |
|
579 | { |
||
580 | 70 | return $this->getOpcode() >= 8; |
|
581 | } |
||
582 | |||
583 | /** |
||
584 | * @param array $config |
||
585 | * @return Frame |
||
586 | */ |
||
587 | 81 | public function setConfig(array $config = []) |
|
588 | { |
||
589 | 81 | $this->config = \array_merge([ |
|
590 | 81 | 'maxPayloadSize' => Frame::$defaultMaxPayloadSize, |
|
591 | ],$config); |
||
592 | |||
593 | 81 | return $this; |
|
594 | } |
||
595 | } |
||
596 |
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 mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.