From b84ef8b9ce6e4050b60ddbb80600263d18c41d96 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sat, 11 Mar 2017 02:16:23 -0500 Subject: [PATCH] Client passes all per message deflate autobahn tests --- src/Handshake/ClientNegotiator.php | 21 ++-- ...validPermessageDeflateOptionsException.php | 7 ++ src/Handshake/PermessageDeflateOptions.php | 107 +++++++++++++++--- src/Handshake/ResponseVerifier.php | 18 +-- src/Handshake/ServerNegotiator.php | 7 +- src/Messaging/MessageBuffer.php | 2 +- tests/ab/clientRunner.php | 13 ++- 7 files changed, 127 insertions(+), 48 deletions(-) create mode 100644 src/Handshake/InvalidPermessageDeflateOptionsException.php diff --git a/src/Handshake/ClientNegotiator.php b/src/Handshake/ClientNegotiator.php index b4394df..a514eb7 100644 --- a/src/Handshake/ClientNegotiator.php +++ b/src/Handshake/ClientNegotiator.php @@ -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) { diff --git a/src/Handshake/InvalidPermessageDeflateOptionsException.php b/src/Handshake/InvalidPermessageDeflateOptionsException.php new file mode 100644 index 0000000..191e7a5 --- /dev/null +++ b/src/Handshake/InvalidPermessageDeflateOptionsException.php @@ -0,0 +1,7 @@ +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); } -} \ No newline at end of file + + 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); + } +} diff --git a/src/Handshake/ResponseVerifier.php b/src/Handshake/ResponseVerifier.php index 7dc3921..38904e8 100644 --- a/src/Handshake/ResponseVerifier.php +++ b/src/Handshake/ResponseVerifier.php @@ -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 - ]; - } -} \ No newline at end of file +} diff --git a/src/Handshake/ServerNegotiator.php b/src/Handshake/ServerNegotiator.php index 8ff15a9..9e72776 100644 --- a/src/Handshake/ServerNegotiator.php +++ b/src/Handshake/ServerNegotiator.php @@ -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')); // }); - $perMessageDeflateRequest = PermessageDeflateOptions::fromRequestOrResponse($request)[0]; + 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); } diff --git a/src/Messaging/MessageBuffer.php b/src/Messaging/MessageBuffer.php index b5aba3e..dbf704c 100644 --- a/src/Messaging/MessageBuffer.php +++ b/src/Messaging/MessageBuffer.php @@ -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(); diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 2cd963e..b51c4c0 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -1,6 +1,7 @@ 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 ); } }