Subir todo el proyecto incluyendo vendor y dependencias

This commit is contained in:
Admin
2026-01-16 20:33:13 -06:00
parent cf8ecfcf64
commit 0b3d76822d
2394 changed files with 2382358 additions and 40 deletions

View File

@@ -0,0 +1,38 @@
name: CI
on:
push:
pull_request:
jobs:
PHPUnit:
name: PHPUnit (PHP ${{ matrix.php }})(${{ matrix.env }}) on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-22.04
env:
- client
- server
php:
- 7.4
- 8.0
- 8.1
- 8.2
- 8.3
- 8.4
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- run: docker pull crossbario/autobahn-testsuite
- run: composer install
- run: sh tests/ab/run_ab_tests.sh
env:
ABTEST: ${{ matrix.env }}
- run: vendor/bin/phpunit --verbose

5
vendor/ratchet/rfc6455/.gitignore vendored Executable file
View File

@@ -0,0 +1,5 @@
.phpunit.result.cache
composer.lock
vendor
tests/ab/reports
reports

19
vendor/ratchet/rfc6455/LICENSE vendored Executable file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 Chris Boden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

13
vendor/ratchet/rfc6455/README.md vendored Executable file
View File

@@ -0,0 +1,13 @@
# RFC6455 - The WebSocket Protocol
[![Build Status](https://github.com/ratchetphp/RFC6455/workflows/CI/badge.svg)](https://github.com/ratchetphp/RFC6455/actions)
[![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg)](http://socketo.me/reports/rfc-server/index.html)
This library a protocol handler for the RFC6455 specification.
It contains components for both server and client side handshake and messaging protocol negotation.
Aspects that are left open to interpretation in the specification are also left open in this library.
It is up to the implementation to determine how those interpretations are to be dealt with.
This library is independent, framework agnostic, and does not deal with any I/O.
HTTP upgrade negotiation integration points are handled with PSR-7 interfaces.

48
vendor/ratchet/rfc6455/composer.json vendored Executable file
View File

@@ -0,0 +1,48 @@
{
"name": "ratchet/rfc6455",
"type": "library",
"description": "RFC6455 WebSocket protocol handler",
"keywords": ["WebSockets", "websocket", "RFC6455"],
"homepage": "http://socketo.me",
"license": "MIT",
"authors": [
{
"name": "Chris Boden"
, "email": "cboden@gmail.com"
, "role": "Developer"
},
{
"name": "Matt Bonneau",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/ratchetphp/RFC6455/issues",
"chat": "https://gitter.im/reactphp/reactphp"
},
"autoload": {
"psr-4": {
"Ratchet\\RFC6455\\": "src"
}
},
"require": {
"php": ">=7.4",
"psr/http-factory-implementation": "^1.0",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"react/socket": "^1.3",
"guzzlehttp/psr7": "^2.7"
},
"scripts": {
"abtest-client": "ABTEST=client && sh tests/ab/run_ab_tests.sh",
"abtest-server": "ABTEST=server && sh tests/ab/run_ab_tests.sh",
"phpunit": "phpunit --colors=always",
"test": [
"@abtest-client",
"@abtest-server",
"@phpunit"
]
}
}

22
vendor/ratchet/rfc6455/phpunit.xml.dist vendored Executable file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
forceCoversAnnotation="true"
bootstrap="tests/bootstrap.php"
colors="true"
backupGlobals="false"
backupStaticAttributes="false"
stopOnError="false"
>
<testsuites>
<testsuite name="tests">
<directory>./tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
</phpunit>

View File

@@ -0,0 +1,68 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\RequestFactoryInterface;
class ClientNegotiator {
private ResponseVerifier $verifier;
private RequestInterface $defaultHeader;
private RequestFactoryInterface $requestFactory;
public function __construct(
RequestFactoryInterface $requestFactory,
?PermessageDeflateOptions $perMessageDeflateOptions = null
) {
$this->verifier = new ResponseVerifier;
$this->requestFactory = $requestFactory;
$this->defaultHeader = $this->requestFactory
->createRequest('GET', '')
->withHeader('Connection' , 'Upgrade')
->withHeader('Upgrade' , 'websocket')
->withHeader('Sec-WebSocket-Version', $this->getVersion())
->withHeader('User-Agent' , 'Ratchet');
$perMessageDeflateOptions ??= PermessageDeflateOptions::createDisabled();
// https://bugs.php.net/bug.php?id=73373
// https://bugs.php.net/bug.php?id=74240 - need >=7.1.4 or >=7.0.18
if ($perMessageDeflateOptions->isEnabled() && !PermessageDeflateOptions::permessageDeflateSupported()) {
trigger_error('permessage-deflate is being disabled because it is not supported by your PHP version.', E_USER_NOTICE);
$perMessageDeflateOptions = PermessageDeflateOptions::createDisabled();
}
if ($perMessageDeflateOptions->isEnabled() && !function_exists('deflate_add')) {
trigger_error('permessage-deflate is being disabled because you do not have the zlib extension.', E_USER_NOTICE);
$perMessageDeflateOptions = PermessageDeflateOptions::createDisabled();
}
$this->defaultHeader = $perMessageDeflateOptions->addHeaderToRequest($this->defaultHeader);
}
public function generateRequest(UriInterface $uri): RequestInterface {
return $this->defaultHeader->withUri($uri)
->withHeader('Sec-WebSocket-Key', $this->generateKey());
}
public function validateResponse(RequestInterface $request, ResponseInterface $response): bool {
return $this->verifier->verifyAll($request, $response);
}
public function generateKey(): string {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwzyz1234567890+/=';
$charRange = strlen($chars) - 1;
$key = '';
for ($i = 0; $i < 16; $i++) {
$key .= $chars[mt_rand(0, $charRange)];
}
return base64_encode($key);
}
public function getVersion(): int {
return 13;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Ratchet\RFC6455\Handshake;
class InvalidPermessageDeflateOptionsException extends \Exception
{
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A standard interface for interacting with the various version of the WebSocket protocol
* @todo Look in to extension support
*/
interface NegotiatorInterface {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* Given an HTTP header, determine if this version should handle the protocol
* @param RequestInterface $request
* @return bool
*/
public function isProtocol(RequestInterface $request): bool;
/**
* Although the version has a name associated with it the integer returned is the proper identification
* @return int
*/
public function getVersionNumber(): int;
/**
* Perform the handshake and return the response headers
* @param RequestInterface $request
* @return ResponseInterface
*/
public function handshake(RequestInterface $request): ResponseInterface;
/**
* Add supported protocols. If the request has any matching the response will include one
* @param array $protocols
*/
public function setSupportedSubProtocols(array $protocols): void;
/**
* If enabled and support for a subprotocol has been added handshake
* will not upgrade if a match between request and supported subprotocols
* @param boolean $enable
* @todo Consider extending this interface and moving this there.
* The spec does say the server can fail for this reason, but
* it is not a requirement. This is an implementation detail.
*/
public function setStrictSubProtocolCheck(bool $enable): void;
}

View File

@@ -0,0 +1,269 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class PermessageDeflateOptions
{
public const MAX_WINDOW_BITS = 15;
private const VALID_BITS = [8, 9, 10, 11, 12, 13, 14, 15];
private bool $deflateEnabled = false;
private ?bool $server_no_context_takeover = null;
private ?bool $client_no_context_takeover = null;
private ?int $server_max_window_bits = null;
private ?int $client_max_window_bits = null;
private function __construct() { }
public static function createEnabled() {
$new = new self();
$new->deflateEnabled = true;
$new->client_max_window_bits = self::MAX_WINDOW_BITS;
$new->client_no_context_takeover = false;
$new->server_max_window_bits = self::MAX_WINDOW_BITS;
$new->server_no_context_takeover = false;
return $new;
}
public static function createDisabled() {
return new self();
}
public function withClientNoContextTakeover(): self {
$new = clone $this;
$new->client_no_context_takeover = true;
return $new;
}
public function withoutClientNoContextTakeover(): self {
$new = clone $this;
$new->client_no_context_takeover = false;
return $new;
}
public function withServerNoContextTakeover(): self {
$new = clone $this;
$new->server_no_context_takeover = true;
return $new;
}
public function withoutServerNoContextTakeover(): self {
$new = clone $this;
$new->server_no_context_takeover = false;
return $new;
}
public function withServerMaxWindowBits(int $bits = self::MAX_WINDOW_BITS): self {
if (!in_array($bits, self::VALID_BITS)) {
throw new \Exception('server_max_window_bits must have a value between 8 and 15.');
}
$new = clone $this;
$new->server_max_window_bits = $bits;
return $new;
}
public function withClientMaxWindowBits(int $bits = self::MAX_WINDOW_BITS): self {
if (!in_array($bits, self::VALID_BITS)) {
throw new \Exception('client_max_window_bits must have a value between 8 and 15.');
}
$new = clone $this;
$new->client_max_window_bits = $bits;
return $new;
}
/**
* https://tools.ietf.org/html/rfc6455#section-9.1
* https://tools.ietf.org/html/rfc7692#section-7
*
* @param MessageInterface $requestOrResponse
* @return PermessageDeflateOptions[]
* @throws \Exception
*/
public static function fromRequestOrResponse(MessageInterface $requestOrResponse): array {
$optionSets = [];
$extHeader = preg_replace('/\s+/', '', join(', ', $requestOrResponse->getHeader('Sec-Websocket-Extensions')));
$configurationRequests = explode(',', $extHeader);
foreach ($configurationRequests as $configurationRequest) {
$parts = explode(';', $configurationRequest);
if (count($parts) == 0) {
continue;
}
if ($parts[0] !== 'permessage-deflate') {
continue;
}
array_shift($parts);
$options = new self();
$options->deflateEnabled = true;
foreach ($parts as $part) {
$kv = explode('=', $part);
$key = $kv[0];
$value = count($kv) > 1 ? $kv[1] : null;
switch ($key) {
case "server_no_context_takeover":
case "client_no_context_takeover":
if ($value !== null) {
throw new InvalidPermessageDeflateOptionsException($key . ' must not have a value.');
}
$value = true;
break;
case "server_max_window_bits":
$value = (int) $value;
if (!in_array($value, self::VALID_BITS)) {
throw new InvalidPermessageDeflateOptionsException($key . ' must have a value between 8 and 15.');
}
break;
case "client_max_window_bits":
if ($value === null) {
$value = 15;
} else {
$value = (int) $value;
}
if (!in_array($value, self::VALID_BITS)) {
throw new InvalidPermessageDeflateOptionsException($key . ' must have no value or a value between 8 and 15.');
}
break;
default:
throw new InvalidPermessageDeflateOptionsException('Option "' . $key . '"is not valid for permessage deflate');
}
if ($options->$key !== null) {
throw new InvalidPermessageDeflateOptionsException($key . ' specified more than once. Connection must be declined.');
}
$options->$key = $value;
}
if ($options->getClientMaxWindowBits() === null) {
$options->client_max_window_bits = 15;
}
if ($options->getServerMaxWindowBits() === null) {
$options->server_max_window_bits = 15;
}
$optionSets[] = $options;
}
// always put a disabled on the end
$optionSets[] = new self();
return $optionSets;
}
/**
* @return bool|null
*/
public function getServerNoContextTakeover(): ?bool
{
return $this->server_no_context_takeover;
}
/**
* @return bool|null
*/
public function getClientNoContextTakeover(): ?bool
{
return $this->client_no_context_takeover;
}
/**
* @return int|null
*/
public function getServerMaxWindowBits(): ?int
{
return $this->server_max_window_bits;
}
/**
* @return int|null
*/
public function getClientMaxWindowBits(): ?int
{
return $this->client_max_window_bits;
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->deflateEnabled;
}
/**
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function addHeaderToResponse(ResponseInterface $response): ResponseInterface
{
if (!$this->deflateEnabled) {
return $response;
}
$header = 'permessage-deflate';
if ($this->client_max_window_bits != 15) {
$header .= '; client_max_window_bits='. $this->client_max_window_bits;
}
if ($this->client_no_context_takeover) {
$header .= '; client_no_context_takeover';
}
if ($this->server_max_window_bits != 15) {
$header .= '; server_max_window_bits=' . $this->server_max_window_bits;
}
if ($this->server_no_context_takeover) {
$header .= '; server_no_context_takeover';
}
return $response->withAddedHeader('Sec-Websocket-Extensions', $header);
}
public function addHeaderToRequest(RequestInterface $request): RequestInterface {
if (!$this->deflateEnabled) {
return $request;
}
$header = 'permessage-deflate';
if ($this->server_no_context_takeover) {
$header .= '; server_no_context_takeover';
}
if ($this->client_no_context_takeover) {
$header .= '; client_no_context_takeover';
}
if ($this->server_max_window_bits != 15) {
$header .= '; server_max_window_bits=' . $this->server_max_window_bits;
}
$header .= '; client_max_window_bits';
if ($this->client_max_window_bits != 15) {
$header .= '='. $this->client_max_window_bits;
}
return $request->withAddedHeader('Sec-Websocket-Extensions', $header);
}
public static function permessageDeflateSupported(string $version = PHP_VERSION): bool {
if (!function_exists('deflate_init')) {
return false;
}
if (version_compare($version, '7.1.3', '>')) {
return true;
}
if (version_compare($version, '7.0.18', '>=')
&& version_compare($version, '7.1.0', '<')) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
/**
* These are checks to ensure the client requested handshake are valid
* Verification rules come from section 4.2.1 of the RFC6455 document
* @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s
*/
class RequestVerifier {
public const VERSION = 13;
/**
* Given an array of the headers this method will run through all verification methods
* @param RequestInterface $request
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
*/
public function verifyAll(RequestInterface $request): bool {
$passes = 0;
$passes += (int)$this->verifyMethod($request->getMethod());
$passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion());
$passes += (int)$this->verifyRequestURI($request->getUri()->getPath());
$passes += (int)$this->verifyHost($request->getHeader('Host'));
$passes += (int)$this->verifyUpgradeRequest($request->getHeader('Upgrade'));
$passes += (int)$this->verifyConnection($request->getHeader('Connection'));
$passes += (int)$this->verifyKey($request->getHeader('Sec-WebSocket-Key'));
$passes += (int)$this->verifyVersion($request->getHeader('Sec-WebSocket-Version'));
return 8 === $passes;
}
/**
* Test the HTTP method. MUST be "GET"
* @param string
* @return bool
*/
public function verifyMethod(string $val): bool {
return 'get' === strtolower($val);
}
/**
* Test the HTTP version passed. MUST be 1.1 or greater
* @param string|int
* @return bool
*/
public function verifyHTTPVersion($val): bool {
return 1.1 <= (double)$val;
}
/**
* @param string
* @return bool
*/
public function verifyRequestURI(string $val): bool {
if ($val[0] !== '/') {
return false;
}
if (false !== strstr($val, '#')) {
return false;
}
if (!extension_loaded('mbstring')) {
return true;
}
return mb_check_encoding($val, 'US-ASCII');
}
/**
* @param array $hostHeader
* @return bool
* @todo Once I fix HTTP::getHeaders just verify this isn't NULL or empty...or maybe need to verify it's a valid domain??? Or should it equal $_SERVER['HOST'] ?
*/
public function verifyHost(array $hostHeader): bool {
return 1 === count($hostHeader);
}
/**
* Verify the Upgrade request to WebSockets.
* @param array $upgradeHeader MUST equal "websocket"
* @return bool
*/
public function verifyUpgradeRequest(array $upgradeHeader): bool {
return 1 === count($upgradeHeader) && 'websocket' === strtolower($upgradeHeader[0]);
}
/**
* Verify the Connection header
* @param array $connectionHeader MUST include "Upgrade"
* @return bool
*/
public function verifyConnection(array $connectionHeader): bool {
foreach ($connectionHeader as $l) {
$upgrades = array_filter(
array_map('trim', array_map('strtolower', explode(',', $l))),
static fn (string $x) => 'upgrade' === $x
);
if (count($upgrades) > 0) {
return true;
}
}
return false;
}
/**
* This function verifies the nonce is valid (64 big encoded, 16 bytes random string)
* @param array $keyHeader
* @return bool
* @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode?
* @todo Check the spec to see what the encoding of the key could be
*/
public function verifyKey(array $keyHeader): bool {
return 1 === count($keyHeader) && 16 === strlen(base64_decode($keyHeader[0]));
}
/**
* Verify the version passed matches this RFC
* @param string[] $versionHeader MUST equal ["13"]
* @return bool
*/
public function verifyVersion(array $versionHeader): bool {
return 1 === count($versionHeader) && static::VERSION === (int)$versionHeader[0];
}
/**
* @todo Write logic for this method. See section 4.2.1.8
*/
public function verifyProtocol($val): bool {
return true;
}
/**
* @todo Write logic for this method. See section 4.2.1.9
*/
public function verifyExtensions($val): bool {
return true;
}
public function getPermessageDeflateOptions(array $requestHeader, array $responseHeader): array {
$headerChecker = static fn (string $val) => 'permessage-deflate' === substr($val, 0, strlen('permessage-deflate'));
$deflate = true;
if (!isset($requestHeader['Sec-WebSocket-Extensions']) || count(array_filter($requestHeader['Sec-WebSocket-Extensions'], $headerChecker)) === 0) {
$deflate = false;
}
if (!isset($responseHeader['Sec-WebSocket-Extensions']) || count(array_filter($responseHeader['Sec-WebSocket-Extensions'], $headerChecker)) === 0) {
$deflate = false;
}
return [
'deflate' => $deflate,
'no_context_takeover' => false,
'max_window_bits' => null,
'request_no_context_takeover' => false,
'request_max_window_bits' => null
];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseVerifier {
public function verifyAll(RequestInterface $request, ResponseInterface $response): bool {
$passes = 0;
$passes += (int)$this->verifyStatus($response->getStatusCode());
$passes += (int)$this->verifyUpgrade($response->getHeader('Upgrade'));
$passes += (int)$this->verifyConnection($response->getHeader('Connection'));
$passes += (int)$this->verifySecWebSocketAccept(
$response->getHeader('Sec-WebSocket-Accept')
, $request->getHeader('Sec-WebSocket-Key')
);
$passes += (int)$this->verifySubProtocol(
$request->getHeader('Sec-WebSocket-Protocol')
, $response->getHeader('Sec-WebSocket-Protocol')
);
$passes += (int)$this->verifyExtensions(
$request->getHeader('Sec-WebSocket-Extensions')
, $response->getHeader('Sec-WebSocket-Extensions')
);
return (6 === $passes);
}
public function verifyStatus(int $status): bool {
return $status === 101;
}
public function verifyUpgrade(array $upgrade): bool {
return in_array('websocket', array_map('strtolower', $upgrade));
}
public function verifyConnection(array $connection): bool {
return in_array('upgrade', array_map('strtolower', $connection));
}
public function verifySecWebSocketAccept(array $swa, array $key): bool {
return
1 === count($swa) &&
1 === count($key) &&
$swa[0] === $this->sign($key[0])
;
}
public function sign(string $key): string {
return base64_encode(sha1($key . NegotiatorInterface::GUID, true));
}
public function verifySubProtocol(array $requestHeader, array $responseHeader): bool {
if (0 === count($responseHeader)) {
return true;
}
$requestedProtocols = array_map('trim', explode(',', implode(',', $requestHeader)));
return count($responseHeader) === 1 && count(array_intersect($responseHeader, $requestedProtocols)) === 1;
}
public function verifyExtensions(array $requestHeader, array $responseHeader): int {
if (in_array('permessage-deflate', $responseHeader)) {
return strpos(implode(',', $requestHeader), 'permessage-deflate') !== false ? 1 : 0;
}
return 1;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
/**
* The latest version of the WebSocket protocol
* @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE');
*/
class ServerNegotiator implements NegotiatorInterface {
private RequestVerifier $verifier;
private ResponseFactoryInterface $responseFactory;
private array $_supportedSubProtocols = [];
private bool $_strictSubProtocols = false;
private bool $enablePerMessageDeflate = false;
public function __construct(
RequestVerifier $requestVerifier,
ResponseFactoryInterface $responseFactory,
$enablePerMessageDeflate = false
) {
$this->verifier = $requestVerifier;
$this->responseFactory = $responseFactory;
// https://bugs.php.net/bug.php?id=73373
// https://bugs.php.net/bug.php?id=74240 - need >=7.1.4 or >=7.0.18
$supported = PermessageDeflateOptions::permessageDeflateSupported();
if ($enablePerMessageDeflate && !$supported) {
throw new \Exception('permessage-deflate is not supported by your PHP version (need >=7.1.4 or >=7.0.18).');
}
if ($enablePerMessageDeflate && !function_exists('deflate_add')) {
throw new \Exception('permessage-deflate is not supported because you do not have the zlib extension.');
}
$this->enablePerMessageDeflate = $enablePerMessageDeflate;
}
/**
* {@inheritdoc}
*/
public function isProtocol(RequestInterface $request): bool {
return $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'));
}
/**
* {@inheritdoc}
*/
public function getVersionNumber(): int {
return RequestVerifier::VERSION;
}
/**
* {@inheritdoc}
*/
public function handshake(RequestInterface $request): ResponseInterface {
$response = $this->responseFactory->createResponse();
if (true !== $this->verifier->verifyMethod($request->getMethod())) {
return $response->withHeader('Allow', 'GET')->withStatus(405);
}
if (true !== $this->verifier->verifyHTTPVersion($request->getProtocolVersion())) {
return $response->withStatus(505);
}
if (true !== $this->verifier->verifyRequestURI($request->getUri()->getPath())) {
return $response->withStatus(400);
}
if (true !== $this->verifier->verifyHost($request->getHeader('Host'))) {
return $response->withStatus(400);
}
$upgradeResponse = $response
->withHeader('Connection' , 'Upgrade')
->withHeader('Upgrade' , 'websocket')
->withHeader('Sec-WebSocket-Version', $this->getVersionNumber());
if (count($this->_supportedSubProtocols) > 0) {
$upgradeResponse = $upgradeResponse->withHeader(
'Sec-WebSocket-Protocol', implode(', ', array_keys($this->_supportedSubProtocols))
);
}
if (true !== $this->verifier->verifyUpgradeRequest($request->getHeader('Upgrade'))) {
return $upgradeResponse->withStatus(426, 'Upgrade header MUST be provided');
}
if (true !== $this->verifier->verifyConnection($request->getHeader('Connection'))) {
return $response->withStatus(400, 'Connection Upgrade MUST be requested');
}
if (true !== $this->verifier->verifyKey($request->getHeader('Sec-WebSocket-Key'))) {
return $response->withStatus(400, 'Invalid Sec-WebSocket-Key');
}
if (true !== $this->verifier->verifyVersion($request->getHeader('Sec-WebSocket-Version'))) {
return $upgradeResponse->withStatus(426);
}
$subProtocols = $request->getHeader('Sec-WebSocket-Protocol');
if (count($subProtocols) > 0 || (count($this->_supportedSubProtocols) > 0 && $this->_strictSubProtocols)) {
$subProtocols = array_map('trim', explode(',', implode(',', $subProtocols)));
$match = array_reduce($subProtocols, fn ($accumulator, $protocol) => $accumulator ?: (isset($this->_supportedSubProtocols[$protocol]) ? $protocol : null), null);
if ($this->_strictSubProtocols && null === $match) {
return $upgradeResponse->withStatus(426, 'No Sec-WebSocket-Protocols requested supported');
}
if (null !== $match) {
$response = $response->withHeader('Sec-WebSocket-Protocol', $match);
}
}
$response = $response
->withStatus(101)
->withHeader('Upgrade' , 'websocket')
->withHeader('Connection' , 'Upgrade')
->withHeader('Sec-WebSocket-Accept', $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]))
->withHeader('X-Powered-By' , 'Ratchet');
try {
$perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0];
} catch (InvalidPermessageDeflateOptionsException $e) {
return new Response(400, [], null, '1.1', $e->getMessage());
}
if ($this->enablePerMessageDeflate && $perMessageDeflateRequest->isEnabled()) {
$response = $perMessageDeflateRequest->addHeaderToResponse($response);
}
return $response;
}
/**
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
* @param string $key
* @return string
* @internal
*/
public function sign(string $key): string {
return base64_encode(sha1($key . static::GUID, true));
}
/**
* @param array $protocols
*/
public function setSupportedSubProtocols(array $protocols): void {
$this->_supportedSubProtocols = array_flip($protocols);
}
/**
* If enabled and support for a subprotocol has been added handshake
* will not upgrade if a match between request and supported subprotocols
* @param boolean $enable
* @todo Consider extending this interface and moving this there.
* The spec does say the server can fail for this reason, but
* it is not a requirement. This is an implementation detail.
*/
public function setStrictSubProtocolCheck(bool $enable): void {
$this->_strictSubProtocols = $enable;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Ratchet\RFC6455\Messaging;
class CloseFrameChecker {
private array $validCloseCodes = [
Frame::CLOSE_NORMAL,
Frame::CLOSE_GOING_AWAY,
Frame::CLOSE_PROTOCOL,
Frame::CLOSE_BAD_DATA,
Frame::CLOSE_BAD_PAYLOAD,
Frame::CLOSE_POLICY,
Frame::CLOSE_TOO_BIG,
Frame::CLOSE_MAND_EXT,
Frame::CLOSE_SRV_ERR,
];
public function __invoke(int $val): bool {
return ($val >= 3000 && $val <= 4999) || in_array($val, $this->validCloseCodes);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Ratchet\RFC6455\Messaging;
interface DataInterface extends \Stringable {
/**
* Determine if the message is complete or still fragmented
* @return bool
*/
public function isCoalesced(): bool;
/**
* Get the number of bytes the payload is set to be
* @return int
*/
public function getPayloadLength(): int;
/**
* Get the payload (message) sent from peer
* @return string
*/
public function getPayload(): string;
/**
* Get raw contents of the message
* @return string
*/
public function getContents(): string;
}

473
vendor/ratchet/rfc6455/src/Messaging/Frame.php vendored Executable file
View File

@@ -0,0 +1,473 @@
<?php
namespace Ratchet\RFC6455\Messaging;
class Frame implements FrameInterface {
const OP_CONTINUE = 0;
const OP_TEXT = 1;
const OP_BINARY = 2;
const OP_CLOSE = 8;
const OP_PING = 9;
const OP_PONG = 10;
const CLOSE_NORMAL = 1000;
const CLOSE_GOING_AWAY = 1001;
const CLOSE_PROTOCOL = 1002;
const CLOSE_BAD_DATA = 1003;
const CLOSE_NO_STATUS = 1005;
const CLOSE_ABNORMAL = 1006;
const CLOSE_BAD_PAYLOAD = 1007;
const CLOSE_POLICY = 1008;
const CLOSE_TOO_BIG = 1009;
const CLOSE_MAND_EXT = 1010;
const CLOSE_SRV_ERR = 1011;
const CLOSE_TLS = 1015;
const MASK_LENGTH = 4;
/**
* The contents of the frame
*/
protected string $data = '';
/**
* Number of bytes received from the frame
*/
public int $bytesRecvd = 0;
/**
* Number of bytes in the payload (as per framing protocol)
*/
protected int $defPayLen = -1;
/**
* If the frame is coalesced this is true
* This is to prevent doing math every time ::isCoalesced is called
*/
private bool $isCoalesced = false;
/**
* The unpacked first byte of the frame
*/
protected int $firstByte = -1;
/**
* The unpacked second byte of the frame
*/
protected int $secondByte = -1;
/**
* @var callable
* @returns \UnderflowException
*/
private $ufeg;
/**
* @param string|null $payload
* @param bool $final
* @param int $opcode
* @param callable<\UnderflowException> $ufExceptionFactory
*/
public function __construct(?string $payload = null, bool $final = true, int $opcode = 1, ?callable $ufExceptionFactory = null) {
$this->ufeg = $ufExceptionFactory ?: static fn (string $msg = '') => new \UnderflowException($msg);
if (null === $payload) {
return;
}
$this->defPayLen = strlen($payload);
$this->firstByte = ($final ? 128 : 0) + $opcode;
$this->secondByte = $this->defPayLen;
$this->isCoalesced = true;
$ext = '';
if ($this->defPayLen > 65535) {
$ext = pack('NN', 0, $this->defPayLen);
$this->secondByte = 127;
} elseif ($this->defPayLen > 125) {
$ext = pack('n', $this->defPayLen);
$this->secondByte = 126;
}
$this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload;
$this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen;
}
/**
* {@inheritdoc}
*/
public function isCoalesced(): bool {
if (true === $this->isCoalesced) {
return true;
}
try {
$payload_length = $this->getPayloadLength();
$payload_start = $this->getPayloadStartingByte();
} catch (\UnderflowException $e) {
return false;
}
$this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start;
return $this->isCoalesced;
}
/**
* {@inheritdoc}
*/
public function addBuffer(string $buf): void {
$len = strlen($buf);
$this->data .= $buf;
$this->bytesRecvd += $len;
if ($this->firstByte === -1 && $this->bytesRecvd !== 0) {
$this->firstByte = ord($this->data[0]);
}
if ($this->secondByte === -1 && $this->bytesRecvd >= 2) {
$this->secondByte = ord($this->data[1]);
}
}
/**
* {@inheritdoc}
*/
public function isFinal(): bool {
if (-1 === $this->firstByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received to determine if this is the final frame in message');
}
return 128 === ($this->firstByte & 128);
}
public function setRsv1(bool $value = true): self {
if (strlen($this->data) == 0) {
throw new \UnderflowException("Cannot set Rsv1 because there is no data.");
}
$this->firstByte =
($this->isFinal() ? 128 : 0)
+ $this->getOpcode()
+ ($value ? 64 : 0)
+ ($this->getRsv2() ? 32 : 0)
+ ($this->getRsv3() ? 16 : 0)
;
$this->data[0] = chr($this->firstByte);
return $this;
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv1(): bool {
if (-1 === $this->firstByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 64 === ($this->firstByte & 64);
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv2(): bool {
if (-1 === $this->firstByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 32 === ($this->firstByte & 32);
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv3(): bool {
if (-1 === $this->firstByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received to determine reserved bit');
}
return 16 === ($this->firstByte & 16);
}
/**
* {@inheritdoc}
*/
public function isMasked(): bool {
if (-1 === $this->secondByte) {
throw call_user_func($this->ufeg, "Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
}
return 128 === ($this->secondByte & 128);
}
/**
* {@inheritdoc}
*/
public function getMaskingKey(): string {
if (!$this->isMasked()) {
return '';
}
$start = 1 + $this->getNumPayloadBytes();
if ($this->bytesRecvd < $start + static::MASK_LENGTH) {
throw call_user_func($this->ufeg, 'Not enough data buffered to calculate the masking key');
}
return substr($this->data, $start, static::MASK_LENGTH);
}
/**
* Create a 4 byte masking key
* @return string
*/
public function generateMaskingKey(): string {
$mask = '';
for ($i = 1; $i <= static::MASK_LENGTH; $i++) {
$mask .= chr(rand(32, 126));
}
return $mask;
}
/**
* Apply a mask to the payload
* @param string|null If NULL is passed a masking key will be generated
* @throws \OutOfBoundsException
* @throws \InvalidArgumentException If there is an issue with the given masking key
* @return Frame
*/
public function maskPayload(?string $maskingKey = null): self {
if (null === $maskingKey) {
$maskingKey = $this->generateMaskingKey();
}
if (static::MASK_LENGTH !== strlen($maskingKey)) {
throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters");
}
if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) {
throw new \OutOfBoundsException("Masking key MUST be ASCII");
}
$this->unMaskPayload();
$this->secondByte = $this->secondByte | 128;
$this->data[1] = chr($this->secondByte);
$this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0);
$this->bytesRecvd += static::MASK_LENGTH;
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
return $this;
}
/**
* Remove a mask from the payload
* @throws \UnderFlowException If the frame is not coalesced
* @return Frame
*/
public function unMaskPayload(): self {
if (!$this->isCoalesced()) {
throw call_user_func($this->ufeg, 'Frame must be coalesced before applying mask');
}
if (!$this->isMasked()) {
return $this;
}
$maskingKey = $this->getMaskingKey();
$this->secondByte = $this->secondByte & ~128;
$this->data[1] = chr($this->secondByte);
$this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH);
$this->bytesRecvd -= static::MASK_LENGTH;
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
return $this;
}
/**
* Apply a mask to a string or the payload of the instance
* @param string $maskingKey The 4 character masking key to be applied
* @param string|null $payload A string to mask or null to use the payload
* @throws \UnderflowException If using the payload but enough hasn't been buffered
* @return string The masked string
*/
public function applyMask(string $maskingKey, ?string $payload = null): string {
if (null === $payload) {
if (!$this->isCoalesced()) {
throw call_user_func($this->ufeg, 'Frame must be coalesced to apply a mask');
}
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
}
$len = strlen($payload);
if (0 === $len) {
return '';
}
return $payload ^ str_pad('', $len, $maskingKey, STR_PAD_RIGHT);
}
/**
* {@inheritdoc}
*/
public function getOpcode(): int {
if (-1 === $this->firstByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received to determine opcode');
}
return ($this->firstByte & ~240);
}
/**
* Gets the decimal value of bits 9 (10th) through 15 inclusive
* @return int
* @throws \UnderflowException If the buffer doesn't have enough data to determine this
*/
protected function getFirstPayloadVal(): int {
if (-1 === $this->secondByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received');
}
return $this->secondByte & 127;
}
/**
* @return int (7|23|71) Number of bits defined for the payload length in the fame
* @throws \UnderflowException
*/
protected function getNumPayloadBits(): int {
if (-1 === $this->secondByte) {
throw call_user_func($this->ufeg, 'Not enough bytes received');
}
// By default 7 bits are used to describe the payload length
// These are bits 9 (10th) through 15 inclusive
$bits = 7;
// Get the value of those bits
$check = $this->getFirstPayloadVal();
// If the value is 126 the 7 bits plus the next 16 are used to describe the payload length
if ($check >= 126) {
$bits += 16;
}
// If the value of the initial payload length are is 127 an additional 48 bits are used to describe length
// Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48)
if ($check === 127) {
$bits += 48;
}
return $bits;
}
/**
* This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits)
* @see getNumPayloadBits
*/
protected function getNumPayloadBytes(): int {
return (1 + $this->getNumPayloadBits()) / 8;
}
/**
* {@inheritdoc}
*/
public function getPayloadLength(): int {
if ($this->defPayLen !== -1) {
return $this->defPayLen;
}
$this->defPayLen = $this->getFirstPayloadVal();
if ($this->defPayLen <= 125) {
return $this->getPayloadLength();
}
$byte_length = $this->getNumPayloadBytes();
if ($this->bytesRecvd < 1 + $byte_length) {
$this->defPayLen = -1;
throw call_user_func($this->ufeg, 'Not enough data buffered to determine payload length');
}
$len = 0;
for ($i = 2; $i <= $byte_length; $i++) {
$len <<= 8;
$len += ord($this->data[$i]);
}
$this->defPayLen = $len;
return $this->getPayloadLength();
}
/**
* {@inheritdoc}
*/
public function getPayloadStartingByte(): int {
return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0);
}
/**
* {@inheritdoc}
* @todo Consider not checking mask, always returning the payload, masked or not
*/
public function getPayload(): string {
if (!$this->isCoalesced()) {
throw call_user_func($this->ufeg, 'Can not return partial message');
}
return $this->__toString();
}
/**
* Get the raw contents of the frame
* @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow
*/
public function getContents(): string {
return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength());
}
public function __toString(): string {
$payload = (string)substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
if ($this->isMasked()) {
$payload = $this->applyMask($this->getMaskingKey(), $payload);
}
return $payload;
}
/**
* Sometimes clients will concatenate more than one frame over the wire
* This method will take the extra bytes off the end and return them
* @return string
*/
public function extractOverflow(): string {
if ($this->isCoalesced()) {
$endPoint = $this->getPayloadLength();
$endPoint += $this->getPayloadStartingByte();
if ($this->bytesRecvd > $endPoint) {
$overflow = substr($this->data, $endPoint);
$this->data = substr($this->data, 0, $endPoint);
return $overflow;
}
}
return '';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Ratchet\RFC6455\Messaging;
interface FrameInterface extends DataInterface {
/**
* Add incoming data to the frame from peer
* @param string
*/
public function addBuffer(string $buf): void;
/**
* Is this the final frame in a fragmented message?
* @return bool
*/
public function isFinal(): bool;
/**
* Is the payload masked?
* @return bool
*/
public function isMasked(): bool;
/**
* @return int
*/
public function getOpcode(): int;
/**
* @return int
*/
//public function getReceivedPayloadLength(): int;
/**
* 32-big string
* @return string
*/
public function getMaskingKey(): string;
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Ratchet\RFC6455\Messaging;
class Message implements \IteratorAggregate, MessageInterface {
private \SplDoublyLinkedList $_frames;
private int $len;
public function __construct() {
$this->_frames = new \SplDoublyLinkedList;
$this->len = 0;
}
public function getIterator(): \Traversable {
return $this->_frames;
}
/**
* {@inheritdoc}
*/
public function count(): int {
return count($this->_frames);
}
/**
* {@inheritdoc}
*/
public function isCoalesced(): bool {
if (count($this->_frames) == 0) {
return false;
}
$last = $this->_frames->top();
return $last->isCoalesced() && $last->isFinal();
}
/**
* {@inheritdoc}
*/
public function addFrame(FrameInterface $fragment): MessageInterface {
$this->len += $fragment->getPayloadLength();
$this->_frames->push($fragment);
return $this;
}
/**
* {@inheritdoc}
*/
public function getOpcode(): int {
if (count($this->_frames) == 0) {
throw new \UnderflowException('No frames have been added to this message');
}
return $this->_frames->bottom()->getOpcode();
}
/**
* {@inheritdoc}
*/
public function getPayloadLength(): int {
return $this->len;
}
/**
* {@inheritdoc}
*/
public function getPayload(): string {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Message has not been put back together yet');
}
return $this->__toString();
}
/**
* {@inheritdoc}
*/
public function getContents(): string {
if (!$this->isCoalesced()) {
throw new \UnderflowException("Message has not been put back together yet");
}
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getContents();
}
return $buffer;
}
public function __toString(): string {
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getPayload();
}
return $buffer;
}
/**
* @return boolean
*/
public function isBinary(): bool {
if ($this->_frames->isEmpty()) {
throw new \UnderflowException('Not enough data has been received to determine if message is binary');
}
return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode();
}
/**
* @return boolean
*/
public function getRsv1(): bool {
if ($this->_frames->isEmpty()) {
return false;
//throw new \UnderflowException('Not enough data has been received to determine if message is binary');
}
return $this->_frames->bottom()->getRsv1();
}
}

View File

@@ -0,0 +1,529 @@
<?php
namespace Ratchet\RFC6455\Messaging;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
class MessageBuffer {
private CloseFrameChecker $closeFrameChecker;
/**
* @var callable
*/
private $exceptionFactory;
private ?MessageInterface $messageBuffer = null;
private ?FrameInterface $frameBuffer = null;
/**
* @var callable
*/
private $onMessage;
/**
* @var callable
*/
private $onControl;
private bool $checkForMask;
/**
* @var callable
*/
private $sender;
private string $leftovers = '';
private int $streamingMessageOpCode = -1;
private PermessageDeflateOptions $permessageDeflateOptions;
private bool $deflateEnabled;
private int $maxMessagePayloadSize;
private int $maxFramePayloadSize;
private bool $compressedMessage = false;
/**
* @var resource|bool|null
*/
private $inflator = null;
/**
* @var resource|bool|null
*/
private $deflator = null;
public function __construct(
CloseFrameChecker $frameChecker,
callable $onMessage,
?callable $onControl = null,
bool $expectMask = true,
?callable $exceptionFactory = null,
?int $maxMessagePayloadSize = null, // null for default - zero for no limit
?int $maxFramePayloadSize = null, // null for default - zero for no limit
?callable $sender = null,
?PermessageDeflateOptions $permessageDeflateOptions = null
) {
$this->closeFrameChecker = $frameChecker;
$this->checkForMask = $expectMask;
$this->exceptionFactory = $exceptionFactory ?: static fn (string $msg) => new \UnderflowException($msg);
$this->onMessage = $onMessage;
$this->onControl = $onControl ?: static function (): void {};
$this->sender = $sender;
$this->permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
$this->deflateEnabled = $this->permessageDeflateOptions->isEnabled();
if ($this->deflateEnabled && !is_callable($this->sender)) {
throw new \InvalidArgumentException('sender must be set when deflate is enabled');
}
$memory_limit_bytes = static::getMemoryLimit();
if ($maxMessagePayloadSize === null) {
$maxMessagePayloadSize = (int)($memory_limit_bytes / 4);
}
if ($maxFramePayloadSize === null) {
$maxFramePayloadSize = (int)($memory_limit_bytes / 4);
}
if ($maxFramePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxFramePayloadSize < 0) { // this should be interesting on non-64 bit systems
throw new \InvalidArgumentException($maxFramePayloadSize . ' is not a valid maxFramePayloadSize');
}
$this->maxFramePayloadSize = $maxFramePayloadSize;
if ($maxMessagePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxMessagePayloadSize < 0) {
throw new \InvalidArgumentException($maxMessagePayloadSize . 'is not a valid maxMessagePayloadSize');
}
$this->maxMessagePayloadSize = $maxMessagePayloadSize;
}
public function onData(string $data): void {
$data = $this->leftovers . $data;
$dataLen = strlen($data);
if ($dataLen < 2) {
$this->leftovers = $data;
return;
}
$frameStart = 0;
while ($frameStart + 2 <= $dataLen) {
$headerSize = 2;
$payload_length = unpack('C', $data[$frameStart + 1] & "\x7f")[1];
$isMasked = ($data[$frameStart + 1] & "\x80") === "\x80";
$headerSize += $isMasked ? 4 : 0;
$payloadLenOver2GB = false;
if ($payload_length > 125 && ($dataLen - $frameStart < $headerSize + 125)) {
// no point of checking - this frame is going to be bigger than the buffer is right now
break;
}
if ($payload_length > 125) {
$payloadLenBytes = $payload_length === 126 ? 2 : 8;
$headerSize += $payloadLenBytes;
$bytesToUpack = substr($data, $frameStart + 2, $payloadLenBytes);
if ($payload_length === 126){
$payload_length = unpack('n', $bytesToUpack)[1];
} else {
$payloadLenOver2GB = unpack('N', $bytesToUpack)[1] > 0; //Decode only the 4 first bytes
if (PHP_INT_SIZE == 4) { // if 32bits PHP
$bytesToUpack = substr($bytesToUpack, 4); //Keep only 4 last bytes
$payload_length = unpack('N', $bytesToUpack)[1];
} else {
$payload_length = unpack('J', $bytesToUpack)[1];
}
}
}
$closeFrame = null;
if ($payload_length < 0) {
// this can happen when unpacking in php
$closeFrame = $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Invalid frame length');
}
if (!$closeFrame && PHP_INT_SIZE == 4 && $payloadLenOver2GB) {
$closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Frame over 2GB can\'t be handled on 32bits PHP');
}
if (!$closeFrame && $this->maxFramePayloadSize > 1 && $payload_length > $this->maxFramePayloadSize) {
$closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum frame size exceeded');
}
if (!$closeFrame && $this->maxMessagePayloadSize > 0
&& $payload_length + ($this->messageBuffer ? $this->messageBuffer->getPayloadLength() : 0) > $this->maxMessagePayloadSize) {
$closeFrame = $this->newCloseFrame(Frame::CLOSE_TOO_BIG, 'Maximum message size exceeded');
}
if ($closeFrame !== null) {
$onControl = $this->onControl;
$onControl($closeFrame);
$this->leftovers = '';
return;
}
$isCoalesced = $dataLen - $frameStart >= $payload_length + $headerSize;
if (!$isCoalesced) {
break;
}
$this->processData(substr($data, $frameStart, $payload_length + $headerSize));
$frameStart = $frameStart + $payload_length + $headerSize;
}
$this->leftovers = substr($data, $frameStart);
}
/**
* @param string $data
* @return void
*/
private function processData(string $data): void {
$this->messageBuffer ?: $this->messageBuffer = $this->newMessage();
$this->frameBuffer ?: $this->frameBuffer = $this->newFrame();
$this->frameBuffer->addBuffer($data);
$onMessage = $this->onMessage;
$onControl = $this->onControl;
$this->frameBuffer = $this->frameCheck($this->frameBuffer);
$this->frameBuffer->unMaskPayload();
$opcode = $this->frameBuffer->getOpcode();
if ($opcode > 2) {
$onControl($this->frameBuffer, $this);
if (Frame::OP_CLOSE === $opcode) {
return;
}
} else {
if ($this->messageBuffer->count() === 0 && $this->frameBuffer->getRsv1()) {
$this->compressedMessage = true;
}
if ($this->compressedMessage) {
$this->frameBuffer = $this->inflateFrame($this->frameBuffer);
}
$this->messageBuffer->addFrame($this->frameBuffer);
}
$this->frameBuffer = null;
if ($this->messageBuffer->isCoalesced()) {
$msgCheck = $this->checkMessage($this->messageBuffer);
$msgBuffer = $this->messageBuffer;
$this->messageBuffer = null;
if (true !== $msgCheck) {
$onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload'), $this);
} else {
$onMessage($msgBuffer, $this);
}
$this->messageBuffer = null;
$this->compressedMessage = false;
if ($this->permessageDeflateOptions->getServerNoContextTakeover()) {
$this->inflator = null;
}
}
}
/**
* Check a frame to be added to the current message buffer
* @param FrameInterface $frame
* @return FrameInterface
*/
public function frameCheck(FrameInterface $frame): FrameInterface {
if ((false !== $frame->getRsv1() && !$this->deflateEnabled) ||
false !== $frame->getRsv2() ||
false !== $frame->getRsv3()
) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid reserve code');
}
if ($this->checkForMask && !$frame->isMasked()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an incorrect frame mask');
}
$opcode = $frame->getOpcode();
if ($opcode > 2) {
if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected a mismatch between final bit and indicated payload length');
}
switch ($opcode) {
case Frame::OP_CLOSE:
$closeCode = 0;
$bin = $frame->getPayload();
if (empty($bin)) {
return $this->newCloseFrame(Frame::CLOSE_NORMAL);
}
if (strlen($bin) === 1) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code');
}
if (strlen($bin) >= 2) {
list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
}
$checker = $this->closeFrameChecker;
if (!$checker($closeCode)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid close code');
}
if (!$this->checkUtf8(substr($bin, 2))) {
return $this->newCloseFrame(Frame::CLOSE_BAD_PAYLOAD, 'Ratchet detected an invalid UTF-8 payload in the close reason');
}
return $frame;
case Frame::OP_PING:
case Frame::OP_PONG:
break;
default:
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected an invalid OP code');
}
return $frame;
}
if (Frame::OP_CONTINUE === $frame->getOpcode() && 0 === count($this->messageBuffer)) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected the first frame of a message was a continue');
}
if (count($this->messageBuffer) > 0 && Frame::OP_CONTINUE !== $frame->getOpcode()) {
return $this->newCloseFrame(Frame::CLOSE_PROTOCOL, 'Ratchet detected invalid OP code when expecting continue frame');
}
return $frame;
}
/**
* Determine if a message is valid
* @param MessageInterface
* @return bool|int true if valid - false if incomplete - int of recommended close code
*/
public function checkMessage(MessageInterface $message) {
if (!$message->isBinary()) {
if (!$this->checkUtf8($message->getPayload())) {
return Frame::CLOSE_BAD_PAYLOAD;
}
}
return true;
}
private function checkUtf8(string $string): bool {
if (extension_loaded('mbstring')) {
return mb_check_encoding($string, 'UTF-8');
}
return preg_match('//u', $string);
}
/**
* @return MessageInterface
*/
public function newMessage(): MessageInterface {
return new Message;
}
/**
* @param string|null $payload
* @param bool $final
* @param int $opcode
* @return FrameInterface
*/
public function newFrame(?string $payload = null, bool $final = true, int $opcode = Frame::OP_TEXT): FrameInterface {
return new Frame($payload, $final, $opcode, $this->exceptionFactory);
}
public function newCloseFrame(int $code, string $reason = ''): FrameInterface {
return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE);
}
public function sendFrame(FrameInterface $frame): void {
if ($this->sender === null) {
throw new \Exception('To send frames using the MessageBuffer, sender must be set.');
}
if ($this->deflateEnabled &&
($frame->getOpcode() === Frame::OP_TEXT || $frame->getOpcode() === Frame::OP_BINARY)) {
$frame = $this->deflateFrame($frame);
}
if (!$this->checkForMask) {
$frame->maskPayload();
}
$sender = $this->sender;
$sender($frame->getContents());
}
public function sendMessage(string $messagePayload, bool $final = true, bool $isBinary = false): void {
$opCode = $isBinary ? Frame::OP_BINARY : Frame::OP_TEXT;
if ($this->streamingMessageOpCode === -1) {
$this->streamingMessageOpCode = $opCode;
}
if ($this->streamingMessageOpCode !== $opCode) {
throw new \Exception('Binary and text message parts cannot be streamed together.');
}
$frame = $this->newFrame($messagePayload, $final, $opCode);
$this->sendFrame($frame);
if ($final) {
// reset deflator if client doesn't remember contexts
if ($this->getDeflateNoContextTakeover()) {
$this->deflator = null;
}
$this->streamingMessageOpCode = -1;
}
}
private function getDeflateNoContextTakeover(): ?bool {
return $this->checkForMask ?
$this->permessageDeflateOptions->getServerNoContextTakeover() :
$this->permessageDeflateOptions->getClientNoContextTakeover();
}
private function getDeflateWindowBits(): int {
return $this->checkForMask ? $this->permessageDeflateOptions->getServerMaxWindowBits() : $this->permessageDeflateOptions->getClientMaxWindowBits();
}
private function getInflateNoContextTakeover(): ?bool {
return $this->checkForMask ?
$this->permessageDeflateOptions->getClientNoContextTakeover() :
$this->permessageDeflateOptions->getServerNoContextTakeover();
}
private function getInflateWindowBits(): int {
return $this->checkForMask ? $this->permessageDeflateOptions->getClientMaxWindowBits() : $this->permessageDeflateOptions->getServerMaxWindowBits();
}
private function inflateFrame(FrameInterface $frame): Frame {
if ($this->inflator === null) {
$this->inflator = inflate_init(
ZLIB_ENCODING_RAW,
[
'level' => -1,
'memory' => 8,
'window' => $this->getInflateWindowBits(),
'strategy' => ZLIB_DEFAULT_STRATEGY
]
);
}
$terminator = '';
if ($frame->isFinal()) {
$terminator = "\x00\x00\xff\xff";
}
gc_collect_cycles(); // memory runs away if we don't collect ??
return new Frame(
inflate_add($this->inflator, $frame->getPayload() . $terminator),
$frame->isFinal(),
$frame->getOpcode()
);
}
private function deflateFrame(FrameInterface $frame): FrameInterface
{
if ($frame->getRsv1()) {
return $frame; // frame is already deflated
}
if ($this->deflator === null) {
$bits = $this->getDeflateWindowBits();
if ($bits === 8) {
$bits = 9;
}
$this->deflator = deflate_init(
ZLIB_ENCODING_RAW,
[
'level' => -1,
'memory' => 8,
'window' => $bits,
'strategy' => ZLIB_DEFAULT_STRATEGY
]
);
}
// there is an issue in the zlib extension for php where
// deflate_add does not check avail_out to see if the buffer filled
// this only seems to be an issue for payloads between 16 and 64 bytes
// This if statement is a hack fix to break the output up allowing us
// to call deflate_add twice which should clear the buffer issue
// if ($frame->getPayloadLength() >= 16 && $frame->getPayloadLength() <= 64) {
// // try processing in 8 byte chunks
// // https://bugs.php.net/bug.php?id=73373
// $payload = "";
// $orig = $frame->getPayload();
// $partSize = 8;
// while (strlen($orig) > 0) {
// $part = substr($orig, 0, $partSize);
// $orig = substr($orig, strlen($part));
// $flags = strlen($orig) > 0 ? ZLIB_PARTIAL_FLUSH : ZLIB_SYNC_FLUSH;
// $payload .= deflate_add($this->deflator, $part, $flags);
// }
// } else {
$payload = deflate_add(
$this->deflator,
$frame->getPayload(),
ZLIB_SYNC_FLUSH
);
// }
$deflatedFrame = new Frame(
substr($payload, 0, $frame->isFinal() ? -4 : strlen($payload)),
$frame->isFinal(),
$frame->getOpcode()
);
if ($frame->isFinal()) {
$deflatedFrame->setRsv1();
}
return $deflatedFrame;
}
/**
* This is a separate function for testing purposes
* $memory_limit is only used for testing
*
* @param null|string $memory_limit
* @return int
*/
private static function getMemoryLimit(?string $memory_limit = null): int {
$memory_limit = $memory_limit === null ? \trim(\ini_get('memory_limit')) : $memory_limit;
$memory_limit_bytes = 0;
if ($memory_limit !== '') {
$shifty = ['k' => 0, 'm' => 10, 'g' => 20];
$multiplier = strlen($memory_limit) > 1 ? substr(strtolower($memory_limit), -1) : '';
$memory_limit = (int)$memory_limit;
$memory_limit_bytes = in_array($multiplier, array_keys($shifty), true) ? $memory_limit * 1024 << $shifty[$multiplier] : $memory_limit;
}
return $memory_limit_bytes < 0 ? 0 : $memory_limit_bytes;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Ratchet\RFC6455\Messaging;
interface MessageInterface extends DataInterface, \Traversable, \Countable {
/**
* @param FrameInterface $fragment
* @return MessageInterface
*/
public function addFrame(FrameInterface $fragment): self;
/**
* @return int
*/
public function getOpcode(): int;
/**
* @return bool
*/
public function isBinary(): bool;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Ratchet\RFC6455\Test;
use PHPUnit\Framework\TestCase;
/**
* @coversNothing
*/
class AbResultsTest extends TestCase {
private function verifyAutobahnResults(string $fileName): void {
if (!file_exists($fileName)) {
$this->markTestSkipped('Autobahn TestSuite results not found');
}
$resultsJson = file_get_contents($fileName);
$results = json_decode($resultsJson);
$agentName = array_keys(get_object_vars($results))[0];
foreach ($results->$agentName as $name => $result) {
if ($result->behavior === "INFORMATIONAL") {
continue;
}
$this->assertTrue(in_array($result->behavior, ["OK", "NON-STRICT"]), "Autobahn test case " . $name . " in " . $fileName);
}
}
public function testAutobahnClientResults(): void {
$this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json');
}
public function testAutobahnServerResults(): void {
$this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json');
}
}

View File

@@ -0,0 +1,271 @@
<?php
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Uri;
use Ratchet\RFC6455\Handshake\InvalidPermessageDeflateOptionsException;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Handshake\ClientNegotiator;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\MessageInterface;
use React\Promise\Deferred;
use Ratchet\RFC6455\Messaging\Frame;
use React\Promise\PromiseInterface;
use GuzzleHttp\Psr7\HttpFactory;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
require __DIR__ . '/../bootstrap.php';
define('AGENT', 'RatchetRFC/0.4');
$testServer = $argc > 1 ? $argv[1] : "127.0.0.1";
$loop = React\EventLoop\Factory::create();
$connector = new Connector($loop);
function echoStreamerFactory(ConnectionInterface $conn, ?PermessageDeflateOptions $permessageDeflateOptions = null): MessageBuffer
{
$permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
return new MessageBuffer(
new CloseFrameChecker,
static function (MessageInterface $msg, MessageBuffer $messageBuffer) use ($conn): void {
$messageBuffer->sendMessage($msg->getPayload(), true, $msg->isBinary());
},
static function (FrameInterface $frame, MessageBuffer $messageBuffer) use ($conn) {
switch ($frame->getOpcode()) {
case Frame::OP_PING:
return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents());
case Frame::OP_CLOSE:
return $conn->end((new Frame($frame->getPayload(), true, Frame::OP_CLOSE))->maskPayload()->getContents());
}
},
false,
null,
null,
null,
[$conn, 'write'],
$permessageDeflateOptions
);
}
function getTestCases(): PromiseInterface {
global $testServer;
global $connector;
$deferred = new Deferred();
$connector->connect($testServer . ':9002')->then(static function (ConnectionInterface $connection) use ($deferred, $testServer): void {
$cn = new ClientNegotiator(new HttpFactory());
$cnRequest = $cn->generateRequest(new Uri('ws://' . $testServer . ':9002/getCaseCount'));
$rawResponse = "";
$response = null;
/** @var MessageBuffer $ms */
$ms = null;
$connection->on('data', static function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest): void {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = Message::parseResponse($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$connection->end();
$deferred->reject();
} else {
$ms = new MessageBuffer(
new CloseFrameChecker,
static function (MessageInterface $msg) use ($deferred, $connection): void {
$deferred->resolve($msg->getPayload());
$connection->close();
},
null,
false,
null,
null,
null,
static function (): void {}
);
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$connection->write(Message::toString($cnRequest));
});
return $deferred->promise();
}
$cn = new ClientNegotiator(
new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null);
function runTest(int $case)
{
global $connector;
global $testServer;
global $cn;
$casePath = "/runCase?case={$case}&agent=" . AGENT;
$deferred = new Deferred();
$connector->connect($testServer . ':9002')->then(static function (ConnectionInterface $connection) use ($deferred, $casePath, $case, $testServer): void {
$cn = new ClientNegotiator(
new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null);
$cnRequest = $cn->generateRequest(new Uri('ws://' . $testServer . ':9002' . $casePath));
$rawResponse = "";
$response = null;
$ms = null;
$connection->on('data', static function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest): void {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = Message::parseResponse($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
echo "Invalid response.\n";
$connection->end();
$deferred->reject();
} else {
try {
$permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0];
$ms = echoStreamerFactory(
$connection,
$permessageDeflateOptions
);
} catch (InvalidPermessageDeflateOptionsException $e) {
$connection->end();
}
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$connection->on('close', static function () use ($deferred): void {
$deferred->resolve(null);
});
$connection->write(Message::toString($cnRequest));
});
return $deferred->promise();
}
function createReport(): PromiseInterface {
global $connector;
global $testServer;
$deferred = new Deferred();
$connector->connect($testServer . ':9002')->then(static function (ConnectionInterface $connection) use ($deferred, $testServer): void {
// $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true";
// we will stop it using docker now instead of just shutting down
$reportPath = "/updateReports?agent=" . AGENT;
$cn = new ClientNegotiator(new HttpFactory());
$cnRequest = $cn->generateRequest(new Uri('ws://' . $testServer . ':9002' . $reportPath));
$rawResponse = "";
$response = null;
/** @var MessageBuffer $ms */
$ms = null;
$connection->on('data', static function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest): void {
if ($response === null) {
$rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n");
if ($pos) {
$data = substr($rawResponse, $pos + 4);
$rawResponse = substr($rawResponse, 0, $pos + 4);
$response = Message::parseResponse($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) {
$connection->end();
$deferred->reject();
} else {
$ms = new MessageBuffer(
new CloseFrameChecker,
static function (MessageInterface $msg) use ($deferred, $connection): void {
$deferred->resolve($msg->getPayload());
$connection->close();
},
null,
false,
null,
null,
null,
static function (): void {}
);
}
}
}
// feed the message streamer
if ($ms) {
$ms->onData($data);
}
});
$connection->write(Message::toString($cnRequest));
});
return $deferred->promise();
}
$testPromises = [];
getTestCases()->then(function ($count) use ($loop) {
$allDeferred = new Deferred();
$runNextCase = static function () use (&$i, &$runNextCase, $count, $allDeferred): void {
$i++;
if ($i > $count) {
$allDeferred->resolve(null);
return;
}
echo "Running test $i/$count...";
$startTime = microtime(true);
runTest($i)
->then(static function () use ($startTime): void {
echo " completed " . round((microtime(true) - $startTime) * 1000) . " ms\n";
})
->then($runNextCase);
};
$i = 0;
$runNextCase();
$allDeferred->promise()->then(static function (): void {
createReport();
});
});
$loop->run();

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -x
echo "Running $0"
echo Adding "$1 host.ratchet.internal" to /etc/hosts file
echo $1 host.ratchet.internal >> /etc/hosts
echo /etc/hosts contains:
cat /etc/hosts
echo

View File

@@ -0,0 +1,16 @@
{
"options": {
"failByDrop": false
}
, "outdir": "/reports/servers"
, "servers": [{
"agent": "RatchetRFC/0.4"
, "url": "ws://host.ratchet.internal:9001"
, "options": {"version": 18}
}]
, "cases": [
"*"
]
, "exclude-cases": []
, "exclude-agent-cases": {}
}

View File

@@ -0,0 +1,14 @@
{
"options": {
"failByDrop": false
}
, "outdir": "/reports/servers"
, "servers": [{
"agent": "RatchetRFC/0.4"
, "url": "ws://host.ratchet.internal:9001"
, "options": {"version": 18}
}]
, "cases": ["*"]
, "exclude-cases": ["12.*", "13.*"]
, "exclude-agent-cases": {}
}

View File

@@ -0,0 +1,12 @@
{
"url": "ws://127.0.0.1:9002"
, "options": {
"failByDrop": false
}
, "outdir": "./reports/clients"
, "cases": [
"*"
]
, "exclude-cases": []
, "exclude-agent-cases": {}
}

View File

@@ -0,0 +1,10 @@
{
"url": "ws://127.0.0.1:9002"
, "options": {
"failByDrop": false
}
, "outdir": "./reports/clients"
, "cases": ["*"]
, "exclude-cases": ["12.*", "13.*"]
, "exclude-agent-cases": {}
}

View File

@@ -0,0 +1,48 @@
set -x
cd tests/ab
if [ "$ABTEST" = "client" ]; then
docker run --rm \
-d \
-v ${PWD}:/config \
-v ${PWD}/reports:/reports \
-p 9002:9002 \
--name fuzzingserver \
crossbario/autobahn-testsuite wstest -m fuzzingserver -s /config/fuzzingserver$SKIP_DEFLATE.json
sleep 5
if [ "$TRAVIS" != "true" ]; then
echo "Running tests vs Autobahn test client"
###docker run -it --rm --name abpytest crossbario/autobahn-testsuite wstest --mode testeeclient -w ws://host.docker.internal:9002
fi
php -d memory_limit=256M clientRunner.php
docker ps -a
docker logs fuzzingserver
docker stop fuzzingserver
sleep 2
fi
if [ "$ABTEST" = "server" ]; then
php -d memory_limit=256M startServer.php &
sleep 3
if [ "$OSTYPE" = "linux-gnu" ]; then
IPADDR=`hostname -I | cut -f 1 -d ' '`
else
IPADDR=`ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -1 | tr -d 'adr:'`
fi
docker run --rm \
-i \
-v ${PWD}:/config \
-v ${PWD}/reports:/reports \
--name fuzzingclient \
crossbario/autobahn-testsuite /bin/sh -c "sh /config/docker_bootstrap.sh $IPADDR; wstest -m fuzzingclient -s /config/fuzzingclient$SKIP_DEFLATE.json"
sleep 1
# send the shutdown command to the PHP echo server
wget -O - -q http://127.0.0.1:9001/shutdown
fi

View File

@@ -0,0 +1,93 @@
<?php
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\Response;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use Ratchet\RFC6455\Handshake\RequestVerifier;
use Ratchet\RFC6455\Handshake\ServerNegotiator;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\Frame;
use GuzzleHttp\Psr7\HttpFactory;
require_once __DIR__ . "/../bootstrap.php";
$loop = \React\EventLoop\Factory::create();
$socket = new \React\Socket\Server('0.0.0.0:9001', $loop);
$closeFrameChecker = new CloseFrameChecker;
$negotiator = new ServerNegotiator(
new RequestVerifier,
new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported()
);
$uException = new \UnderflowException;
$socket->on('connection', static function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException, $socket): void {
$headerComplete = false;
$buffer = '';
$parser = null;
$connection->on('data', static function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException, $socket): void {
if ($headerComplete) {
$parser->onData($data);
return;
}
$buffer .= $data;
$parts = explode("\r\n\r\n", $buffer);
if (count($parts) < 2) {
return;
}
$headerComplete = true;
$psrRequest = Message::parseRequest($parts[0] . "\r\n\r\n");
$negotiatorResponse = $negotiator->handshake($psrRequest);
$negotiatorResponse = $negotiatorResponse->withAddedHeader("Content-Length", "0");
if ($negotiatorResponse->getStatusCode() !== 101 && $psrRequest->getUri()->getPath() === '/shutdown') {
$connection->end(Message::toString(new Response(200, [], 'Shutting down echo server.' . PHP_EOL)));
$socket->close();
return;
};
$connection->write(Message::toString($negotiatorResponse));
if ($negotiatorResponse->getStatusCode() !== 101) {
$connection->end();
return;
}
// there is no need to look through the client requests
// we support any valid permessage deflate
$deflateOptions = PermessageDeflateOptions::fromRequestOrResponse($psrRequest)[0];
$parser = new MessageBuffer($closeFrameChecker,
static function (MessageInterface $message, MessageBuffer $messageBuffer): void {
$messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary());
}, static function (FrameInterface $frame) use ($connection, &$parser): void {
switch ($frame->getOpCode()) {
case Frame::OP_CLOSE:
$connection->end($frame->getContents());
break;
case Frame::OP_PING:
$connection->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents());
break;
}
}, true, static fn (): \Exception => $uException,
null,
null,
[$connection, 'write'],
$deflateOptions);
array_shift($parts);
$parser->onData(implode("\r\n\r\n", $parts));
});
});
$loop->run();

19
vendor/ratchet/rfc6455/tests/bootstrap.php vendored Executable file
View File

@@ -0,0 +1,19 @@
<?php
/**
* Find the auto loader file
*/
$files = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../vendor/autoload.php',
__DIR__ . '/../../../vendor/autoload.php',
__DIR__ . '/../../../../vendor/autoload.php',
];
foreach ($files as $file) {
if (file_exists($file)) {
$loader = require $file;
$loader->addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__);
break;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Handshake\PermessageDeflateOptions
*/
class PermessageDeflateOptionsTest extends TestCase
{
public static function versionSupportProvider(): array {
return [
['7.0.17', false],
['7.0.18', true],
['7.0.200', true],
['5.6.0', false],
['7.1.3', false],
['7.1.4', true],
['7.1.200', true],
['10.0.0', true]
];
}
/**
* @requires function deflate_init
* @dataProvider versionSupportProvider
*/
public function testVersionSupport(string $version, bool $supported): void {
$this->assertEquals($supported, PermessageDeflateOptions::permessageDeflateSupported($version));
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\RequestVerifier;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Handshake\RequestVerifier
*/
class RequestVerifierTest extends TestCase {
/**
* @var RequestVerifier
*/
protected $_v;
protected function setUp(): void {
$this->_v = new RequestVerifier();
}
public static function methodProvider(): array {
return array(
array(true, 'GET'),
array(true, 'get'),
array(true, 'Get'),
array(false, 'POST'),
array(false, 'DELETE'),
array(false, 'PUT'),
array(false, 'PATCH')
);
}
/**
* @dataProvider methodProvider
*/
public function testMethodMustBeGet(bool $result, string $in): void {
$this->assertEquals($result, $this->_v->verifyMethod($in));
}
public static function httpVersionProvider(): array {
return array(
array(true, 1.1),
array(true, '1.1'),
array(true, 1.2),
array(true, '1.2'),
array(true, 2),
array(true, '2'),
array(true, '2.0'),
array(false, '1.0'),
array(false, 1),
array(false, '0.9'),
array(false, ''),
array(false, 'hello')
);
}
/**
* @dataProvider httpVersionProvider
*/
public function testHttpVersionIsAtLeast1Point1(bool $expected, $in): void {
$this->assertEquals($expected, $this->_v->verifyHTTPVersion($in));
}
public static function uRIProvider(): array {
return array(
array(true, '/chat'),
array(true, '/hello/world?key=val'),
array(false, '/chat#bad'),
array(false, 'nope'),
array(false, '/ ಠ_ಠ '),
array(false, '/✖')
);
}
/**
* @dataProvider URIProvider
*/
public function testRequestUri(bool $expected, string $in): void {
$this->assertEquals($expected, $this->_v->verifyRequestURI($in));
}
public static function hostProvider(): array {
return array(
array(true, ['server.example.com']),
array(false, [])
);
}
/**
* @dataProvider HostProvider
*/
public function testVerifyHostIsSet(bool $expected, array $in): void {
$this->assertEquals($expected, $this->_v->verifyHost($in));
}
public static function upgradeProvider(): array {
return array(
array(true, ['websocket']),
array(true, ['Websocket']),
array(true, ['webSocket']),
array(false, []),
array(false, [''])
);
}
/**
* @dataProvider upgradeProvider
*/
public function testVerifyUpgradeIsWebSocket(bool $expected, array $val): void {
$this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val));
}
public static function connectionProvider(): array {
return array(
array(true, ['Upgrade']),
array(true, ['upgrade']),
array(true, ['keep-alive', 'Upgrade']),
array(true, ['Upgrade', 'keep-alive']),
array(true, ['keep-alive', 'Upgrade', 'something']),
// as seen in Firefox 47.0.1 - see https://github.com/ratchetphp/RFC6455/issues/14
array(true, ['keep-alive, Upgrade']),
array(true, ['Upgrade, keep-alive']),
array(true, ['keep-alive, Upgrade, something']),
array(true, ['keep-alive, Upgrade', 'something']),
array(false, ['']),
array(false, [])
);
}
/**
* @dataProvider connectionProvider
*/
public function testConnectionHeaderVerification(bool $expected, array $val): void {
$this->assertEquals($expected, $this->_v->verifyConnection($val));
}
public static function keyProvider(): array {
return array(
array(true, ['hkfa1L7uwN6DCo4IS3iWAw==']),
array(true, ['765vVoQpKSGJwPzJIMM2GA==']),
array(true, ['AQIDBAUGBwgJCgsMDQ4PEC==']),
array(true, ['axa2B/Yz2CdpfQAY2Q5P7w==']),
array(false, [0]),
array(false, ['Hello World']),
array(false, ['1234567890123456']),
array(false, ['123456789012345678901234']),
array(true, [base64_encode('UTF8allthngs+✓')]),
array(true, ['dGhlIHNhbXBsZSBub25jZQ==']),
array(false, []),
array(false, ['dGhlIHNhbXBsZSBub25jZQ==', 'Some other value']),
array(false, ['Some other value', 'dGhlIHNhbXBsZSBub25jZQ=='])
);
}
/**
* @dataProvider keyProvider
*/
public function testKeyIsBase64Encoded16BitNonce(bool $expected, array $val): void {
$this->assertEquals($expected, $this->_v->verifyKey($val));
}
public static function versionProvider(): array {
return array(
array(true, [13]),
array(true, ['13']),
array(false, [12]),
array(false, [14]),
array(false, ['14']),
array(false, ['hi']),
array(false, ['']),
array(false, [])
);
}
/**
* @dataProvider versionProvider
*/
public function testVersionEquals13(bool $expected, array $in): void {
$this->assertEquals($expected, $this->_v->verifyVersion($in));
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\ResponseVerifier;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Handshake\ResponseVerifier
*/
class ResponseVerifierTest extends TestCase {
/**
* @var ResponseVerifier
*/
protected $_v;
protected function setUp(): void {
$this->_v = new ResponseVerifier;
}
public static function subProtocolsProvider(): array {
return [
[true, ['a'], ['a']]
, [true, ['c', 'd', 'a'], ['a']]
, [true, ['c, a', 'd'], ['a']]
, [true, [], []]
, [true, ['a', 'b'], []]
, [false, ['c', 'd', 'a'], ['b', 'a']]
, [false, ['a', 'b', 'c'], ['d']]
];
}
/**
* @dataProvider subProtocolsProvider
*/
public function testVerifySubProtocol(bool $expected, array $request, array $response): void {
$this->assertEquals($expected, $this->_v->verifySubProtocol($request, $response));
}
}

View File

@@ -0,0 +1,223 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use GuzzleHttp\Psr7\Message;
use GuzzleHttp\Psr7\HttpFactory;
use Ratchet\RFC6455\Handshake\RequestVerifier;
use Ratchet\RFC6455\Handshake\ServerNegotiator;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Handshake\ServerNegotiator
*/
class ServerNegotiatorTest extends TestCase
{
public function testNoUpgradeRequested(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(426, $response->getStatusCode());
$this->assertEquals('Upgrade header MUST be provided', $response->getReasonPhrase());
$this->assertEquals('Upgrade', $response->getHeaderLine('Connection'));
$this->assertEquals('websocket', $response->getHeaderLine('Upgrade'));
$this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version'));
}
public function testNoConnectionUpgradeRequested(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(400, $response->getStatusCode());
$this->assertEquals('Connection Upgrade MUST be requested', $response->getReasonPhrase());
}
public function testInvalidSecWebsocketKey(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Key: 12345
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(400, $response->getStatusCode());
$this->assertEquals('Invalid Sec-WebSocket-Key', $response->getReasonPhrase());
}
public function testInvalidSecWebsocketVersion(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(426, $response->getStatusCode());
$this->assertEquals('Upgrade Required', $response->getReasonPhrase());
$this->assertEquals('Upgrade', $response->getHeaderLine('Connection'));
$this->assertEquals('websocket', $response->getHeaderLine('Upgrade'));
$this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version'));
}
public function testBadSubprotocolResponse(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$negotiator->setStrictSubProtocolCheck(true);
$negotiator->setSupportedSubProtocols([]);
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: someprotocol
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(426, $response->getStatusCode());
$this->assertEquals('No Sec-WebSocket-Protocols requested supported', $response->getReasonPhrase());
$this->assertEquals('Upgrade', $response->getHeaderLine('Connection'));
$this->assertEquals('websocket', $response->getHeaderLine('Upgrade'));
$this->assertEquals('13', $response->getHeaderLine('Sec-WebSocket-Version'));
}
public function testNonStrictSubprotocolDoesNotIncludeHeaderWhenNoneAgreedOn(): void {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$negotiator->setStrictSubProtocolCheck(false);
$negotiator->setSupportedSubProtocols(['someproto']);
$requestText = 'GET / HTTP/1.1
Host: 127.0.0.1:6789
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: someotherproto
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(101, $response->getStatusCode());
$this->assertEquals('Upgrade', $response->getHeaderLine('Connection'));
$this->assertEquals('websocket', $response->getHeaderLine('Upgrade'));
$this->assertFalse($response->hasHeader('Sec-WebSocket-Protocol'));
}
public function testSuggestsAppropriateSubprotocol(): void
{
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());
$negotiator->setStrictSubProtocolCheck(true);
$negotiator->setSupportedSubProtocols(['someproto']);
$requestText = 'GET / HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Sec-WebSocket-Key: HGt8eQax7nAOlXUw0/asPQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
';
$request = Message::parseRequest($requestText);
$response = $negotiator->handshake($request);
$this->assertEquals('1.1', $response->getProtocolVersion());
$this->assertEquals(426, $response->getStatusCode());
$this->assertEquals('Upgrade', $response->getHeaderLine('Connection'));
$this->assertEquals('websocket', $response->getHeaderLine('Upgrade'));
$this->assertEquals('someproto', $response->getHeaderLine('Sec-WebSocket-Protocol'));
}
}

View File

@@ -0,0 +1,431 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Messaging\Frame
* @todo getMaskingKey, getPayloadStartingByte don't have tests yet
* @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry.
*/
class FrameTest extends TestCase {
protected $_firstByteFinText = '10000001';
protected $_secondByteMaskedSPL = '11111101';
/** @var Frame */
protected $_frame;
protected $_packer;
protected function setUp(): void {
$this->_frame = new Frame;
}
/**
* Encode the fake binary string to send over the wire
* @param string of 1's and 0's
* @return string
*/
public static function encode(string $in): string {
if (strlen($in) > 8) {
$out = '';
while (strlen($in) >= 8) {
$out .= static::encode(substr($in, 0, 8));
$in = substr($in, 8);
}
return $out;
}
return chr(bindec($in));
}
/**
* This is a data provider
* param string The UTF8 message
* param string The WebSocket framed message, then base64_encoded
*/
public static function UnframeMessageProvider(): array {
return array(
array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7'),
array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg'),
array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow=='),
array(
"The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...",
'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY='
)
);
}
public static function underflowProvider(): array {
return array(
array('isFinal', ''),
array('getRsv1', ''),
array('getRsv2', ''),
array('getRsv3', ''),
array('getOpcode', ''),
array('isMasked', '10000001'),
array('getPayloadLength', '10000001'),
array('getPayloadLength', '1000000111111110'),
array('getMaskingKey', '1000000110000111'),
array('getPayload', '100000011000000100011100101010101001100111110100')
);
}
/**
* @dataProvider underflowProvider
*/
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering(string $method, string $bin): void {
$this->expectException(\UnderflowException::class);
if (!empty($bin)) {
$this->_frame->addBuffer(static::encode($bin));
}
call_user_func(array($this->_frame, $method));
}
/**
* A data provider for testing the first byte of a WebSocket frame
* param bool Given, is the byte indicate this is the final frame
* param int Given, what is the expected opcode
* param string of 0|1 Each character represents a bit in the byte
*/
public static function firstByteProvider(): array {
return array(
array(false, false, false, true, 8, '00011000'),
array(true, false, true, false, 10, '10101010'),
array(false, false, false, false, 15, '00001111'),
array(true, false, false, false, 1, '10000001'),
array(true, true, true, true, 15, '11111111'),
array(true, true, false, false, 7, '11000111')
);
}
/**
* @dataProvider firstByteProvider
*/
public function testFinCodeFromBits(bool $fin, bool $rsv1, bool $rsv2, bool $rsv3, int $opcode, string $bin): void {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($fin, $this->_frame->isFinal());
}
/**
* @dataProvider firstByteProvider
*/
public function testGetRsvFromBits(bool $fin, bool $rsv1, bool $rsv2, bool $rsv3, int $opcode, string $bin): void {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($rsv1, $this->_frame->getRsv1());
$this->assertEquals($rsv2, $this->_frame->getRsv2());
$this->assertEquals($rsv3, $this->_frame->getRsv3());
}
/**
* @dataProvider firstByteProvider
*/
public function testOpcodeFromBits(bool $fin, bool $rsv1, bool $rsv2, bool $rsv3, int $opcode, string $bin): void {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($opcode, $this->_frame->getOpcode());
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testFinCodeFromFullMessage(string $msg, string $encoded): void {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isFinal());
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testOpcodeFromFullMessage(string $msg, string $encoded): void {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(1, $this->_frame->getOpcode());
}
public static function payloadLengthDescriptionProvider(): array {
return array(
array(7, '01110101'),
array(7, '01111101'),
array(23, '01111110'),
array(71, '01111111'),
array(7, '00000000'), // Should this throw an exception? Can a payload be empty?
array(7, '00000001')
);
}
/**
* @dataProvider payloadLengthDescriptionProvider
*/
public function testFirstPayloadDesignationValue(int $bits, string $bin): void {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->assertEquals(bindec($bin), $cb->invoke($this->_frame));
}
public function testFirstPayloadValUnderflow(): void {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->expectException(\UnderflowException::class);
$cb->invoke($this->_frame);
}
/**
* @dataProvider payloadLengthDescriptionProvider
*/
public function testDetermineHowManyBitsAreUsedToDescribePayload(int $expected_bits, string $bin): void {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->assertEquals($expected_bits, $cb->invoke($this->_frame));
}
public function testgetNumPayloadBitsUnderflow(): void {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->expectException(\UnderflowException::class);
$cb->invoke($this->_frame);
}
public function secondByteProvider(): array {
return array(
array(true, 1, '10000001'),
array(false, 1, '00000001'),
array(true, 125, $this->_secondByteMaskedSPL)
);
}
/**
* @dataProvider secondByteProvider
*/
public function testIsMaskedReturnsExpectedValue(bool $masked, int $payload_length, string $bin): void {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($masked, $this->_frame->isMasked());
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testIsMaskedFromFullMessage(string $msg, string $encoded): void {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isMasked());
}
/**
* @dataProvider secondByteProvider
*/
public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed(bool $masked, int $payload_length, string $bin): void {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($payload_length, $this->_frame->getPayloadLength());
}
/**
* @dataProvider UnframeMessageProvider
* @todo Not yet testing when second additional payload length descriptor
*/
public function testGetPayloadLengthFromFullMessage(string $msg, string $encoded): void {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(strlen($msg), $this->_frame->getPayloadLength());
}
public function maskingKeyProvider(): array {
$frame = new Frame;
return array(
array($frame->generateMaskingKey()),
array($frame->generateMaskingKey()),
array($frame->generateMaskingKey())
);
}
/**
* @dataProvider maskingKeyProvider
* @todo I I wrote the dataProvider incorrectly, skipping for now
*/
public function testGetMaskingKey(string $mask): void {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL));
$this->_frame->addBuffer($mask);
$this->assertEquals($mask, $this->_frame->getMaskingKey());
}
public function testGetMaskingKeyOnUnmaskedPayload(): void {
$frame = new Frame('Hello World!');
$this->assertEquals('', $frame->getMaskingKey());
}
/**
* @dataProvider UnframeMessageProvider
* @todo Move this test to bottom as it requires all methods of the class
*/
public function testUnframeFullMessage(string $unframed, string $base_framed): void {
$this->_frame->addBuffer(base64_decode($base_framed));
$this->assertEquals($unframed, $this->_frame->getPayload());
}
public static function messageFragmentProvider(): array {
return array(
array(false, '', '', '', '', '')
);
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testCheckPiecingTogetherMessage(string $msg, string $encoded): void {
$framed = base64_decode($encoded);
for ($i = 0, $len = strlen($framed);$i < $len; $i++) {
$this->_frame->addBuffer(substr($framed, $i, 1));
}
$this->assertEquals($msg, $this->_frame->getPayload());
}
public function testLongCreate(): void {
$len = 65525;
$pl = $this->generateRandomString($len);
$frame = new Frame($pl, true, Frame::OP_PING);
$this->assertTrue($frame->isFinal());
$this->assertEquals(Frame::OP_PING, $frame->getOpcode());
$this->assertFalse($frame->isMasked());
$this->assertEquals($len, $frame->getPayloadLength());
$this->assertEquals($pl, $frame->getPayload());
}
public function testReallyLongCreate(): void {
$len = 65575;
$frame = new Frame($this->generateRandomString($len));
$this->assertEquals($len, $frame->getPayloadLength());
}
public function testExtractOverflow(): void {
$string1 = $this->generateRandomString();
$frame1 = new Frame($string1);
$string2 = $this->generateRandomString();
$frame2 = new Frame($string2);
$cat = new Frame;
$cat->addBuffer($frame1->getContents() . $frame2->getContents());
$this->assertEquals($frame1->getContents(), $cat->getContents());
$this->assertEquals($string1, $cat->getPayload());
$uncat = new Frame;
$uncat->addBuffer($cat->extractOverflow());
$this->assertEquals($string1, $cat->getPayload());
$this->assertEquals($string2, $uncat->getPayload());
}
public function testEmptyExtractOverflow(): void {
$string = $this->generateRandomString();
$frame = new Frame($string);
$this->assertEquals($string, $frame->getPayload());
$this->assertEquals('', $frame->extractOverflow());
$this->assertEquals($string, $frame->getPayload());
}
public function testGetContents(): void {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame1 = new Frame($msg);
$frame2 = new Frame($msg);
$frame2->maskPayload();
$this->assertNotEquals($frame1->getContents(), $frame2->getContents());
$this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents()));
}
public function testMasking(): void {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame = new Frame($msg);
$frame->maskPayload();
$this->assertTrue($frame->isMasked());
$this->assertEquals($msg, $frame->getPayload());
}
public function testUnMaskPayload(): void {
$string = $this->generateRandomString();
$frame = new Frame($string);
$frame->maskPayload()->unMaskPayload();
$this->assertFalse($frame->isMasked());
$this->assertEquals($string, $frame->getPayload());
}
public function testGenerateMaskingKey(): void {
$dupe = false;
$done = array();
for ($i = 0; $i < 10; $i++) {
$new = $this->_frame->generateMaskingKey();
if (in_array($new, $done)) {
$dupe = true;
}
$done[] = $new;
}
$this->assertEquals(4, strlen($new));
$this->assertFalse($dupe);
}
public function testGivenMaskIsValid(): void {
$this->expectException(\InvalidArgumentException::class);
$this->_frame->maskPayload('hello world');
}
/**
* @requires extension mbstring
*/
public function testGivenMaskIsValidAscii(): void {
$this->expectException(\OutOfBoundsException::class);
$this->_frame->maskPayload('x✖');
}
protected function generateRandomString(int $length = 10, bool $addSpaces = true, bool $addNumbers = true): string {
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง
$useChars = array();
for($i = 0; $i < $length; $i++) {
$useChars[] = $characters[mt_rand(0, strlen($characters) - 1)];
}
if($addSpaces === true) {
array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' ');
}
if($addNumbers === true) {
array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9));
}
shuffle($useChars);
$randomString = trim(implode('', $useChars));
$randomString = substr($randomString, 0, $length);
return $randomString;
}
/**
* There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than
* 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame
* to set the payload length to 126 and then not recalculate it once the full length information was available.
*
* This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown.
*/
public function testFrameDeliveredOneByteAtATime(): void {
$startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final
$framePayload = str_repeat("*", 256);
$rawOverflow = "xyz";
$rawFrame = $startHeader . $framePayload . $rawOverflow;
$frame = new Frame();
$payloadLen = 256;
for ($i = 0; $i < strlen($rawFrame); $i++) {
$frame->addBuffer($rawFrame[$i]);
try {
// payloadLen will
$payloadLen = $frame->getPayloadLength();
} catch (\UnderflowException $e) {
if ($i > 2) { // we should get an underflow on 0,1,2
$this->fail("Underflow exception when the frame length should be available");
}
}
if ($payloadLen !== 256) {
$this->fail("Payload length of " . $payloadLen . " should have been 256.");
}
}
// make sure the overflow is good
$this->assertEquals($rawOverflow, $frame->extractOverflow());
}
}

View File

@@ -0,0 +1,375 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\Message;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use React\EventLoop\Factory;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Messaging\MessageBuffer
*/
class MessageBufferTest extends TestCase
{
/**
* This is to test that MessageBuffer can handle a large receive
* buffer with many many frames without blowing the stack (pre-v0.4 issue)
*/
public function testProcessingLotsOfFramesInASingleChunk(): void {
$frame = new Frame('a', true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
$data = str_repeat($frameRaw, 1000);
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) use (&$messageCount): void {
$messageCount++;
$this->assertEquals('a', $message->getPayload());
},
null,
false
);
$messageBuffer->onData($data);
$this->assertEquals(1000, $messageCount);
}
public function testProcessingMessagesAsynchronouslyWhileBlockingInMessageHandler(): void {
$loop = Factory::create();
$frameA = new Frame('a', true, Frame::OP_TEXT);
$frameB = new Frame('b', true, Frame::OP_TEXT);
$bReceived = false;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message) use (&$messageCount, &$bReceived, $loop): void {
$payload = $message->getPayload();
$bReceived = $payload === 'b';
if (!$bReceived) {
$loop->run();
}
},
null,
false
);
$loop->addPeriodicTimer(0.1, static function () use ($messageBuffer, $frameB, $loop): void {
$loop->stop();
$messageBuffer->onData($frameB->getContents());
});
$messageBuffer->onData($frameA->getContents());
$this->assertTrue($bReceived);
}
public function testInvalidFrameLength(): void {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
$frameRaw[1] = "\x7f"; // 127 in the first spot
$frameRaw[2] = "\xff"; // this will unpack to -1
$frameRaw[3] = "\xff";
$frameRaw[4] = "\xff";
$frameRaw[5] = "\xff";
$frameRaw[6] = "\xff";
$frameRaw[7] = "\xff";
$frameRaw[8] = "\xff";
$frameRaw[9] = "\xff";
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message) use (&$messageCount): void {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertInstanceOf(Frame::class, $controlFrame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_PROTOCOL], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testFrameLengthTooBig(): void {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
$frameRaw[1] = "\x7f"; // 127 in the first spot
$frameRaw[2] = "\x7f"; // this will unpack to -1
$frameRaw[3] = "\xff";
$frameRaw[4] = "\xff";
$frameRaw[5] = "\xff";
$frameRaw[6] = "\xff";
$frameRaw[7] = "\xff";
$frameRaw[8] = "\xff";
$frameRaw[9] = "\xff";
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message) use (&$messageCount): void {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame): void {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertInstanceOf(Frame::class, $controlFrame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testFrameLengthBiggerThanMaxMessagePayload(): void {
$frame = new Frame(str_repeat('a', 200), true, Frame::OP_TEXT);
$frameRaw = $frame->getContents();
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message) use (&$messageCount): void {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame): void {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
100,
0
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertInstanceOf(Frame::class, $controlFrame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testSecondFrameLengthPushesPastMaxMessagePayload(): void {
$frame = new Frame(str_repeat('a', 200), false, Frame::OP_TEXT);
$firstFrameRaw = $frame->getContents();
$frame = new Frame(str_repeat('b', 200), true, Frame::OP_TEXT);
$secondFrameRaw = $frame->getContents();
/** @var Frame $controlFrame */
$controlFrame = null;
$messageCount = 0;
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message) use (&$messageCount): void {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame): void {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
300,
0
);
$messageBuffer->onData($firstFrameRaw);
// only put part of the second frame in to watch it fail fast
$messageBuffer->onData(substr($secondFrameRaw, 0, 150));
$this->assertEquals(0, $messageCount);
$this->assertInstanceOf(Frame::class, $controlFrame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_TOO_BIG], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
/**
* Some test cases from memory limit inspired by https://github.com/BrandEmbassy/php-memory
*
* Here is the license for that project:
* MIT License
*
* Copyright (c) 2018 Brand Embassy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* @dataProvider phpConfigurationProvider
*
* @param string $phpConfigurationValue
* @param int $expectedLimit
*/
public function testMemoryLimits(string $phpConfigurationValue, int $expectedLimit): void {
$method = new \ReflectionMethod(MessageBuffer::class, 'getMemoryLimit');
$method->setAccessible(true);
$actualLimit = $method->invoke(null, $phpConfigurationValue);
$this->assertSame($expectedLimit, $actualLimit);
}
public function phpConfigurationProvider(): array {
return [
'without unit type, just bytes' => ['500', 500],
'1 GB with big "G"' => ['1G', 1 * 1024 * 1024 * 1024],
'128 MB with big "M"' => ['128M', 128 * 1024 * 1024],
'128 MB with small "m"' => ['128m', 128 * 1024 * 1024],
'24 kB with small "k"' => ['24k', 24 * 1024],
'2 GB with small "g"' => ['2g', 2 * 1024 * 1024 * 1024],
'unlimited memory' => ['-1', 0],
'invalid float value' => ['2.5M', 2 * 1024 * 1024],
'empty value' => ['', 0],
'invalid ini setting' => ['whatever it takes', 0]
];
}
public function testInvalidMaxFramePayloadSizes(): void {
$this->expectException(\InvalidArgumentException::class);
new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message): void {},
static function (Frame $frame): void {},
false,
null,
0,
-1
);
}
public function testInvalidMaxMessagePayloadSizes(): void {
$this->expectException(\InvalidArgumentException::class);
new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message): void {},
static function (Frame $frame): void {},
false,
null,
-1,
0
);
}
/**
* @dataProvider phpConfigurationProvider
*
* @param string $phpConfigurationValue
* @param int $expectedLimit
*
* @runInSeparateProcess
*/
public function testIniSizes(string $phpConfigurationValue, int $expectedLimit): void {
$value = @ini_set('memory_limit', $phpConfigurationValue);
if ($value === false) {
$this->markTestSkipped("Does not support setting the memory_limit lower than current memory_usage");
}
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
static function (Message $message): void {},
static function (Frame $frame): void {},
false,
null
);
if ($expectedLimit === -1) {
$expectedLimit = 0;
}
$prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize');
$prop->setAccessible(true);
$this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer));
$prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize');
$prop->setAccessible(true);
$this->assertEquals($expectedLimit / 4, $prop->getValue($messageBuffer));
}
/**
* @runInSeparateProcess
*/
public function testInvalidIniSize(): void {
$value = @ini_set('memory_limit', 'lots of memory');
if ($value === false) {
$this->markTestSkipped("Does not support setting the memory_limit lower than current memory_usage");
}
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null
);
$prop = new \ReflectionProperty($messageBuffer, 'maxMessagePayloadSize');
$prop->setAccessible(true);
$this->assertEquals(0, $prop->getValue($messageBuffer));
$prop = new \ReflectionProperty($messageBuffer, 'maxFramePayloadSize');
$prop->setAccessible(true);
$this->assertEquals(0, $prop->getValue($messageBuffer));
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\Message;
use PHPUnit\Framework\TestCase;
/**
* @covers Ratchet\RFC6455\Messaging\Message
*/
class MessageTest extends TestCase {
/** @var Message */
protected $message;
protected function setUp(): void {
$this->message = new Message;
}
public function testNoFrames(): void {
$this->assertFalse($this->message->isCoalesced());
}
public function testNoFramesOpCode(): void {
$this->expectException(\UnderflowException::class);
$this->message->getOpCode();
}
public function testFragmentationPayload(): void {
$a = 'Hello ';
$b = 'World!';
$f1 = new Frame($a, false);
$f2 = new Frame($b, true, Frame::OP_CONTINUE);
$this->message->addFrame($f1)->addFrame($f2);
$this->assertEquals(strlen($a . $b), $this->message->getPayloadLength());
$this->assertEquals($a . $b, $this->message->getPayload());
}
public function testUnbufferedFragment(): void {
$this->message->addFrame(new Frame('The quick brow', false));
$this->expectException(\UnderflowException::class);
$this->message->getPayload();
}
public function testGetOpCode(): void {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE))
;
$this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode());
}
public function testGetUnBufferedPayloadLength(): void {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
;
$this->assertEquals(28, $this->message->getPayloadLength());
}
}