diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 738288e4..59ccb15a 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -44,6 +44,14 @@ Improvements * :func:`~asyncio.client.connect` now closes connections with close code 1011 (internal error) when exiting the context manager with an exception. +Bug fixes +......... + +* Negotiating ``permessage-deflate`` no longer fails with a :exc:`ValueError` + when the peer asks for a window of 8 bits on the side that compresses. + :mod:`zlib` cannot create a raw deflate compressor with such a window, so + the extension is declined instead. + .. _17.1: 17.1 diff --git a/src/websockets/extensions/permessage_deflate.py b/src/websockets/extensions/permessage_deflate.py index 50276637..0c926f69 100644 --- a/src/websockets/extensions/permessage_deflate.py +++ b/src/websockets/extensions/permessage_deflate.py @@ -464,6 +464,12 @@ def process_response_params( elif client_max_window_bits > self.client_max_window_bits: raise NegotiationError("unsupported client_max_window_bits") + # zlib cannot build a raw deflate compressor with an 8 bit window, so + # 8 is unusable for the window we compress with, even though it is a + # legal value on the wire and fine for the window we decompress with. + if client_max_window_bits == 8: + raise NegotiationError("unsupported client_max_window_bits") + return PerMessageDeflate( server_no_context_takeover, # remote_no_context_takeover client_no_context_takeover, # local_no_context_takeover @@ -658,6 +664,13 @@ def process_request_params( elif self.client_max_window_bits < client_max_window_bits: client_max_window_bits = self.client_max_window_bits + # zlib cannot build a raw deflate compressor with an 8 bit window, so + # 8 is unusable for the window we compress with, even though it is a + # legal value on the wire and fine for the window we decompress with. + # RFC 7692 7.1.2.1 lets a server decline an offer it cannot support. + if server_max_window_bits == 8: + raise NegotiationError("unsupported server_max_window_bits") + return ( _build_parameters( server_no_context_takeover, diff --git a/tests/extensions/test_permessage_deflate.py b/tests/extensions/test_permessage_deflate.py index 71f8e8e4..bdb88113 100644 --- a/tests/extensions/test_permessage_deflate.py +++ b/tests/extensions/test_permessage_deflate.py @@ -463,6 +463,12 @@ def test_process_response_params(self): [("server_max_window_bits", "7")], NegotiationError, ), + ( + # 8 is legal for the window we decompress with. + (False, False, None, None), + [("server_max_window_bits", "8")], + (False, False, 8, 15), + ), ( (False, False, None, None), [("server_max_window_bits", "10")], @@ -519,6 +525,12 @@ def test_process_response_params(self): [("client_max_window_bits", "7")], NegotiationError, ), + ( + # 8 is unusable for the window we compress with. + (False, False, None, True), + [("client_max_window_bits", "8")], + NegotiationError, + ), ( (False, False, None, True), [("client_max_window_bits", "10")], @@ -774,6 +786,14 @@ def test_process_request_params(self): None, NegotiationError, ), + ( + # 8 is unusable for the window we compress with; RFC 7692 + # 7.1.2.1 lets the server decline the offer. + (False, False, None, None), + [("server_max_window_bits", "8")], + None, + NegotiationError, + ), ( (False, False, None, None), [("server_max_window_bits", "10")], @@ -835,6 +855,13 @@ def test_process_request_params(self): None, InvalidParameterValue, ), + ( + # 8 is legal for the window we decompress with. + (False, False, None, None), + [("client_max_window_bits", "8")], + [("client_max_window_bits", "8")], # doesn't matter + (False, False, 8, 15), + ), ( (False, False, None, None), [("client_max_window_bits", "10")],