Passed
Push — master ( f0b67b...ec9c11 )
by y
01:29
created

FrameHandler::getFragmentSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
3
namespace Helix\Socket\WebSocket;
4
5
/**
6
 * Handles received frames from the peer, and packs and sends frames.
7
 */
8
class FrameHandler {
9
10
    /**
11
     * The binary message buffer when streaming is disabled.
12
     *
13
     * @var string
14
     */
15
    protected $binary = '';
16
17
    /**
18
     * @var WebSocketClient
19
     */
20
    protected $client;
21
22
    /**
23
     * Resume opCode for the `CONTINUE` handler.
24
     *
25
     * @var int|null
26
     */
27
    protected $continue;
28
29
    /**
30
     * Max outgoing fragment size.
31
     *
32
     * Each browser has its own standard, so this is generalized.
33
     *
34
     * Defaults to 128 KiB.
35
     *
36
     * @var int
37
     */
38
    protected $fragmentSize = 128 * 1024;
39
40
    /**
41
     * Maximum inbound message length.
42
     *
43
     * Defaults to 10 MiB.
44
     *
45
     * @var int
46
     */
47
    protected $maxLength = 10 * 1024 * 1024;
48
49
    /**
50
     * The text message buffer when streaming is disabled.
51
     *
52
     * @var string
53
     */
54
    protected $text = '';
55
56
    public function __construct (WebSocketClient $client) {
57
        $this->client = $client;
58
    }
59
60
    /**
61
     * @return int
62
     */
63
    public function getFragmentSize (): int {
64
        return $this->fragmentSize;
65
    }
66
67
    /**
68
     * @return int
69
     */
70
    public function getMaxLength (): int {
71
        return $this->maxLength;
72
    }
73
74
    /**
75
     * When a `BINARY` frame is received.
76
     *
77
     * Throws by default.
78
     *
79
     * @param Frame $binary
80
     * @throws WebSocketError
81
     */
82
    protected function onBinary (Frame $binary): void {
83
        $this->binary .= $binary->getPayload();
84
        if ($binary->isFinal()) {
85
            $message = $this->binary;
86
            $this->binary = '';
87
            $this->client->getMessageHandler()->onBinary($message);
88
        }
89
    }
90
91
    /**
92
     * When a `CLOSE` frame is received.
93
     *
94
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
95
     * > If an endpoint receives a Close frame and did not previously send a
96
     * > Close frame, the endpoint MUST send a Close frame in response.  (When
97
     * > sending a Close frame in response, the endpoint typically echos the
98
     * > status code it received.)
99
     *
100
     * @param Frame $close
101
     */
102
    protected function onClose (Frame $close): void {
103
        $this->client->close($close->getCloseCode());
104
    }
105
106
    /**
107
     * When a `CONTINUE` data fragment is received.
108
     *
109
     * @param Frame $frame
110
     * @throws WebSocketError
111
     */
112
    protected function onContinue (Frame $frame): void {
113
        switch ($this->continue) {
114
            case Frame::OP_TEXT:
115
                $this->onText($frame);
116
                break;
117
            case Frame::OP_BINARY:
118
                $this->onBinary($frame);
119
                break;
120
            default:
121
                throw new WebSocketError(
122
                    Frame::CLOSE_PROTOCOL_ERROR,
123
                    "Received CONTINUE without a prior fragment.",
124
                    $frame
125
                );
126
        }
127
    }
128
129
    /**
130
     * When a control frame is received.
131
     *
132
     * https://tools.ietf.org/html/rfc6455#section-5.4
133
     * > Control frames (see Section 5.5) MAY be injected in the middle of
134
     * > a fragmented message.
135
     *
136
     * @param Frame $control
137
     */
138
    protected function onControl (Frame $control): void {
139
        if ($control->isClose()) {
140
            $this->onClose($control);
141
        }
142
        elseif ($control->isPing()) {
143
            $this->onPing($control);
144
        }
145
        elseif ($control->isPong()) {
146
            $this->onPong($control);
147
        }
148
    }
149
150
    /**
151
     * When an initial data frame (not `CONTINUE`) is received.
152
     *
153
     * @param Frame $data
154
     */
155
    protected function onData (Frame $data): void {
156
        if (!$data->isFinal()) {
157
            $this->onData_SetContinue($data);
158
        }
159
        if ($data->isText()) {
160
            $this->onText($data);
161
        }
162
        elseif ($data->isBinary()) {
163
            $this->onBinary($data);
164
        }
165
    }
166
167
    protected function onData_SetContinue (Frame $data): void {
168
        if ($this->continue) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->continue of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
169
            $existing = Frame::NAMES[$this->continue];
170
            throw new WebSocketError(
171
                Frame::CLOSE_PROTOCOL_ERROR,
172
                "Received interleaved {$data->getName()} against existing {$existing}",
173
                $data
174
            );
175
        }
176
        $this->continue = $data->getOpCode();
177
    }
178
179
    /**
180
     * Invoked by the client when a complete frame has been received.
181
     *
182
     * Delegates to the other handler methods using the control flow outlined in the RFC.
183
     *
184
     * @param Frame $frame
185
     */
186
    public function onFrame (Frame $frame): void {
187
        $this->onFrame_CheckRsv($frame);
188
        $this->onFrame_CheckLength($frame);
189
        if ($frame->isControl()) {
190
            $this->onControl($frame);
191
        }
192
        elseif ($frame->isContinue()) {
193
            $this->onContinue($frame);
194
        }
195
        else {
196
            $this->onData($frame);
197
        }
198
    }
199
200
    /**
201
     * @param Frame $frame
202
     */
203
    protected function onFrame_CheckLength (Frame $frame): void {
204
        if ($frame->isData()) {
205
            if ($frame->isBinary()) {
206
                $length = strlen($this->binary);
207
            }
208
            else {
209
                $length = strlen($this->text);
210
            }
211
            if ($length + $frame->getLength() > $this->maxLength) {
212
                throw new WebSocketError(
213
                    Frame::CLOSE_TOO_LARGE,
214
                    "Message would exceed {$this->maxLength} bytes",
215
                    $frame
216
                );
217
            }
218
        }
219
    }
220
221
    /**
222
     * Throws if unknown RSV bits are received.
223
     *
224
     * @param Frame $frame
225
     * @throws WebSocketError
226
     */
227
    protected function onFrame_CheckRsv (Frame $frame): void {
228
        if ($badRsv = $frame->getRsv() & ~$this->client->getHandshake()->getRsv()) {
229
            $badRsv = str_pad(base_convert($badRsv >> 4, 10, 2), 3, '0', STR_PAD_LEFT);
230
            throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received unknown RSV bits: 0b{$badRsv}");
231
        }
232
    }
233
234
    /**
235
     * When a `PING` frame is received.
236
     *
237
     * Automatically pongs the payload back by default.
238
     *
239
     * @param Frame $ping
240
     */
241
    protected function onPing (Frame $ping): void {
242
        $this->writePong($ping->getPayload());
243
    }
244
245
    /**
246
     * When a `PONG` frame is received.
247
     *
248
     * Does nothing by default.
249
     *
250
     * @param Frame $pong
251
     */
252
    protected function onPong (Frame $pong): void {
0 ignored issues
show
Unused Code introduced by
The parameter $pong is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

252
    protected function onPong (/** @scrutinizer ignore-unused */ Frame $pong): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
253
        // stub
254
    }
255
256
    /**
257
     * When a `BINARY` frame is received.
258
     *
259
     * Throws by default.
260
     *
261
     * @param Frame $text
262
     * @throws WebSocketError
263
     */
264
    protected function onText (Frame $text): void {
265
        $this->text .= $text->getPayload();
266
        if ($text->isFinal()) {
267
            $message = $this->text;
268
            $this->text = '';
269
            $this->client->getMessageHandler()->onText($message);
270
        }
271
    }
272
273
    /**
274
     * @param int $bytes
275
     * @return $this
276
     */
277
    public function setFragmentSize (int $bytes) {
278
        $this->fragmentSize = $bytes;
279
        return $this;
280
    }
281
282
    /**
283
     * @param int $bytes
284
     * @return $this
285
     */
286
    public function setMaxLength (int $bytes) {
287
        $this->maxLength = $bytes;
288
        return $this;
289
    }
290
291
    /**
292
     * Fragments data into frames and writes them to the peer.
293
     *
294
     * @param int $opCode
295
     * @param string $payload
296
     */
297
    public function write (int $opCode, string $payload): void {
298
        $offset = 0;
299
        $total = strlen($payload);
300
        do {
301
            $fragment = substr($payload, $offset, $this->fragmentSize);
302
            if ($offset) {
303
                $opCode = Frame::OP_CONTINUE;
304
            }
305
            $offset += strlen($fragment);
306
            $this->writeFrame($offset >= $total, $opCode, $fragment);
307
        } while ($offset < $total);
308
    }
309
310
    /**
311
     * @param string $payload
312
     */
313
    public function writeBinary (string $payload): void {
314
        $this->write(Frame::OP_BINARY, $payload);
315
    }
316
317
    /**
318
     * @param int $code
319
     * @param string $reason
320
     */
321
    public function writeClose (int $code = Frame::CLOSE_NORMAL, string $reason = ''): void {
322
        $this->writeFrame(true, Frame::OP_CLOSE, pack('n', $code) . $reason);
323
    }
324
325
    /**
326
     * Writes a single frame.
327
     *
328
     * @param bool $final
329
     * @param int $opCode
330
     * @param string $payload
331
     */
332
    protected function writeFrame (bool $final, int $opCode, string $payload): void {
333
        if ($opCode & 0x08 and !$final) {
334
            throw new WebSocketError(
335
                Frame::CLOSE_INTERNAL_ERROR,
336
                "Would have sent a fragmented control frame ({$opCode}) {$payload}"
337
            );
338
        }
339
        $head = chr($final ? 0x80 | $opCode : $opCode);
340
        $length = strlen($payload);
341
        if ($length > 65535) {
342
            $head .= chr(127);
343
            $head .= pack('J', $length);
344
        }
345
        elseif ($length >= 126) {
346
            $head .= chr(126);
347
            $head .= pack('n', $length);
348
        }
349
        else {
350
            $head .= chr($length);
351
        }
352
        $this->client->write($head . $payload);
353
    }
354
355
    /**
356
     * @param string $payload
357
     */
358
    public function writePing (string $payload = ''): void {
359
        $this->writeFrame(true, Frame::OP_PING, $payload);
360
    }
361
362
    /**
363
     * @param string $payload
364
     */
365
    public function writePong (string $payload = ''): void {
366
        $this->writeFrame(true, Frame::OP_PONG, $payload);
367
    }
368
369
    /**
370
     * @param string $payload
371
     */
372
    public function writeText (string $payload): void {
373
        $this->write(Frame::OP_TEXT, $payload);
374
    }
375
}