Merge pull request #23 from ratchetphp/permessage-deflate

Permessage deflate
This commit is contained in:
Chris Boden 2020-05-07 15:52:21 -04:00 committed by GitHub
commit d8075617ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 864 additions and 82 deletions

View File

@ -1,5 +1,7 @@
language: php language: php
services: docker
php: php:
- 5.6 - 5.6
- 7.0 - 7.0
@ -7,11 +9,18 @@ php:
- 7.2 - 7.2
- 7.3 - 7.3
- 7.4 - 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,14 +30,16 @@
"guzzlehttp/psr7": "^1.0" "guzzlehttp/psr7": "^1.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "4.8.*", "phpunit/phpunit": "5.7.*",
"react/socket": "^1.3" "react/socket": "^1.3"
}, },
"scripts": { "scripts": {
"abtests": "sh tests/ab/run_ab_tests.sh", "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", "phpunit": "phpunit --colors=always",
"test": [ "test": [
"@abtests", "@abtest-client",
"@abtest-server",
"@phpunit" "@phpunit"
] ]
} }

View File

@ -16,7 +16,7 @@ class ClientNegotiator {
*/ */
private $defaultHeader; private $defaultHeader;
function __construct() { function __construct(PermessageDeflateOptions $perMessageDeflateOptions = null) {
$this->verifier = new ResponseVerifier; $this->verifier = new ResponseVerifier;
$this->defaultHeader = new Request('GET', '', [ $this->defaultHeader = new Request('GET', '', [
@ -25,6 +25,24 @@ class ClientNegotiator {
, 'Sec-WebSocket-Version' => $this->getVersion() , 'Sec-WebSocket-Version' => $this->getVersion()
, 'User-Agent' => "Ratchet" , '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

@ -17,8 +17,22 @@ class ServerNegotiator implements NegotiatorInterface {
private $_strictSubProtocols = false; private $_strictSubProtocols = false;
public function __construct(RequestVerifier $requestVerifier) { private $enablePerMessageDeflate = false;
public function __construct(RequestVerifier $requestVerifier, $enablePerMessageDeflate = false) {
$this->verifier = $requestVerifier; $this->verifier = $requestVerifier;
// 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;
} }
/** /**
@ -97,12 +111,24 @@ class ServerNegotiator implements NegotiatorInterface {
} }
} }
return new Response(101, array_merge($headers, [ $response = new Response(101, array_merge($headers, [
'Upgrade' => 'websocket' 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade' , 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0]) , 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')[0])
, 'X-Powered-By' => 'Ratchet' , '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

@ -117,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,11 +39,31 @@ class MessageBuffer {
*/ */
private $checkForMask; private $checkForMask;
/**
* @var callable
*/
private $sender;
/** /**
* @var string * @var string
*/ */
private $leftovers; private $leftovers;
/**
* @var int
*/
private $streamingMessageOpCode = -1;
/**
* @var PermessageDeflateOptions
*/
private $permessageDeflateOptions;
/**
* @var bool
*/
private $deflateEnabled = false;
/** /**
* @var int * @var int
*/ */
@ -52,6 +74,11 @@ class MessageBuffer {
*/ */
private $maxFramePayloadSize; private $maxFramePayloadSize;
/**
* @var bool
*/
private $compressedMessage;
function __construct( function __construct(
CloseFrameChecker $frameChecker, CloseFrameChecker $frameChecker,
callable $onMessage, callable $onMessage,
@ -59,18 +86,32 @@ class MessageBuffer {
$expectMask = true, $expectMask = true,
$exceptionFactory = null, $exceptionFactory = null,
$maxMessagePayloadSize = null, // null for default - zero for no limit $maxMessagePayloadSize = null, // null for default - zero for no limit
$maxFramePayloadSize = 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 = ''; $this->leftovers = '';
$memory_limit_bytes = static::getMemoryLimit(); $memory_limit_bytes = static::getMemoryLimit();
@ -177,12 +218,19 @@ class MessageBuffer {
$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);
} }
@ -195,9 +243,16 @@ 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;
} }
} }
} }
@ -208,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()
) { ) {
@ -321,6 +376,158 @@ class MessageBuffer {
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 * This is a separate function for testing purposes
* $memory_limit is only used for testing * $memory_limit is only used for testing

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,9 +1,10 @@
<?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\Handshake\ClientNegotiator;
use Ratchet\RFC6455\Messaging\CloseFrameChecker; use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\RFC6455\Messaging\MessageInterface;
use React\Promise\Deferred; use React\Promise\Deferred;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
@ -12,7 +13,7 @@ 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";
@ -20,18 +21,16 @@ $loop = React\EventLoop\Factory::create();
$connector = new Connector($loop); $connector = new Connector($loop);
function echoStreamerFactory($conn) function echoStreamerFactory($conn, $permessageDeflateOptions = null)
{ {
return new MessageBuffer( $permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
new CloseFrameChecker,
function (MessageInterface $msg) use ($conn) { return new \Ratchet\RFC6455\Messaging\MessageBuffer(
/** @var Frame $frame */ new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
foreach ($msg as $frame) { function (\Ratchet\RFC6455\Messaging\MessageInterface $msg, MessageBuffer $messageBuffer) use ($conn) {
$frame->maskPayload(); $messageBuffer->sendMessage($msg->getPayload(), true, $msg->isBinary());
}
$conn->write($msg->getContents());
}, },
function (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());
@ -41,7 +40,12 @@ function echoStreamerFactory($conn)
break; break;
} }
}, },
false false,
null,
null,
null,
[$conn, 'write'],
$permessageDeflateOptions
); );
} }
@ -81,7 +85,11 @@ function getTestCases() {
$connection->close(); $connection->close();
}, },
null, null,
false false,
null,
null,
null,
function () {}
); );
} }
} }
@ -99,17 +107,22 @@ function getTestCases() {
return $deferred->promise(); return $deferred->promise();
} }
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(
PermessageDeflateOptions::permessageDeflateSupported() ? PermessageDeflateOptions::createEnabled() : null);
function runTest($case) function runTest($case)
{ {
global $connector; 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();
$connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred, $casePath, $case) { $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred, $casePath, $case) {
$cn = new ClientNegotiator(); $cn = new ClientNegotiator(
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 = "";
@ -127,10 +140,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)) {
echo "Invalid response.\n";
$connection->end(); $connection->end();
$deferred->reject(); $deferred->reject();
} else { } else {
$ms = echoStreamerFactory($connection); try {
$permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0];
$ms = echoStreamerFactory(
$connection,
$permessageDeflateOptions
);
} catch (InvalidPermessageDeflateOptionsException $e) {
$connection->end();
}
} }
} }
} }
@ -158,7 +180,9 @@ function createReport() {
$deferred = new Deferred(); $deferred = new Deferred();
$connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) { $connector->connect($testServer . ':9001')->then(function (ConnectionInterface $connection) use ($deferred) {
$reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; // $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(); $cn = new ClientNegotiator();
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath)); $cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $reportPath));
@ -183,12 +207,16 @@ function createReport() {
} else { } else {
$ms = new MessageBuffer( $ms = new MessageBuffer(
new CloseFrameChecker, new CloseFrameChecker,
function (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 () {}
); );
} }
} }
@ -218,7 +246,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,13 +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
sleep 1
kill $(ps aux | grep 'php startServer.php' | awk '{print $2}' | head -n 1)

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;
@ -7,18 +11,19 @@ require_once __DIR__ . "/../bootstrap.php";
$loop = \React\EventLoop\Factory::create(); $loop = \React\EventLoop\Factory::create();
$socket = new \React\Socket\Server('127.0.0.1:9001', $loop); $socket = new \React\Socket\Server('0.0.0.0:9001', $loop);
$closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker; $closeFrameChecker = new \Ratchet\RFC6455\Messaging\CloseFrameChecker;
$negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier); $negotiator = new \Ratchet\RFC6455\Handshake\ServerNegotiator(new \Ratchet\RFC6455\Handshake\RequestVerifier, PermessageDeflateOptions::permessageDeflateSupported());
$uException = new \UnderflowException; $uException = new \UnderflowException;
$socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException) {
$socket->on('connection', function (React\Socket\ConnectionInterface $connection) use ($negotiator, $closeFrameChecker, $uException, $socket) {
$headerComplete = false; $headerComplete = false;
$buffer = ''; $buffer = '';
$parser = null; $parser = null;
$connection->on('data', function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException) { $connection->on('data', function ($data) use ($connection, &$parser, &$headerComplete, &$buffer, $negotiator, $closeFrameChecker, $uException, $socket) {
if ($headerComplete) { if ($headerComplete) {
$parser->onData($data); $parser->onData($data);
return; return;
@ -35,6 +40,12 @@ $socket->on('connection', function (React\Socket\ConnectionInterface $connection
$negotiatorResponse = $negotiatorResponse->withAddedHeader("Content-Length", "0"); $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)); $connection->write(\GuzzleHttp\Psr7\str($negotiatorResponse));
if ($negotiatorResponse->getStatusCode() !== 101) { if ($negotiatorResponse->getStatusCode() !== 101) {
@ -42,9 +53,13 @@ $socket->on('connection', function (React\Socket\ConnectionInterface $connection
return; 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, $parser = new \Ratchet\RFC6455\Messaging\MessageBuffer($closeFrameChecker,
function (MessageInterface $message) use ($connection) { function (MessageInterface $message, MessageBuffer $messageBuffer) {
$connection->write($message->getContents()); $messageBuffer->sendMessage($message->getPayload(), true, $message->isBinary());
}, function (FrameInterface $frame) use ($connection, &$parser) { }, function (FrameInterface $frame) use ($connection, &$parser) {
switch ($frame->getOpCode()) { switch ($frame->getOpCode()) {
case Frame::OP_CLOSE: case Frame::OP_CLOSE:
@ -56,7 +71,11 @@ $socket->on('connection', function (React\Socket\ConnectionInterface $connection
} }
}, true, function () use ($uException) { }, true, function () use ($uException) {
return $uException; return $uException;
}); },
null,
null,
[$connection, 'write'],
$deflateOptions);
array_shift($parts); array_shift($parts);
$parser->onData(implode("\r\n\r\n", $parts)); $parser->onData(implode("\r\n\r\n", $parts));

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

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

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

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;