Completed
Push — master ( 0fe4dc...6c70b9 )
by Charlotte
9s
created

Frame::getContent()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 8.8571
cc 5
eloc 8
nc 3
nop 0
crap 5
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 1MiB.
65
     *
66
     * @var int
67
     */
68
    private static $maxPayloadSize = 1049000;
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 72
    public function __construct(string $data = null)
135
    {
136 72
        if (null !== $data) {
137 66
            $this->setRawData($data);
138
        }
139 67
    }
140
141
    /**
142
     * It also run checks on data.
143
     *
144
     * @param string|int $rawData Probably more likely a string than an int, but well... why not.
145
     * @return self
146
     * @throws IncompleteFrameException
147
     */
148 66
    public function setRawData(string $rawData)
149
    {
150 66
        $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...
151 66
        $this->frameSize = BitManipulation::frameSize($rawData);
152
153 66
        if ($this->frameSize < 2) {
154 1
            throw new IncompleteFrameException('Not enough data to be a frame.');
155
        }
156 65
        $this->getInformationFromRawData();
157
158
        try {
159 65
            $this->checkFrameSize();
160 12
        } catch (TooBigFrameException $e) {
161 10
            $this->frameSize = $e->getMaxLength();
162 10
            $this->rawData = BitManipulation::bytesFromToString($this->rawData, 0, $this->frameSize, BitManipulation::MODE_PHP);
163
        }
164
165 63
        Frame::checkFrame($this);
166
167 61
        return $this;
168
    }
169
170
    /**
171
     * Return cached raw data or generate it from data (payload/frame type).
172
     *
173
     * @return string
174
     * @throws InvalidFrameException
175
     */
176 22
    public function getRawData() : string
177
    {
178 22
        if (null !== $this->rawData) {
179 17
            return $this->rawData;
180
        }
181
182 7
        if (!$this->isValid()) {
183
            throw new InvalidFrameException('The frame you composed is not valid !');
184
        }
185 7
        $data = '';
186
187 7
        $secondLen = null;
188 7
        if ($this->payloadLen < 126) {
189 6
            $firstLen = $this->payloadLen;
190
191
        } else {
192 1
            if ($this->payloadLen < 65536) {
193
                $firstLen = 126;
194
            } else {
195 1
                $firstLen = 127;
196
            }
197 1
            $secondLen = $this->payloadLen;
198
        }
199
200 7
        $data .= BitManipulation::intToString(
201 7
            ((((null === $this->final ? 1 : (int) $this->final) << 7) + $this->opcode) << 8)
202 7
            + ($this->isMasked() << 7) + $firstLen
203
        );
204 7
        if (null !== $secondLen) {
205 1
            $data .= BitManipulation::intToString($secondLen, $firstLen === 126 ? 2 : 8);
206
        }
207 7
        if ($this->isMasked()) {
208 1
            $data .= $this->getMaskingKey();
209 1
            $data .= $this->applyMask();
210
211 1
            return $this->rawData = $data;
212
        }
213
214 6
        return $this->rawData = $data . $this->getPayload();
215
    }
216
217
    /**
218
     * As a message is composed by many frames, the frame have the information of "last" or not.
219
     * The frame is final if the first bit is 0.
220
     */
221 55
    public function isFinal() : bool
222
    {
223 55
        return $this->final;
224
    }
225
226
    /**
227
     * @param bool $final
228
     * @return Frame
229
     */
230 2
    public function setFinal(bool $final) : Frame
231
    {
232 2
        $this->final = $final;
233
234 2
        return $this;
235
    }
236
237
    /**
238
     * @return boolean
239
     */
240 4
    public function getRsv1() : bool
241
    {
242 4
        return (bool) BitManipulation::nthBit($this->firstByte, 2);
243
    }
244
245
    /**
246
     * @return boolean
247
     */
248 3
    public function getRsv2() : bool
249
    {
250 3
        return (bool) BitManipulation::nthBit($this->firstByte, 3);
251
    }
252
253
    /**
254
     * @return boolean
255
     */
256 3
    public function getRsv3() : bool
257
    {
258 3
        return (bool) BitManipulation::nthBit($this->firstByte, 4);
259
    }
260
261
    /**
262
     * @return int
263
     */
264 64
    public function getOpcode() : int
265
    {
266 64
        return BitManipulation::partOfByte($this->firstByte, 2);
267
    }
268
269
    /**
270
     * Set the opcode of the frame.
271
     *
272
     * @param int $opcode
273
     * @return Frame
274
     */
275 8
    public function setOpcode(int $opcode) : Frame
276
    {
277 8
        if (!in_array($opcode, [Frame::OP_TEXT, Frame::OP_BINARY, Frame::OP_CLOSE, Frame::OP_CONTINUE, Frame::OP_PING, Frame::OP_PONG])) {
278
            throw new \InvalidArgumentException('Wrong opcode !');
279
        }
280
281 8
        $this->rawData = null;
282 8
        $this->opcode = $opcode;
283
284 8
        return $this;
285
    }
286
287
    /**
288
     * Set the masking key of the frame. As a consequence the frame is now considered as masked.
289
     *
290
     * @param string $mask
291
     * @return Frame
292
     */
293 1
    public function setMaskingKey(string $mask) : Frame
294
    {
295 1
        if (null === $mask) {
296
            $this->isMasked();
297
        }
298 1
        $this->mask = $mask;
299 1
        $this->rawData = null;
300
301 1
        return $this;
302
    }
303
304
    /**
305
     * Get the masking key (from cache if possible).
306
     *
307
     * @return string
308
     */
309 10
    public function getMaskingKey() : string
310
    {
311 10
        if (null !== $this->mask) {
312 2
            return $this->mask;
313
        }
314 9
        if (!$this->isMasked()) {
315
            return '';
316
        }
317
318 9
        if (null === $this->payloadLenSize) {
319
            throw new \LogicException('The payload length size must be load before anything.');
320
        }
321
322
        // 8 is the numbers of bits before the payload len.
323 9
        $start = ((9 + $this->payloadLenSize) / 8);
324
325 9
        $value = BitManipulation::bytesFromTo($this->rawData, $start, $start + 3);
326
327 9
        return $this->mask = BitManipulation::intToString($value, 4);
328
    }
329
330
    /**
331
     * Get payload from cache or generate it from raw data.
332
     *
333
     * @return string
334
     */
335 30
    public function getPayload()
336
    {
337 30
        if ($this->payload !== null) {
338 12
            return $this->payload;
339
        }
340
341 26
        $infoBytesLen = $this->getInfoBytesLen();
342 26
        $payload = (string) BitManipulation::bytesFromToString($this->rawData, $infoBytesLen, $this->payloadLen, BitManipulation::MODE_PHP);
343
344 26
        if ($this->isMasked()) {
345 9
            $this->payload = $payload;
346
347 9
            return $this->payload = $this->applyMask();
348
        }
349
350 19
        return $this->payload = $payload;
351
    }
352
353
    /**
354
     * Returns the content and not potential metadata of the body.
355
     * If you want to get the real body you will prefer using `getPayload`
356
     *
357
     * @return string
358
     */
359 20
    public function getContent()
360
    {
361 20
        $payload = $this->getPayload();
362 20
        if ($this->getOpcode() === Frame::OP_TEXT || $this->getOpcode() === Frame::OP_BINARY) {
363 12
            return $payload;
364
        }
365
366 15
        $len = BitManipulation::frameSize($payload);
367 15
        if ($len !== 0 && $this->getOpcode() === Frame::OP_CLOSE) {
368 4
            return BitManipulation::bytesFromToString($payload, 2, $len);
369
        }
370
371 11
        return $payload;
372
    }
373
374
    /**
375
     * Get length of meta data of the frame.
376
     * Metadata contains type of frame, length, masking key and rsv data.
377
     *
378
     * @return int
379
     */
380 65
    public function getInfoBytesLen()
381
    {
382 65
        if ($this->infoBytesLen) {
383 26
            return $this->infoBytesLen;
384
        }
385
386
        // Calculate headers (infos) length
387
        // which can depend on mask and payload length information size
388 65
        return $this->infoBytesLen = (9 + $this->payloadLenSize) / 8 + ($this->isMasked() ? 4 : 0);
389
    }
390
391
    /**
392
     * Set the payload.
393
     * The raw data is reset.
394
     *
395
     * @param string $payload
396
     * @return Frame
397
     */
398 8
    public function setPayload(string $payload) : Frame
399
    {
400 8
        $this->rawData = null;
401 8
        $this->payload = $payload;
402 8
        $this->payloadLen = BitManipulation::frameSize($this->payload);
403 8
        $this->payloadLenSize = 7;
404
405 8
        if ($this->payloadLen > 126 && $this->payloadLen < 65536) {
406
            $this->payloadLenSize += 16;
407 8
        } else if ($this->payloadLen > 126) {
408 1
            $this->payloadLenSize += 64;
409
        }
410
411 8
        return $this;
412
    }
413
414
    /**
415
     * @return int
416
     * @throws TooBigFrameException
417
     */
418 65
    public function getPayloadLength() : int
419
    {
420 65
        if (null !== $this->payloadLen) {
421 36
            return $this->payloadLen;
422
        }
423
424 65
        if ($this->secondByte === null) {
425
            throw new \RuntimeException('Impossible to get the payload length at this state of the frame, there is no data.');
426
        }
427
428
        // Get the first part of the payload length by removing mask information from the second byte
429 65
        $payloadLen = $this->secondByte & 127;
430 65
        $this->payloadLenSize = 7;
431
432 65
        if ($payloadLen === 126) {
433 2
            $this->payloadLenSize += 16;
434 2
            $payloadLen = BitManipulation::bytesFromTo($this->rawData, 2, 3);
435 64
        } else if ($payloadLen === 127) {
436 4
            $this->payloadLenSize += 64;
437
438 4
            $payloadLen = BitManipulation::bytesFromTo($this->rawData, 2, 9, true);
439
        }
440
441
        // Check < 0 because 64th bit is the negative one in PHP.
442 65
        if ($payloadLen < 0 || $payloadLen > Frame::$maxPayloadSize) {
443 2
            throw new TooBigFrameException(Frame::$maxPayloadSize);
444
        }
445
446 65
        return $this->payloadLen = $payloadLen;
447
    }
448
449
    /**
450
     * 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
451
     * for you.
452
     *
453
     * @return bool
454
     */
455 70
    public function isMasked() : bool
456
    {
457 70
        if ($this->mask !== null) {
458 2
            return true;
459
        }
460
461 69
        if ($this->rawData !== null) {
462 65
            return (bool) BitManipulation::nthBit($this->secondByte, 1);
463
        }
464
465 6
        return false;
466
    }
467
468
    /**
469
     * This method works for mask and unmask (it's the same operation)
470
     *
471
     * @return string
472
     */
473 10
    public function applyMask() : string
474
    {
475 10
        $res = '';
476 10
        $mask = $this->getMaskingKey();
477
478 10
        for ($i = 0; $i < $this->payloadLen; $i++) {
479 10
            $payloadByte = $this->payload[$i];
480 10
            $res .= $payloadByte ^ $mask[$i % 4];
481
        }
482
483 10
        return $res;
484
    }
485
486
    /**
487
     * Fill metadata of the frame from the raw data.
488
     */
489 65
    private function getInformationFromRawData()
490
    {
491 65
        $this->firstByte = BitManipulation::nthByte($this->rawData, 0);
492 65
        $this->secondByte = BitManipulation::nthByte($this->rawData, 1);
493
494 65
        $this->final = (bool) BitManipulation::nthBit($this->firstByte, 1);
495 65
        $this->payloadLen = $this->getPayloadLength();
496 65
    }
497
498
    /**
499
     * Check if the frame have the good size based on payload size.
500
     *
501
     * @throws IncompleteFrameException
502
     * @throws TooBigFrameException
503
     */
504 65
    public function checkFrameSize()
505
    {
506 65
        $infoBytesLen = $this->getInfoBytesLen();
507 65
        $this->frameSize = BitManipulation::frameSize($this->rawData);
508 65
        $theoricDataLength = $infoBytesLen + $this->payloadLen;
509
510 65
        if ($this->frameSize < $theoricDataLength) {
511 1
            throw new IncompleteFrameException(
512 1
                sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $this->frameSize)
513
            );
514
        }
515
516 64
        if ($this->frameSize > $theoricDataLength) {
517 10
            throw new TooBigFrameException($theoricDataLength);
518
        }
519
520 61
        if ($this->getOpcode() === Frame::OP_CLOSE && $this->payloadLen === 1) {
521 1
            throw new ProtocolErrorException('The close frame cannot be only 1 bytes as the close code MUST be send as 2 bytes unsigned int.');
522
        }
523 60
    }
524
525
526
    /**
527
     * Validate a frame with RFC criteria
528
     *
529
     * @param Frame $frame
530
     *
531
     * @throws ControlFrameException
532
     * @throws TooBigControlFrameException
533
     */
534 63
    public static function checkFrame(Frame $frame)
535
    {
536 63
        if ($frame->isControlFrame()) {
537 35
            if (!$frame->isFinal()) {
538 1
                throw new ControlFrameException('The frame cannot be fragmented');
539
            }
540
541 34
            if ($frame->getPayloadLength() > Frame::MAX_CONTROL_FRAME_SIZE) {
542 2
                throw new TooBigControlFrameException('A control frame cannot be larger than 125 bytes.');
543
            }
544
        }
545 61
    }
546
547
    /**
548
     * You can call this method to be sure your frame is valid before trying to get the raw data.
549
     *
550
     * @return bool
551
     */
552 7
    public function isValid() : bool
553
    {
554 7
        return !empty($this->opcode);
555
    }
556
557
    /**
558
     * The Control Frame is a pong, ping, close frame or a reserved frame between 0xB-0xF.
559
     * @see https://tools.ietf.org/html/rfc6455#section-5.5
560
     *
561
     * @return bool
562
     */
563 63
    public function isControlFrame()
564
    {
565 63
        return $this->getOpcode() >= 8;
566
    }
567
}
568