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

@ -8,7 +8,10 @@ use Psr\Http\Message\ResponseInterface;
final class PermessageDeflateOptions 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 $server_no_context_takeover;
private $client_no_context_takeover; private $client_no_context_takeover;
@ -17,6 +20,56 @@ final class PermessageDeflateOptions
private function __construct() { } 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/rfc6455#section-9.1
* https://tools.ietf.org/html/rfc7692#section-7 * https://tools.ietf.org/html/rfc7692#section-7
@ -49,34 +102,33 @@ final class PermessageDeflateOptions
$key = $kv[0]; $key = $kv[0];
$value = count($kv) > 1 ? $kv[1] : null; $value = count($kv) > 1 ? $kv[1] : null;
$validBits = ['8', '9', '10', '11', '12', '13', '14', '15'];
switch ($key) { switch ($key) {
case "server_no_context_takeover": case "server_no_context_takeover":
case "client_no_context_takeover": case "client_no_context_takeover":
if ($value !== null) { if ($value !== null) {
throw new \Exception($key . ' must not have a value.'); throw new InvalidPermessageDeflateOptionsException($key . ' must not have a value.');
} }
$value = true; $value = true;
break; break;
case "server_max_window_bits": case "server_max_window_bits":
if (!in_array($value, $validBits)) { if (!in_array($value, self::VALID_BITS)) {
throw new \Exception($key . ' must have a value between 8 and 15.'); throw new InvalidPermessageDeflateOptionsException($key . ' must have a value between 8 and 15.');
} }
break; break;
case "client_max_window_bits": case "client_max_window_bits":
if ($value === null) { if ($value === null) {
$value = '15'; $value = '15';
} }
if (!in_array($value, $validBits)) { if (!in_array($value, self::VALID_BITS)) {
throw new \Exception($key . ' must have no value or a value between 8 and 15.'); throw new InvalidPermessageDeflateOptionsException($key . ' must have no value or a value between 8 and 15.');
} }
break; break;
default: 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) { 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; $options->$key = $value;
@ -99,14 +151,10 @@ final class PermessageDeflateOptions
return $optionSets; return $optionSets;
} }
public static function createDisabled() { // public static function validateResponseToRequest(ResponseInterface $response, RequestInterface $request) {
return new static(); // $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 * @return mixed
@ -174,4 +222,27 @@ final class PermessageDeflateOptions
return $response->withAddedHeader('Sec-Websocket-Extensions', $header); 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) { public function verifyExtensions(array $requestHeader, array $responseHeader) {
if (in_array('permessage-deflate', $responseHeader)) { if (in_array('permessage-deflate', $responseHeader)) {
return in_array('permessage-deflate', $requestHeader); return strpos(implode(',', $requestHeader), 'permessage-deflate') !== false ? 1 : 0;
} }
return 1; 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) { // $perMessageDeflate = array_filter($request->getHeader('Sec-WebSocket-Extensions'), function ($x) {
// return 'permessage-deflate' === substr($x, 0, strlen('permessage-deflate')); // return 'permessage-deflate' === substr($x, 0, strlen('permessage-deflate'));
// }); // });
try {
$perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0]; $perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0];
} catch (InvalidPermessageDeflateOptionsException $e) {
return new Response(400, [], null, '1.1', $e->getMessage());
}
if ($this->enablePerMessageDeflate && $perMessageDeflateRequest->getDeflate()) { if ($this->enablePerMessageDeflate && $perMessageDeflateRequest->getDeflate()) {
$response = $perMessageDeflateRequest->addHeaderToResponse($response); $response = $perMessageDeflateRequest->addHeaderToResponse($response);
} }

View File

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

View File

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