Passed
Push — master ( a91d54...875978 )
by y
02:08
created

FrameHandler::onBinary()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
nc 4
nop 1
dl 0
loc 14
rs 9.9
c 1
b 0
f 0
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
     * Opcode for the `CONTINUE` handler.
24
     *
25
     * @var int|null
26
     */
27
    protected $continue;
28
29
    /**
30
     * The text message buffer when streaming is disabled.
31
     *
32
     * @var string
33
     */
34
    protected $text = '';
35
36
    public function __construct (WebSocketClient $client) {
37
        $this->client = $client;
38
    }
39
40
    /**
41
     * When a `BINARY` frame is received.
42
     *
43
     * Throws by default.
44
     *
45
     * @param Frame $binary
46
     */
47
    protected function onBinary (Frame $binary): void {
48
        $handler = $this->client->getMessageHandler();
49
        if ($handler->isBinaryStream()) {
50
            $handler->onBinary($binary->getPayload());
51
        }
52
        else {
53
            if (strlen($this->binary) + $binary->getLength() > $handler->getMaxLength()) {
54
                throw new WebSocketError(Frame::CLOSE_TOO_LARGE, $handler->getMaxLength(), $binary);
55
            }
56
            $this->binary .= $binary->getPayload();
57
            if ($binary->isFinal()) {
58
                $message = $this->binary;
59
                $this->binary = '';
60
                $handler->onBinary($message);
61
            }
62
        }
63
    }
64
65
    /**
66
     * When a `CLOSE` frame is received.
67
     *
68
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
69
     * > If an endpoint receives a Close frame and did not previously send a
70
     * > Close frame, the endpoint MUST send a Close frame in response.  (When
71
     * > sending a Close frame in response, the endpoint typically echos the
72
     * > status code it received.)
73
     *
74
     * @param Frame $close
75
     */
76
    protected function onClose (Frame $close): void {
77
        $this->client->close($close->getCloseCode());
78
    }
79
80
    /**
81
     * When a `CONTINUE` data fragment is received.
82
     *
83
     * @param Frame $frame
84
     */
85
    protected function onContinue (Frame $frame): void {
86
        switch ($this->continue) {
87
            case Frame::OP_TEXT:
88
                $this->onText($frame);
89
                break;
90
            case Frame::OP_BINARY:
91
                $this->onBinary($frame);
92
                break;
93
            default:
94
                throw new WebSocketError(
95
                    Frame::CLOSE_PROTOCOL_ERROR,
96
                    "Received CONTINUE without a prior fragment.",
97
                    $frame
98
                );
99
        }
100
    }
101
102
    /**
103
     * When a control frame is received.
104
     *
105
     * https://tools.ietf.org/html/rfc6455#section-5.4
106
     * > Control frames (see Section 5.5) MAY be injected in the middle of
107
     * > a fragmented message.
108
     *
109
     * @param Frame $control
110
     */
111
    protected function onControl (Frame $control): void {
112
        if ($control->isClose()) {
113
            $this->onClose($control);
114
        }
115
        elseif ($control->isPing()) {
116
            $this->onPing($control);
117
        }
118
        elseif ($control->isPong()) {
119
            $this->onPong($control);
120
        }
121
    }
122
123
    /**
124
     * When an initial data frame (not `CONTINUE`) is received.
125
     *
126
     * @param Frame $data
127
     */
128
    protected function onData (Frame $data): void {
129
        if ($data->isText()) {
130
            $this->onText($data);
131
        }
132
        elseif ($data->isBinary()) {
133
            $this->onBinary($data);
134
        }
135
    }
136
137
    /**
138
     * Invoked by the client when a complete frame has been received.
139
     *
140
     * Delegates to the other handler methods using the control flow outlined in the RFC.
141
     *
142
     * @param Frame $frame
143
     */
144
    public function onFrame (Frame $frame): void {
145
        $this->onFrame_CheckRsv($frame);
146
        $frame->validate();
147
        if ($frame->isControl()) {
148
            $this->onControl($frame);
149
        }
150
        elseif ($frame->isContinue()) {
151
            $this->onContinue($frame);
152
        }
153
        else {
154
            $this->onData($frame);
155
        }
156
    }
157
158
    /**
159
     * Throws if unknown RSV bits are received.
160
     *
161
     * @param Frame $frame
162
     */
163
    protected function onFrame_CheckRsv (Frame $frame): void {
164
        if ($badRsv = $frame->getRsv() & ~$this->client->getHandshake()->getRsv()) {
165
            $badRsv = str_pad(base_convert($badRsv, 10, 2), 3, 0, STR_PAD_LEFT);
166
            throw new WebSocketError(Frame::CLOSE_PROTOCOL_ERROR, "Received unknown RSV: 0b{$badRsv}");
167
        }
168
    }
169
170
    /**
171
     * When a `PING` frame is received.
172
     *
173
     * Automatically pongs the payload back by default.
174
     *
175
     * @param Frame $ping
176
     */
177
    protected function onPing (Frame $ping): void {
178
        $this->writePong($ping->getPayload());
179
    }
180
181
    /**
182
     * When a `PONG` frame is received.
183
     *
184
     * Does nothing by default.
185
     *
186
     * @param Frame $pong
187
     */
188
    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

188
    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...
189
        // stub
190
    }
191
192
    /**
193
     * When a `BINARY` frame is received.
194
     *
195
     * Throws by default.
196
     *
197
     * @param Frame $text
198
     */
199
    protected function onText (Frame $text): void {
200
        $handler = $this->client->getMessageHandler();
201
        if ($handler->isTextStream()) {
202
            $handler->onText($text->getPayload());
203
        }
204
        else {
205
            if (strlen($this->text) + $text->getLength() > $handler->getMaxLength()) {
206
                throw new WebSocketError(Frame::CLOSE_TOO_LARGE, $handler->getMaxLength(), $text);
207
            }
208
            $this->text .= $text->getPayload();
209
            if ($text->isFinal()) {
210
                $message = $this->text;
211
                $this->text = '';
212
                $handler->onText($message);
213
            }
214
        }
215
    }
216
217
    /**
218
     * Writes a frame to the peer.
219
     *
220
     * @param string $payload
221
     * @param int $opCode
222
     * @param bool $final
223
     */
224
    public function write (string $payload, int $opCode = Frame::OP_TEXT, bool $final = true): void {
225
        $head = chr(($final ? 0x80 : 0) | $opCode);
226
        $length = strlen($payload);
227
        if ($length > 65535) {
228
            $head .= chr(127);
229
            $head .= pack('J', $length);
230
        }
231
        elseif ($length >= 126) {
232
            $head .= chr(126);
233
            $head .= pack('n', $length);
234
        }
235
        else {
236
            $head .= chr($length);
237
        }
238
        $this->client->write($head . $payload);
239
    }
240
241
    public function writeBinary (string $payload, bool $final = true): void {
242
        $this->write($payload, Frame::OP_BINARY, $final);
243
    }
244
245
    public function writePing (string $payload = ''): void {
246
        $this->write($payload, Frame::OP_PING);
247
    }
248
249
    public function writePong (string $payload = ''): void {
250
        $this->write($payload, Frame::OP_PONG);
251
    }
252
253
    public function writeText (string $payload, bool $final = true): void {
254
        $this->write($payload, Frame::OP_TEXT, $final);
255
    }
256
}