diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 3e147f850..4420e8f86 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -44,6 +44,8 @@ Improvements * :func:`~asyncio.client.connect` now closes connections with close code 1011 (internal error) when exiting the context manager with an exception. +* Improved error handling when a compression window of size 8 is requested. + Bug fixes ......... diff --git a/src/websockets/extensions/permessage_deflate.py b/src/websockets/extensions/permessage_deflate.py index 502766379..05c629bc9 100644 --- a/src/websockets/extensions/permessage_deflate.py +++ b/src/websockets/extensions/permessage_deflate.py @@ -56,7 +56,10 @@ def __init__( assert remote_no_context_takeover in [False, True] assert local_no_context_takeover in [False, True] assert 8 <= remote_max_window_bits <= 15 - assert 8 <= local_max_window_bits <= 15 + # Due to https://github.com/madler/zlib/issues/171, zlib.compressobj + # rejects wbits=-8 with ValueError: Invalid initialization option. + # This makes it impossible to support local_max_window_bits = 8. + assert 9 <= local_max_window_bits <= 15 assert "wbits" not in compress_settings self.remote_no_context_takeover = remote_no_context_takeover @@ -314,11 +317,13 @@ class ClientPerMessageDeflateFactory(ClientExtensionFactory): server_max_window_bits: Maximum size of the server's LZ77 sliding window in bits, between 8 and 15. client_max_window_bits: Maximum size of the client's LZ77 sliding window - in bits, between 8 and 15, or :obj:`True` to indicate support without - setting a limit. + in bits, between 9 and 15, or :obj:`True` to indicate support without + setting a limit. 8 isn't supported due to a `limitation of zlib`_. compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, excluding ``wbits``. + .. _limitation of zlib: https://github.com/madler/zlib/issues/171 + """ name = ExtensionName("permessage-deflate") @@ -340,9 +345,9 @@ def __init__( if not ( client_max_window_bits is None or client_max_window_bits is True - or 8 <= client_max_window_bits <= 15 + or 9 <= client_max_window_bits <= 15 ): - raise ValueError("client_max_window_bits must be between 8 and 15") + raise ValueError("client_max_window_bits must be between 9 and 15") if compress_settings is not None and "wbits" in compress_settings: raise ValueError( "compress_settings must not include wbits, " @@ -464,6 +469,9 @@ def process_response_params( elif client_max_window_bits > self.client_max_window_bits: raise NegotiationError("unsupported client_max_window_bits") + if client_max_window_bits == 8: + raise NegotiationError("unsupported client_max_window_bits = 8, use 9") + return PerMessageDeflate( server_no_context_takeover, # remote_no_context_takeover client_no_context_takeover, # local_no_context_takeover @@ -512,7 +520,8 @@ class ServerPerMessageDeflateFactory(ServerExtensionFactory): server_no_context_takeover: Prevent server from using context takeover. client_no_context_takeover: Prevent client from using context takeover. server_max_window_bits: Maximum size of the server's LZ77 sliding window - in bits, between 8 and 15. + in bits, between 9 and 15. 8 isn't supported due to a `limitation of + zlib`_. client_max_window_bits: Maximum size of the client's LZ77 sliding window in bits, between 8 and 15. compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, @@ -522,6 +531,8 @@ class ServerPerMessageDeflateFactory(ServerExtensionFactory): the default behavior is to enable compression without enforcing ``client_max_window_bits``. + .. _limitation of zlib: https://github.com/madler/zlib/issues/171 + """ name = ExtensionName("permessage-deflate") @@ -539,8 +550,8 @@ def __init__( Configure the Per-Message Deflate extension factory. """ - if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): - raise ValueError("server_max_window_bits must be between 8 and 15") + if not (server_max_window_bits is None or 9 <= server_max_window_bits <= 15): + raise ValueError("server_max_window_bits must be between 9 and 15") if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15): raise ValueError("client_max_window_bits must be between 8 and 15") if compress_settings is not None and "wbits" in compress_settings: @@ -633,6 +644,9 @@ def process_request_params( elif server_max_window_bits > self.server_max_window_bits: server_max_window_bits = self.server_max_window_bits + if server_max_window_bits == 8: + raise NegotiationError("unsupported server_max_window_bits = 8, use 9") + # client_max_window_bits # Config Req. Resp. diff --git a/tests/extensions/test_permessage_deflate.py b/tests/extensions/test_permessage_deflate.py index 71f8e8e44..7736105c5 100644 --- a/tests/extensions/test_permessage_deflate.py +++ b/tests/extensions/test_permessage_deflate.py @@ -324,7 +324,7 @@ def test_init(self): for config in [ (False, False, 8, None), # server_max_window_bits ≥ 8 (False, True, 15, None), # server_max_window_bits ≤ 15 - (True, False, None, 8), # client_max_window_bits ≥ 8 + (True, False, None, 9), # client_max_window_bits ≥ 9 (True, True, None, 15), # client_max_window_bits ≤ 15 (False, False, None, True), # client_max_window_bits (False, False, None, None, {"memLevel": 4}), @@ -337,6 +337,7 @@ def test_init_error(self): for config in [ (False, False, 7, 8), # server_max_window_bits < 8 (False, True, 8, 7), # client_max_window_bits < 8 + (False, True, 8, 8), # client_max_window_bits = 8 (True, False, 16, 15), # server_max_window_bits > 15 (True, True, 15, 16), # client_max_window_bits > 15 (False, False, True, None), # server_max_window_bits @@ -463,11 +464,21 @@ def test_process_response_params(self): [("server_max_window_bits", "7")], NegotiationError, ), + ( + (False, False, None, None), + [("server_max_window_bits", "8")], + (False, False, 8, 15), + ), ( (False, False, None, None), [("server_max_window_bits", "10")], (False, False, 10, 15), ), + ( + (False, False, None, None), + [("server_max_window_bits", "15")], + (False, False, 15, 15), + ), ( (False, False, None, None), [("server_max_window_bits", "16")], @@ -519,6 +530,11 @@ def test_process_response_params(self): [("client_max_window_bits", "7")], NegotiationError, ), + ( + (False, False, None, True), + [("client_max_window_bits", "8")], + NegotiationError, + ), ( (False, False, None, True), [("client_max_window_bits", "10")], @@ -662,7 +678,7 @@ def test_name(self): def test_init(self): for config in [ - (False, False, 8, None), # server_max_window_bits ≥ 8 + (False, False, 9, None), # server_max_window_bits ≥ 9 (False, True, 15, None), # server_max_window_bits ≤ 15 (True, False, None, 8), # client_max_window_bits ≥ 8 (True, True, None, 15), # client_max_window_bits ≤ 15 @@ -677,6 +693,7 @@ def test_init_error(self): for config in [ (False, False, 7, 8), # server_max_window_bits < 8 (False, True, 8, 7), # client_max_window_bits < 8 + (False, False, 8, 8), # server_max_window_bits = 8 (True, False, 16, 15), # server_max_window_bits > 15 (True, True, 15, 16), # client_max_window_bits > 15 (False, False, None, True), # client_max_window_bits @@ -774,6 +791,12 @@ def test_process_request_params(self): None, NegotiationError, ), + ( + (False, False, None, None), + [("server_max_window_bits", "8")], + None, + NegotiationError, + ), ( (False, False, None, None), [("server_max_window_bits", "10")], @@ -835,12 +858,24 @@ def test_process_request_params(self): None, InvalidParameterValue, ), + ( + (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")], [("client_max_window_bits", "10")], # doesn't matter (False, False, 10, 15), ), + ( + (False, False, None, None), + [("client_max_window_bits", "15")], + [("client_max_window_bits", "15")], # doesn't matter + (False, False, 15, 15), + ), ( (False, False, None, None), [("client_max_window_bits", "16")],