Client passes all per message deflate autobahn tests

This commit is contained in:
Matt Bonneau 2017-03-11 02:16:23 -05:00
parent 8eed9e7db2
commit b84ef8b9ce
7 changed files with 127 additions and 48 deletions

View File

@ -16,7 +16,7 @@ class ClientNegotiator {
*/
private $defaultHeader;
function __construct($enablePerMessageDeflate = false) {
function __construct(PermessageDeflateOptions $perMessageDeflateOptions = null) {
$this->verifier = new ResponseVerifier;
$this->defaultHeader = new Request('GET', '', [
@ -26,18 +26,19 @@ class ClientNegotiator {
, 'User-Agent' => "Ratchet"
]);
if ($enablePerMessageDeflate && (version_compare(PHP_VERSION, '7.0.15', '<') || version_compare(PHP_VERSION, '7.1.0', '='))) {
$enablePerMessageDeflate = false;
// https://bugs.php.net/bug.php?id=73373
if ($perMessageDeflateOptions === null) {
$perMessageDeflateOptions = PermessageDeflateOptions::createDisabled();
}
if ($enablePerMessageDeflate && !function_exists('deflate_add')) {
$enablePerMessageDeflate = false;
if (
version_compare(PHP_VERSION, '7.0.15', '<')
|| version_compare(PHP_VERSION, '7.1.0', '=')
|| !function_exists('deflate_add')
) {
$perMessageDeflateOptions = PermessageDeflateOptions::createDisabled();
}
if ($enablePerMessageDeflate) {
$this->defaultHeader = $this->defaultHeader->withAddedHeader(
'Sec-WebSocket-Extensions',
'permessage-deflate; client_max_window_bits');
}
$this->defaultHeader = $perMessageDeflateOptions->addHeaderToRequest($this->defaultHeader);
}
public function generateRequest(UriInterface $uri) {

View File

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

View File

@ -8,7 +8,10 @@ use Psr\Http\Message\ResponseInterface;
final class PermessageDeflateOptions
{
private $deflate = false; // disable by default
const MAX_WINDOW_BITS = 15;
const VALID_BITS = ['8', '9', '10', '11', '12', '13', '14', '15'];
private $deflate = false;
private $server_no_context_takeover;
private $client_no_context_takeover;
@ -17,6 +20,56 @@ final class PermessageDeflateOptions
private function __construct() { }
public static function createDefault() {
$new = new static();
$new->deflate = 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
@ -49,34 +102,33 @@ final class PermessageDeflateOptions
$key = $kv[0];
$value = count($kv) > 1 ? $kv[1] : null;
$validBits = ['8', '9', '10', '11', '12', '13', '14', '15'];
switch ($key) {
case "server_no_context_takeover":
case "client_no_context_takeover":
if ($value !== null) {
throw new \Exception($key . ' must not have a value.');
throw new InvalidPermessageDeflateOptionsException($key . ' must not have a value.');
}
$value = true;
break;
case "server_max_window_bits":
if (!in_array($value, $validBits)) {
throw new \Exception($key . ' must have a value between 8 and 15.');
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, $validBits)) {
throw new \Exception($key . ' must have no value or a value between 8 and 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 \Exception('Option "' . $key . '"is not valid for this extension');
throw new InvalidPermessageDeflateOptionsException('Option "' . $key . '"is not valid for permessage deflate');
}
if ($options->$key !== null) {
throw new \Exception('Key specified more than once. Connection must be declined.');
throw new InvalidPermessageDeflateOptionsException($key . ' specified more than once. Connection must be declined.');
}
$options->$key = $value;
@ -99,14 +151,10 @@ final class PermessageDeflateOptions
return $optionSets;
}
public static function createDisabled() {
return new static();
}
public static function validateResponseToRequest(ResponseInterface $response, RequestInterface $request) {
$requestOptions = static::fromRequestOrResponse($request);
$responseOptions = static::fromRequestOrResponse($response);
}
// public static function validateResponseToRequest(ResponseInterface $response, RequestInterface $request) {
// $requestOptions = static::fromRequestOrResponse($request);
// $responseOptions = static::fromRequestOrResponse($response);
// }
/**
* @return mixed
@ -174,4 +222,27 @@ final class PermessageDeflateOptions
return $response->withAddedHeader('Sec-Websocket-Extensions', $header);
}
public function addHeaderToRequest(RequestInterface $request) {
if (!$this->deflate) {
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);
}
}

View File

@ -56,23 +56,9 @@ class ResponseVerifier {
public function verifyExtensions(array $requestHeader, array $responseHeader) {
if (in_array('permessage-deflate', $responseHeader)) {
return in_array('permessage-deflate', $requestHeader);
return strpos(implode(',', $requestHeader), 'permessage-deflate') !== false ? 1 : 0;
}
return 1;
}
public function getPermessageDeflateOptions(array $requestHeader, array $responseHeader) {
if (!$this->verifyExtensions($requestHeader, $responseHeader)) {
return false;
}
return [
'deflate' => in_array('permessage-deflate', $responseHeader),
'no_context_takeover' => false,
'max_window_bits' => null,
'request_no_context_takeover' => false,
'request_max_window_bits' => null
];
}
}

View File

@ -119,7 +119,12 @@ class ServerNegotiator implements NegotiatorInterface {
// $perMessageDeflate = array_filter($request->getHeader('Sec-WebSocket-Extensions'), function ($x) {
// return 'permessage-deflate' === substr($x, 0, strlen('permessage-deflate'));
// });
try {
$perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0];
} catch (InvalidPermessageDeflateOptionsException $e) {
return new Response(400, [], null, '1.1', $e->getMessage());
}
if ($this->enablePerMessageDeflate && $perMessageDeflateRequest->getDeflate()) {
$response = $perMessageDeflateRequest->addHeaderToResponse($response);
}

View File

@ -81,7 +81,7 @@ class MessageBuffer {
$this->sender = $sender;
$this->permessageDeflateOptions = $permessageDeflateOptions ? $permessageDeflateOptions : PermessageDeflateOptions::createDisabled();
$this->permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
$this->deflate = $this->permessageDeflateOptions->getDeflate();

View File

@ -1,6 +1,7 @@
<?php
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;
use Ratchet\RFC6455\Handshake\InvalidPermessageDeflateOptionsException;
use Ratchet\RFC6455\Handshake\PermessageDeflateOptions;
use Ratchet\RFC6455\Handshake\ResponseVerifier;
use Ratchet\RFC6455\Messaging\MessageBuffer;
@ -22,6 +23,8 @@ $factory = new \React\SocketClient\Connector($loop, $dnsResolver);
function echoStreamerFactory($conn, $permessageDeflateOptions = null)
{
$permessageDeflateOptions = $permessageDeflateOptions ?: PermessageDeflateOptions::createDisabled();
return new \Ratchet\RFC6455\Messaging\MessageBuffer(
new \Ratchet\RFC6455\Messaging\CloseFrameChecker,
function (\Ratchet\RFC6455\Messaging\MessageInterface $msg, MessageBuffer $messageBuffer) use ($conn) {
@ -109,7 +112,7 @@ function runTest($case)
$deferred = new Deferred();
$factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred, $casePath, $case) {
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(true);
$cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator(PermessageDeflateOptions::createDefault());
/** @var RequestInterface $cnRequest */
$cnRequest = $cn->generateRequest(new Uri('ws://127.0.0.1:9001' . $casePath));
@ -132,9 +135,15 @@ function runTest($case)
$stream->end();
$deferred->reject();
} else {
try {
$permessageDeflateOptions = PermessageDeflateOptions::fromRequestOrResponse($response)[0];
} catch (InvalidPermessageDeflateOptionsException $e) {
$stream->end();
}
$ms = echoStreamerFactory(
$stream,
PermessageDeflateOptions::fromRequestOrResponse($response)[0]
$permessageDeflateOptions
);
}
}