Add permessage-deflate support.
Review URL: https://codereview.appspot.com/8666044
diff --git a/src/mod_pywebsocket/extensions.py b/src/mod_pywebsocket/extensions.py
index dffc45e..007382f 100644
--- a/src/mod_pywebsocket/extensions.py
+++ b/src/mod_pywebsocket/extensions.py
@@ -34,6 +34,7 @@
_available_processors = {}
+_compression_extension_names = []
class ExtensionProcessorInterface(object):
@@ -314,12 +315,14 @@
_available_processors[common.DEFLATE_FRAME_EXTENSION] = (
DeflateFrameExtensionProcessor)
+_compression_extension_names.append(common.DEFLATE_FRAME_EXTENSION)
# Adding vendor-prefixed deflate-frame extension.
# TODO(bashi): Remove this after WebKit stops using vendor prefix.
#_available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
# DeflateFrameExtensionProcessor)
+#_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION)
def _parse_compression_method(data):
@@ -439,6 +442,7 @@
_available_processors[common.PERFRAME_COMPRESSION_EXTENSION] = (
PerFrameCompressionExtensionProcessor)
+_compression_extension_names.append(common.PERFRAME_COMPRESSION_EXTENSION)
class DeflateMessageProcessor(ExtensionProcessorInterface):
@@ -449,18 +453,38 @@
_C2S_MAX_WINDOW_BITS_PARAM = 'c2s_max_window_bits'
_C2S_NO_CONTEXT_TAKEOVER_PARAM = 'c2s_no_context_takeover'
- def __init__(self, request):
+ def __init__(self, request, draft08=True):
+ """Construct DeflateMessageProcessor
+
+ Args:
+ draft08: Follow the constraints on the parameters that were not
+ specified for permessage-compress but are specified for
+ permessage-deflate as on
+ draft-ietf-hybi-permessage-compression-08.
+ """
+
ExtensionProcessorInterface.__init__(self, request)
self._logger = util.get_class_logger(self)
self._c2s_max_window_bits = None
self._c2s_no_context_takeover = False
+ self._draft08 = draft08
+
def name(self):
return 'deflate'
def _get_extension_response_internal(self):
- # Any unknown parameter will be just ignored.
+ if self._draft08:
+ for name in self._request.get_parameter_names():
+ if name not in [self._S2C_MAX_WINDOW_BITS_PARAM,
+ self._S2C_NO_CONTEXT_TAKEOVER_PARAM,
+ self._C2S_MAX_WINDOW_BITS_PARAM]:
+ self._logger.debug('Unknown parameter: %r', name)
+ return None
+ else:
+ # Any unknown parameter will be just ignored.
+ pass
s2c_max_window_bits = None
if self._request.has_parameter(self._S2C_MAX_WINDOW_BITS_PARAM):
@@ -484,6 +508,18 @@
s2c_no_context_takeover)
return None
+ c2s_max_window_bits = self._request.has_parameter(
+ self._C2S_MAX_WINDOW_BITS_PARAM)
+ if (self._draft08 and
+ c2s_max_window_bits and
+ self._request.get_parameter_value(
+ self._C2S_MAX_WINDOW_BITS_PARAM) is not None):
+ self._logger.debug('%s parameter must not have a value in a '
+ 'client\'s opening handshake: %r',
+ self._C2S_MAX_WINDOW_BITS_PARAM,
+ c2s_max_window_bits)
+ return None
+
self._rfc1979_deflater = util._RFC1979Deflater(
s2c_max_window_bits, s2c_no_context_takeover)
@@ -505,9 +541,15 @@
self._S2C_NO_CONTEXT_TAKEOVER_PARAM, None)
if self._c2s_max_window_bits is not None:
+ if self._draft08 and c2s_max_window_bits:
+ self._logger.debug('Processor is configured to use %s but '
+ 'the client cannot accept it',
+ self._C2S_MAX_WINDOW_BITS_PARAM)
+ return None
response.add_parameter(
self._C2S_MAX_WINDOW_BITS_PARAM,
str(self._c2s_max_window_bits))
+
if self._c2s_no_context_takeover:
response.add_parameter(
self._C2S_NO_CONTEXT_TAKEOVER_PARAM, None)
@@ -533,6 +575,13 @@
LZ77 sliding window size of its inflater. I.e., you can use this for
testing client implementation but cannot reduce memory usage of this
class.
+
+ If this method has been called with True and an offer without the
+ c2s_max_window_bits extension parameter is received,
+ - (When processing the permessage-deflate extension) this processor
+ declines the request.
+ - (When processing the permessage-compress extension) this processor
+ accepts the request.
"""
self._c2s_max_window_bits = value
@@ -721,7 +770,9 @@
_available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
- DeflateMessageProcessor)
+ DeflateMessageProcessor)
+# TODO(tyoshino): Reorganize class names.
+_compression_extension_names.append('deflate')
class PerMessageCompressionExtensionProcessor(
@@ -742,18 +793,21 @@
def _lookup_compression_processor(self, method_desc):
if method_desc.name() == self._DEFLATE_METHOD:
- return DeflateMessageProcessor(method_desc)
+ return DeflateMessageProcessor(method_desc, False)
return None
_available_processors[common.PERMESSAGE_COMPRESSION_EXTENSION] = (
PerMessageCompressionExtensionProcessor)
+_compression_extension_names.append(common.PERMESSAGE_COMPRESSION_EXTENSION)
# Adding vendor-prefixed permessage-compress extension.
# TODO(bashi): Remove this after WebKit stops using vendor prefix.
#_available_processors[common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION] = (
# PerMessageCompressionExtensionProcessor)
+#_compression_extension_names.append(
+# common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION)
class MuxExtensionProcessor(ExtensionProcessorInterface):
@@ -833,11 +887,14 @@
def get_extension_processor(extension_request):
- global _available_processors
processor_class = _available_processors.get(extension_request.name())
if processor_class is None:
return None
return processor_class(extension_request)
+def is_compression_extension(extension_name):
+ return extension_name in _compression_extension_names
+
+
# vi:sts=4 sw=4 et
diff --git a/src/mod_pywebsocket/handshake/hybi.py b/src/mod_pywebsocket/handshake/hybi.py
index 1df7ef3..1d54a66 100644
--- a/src/mod_pywebsocket/handshake/hybi.py
+++ b/src/mod_pywebsocket/handshake/hybi.py
@@ -49,6 +49,7 @@
from mod_pywebsocket import common
from mod_pywebsocket.extensions import get_extension_processor
+from mod_pywebsocket.extensions import is_compression_extension
from mod_pywebsocket.handshake._base import check_request_line
from mod_pywebsocket.handshake._base import format_header
from mod_pywebsocket.handshake._base import get_mandatory_header
@@ -230,7 +231,10 @@
stream_options = StreamOptions()
- for processor in processors:
+ for index, processor in enumerate(processors):
+ if not processor.is_active():
+ continue
+
extension_response = processor.get_extension_response()
if extension_response is None:
# Rejected.
@@ -240,6 +244,14 @@
processor.setup_stream_options(stream_options)
+ if not is_compression_extension(processor.name()):
+ continue
+
+ # Inactivate all of the following compression extensions.
+ for j in xrange(index + 1, len(processors)):
+ if is_compression_extension(processors[j].name()):
+ processors[j].set_active(False)
+
if len(accepted_extensions) > 0:
self._request.ws_extensions = accepted_extensions
self._logger.debug(
diff --git a/src/test/client_for_testing.py b/src/test/client_for_testing.py
index 443bdd0..ad7b75d 100644
--- a/src/test/client_for_testing.py
+++ b/src/test/client_for_testing.py
@@ -54,6 +54,7 @@
import socket
import struct
+from mod_pywebsocket import common
from mod_pywebsocket import util
@@ -93,6 +94,7 @@
_DEFLATE_FRAME_EXTENSION = 'deflate-frame'
# TODO(bashi): Update after mux implementation finished.
_MUX_EXTENSION = 'mux_DO_NOT_USE'
+_PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
def _method_line(resource):
return 'GET %s HTTP/1.1\r\n' % resource
@@ -420,13 +422,10 @@
'(actual)' % (accept, expected_accept))
server_extensions_header = fields.get('sec-websocket-extensions')
- if (server_extensions_header is None or
- len(server_extensions_header) != 1):
- accepted_extensions = []
- else:
- accepted_extensions = server_extensions_header[0].split(',')
- # TODO(tyoshino): Follow the ABNF in the spec.
- accepted_extensions = [s.strip() for s in accepted_extensions]
+ accepted_extensions = []
+ if server_extensions_header is not None:
+ accepted_extensions = common.parse_extensions(
+ ', '.join(server_extensions_header))
# Scan accepted extension list to check if there is any unrecognized
# extensions or extensions we didn't request in it. Then, for
@@ -435,19 +434,22 @@
deflate_frame_accepted = False
mux_accepted = False
for extension in accepted_extensions:
- if extension == '':
- continue
- if extension == _DEFLATE_FRAME_EXTENSION:
+ if extension.name() == _DEFLATE_FRAME_EXTENSION:
if self._options.use_deflate_frame:
deflate_frame_accepted = True
continue
- if extension == _MUX_EXTENSION:
+ if extension.name() == _MUX_EXTENSION:
if self._options.use_mux:
mux_accepted = True
continue
+ if extension.name() == _PERMESSAGE_DEFLATE_EXTENSION:
+ checker = self._options.check_permessage_deflate
+ if checker:
+ checker(extension)
+ continue
raise Exception(
- 'Received unrecognized extension: %s' % extension)
+ 'Received unrecognized extension: %s' % extension.name())
# Let all extensions check the response for extension request.
@@ -767,7 +769,8 @@
def send_frame_of_arbitrary_bytes(self, header, body):
self._socket.sendall(header + self._mask_hybi(body))
- def send_data(self, payload, frame_type, end=True, mask=True):
+ def send_data(self, payload, frame_type, end=True, mask=True,
+ rsv1=0, rsv2=0, rsv3=0):
if self._outgoing_frame_filter is not None:
payload = self._outgoing_frame_filter.filter(payload)
@@ -783,7 +786,6 @@
self._fragmented = True
fin = 0
- rsv1 = 0
if self._handshake._options.use_deflate_frame:
rsv1 = 1
@@ -792,7 +794,7 @@
else:
mask_bit = 0
- header = chr(fin << 7 | rsv1 << 6 | opcode)
+ header = chr(fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 | opcode)
payload_length = len(payload)
if payload_length <= 125:
header += chr(mask_bit | payload_length)
diff --git a/src/test/test_endtoend.py b/src/test/test_endtoend.py
index 87a0028..45cf153 100755
--- a/src/test/test_endtoend.py
+++ b/src/test/test_endtoend.py
@@ -249,6 +249,28 @@
finally:
self._kill_process(server.pid)
+ def _run_hybi_permessage_deflate_test(
+ self, offer, response_checker, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(0.2)
+
+ self._options.extensions += offer
+ self._options.check_permessage_deflate = response_checker
+ client = client_for_testing.create_client(self._options)
+
+ try:
+ client.connect()
+
+ if test_function is not None:
+ test_function(client)
+
+ client.assert_connection_closed()
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
def _run_hybi_close_with_code_and_reason_test(self, test_function, code,
reason):
server = self._run_server()
@@ -313,6 +335,151 @@
self._run_hybi_deflate_frame_test(
_echo_check_procedure_with_goodbye)
+ def test_echo_permessage_deflate(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_frames(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd',
+ client_for_testing.OPCODE_TEXT,
+ end=False,
+ rsv1=1)
+ client._stream.send_data(
+ '\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_preference(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([], parameter.get_parameters())
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate', 'deflate-frame'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_parameters(self):
+ def test_function(client):
+ # From the examples in the spec.
+ compressed_hello = '\xf2\x48\xcd\xc9\xc9\x07\x00'
+ client._stream.send_data(
+ compressed_hello,
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ compressed_hello,
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ self.assertEquals('permessage-deflate', parameter.name())
+ self.assertEquals([('s2c_max_window_bits', '10'),
+ ('s2c_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate; s2c_max_window_bits=10; '
+ 's2c_no_context_takeover'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_s2c_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate; s2c_max_window_bits=3000000'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_s2c_max_window_bits(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate; s2c_max_window_bits=3000000'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_undefined_parameter(self):
+ def test_function(client):
+ client.send_close()
+ client.assert_receive_close()
+
+ def response_checker(parameter):
+ raise Exception('Unexpected acceptance of permessage-deflate')
+
+ self._run_hybi_permessage_deflate_test(
+ ['permessage-deflate; foo=bar'],
+ response_checker,
+ test_function)
+
def test_echo_close_with_code_and_reason(self):
self._options.resource = '/close'
self._run_hybi_close_with_code_and_reason_test(
diff --git a/src/test/test_extensions.py b/src/test/test_extensions.py
index b8c8e61..1fa0ea4 100755
--- a/src/test/test_extensions.py
+++ b/src/test/test_extensions.py
@@ -279,9 +279,7 @@
parameter.add_parameter('foo', 'bar')
processor = extensions.DeflateMessageProcessor(parameter)
- response = processor.get_extension_response()
- self.assertEqual('permessage-deflate', response.name())
- self.assertEqual(0, len(response.get_parameters()))
+ self.assertIsNone(processor.get_extension_response())
class DeflateMessageProcessorBuildingTest(unittest.TestCase):