Completed
Branch master (ec9c11)
by y
01:23
created

FrameHandler::onFrame_CheckLength()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 4
nc 5
nop 1
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 View Code Duplication
    protected function onBinary (Frame $binary): void {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 zero. 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.

This check looks from 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 View Code Duplication
    protected function onText (Frame $text): void {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
}