Moved frame logic to WebSocketCodec
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,3 @@
 | 
				
			|||||||
/vendor/
 | 
					/vendor/
 | 
				
			||||||
/composer.lock
 | 
					/composer.lock
 | 
				
			||||||
 | 
					/.phpunit.*
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,5 +16,8 @@
 | 
				
			|||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "require": {
 | 
					    "require": {
 | 
				
			||||||
        "react/http": "^1.9.0"
 | 
					        "react/http": "^1.9.0"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "require-dev": {
 | 
				
			||||||
 | 
					        "phpunit/phpunit": "^11.0"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								phpunit.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								phpunit.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
 | 
				
			||||||
 | 
					         bootstrap="vendor/autoload.php"
 | 
				
			||||||
 | 
					         cacheDirectory=".phpunit.cache"
 | 
				
			||||||
 | 
					         executionOrder="depends,defects"
 | 
				
			||||||
 | 
					         requireCoverageMetadata="true"
 | 
				
			||||||
 | 
					         beStrictAboutCoverageMetadata="true"
 | 
				
			||||||
 | 
					         beStrictAboutOutputDuringTests="true"
 | 
				
			||||||
 | 
					         failOnRisky="true"
 | 
				
			||||||
 | 
					         failOnWarning="true">
 | 
				
			||||||
 | 
					    <testsuites>
 | 
				
			||||||
 | 
					        <testsuite name="default">
 | 
				
			||||||
 | 
					            <directory>tests</directory>
 | 
				
			||||||
 | 
					        </testsuite>
 | 
				
			||||||
 | 
					    </testsuites>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
 | 
				
			||||||
 | 
					        <include>
 | 
				
			||||||
 | 
					            <directory>src</directory>
 | 
				
			||||||
 | 
					        </include>
 | 
				
			||||||
 | 
					    </source>
 | 
				
			||||||
 | 
					</phpunit>
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/WebSocketCodec.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/WebSocketCodec.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
				
			|||||||
 | 
					<?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));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -27,6 +27,8 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private ?string $groupName = null;
 | 
					    private ?string $groupName = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private WebSocketCodec $codec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ?ConnectionGroup $group = null;
 | 
					    private ?ConnectionGroup $group = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private GroupManager $groupManager;
 | 
					    private GroupManager $groupManager;
 | 
				
			||||||
@@ -43,6 +45,9 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function __construct(ServerRequestInterface $request, ReadableStreamInterface $inStream, WritableStreamInterface $outStream, GroupManager $groupManager)
 | 
					    public function __construct(ServerRequestInterface $request, ReadableStreamInterface $inStream, WritableStreamInterface $outStream, GroupManager $groupManager)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        // The codec is used to encode and decode frames
 | 
				
			||||||
 | 
					        $this->codec = new WebSocketCodec();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        $this->request = $request;
 | 
					        $this->request = $request;
 | 
				
			||||||
        $this->inStream = $inStream;
 | 
					        $this->inStream = $inStream;
 | 
				
			||||||
        $this->outStream = $outStream;
 | 
					        $this->outStream = $outStream;
 | 
				
			||||||
@@ -54,42 +59,10 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
    private function onWebSocketData($data)
 | 
					    private function onWebSocketData($data)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Keep track of the number of bytes in the header
 | 
					        $decoded = $this->codec->decode($data);
 | 
				
			||||||
        $header = 2;
 | 
					        $opcode = $decoded['opcode'];
 | 
				
			||||||
 | 
					        $final = $decoded['final'];
 | 
				
			||||||
        // Peek at the first byte, holding flags and opcode
 | 
					        $payload = $decoded['payload'];
 | 
				
			||||||
        $byte0 = ord($data[0]);
 | 
					 | 
				
			||||||
        $final = !!($byte0 & 0x80);
 | 
					 | 
				
			||||||
        $opcode = $byte0 & 0x0F;
 | 
					 | 
				
			||||||
        // Peek at the second byte, holding mask bit and len
 | 
					 | 
				
			||||||
        $byte1 = ord($data[1]);
 | 
					 | 
				
			||||||
        $masked = !!($byte1 & 0x80);
 | 
					 | 
				
			||||||
        $len = $byte1 & 0x7F;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Read extended length if present
 | 
					 | 
				
			||||||
        if ($len == 126) {
 | 
					 | 
				
			||||||
            $len = (ord($data[$header+0]) << 8) 
 | 
					 | 
				
			||||||
                 | (ord($data[$header+1]));
 | 
					 | 
				
			||||||
            $header += 2;
 | 
					 | 
				
			||||||
        } elseif ($len == 127) {
 | 
					 | 
				
			||||||
            $len = (ord($data[$header+0]) << 24) 
 | 
					 | 
				
			||||||
                 | (ord($data[$header+1]) << 16) 
 | 
					 | 
				
			||||||
                 | (ord($data[$header+2]) << 8) 
 | 
					 | 
				
			||||||
                 | (ord($data[$header+3]));
 | 
					 | 
				
			||||||
            $header += 4;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Now for the masking
 | 
					 | 
				
			||||||
        if ($masked) {
 | 
					 | 
				
			||||||
            $mask = substr($data, $header, 4);
 | 
					 | 
				
			||||||
            $header += 4;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Extract and unmask payload
 | 
					 | 
				
			||||||
        $payload = substr($data, $header, $len);
 | 
					 | 
				
			||||||
        if ($masked) {
 | 
					 | 
				
			||||||
            $payload = $this->unmask($payload, $mask);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (!$final) {
 | 
					        if (!$final) {
 | 
				
			||||||
            if ($this->bufferedOp === null) {
 | 
					            if ($this->bufferedOp === null) {
 | 
				
			||||||
@@ -111,7 +84,12 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        switch ($opcode) {
 | 
					        switch ($opcode) {
 | 
				
			||||||
            case self::OP_PING:
 | 
					            case self::OP_PING:
 | 
				
			||||||
                $this->sendPong();
 | 
					                $this->sendPong($payload);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            case self::OP_PONG:
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            case self::OP_CLOSE:
 | 
				
			||||||
 | 
					                // TODO implement
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            case self::OP_CONTINUATION:
 | 
					            case self::OP_CONTINUATION:
 | 
				
			||||||
                $this->buffer .= $payload;
 | 
					                $this->buffer .= $payload;
 | 
				
			||||||
@@ -125,24 +103,9 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function unmask(string $payload, string $mask): string
 | 
					    private function sendPong(string $data): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $payloadData = array_map("ord", str_split($payload,1));
 | 
					        $this->send(self::OP_PONG, $data, true);
 | 
				
			||||||
        $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];
 | 
					 | 
				
			||||||
            //printf("%02x ^ %02x = %02x %s\n", $payloadData[$n], $maskData[$n%4], $payloadData[$n]^$maskData[$n%4], chr($payloadData[$n]^$maskData[$n%4]));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return join("", array_map("chr", $unmasked));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private function sendPong(): void
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function setGroup(?string $name): void
 | 
					    public function setGroup(?string $name): void
 | 
				
			||||||
@@ -221,17 +184,15 @@ class WebSocketConnection implements WebSocketInterface
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function send(int $opcode, string $data, bool $final = true)
 | 
					    public function send(int $opcode, string $data, bool $final = true, bool $masked = false)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $frame = chr(($final?0x80:0x00) | ($opcode & 0xF));
 | 
					
 | 
				
			||||||
        
 | 
					        $frame = $this->codec->encode([
 | 
				
			||||||
        $len = strlen($data);
 | 
					            'opcode' => $opcode,
 | 
				
			||||||
        if ($len > 126) {
 | 
					            'payload' => $data,
 | 
				
			||||||
            $frame .= chr(0x7E) . chr(($len >> 8) & 0xFF) . chr($len & 0xFF);
 | 
					            'final' => $final,
 | 
				
			||||||
        } else {
 | 
					            'masked' => $masked
 | 
				
			||||||
            $frame .= chr($len);
 | 
					        ]);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        $frame .= $data;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->outStream->write($frame);
 | 
					        $this->outStream->write($frame);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										100
									
								
								tests/WebSocketCodecTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								tests/WebSocketCodecTest.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace NoccyLabs\React\WebSocket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use PHPUnit\Framework\Attributes\CoversClass;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[CoversClass(WebSocketCodec::class)]
 | 
				
			||||||
 | 
					class WebSocketCodecTest extends \PHPUnit\Framework\TestCase
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testEncodingFrames()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $codec = new WebSocketCodec();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->encode([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_PING,
 | 
				
			||||||
 | 
					            'payload'=>"ping"
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $this->assertEquals("\x89\x04ping", $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->encode([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh"]);
 | 
				
			||||||
 | 
					        $this->assertEquals("\x81\x08abcdefgh", $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->encode([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh",
 | 
				
			||||||
 | 
					            'masked'=>true,
 | 
				
			||||||
 | 
					            'mask'=>"\x00\x00\x00\x00"
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $this->assertEquals("\x81\x88\x00\x00\x00\x00abcdefgh", $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->encode([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh",
 | 
				
			||||||
 | 
					            'masked'=>true,
 | 
				
			||||||
 | 
					            'mask'=>"\x00\xFF\x00\xFF"
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        $this->assertEquals("\x81\x88\x00\xFF\x00\xFFa\x9dc\x9be\x99g\x97", $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function testDecodingFrames()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $codec = new WebSocketCodec();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->decode("\x89\x04ping");
 | 
				
			||||||
 | 
					        $this->assertEquals([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_PING,
 | 
				
			||||||
 | 
					            'payload'=>"ping",
 | 
				
			||||||
 | 
					            'final'=>true,
 | 
				
			||||||
 | 
					            'rsv1'=>false,
 | 
				
			||||||
 | 
					            'rsv2'=>false,
 | 
				
			||||||
 | 
					            'rsv3'=>false,
 | 
				
			||||||
 | 
					            'length'=>4,
 | 
				
			||||||
 | 
					            'masked'=>false
 | 
				
			||||||
 | 
					        ], $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->decode("\x81\x08abcdefgh");
 | 
				
			||||||
 | 
					        $this->assertEquals([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh",
 | 
				
			||||||
 | 
					            'final'=>true,
 | 
				
			||||||
 | 
					            'rsv1'=>false,
 | 
				
			||||||
 | 
					            'rsv2'=>false,
 | 
				
			||||||
 | 
					            'rsv3'=>false,
 | 
				
			||||||
 | 
					            'length'=>8,
 | 
				
			||||||
 | 
					            'masked'=>false
 | 
				
			||||||
 | 
					        ], $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->decode("\x81\x88\x00\x00\x00\x00abcdefgh");
 | 
				
			||||||
 | 
					        $this->assertEquals([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh",
 | 
				
			||||||
 | 
					            'final'=>true,
 | 
				
			||||||
 | 
					            'rsv1'=>false,
 | 
				
			||||||
 | 
					            'rsv2'=>false,
 | 
				
			||||||
 | 
					            'rsv3'=>false,
 | 
				
			||||||
 | 
					            'length'=>8,
 | 
				
			||||||
 | 
					            'masked'=>true,
 | 
				
			||||||
 | 
					            'mask'=>"\x00\x00\x00\x00"
 | 
				
			||||||
 | 
					        ], $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $msg = $codec->decode("\x81\x88\x00\xFF\x00\xFFa\x9dc\x9be\x99g\x97");
 | 
				
			||||||
 | 
					        $this->assertEquals([
 | 
				
			||||||
 | 
					            'opcode'=>WebSocketCodec::OP_FRAME_TEXT,
 | 
				
			||||||
 | 
					            'payload'=>"abcdefgh",
 | 
				
			||||||
 | 
					            'final'=>true,
 | 
				
			||||||
 | 
					            'rsv1'=>false,
 | 
				
			||||||
 | 
					            'rsv2'=>false,
 | 
				
			||||||
 | 
					            'rsv3'=>false,
 | 
				
			||||||
 | 
					            'length'=>8,
 | 
				
			||||||
 | 
					            'masked'=>true,
 | 
				
			||||||
 | 
					            'mask'=>"\x00\xFF\x00\xFF"
 | 
				
			||||||
 | 
					        ], $msg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user