1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace WSSC\Components; |
4
|
|
|
|
5
|
|
|
use WSSC\Contracts\CommonsContract; |
6
|
|
|
use WSSC\Contracts\WscCommonsContract; |
7
|
|
|
use WSSC\Exceptions\BadOpcodeException; |
8
|
|
|
use WSSC\Exceptions\BadUriException; |
9
|
|
|
use WSSC\Exceptions\ConnectionException; |
10
|
|
|
|
11
|
|
|
class WscMain implements WscCommonsContract |
12
|
|
|
{ |
13
|
|
|
|
14
|
|
|
private $socket; |
15
|
|
|
private $isConnected = false; |
16
|
|
|
private $isClosing = false; |
17
|
|
|
private $lastOpcode; |
18
|
|
|
private $closeStatus; |
19
|
|
|
private $hugePayload; |
20
|
|
|
|
21
|
|
|
private static $opcodes = [ |
22
|
|
|
CommonsContract::EVENT_TYPE_CONTINUATION => 0, |
23
|
|
|
CommonsContract::EVENT_TYPE_TEXT => 1, |
24
|
|
|
CommonsContract::EVENT_TYPE_BINARY => 2, |
25
|
|
|
CommonsContract::EVENT_TYPE_CLOSE => 8, |
26
|
|
|
CommonsContract::EVENT_TYPE_PING => 9, |
27
|
|
|
CommonsContract::EVENT_TYPE_PONG => 10, |
28
|
|
|
]; |
29
|
|
|
|
30
|
|
|
protected $socketUrl = ''; |
31
|
|
|
protected $options = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @throws \InvalidArgumentException |
35
|
|
|
* @throws BadUriException |
36
|
|
|
* @throws ConnectionException |
37
|
|
|
* @throws \Exception |
38
|
|
|
*/ |
39
|
|
|
protected function connect() |
40
|
|
|
{ |
41
|
|
|
$urlParts = parse_url($this->socketUrl); |
42
|
|
|
|
43
|
|
|
$scheme = $urlParts['scheme']; |
44
|
|
|
$host = $urlParts['host']; |
45
|
|
|
$user = isset($urlParts['user']) ? $urlParts['user'] : ''; |
46
|
|
|
$pass = isset($urlParts['pass']) ? $urlParts['pass'] : ''; |
47
|
|
|
$port = isset($urlParts['port']) ? $urlParts['port'] : ($scheme === 'wss' ? 443 : 80); |
48
|
|
|
|
49
|
|
|
$pathWithQuery = $this->getPathWithQuery($urlParts); |
50
|
|
|
$hostUri = $this->getHostUri($scheme, $host); |
51
|
|
|
// Set the stream context options if they're already set in the config |
52
|
|
|
$context = $this->getStreamContext(); |
53
|
|
|
$this->socket = @stream_socket_client( |
54
|
|
|
$hostUri . ':' . $port, $errno, $errstr, $this->options['timeout'], STREAM_CLIENT_CONNECT, $context |
55
|
|
|
); |
56
|
|
|
if ($this->socket === false) { |
57
|
|
|
throw new ConnectionException( |
58
|
|
|
"Could not open socket to \"$host:$port\": $errstr ($errno)." |
59
|
|
|
); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
// Set timeout on the stream as well. |
63
|
|
|
stream_set_timeout($this->socket, $this->options['timeout']); |
64
|
|
|
|
65
|
|
|
// Generate the WebSocket key. |
66
|
|
|
$key = $this->generateKey(); |
67
|
|
|
$headers = [ |
68
|
|
|
'Host' => $host . ':' . $port, |
69
|
|
|
'User-Agent' => 'websocket-client-php', |
70
|
|
|
'Connection' => 'Upgrade', |
71
|
|
|
'Upgrade' => 'WebSocket', |
72
|
|
|
'Sec-WebSocket-Key' => $key, |
73
|
|
|
'Sec-Websocket-Version' => '13', |
74
|
|
|
]; |
75
|
|
|
|
76
|
|
|
// Handle basic authentication. |
77
|
|
|
if ($user || $pass) { |
78
|
|
|
$headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass) . "\r\n"; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
// Add and override with headers from options. |
82
|
|
|
if (isset($this->options['headers'])) { |
83
|
|
|
$headers = array_merge($headers, $this->options['headers']); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
$header = $this->getHeaders($pathWithQuery, $headers); |
87
|
|
|
|
88
|
|
|
// Send headers. |
89
|
|
|
$this->write($header); |
90
|
|
|
|
91
|
|
|
// Get server response header |
92
|
|
|
// @todo Handle version switching |
93
|
|
|
$this->validateResponse($scheme, $host, $pathWithQuery, $key); |
94
|
|
|
$this->isConnected = true; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param string $scheme |
99
|
|
|
* @param string $host |
100
|
|
|
* @return string |
101
|
|
|
* @throws BadUriException |
102
|
|
|
*/ |
103
|
|
|
private function getHostUri(string $scheme, string $host): string |
104
|
|
|
{ |
105
|
|
|
if (in_array($scheme, ['ws', 'wss'], true) === false) { |
106
|
|
|
throw new BadUriException( |
107
|
|
|
"Url should have scheme ws or wss, not '$scheme' from URI '$this->socketUrl' ." |
108
|
|
|
); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
return ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @param string $scheme |
116
|
|
|
* @param string $host |
117
|
|
|
* @param string $pathWithQuery |
118
|
|
|
* @param string $key |
119
|
|
|
* @throws ConnectionException |
120
|
|
|
*/ |
121
|
|
|
private function validateResponse(string $scheme, string $host, string $pathWithQuery, string $key) |
122
|
|
|
{ |
123
|
|
|
$response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n"); |
124
|
|
|
if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) { |
125
|
|
|
$address = $scheme . '://' . $host . $pathWithQuery; |
126
|
|
|
throw new ConnectionException( |
127
|
|
|
"Connection to '{$address}' failed: Server sent invalid upgrade response:\n" |
128
|
|
|
. $response |
129
|
|
|
); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
$keyAccept = trim($matches[1]); |
133
|
|
|
$expectedResonse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT))); |
134
|
|
|
if ($keyAccept !== $expectedResonse) { |
135
|
|
|
throw new ConnectionException('Server sent bad upgrade response.'); |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* @return mixed|resource |
141
|
|
|
* @throws \InvalidArgumentException |
142
|
|
|
*/ |
143
|
|
|
private function getStreamContext() |
144
|
|
|
{ |
145
|
|
|
if (isset($this->options['context'])) { |
146
|
|
|
// Suppress the error since we'll catch it below |
147
|
|
|
if (@get_resource_type($this->options['context']) === 'stream-context') { |
148
|
|
|
return $this->options['context']; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
throw new \InvalidArgumentException( |
152
|
|
|
"Stream context in \$options['context'] isn't a valid context" |
153
|
|
|
); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return stream_context_create(); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @param mixed $urlParts |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
private function getPathWithQuery($urlParts): string |
164
|
|
|
{ |
165
|
|
|
$path = isset($urlParts['path']) ? $urlParts['path'] : '/'; |
166
|
|
|
$query = isset($urlParts['query']) ? $urlParts['query'] : ''; |
167
|
|
|
$fragment = isset($urlParts['fragment']) ? $urlParts['fragment'] : ''; |
168
|
|
|
$pathWithQuery = $path; |
169
|
|
|
if (!empty($query)) { |
170
|
|
|
$pathWithQuery .= '?' . $query; |
171
|
|
|
} |
172
|
|
|
if (!empty($fragment)) { |
173
|
|
|
$pathWithQuery .= '#' . $fragment; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
return $pathWithQuery; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @param string $pathWithQuery |
181
|
|
|
* @param array $headers |
182
|
|
|
* @return string |
183
|
|
|
*/ |
184
|
|
|
private function getHeaders(string $pathWithQuery, array $headers): string |
185
|
|
|
{ |
186
|
|
|
return 'GET ' . $pathWithQuery . " HTTP/1.1\r\n" |
187
|
|
|
. implode( |
188
|
|
|
"\r\n", array_map( |
189
|
|
|
function ($key, $value) { |
190
|
|
|
return "$key: $value"; |
191
|
|
|
}, array_keys($headers), $headers |
192
|
|
|
) |
193
|
|
|
) |
194
|
|
|
. "\r\n\r\n"; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @return string |
199
|
|
|
*/ |
200
|
|
|
public function getLastOpcode(): string |
201
|
|
|
{ |
202
|
|
|
return $this->lastOpcode; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @return int |
207
|
|
|
*/ |
208
|
|
|
public function getCloseStatus(): int |
209
|
|
|
{ |
210
|
|
|
return $this->closeStatus; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* @return bool |
215
|
|
|
*/ |
216
|
|
|
public function isConnected(): bool |
217
|
|
|
{ |
218
|
|
|
return $this->isConnected; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* @param int $timeout |
223
|
|
|
* @param null $microSecs |
224
|
|
|
* @return WscMain |
225
|
|
|
*/ |
226
|
|
|
public function setTimeout(int $timeout, $microSecs = NULL): WscMain |
227
|
|
|
{ |
228
|
|
|
$this->options['timeout'] = $timeout; |
229
|
|
|
|
230
|
|
|
if ($this->socket && get_resource_type($this->socket) === 'stream') { |
231
|
|
|
stream_set_timeout($this->socket, $timeout, $microSecs); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return $this; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @param $fragmentSize |
239
|
|
|
* @return WscMain |
240
|
|
|
*/ |
241
|
|
|
public function setFragmentSize(int $fragmentSize): WscMain |
242
|
|
|
{ |
243
|
|
|
$this->options['fragment_size'] = $fragmentSize; |
244
|
|
|
|
245
|
|
|
return $this; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
public function getFragmentSize() |
249
|
|
|
{ |
250
|
|
|
return $this->options['fragment_size']; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
public function setHeaders(array $headers) |
254
|
|
|
{ |
255
|
|
|
$this->options['headers'] = $headers; |
256
|
|
|
|
257
|
|
|
return $this; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
public function send($payload, $opcode = CommonsContract::EVENT_TYPE_TEXT) |
261
|
|
|
{ |
262
|
|
|
if (!$this->isConnected) { |
263
|
|
|
$this->connect(); |
264
|
|
|
} |
265
|
|
|
if (array_key_exists($opcode, self::$opcodes) === false) { |
266
|
|
|
throw new BadOpcodeException("Bad opcode '$opcode'. Try 'text' or 'binary'."); |
267
|
|
|
} |
268
|
|
|
// record the length of the payload |
269
|
|
|
$payload_length = strlen($payload); |
270
|
|
|
|
271
|
|
|
$fragment_cursor = 0; |
272
|
|
|
// while we have data to send |
273
|
|
|
while ($payload_length > $fragment_cursor) { |
274
|
|
|
// get a fragment of the payload |
275
|
|
|
$sub_payload = substr($payload, $fragment_cursor, $this->options['fragment_size']); |
276
|
|
|
|
277
|
|
|
// advance the cursor |
278
|
|
|
$fragment_cursor += $this->options['fragment_size']; |
279
|
|
|
|
280
|
|
|
// is this the final fragment to send? |
281
|
|
|
$final = $payload_length <= $fragment_cursor; |
282
|
|
|
|
283
|
|
|
// send the fragment |
284
|
|
|
$this->sendFragment($final, $sub_payload, $opcode, true); |
285
|
|
|
|
286
|
|
|
// all fragments after the first will be marked a continuation |
287
|
|
|
$opcode = 'continuation'; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @param $final |
293
|
|
|
* @param $payload |
294
|
|
|
* @param $opcode |
295
|
|
|
* @param $masked |
296
|
|
|
* @throws ConnectionException |
297
|
|
|
* @throws \Exception |
298
|
|
|
*/ |
299
|
|
|
protected function sendFragment($final, $payload, $opcode, $masked) |
300
|
|
|
{ |
301
|
|
|
// Binary string for header. |
302
|
|
|
$frameHeadBin = ''; |
303
|
|
|
// Write FIN, final fragment bit. |
304
|
|
|
$frameHeadBin .= (bool)$final ? '1' : '0'; |
305
|
|
|
// RSV 1, 2, & 3 false and unused. |
|
|
|
|
306
|
|
|
$frameHeadBin .= '000'; |
307
|
|
|
// Opcode rest of the byte. |
308
|
|
|
$frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]); |
309
|
|
|
// Use masking? |
310
|
|
|
$frameHeadBin .= $masked ? '1' : '0'; |
311
|
|
|
|
312
|
|
|
// 7 bits of payload length... |
313
|
|
|
$payloadLen = strlen($payload); |
314
|
|
|
if ($payloadLen > self::MAX_BYTES_READ) { |
315
|
|
|
$frameHeadBin .= decbin(self::MASK_127); |
316
|
|
|
$frameHeadBin .= sprintf('%064b', $payloadLen); |
317
|
|
|
} else if ($payloadLen > self::MASK_125) { |
318
|
|
|
$frameHeadBin .= decbin(self::MASK_126); |
319
|
|
|
$frameHeadBin .= sprintf('%016b', $payloadLen); |
320
|
|
|
} else { |
321
|
|
|
$frameHeadBin .= sprintf('%07b', $payloadLen); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
$frame = ''; |
325
|
|
|
|
326
|
|
|
// Write frame head to frame. |
327
|
|
|
foreach (str_split($frameHeadBin, 8) as $binstr) { |
328
|
|
|
$frame .= chr(bindec($binstr)); |
329
|
|
|
} |
330
|
|
|
// Handle masking |
331
|
|
|
if ($masked) { |
332
|
|
|
// generate a random mask: |
333
|
|
|
$mask = ''; |
334
|
|
View Code Duplication |
for ($i = 0; $i < 4; $i++) { |
|
|
|
|
335
|
|
|
$mask .= chr(random_int(0, 255)); |
336
|
|
|
} |
337
|
|
|
$frame .= $mask; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
// Append payload to frame: |
341
|
|
View Code Duplication |
for ($i = 0; $i < $payloadLen; $i++) { |
|
|
|
|
342
|
|
|
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; |
|
|
|
|
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$this->write($frame); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
public function receive() |
349
|
|
|
{ |
350
|
|
|
if (!$this->isConnected) { |
351
|
|
|
$this->connect(); |
352
|
|
|
} |
353
|
|
|
$this->hugePayload = ''; |
354
|
|
|
|
355
|
|
|
$response = NULL; |
356
|
|
|
while (NULL === $response) { |
357
|
|
|
$response = $this->receiveFragment(); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
return $response; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* @return null|string |
365
|
|
|
* @throws BadOpcodeException |
366
|
|
|
* @throws ConnectionException |
367
|
|
|
*/ |
368
|
|
|
protected function receiveFragment() |
369
|
|
|
{ |
370
|
|
|
// Just read the main fragment information first. |
371
|
|
|
$data = $this->read(2); |
372
|
|
|
|
373
|
|
|
// Is this the final fragment? // Bit 0 in byte 0 |
374
|
|
|
/// @todo Handle huge payloads with multiple fragments. |
375
|
|
|
$final = (bool)(ord($data[0]) & 1 << 7); |
376
|
|
|
|
377
|
|
|
// Parse opcode |
378
|
|
|
$opcode_int = ord($data[0]) & 31; // Bits 4-7 |
379
|
|
|
$opcode_ints = array_flip(self::$opcodes); |
380
|
|
|
if (!array_key_exists($opcode_int, $opcode_ints)) { |
381
|
|
|
throw new ConnectionException("Bad opcode in websocket frame: $opcode_int"); |
382
|
|
|
} |
383
|
|
|
$opcode = $opcode_ints[$opcode_int]; |
384
|
|
|
|
385
|
|
|
// record the opcode if we are not receiving a continutation fragment |
386
|
|
|
if ($opcode !== 'continuation') { |
387
|
|
|
$this->lastOpcode = $opcode; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
$payloadLength = $this->getPayloadLength($data); |
391
|
|
|
$payload = $this->getPayloadData($data, $payloadLength); |
392
|
|
|
if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) { |
393
|
|
|
// Get the close status. |
394
|
|
|
if ($payloadLength >= 2) { |
395
|
|
|
$statusBin = $payload[0] . $payload[1]; |
396
|
|
|
$status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1]))); |
397
|
|
|
$this->closeStatus = $status; |
398
|
|
|
$payload = substr($payload, 2); |
399
|
|
|
|
400
|
|
|
if (!$this->isClosing) { |
401
|
|
|
$this->send($statusBin . 'Close acknowledged: ' . $status, |
402
|
|
|
CommonsContract::EVENT_TYPE_CLOSE); // Respond. |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
if ($this->isClosing) { |
407
|
|
|
$this->isClosing = false; // A close response, all done. |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
fclose($this->socket); |
411
|
|
|
$this->isConnected = false; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
if (!$final) { |
415
|
|
|
$this->hugePayload .= $payload; |
416
|
|
|
|
417
|
|
|
return NULL; |
418
|
|
|
} // this is the last fragment, and we are processing a huge_payload |
419
|
|
|
|
420
|
|
|
if ($this->hugePayload) { |
421
|
|
|
$payload = $this->hugePayload .= $payload; |
422
|
|
|
$this->hugePayload = NULL; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
return $payload; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* @param string $data |
430
|
|
|
* @param int $payloadLength |
431
|
|
|
* @return string |
432
|
|
|
* @throws ConnectionException |
433
|
|
|
*/ |
434
|
|
|
private function getPayloadData(string $data, int $payloadLength) |
435
|
|
|
{ |
436
|
|
|
// Masking? |
437
|
|
|
$mask = (bool)(ord($data[1]) >> 7); // Bit 0 in byte 1 |
438
|
|
|
$payload = ''; |
439
|
|
|
$maskingKey = ''; |
440
|
|
|
// Get masking key. |
441
|
|
|
if ($mask) { |
442
|
|
|
$maskingKey = $this->read(4); |
443
|
|
|
} |
444
|
|
|
// Get the actual payload, if any (might not be for e.g. close frames. |
445
|
|
|
if ($payloadLength > 0) { |
446
|
|
|
$data = $this->read($payloadLength); |
447
|
|
|
|
448
|
|
|
if ($mask) { |
449
|
|
|
// Unmask payload. |
450
|
|
|
for ($i = 0; $i < $payloadLength; $i++) { |
451
|
|
|
$payload .= ($data[$i] ^ $maskingKey[$i % 4]); |
452
|
|
|
} |
453
|
|
|
} else { |
454
|
|
|
$payload = $data; |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
return $payload; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* @param string $data |
463
|
|
|
* @return float|int |
464
|
|
|
* @throws ConnectionException |
465
|
|
|
*/ |
466
|
|
|
private function getPayloadLength(string $data) |
467
|
|
|
{ |
468
|
|
|
$payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1 |
469
|
|
|
if ($payloadLength > self::MASK_125) { |
470
|
|
|
if ($payloadLength === self::MASK_126) { |
471
|
|
|
$data = $this->read(2); // 126: Payload is a 16-bit unsigned int |
472
|
|
|
} else { |
473
|
|
|
$data = $this->read(8); // 127: Payload is a 64-bit unsigned int |
474
|
|
|
} |
475
|
|
|
$payloadLength = bindec(self::sprintB($data)); |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
return $payloadLength; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* Tell the socket to close. |
483
|
|
|
* |
484
|
|
|
* @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4 |
485
|
|
|
* @param string $message A closing message, max 125 bytes. |
486
|
|
|
* @return bool|null|string |
487
|
|
|
* @throws BadOpcodeException |
488
|
|
|
*/ |
489
|
|
|
public function close(int $status = 1000, string $message = 'ttfn') |
490
|
|
|
{ |
491
|
|
|
$statusBin = sprintf('%016b', $status); |
492
|
|
|
$status_str = ''; |
493
|
|
|
foreach (str_split($statusBin, 8) as $binstr) { |
494
|
|
|
$status_str .= chr(bindec($binstr)); |
495
|
|
|
} |
496
|
|
|
$this->send($status_str . $message, CommonsContract::EVENT_TYPE_CLOSE); |
497
|
|
|
$this->isClosing = true; |
498
|
|
|
|
499
|
|
|
return $this->receive(); // Receiving a close frame will close the socket now. |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* @param $data |
504
|
|
|
* @throws ConnectionException |
505
|
|
|
*/ |
506
|
|
|
protected function write(string $data) |
507
|
|
|
{ |
508
|
|
|
$written = fwrite($this->socket, $data); |
509
|
|
|
|
510
|
|
|
if ($written < strlen($data)) { |
511
|
|
|
throw new ConnectionException( |
512
|
|
|
"Could only write $written out of " . strlen($data) . ' bytes.' |
513
|
|
|
); |
514
|
|
|
} |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* @param int $len |
519
|
|
|
* @return string |
520
|
|
|
* @throws ConnectionException |
521
|
|
|
*/ |
522
|
|
|
protected function read(int $len): string |
523
|
|
|
{ |
524
|
|
|
$data = ''; |
525
|
|
|
while (($dataLen = strlen($data)) < $len) { |
526
|
|
|
$buff = fread($this->socket, $len - $dataLen); |
527
|
|
|
if ($buff === false) { |
528
|
|
|
$metadata = stream_get_meta_data($this->socket); |
529
|
|
|
throw new ConnectionException( |
530
|
|
|
'Broken frame, read ' . strlen($data) . ' of stated ' |
531
|
|
|
. $len . ' bytes. Stream state: ' |
532
|
|
|
. json_encode($metadata) |
533
|
|
|
); |
534
|
|
|
} |
535
|
|
|
if ($buff === '') { |
536
|
|
|
$metadata = stream_get_meta_data($this->socket); |
537
|
|
|
throw new ConnectionException( |
538
|
|
|
'Empty read; connection dead? Stream state: ' . json_encode($metadata) |
539
|
|
|
); |
540
|
|
|
} |
541
|
|
|
$data .= $buff; |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
return $data; |
545
|
|
|
} |
546
|
|
|
|
547
|
|
|
/** |
548
|
|
|
* Helper to convert a binary to a string of '0' and '1'. |
549
|
|
|
* |
550
|
|
|
* @param $string |
551
|
|
|
* @return string |
552
|
|
|
*/ |
553
|
|
|
protected static function sprintB(string $string): string |
554
|
|
|
{ |
555
|
|
|
$return = ''; |
556
|
|
|
$strLen = strlen($string); |
557
|
|
|
for ($i = 0; $i < $strLen; $i++) { |
558
|
|
|
$return .= sprintf('%08b', ord($string[$i])); |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
return $return; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Sec-WebSocket-Key generator |
566
|
|
|
* |
567
|
|
|
* @return string the 16 character length key |
568
|
|
|
* @throws \Exception |
569
|
|
|
*/ |
570
|
|
|
private function generateKey(): string |
571
|
|
|
{ |
572
|
|
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789'; |
573
|
|
|
$key = ''; |
574
|
|
|
$chLen = strlen($chars); |
575
|
|
|
for ($i = 0; $i < self::KEY_GEN_LENGTH; $i++) { |
576
|
|
|
$key .= $chars[random_int(0, $chLen - 1)]; |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
return base64_encode($key); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
} |
583
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.