Frame   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 573
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 94.74%

Importance

Changes 0
Metric Value
wmc 69
lcom 1
cbo 7
dl 0
loc 573
ccs 162
cts 171
cp 0.9474
rs 2.88
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A setRawData() 0 21 3
B getRawData() 0 40 9
A isFinal() 0 4 1
A setFinal() 0 6 1
A getRsv1() 0 4 1
A getRsv2() 0 4 1
A getRsv3() 0 4 1
A getOpcode() 0 4 1
A setOpcode() 0 11 2
A setMaskingKey() 0 10 2
A getMaskingKey() 0 20 4
A getPayload() 0 17 3
A getContent() 0 14 5
A getInfoBytesLen() 0 10 3
A setPayload() 0 15 4
B getPayloadLength() 0 34 8
A isMasked() 0 12 3
A applyMask() 0 12 2
A getInformationFromRawData() 0 8 1
A checkFrameSize() 0 20 5
A checkFrame() 0 12 4
A isValid() 0 4 1
A isControlFrame() 0 4 1
A setConfig() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like Frame often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Frame, and based on these observations, apply Extract Interface, too.

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