Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Dawid Królak 2020-05-14 19:02:31 +02:00
commit 6df0bd1b68
28 changed files with 1374 additions and 155 deletions

View File

@ -1,17 +1,26 @@
language: php language: php
services: docker
php: php:
- 5.4
- 5.5
- 5.6 - 5.6
- 7.0 - 7.0
- 7.1 - 7.1
- 7.2 - 7.2
- 7.3
- 7.4
- nightly
env:
- ABTEST=client
- ABTEST=server
matrix:
allow_failures:
- php: nightly
before_install: before_install:
- export PATH=$HOME/.local/bin:$PATH - docker pull crossbario/autobahn-testsuite
- pip install --user autobahntestsuite
- pip list --user autobahntestsuite
before_script: before_script:
- composer install - composer install

View File

@ -1,7 +1,7 @@
# RFC6455 - The WebSocket Protocol # RFC6455 - The WebSocket Protocol
[![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455) [![Build Status](https://travis-ci.org/ratchetphp/RFC6455.svg?branch=master)](https://travis-ci.org/ratchetphp/RFC6455)
![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg) [![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. This library a protocol handler for the RFC6455 specification.
It contains components for both server and client side handshake and messaging protocol negotation. It contains components for both server and client side handshake and messaging protocol negotation.

View File

@ -5,15 +5,20 @@
"keywords": ["WebSockets", "websocket", "RFC6455"], "keywords": ["WebSockets", "websocket", "RFC6455"],
"homepage": "http://socketo.me", "homepage": "http://socketo.me",
"license": "MIT", "license": "MIT",
"authors": [{ "authors": [
"name": "Chris Boden" {
, "email": "cboden@gmail.com" "name": "Chris Boden"
, "role": "Developer" , "email": "cboden@gmail.com"
}], , "role": "Developer"
},
{
"name": "Matt Bonneau",
"role": "Developer"
}
],
"support": { "support": {
"forum": "https://groups.google.com/forum/#!forum/ratchet-php" "issues": "https://github.com/ratchetphp/RFC6455/issues",
, "issues": "https://github.com/ratchetphp/RFC6455/issues" "chat": "https://gitter.im/reactphp/reactphp"
, "irc": "irc://irc.freenode.org/reactphp"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -25,9 +30,18 @@
"psr/http-factory-implementation": "^1.0" "psr/http-factory-implementation": "^1.0"
}, },
"require-dev": { "require-dev": {
"react/http": "^0.4.1", "guzzlehttp/psr7": "^2.0-dev",
"react/socket-client": "^0.4.3", "phpunit/phpunit": "5.7.*",
"phpunit/phpunit": "4.8.*", "react/socket": "^1.3"
"guzzlehttp/psr7": "^2.0-dev" },
"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"
]
} }
} }

View File

@ -21,7 +21,10 @@ class ClientNegotiator {
*/ */
private $requestFactory; private $requestFactory;
function __construct(RequestFactoryInterface $requestFactory) { function __construct(
RequestFactoryInterface $requestFactory,
PermessageDeflateOptions $perMessageDeflateOptions = null
) {
$this->verifier = new ResponseVerifier; $this->verifier = new ResponseVerifier;
$this->requestFactory = $requestFactory; $this->requestFactory = $requestFactory;
@ -31,6 +34,24 @@ class ClientNegotiator {
->withHeader('Upgrade' , 'websocket') ->withHeader('Upgrade' , 'websocket')
->withHeader('Sec-WebSocket-Version', $this->getVersion()) ->withHeader('Sec-WebSocket-Version', $this->getVersion())
->withHeader('User-Agent' , 'Ratchet'); ->withHeader('User-Agent' , 'Ratchet');
if ($perMessageDeflateOptions === null) {
$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 support 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) { public function generateRequest(UriInterface $uri) {

View File

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

View File

@ -0,0 +1,260 @@
<?php
namespace Ratchet\RFC6455\Handshake;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class PermessageDeflateOptions
{
const MAX_WINDOW_BITS = 15;
/* this is a private instead of const for 5.4 compatibility */
private static $VALID_BITS = ['8', '9', '10', '11', '12', '13', '14', '15'];
private $deflateEnabled = false;
private $server_no_context_takeover;
private $client_no_context_takeover;
private $server_max_window_bits;
private $client_max_window_bits;
private function __construct() { }
public static function createEnabled() {
$new = new static();
$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 static();
}
public function withClientNoContextTakeover() {
$new = clone $this;
$new->client_no_context_takeover = true;
}
public function withoutClientNoContextTakeover() {
$new = clone $this;
$new->client_no_context_takeover = false;
}
public function withServerNoContextTakeover() {
$new = clone $this;
$new->server_no_context_takeover = true;
}
public function withoutServerNoContextTakeover() {
$new = clone $this;
$new->server_no_context_takeover = false;
}
public function withServerMaxWindowBits($bits = self::MAX_WINDOW_BITS) {
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;
}
public function withClientMaxWindowBits($bits = self::MAX_WINDOW_BITS) {
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;
}
/**
* 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) {
$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 static();
$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":
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';
}
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 static();
return $optionSets;
}
/**
* @return mixed
*/
public function getServerNoContextTakeover()
{
return $this->server_no_context_takeover;
}
/**
* @return mixed
*/
public function getClientNoContextTakeover()
{
return $this->client_no_context_takeover;
}
/**
* @return mixed
*/
public function getServerMaxWindowBits()
{
return $this->server_max_window_bits;
}
/**
* @return mixed
*/
public function getClientMaxWindowBits()
{
return $this->client_max_window_bits;
}
/**
* @return bool
*/
public function isEnabled()
{
return $this->deflateEnabled;
}
/**
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function addHeaderToResponse(ResponseInterface $response)
{
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) {
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($version = PHP_VERSION) {
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

@ -137,4 +137,27 @@ class RequestVerifier {
*/ */
public function verifyExtensions($val) { public function verifyExtensions($val) {
} }
public function getPermessageDeflateOptions(array $requestHeader, array $responseHeader) {
$deflate = true;
if (!isset($requestHeader['Sec-WebSocket-Extensions']) || count(array_filter($requestHeader['Sec-WebSocket-Extensions'], function ($val) {
return 'permessage-deflate' === substr($val, 0, strlen('permessage-deflate'));
})) === 0) {
$deflate = false;
}
if (!isset($responseHeader['Sec-WebSocket-Extensions']) || count(array_filter($responseHeader['Sec-WebSocket-Extensions'], function ($val) {
return 'permessage-deflate' === substr($val, 0, strlen('permessage-deflate'));
})) === 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

@ -18,8 +18,12 @@ class ResponseVerifier {
$request->getHeader('Sec-WebSocket-Protocol') $request->getHeader('Sec-WebSocket-Protocol')
, $response->getHeader('Sec-WebSocket-Protocol') , $response->getHeader('Sec-WebSocket-Protocol')
); );
$passes += (int)$this->verifyExtensions(
$request->getHeader('Sec-WebSocket-Extensions')
, $response->getHeader('Sec-WebSocket-Extensions')
);
return (5 === $passes); return (6 === $passes);
} }
public function verifyStatus($status) { public function verifyStatus($status) {
@ -49,4 +53,12 @@ class ResponseVerifier {
public function verifySubProtocol(array $requestHeader, array $responseHeader) { public function verifySubProtocol(array $requestHeader, array $responseHeader) {
return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0; return 0 === count($responseHeader) || count(array_intersect($responseHeader, $requestHeader)) > 0;
} }
}
public function verifyExtensions(array $requestHeader, array $responseHeader) {
if (in_array('permessage-deflate', $responseHeader)) {
return strpos(implode(',', $requestHeader), 'permessage-deflate') !== false ? 1 : 0;
}
return 1;
}
}

View File

@ -22,9 +22,27 @@ class ServerNegotiator implements NegotiatorInterface {
private $_strictSubProtocols = false; private $_strictSubProtocols = false;
public function __construct(RequestVerifier $requestVerifier, ResponseFactoryInterface $responseFactory) { private $enablePerMessageDeflate = false;
public function __construct(
RequestVerifier $requestVerifier,
ResponseFactoryInterface $responseFactory,
$enablePerMessageDeflate = false
) {
$this->verifier = $requestVerifier; $this->verifier = $requestVerifier;
$this->responseFactory = $responseFactory; $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;
} }
/** /**
@ -104,12 +122,25 @@ class ServerNegotiator implements NegotiatorInterface {
$response = $response->withHeader('Sec-WebSocket-Protocol', $match); $response = $response->withHeader('Sec-WebSocket-Protocol', $match);
} }
} }
return $response
$response = $response
->withStatus(101) ->withStatus(101)
->withHeader('Upgrade' , 'websocket') ->withHeader('Upgrade' , 'websocket')
->withHeader('Connection' , 'Upgrade') ->withHeader('Connection' , 'Upgrade')
->withHeader('Sec-WebSocket-Accept', $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0])) ->withHeader('Sec-WebSocket-Accept', $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]))
->withHeader('X-Powered-By' , 'Ratchet'); ->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;
} }
/** /**

View File

@ -149,6 +149,23 @@ class Frame implements FrameInterface {
return 128 === ($this->firstByte & 128); return 128 === ($this->firstByte & 128);
} }
public function setRsv1($value = true) {
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 * @return boolean
* @throws \UnderflowException * @throws \UnderflowException

View File

@ -7,8 +7,14 @@ class Message implements \IteratorAggregate, MessageInterface {
*/ */
private $_frames; private $_frames;
/**
* @var int
*/
private $len;
public function __construct() { public function __construct() {
$this->_frames = new \SplDoublyLinkedList; $this->_frames = new \SplDoublyLinkedList;
$this->len = 0;
} }
public function getIterator() { public function getIterator() {
@ -39,6 +45,7 @@ class Message implements \IteratorAggregate, MessageInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function addFrame(FrameInterface $fragment) { public function addFrame(FrameInterface $fragment) {
$this->len += $fragment->getPayloadLength();
$this->_frames->push($fragment); $this->_frames->push($fragment);
return $this; return $this;
@ -59,17 +66,7 @@ class Message implements \IteratorAggregate, MessageInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getPayloadLength() { public function getPayloadLength() {
$len = 0; return $this->len;
foreach ($this->_frames as $frame) {
try {
$len += $frame->getPayloadLength();
} catch (\UnderflowException $e) {
// Not an error, want the current amount buffered
}
}
return $len;
} }
/** /**
@ -120,4 +117,16 @@ class Message implements \IteratorAggregate, MessageInterface {
return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode(); return Frame::OP_BINARY === $this->_frames->bottom()->getOpcode();
} }
/**
* @return boolean
*/
public function getRsv1() {
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

@ -1,6 +1,8 @@
<?php <?php
namespace Ratchet\RFC6455\Messaging; namespace Ratchet\RFC6455\Messaging;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
class MessageBuffer { class MessageBuffer {
/** /**
* @var \Ratchet\RFC6455\Messaging\CloseFrameChecker * @var \Ratchet\RFC6455\Messaging\CloseFrameChecker
@ -37,28 +39,163 @@ class MessageBuffer {
*/ */
private $checkForMask; private $checkForMask;
/**
* @var callable
*/
private $sender;
/**
* @var string
*/
private $leftovers;
/**
* @var int
*/
private $streamingMessageOpCode = -1;
/**
* @var PermessageDeflateOptions
*/
private $permessageDeflateOptions;
/**
* @var bool
*/
private $deflateEnabled = false;
/**
* @var int
*/
private $maxMessagePayloadSize;
/**
* @var int
*/
private $maxFramePayloadSize;
/**
* @var bool
*/
private $compressedMessage;
function __construct( function __construct(
CloseFrameChecker $frameChecker, CloseFrameChecker $frameChecker,
callable $onMessage, callable $onMessage,
callable $onControl = null, callable $onControl = null,
$expectMask = true, $expectMask = true,
$exceptionFactory = null $exceptionFactory = null,
$maxMessagePayloadSize = null, // null for default - zero for no limit
$maxFramePayloadSize = null, // null for default - zero for no limit
callable $sender = null,
PermessageDeflateOptions $permessageDeflateOptions = null
) { ) {
$this->closeFrameChecker = $frameChecker; $this->closeFrameChecker = $frameChecker;
$this->checkForMask = (bool)$expectMask; $this->checkForMask = (bool)$expectMask;
$this->exceptionFactory ?: $this->exceptionFactory = function($msg) { $this->exceptionFactory ?: $exceptionFactory = function($msg) {
return new \UnderflowException($msg); return new \UnderflowException($msg);
}; };
$this->onMessage = $onMessage; $this->onMessage = $onMessage;
$this->onControl = $onControl ?: function() {}; $this->onControl = $onControl ?: function() {};
$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');
}
$this->compressedMessage = false;
$this->leftovers = '';
$memory_limit_bytes = static::getMemoryLimit();
if ($maxMessagePayloadSize === null) {
$maxMessagePayloadSize = $memory_limit_bytes / 4;
}
if ($maxFramePayloadSize === null) {
$maxFramePayloadSize = $memory_limit_bytes / 4;
}
if (!is_int($maxFramePayloadSize) || $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 (!is_int($maxMessagePayloadSize) || $maxMessagePayloadSize > 0x7FFFFFFFFFFFFFFF || $maxMessagePayloadSize < 0) {
throw new \InvalidArgumentException($maxMessagePayloadSize . 'is not a valid maxMessagePayloadSize');
}
$this->maxMessagePayloadSize = $maxMessagePayloadSize;
} }
public function onData($data) { public function onData($data) {
while (strlen($data) > 0) { $data = $this->leftovers . $data;
$data = $this->processData($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;
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);
$payload_length = $payload_length === 126
? unpack('n', $bytesToUpack)[1]
: 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 && $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);
} }
/** /**
@ -70,27 +207,30 @@ class MessageBuffer {
$this->frameBuffer ?: $this->frameBuffer = $this->newFrame(); $this->frameBuffer ?: $this->frameBuffer = $this->newFrame();
$this->frameBuffer->addBuffer($data); $this->frameBuffer->addBuffer($data);
if (!$this->frameBuffer->isCoalesced()) {
return '';
}
$onMessage = $this->onMessage; $onMessage = $this->onMessage;
$onControl = $this->onControl; $onControl = $this->onControl;
$this->frameBuffer = $this->frameCheck($this->frameBuffer); $this->frameBuffer = $this->frameCheck($this->frameBuffer);
$overflow = $this->frameBuffer->extractOverflow();
$this->frameBuffer->unMaskPayload(); $this->frameBuffer->unMaskPayload();
$opcode = $this->frameBuffer->getOpcode(); $opcode = $this->frameBuffer->getOpcode();
if ($opcode > 2) { if ($opcode > 2) {
$onControl($this->frameBuffer); $onControl($this->frameBuffer, $this);
if (Frame::OP_CLOSE === $opcode) { if (Frame::OP_CLOSE === $opcode) {
return ''; return '';
} }
} else { } 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->messageBuffer->addFrame($this->frameBuffer);
} }
@ -103,13 +243,18 @@ class MessageBuffer {
$this->messageBuffer = null; $this->messageBuffer = null;
if (true !== $msgCheck) { if (true !== $msgCheck) {
$onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload')); $onControl($this->newCloseFrame($msgCheck, 'Ratchet detected an invalid UTF-8 payload'), $this);
} else { } else {
$onMessage($msgBuffer); $onMessage($msgBuffer, $this);
}
$this->messageBuffer = null;
$this->compressedMessage = false;
if ($this->permessageDeflateOptions->getServerNoContextTakeover()) {
$this->inflator = null;
} }
} }
return $overflow;
} }
/** /**
@ -118,7 +263,7 @@ class MessageBuffer {
* @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface * @return \Ratchet\RFC6455\Messaging\FrameInterface|FrameInterface
*/ */
public function frameCheck(FrameInterface $frame) { public function frameCheck(FrameInterface $frame) {
if (false !== $frame->getRsv1() || if ((false !== $frame->getRsv1() && !$this->deflateEnabled) ||
false !== $frame->getRsv2() || false !== $frame->getRsv2() ||
false !== $frame->getRsv3() false !== $frame->getRsv3()
) { ) {
@ -230,4 +375,176 @@ class MessageBuffer {
public function newCloseFrame($code, $reason = '') { public function newCloseFrame($code, $reason = '') {
return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE); return $this->newFrame(pack('n', $code) . $reason, true, Frame::OP_CLOSE);
} }
public function sendFrame(Frame $frame) {
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($messagePayload, $final = true, $isBinary = false) {
$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 $inflator;
private function getDeflateNoContextTakeover() {
return $this->checkForMask ?
$this->permessageDeflateOptions->getServerNoContextTakeover() :
$this->permessageDeflateOptions->getClientNoContextTakeover();
}
private function getDeflateWindowBits() {
return $this->checkForMask ? $this->permessageDeflateOptions->getServerMaxWindowBits() : $this->permessageDeflateOptions->getClientMaxWindowBits();
}
private function getInflateNoContextTakeover() {
return $this->checkForMask ?
$this->permessageDeflateOptions->getClientNoContextTakeover() :
$this->permessageDeflateOptions->getServerNoContextTakeover();
}
private function getInflateWindowBits() {
return $this->checkForMask ? $this->permessageDeflateOptions->getClientMaxWindowBits() : $this->permessageDeflateOptions->getServerMaxWindowBits();
}
private function inflateFrame(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 $deflator;
private function deflateFrame(Frame $frame)
{
if ($frame->getRsv1()) {
return $frame; // frame is already deflated
}
if ($this->deflator === null) {
$bits = (int)$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($memory_limit = null) {
$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

@ -1,7 +1,9 @@
<?php <?php
namespace Ratchet\RFC6455\Test;
class AbResultsTest extends \PHPUnit_Framework_TestCase { namespace Ratchet\RFC6455\Test;
use PHPUnit\Framework\TestCase;
class AbResultsTest extends TestCase {
private function verifyAutobahnResults($fileName) { private function verifyAutobahnResults($fileName) {
if (!file_exists($fileName)) { if (!file_exists($fileName)) {
return $this->markTestSkipped('Autobahn TestSuite results not found'); return $this->markTestSkipped('Autobahn TestSuite results not found');

View File

@ -1,34 +1,37 @@
<?php <?php
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Ratchet\RFC6455\Handshake\InvalidPermessageDeflateOptionsException;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
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 React\Promise\Deferred;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
use GuzzleHttp\Psr7\HttpFactory; use GuzzleHttp\Psr7\HttpFactory;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/../bootstrap.php';
define('AGENT', 'RatchetRFC/0.0.0'); define('AGENT', 'RatchetRFC/0.3');
$testServer = "127.0.0.1"; $testServer = "127.0.0.1";
$loop = React\EventLoop\Factory::create(); $loop = React\EventLoop\Factory::create();
$dnsResolverFactory = new React\Dns\Resolver\Factory(); $connector = new Connector($loop);
$dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$factory = new \React\SocketClient\Connector($loop, $dnsResolver); function echoStreamerFactory($conn, $permessageDeflateOptions = null)
function echoStreamerFactory($conn)
{ {
$permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
return new \Ratchet\RFC6455\Messaging\MessageBuffer( return new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker, new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($conn) { function (\Ratchet\RFC6455\Messaging\MessageInterface $msg, MessageBuffer $messageBuffer) use ($conn) {
/** @var Frame $frame */ $messageBuffer->sendMessage($msg->getPayload(), true, $msg->isBinary());
foreach ($msg as $frame) {
$frame->maskPayload();
}
$conn->write($msg->getContents());
}, },
function (\Ratchet\RFC6455\Messaging\FrameInterface $frame) use ($conn) { function (\Ratchet\RFC6455\Messaging\FrameInterface $frame, MessageBuffer $messageBuffer) use ($conn) {
switch ($frame->getOpcode()) { switch ($frame->getOpcode()) {
case Frame::OP_PING: case Frame::OP_PING:
return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents()); return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents());
@ -38,27 +41,32 @@ function echoStreamerFactory($conn)
break; break;
} }
}, },
false false,
null,
null,
null,
[$conn, 'write'],
$permessageDeflateOptions
); );
} }
function getTestCases() { function getTestCases() {
global $factory;
global $testServer; global $testServer;
global $connector;
$deferred = new Deferred(); $deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(new HttpFactory()); $cn = new ClientNegotiator(new HttpFactory());
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount')); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001/getCaseCount'));
$rawResponse = ""; $rawResponse = "";
$response = null; $response = null;
/** @var \Ratchet\RFC6455\Messaging\Streaming\MessageBuffer $ms */ /** @var MessageBuffer $ms */
$ms = null; $ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) { if ($response === null) {
$rawResponse .= $data; $rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n"); $pos = strpos($rawResponse, "\r\n\r\n");
@ -68,17 +76,21 @@ function getTestCases() {
$response = \GuzzleHttp\Psr7\parse_response($rawResponse); $response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) { if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end(); $connection->end();
$deferred->reject(); $deferred->reject();
} else { } else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( $ms = new MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker, new CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { function (MessageInterface $msg) use ($deferred, $connection) {
$deferred->resolve($msg->getPayload()); $deferred->resolve($msg->getPayload());
$stream->close(); $connection->close();
}, },
null, null,
false false,
null,
null,
null,
function () {}
); );
} }
} }
@ -90,23 +102,30 @@ function getTestCases() {
} }
}); });
$stream->write(\GuzzleHttp\Psr7\str($cnRequest)); $connection->write(\GuzzleHttp\Psr7\str($cnRequest));
}); });
return $deferred->promise(); return $deferred->promise();
} }
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(
new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null);
function runTest($case) function runTest($case)
{ {
global $factory; global $connector;
global $testServer; global $testServer;
global $cn;
$casePath = "/runCase?case={$case}&agent=" . AGENT; $casePath = "/runCase?case={$case}&agent=" . AGENT;
$deferred = new Deferred(); $deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) { $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred, $casePath, $case) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(new HttpFactory()); $cn = new ClientNegotiator(
new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null);
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath)); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath));
$rawResponse = ""; $rawResponse = "";
@ -114,7 +133,7 @@ function runTest($case)
$ms = null; $ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) { if ($response === null) {
$rawResponse .= $data; $rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n"); $pos = strpos($rawResponse, "\r\n\r\n");
@ -124,10 +143,19 @@ function runTest($case)
$response = \GuzzleHttp\Psr7\parse_response($rawResponse); $response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) { if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end(); echo "Invalid response.\n";
$connection->end();
$deferred->reject(); $deferred->reject();
} else { } else {
$ms = echoStreamerFactory($stream); try {
$permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0];
$ms = echoStreamerFactory(
$connection,
$permessageDeflateOptions
);
} catch (InvalidPermessageDeflateOptionsException $e) {
$connection->end();
}
} }
} }
} }
@ -138,34 +166,36 @@ function runTest($case)
} }
}); });
$stream->on('close', function () use ($deferred) { $connection->on('close', function () use ($deferred) {
$deferred->resolve(); $deferred->resolve();
}); });
$stream->write(\GuzzleHttp\Psr7\str($cnRequest)); $connection->write(\GuzzleHttp\Psr7\str($cnRequest));
}); });
return $deferred->promise(); return $deferred->promise();
} }
function createReport() { function createReport() {
global $factory; global $connector;
global $testServer; global $testServer;
$deferred = new Deferred(); $deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) {
$reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; // $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true";
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(new HttpFactory()); // 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://127.0.0.1:9001' . $reportPath)); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath));
$rawResponse = ""; $rawResponse = "";
$response = null; $response = null;
/** @var \Ratchet\RFC6455\Messaging\MessageBuffer $ms */ /** @var MessageBuffer $ms */
$ms = null; $ms = null;
$stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) { $connection->on('data', function ($data) use ($connection, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context, $cnRequest) {
if ($response === null) { if ($response === null) {
$rawResponse .= $data; $rawResponse .= $data;
$pos = strpos($rawResponse, "\r\n\r\n"); $pos = strpos($rawResponse, "\r\n\r\n");
@ -175,17 +205,21 @@ function createReport() {
$response = \GuzzleHttp\Psr7\parse_response($rawResponse); $response = \GuzzleHttp\Psr7\parse_response($rawResponse);
if (!$cn->validateResponse($cnRequest, $response)) { if (!$cn->validateResponse($cnRequest, $response)) {
$stream->end(); $connection->end();
$deferred->reject(); $deferred->reject();
} else { } else {
$ms = new \Ratchet\RFC6455\Messaging\MessageBuffer( $ms = new MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker, new CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg) use ($deferred, $stream) { function (MessageInterface $msg) use ($deferred, $connection) {
$deferred->resolve($msg->getPayload()); $deferred->resolve($msg->getPayload());
$stream->close(); $connection->close();
}, },
null, null,
false false,
null,
null,
null,
function () {}
); );
} }
} }
@ -197,7 +231,7 @@ function createReport() {
} }
}); });
$stream->write(\GuzzleHttp\Psr7\str($cnRequest)); $connection->write(\GuzzleHttp\Psr7\str($cnRequest));
}); });
return $deferred->promise(); return $deferred->promise();
@ -215,7 +249,13 @@ getTestCases()->then(function ($count) use ($loop) {
$allDeferred->resolve(); $allDeferred->resolve();
return; return;
} }
runTest($i)->then($runNextCase); echo "Running test $i/$count...";
$startTime = microtime(true);
runTest($i)
->then(function () use ($startTime) {
echo " completed " . round((microtime(true) - $startTime) * 1000) . " ms\n";
})
->then($runNextCase);
}; };
$i = 0; $i = 0;

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

@ -2,13 +2,15 @@
"options": { "options": {
"failByDrop": false "failByDrop": false
} }
, "outdir": "./reports/servers" , "outdir": "/reports/servers"
, "servers": [{ , "servers": [{
"agent": "RatchetRFC/0.1.0" "agent": "RatchetRFC/0.3"
, "url": "ws://localhost:9001" , "url": "ws://host.ratchet.internal:9001"
, "options": {"version": 18} , "options": {"version": 18}
}] }]
, "cases": ["*"] , "cases": [
, "exclude-cases": ["6.4.*", "12.*","13.*"] "*"
]
, "exclude-cases": []
, "exclude-agent-cases": {} , "exclude-agent-cases": {}
} }

View File

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

View File

@ -4,7 +4,9 @@
"failByDrop": false "failByDrop": false
} }
, "outdir": "./reports/clients" , "outdir": "./reports/clients"
, "cases": ["*"] , "cases": [
, "exclude-cases": ["6.4.*", "12.*", "13.*"] "*"
]
, "exclude-cases": []
, "exclude-agent-cases": {} , "exclude-agent-cases": {}
} }

View File

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

View File

@ -1,11 +1,58 @@
set -x
cd tests/ab cd tests/ab
wstest -m fuzzingserver -s fuzzingserver.json & SKIP_DEFLATE=
sleep 5 if [ "$TRAVIS" = "true" ]; then
php clientRunner.php if [ $(phpenv version-name) = "hhvm" -o $(phpenv version-name) = "5.4" -o $(phpenv version-name) = "5.5" -o $(phpenv version-name) = "5.6" ]; then
echo "Skipping deflate autobahn tests for $(phpenv version-name)"
SKIP_DEFLATE=_skip_deflate
fi
fi
if [ "$ABTEST" = "client" ]; then
docker run --rm \
-d \
-v ${PWD}:/config \
-v ${PWD}/reports:/reports \
-p 9001:9001 \
--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:9001
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 \
-it \
-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
sleep 2
php startServer.php &
sleep 3
wstest -m fuzzingclient -s fuzzingclient.json

View File

@ -1,4 +1,8 @@
<?php <?php
use GuzzleHttp\Psr7\Response;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\RFC6455\Messaging\FrameInterface; use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
@ -8,52 +12,80 @@ require_once __DIR__ . "/../bootstrap.php";
$loop = \React\EventLoop\Factory::create(); $loop = \React\EventLoop\Factory::create();
$socket = new \React\Socket\Server($loop); $socket = new \React\Socket\Server('0.0.0.0:9001', $loop);
$server = new \React\Http\Server($socket);
$closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker;
$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator( $negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(
new \Ratchet\RFC6455\Handshake\RequestVerifier, new \Ratchet\RFC6455\Handshake\RequestVerifier,
new HttpFactory() new HttpFactory(),
PermessageDeflateOptions::permessageDeflateSupported()
); );
$uException = new \UnderflowException; $uException = new \UnderflowException;
$server->on('request', function (\React\Http\Request $request, \React\Http\Response $response) use ($negotiator, $closeFrameChecker, $uException) {
$psrRequest = new \GuzzleHttp\Psr7\Request($request->getMethod(), $request->getPath(), $request->getHeaders());
$negotiatorResponse = $negotiator->handshake($psrRequest); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException, $socket) {
$headerComplete = false;
$response->writeHead( $buffer = '';
$negotiatorResponse->getStatusCode(), $parser = null;
array_merge( $connection->on('data', function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException, $socket) {
$negotiatorResponse->getHeaders(), if ($headerComplete) {
["Content-Length" => "0"] $parser->onData($data);
) return;
);
if ($negotiatorResponse->getStatusCode() !== 101) {
$response->end();
return;
}
$parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker, function(MessageInterface $message) use ($response) {
$response->write($message->getContents());
}, function(FrameInterface $frame) use ($response, &$parser) {
switch ($frame->getOpCode()) {
case Frame::OP_CLOSE:
$response->end($frame->getContents());
break;
case Frame::OP_PING:
$response->write($parser->newFrame($frame->getPayload(), true, Frame::OP_PONG)->getContents());
break;
} }
}, true, function() use ($uException) {
return $uException;
});
$request->on('data', [$parser, 'onData']); $buffer .= $data;
$parts = explode("\r\n\r\n", $buffer);
if (count($parts) < 2) {
return;
}
$headerComplete = true;
$psrRequest = \GuzzleHttp\Psr7\parse_request($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(\GuzzleHttp\Psr7\str(new Response(200, [], 'Shutting down echo server.' . PHP_EOL)));
$socket->close();
return;
};
$connection->write(\GuzzleHttp\Psr7\str($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 \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker,
function (MessageInterface $message, MessageBuffer $messageBuffer) {
$messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary());
}, function (FrameInterface $frame) use ($connection, &$parser) {
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, function () use ($uException) {
return $uException;
},
null,
null,
[$connection, 'write'],
$deflateOptions);
array_shift($parts);
$parser->onData(implode("\r\n\r\n", $parts));
});
}); });
$socket->listen(9001, '0.0.0.0');
$loop->run(); $loop->run();

View File

@ -0,0 +1,30 @@
<?php
namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use PHPUnit\Framework\TestCase;
class PermessageDeflateOptionsTest extends TestCase
{
public static function versionSupportProvider() {
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($version, $supported) {
$this->assertEquals($supported, PermessageDeflateOptions::permessageDeflateSupported($version));
}
}

View File

@ -1,11 +1,14 @@
<?php <?php
namespace Ratchet\RFC6455\Test\Unit\Handshake; namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\RequestVerifier; use Ratchet\RFC6455\Handshake\RequestVerifier;
use PHPUnit\Framework\TestCase;
/** /**
* @covers Ratchet\RFC6455\Handshake\RequestVerifier * @covers Ratchet\RFC6455\Handshake\RequestVerifier
*/ */
class RequestVerifierTest extends \PHPUnit_Framework_TestCase { class RequestVerifierTest extends TestCase {
/** /**
* @var RequestVerifier * @var RequestVerifier
*/ */

View File

@ -1,11 +1,14 @@
<?php <?php
namespace Ratchet\RFC6455\Test\Unit\Handshake; namespace Ratchet\RFC6455\Test\Unit\Handshake;
use Ratchet\RFC6455\Handshake\ResponseVerifier; use Ratchet\RFC6455\Handshake\ResponseVerifier;
use PHPUnit\Framework\TestCase;
/** /**
* @covers Ratchet\RFC6455\Handshake\ResponseVerifier * @covers Ratchet\RFC6455\Handshake\ResponseVerifier
*/ */
class ResponseVerifierTest extends \PHPUnit_Framework_TestCase { class ResponseVerifierTest extends TestCase {
/** /**
* @var ResponseVerifier * @var ResponseVerifier
*/ */

View File

@ -2,12 +2,12 @@
namespace Ratchet\RFC6455\Test\Unit\Handshake; namespace Ratchet\RFC6455\Test\Unit\Handshake;
use GuzzleHttp\Psr7\HttpFactory;
use Ratchet\RFC6455\Handshake\RequestVerifier; use Ratchet\RFC6455\Handshake\RequestVerifier;
use Ratchet\RFC6455\Handshake\ServerNegotiator; use Ratchet\RFC6455\Handshake\ServerNegotiator;
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Psr7\HttpFactory; class ServerNegotiatorTest extends TestCase
class ServerNegotiatorTest extends \PHPUnit_Framework_TestCase
{ {
public function testNoUpgradeRequested() { public function testNoUpgradeRequested() {
$negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory()); $negotiator = new ServerNegotiator(new RequestVerifier(), new HttpFactory());

View File

@ -1,13 +1,16 @@
<?php <?php
namespace Ratchet\RFC6455\Test\Unit\Messaging; namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
use PHPUnit\Framework\TestCase;
/** /**
* @covers Ratchet\RFC6455\Messaging\Frame * @covers Ratchet\RFC6455\Messaging\Frame
* @todo getMaskingKey, getPayloadStartingByte don't have tests yet * @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. * @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry.
*/ */
class FrameTest extends \PHPUnit_Framework_TestCase { class FrameTest extends TestCase {
protected $_firstByteFinText = '10000001'; protected $_firstByteFinText = '10000001';
protected $_secondByteMaskedSPL = '11111101'; protected $_secondByteMaskedSPL = '11111101';

View File

@ -7,8 +7,9 @@ use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\Message; use Ratchet\RFC6455\Messaging\Message;
use Ratchet\RFC6455\Messaging\MessageBuffer; use Ratchet\RFC6455\Messaging\MessageBuffer;
use React\EventLoop\Factory; use React\EventLoop\Factory;
use PHPUnit\Framework\TestCase;
class MessageBufferTest extends \PHPUnit_Framework_TestCase class MessageBufferTest extends TestCase
{ {
/** /**
* This is to test that MessageBuffer can handle a large receive * This is to test that MessageBuffer can handle a large receive
@ -69,4 +70,299 @@ class MessageBufferTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($bReceived); $this->assertTrue($bReceived);
} }
public function testInvalidFrameLength() {
$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(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$this->assertEquals(Frame::OP_CLOSE, $controlFrame->getOpcode());
$this->assertEquals([Frame::CLOSE_PROTOCOL], array_merge(unpack('n*', substr($controlFrame->getPayload(), 0, 2))));
}
public function testFrameLengthTooBig() {
$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(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
0,
10
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$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() {
$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(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$this->assertNull($controlFrame);
$controlFrame = $frame;
},
false,
null,
100,
0
);
$messageBuffer->onData($frameRaw);
$this->assertEquals(0, $messageCount);
$this->assertTrue($controlFrame instanceof Frame);
$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() {
$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(),
function (Message $message) use (&$messageCount) {
$messageCount++;
},
function (Frame $frame) use (&$controlFrame) {
$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->assertTrue($controlFrame instanceof Frame);
$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($phpConfigurationValue, $expectedLimit) {
$method = new \ReflectionMethod('Ratchet\RFC6455\Messaging\MessageBuffer', 'getMemoryLimit');
$method->setAccessible(true);
$actualLimit = $method->invoke(null, $phpConfigurationValue);
$this->assertSame($expectedLimit, $actualLimit);
}
public function phpConfigurationProvider() {
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]
];
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidMaxFramePayloadSizes() {
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null,
0,
0x8000000000000000
);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidMaxMessagePayloadSizes() {
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
false,
null,
0x8000000000000000,
0
);
}
/**
* @dataProvider phpConfigurationProvider
*
* @param string $phpConfigurationValue
* @param int $expectedLimit
*
* @runInSeparateProcess
* @requires PHP 7.0
*/
public function testIniSizes($phpConfigurationValue, $expectedLimit) {
ini_set('memory_limit', $phpConfigurationValue);
$messageBuffer = new MessageBuffer(
new CloseFrameChecker(),
function (Message $message) {},
function (Frame $frame) {},
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
* @requires PHP 7.0
*/
public function testInvalidIniSize() {
ini_set('memory_limit', 'lots of memory');
$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

@ -1,12 +1,15 @@
<?php <?php
namespace Ratchet\RFC6455\Test\Unit\Messaging; namespace Ratchet\RFC6455\Test\Unit\Messaging;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\Message; use Ratchet\RFC6455\Messaging\Message;
use PHPUnit\Framework\TestCase;
/** /**
* @covers Ratchet\RFC6455\Messaging\Message * @covers Ratchet\RFC6455\Messaging\Message
*/ */
class MessageTest extends \PHPUnit_Framework_TestCase { class MessageTest extends TestCase {
/** @var Message */ /** @var Message */
protected $message; protected $message;