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):