Completed
Push — master ( 46f060...0fe4dc )
by Charlotte
11s
created

Frame   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 516
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 94.77%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 60
c 1
b 0
f 0
lcom 1
cbo 6
dl 0
loc 516
ccs 145
cts 153
cp 0.9477
rs 6.0975

23 Methods

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