Client passes all per message deflate autobahn tests
This commit is contained in:
parent
8eed9e7db2
commit
b84ef8b9ce
@ -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) {
|
||||
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Ratchet\RFC6455\Handshake;
|
||||
|
||||
class InvalidPermessageDeflateOptionsException extends \Exception
|
||||
{
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user