Completed
Push — master ( 4b839d...94a773 )
by Valentin
02:14
created

src/Rfc6455/Frame.php (1 issue)

Upgrade to new PHP Analysis Engine

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
Documentation Bug introduced by
It seems like $rawData can also be of type integer. However, the property $rawData is declared as type string. Maybe add an additional type 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 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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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