1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\Socket\WebSocket; |
4
|
|
|
|
5
|
|
|
use Generator; |
6
|
|
|
use InvalidArgumentException; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Reads frames from the peer. |
10
|
|
|
*/ |
11
|
|
|
class FrameReader { |
12
|
|
|
|
13
|
|
|
// todo? doesn't allow unmasked frames. |
14
|
|
|
// op((char )|(short )|(bigint ))(mask ) |
15
|
|
|
protected const REGEXP = '/^.([\x80-\xfd]|\xfe(?<n>..)|\xff(?<J>.{8}))(?<mask>.{4})/s'; |
16
|
|
|
|
17
|
|
|
const MAX_LENGTH_RANGE = [125, 2 ** 63 - 1]; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var string |
21
|
|
|
*/ |
22
|
|
|
protected $buffer = ''; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var WebSocketClient |
26
|
|
|
*/ |
27
|
|
|
protected $client; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
protected $head = []; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Payload size limit. |
36
|
|
|
* |
37
|
|
|
* Must fall within {@see MAX_LENGTH_RANGE} (inclusive). |
38
|
|
|
* |
39
|
|
|
* Defaults to 10 MiB. |
40
|
|
|
* |
41
|
|
|
* https://tools.ietf.org/html/rfc6455#section-5.2 |
42
|
|
|
* > ... interpreted as a 64-bit unsigned integer (the |
43
|
|
|
* > most significant bit MUST be 0) ... |
44
|
|
|
* |
45
|
|
|
* @var int |
46
|
|
|
*/ |
47
|
|
|
protected $maxLength = 10 * 1024 * 1024; |
48
|
|
|
|
49
|
|
|
public function __construct (WebSocketClient $client) { |
50
|
|
|
$this->client = $client; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @return Frame|null |
55
|
|
|
*/ |
56
|
|
|
protected function getFrame () { |
57
|
|
|
if (!$this->head) { |
|
|
|
|
58
|
|
|
if (preg_match(self::REGEXP, $this->buffer, $head)) { |
59
|
|
|
[, $op, $len] = unpack('C2', $head[0]); |
60
|
|
|
$len = [0xfe => 'n', 0xff => 'J'][$len] ?? ($len & 0x7f); |
61
|
|
|
$this->head = [ |
62
|
|
|
'final' => $op & 0x80, |
63
|
|
|
'rsv' => $op & Frame::RSV123, |
64
|
|
|
'opCode' => $op & 0x0f, |
65
|
|
|
'length' => is_int($len) ? $len : unpack($len, $head[$len])[1], |
66
|
|
|
'mask' => array_values(unpack('C*', $head['mask'])), |
|
|
|
|
67
|
|
|
]; |
68
|
|
|
$this->buffer = substr($this->buffer, strlen($head[0])); |
69
|
|
|
$this->validate(); |
70
|
|
|
} |
71
|
|
|
elseif (strlen($this->buffer) >= 14) { // max head room |
72
|
|
|
throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, 'Bad frame.'); |
73
|
|
|
} |
74
|
|
|
else { |
75
|
|
|
return null; |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
$length = $this->head['length']; |
79
|
|
|
if (strlen($this->buffer) >= $length) { |
80
|
|
|
$payload = substr($this->buffer, 0, $length); |
81
|
|
|
$this->buffer = substr($this->buffer, $length); |
82
|
|
|
$mask = $this->head['mask']; |
83
|
|
|
for ($i = 0; $i < $length; $i++) { |
84
|
|
|
$payload[$i] = chr(ord($payload[$i]) ^ $mask[$i % 4]); |
85
|
|
|
} |
86
|
|
|
$frame = new Frame($this->head['final'], $this->head['rsv'], $this->head['opCode'], $payload); |
87
|
|
|
$this->head = []; |
88
|
|
|
return $frame; |
89
|
|
|
} |
90
|
|
|
return null; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Constructs and yields all available frames from the peer. |
95
|
|
|
* |
96
|
|
|
* @return Generator|Frame[] |
97
|
|
|
*/ |
98
|
|
|
public function getFrames () { |
99
|
|
|
$this->buffer .= $this->client->recvAll(); |
100
|
|
|
while ($frame = $this->getFrame()) { |
101
|
|
|
yield $frame; |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @return int |
107
|
|
|
*/ |
108
|
|
|
public function getMaxLength (): int { |
109
|
|
|
return $this->maxLength; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* @param int $bytes |
114
|
|
|
* @return $this |
115
|
|
|
*/ |
116
|
|
|
public function setMaxLength (int $bytes) { |
117
|
|
|
if ($bytes < self::MAX_LENGTH_RANGE[0] or $bytes > self::MAX_LENGTH_RANGE[1]) { |
118
|
|
|
throw new InvalidArgumentException('Max length must be within range [125,2^63-1]'); |
119
|
|
|
} |
120
|
|
|
$this->maxLength = $bytes; |
121
|
|
|
return $this; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Validates the current head by not throwing. |
126
|
|
|
* |
127
|
|
|
* @throws WebSocketError |
128
|
|
|
*/ |
129
|
|
|
protected function validate (): void { |
130
|
|
|
if ($this->head['length'] > $this->maxLength) { |
131
|
|
|
throw new WebSocketError(Frame::CLOSE_TOO_LARGE, "Payload would exceed {$this->maxLength} bytes"); |
132
|
|
|
} |
133
|
|
|
$opCode = $this->head['opCode']; |
134
|
|
|
$name = Frame::NAMES[$opCode]; |
135
|
|
|
if ($opCode & 0x08) { // control |
136
|
|
|
if ($opCode > Frame::OP_PONG) { |
137
|
|
|
throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received {$name}"); |
138
|
|
|
} |
139
|
|
|
if (!$this->head['final']) { |
140
|
|
|
throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received fragmented {$name}"); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
elseif ($opCode > Frame::OP_BINARY) { // data |
144
|
|
|
throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received {$name}"); |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.