Completed
Push — master ( 0a022c...1f28ab )
by y
01:37
created

FrameReader::validate()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.0444
c 0
b 0
f 0
cc 6
nc 6
nop 0
1
<?php
2
3
namespace Helix\Socket\WebSocket;
4
5
use Generator;
6
7
/**
8
 * Reads frames from the peer.
9
 *
10
 * https://tools.ietf.org/html/rfc6455#section-5
11
 *
12
 * TODO: Support unmasked frames.
13
 */
14
class FrameReader {
15
16
    /**
17
     * https://tools.ietf.org/html/rfc6455#section-5.2
18
     */
19
    protected const REGEXP =
20
        //op((char     )|(short     )|(bigint      ))(mask       )
21
        '/^.([\x80-\xfd]|\xfe(?<n>..)|\xff(?<J>.{8}))(?<mask>.{4})/s';
22
23
    /**
24
     * Peer read buffer.
25
     *
26
     * @var string
27
     */
28
    protected $buffer = '';
29
30
    /**
31
     * @var WebSocketClient
32
     */
33
    protected $client;
34
35
    /**
36
     * Frame header buffer.
37
     *
38
     * @var null|array
39
     */
40
    protected $header;
41
42
    /**
43
     * Maximum inbound per-frame payload length (fragment).
44
     *
45
     * Must be greater than or equal to `125`
46
     *
47
     * Defaults to 128 KiB.
48
     *
49
     * https://tools.ietf.org/html/rfc6455#section-5.2
50
     *
51
     * @var int
52
     */
53
    protected $maxLength = 128 * 1024;
54
55
    /**
56
     * RSV bit mask claimed by extensions.
57
     *
58
     * @var int
59
     */
60
    protected $rsv = 0;
61
62
    /**
63
     * @param WebSocketClient $client
64
     */
65
    public function __construct (WebSocketClient $client) {
66
        $this->client = $client;
67
    }
68
69
    /**
70
     * Reads and returns a single pending frame from the buffer, or nothing.
71
     *
72
     * @return null|Frame
73
     * @throws WebSocketError
74
     */
75
    protected function getFrame (): ?Frame {
76
        // wait for the header
77
        if (!$this->header ??= $this->getFrame_header()) {
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '='
Loading history...
78
            return null;
79
        }
80
81
        // wait for the whole frame
82
        $length = $this->header['length'];
83
        if (strlen($this->buffer) < $length) {
84
            return null;
85
        }
86
87
        // extract the payload
88
        $payload = substr($this->buffer, 0, $length);
89
90
        // chop the buffer
91
        $this->buffer = substr($this->buffer, $length);
92
93
        // unmask the payload
94
        $mask = $this->header['mask'];
95
        for ($i = 0; $i < $length; $i++) {
96
            $payload[$i] = chr(ord($payload[$i]) ^ $mask[$i % 4]);
97
        }
98
99
        // construct the frame instance
100
        $frame = $this->newFrame($payload);
101
102
        // destroy the header buffer
103
        $this->header = null;
104
105
        // return the frame
106
        return $frame;
107
    }
108
109
    /**
110
     * https://tools.ietf.org/html/rfc6455#section-5.2
111
     * @return null|array
112
     */
113
    protected function getFrame_header (): ?array {
114
        if (!preg_match(self::REGEXP, $this->buffer, $match)) {
115
            if (strlen($this->buffer) >= 14) { // max head room
116
                throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, 'Bad frame.');
117
            }
118
            return null;
119
        }
120
121
        // unpack the first two bytes
122
        [, $b0, $b1] = unpack('C2', $match[0]); // 1-based indices
123
124
        // convert the second byte into an unpack() format, or the actual length (sans the MASK bit).
125
        // the unpack() format is also used as the length's named-group in the regexp match.
126
        $len = [0xfe => 'n', 0xff => 'J'][$b1] ?? ($b1 & Frame::LEN);
127
128
        // fill the header buffer
129
        $header = [
130
            'final' => $final = $b0 & Frame::FIN,
131
            'rsv' => $rsv = $b0 & Frame::RSV123,
132
            'opCode' => $opCode = $b0 & Frame::OP,
133
            'length' => $length = is_int($len) ? $len : unpack($len, $match[$len])[1],
134
            'mask' => array_values(unpack('C*', $match['mask'])),
135
        ];
136
137
        // chop the peer buffer
138
        $this->buffer = substr($this->buffer, strlen($match[0]));
139
140
        // validate
141
        if ($badRsv = $rsv & ~$this->rsv) {
142
            $badRsv = str_pad(base_convert($badRsv >> 4, 10, 2), 3, '0', STR_PAD_LEFT);
143
            throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received unknown RSV bits: 0b{$badRsv}");
144
        }
145
        elseif ($opCode >= Frame::OP_CLOSE) {
146
            if ($opCode > Frame::OP_PONG) {
147
                throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received unsupported control frame ({$opCode})");
148
            }
149
            elseif (!$final) {
150
                throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received fragmented control frame ({$opCode})");
151
            }
152
        }
153
        elseif ($opCode > Frame::OP_BINARY) {
154
            throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received unsupported data frame ({$opCode})");
155
        }
156
        elseif ($length > $this->maxLength) {
157
            throw new WebSocketError(Frame::CLOSE_TOO_LARGE, "Payload would exceed {$this->maxLength} bytes");
158
        }
159
160
        // return the header
161
        return $header;
162
    }
163
164
    /**
165
     * Yields all available frames from the peer.
166
     *
167
     * @return Generator|Frame[]
168
     */
169
    public function getFrames () {
170
        // read into the buffer
171
        $this->buffer .= $bytes = $this->client->recvAll();
172
173
        // check for peer disconnection
174
        if (!strlen($bytes)) {
175
            $this->client->close();
176
        }
177
178
        // yield frames
179
        while ($frame = $this->getFrame()) {
180
            yield $frame;
181
        }
182
    }
183
184
    /**
185
     * @return int
186
     */
187
    public function getMaxLength (): int {
188
        return $this->maxLength;
189
    }
190
191
    /**
192
     * @return int
193
     */
194
    public function getRsv (): int {
195
        return $this->rsv;
196
    }
197
198
    /**
199
     * {@link Frame} factory.
200
     *
201
     * @param string $payload
202
     * @return Frame
203
     */
204
    protected function newFrame (string $payload): Frame {
205
        return new Frame($this->header['final'], $this->header['rsv'], $this->header['opCode'], $payload);
206
    }
207
208
    /**
209
     * @param int $bytes
210
     * @return $this
211
     */
212
    public function setMaxLength (int $bytes) {
213
        $this->maxLength = min(max(125, $bytes), 2 ** 63 - 1);
214
        return $this;
215
    }
216
217
    /**
218
     * @param int $rsv
219
     * @return $this
220
     */
221
    public function setRsv (int $rsv) {
222
        $this->rsv = $rsv;
223
        return $this;
224
    }
225
}