189 lines
5.7 KiB
PHP
189 lines
5.7 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace NoccyLabs\React\WebSocket;
|
||
|
|
||
|
use Evenement\EventEmitterTrait;
|
||
|
use NoccyLabs\React\WebSocket\Group\ConnectionGroup;
|
||
|
use NoccyLabs\React\WebSocket\Group\GroupManager;
|
||
|
use Psr\Http\Message\ServerRequestInterface;
|
||
|
use React\Http\Message\Response;
|
||
|
use React\Socket\ConnectionInterface;
|
||
|
use React\Stream\CompositeStream;
|
||
|
use React\Stream\DuplexStreamInterface;
|
||
|
use React\Stream\ReadableStreamInterface;
|
||
|
use React\Stream\ThroughStream;
|
||
|
use React\Stream\WritableStreamInterface;
|
||
|
|
||
|
class WebSocketCodec
|
||
|
{
|
||
|
|
||
|
const OP_CONTINUATION = 0x0;
|
||
|
const OP_FRAME_TEXT = 0x1;
|
||
|
const OP_FRAME_BINARY = 0x2;
|
||
|
const OP_CLOSE = 0x8;
|
||
|
const OP_PING = 0x9;
|
||
|
const OP_PONG = 0xA;
|
||
|
|
||
|
public function encode(array $frame): string
|
||
|
{
|
||
|
// Encoded frame
|
||
|
$encoded = null;
|
||
|
|
||
|
// Unpack frame with defaults
|
||
|
$frame = [
|
||
|
...[
|
||
|
'final' => true,
|
||
|
'opcode' => null,
|
||
|
'masked' => false,
|
||
|
'mask' => null,
|
||
|
'rsv1' => false,
|
||
|
'rsv2' => false,
|
||
|
'rsv3' => false,
|
||
|
'payload' => null
|
||
|
],
|
||
|
...$frame
|
||
|
];
|
||
|
|
||
|
$len = strlen($frame['payload']);
|
||
|
if ($len > 65535) {
|
||
|
$size0 = 127;
|
||
|
$size1 = ($len >> 24) & 0xFF;
|
||
|
$size2 = ($len >> 16) & 0xFF;
|
||
|
$size3 = ($len >> 8) & 0xFF;
|
||
|
$size4 = $len & 0xFF;
|
||
|
} elseif ($len > 126) {
|
||
|
$size0 = 126;
|
||
|
$size1 = ($len >> 8) & 0xFF;
|
||
|
$size2 = $len & 0xFF;
|
||
|
$size3 = null;
|
||
|
} else {
|
||
|
$size0 = $len;
|
||
|
$size1 = null;
|
||
|
$size3 = null;
|
||
|
}
|
||
|
|
||
|
$encoded .= chr(($frame['final']?0x80:0x00)
|
||
|
| ($frame['rsv1']?0x40:0x00)
|
||
|
| ($frame['rsv2']?0x20:0x00)
|
||
|
| ($frame['rsv3']?0x10:0x00)
|
||
|
| ($frame['opcode'] & 0xF));
|
||
|
$encoded .= chr(($frame['masked']?0x80:0x00)
|
||
|
| ($size0 & 0x7F));
|
||
|
if ($size1 !== null) {
|
||
|
$encoded .= chr($size1) . chr($size2);
|
||
|
}
|
||
|
if ($size3 !== null) {
|
||
|
$encoded .= chr($size3) . chr($size4);
|
||
|
}
|
||
|
|
||
|
if ($frame['masked'] === true) {
|
||
|
if ($frame['mask'] === null || strlen($frame['mask']) !== 4) {
|
||
|
$frame['mask'] = chr(mt_rand(0,255)).chr(mt_rand(0,255)).chr(mt_rand(0,255)).chr(mt_rand(0,255));
|
||
|
}
|
||
|
$encoded .= $frame['mask'];
|
||
|
$encoded .= $this->mask($frame['payload'], $frame['mask']);
|
||
|
} else {
|
||
|
$encoded .= $frame['payload'];
|
||
|
}
|
||
|
|
||
|
//$this->hexdump($encoded);
|
||
|
|
||
|
return $encoded;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Decode a websocket frame and return an array with the keys:
|
||
|
* opcode (int) - the opcode
|
||
|
* fin (bool) - final frame
|
||
|
* rsv1-3 (bool) - reserved bits
|
||
|
* masked (bool) - if the frame was masked
|
||
|
* length (int) - length of payload
|
||
|
* payload (string) - the payload
|
||
|
*/
|
||
|
public function decode($frame): array
|
||
|
{
|
||
|
// Decoded frame
|
||
|
$decoded = [];
|
||
|
|
||
|
// Keep track of the number of bytes in the header
|
||
|
$header = 2;
|
||
|
|
||
|
// Peek at the first byte, holding flags and opcode
|
||
|
$byte0 = ord($frame[0]);
|
||
|
$decoded['final'] = !!($byte0 & 0x80);
|
||
|
$decoded['opcode'] = $byte0 & 0x0F;
|
||
|
// Peek at the second byte, holding mask bit and len
|
||
|
$byte1 = ord($frame[1]);
|
||
|
$decoded['masked'] = $masked = !!($byte1 & 0x80);
|
||
|
$decoded['rsv1'] = !!($byte1 & 0x40);
|
||
|
$decoded['rsv2'] = !!($byte1 & 0x20);
|
||
|
$decoded['rsv3'] = !!($byte1 & 0x10);
|
||
|
$len = $byte1 & 0x7F;
|
||
|
|
||
|
// Read extended length if present
|
||
|
if ($len == 126) {
|
||
|
$len = (ord($frame[$header+0]) << 8)
|
||
|
| (ord($frame[$header+1]));
|
||
|
$header += 2;
|
||
|
} elseif ($len == 127) {
|
||
|
$len = (ord($frame[$header+0]) << 24)
|
||
|
| (ord($frame[$header+1]) << 16)
|
||
|
| (ord($frame[$header+2]) << 8)
|
||
|
| (ord($frame[$header+3]));
|
||
|
$header += 4;
|
||
|
}
|
||
|
|
||
|
// Now for the masking
|
||
|
if ($masked) {
|
||
|
$mask = substr($frame, $header, 4);
|
||
|
$header += 4;
|
||
|
}
|
||
|
|
||
|
// Extract and unmask payload
|
||
|
$payload = substr($frame, $header, $len);
|
||
|
if ($masked) {
|
||
|
$payload = $this->mask($payload, $mask);
|
||
|
$decoded['mask'] = $mask;
|
||
|
}
|
||
|
|
||
|
$decoded['length'] = $len;
|
||
|
$decoded['payload'] = $payload;
|
||
|
|
||
|
return $decoded;
|
||
|
}
|
||
|
|
||
|
private function mask(string $payload, string $mask): string
|
||
|
{
|
||
|
$payloadData = array_map("ord", str_split($payload,1));
|
||
|
$maskData = array_map("ord", str_split($mask,1));
|
||
|
|
||
|
//printf("Mask: %02x %02x %02x %02x\n", ...$maskData);
|
||
|
|
||
|
$unmasked = [];
|
||
|
for ($n = 0; $n < count($payloadData); $n++) {
|
||
|
$unmasked[] = $payloadData[$n] ^ $maskData[$n % 4];
|
||
|
}
|
||
|
return join("", array_map("chr", $unmasked));
|
||
|
}
|
||
|
|
||
|
private function hexdump($data): void
|
||
|
{
|
||
|
printf("%4d .\n", strlen($data));
|
||
|
$rows = str_split($data, 16);
|
||
|
$offs = 0;
|
||
|
foreach ($rows as $row) {
|
||
|
$h = []; $a = [];
|
||
|
for ($n = 0; $n < 16; $n++) {
|
||
|
if ($n < strlen($row)) {
|
||
|
$h[] = sprintf("%02x%s", ord($row[$n]), ($n==7)?" ":" ");
|
||
|
$a[] = sprintf("%s%s", (ctype_print($row[$n])?$row[$n]:"."), ($n==7)?" ":"");
|
||
|
} else {
|
||
|
$h[] = (($n==7)?" ":" ");
|
||
|
$a[] = (($n==7)?" ":" ");
|
||
|
}
|
||
|
}
|
||
|
printf("%04x | %s | %s\n", 16 * $offs++, join("", $h), join("", $a));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|