Passed
Push — master ( b4e552...a91d54 )
by y
03:05
created

WebSocketServer::acceptHandshakeUpgrade()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Helix\Socket;
4
5
use RuntimeException;
6
7
/**
8
 * https://tools.ietf.org/html/rfc6455
9
 */
10
class WebSocketServer extends StreamServer implements ReactiveInterface {
11
12
    /**
13
     * @var WebSocketClient[]
14
     */
15
    protected $clients = [];
16
17
    /**
18
     * @var Reactor
19
     */
20
    protected $reactor;
21
22
    /**
23
     * @param $resource
24
     * @param Reactor $reactor
25
     */
26
    public function __construct ($resource, Reactor $reactor) {
27
        parent::__construct($resource);
28
        $reactor->add($this);
29
        $this->reactor = $reactor;
30
    }
31
32
    /**
33
     * Verifies and accepts a websocket connection.
34
     *
35
     * https://tools.ietf.org/html/rfc6455#section-4.2
36
     * @return WebSocketClient
37
     */
38
    public function accept () {
39
        if (!$resource = @socket_accept($this->resource)) {
40
            throw new Error($this->resource); // reliable errno
41
        }
42
        $client = $this->newClient($resource);
43
        $this->acceptHandshake($client);
44
        $this->clients[$client->getId()] = $client;
45
        $this->reactor->add($client);
46
        $this->onAccept($client);
47
        return $client;
48
    }
49
50
    /**
51
     * @param WebSocketClient $client
52
     */
53
    protected function acceptHandshake (WebSocketClient $client) {
54
        $head = $client->await(self::CH_READ)->recv(4096); // todo client->readUntil(string,maxlength)
55
        $head = explode("\r\n", $head);
56
        $method = array_shift($head);
57
        $this->acceptHandshakeMethod($client, $method);
58
59
        $headers = [];
60
        foreach ($head as $header) {
61
            if (strlen($header)) {
62
                [$key, $value] = explode(':', $header, 2);
63
                $key = strtolower(trim($key));
64
                $value = trim($value);
65
                $headers[$key] = $value;
66
            }
67
        }
68
69
        $this->acceptHandshakeHost($client, $headers['host'] ?? '');
70
        $this->acceptHandshakeOrigin($client, $headers['origin'] ?? '');
71
        $this->acceptHandshakeConnection($client, $headers['connection'] ?? '');
72
        $this->acceptHandshakeUpgrade($client, $headers['upgrade'] ?? '');
73
        $this->acceptHandshakeVersion($client, $headers['sec-websocket-version'] ?? '');
74
        $base64Key = $headers['sec-websocket-key'] ?? '';
75
        $this->acceptHandshakeKey($client, $base64Key);
76
77
        $response = [
78
            "HTTP/1.1 101 Switching Protocols",
79
            "Upgrade: websocket",
80
            "Connection: Upgrade",
81
            "Sec-WebSocket-Accept: " . base64_encode(sha1($base64Key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)),
82
        ];
83
84
        $client->write(implode("\r\n", $response) . "\r\n\r\n");
85
    }
86
87
    /**
88
     * @param WebSocketClient $client
89
     * @param string $connection
90
     */
91
    protected function acceptHandshakeConnection (WebSocketClient $client, string $connection) {
92
        if (!preg_match('/^upgrade$/i', $connection)) {
93
            $this->reject($client, "Expected: Connection: Upgrade");
94
        }
95
    }
96
97
    /**
98
     * Stub.
99
     *
100
     * @param WebSocketClient $client
101
     * @param string $host
102
     */
103
    protected function acceptHandshakeHost (WebSocketClient $client, string $host) {
0 ignored issues
show
Unused Code introduced by
The parameter $client 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

103
    protected function acceptHandshakeHost (/** @scrutinizer ignore-unused */ WebSocketClient $client, string $host) {

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...
Unused Code introduced by
The parameter $host 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

103
    protected function acceptHandshakeHost (WebSocketClient $client, /** @scrutinizer ignore-unused */ string $host) {

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...
104
105
    }
106
107
    /**
108
     * @param WebSocketClient $client
109
     * @param string $base64Key
110
     */
111
    protected function acceptHandshakeKey (WebSocketClient $client, string $base64Key) {
112
        $key = base64_decode($base64Key);
113
        if ($key === false or strlen($key) !== 16) {
114
            $this->reject($client, "Expected Sec-WebSocket-Key");
115
        }
116
    }
117
118
    /**
119
     * @param WebSocketClient $client
120
     * @param string $method
121
     */
122
    protected function acceptHandshakeMethod (WebSocketClient $client, string $method) {
123
        if (!preg_match('/HTTP\/1\.1$/i', $method)) {
124
            $this->reject($client, "Expected: HTTP/1.1");
125
        }
126
    }
127
128
    /**
129
     * Stub.
130
     *
131
     * @param WebSocketClient $client
132
     * @param string $origin
133
     */
134
    protected function acceptHandshakeOrigin (WebSocketClient $client, string $origin) {
0 ignored issues
show
Unused Code introduced by
The parameter $origin 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

134
    protected function acceptHandshakeOrigin (WebSocketClient $client, /** @scrutinizer ignore-unused */ string $origin) {

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...
Unused Code introduced by
The parameter $client 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

134
    protected function acceptHandshakeOrigin (/** @scrutinizer ignore-unused */ WebSocketClient $client, string $origin) {

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...
135
136
    }
137
138
    /**
139
     * @param WebSocketClient $client
140
     * @param string $upgrade
141
     */
142
    protected function acceptHandshakeUpgrade (WebSocketClient $client, string $upgrade) {
143
        if (!preg_match('/^websocket$/i', $upgrade)) {
144
            $this->reject($client, "Expected Upgrade: websocket");
145
        }
146
    }
147
148
    /**
149
     * @param WebSocketClient $client
150
     * @param string $version
151
     */
152
    protected function acceptHandshakeVersion (WebSocketClient $client, string $version) {
153
        if ($version !== '13') {
154
            $this->reject($client, "Expected Sec-WebSocket-Version: 13");
155
        }
156
    }
157
158
    /**
159
     * @return WebSocketClient[]
160
     */
161
    public function getClients () {
162
        return $this->clients;
163
    }
164
165
    /**
166
     * @param resource $resource
167
     * @return WebSocketClient
168
     */
169
    protected function newClient ($resource) {
170
        return new WebSocketClient($resource, $this);
171
    }
172
173
    /**
174
     * WebSockets do not use the out-of-band channel.
175
     *
176
     * @inheritDoc
177
     */
178
    final public function onOutOfBand () {
179
        // do nothing
180
    }
181
182
    /**
183
     * @inheritDoc
184
     */
185
    public function onReadable () {
186
        $this->accept();
187
    }
188
189
    /**
190
     * Stub.
191
     *
192
     * @param WebSocketClient $client
193
     * @param string $reason
194
     * @throws RuntimeException
195
     */
196
    protected function reject (WebSocketClient $client, string $reason) {
197
        throw new RuntimeException("Rejected {$client}: {$reason}");
198
    }
199
200
    /**
201
     * Removes the client from the server and reactor.
202
     *
203
     * @param WebSocketClient $client
204
     * @return $this
205
     */
206
    public function remove (WebSocketClient $client) {
207
        unset($this->clients[$client->getId()]);
208
        $this->reactor->remove($client->getId());
209
        return $this;
210
    }
211
}