summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/pywebsocket/src/test
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/tools/pywebsocket/src/test
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/web-platform/tests/tools/pywebsocket/src/test')
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem17
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem61
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12bin0 -> 2582 bytes
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem15
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py1100
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py67
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/mock.py221
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py690
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/run_all.py89
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py45
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py288
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py753
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py360
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py188
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py534
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py516
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py90
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py104
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_mock.py145
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py1356
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py2089
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_stream.py77
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py59
-rwxr-xr-xtesting/web-platform/tests/tools/pywebsocket/src/test/test_util.py200
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README1
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py31
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py42
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py44
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py39
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py40
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py45
-rw-r--r--testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl32
36 files changed, 9470 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/__init__.py
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem
new file mode 100644
index 000000000..4dadae121
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cacert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICvDCCAiWgAwIBAgIJAKqVghkGF1rSMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
+BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQxFDAS
+BgNVBAMTC3B5d2Vic29ja2V0MB4XDTEyMDYwNjA3MjQzM1oXDTM5MTAyMzA3MjQz
+M1owSTELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRQwEgYDVQQKEwtweXdl
+YnNvY2tldDEUMBIGA1UEAxMLcHl3ZWJzb2NrZXQwgZ8wDQYJKoZIhvcNAQEBBQAD
+gY0AMIGJAoGBAKoSEW2biQxVrMMKdn/8PJzDYiSXDPR9WQbLRRQ1Gm5jkCYiahXW
+u2CbTThfPPfi2NHA3I+HlT7gO9yR7RVUvN6ISUzGwXDEq4f4UNqtQOhQaqqK+CZ9
+LO/BhO/YYfNrbSPlYzHUKaT9ese7xO9VzVKLW+qUf2Mjh4/+SzxBDNP7AgMBAAGj
+gaswgagwHQYDVR0OBBYEFOsWdxCSuyhwaZeab6BoTho3++bzMHkGA1UdIwRyMHCA
+FOsWdxCSuyhwaZeab6BoTho3++bzoU2kSzBJMQswCQYDVQQGEwJKUDEOMAwGA1UE
+CBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtweXdlYnNv
+Y2tldIIJAKqVghkGF1rSMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
+gsMI1WEYqNw/jhUIdrTBcCxJ0X6hJvA9ziKANVm1Rs+4P3YDArkQ8bCr6xY+Kw7s
+Zp0yE7dM8GMdi+DU6hL3t3E5eMkTS1yZr9WCK4f2RLo+et98selZydpHemF3DJJ3
+gAj8Sx4LBaG8Cb/WnEMPv3MxG3fBE5favF6V4jU07hQ=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem
new file mode 100644
index 000000000..25379a72b
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/cert.pem
@@ -0,0 +1,61 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Validity
+ Not Before: Jun 6 07:25:08 2012 GMT
+ Not After : Oct 23 07:25:08 2039 GMT
+ Subject: C=JP, ST=Tokyo, O=pywebsocket, CN=pywebsocket
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public Key: (1024 bit)
+ Modulus (1024 bit):
+ 00:de:10:ce:3a:5a:04:a4:1c:29:93:5c:23:82:1a:
+ f2:06:01:e6:2b:a4:0f:dd:77:49:76:89:03:a2:21:
+ de:04:75:c6:e2:dd:fb:35:27:3a:a2:92:8e:12:62:
+ 2b:3e:1f:f4:78:df:b6:94:cb:27:d6:cb:d6:37:d7:
+ 5c:08:f0:09:3e:c9:ce:24:2d:00:c9:df:4a:e0:99:
+ e5:fb:23:a9:e2:d6:c9:3d:96:fa:01:88:de:5a:89:
+ b0:cf:03:67:6f:04:86:1d:ef:62:1c:55:a9:07:9a:
+ 2e:66:2a:73:5b:4c:62:03:f9:82:83:db:68:bf:b8:
+ 4b:0b:8b:93:11:b8:54:73:7b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Cert Type:
+ SSL Server
+ Netscape Comment:
+ OpenSSL Generated Certificate
+ X509v3 Subject Key Identifier:
+ 82:A1:73:8B:16:0C:7C:E4:D3:46:95:13:95:1A:32:C1:84:E9:06:00
+ X509v3 Authority Key Identifier:
+ keyid:EB:16:77:10:92:BB:28:70:69:97:9A:6F:A0:68:4E:1A:37:FB:E6:F3
+
+ Signature Algorithm: sha1WithRSAEncryption
+ 6b:b3:46:29:02:df:b0:c8:8e:c4:d7:7f:a0:1e:0d:1a:eb:2f:
+ df:d1:48:57:36:5f:95:8c:1b:f0:51:d6:52:e7:8d:84:3b:9f:
+ d8:ed:22:9c:aa:bd:ee:9b:90:1d:84:a3:4c:0b:cb:eb:64:73:
+ ba:f7:15:ce:da:5f:db:8b:15:07:a6:28:7f:b9:8c:11:9b:64:
+ d3:f1:be:52:4f:c3:d8:58:fe:de:56:63:63:3b:51:ed:a7:81:
+ f9:05:51:70:63:32:09:0e:94:7e:05:fe:a1:56:18:34:98:d5:
+ 99:1e:4e:27:38:89:90:6a:e5:ce:60:35:01:f5:de:34:60:b1:
+ cb:ae
+-----BEGIN CERTIFICATE-----
+MIICmDCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJKUDEO
+MAwGA1UECBMFVG9reW8xFDASBgNVBAoTC3B5d2Vic29ja2V0MRQwEgYDVQQDEwtw
+eXdlYnNvY2tldDAeFw0xMjA2MDYwNzI1MDhaFw0zOTEwMjMwNzI1MDhaMEkxCzAJ
+BgNVBAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEUMBIGA1UEChMLcHl3ZWJzb2NrZXQx
+FDASBgNVBAMTC3B5d2Vic29ja2V0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1Jzqiko4SYis+
+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoBiN5aibDPA2dv
+BIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQABo4GPMIGMMAkG
+A1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgZAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
+U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUgqFzixYMfOTTRpUT
+lRoywYTpBgAwHwYDVR0jBBgwFoAU6xZ3EJK7KHBpl5pvoGhOGjf75vMwDQYJKoZI
+hvcNAQEFBQADgYEAa7NGKQLfsMiOxNd/oB4NGusv39FIVzZflYwb8FHWUueNhDuf
+2O0inKq97puQHYSjTAvL62RzuvcVztpf24sVB6Yof7mMEZtk0/G+Uk/D2Fj+3lZj
+YztR7aeB+QVRcGMyCQ6UfgX+oVYYNJjVmR5OJziJkGrlzmA1AfXeNGCxy64=
+-----END CERTIFICATE-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12
new file mode 100644
index 000000000..14e139927
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12
Binary files differ
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem
new file mode 100644
index 000000000..fae858318
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/key.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDeEM46WgSkHCmTXCOCGvIGAeYrpA/dd0l2iQOiId4Edcbi3fs1
+Jzqiko4SYis+H/R437aUyyfWy9Y311wI8Ak+yc4kLQDJ30rgmeX7I6ni1sk9lvoB
+iN5aibDPA2dvBIYd72IcVakHmi5mKnNbTGID+YKD22i/uEsLi5MRuFRzewIDAQAB
+AoGBAIuCuV1Vcnb7rm8CwtgZP5XgmY8vSjxTldafa6XvawEYUTP0S77v/1llg1Yv
+UIV+I+PQgG9oVoYOl22LoimHS/Z3e1fsot5tDYszGe8/Gkst4oaReSoxvBUa6WXp
+QSo7YFCajuHtE+W/gzF+UHbdzzXIDjQZ314LNF5t+4UnsEPBAkEA+girImqWoM2t
+3UR8f8oekERwsmEMf9DH5YpH4cvUnvI+kwesC/r2U8Sho++fyEMUNm7aIXGqNLga
+ogAM+4NX4QJBAONdSxSay22egTGNoIhLndljWkuOt/9FWj2klf/4QxD4blMJQ5Oq
+QdOGAh7nVQjpPLQ5D7CBVAKpGM2CD+QJBtsCQEP2kz35pxPylG3urcC2mfQxBkkW
+ZCViBNP58GwJ0bOauTOSBEwFXWuLqTw8aDwxL49UNmqc0N0fpe2fAehj3UECQQCm
+FH/DjU8Lw7ybddjNtm6XXPuYNagxz3cbkB4B3FchDleIUDwMoVF0MW9bI5/54mV1
+QDk1tUKortxvQZJaAD4BAkEAhGOHQqPd6bBBoFBvpaLzPJMxwLKrB+Wtkq/QlC72
+ClRiMn2g8SALiIL3BDgGXKcKE/Wy7jo/af/JCzQ/cPqt/A==
+-----END RSA PRIVATE KEY-----
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py
new file mode 100644
index 000000000..c7f805ee9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/client_for_testing.py
@@ -0,0 +1,1100 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket client utility for testing.
+
+This module contains helper methods for performing handshake, frame
+sending/receiving as a WebSocket client.
+
+This is code for testing mod_pywebsocket. Keep this code independent from
+mod_pywebsocket. Don't import e.g. Stream class for generating frame for
+testing. Using util.hexify, etc. that are not related to protocol processing
+is allowed.
+
+Note:
+This code is far from robust, e.g., we cut corners in handshake.
+"""
+
+
+import base64
+import errno
+import logging
+import os
+import random
+import re
+import socket
+import struct
+import time
+
+from mod_pywebsocket import common
+from mod_pywebsocket import util
+
+
+DEFAULT_PORT = 80
+DEFAULT_SECURE_PORT = 443
+
+# Opcodes introduced in IETF HyBi 01 for the new framing format
+OPCODE_CONTINUATION = 0x0
+OPCODE_CLOSE = 0x8
+OPCODE_PING = 0x9
+OPCODE_PONG = 0xa
+OPCODE_TEXT = 0x1
+OPCODE_BINARY = 0x2
+
+# Strings used for handshake
+_UPGRADE_HEADER = 'Upgrade: websocket\r\n'
+_UPGRADE_HEADER_HIXIE75 = 'Upgrade: WebSocket\r\n'
+_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
+
+WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+# Status codes
+STATUS_NORMAL_CLOSURE = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA = 1003
+STATUS_NO_STATUS_RECEIVED = 1005
+STATUS_ABNORMAL_CLOSURE = 1006
+STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_MANDATORY_EXT = 1010
+STATUS_INTERNAL_ENDPOINT_ERROR = 1011
+STATUS_TLS_HANDSHAKE = 1015
+
+# Extension tokens
+_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
+
+
+def _sec_origin_header(origin):
+ return 'Sec-WebSocket-Origin: %s\r\n' % origin.lower()
+
+
+def _origin_header(origin):
+ # 4.1 13. concatenation of the string "Origin:", a U+0020 SPACE character,
+ # and the /origin/ value, converted to ASCII lowercase, to /fields/.
+ return 'Origin: %s\r\n' % origin.lower()
+
+
+def _format_host_header(host, port, secure):
+ # 4.1 9. Let /hostport/ be an empty string.
+ # 4.1 10. Append the /host/ value, converted to ASCII lowercase, to
+ # /hostport/
+ hostport = host.lower()
+ # 4.1 11. If /secure/ is false, and /port/ is not 80, or if /secure/
+ # is true, and /port/ is not 443, then append a U+003A COLON character
+ # (:) followed by the value of /port/, expressed as a base-ten integer,
+ # to /hostport/
+ if ((not secure and port != DEFAULT_PORT) or
+ (secure and port != DEFAULT_SECURE_PORT)):
+ hostport += ':' + str(port)
+ # 4.1 12. concatenation of the string "Host:", a U+0020 SPACE
+ # character, and /hostport/, to /fields/.
+ return 'Host: %s\r\n' % hostport
+
+
+# TODO(tyoshino): Define a base class and move these shared methods to that.
+
+
+def receive_bytes(socket, length):
+ received_bytes = []
+ remaining = length
+ while remaining > 0:
+ new_received_bytes = socket.recv(remaining)
+ if not new_received_bytes:
+ raise Exception(
+ 'Connection closed before receiving requested length '
+ '(requested %d bytes but received only %d bytes)' %
+ (length, length - remaining))
+ received_bytes.append(new_received_bytes)
+ remaining -= len(new_received_bytes)
+ return ''.join(received_bytes)
+
+
+# TODO(tyoshino): Now the WebSocketHandshake class diverts these methods. We
+# should move to HTTP parser as specified in RFC 6455. For HyBi 00 and
+# Hixie 75, pack these methods as some parser class.
+
+
+def _read_fields(socket):
+ # 4.1 32. let /fields/ be a list of name-value pairs, initially empty.
+ fields = {}
+ while True:
+ # 4.1 33. let /name/ and /value/ be empty byte arrays
+ name = ''
+ value = ''
+ # 4.1 34. read /name/
+ name = _read_name(socket)
+ if name is None:
+ break
+ # 4.1 35. read spaces
+ # TODO(tyoshino): Skip only one space as described in the spec.
+ ch = _skip_spaces(socket)
+ # 4.1 36. read /value/
+ value = _read_value(socket, ch)
+ # 4.1 37. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception(
+ 'Expected LF but found %r while reading value %r for header '
+ '%r' % (ch, name, value))
+ # 4.1 38. append an entry to the /fields/ list that has the name
+ # given by the string obtained by interpreting the /name/ byte
+ # array as a UTF-8 stream and the value given by the string
+ # obtained by interpreting the /value/ byte array as a UTF-8 byte
+ # stream.
+ fields.setdefault(name, []).append(value)
+ # 4.1 39. return to the "Field" step above
+ return fields
+
+
+def _read_name(socket):
+ # 4.1 33. let /name/ be empty byte arrays
+ name = ''
+ while True:
+ # 4.1 34. read a byte from the server
+ ch = receive_bytes(socket, 1)
+ if ch == '\r': # 0x0D
+ return None
+ elif ch == '\n': # 0x0A
+ raise Exception(
+ 'Unexpected LF when reading header name %r' % name)
+ elif ch == ':': # 0x3A
+ return name
+ elif ch >= 'A' and ch <= 'Z': # range 0x31 to 0x5A
+ ch = chr(ord(ch) + 0x20)
+ name += ch
+ else:
+ name += ch
+
+
+def _skip_spaces(socket):
+ # 4.1 35. read a byte from the server
+ while True:
+ ch = receive_bytes(socket, 1)
+ if ch == ' ': # 0x20
+ continue
+ return ch
+
+
+def _read_value(socket, ch):
+ # 4.1 33. let /value/ be empty byte arrays
+ value = ''
+ # 4.1 36. read a byte from server.
+ while True:
+ if ch == '\r': # 0x0D
+ return value
+ elif ch == '\n': # 0x0A
+ raise Exception(
+ 'Unexpected LF when reading header value %r' % value)
+ else:
+ value += ch
+ ch = receive_bytes(socket, 1)
+
+
+def read_frame_header(socket):
+ received = receive_bytes(socket, 2)
+
+ first_byte = ord(received[0])
+ fin = (first_byte >> 7) & 1
+ rsv1 = (first_byte >> 6) & 1
+ rsv2 = (first_byte >> 5) & 1
+ rsv3 = (first_byte >> 4) & 1
+ opcode = first_byte & 0xf
+
+ second_byte = ord(received[1])
+ mask = (second_byte >> 7) & 1
+ payload_length = second_byte & 0x7f
+
+ if mask != 0:
+ raise Exception(
+ 'Mask bit must be 0 for frames coming from server')
+
+ if payload_length == 127:
+ extended_payload_length = receive_bytes(socket, 8)
+ payload_length = struct.unpack(
+ '!Q', extended_payload_length)[0]
+ if payload_length > 0x7FFFFFFFFFFFFFFF:
+ raise Exception('Extended payload length >= 2^63')
+ elif payload_length == 126:
+ extended_payload_length = receive_bytes(socket, 2)
+ payload_length = struct.unpack(
+ '!H', extended_payload_length)[0]
+
+ return fin, rsv1, rsv2, rsv3, opcode, payload_length
+
+
+class _TLSSocket(object):
+ """Wrapper for a TLS connection."""
+
+ def __init__(self, raw_socket):
+ self._ssl = socket.ssl(raw_socket)
+
+ def send(self, bytes):
+ return self._ssl.write(bytes)
+
+ def recv(self, size=-1):
+ return self._ssl.read(size)
+
+ def close(self):
+ # Nothing to do.
+ pass
+
+
+class HttpStatusException(Exception):
+ """This exception will be raised when unexpected http status code was
+ received as a result of handshake.
+ """
+
+ def __init__(self, name, status):
+ super(HttpStatusException, self).__init__(name)
+ self.status = status
+
+
+class WebSocketHandshake(object):
+ """Opening handshake processor for the WebSocket protocol (RFC 6455)."""
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ def handshake(self, socket):
+ """Handshake WebSocket.
+
+ Raises:
+ Exception: handshake failed.
+ """
+
+ self._socket = socket
+
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+
+ fields = []
+ fields.append(_UPGRADE_HEADER)
+ fields.append(_CONNECTION_HEADER)
+
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+
+ if self._options.version is 8:
+ fields.append(_sec_origin_header(self._options.origin))
+ else:
+ fields.append(_origin_header(self._options.origin))
+
+ original_key = os.urandom(16)
+ key = base64.b64encode(original_key)
+ self._logger.debug(
+ 'Sec-WebSocket-Key: %s (%s)', key, util.hexify(original_key))
+ fields.append('Sec-WebSocket-Key: %s\r\n' % key)
+
+ fields.append('Sec-WebSocket-Version: %d\r\n' % self._options.version)
+
+ # Setting up extensions.
+ if len(self._options.extensions) > 0:
+ fields.append('Sec-WebSocket-Extensions: %s\r\n' %
+ ', '.join(self._options.extensions))
+
+ self._logger.debug('Opening handshake request headers: %r', fields)
+
+ for field in fields:
+ self._socket.sendall(field)
+ self._socket.sendall('\r\n')
+
+ self._logger.info('Sent opening handshake request')
+
+ field = ''
+ while True:
+ ch = receive_bytes(self._socket, 1)
+ field += ch
+ if ch == '\n':
+ break
+
+ self._logger.debug('Opening handshake Response-Line: %r', field)
+
+ if len(field) < 7 or not field.endswith('\r\n'):
+ raise Exception('Wrong status line: %r' % field)
+ m = re.match('[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise Exception(
+ 'No HTTP status code found in status line: %r' % field)
+ code = m.group(1)
+ if not re.match('[0-9][0-9][0-9]', code):
+ raise Exception(
+ 'HTTP status code %r is not three digit in status line: %r' %
+ (code, field))
+ if code != '101':
+ raise HttpStatusException(
+ 'Expected HTTP status code 101 but found %r in status line: '
+ '%r' % (code, field), int(code))
+ fields = _read_fields(self._socket)
+ ch = receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception('Expected LF but found: %r' % ch)
+
+ self._logger.debug('Opening handshake response headers: %r', fields)
+
+ # Check /fields/
+ if len(fields['upgrade']) != 1:
+ raise Exception(
+ 'Multiple Upgrade headers found: %s' % fields['upgrade'])
+ if len(fields['connection']) != 1:
+ raise Exception(
+ 'Multiple Connection headers found: %s' % fields['connection'])
+ if fields['upgrade'][0] != 'websocket':
+ raise Exception(
+ 'Unexpected Upgrade header value: %s' % fields['upgrade'][0])
+ if fields['connection'][0].lower() != 'upgrade':
+ raise Exception(
+ 'Unexpected Connection header value: %s' %
+ fields['connection'][0])
+
+ if len(fields['sec-websocket-accept']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Accept headers found: %s' %
+ fields['sec-websocket-accept'])
+
+ accept = fields['sec-websocket-accept'][0]
+
+ # Validate
+ try:
+ decoded_accept = base64.b64decode(accept)
+ except TypeError, e:
+ raise HandshakeException(
+ 'Illegal value for header Sec-WebSocket-Accept: ' + accept)
+
+ if len(decoded_accept) != 20:
+ raise HandshakeException(
+ 'Decoded value of Sec-WebSocket-Accept is not 20-byte long')
+
+ self._logger.debug('Actual Sec-WebSocket-Accept: %r (%s)',
+ accept, util.hexify(decoded_accept))
+
+ original_expected_accept = util.sha1_hash(
+ key + WEBSOCKET_ACCEPT_UUID).digest()
+ expected_accept = base64.b64encode(original_expected_accept)
+
+ self._logger.debug('Expected Sec-WebSocket-Accept: %r (%s)',
+ expected_accept,
+ util.hexify(original_expected_accept))
+
+ if accept != expected_accept:
+ raise Exception(
+ 'Invalid Sec-WebSocket-Accept header: %r (expected) != %r '
+ '(actual)' % (accept, expected_accept))
+
+ server_extensions_header = fields.get('sec-websocket-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
+ # extensions we request, parse them and store parameters. They will be
+ # used later by each extension.
+ deflate_frame_accepted = False
+ mux_accepted = False
+ for extension in accepted_extensions:
+ if extension.name() == _DEFLATE_FRAME_EXTENSION:
+ if self._options.use_deflate_frame:
+ deflate_frame_accepted = True
+ continue
+ 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.name())
+
+ # Let all extensions check the response for extension request.
+
+ if (self._options.use_deflate_frame and
+ not deflate_frame_accepted):
+ raise Exception('%s extension not accepted' %
+ _DEFLATE_FRAME_EXTENSION)
+
+ if self._options.use_mux and not mux_accepted:
+ raise Exception('%s extension not accepted' % _MUX_EXTENSION)
+
+
+class WebSocketHybi00Handshake(object):
+ """Opening handshake processor for the WebSocket protocol version HyBi 00.
+ """
+
+ def __init__(self, options, draft_field):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._draft_field = draft_field
+
+ def handshake(self, socket):
+ """Handshake WebSocket.
+
+ Raises:
+ Exception: handshake failed.
+ """
+
+ self._socket = socket
+
+ # 4.1 5. send request line.
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+ # 4.1 6. Let /fields/ be an empty list of strings.
+ fields = []
+ # 4.1 7. Add the string "Upgrade: WebSocket" to /fields/.
+ fields.append(_UPGRADE_HEADER_HIXIE75)
+ # 4.1 8. Add the string "Connection: Upgrade" to /fields/.
+ fields.append(_CONNECTION_HEADER)
+ # 4.1 9-12. Add Host: field to /fields/.
+ fields.append(_format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls))
+ # 4.1 13. Add Origin: field to /fields/.
+ fields.append(_origin_header(self._options.origin))
+ # TODO: 4.1 14 Add Sec-WebSocket-Protocol: field to /fields/.
+ # TODO: 4.1 15 Add cookie headers to /fields/.
+
+ # 4.1 16-23. Add Sec-WebSocket-Key<n> to /fields/.
+ self._number1, key1 = self._generate_sec_websocket_key()
+ self._logger.debug('Number1: %d', self._number1)
+ fields.append('Sec-WebSocket-Key1: %s\r\n' % key1)
+ self._number2, key2 = self._generate_sec_websocket_key()
+ self._logger.debug('Number2: %d', self._number1)
+ fields.append('Sec-WebSocket-Key2: %s\r\n' % key2)
+
+ fields.append('Sec-WebSocket-Draft: %s\r\n' % self._draft_field)
+
+ # 4.1 24. For each string in /fields/, in a random order: send the
+ # string, encoded as UTF-8, followed by a UTF-8 encoded U+000D CARRIAGE
+ # RETURN U+000A LINE FEED character pair (CRLF).
+ random.shuffle(fields)
+
+ self._logger.debug('Opening handshake request headers: %r', fields)
+ for field in fields:
+ self._socket.sendall(field)
+
+ # 4.1 25. send a UTF-8-encoded U+000D CARRIAGE RETURN U+000A LINE FEED
+ # character pair (CRLF).
+ self._socket.sendall('\r\n')
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ self._key3 = self._generate_key3()
+ # 4.1 27. send /key3/ to the server.
+ self._socket.sendall(self._key3)
+ self._logger.debug(
+ 'Key3: %r (%s)', self._key3, util.hexify(self._key3))
+
+ self._logger.info('Sent opening handshake request')
+
+ # 4.1 28. Read bytes from the server until either the connection
+ # closes, or a 0x0A byte is read. let /field/ be these bytes, including
+ # the 0x0A bytes.
+ field = ''
+ while True:
+ ch = receive_bytes(self._socket, 1)
+ field += ch
+ if ch == '\n':
+ break
+
+ self._logger.debug('Opening handshake Response-Line: %r', field)
+
+ # if /field/ is not at least seven bytes long, or if the last
+ # two bytes aren't 0x0D and 0x0A respectively, or if it does not
+ # contain at least two 0x20 bytes, then fail the WebSocket connection
+ # and abort these steps.
+ if len(field) < 7 or not field.endswith('\r\n'):
+ raise Exception('Wrong status line: %r' % field)
+ m = re.match('[^ ]* ([^ ]*) .*', field)
+ if m is None:
+ raise Exception('No code found in status line: %r' % field)
+ # 4.1 29. let /code/ be the substring of /field/ that starts from the
+ # byte after the first 0x20 byte, and ends with the byte before the
+ # second 0x20 byte.
+ code = m.group(1)
+ # 4.1 30. if /code/ is not three bytes long, or if any of the bytes in
+ # /code/ are not in the range 0x30 to 0x90, then fail the WebSocket
+ # connection and abort these steps.
+ if not re.match('[0-9][0-9][0-9]', code):
+ raise Exception(
+ 'HTTP status code %r is not three digit in status line: %r' %
+ (code, field))
+ # 4.1 31. if /code/, interpreted as UTF-8, is "101", then move to the
+ # next step.
+ if code != '101':
+ raise HttpStatusException(
+ 'Expected HTTP status code 101 but found %r in status line: '
+ '%r' % (code, field), int(code))
+ # 4.1 32-39. read fields into /fields/
+ fields = _read_fields(self._socket)
+
+ self._logger.debug('Opening handshake response headers: %r', fields)
+
+ # 4.1 40. _Fields processing_
+ # read a byte from server
+ ch = receive_bytes(self._socket, 1)
+ if ch != '\n': # 0x0A
+ raise Exception('Expected LF but found %r' % ch)
+ # 4.1 41. check /fields/
+ if len(fields['upgrade']) != 1:
+ raise Exception(
+ 'Multiple Upgrade headers found: %s' % fields['upgrade'])
+ if len(fields['connection']) != 1:
+ raise Exception(
+ 'Multiple Connection headers found: %s' % fields['connection'])
+ if len(fields['sec-websocket-origin']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Origin headers found: %s' %
+ fields['sec-sebsocket-origin'])
+ if len(fields['sec-websocket-location']) != 1:
+ raise Exception(
+ 'Multiple Sec-WebSocket-Location headers found: %s' %
+ fields['sec-sebsocket-location'])
+ # TODO(ukai): protocol
+ # if the entry's name is "upgrade"
+ # if the value is not exactly equal to the string "WebSocket",
+ # then fail the WebSocket connection and abort these steps.
+ if fields['upgrade'][0] != 'WebSocket':
+ raise Exception(
+ 'Unexpected Upgrade header value: %s' % fields['upgrade'][0])
+ # if the entry's name is "connection"
+ # if the value, converted to ASCII lowercase, is not exactly equal
+ # to the string "upgrade", then fail the WebSocket connection and
+ # abort these steps.
+ if fields['connection'][0].lower() != 'upgrade':
+ raise Exception(
+ 'Unexpected Connection header value: %s' %
+ fields['connection'][0])
+ # TODO(ukai): check origin, location, cookie, ..
+
+ # 4.1 42. let /challenge/ be the concatenation of /number_1/,
+ # expressed as a big endian 32 bit integer, /number_2/, expressed
+ # as big endian 32 bit integer, and the eight bytes of /key_3/ in the
+ # order they were sent on the wire.
+ challenge = struct.pack('!I', self._number1)
+ challenge += struct.pack('!I', self._number2)
+ challenge += self._key3
+
+ self._logger.debug(
+ 'Challenge: %r (%s)', challenge, util.hexify(challenge))
+
+ # 4.1 43. let /expected/ be the MD5 fingerprint of /challenge/ as a
+ # big-endian 128 bit string.
+ expected = util.md5_hash(challenge).digest()
+ self._logger.debug(
+ 'Expected challenge response: %r (%s)',
+ expected, util.hexify(expected))
+
+ # 4.1 44. read sixteen bytes from the server.
+ # let /reply/ be those bytes.
+ reply = receive_bytes(self._socket, 16)
+ self._logger.debug(
+ 'Actual challenge response: %r (%s)', reply, util.hexify(reply))
+
+ # 4.1 45. if /reply/ does not exactly equal /expected/, then fail
+ # the WebSocket connection and abort these steps.
+ if expected != reply:
+ raise Exception(
+ 'Bad challenge response: %r (expected) != %r (actual)' %
+ (expected, reply))
+ # 4.1 46. The *WebSocket connection is established*.
+
+ def _generate_sec_websocket_key(self):
+ # 4.1 16. let /spaces_n/ be a random integer from 1 to 12 inclusive.
+ spaces = random.randint(1, 12)
+ # 4.1 17. let /max_n/ be the largest integer not greater than
+ # 4,294,967,295 divided by /spaces_n/.
+ maxnum = 4294967295 / spaces
+ # 4.1 18. let /number_n/ be a random integer from 0 to /max_n/
+ # inclusive.
+ number = random.randint(0, maxnum)
+ # 4.1 19. let /product_n/ be the result of multiplying /number_n/ and
+ # /spaces_n/ together.
+ product = number * spaces
+ # 4.1 20. let /key_n/ be a string consisting of /product_n/, expressed
+ # in base ten using the numerals in the range U+0030 DIGIT ZERO (0) to
+ # U+0039 DIGIT NINE (9).
+ key = str(product)
+ # 4.1 21. insert between one and twelve random characters from the
+ # range U+0021 to U+002F and U+003A to U+007E into /key_n/ at random
+ # positions.
+ available_chars = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
+ n = random.randint(1, 12)
+ for _ in xrange(n):
+ ch = random.choice(available_chars)
+ pos = random.randint(0, len(key))
+ key = key[0:pos] + chr(ch) + key[pos:]
+ # 4.1 22. insert /spaces_n/ U+0020 SPACE characters into /key_n/ at
+ # random positions other than start or end of the string.
+ for _ in xrange(spaces):
+ pos = random.randint(1, len(key) - 1)
+ key = key[0:pos] + ' ' + key[pos:]
+ return number, key
+
+ def _generate_key3(self):
+ # 4.1 26. let /key3/ be a string consisting of eight random bytes (or
+ # equivalently, a random 64 bit integer encoded in a big-endian order).
+ return ''.join([chr(random.randint(0, 255)) for _ in xrange(8)])
+
+
+class WebSocketHixie75Handshake(object):
+ """WebSocket handshake processor for IETF Hixie 75."""
+
+ _EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
+ _UPGRADE_HEADER_HIXIE75 +
+ _CONNECTION_HEADER)
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+
+ def _skip_headers(self):
+ terminator = '\r\n\r\n'
+ pos = 0
+ while pos < len(terminator):
+ received = receive_bytes(self._socket, 1)
+ if received == terminator[pos]:
+ pos += 1
+ elif received == terminator[0]:
+ pos = 1
+ else:
+ pos = 0
+
+ def handshake(self, socket):
+ self._socket = socket
+
+ request_line = _method_line(self._options.resource)
+ self._logger.debug('Opening handshake Request-Line: %r', request_line)
+ self._socket.sendall(request_line)
+
+ headers = _UPGRADE_HEADER_HIXIE75 + _CONNECTION_HEADER
+ headers += _format_host_header(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.use_tls)
+ headers += _origin_header(self._options.origin)
+ self._logger.debug('Opening handshake request headers: %r', headers)
+ self._socket.sendall(headers)
+
+ self._socket.sendall('\r\n')
+
+ self._logger.info('Sent opening handshake request')
+
+ for expected_char in WebSocketHixie75Handshake._EXPECTED_RESPONSE:
+ received = receive_bytes(self._socket, 1)
+ if expected_char != received:
+ raise Exception('Handshake failure')
+ # We cut corners and skip other headers.
+ self._skip_headers()
+
+
+class WebSocketStream(object):
+ """Frame processor for the WebSocket protocol (RFC 6455)."""
+
+ def __init__(self, socket, handshake):
+ self._handshake = handshake
+ self._socket = socket
+
+ # Filters applied to application data part of data frames.
+ self._outgoing_frame_filter = None
+ self._incoming_frame_filter = None
+
+ if self._handshake._options.use_deflate_frame:
+ self._outgoing_frame_filter = (
+ util._RFC1979Deflater(None, False))
+ self._incoming_frame_filter = util._RFC1979Inflater()
+
+ self._fragmented = False
+
+ def _mask_hybi(self, s):
+ # TODO(tyoshino): os.urandom does open/read/close for every call. If
+ # performance matters, change this to some library call that generates
+ # cryptographically secure pseudo random number sequence.
+ masking_nonce = os.urandom(4)
+ result = [masking_nonce]
+ count = 0
+ for c in s:
+ result.append(chr(ord(c) ^ ord(masking_nonce[count])))
+ count = (count + 1) % len(masking_nonce)
+ return ''.join(result)
+
+ 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,
+ rsv1=0, rsv2=0, rsv3=0):
+ if self._outgoing_frame_filter is not None:
+ payload = self._outgoing_frame_filter.filter(payload)
+
+ if self._fragmented:
+ opcode = OPCODE_CONTINUATION
+ else:
+ opcode = frame_type
+
+ if end:
+ self._fragmented = False
+ fin = 1
+ else:
+ self._fragmented = True
+ fin = 0
+
+ if self._handshake._options.use_deflate_frame:
+ rsv1 = 1
+
+ if mask:
+ mask_bit = 1 << 7
+ else:
+ mask_bit = 0
+
+ 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)
+ elif payload_length < 1 << 16:
+ header += chr(mask_bit | 126) + struct.pack('!H', payload_length)
+ elif payload_length < 1 << 63:
+ header += chr(mask_bit | 127) + struct.pack('!Q', payload_length)
+ else:
+ raise Exception('Too long payload (%d byte)' % payload_length)
+ if mask:
+ payload = self._mask_hybi(payload)
+ self._socket.sendall(header + payload)
+
+ def send_binary(self, payload, end=True, mask=True):
+ self.send_data(payload, OPCODE_BINARY, end, mask)
+
+ def send_text(self, payload, end=True, mask=True):
+ self.send_data(payload.encode('utf-8'), OPCODE_TEXT, end, mask)
+
+ def _assert_receive_data(self, payload, opcode, fin, rsv1, rsv2, rsv3):
+ (actual_fin, actual_rsv1, actual_rsv2, actual_rsv3, actual_opcode,
+ payload_length) = read_frame_header(self._socket)
+
+ if actual_opcode != opcode:
+ raise Exception(
+ 'Unexpected opcode: %d (expected) vs %d (actual)' %
+ (opcode, actual_opcode))
+
+ if actual_fin != fin:
+ raise Exception(
+ 'Unexpected fin: %d (expected) vs %d (actual)' %
+ (fin, actual_fin))
+
+ if rsv1 is None:
+ rsv1 = 0
+ if self._handshake._options.use_deflate_frame:
+ rsv1 = 1
+
+ if rsv2 is None:
+ rsv2 = 0
+
+ if rsv3 is None:
+ rsv3 = 0
+
+ if actual_rsv1 != rsv1:
+ raise Exception(
+ 'Unexpected rsv1: %r (expected) vs %r (actual)' %
+ (rsv1, actual_rsv1))
+
+ if actual_rsv2 != rsv2:
+ raise Exception(
+ 'Unexpected rsv2: %r (expected) vs %r (actual)' %
+ (rsv2, actual_rsv2))
+
+ if actual_rsv3 != rsv3:
+ raise Exception(
+ 'Unexpected rsv3: %r (expected) vs %r (actual)' %
+ (rsv3, actual_rsv3))
+
+ received = receive_bytes(self._socket, payload_length)
+
+ if self._incoming_frame_filter is not None:
+ received = self._incoming_frame_filter.filter(received)
+
+ if len(received) != len(payload):
+ raise Exception(
+ 'Unexpected payload length: %d (expected) vs %d (actual)' %
+ (len(payload), len(received)))
+
+ if payload != received:
+ raise Exception(
+ 'Unexpected payload: %r (expected) vs %r (actual)' %
+ (payload, received))
+
+ def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1,
+ rsv1=None, rsv2=None, rsv3=None):
+ self._assert_receive_data(payload, opcode, fin, rsv1, rsv2, rsv3)
+
+ def assert_receive_text(self, payload, opcode=OPCODE_TEXT, fin=1,
+ rsv1=None, rsv2=None, rsv3=None):
+ self._assert_receive_data(payload.encode('utf-8'), opcode, fin, rsv1,
+ rsv2, rsv3)
+
+ def _build_close_frame(self, code, reason, mask):
+ frame = chr(1 << 7 | OPCODE_CLOSE)
+
+ if code is not None:
+ body = struct.pack('!H', code) + reason.encode('utf-8')
+ else:
+ body = ''
+ if mask:
+ frame += chr(1 << 7 | len(body)) + self._mask_hybi(body)
+ else:
+ frame += chr(len(body)) + body
+ return frame
+
+ def send_close(self, code, reason):
+ self._socket.sendall(
+ self._build_close_frame(code, reason, True))
+
+ def assert_receive_close(self, code, reason):
+ expected_frame = self._build_close_frame(code, reason, False)
+ actual_frame = receive_bytes(self._socket, len(expected_frame))
+ if actual_frame != expected_frame:
+ raise Exception(
+ 'Unexpected close frame: %r (expected) vs %r (actual)' %
+ (expected_frame, actual_frame))
+
+
+class WebSocketStreamHixie75(object):
+ """Frame processor for the WebSocket protocol version Hixie 75 and HyBi 00.
+ """
+
+ _CLOSE_FRAME = '\xff\x00'
+
+ def __init__(self, socket, unused_handshake):
+ self._socket = socket
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._socket.sendall(header + body)
+
+ def send_data(self, payload, unused_frame_typem, unused_end, unused_mask):
+ frame = ''.join(['\x00', payload, '\xff'])
+ self._socket.sendall(frame)
+
+ def send_binary(self, unused_payload, unused_end, unused_mask):
+ pass
+
+ def send_text(self, payload, unused_end, unused_mask):
+ encoded_payload = payload.encode('utf-8')
+ frame = ''.join(['\x00', encoded_payload, '\xff'])
+ self._socket.sendall(frame)
+
+ def assert_receive_binary(self, payload, opcode=OPCODE_BINARY, fin=1,
+ rsv1=0, rsv2=0, rsv3=0):
+ raise Exception('Binary frame is not supported in hixie75')
+
+ def assert_receive_text(self, payload):
+ received = receive_bytes(self._socket, 1)
+
+ if received != '\x00':
+ raise Exception(
+ 'Unexpected frame type: %d (expected) vs %d (actual)' %
+ (0, ord(received)))
+
+ received = receive_bytes(self._socket, len(payload) + 1)
+ if received[-1] != '\xff':
+ raise Exception(
+ 'Termination expected: 0xff (expected) vs %r (actual)' %
+ received)
+
+ if received[0:-1] != payload:
+ raise Exception(
+ 'Unexpected payload: %r (expected) vs %r (actual)' %
+ (payload, received[0:-1]))
+
+ def send_close(self, code, reason):
+ self._socket.sendall(self._CLOSE_FRAME)
+
+ def assert_receive_close(self, unused_code, unused_reason):
+ closing = receive_bytes(self._socket, len(self._CLOSE_FRAME))
+ if closing != self._CLOSE_FRAME:
+ raise Exception('Didn\'t receive closing handshake')
+
+
+class ClientOptions(object):
+ """Holds option values to configure the Client object."""
+
+ def __init__(self):
+ self.version = 13
+ self.server_host = ''
+ self.origin = ''
+ self.resource = ''
+ self.server_port = -1
+ self.socket_timeout = 1000
+ self.use_tls = False
+ self.extensions = []
+ # Enable deflate-application-data.
+ self.use_deflate_frame = False
+ # Enable mux
+ self.use_mux = False
+
+ def enable_deflate_frame(self):
+ self.use_deflate_frame = True
+ self.extensions.append(_DEFLATE_FRAME_EXTENSION)
+
+ def enable_mux(self):
+ self.use_mux = True
+ self.extensions.append(_MUX_EXTENSION)
+
+
+def connect_socket_with_retry(host, port, timeout, use_tls,
+ retry=10, sleep_sec=0.1):
+ retry_count = 0
+ while retry_count < retry:
+ try:
+ s = socket.socket()
+ s.settimeout(timeout)
+ s.connect((host, port))
+ if use_tls:
+ return _TLSSocket(s)
+ return s
+ except socket.error, e:
+ if e.errno != errno.ECONNREFUSED:
+ raise
+ else:
+ retry_count = retry_count + 1
+ time.sleep(sleep_sec)
+
+ return None
+
+
+class Client(object):
+ """WebSocket client."""
+
+ def __init__(self, options, handshake, stream_class):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._socket = None
+
+ self._handshake = handshake
+ self._stream_class = stream_class
+
+ def connect(self):
+ self._socket = connect_socket_with_retry(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.socket_timeout,
+ self._options.use_tls)
+
+ self._handshake.handshake(self._socket)
+
+ self._stream = self._stream_class(self._socket, self._handshake)
+
+ self._logger.info('Connection established')
+
+ def send_frame_of_arbitrary_bytes(self, header, body):
+ self._stream.send_frame_of_arbitrary_bytes(header, body)
+
+ def send_message(self, message, end=True, binary=False, raw=False,
+ mask=True):
+ if binary:
+ self._stream.send_binary(message, end, mask)
+ elif raw:
+ self._stream.send_data(message, OPCODE_TEXT, end, mask)
+ else:
+ self._stream.send_text(message, end, mask)
+
+ def assert_receive(self, payload, binary=False):
+ if binary:
+ self._stream.assert_receive_binary(payload)
+ else:
+ self._stream.assert_receive_text(payload)
+
+ def send_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.send_close(code, reason)
+
+ def assert_receive_close(self, code=STATUS_NORMAL_CLOSURE, reason=''):
+ self._stream.assert_receive_close(code, reason)
+
+ def close_socket(self):
+ self._socket.close()
+
+ def assert_connection_closed(self):
+ try:
+ read_data = receive_bytes(self._socket, 1)
+ except Exception, e:
+ if str(e).find(
+ 'Connection closed before receiving requested length ') == 0:
+ return
+ try:
+ error_number, message = e
+ for error_name in ['ECONNRESET', 'WSAECONNRESET']:
+ if (error_name in dir(errno) and
+ error_number == getattr(errno, error_name)):
+ return
+ except:
+ raise e
+ raise e
+
+ raise Exception('Connection is not closed (Read: %r)' % read_data)
+
+
+def create_client(options):
+ return Client(
+ options, WebSocketHandshake(options), WebSocketStream)
+
+
+def create_client_hybi00(options):
+ return Client(
+ options,
+ WebSocketHybi00Handshake(options, '0'),
+ WebSocketStreamHixie75)
+
+
+def create_client_hixie75(options):
+ return Client(
+ options, WebSocketHixie75Handshake(options), WebSocketStreamHixie75)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py
new file mode 100755
index 000000000..47f86fdb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/endtoend_with_external_server.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Test for end-to-end with external server.
+
+This test is not run by run_all.py because it requires some preparations.
+If you would like to run this test correctly, launch Apache with mod_python
+and mod_pywebsocket manually. In addition, you should pass allow_draft75 option
+and example path as handler_scan option and Apache's DocumentRoot.
+"""
+
+
+import optparse
+import sys
+import test.test_endtoend
+import unittest
+
+
+_DEFAULT_WEB_SOCKET_PORT = 80
+
+
+class EndToEndTestWithExternalServer(test.test_endtoend.EndToEndTest):
+ pass
+
+if __name__ == '__main__':
+ parser = optparse.OptionParser()
+ parser.add_option('-p', '--port', dest='port', type='int',
+ default=_DEFAULT_WEB_SOCKET_PORT,
+ help='external test server port.')
+ (options, args) = parser.parse_args()
+
+ test.test_endtoend._use_external_server = True
+ test.test_endtoend._external_server_port = options.port
+
+ unittest.main(argv=[sys.argv[0]])
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
new file mode 100644
index 000000000..6bffcac48
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mock.py
@@ -0,0 +1,221 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Mocks for testing.
+"""
+
+
+import Queue
+import threading
+
+from mod_pywebsocket import common
+from mod_pywebsocket.stream import StreamHixie75
+
+
+class _MockConnBase(object):
+ """Base class of mocks for mod_python.apache.mp_conn.
+
+ This enables tests to check what is written to a (mock) mp_conn.
+ """
+
+ def __init__(self):
+ self._write_data = []
+ self.remote_addr = 'fake_address'
+
+ def write(self, data):
+ """Override mod_python.apache.mp_conn.write."""
+
+ self._write_data.append(data)
+
+ def written_data(self):
+ """Get bytes written to this mock."""
+
+ return ''.join(self._write_data)
+
+
+class MockConn(_MockConnBase):
+ """Mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ """
+
+ def __init__(self, read_data):
+ """Constructs an instance.
+
+ Args:
+ read_data: bytes that should be returned when read* methods are
+ called.
+ """
+
+ _MockConnBase.__init__(self)
+ self._read_data = read_data
+ self._read_pos = 0
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+
+ if self._read_pos >= len(self._read_data):
+ return ''
+ end_index = self._read_data.find('\n', self._read_pos) + 1
+ if not end_index:
+ end_index = len(self._read_data)
+ return self._read_up_to(end_index)
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ if self._read_pos >= len(self._read_data):
+ return ''
+ end_index = min(len(self._read_data), self._read_pos + length)
+ return self._read_up_to(end_index)
+
+ def _read_up_to(self, end_index):
+ line = self._read_data[self._read_pos:end_index]
+ self._read_pos = end_index
+ return line
+
+
+class MockBlockingConn(_MockConnBase):
+ """Blocking mock for mod_python.apache.mp_conn.
+
+ This enables tests to specify what should be read from a (mock) mp_conn as
+ well as to check what is written to it.
+ Callers of read* methods will block if there is no bytes available.
+ """
+
+ def __init__(self):
+ _MockConnBase.__init__(self)
+ self._queue = Queue.Queue()
+
+ def readline(self):
+ """Override mod_python.apache.mp_conn.readline."""
+ line = ''
+ while True:
+ c = self._queue.get()
+ line += c
+ if c == '\n':
+ return line
+
+ def read(self, length):
+ """Override mod_python.apache.mp_conn.read."""
+
+ data = ''
+ for unused in range(length):
+ data += self._queue.get()
+ return data
+
+ def put_bytes(self, bytes):
+ """Put bytes to be read from this mock.
+
+ Args:
+ bytes: bytes to be read.
+ """
+
+ for byte in bytes:
+ self._queue.put(byte)
+
+
+class MockTable(dict):
+ """Mock table.
+
+ This mimics mod_python mp_table. Note that only the methods used by
+ tests are overridden.
+ """
+
+ def __init__(self, copy_from={}):
+ if isinstance(copy_from, dict):
+ copy_from = copy_from.items()
+ for key, value in copy_from:
+ self.__setitem__(key, value)
+
+ def __getitem__(self, key):
+ return super(MockTable, self).__getitem__(key.lower())
+
+ def __setitem__(self, key, value):
+ super(MockTable, self).__setitem__(key.lower(), value)
+
+ def get(self, key, def_value=None):
+ return super(MockTable, self).get(key.lower(), def_value)
+
+
+class MockRequest(object):
+ """Mock request.
+
+ This mimics mod_python request.
+ """
+
+ def __init__(self, uri=None, headers_in={}, connection=None, method='GET',
+ protocol='HTTP/1.1', is_https=False):
+ """Construct an instance.
+
+ Arguments:
+ uri: URI of the request.
+ headers_in: Request headers.
+ connection: Connection used for the request.
+ method: request method.
+ is_https: Whether this request is over SSL.
+
+ See the document of mod_python Request for details.
+ """
+ self.uri = uri
+ self.unparsed_uri = uri
+ self.connection = connection
+ self.method = method
+ self.protocol = protocol
+ self.headers_in = MockTable(headers_in)
+ # self.is_https_ needs to be accessible from tests. To avoid name
+ # conflict with self.is_https(), it is named as such.
+ self.is_https_ = is_https
+ self.ws_stream = StreamHixie75(self, True)
+ self.ws_close_code = None
+ self.ws_close_reason = None
+ self.ws_version = common.VERSION_HYBI00
+ self.ws_deflate = False
+
+ def is_https(self):
+ """Return whether this request is over SSL."""
+ return self.is_https_
+
+
+class MockDispatcher(object):
+ """Mock for dispatch.Dispatcher."""
+
+ def __init__(self):
+ self.do_extra_handshake_called = False
+
+ def do_extra_handshake(self, conn_context):
+ self.do_extra_handshake_called = True
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py
new file mode 100644
index 000000000..dd5435a8c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/mux_client_for_testing.py
@@ -0,0 +1,690 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""WebSocket client utility for testing mux extension.
+
+This code should be independent from mod_pywebsocket. See the comment of
+client_for_testing.py.
+
+NOTE: This code is far from robust like client_for_testing.py.
+"""
+
+
+
+import Queue
+import base64
+import collections
+import email
+import email.parser
+import logging
+import math
+import os
+import random
+import socket
+import struct
+import threading
+
+from mod_pywebsocket import util
+
+from test import client_for_testing
+
+
+_CONTROL_CHANNEL_ID = 0
+_DEFAULT_CHANNEL_ID = 1
+
+_MUX_OPCODE_ADD_CHANNEL_REQUEST = 0
+_MUX_OPCODE_ADD_CHANNEL_RESPONSE = 1
+_MUX_OPCODE_FLOW_CONTROL = 2
+_MUX_OPCODE_DROP_CHANNEL = 3
+_MUX_OPCODE_NEW_CHANNEL_SLOT = 4
+
+
+class _ControlBlock:
+ def __init__(self, opcode):
+ self.opcode = opcode
+
+
+def _parse_handshake_response(response):
+ status_line, header_lines = response.split('\r\n', 1)
+
+ words = status_line.split(' ')
+ if len(words) < 3:
+ raise ValueError('Bad Status-Line syntax %r' % status_line)
+ [version, response_code] = words[:2]
+ if version != 'HTTP/1.1':
+ raise ValueError('Bad response version %r' % version)
+
+ if response_code != '101':
+ raise ValueError('Bad response code %r ' % response_code)
+ headers = email.parser.Parser().parsestr(header_lines)
+ return headers
+
+
+def _parse_channel_id(data, offset=0):
+ length = len(data)
+ remaining = length - offset
+
+ if remaining <= 0:
+ raise Exception('No channel id found')
+
+ channel_id = ord(data[offset])
+ channel_id_length = 1
+ if channel_id & 0xe0 == 0xe0:
+ if remaining < 4:
+ raise Exception('Invalid channel id format')
+ channel_id = struct.unpack('!L',
+ data[offset:offset+4])[0] & 0x1fffffff
+ channel_id_length = 4
+ elif channel_id & 0xc0 == 0xc0:
+ if remaining < 3:
+ raise Exception('Invalid channel id format')
+ channel_id = (((channel_id & 0x1f) << 16) +
+ struct.unpack('!H', data[offset+1:offset+3])[0])
+ channel_id_length = 3
+ elif channel_id & 0x80 == 0x80:
+ if remaining < 2:
+ raise Exception('Invalid channel id format')
+ channel_id = struct.unpack('!H', data[offset:offset+2])[0] & 0x3fff
+ channel_id_length = 2
+
+ return channel_id, channel_id_length
+
+
+def _parse_number(data, offset=0):
+ first_byte = ord(data[offset])
+ if (first_byte & 0x80) != 0:
+ raise Exception('The MSB of number field must be unset')
+ first_byte = first_byte & 0x7f
+ if first_byte == 127:
+ if offset + 9 > len(data):
+ raise Exception('Invalid number')
+ return struct.unpack('!Q', data[offset+1:offset+9])[0], 9
+ if first_byte == 126:
+ if offset + 3 > len(data):
+ raise Exception('Invalid number')
+ return struct.unpack('!H', data[offset+1:offset+3])[0], 3
+ return first_byte, 1
+
+
+def _parse_size_and_contents(data, offset=0):
+ size, advance = _parse_number(data, offset)
+ start_position = offset + advance
+ end_position = start_position + size
+ if len(data) < end_position:
+ raise Exception('Invalid size of control block (%d < %d)' % (
+ len(data), end_position))
+ return data[start_position:end_position], size + advance
+
+
+def _parse_control_blocks(data):
+ blocks = []
+ length = len(data)
+ pos = 0
+
+ while pos < length:
+ first_byte = ord(data[pos])
+ pos += 1
+ opcode = (first_byte >> 5) & 0x7
+ block = _ControlBlock(opcode)
+
+ # TODO(bashi): Support more opcode
+ if opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ block.encode = first_byte & 3
+ block.rejected = (first_byte >> 4) & 1
+
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+
+ encoded_handshake, advance = _parse_size_and_contents(data, pos)
+ block.encoded_handshake = encoded_handshake
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_DROP_CHANNEL:
+ block.mux_error = (first_byte >> 4) & 1
+
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+
+ reason, advance = _parse_size_and_contents(data, pos)
+ if len(reason) == 0:
+ block.drop_code = None
+ block.drop_message = ''
+ elif len(reason) >= 2:
+ block.drop_code = struct.unpack('!H', reason[:2])[0]
+ block.drop_message = reason[2:]
+ else:
+ raise Exception('Invalid DropChannel')
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_FLOW_CONTROL:
+ channel_id, advance = _parse_channel_id(data, pos)
+ block.channel_id = channel_id
+ pos += advance
+ send_quota, advance = _parse_number(data, pos)
+ block.send_quota = send_quota
+ pos += advance
+ blocks.append(block)
+ elif opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ fallback = first_byte & 1
+ slots, advance = _parse_number(data, pos)
+ pos += advance
+ send_quota, advance = _parse_number(data, pos)
+ pos += advance
+ if fallback == 1 and (slots != 0 or send_quota != 0):
+ raise Exception('slots and send_quota must be zero if F bit '
+ 'is set')
+ block.fallback = fallback
+ block.slots = slots
+ block.send_quota = send_quota
+ blocks.append(block)
+ else:
+ raise Exception(
+ 'Unsupported mux opcode %d received' % opcode)
+
+ return blocks
+
+
+def _encode_channel_id(channel_id):
+ if channel_id < 0:
+ raise ValueError('Channel id %d must not be negative' % channel_id)
+
+ if channel_id < 2 ** 7:
+ return chr(channel_id)
+ if channel_id < 2 ** 14:
+ return struct.pack('!H', 0x8000 + channel_id)
+ if channel_id < 2 ** 21:
+ first = chr(0xc0 + (channel_id >> 16))
+ return first + struct.pack('!H', channel_id & 0xffff)
+ if channel_id < 2 ** 29:
+ return struct.pack('!L', 0xe0000000 + channel_id)
+
+ raise ValueError('Channel id %d is too large' % channel_id)
+
+
+def _encode_number(number):
+ if number <= 125:
+ return chr(number)
+ elif number < (1 << 16):
+ return chr(0x7e) + struct.pack('!H', number)
+ elif number < (1 << 63):
+ return chr(0x7f) + struct.pack('!Q', number)
+ else:
+ raise Exception('Invalid number')
+
+
+def _create_add_channel_request(channel_id, encoded_handshake,
+ encoding=0):
+ length = len(encoded_handshake)
+ handshake_length = _encode_number(length)
+
+ first_byte = (_MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding
+ return (chr(first_byte) + _encode_channel_id(channel_id) +
+ handshake_length + encoded_handshake)
+
+
+def _create_flow_control(channel_id, replenished_quota):
+ first_byte = (_MUX_OPCODE_FLOW_CONTROL << 5)
+ return (chr(first_byte) + _encode_channel_id(channel_id) +
+ _encode_number(replenished_quota))
+
+
+class _MuxReaderThread(threading.Thread):
+ """Mux reader thread.
+
+ Reads frames and passes them to the mux client. This thread accesses
+ private functions/variables of the mux client.
+ """
+
+ def __init__(self, mux):
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+ self._mux = mux
+ self._stop_requested = False
+
+ def _receive_message(self):
+ first_opcode = None
+ pending_payload = []
+ while not self._stop_requested:
+ fin, rsv1, rsv2, rsv3, opcode, payload_length = (
+ client_for_testing.read_frame_header(self._mux._socket))
+
+ if not first_opcode:
+ if opcode == client_for_testing.OPCODE_TEXT:
+ raise Exception('Received a text message on physical '
+ 'connection')
+ if opcode == client_for_testing.OPCODE_CONTINUATION:
+ raise Exception('Received an intermediate frame but '
+ 'fragmentation was not started')
+ if (opcode == client_for_testing.OPCODE_BINARY or
+ opcode == client_for_testing.OPCODE_PONG or
+ opcode == client_for_testing.OPCODE_PONG or
+ opcode == client_for_testing.OPCODE_CLOSE):
+ first_opcode = opcode
+ else:
+ raise Exception('Received an undefined opcode frame: %d' %
+ opcode)
+
+ elif opcode != client_for_testing.OPCODE_CONTINUATION:
+ raise Exception('Received a new opcode before '
+ 'terminating fragmentation')
+
+ payload = client_for_testing.receive_bytes(
+ self._mux._socket, payload_length)
+
+ if self._mux._incoming_frame_filter is not None:
+ payload = self._mux._incoming_frame_filter.filter(payload)
+
+ pending_payload.append(payload)
+
+ if fin:
+ break
+
+ if self._stop_requested:
+ return None, None
+
+ message = ''.join(pending_payload)
+ return first_opcode, message
+
+ def request_stop(self):
+ self._stop_requested = True
+
+ def run(self):
+ try:
+ while not self._stop_requested:
+ # opcode is OPCODE_BINARY or control opcodes when a message
+ # is succesfully received.
+ opcode, message = self._receive_message()
+ if not opcode:
+ return
+ if opcode == client_for_testing.OPCODE_BINARY:
+ channel_id, advance = _parse_channel_id(message)
+ self._mux._dispatch_frame(channel_id, message[advance:])
+ else:
+ self._mux._process_control_message(opcode, message)
+ finally:
+ self._mux._notify_reader_done()
+
+
+class _InnerFrame(object):
+ def __init__(self, fin, rsv1, rsv2, rsv3, opcode, payload):
+ self.fin = fin
+ self.rsv1 = rsv1
+ self.rsv2 = rsv2
+ self.rsv3 = rsv3
+ self.opcode = opcode
+ self.payload = payload
+
+
+class _LogicalChannelData(object):
+ def __init__(self):
+ self.queue = Queue.Queue()
+ self.send_quota = 0
+ self.receive_quota = 0
+
+
+class MuxClient(object):
+ """WebSocket mux client.
+
+ Note that this class is NOT thread-safe. Do not access an instance of this
+ class from multiple threads at a same time.
+ """
+
+ def __init__(self, options):
+ self._logger = util.get_class_logger(self)
+
+ self._options = options
+ self._options.enable_mux()
+ self._stream = None
+ self._socket = None
+ self._handshake = client_for_testing.WebSocketHandshake(self._options)
+ self._incoming_frame_filter = None
+ self._outgoing_frame_filter = None
+
+ self._is_active = False
+ self._read_thread = None
+ self._control_blocks_condition = threading.Condition()
+ self._control_blocks = []
+ self._channel_slots = collections.deque()
+ self._logical_channels_condition = threading.Condition();
+ self._logical_channels = {}
+ self._timeout = 2
+ self._physical_connection_close_event = None
+ self._physical_connection_close_message = None
+
+ def _parse_inner_frame(self, data):
+ if len(data) == 0:
+ raise Exception('Invalid encapsulated frame received')
+
+ first_byte = ord(data[0])
+ fin = (first_byte << 7) & 1
+ rsv1 = (first_byte << 6) & 1
+ rsv2 = (first_byte << 5) & 1
+ rsv3 = (first_byte << 4) & 1
+ opcode = first_byte & 0xf
+
+ if self._outgoing_frame_filter:
+ payload = self._outgoing_frame_filter.filter(
+ data[1:])
+ else:
+ payload = data[1:]
+
+ return _InnerFrame(fin, rsv1, rsv2, rsv3, opcode, payload)
+
+ def _process_mux_control_blocks(self):
+ for block in self._control_blocks:
+ if block.opcode == _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ # AddChannelResponse will be handled in add_channel().
+ continue
+ elif block.opcode == _MUX_OPCODE_FLOW_CONTROL:
+ try:
+ self._logical_channels_condition.acquire()
+ if not block.channel_id in self._logical_channels:
+ raise Exception('Invalid flow control received for '
+ 'channel id %d' % block.channel_id)
+ self._logical_channels[block.channel_id].send_quota += (
+ block.send_quota)
+ self._logical_channels_condition.notify()
+ finally:
+ self._logical_channels_condition.release()
+ elif block.opcode == _MUX_OPCODE_NEW_CHANNEL_SLOT:
+ self._channel_slots.extend([block.send_quota] * block.slots)
+
+ def _dispatch_frame(self, channel_id, payload):
+ if channel_id == _CONTROL_CHANNEL_ID:
+ try:
+ self._control_blocks_condition.acquire()
+ self._control_blocks += _parse_control_blocks(payload)
+ self._process_mux_control_blocks()
+ self._control_blocks_condition.notify()
+ finally:
+ self._control_blocks_condition.release()
+ else:
+ try:
+ self._logical_channels_condition.acquire()
+ if not channel_id in self._logical_channels:
+ raise Exception('Received logical frame on channel id '
+ '%d, which is not established' %
+ channel_id)
+
+ inner_frame = self._parse_inner_frame(payload)
+ self._logical_channels[channel_id].receive_quota -= (
+ len(inner_frame.payload))
+ if self._logical_channels[channel_id].receive_quota < 0:
+ raise Exception('The server violates quota on '
+ 'channel id %d' % channel_id)
+ finally:
+ self._logical_channels_condition.release()
+ self._logical_channels[channel_id].queue.put(inner_frame)
+
+ def _process_control_message(self, opcode, message):
+ # Ping/Pong are not supported.
+ if opcode == client_for_testing.OPCODE_CLOSE:
+ self._physical_connection_close_message = message
+ if self._is_active:
+ self._stream.send_close(
+ code=client_for_testing.STATUS_NORMAL_CLOSURE, reason='')
+ self._read_thread.request_stop()
+
+ if self._physical_connection_close_event:
+ self._physical_connection_close_event.set()
+
+ def _notify_reader_done(self):
+ self._logger.debug('Read thread terminated.')
+ self.close_socket()
+
+ def _assert_channel_slot_available(self):
+ try:
+ self._control_blocks_condition.acquire()
+ if len(self._channel_slots) == 0:
+ # Wait once
+ self._control_blocks_condition.wait(timeout=self._timeout)
+ finally:
+ self._control_blocks_condition.release()
+
+ if len(self._channel_slots) == 0:
+ raise Exception('Failed to receive NewChannelSlot')
+
+ def _assert_send_quota_available(self, channel_id):
+ try:
+ self._logical_channels_condition.acquire()
+ if self._logical_channels[channel_id].send_quota == 0:
+ # Wait once
+ self._logical_channels_condition.wait(timeout=self._timeout)
+ finally:
+ self._logical_channels_condition.release()
+
+ if self._logical_channels[channel_id].send_quota == 0:
+ raise Exception('Failed to receive FlowControl for channel id %d' %
+ channel_id)
+
+ def connect(self):
+ self._socket = client_for_testing.connect_socket_with_retry(
+ self._options.server_host,
+ self._options.server_port,
+ self._options.socket_timeout,
+ self._options.use_tls)
+
+ self._handshake.handshake(self._socket)
+ self._stream = client_for_testing.WebSocketStream(
+ self._socket, self._handshake)
+
+ self._logical_channels[_DEFAULT_CHANNEL_ID] = _LogicalChannelData()
+
+ self._read_thread = _MuxReaderThread(self)
+ self._read_thread.start()
+
+ self._assert_channel_slot_available()
+ self._assert_send_quota_available(_DEFAULT_CHANNEL_ID)
+
+ self._is_active = True
+ self._logger.info('Connection established')
+
+ def add_channel(self, channel_id, options):
+ if not self._is_active:
+ raise Exception('Mux client is not active')
+
+ if channel_id in self._logical_channels:
+ raise Exception('Channel id %d already exists' % channel_id)
+
+ try:
+ send_quota = self._channel_slots.popleft()
+ except IndexError, e:
+ raise Exception('No channel slots: %r' % e)
+
+ # Create AddChannel request
+ request_line = 'GET %s HTTP/1.1\r\n' % options.resource
+ fields = []
+ if options.server_port == client_for_testing.DEFAULT_PORT:
+ fields.append('Host: %s\r\n' % options.server_host.lower())
+ else:
+ fields.append('Host: %s:%d\r\n' % (options.server_host.lower(),
+ options.server_port))
+ fields.append('Origin: %s\r\n' % options.origin.lower())
+ fields.append('Connection: Upgrade\r\n')
+
+ if len(options.extensions) > 0:
+ fields.append('Sec-WebSocket-Extensions: %s\r\n' %
+ ', '.join(options.extensions))
+
+ handshake = request_line + ''.join(fields) + '\r\n'
+ add_channel_request = _create_add_channel_request(
+ channel_id, handshake)
+ payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + add_channel_request
+ self._stream.send_binary(payload)
+
+ # Wait AddChannelResponse
+ self._logger.debug('Waiting AddChannelResponse for the request...')
+ response = None
+ try:
+ self._control_blocks_condition.acquire()
+ while True:
+ for block in self._control_blocks:
+ if block.opcode != _MUX_OPCODE_ADD_CHANNEL_RESPONSE:
+ continue
+ if block.channel_id == channel_id:
+ response = block
+ self._control_blocks.remove(response)
+ break
+ if response:
+ break
+ self._control_blocks_condition.wait(self._timeout)
+ if not self._is_active:
+ raise Exception('AddChannelRequest timed out')
+ finally:
+ self._control_blocks_condition.release()
+
+ # Validate AddChannelResponse
+ if response.rejected:
+ raise Exception('The server rejected AddChannelRequest')
+
+ fields = _parse_handshake_response(response.encoded_handshake)
+
+ # Should we reject when Upgrade, Connection, or Sec-WebSocket-Accept
+ # headers exist?
+
+ self._logical_channels_condition.acquire()
+ self._logical_channels[channel_id] = _LogicalChannelData()
+ self._logical_channels[channel_id].send_quota = send_quota
+ self._logical_channels_condition.release()
+
+ self._logger.debug('Logical channel %d established' % channel_id)
+
+ def _check_logical_channel_is_opened(self, channel_id):
+ if not self._is_active:
+ raise Exception('Mux client is not active')
+
+ if not channel_id in self._logical_channels:
+ raise Exception('Logical channel %d is not established.')
+
+ def drop_channel(self, channel_id):
+ # TODO(bashi): Implement
+ pass
+
+ def send_flow_control(self, channel_id, replenished_quota):
+ self._check_logical_channel_is_opened(channel_id)
+ flow_control = _create_flow_control(channel_id, replenished_quota)
+ payload = _encode_channel_id(_CONTROL_CHANNEL_ID) + flow_control
+ # Replenish receive quota
+ try:
+ self._logical_channels_condition.acquire()
+ self._logical_channels[channel_id].receive_quota += (
+ replenished_quota)
+ finally:
+ self._logical_channels_condition.release()
+ self._stream.send_binary(payload)
+
+ def send_message(self, channel_id, message, end=True, binary=False):
+ self._check_logical_channel_is_opened(channel_id)
+
+ if binary:
+ first_byte = (end << 7) | client_for_testing.OPCODE_BINARY
+ else:
+ first_byte = (end << 7) | client_for_testing.OPCODE_TEXT
+ message = message.encode('utf-8')
+
+ try:
+ self._logical_channels_condition.acquire()
+ if self._logical_channels[channel_id].send_quota < len(message):
+ raise Exception('Send quota violation: %d < %d' % (
+ self._logical_channels[channel_id].send_quota,
+ len(message)))
+
+ self._logical_channels[channel_id].send_quota -= len(message)
+ finally:
+ self._logical_channels_condition.release()
+ payload = _encode_channel_id(channel_id) + chr(first_byte) + message
+ self._stream.send_binary(payload)
+
+ def assert_receive(self, channel_id, payload, binary=False):
+ self._check_logical_channel_is_opened(channel_id)
+
+ try:
+ inner_frame = self._logical_channels[channel_id].queue.get(
+ timeout=self._timeout)
+ except Queue.Empty, e:
+ raise Exception('Cannot receive message from channel id %d' %
+ channel_id)
+
+ if binary:
+ opcode = client_for_testing.OPCODE_BINARY
+ else:
+ opcode = client_for_testing.OPCODE_TEXT
+
+ if inner_frame.opcode != opcode:
+ raise Exception('Unexpected opcode received (%r != %r)' %
+ (expected_opcode, inner_frame.opcode))
+
+ if inner_frame.payload != payload:
+ raise Exception('Unexpected payload received')
+
+ def send_close(self, channel_id, code=None, reason=''):
+ self._check_logical_channel_is_opened(channel_id)
+
+ if code is not None:
+ body = struct.pack('!H', code) + reason.encode('utf-8')
+ else:
+ body = ''
+
+ first_byte = (1 << 7) | client_for_testing.OPCODE_CLOSE
+ payload = _encode_channel_id(channel_id) + chr(first_byte) + body
+ self._stream.send_binary(payload)
+
+ def assert_receive_close(self, channel_id):
+ self._check_logical_channel_is_opened(channel_id)
+
+ try:
+ inner_frame = self._logical_channels[channel_id].queue.get(
+ timeout=self._timeout)
+ except Queue.Empty, e:
+ raise Exception('Cannot receive message from channel id %d' %
+ channel_id)
+ if inner_frame.opcode != client_for_testing.OPCODE_CLOSE:
+ raise Exception('Didn\'t receive close frame')
+
+ def send_physical_connection_close(self, code=None, reason=''):
+ self._physical_connection_close_event = threading.Event()
+ self._stream.send_close(code, reason)
+
+ # This method can be used only after calling
+ # send_physical_connection_close().
+ def assert_physical_connection_receive_close(
+ self, code=client_for_testing.STATUS_NORMAL_CLOSURE, reason=''):
+ self._physical_connection_close_event.wait(timeout=self._timeout)
+ if (not self._physical_connection_close_event.isSet() or
+ not self._physical_connection_close_message):
+ raise Exception('Didn\'t receive closing handshake')
+
+ def close_socket(self):
+ self._is_active = False
+ self._socket.close()
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py
new file mode 100755
index 000000000..80a5d87d8
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/run_all.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Run all tests in the same directory.
+
+This suite is expected to be run under pywebsocket's src directory, i.e. the
+directory containing mod_pywebsocket, test, etc.
+
+To change loggin level, please specify --log-level option.
+ python test/run_test.py --log-level debug
+
+To pass any option to unittest module, please specify options after '--'. For
+example, run this for making the test runner verbose.
+ python test/run_test.py --log-level debug -- -v
+"""
+
+
+import logging
+import optparse
+import os
+import re
+import sys
+import unittest
+
+
+_TEST_MODULE_PATTERN = re.compile(r'^(test_.+)\.py$')
+
+
+def _list_test_modules(directory):
+ module_names = []
+ for filename in os.listdir(directory):
+ match = _TEST_MODULE_PATTERN.search(filename)
+ if match:
+ module_names.append(match.group(1))
+ return module_names
+
+
+def _suite():
+ loader = unittest.TestLoader()
+ return loader.loadTestsFromNames(
+ _list_test_modules(os.path.join(os.path.split(__file__)[0], '.')))
+
+
+if __name__ == '__main__':
+ parser = optparse.OptionParser()
+ parser.add_option('--log-level', '--log_level', type='choice',
+ dest='log_level', default='warning',
+ choices=['debug', 'info', 'warning', 'warn', 'error',
+ 'critical'])
+ options, args = parser.parse_args()
+ logging.basicConfig(
+ level=logging.getLevelName(options.log_level.upper()),
+ format='%(levelname)s %(asctime)s '
+ '%(filename)s:%(lineno)d] '
+ '%(message)s',
+ datefmt='%H:%M:%S')
+ unittest.main(defaultTest='_suite', argv=[sys.argv[0]] + args)
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py
new file mode 100644
index 000000000..e3c6db9ea
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/set_sys_path.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Configuration for testing.
+
+Test files should import this module before mod_pywebsocket.
+"""
+
+
+import os
+import sys
+
+
+# Add the parent directory to sys.path to enable importing mod_pywebsocket.
+sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '..'))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py
new file mode 100755
index 000000000..9ca3d4f3a
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_dispatch.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for dispatch module."""
+
+
+import os
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from test import mock
+
+
+_TEST_HANDLERS_DIR = os.path.join(
+ os.path.split(__file__)[0], 'testdata', 'handlers')
+
+_TEST_HANDLERS_SUB_DIR = os.path.join(_TEST_HANDLERS_DIR, 'sub')
+
+
+class DispatcherTest(unittest.TestCase):
+ """A unittest for dispatch module."""
+
+ def test_normalize_path(self):
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/b'))
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('\\a\\b'))
+ self.assertEqual(os.path.abspath('/a/b').replace('\\', '/'),
+ dispatch._normalize_path('/a/c/../b'))
+ self.assertEqual(os.path.abspath('abc').replace('\\', '/'),
+ dispatch._normalize_path('abc'))
+
+ def test_converter(self):
+ converter = dispatch._create_path_to_resource_converter('/a/b')
+ # Python built by MSC inserts a drive name like 'C:\' via realpath().
+ # Converter Generator expands provided path using realpath() and uses
+ # the path including a drive name to verify the prefix.
+ os_root = os.path.realpath('/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual('/c/h', converter(os_root + 'a/b/c/h_wsh.py'))
+ self.assertEqual(None, converter(os_root + 'a/b/h.py'))
+ self.assertEqual(None, converter('a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter('a/b')
+ self.assertEqual('/h', converter(dispatch._normalize_path(
+ 'a/b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter('/a/b///')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+ self.assertEqual('/h', converter(dispatch._normalize_path(
+ '/a/b/../b/h_wsh.py')))
+
+ converter = dispatch._create_path_to_resource_converter(
+ '/a/../a/b/../b/')
+ self.assertEqual('/h', converter(os_root + 'a/b/h_wsh.py'))
+
+ converter = dispatch._create_path_to_resource_converter(r'\a\b')
+ self.assertEqual('/h', converter(os_root + r'a\b\h_wsh.py'))
+ self.assertEqual('/h', converter(os_root + r'a/b/h_wsh.py'))
+
+ def test_enumerate_handler_file_paths(self):
+ paths = list(
+ dispatch._enumerate_handler_file_paths(_TEST_HANDLERS_DIR))
+ paths.sort()
+ self.assertEqual(8, len(paths))
+ expected_paths = [
+ os.path.join(_TEST_HANDLERS_DIR, 'abort_by_user_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'blank_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'origin_check_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'exception_in_transfer_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub', 'plain_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_handshake_sig_wsh.py'),
+ os.path.join(_TEST_HANDLERS_DIR, 'sub',
+ 'wrong_transfer_sig_wsh.py'),
+ ]
+ for expected, actual in zip(expected_paths, paths):
+ self.assertEqual(expected, actual)
+
+ def test_source_handler_file(self):
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, '')
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, 'def')
+ self.assertRaises(
+ dispatch.DispatchException, dispatch._source_handler_file, '1/0')
+ self.failUnless(dispatch._source_handler_file(
+ 'def web_socket_do_extra_handshake(request):pass\n'
+ 'def web_socket_transfer_data(request):pass\n'))
+
+ def test_source_warnings(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ warnings = dispatcher.source_warnings()
+ warnings.sort()
+ expected_warnings = [
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'blank_wsh.py')) +
+ ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'non_callable_wsh.py')) +
+ ': web_socket_do_extra_handshake is not callable.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'wrong_handshake_sig_wsh.py')) +
+ ': web_socket_do_extra_handshake is not defined.'),
+ (os.path.realpath(os.path.join(
+ _TEST_HANDLERS_DIR, 'sub', 'wrong_transfer_sig_wsh.py')) +
+ ': web_socket_transfer_data is not defined.'),
+ ]
+ self.assertEquals(4, len(warnings))
+ for expected, actual in zip(expected_warnings, warnings):
+ self.assertEquals(expected, actual)
+
+ def test_do_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/origin_check'
+ request.ws_origin = 'http://example.com'
+ dispatcher.do_extra_handshake(request) # Must not raise exception.
+
+ request.ws_origin = 'http://bad.example.com'
+ try:
+ dispatcher.do_extra_handshake(request)
+ self.fail('Could not catch HandshakeException with 403 status')
+ except handshake.HandshakeException, e:
+ self.assertEquals(403, e.status)
+ except Exception, e:
+ self.fail('Unexpected exception: %r' % e)
+
+ def test_abort_extra_handshake(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.do_extra_handshake, request)
+
+ def test_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/origin_check'
+ request.ws_protocol = 'p1'
+ dispatcher.transfer_data(request)
+ self.assertEqual('origin_check_wsh.py is called for /origin_check, p1'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain?'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain?, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ request = mock.MockRequest(connection=mock.MockConn('\xff\x00'))
+ request.ws_resource = '/sub/plain?q=v'
+ request.ws_protocol = None
+ dispatcher.transfer_data(request)
+ self.assertEqual('sub/plain_wsh.py is called for /sub/plain?q=v, None'
+ '\xff\x00',
+ request.connection.written_data())
+
+ def test_transfer_data_no_handler(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ for resource in ['/blank', '/sub/non_callable',
+ '/sub/no_wsh_at_the_end', '/does/not/exist']:
+ request = mock.MockRequest(connection=mock.MockConn(''))
+ request.ws_resource = resource
+ request.ws_protocol = 'p2'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except dispatch.DispatchException, e:
+ self.failUnless(str(e).find('No handler') != -1)
+ except Exception:
+ self.fail()
+
+ def test_transfer_data_handler_exception(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest(connection=mock.MockConn(''))
+ request.ws_resource = '/sub/exception_in_transfer'
+ request.ws_protocol = 'p3'
+ try:
+ dispatcher.transfer_data(request)
+ self.fail()
+ except Exception, e:
+ self.failUnless(str(e).find('Intentional') != -1,
+ 'Unexpected exception: %s' % e)
+
+ def test_abort_transfer_data(self):
+ dispatcher = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ request = mock.MockRequest()
+ request.ws_resource = '/abort_by_user'
+ self.assertRaises(handshake.AbortedByUserException,
+ dispatcher.transfer_data, request)
+
+ def test_scan_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ self.assertEqual(4, len(disp._handler_suite_map))
+ self.failUnless('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.failIf('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+
+ def test_scan_sub_dir_as_root(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_SUB_DIR,
+ _TEST_HANDLERS_SUB_DIR)
+ self.assertEqual(2, len(disp._handler_suite_map))
+ self.failIf('/origin_check' in disp._handler_suite_map)
+ self.failIf('/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failIf('/sub/plain' in disp._handler_suite_map)
+ self.failUnless('/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/plain' in disp._handler_suite_map)
+
+ def test_scan_dir_must_under_root(self):
+ dispatch.Dispatcher('a/b', 'a/b/c') # OK
+ dispatch.Dispatcher('a/b///', 'a/b') # OK
+ self.assertRaises(dispatch.DispatchException,
+ dispatch.Dispatcher, 'a/b/c', 'a/b')
+
+ def test_resource_path_alias(self):
+ disp = dispatch.Dispatcher(_TEST_HANDLERS_DIR, None)
+ disp.add_resource_path_alias('/', '/origin_check')
+ self.assertEqual(5, len(disp._handler_suite_map))
+ self.failUnless('/origin_check' in disp._handler_suite_map)
+ self.failUnless(
+ '/sub/exception_in_transfer' in disp._handler_suite_map)
+ self.failUnless('/sub/plain' in disp._handler_suite_map)
+ self.failUnless('/' in disp._handler_suite_map)
+ self.assertRaises(dispatch.DispatchException,
+ disp.add_resource_path_alias, '/alias', '/not-exist')
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py
new file mode 100755
index 000000000..5e5cf6157
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_endtoend.py
@@ -0,0 +1,753 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""End-to-end tests for pywebsocket. Tests standalone.py by default. You
+can also test mod_pywebsocket hosted on an Apache server by setting
+_use_external_server to True and modifying _external_server_port to point to
+the port on which the Apache server is running.
+"""
+
+
+import logging
+import os
+import signal
+import socket
+import subprocess
+import sys
+import time
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import client_for_testing
+from test import mux_client_for_testing
+
+
+# Special message that tells the echo server to start closing handshake
+_GOODBYE_MESSAGE = 'Goodbye'
+
+_SERVER_WARMUP_IN_SEC = 0.2
+
+# If you want to use external server to run end to end tests, set following
+# parameters correctly.
+_use_external_server = False
+_external_server_port = 0
+
+
+# Test body functions
+def _echo_check_procedure(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+ client.send_message('helloworld')
+ client.assert_receive('helloworld')
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_binary(client):
+ client.connect()
+
+ client.send_message('binary', binary=True)
+ client.assert_receive('binary', binary=True)
+ client.send_message('\x00\x80\xfe\xff\x00\x80', binary=True)
+ client.assert_receive('\x00\x80\xfe\xff\x00\x80', binary=True)
+
+ client.send_close()
+ client.assert_receive_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_goodbye(client):
+ client.connect()
+
+ client.send_message('test')
+ client.assert_receive('test')
+
+ client.send_message(_GOODBYE_MESSAGE)
+ client.assert_receive(_GOODBYE_MESSAGE)
+
+ client.assert_receive_close()
+ client.send_close()
+
+ client.assert_connection_closed()
+
+
+def _echo_check_procedure_with_code_and_reason(client, code, reason):
+ client.connect()
+
+ client.send_close(code, reason)
+ client.assert_receive_close(code, reason)
+
+ client.assert_connection_closed()
+
+
+def _unmasked_frame_check_procedure(client):
+ client.connect()
+
+ client.send_message('test', mask=False)
+ client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '')
+
+ client.assert_connection_closed()
+
+
+def _mux_echo_check_procedure(mux_client):
+ mux_client.connect()
+ mux_client.send_flow_control(1, 1024)
+
+ logical_channel_options = client_for_testing.ClientOptions()
+ logical_channel_options.server_host = 'localhost'
+ logical_channel_options.server_port = 80
+ logical_channel_options.origin = 'http://localhost'
+ logical_channel_options.resource = '/echo'
+ mux_client.add_channel(2, logical_channel_options)
+ mux_client.send_flow_control(2, 1024)
+
+ mux_client.send_message(2, 'test')
+ mux_client.assert_receive(2, 'test')
+
+ mux_client.add_channel(3, logical_channel_options)
+ mux_client.send_flow_control(3, 1024)
+
+ mux_client.send_message(2, 'hello')
+ mux_client.send_message(3, 'world')
+ mux_client.assert_receive(2, 'hello')
+ mux_client.assert_receive(3, 'world')
+
+ # Don't send close message on channel id 1 so that server-initiated
+ # closing handshake won't occur.
+ mux_client.send_close(2)
+ mux_client.send_close(3)
+ mux_client.assert_receive_close(2)
+ mux_client.assert_receive_close(3)
+
+ mux_client.send_physical_connection_close()
+ mux_client.assert_physical_connection_receive_close()
+
+
+class EndToEndTestBase(unittest.TestCase):
+ """Base class for end-to-end tests that launch pywebsocket standalone
+ server as a separate process, connect to it using the client_for_testing
+ module, and check if the server behaves correctly by exchanging opening
+ handshake and frames over a TCP connection.
+ """
+
+ def setUp(self):
+ self.server_stderr = None
+ self.top_dir = os.path.join(os.path.split(__file__)[0], '..')
+ os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
+ self.standalone_command = os.path.join(
+ self.top_dir, 'mod_pywebsocket', 'standalone.py')
+ self.document_root = os.path.join(self.top_dir, 'example')
+ s = socket.socket()
+ s.bind(('localhost', 0))
+ (_, self.test_port) = s.getsockname()
+ s.close()
+
+ self._options = client_for_testing.ClientOptions()
+ self._options.server_host = 'localhost'
+ self._options.origin = 'http://localhost'
+ self._options.resource = '/echo'
+
+ # TODO(toyoshim): Eliminate launching a standalone server on using
+ # external server.
+
+ if _use_external_server:
+ self._options.server_port = _external_server_port
+ else:
+ self._options.server_port = self.test_port
+
+ # TODO(tyoshino): Use tearDown to kill the server.
+
+ def _run_python_command(self, commandline, stdout=None, stderr=None):
+ return subprocess.Popen([sys.executable] + commandline, close_fds=True,
+ stdout=stdout, stderr=stderr)
+
+ def _run_server(self):
+ args = [self.standalone_command,
+ '-H', 'localhost',
+ '-V', 'localhost',
+ '-p', str(self.test_port),
+ '-P', str(self.test_port),
+ '-d', self.document_root]
+
+ # Inherit the level set to the root logger by test runner.
+ root_logger = logging.getLogger()
+ log_level = root_logger.getEffectiveLevel()
+ if log_level != logging.NOTSET:
+ args.append('--log-level')
+ args.append(logging.getLevelName(log_level).lower())
+
+ return self._run_python_command(args,
+ stderr=self.server_stderr)
+
+ def _kill_process(self, pid):
+ if sys.platform in ('win32', 'cygwin'):
+ subprocess.call(
+ ('taskkill.exe', '/f', '/pid', str(pid)), close_fds=True)
+ else:
+ os.kill(pid, signal.SIGKILL)
+
+
+class EndToEndHyBiTest(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _run_test_with_client_options(self, test_function, options):
+ server = self._run_server()
+ try:
+ # TODO(tyoshino): add some logic to poll the server until it
+ # becomes ready
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_test(self, test_function):
+ self._run_test_with_client_options(test_function, self._options)
+
+ def _run_deflate_frame_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ self._options.enable_deflate_frame()
+ client = client_for_testing.create_client(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_permessage_deflate_test(
+ self, offer, response_checker, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ 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_close_with_code_and_reason_test(self, test_function, code,
+ reason):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(self._options)
+ try:
+ test_function(client, code, reason)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_http_fallback_test(self, options, status):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client(options)
+ try:
+ client.connect()
+ self.fail('Could not catch HttpStatusException')
+ except client_for_testing.HttpStatusException, e:
+ self.assertEqual(status, e.status)
+ except Exception, e:
+ self.fail('Catch unexpected exception')
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def _run_mux_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = mux_client_for_testing.MuxClient(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def test_echo(self):
+ self._run_test(_echo_check_procedure)
+
+ def test_echo_binary(self):
+ self._run_test(_echo_check_procedure_with_binary)
+
+ def test_echo_server_close(self):
+ self._run_test(_echo_check_procedure_with_goodbye)
+
+ def test_unmasked_frame(self):
+ self._run_test(_unmasked_frame_check_procedure)
+
+ def test_echo_deflate_frame(self):
+ self._run_deflate_frame_test(_echo_check_procedure)
+
+ def test_echo_deflate_frame_server_close(self):
+ self._run_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_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_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_messages(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(
+ '\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x00\x11\x00\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_permessage_deflate_test(
+ ['permessage-deflate'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_two_msgs_server_no_context_takeover(self):
+ def test_function(client):
+ # From the examples in the spec.
+ client._stream.send_data(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.send_data(
+ '\xf2\x00\x11\x00\x00',
+ client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ client._stream.assert_receive_binary(
+ '\xf2\x48\xcd\xc9\xc9\x07\x00',
+ opcode=client_for_testing.OPCODE_TEXT,
+ rsv1=1)
+ 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([('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_no_context_takeover'],
+ 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_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([('server_max_window_bits', '10'),
+ ('server_no_context_takeover', None)],
+ parameter.get_parameters())
+
+ self._run_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=10; '
+ 'server_no_context_takeover'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_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_permessage_deflate_test(
+ ['permessage-deflate; server_max_window_bits=3000000'],
+ response_checker,
+ test_function)
+
+ def test_echo_permessage_deflate_with_bad_server_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_permessage_deflate_test(
+ ['permessage-deflate; server_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_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_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun')
+
+ def test_echo_close_with_empty_body(self):
+ self._options.resource = '/close'
+ self._run_close_with_code_and_reason_test(
+ _echo_check_procedure_with_code_and_reason, None, '')
+
+ def test_mux_echo(self):
+ self._run_mux_test(_mux_echo_check_procedure)
+
+ def test_close_on_protocol_error(self):
+ """Tests that the server sends a close frame with protocol error status
+ code when the client sends data with some protocol error.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Intermediate frame without any preceding start of fragmentation
+ # frame.
+ client.send_frame_of_arbitrary_bytes('\x80\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_PROTOCOL_ERROR)
+
+ self._run_test(test_function)
+
+ def test_close_on_unsupported_frame(self):
+ """Tests that the server sends a close frame with unsupported operation
+ status code when the client sends data asking some operation that is
+ not supported by the server.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Text frame with RSV3 bit raised.
+ client.send_frame_of_arbitrary_bytes('\x91\x80', '')
+ client.assert_receive_close(
+ client_for_testing.STATUS_UNSUPPORTED_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_invalid_frame(self):
+ """Tests that the server sends a close frame with invalid frame payload
+ data status code when the client sends an invalid frame like containing
+ invalid UTF-8 character.
+ """
+
+ def test_function(client):
+ client.connect()
+
+ # Text frame with invalid UTF-8 string.
+ client.send_message('\x80', raw=True)
+ client.assert_receive_close(
+ client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA)
+
+ self._run_test(test_function)
+
+ def test_close_on_internal_endpoint_error(self):
+ """Tests that the server sends a close frame with internal endpoint
+ error status code when the handler does bad operation.
+ """
+
+ self._options.resource = '/internal_error'
+
+ def test_function(client):
+ client.connect()
+ client.assert_receive_close(
+ client_for_testing.STATUS_INTERNAL_ENDPOINT_ERROR)
+
+ self._run_test(test_function)
+
+ # TODO(toyoshim): Add tests to verify invalid absolute uri handling like
+ # host unmatch, port unmatch and invalid port description (':' without port
+ # number).
+
+ def test_absolute_uri(self):
+ """Tests absolute uri request."""
+
+ options = self._options
+ options.resource = 'ws://localhost:%d/echo' % options.server_port
+ self._run_test_with_client_options(_echo_check_procedure, options)
+
+ def test_origin_check(self):
+ """Tests http fallback on origin check fail."""
+
+ options = self._options
+ options.resource = '/origin_check'
+ # Server shows warning message for http 403 fallback. This warning
+ # message is confusing. Following pipe disposes warning messages.
+ self.server_stderr = subprocess.PIPE
+ self._run_http_fallback_test(options, 403)
+
+ def test_version_check(self):
+ """Tests http fallback on version check fail."""
+
+ options = self._options
+ options.version = 99
+ self._run_http_fallback_test(options, 400)
+
+
+class EndToEndHyBi00Test(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _run_test(self, test_function):
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client = client_for_testing.create_client_hybi00(self._options)
+ try:
+ test_function(client)
+ finally:
+ client.close_socket()
+ finally:
+ self._kill_process(server.pid)
+
+ def test_echo(self):
+ self._run_test(_echo_check_procedure)
+
+ def test_echo_server_close(self):
+ self._run_test(_echo_check_procedure_with_goodbye)
+
+
+class EndToEndTestWithEchoClient(EndToEndTestBase):
+ def setUp(self):
+ EndToEndTestBase.setUp(self)
+
+ def _check_example_echo_client_result(
+ self, expected, stdoutdata, stderrdata):
+ actual = stdoutdata.decode("utf-8")
+ if actual != expected:
+ raise Exception('Unexpected result on example echo client: '
+ '%r (expected) vs %r (actual)' %
+ (expected, actual))
+ if stderrdata is not None:
+ raise Exception('Unexpected error message on example echo '
+ 'client: %r' % stderrdata)
+
+ def test_example_echo_client(self):
+ """Tests that the echo_client.py example can talk with the server."""
+
+ server = self._run_server()
+ try:
+ time.sleep(_SERVER_WARMUP_IN_SEC)
+
+ client_command = os.path.join(
+ self.top_dir, 'example', 'echo_client.py')
+
+ # Expected output for the default messages.
+ default_expectation = ('Send: Hello\n' 'Recv: Hello\n'
+ u'Send: \u65e5\u672c\n' u'Recv: \u65e5\u672c\n'
+ 'Send close\n' 'Recv ack\n')
+
+ args = [client_command,
+ '-p', str(self._options.server_port)]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(
+ default_expectation, stdoutdata, stderrdata)
+
+ # Process a big message for which extended payload length is used.
+ # To handle extended payload length, ws_version attribute will be
+ # accessed. This test checks that ws_version is correctly set.
+ big_message = 'a' * 1024
+ args = [client_command,
+ '-p', str(self._options.server_port),
+ '-m', big_message]
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' %
+ (big_message, big_message))
+ self._check_example_echo_client_result(
+ expected, stdoutdata, stderrdata)
+
+ # Test the permessage-deflate extension.
+ args = [client_command,
+ '-p', str(self._options.server_port),
+ '--use_permessage_deflate']
+ client = self._run_python_command(args, stdout=subprocess.PIPE)
+ stdoutdata, stderrdata = client.communicate()
+ self._check_example_echo_client_result(
+ default_expectation, stdoutdata, stderrdata)
+ finally:
+ self._kill_process(server.pid)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py
new file mode 100755
index 000000000..6c8b1262d
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_extensions.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for extensions module."""
+
+
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import extensions
+
+
+class ExtensionsTest(unittest.TestCase):
+ """A unittest for non-class methods in extensions.py"""
+
+ def test_parse_window_bits(self):
+ self.assertRaises(ValueError, extensions._parse_window_bits, None)
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'foobar')
+ self.assertRaises(ValueError, extensions._parse_window_bits, ' 8 ')
+ self.assertRaises(ValueError, extensions._parse_window_bits, 'a8a')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00000')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '00008')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0x8')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '9.5')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '8.0')
+
+ self.assertTrue(extensions._parse_window_bits, '8')
+ self.assertTrue(extensions._parse_window_bits, '15')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '-8')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '0')
+ self.assertRaises(ValueError, extensions._parse_window_bits, '7')
+
+ self.assertRaises(ValueError, extensions._parse_window_bits, '16')
+ self.assertRaises(
+ ValueError, extensions._parse_window_bits, '10000000')
+
+
+class CompressionMethodParameterParserTest(unittest.TestCase):
+ """A unittest for _parse_compression_method which parses the compression
+ method description used by perframe-compression and permessage-compression
+ extension in their "method" extension parameter.
+ """
+
+ def test_parse_method_simple(self):
+ method_list = extensions._parse_compression_method('foo')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(0, len(method.get_parameters()))
+
+ def test_parse_method_with_parameter(self):
+ method_list = extensions._parse_compression_method('foo; x; y=10')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(2, len(method.get_parameters()))
+ self.assertTrue(method.has_parameter('x'))
+ self.assertEqual(None, method.get_parameter_value('x'))
+ self.assertTrue(method.has_parameter('y'))
+ self.assertEqual('10', method.get_parameter_value('y'))
+
+ def test_parse_method_with_quoted_parameter(self):
+ method_list = extensions._parse_compression_method(
+ 'foo; x="Hello World"; y=10')
+ self.assertEqual(1, len(method_list))
+ method = method_list[0]
+ self.assertEqual('foo', method.name())
+ self.assertEqual(2, len(method.get_parameters()))
+ self.assertTrue(method.has_parameter('x'))
+ self.assertEqual('Hello World', method.get_parameter_value('x'))
+ self.assertTrue(method.has_parameter('y'))
+ self.assertEqual('10', method.get_parameter_value('y'))
+
+ def test_parse_method_multiple(self):
+ method_list = extensions._parse_compression_method('foo, bar')
+ self.assertEqual(2, len(method_list))
+ self.assertEqual('foo', method_list[0].name())
+ self.assertEqual(0, len(method_list[0].get_parameters()))
+ self.assertEqual('bar', method_list[1].name())
+ self.assertEqual(0, len(method_list[1].get_parameters()))
+
+ def test_parse_method_multiple_methods_with_quoted_parameter(self):
+ method_list = extensions._parse_compression_method(
+ 'foo; x="Hello World", bar; y=10')
+ self.assertEqual(2, len(method_list))
+ self.assertEqual('foo', method_list[0].name())
+ self.assertEqual(1, len(method_list[0].get_parameters()))
+ self.assertTrue(method_list[0].has_parameter('x'))
+ self.assertEqual('Hello World',
+ method_list[0].get_parameter_value('x'))
+ self.assertEqual('bar', method_list[1].name())
+ self.assertEqual(1, len(method_list[1].get_parameters()))
+ self.assertTrue(method_list[1].has_parameter('y'))
+ self.assertEqual('10', method_list[1].get_parameter_value('y'))
+
+ def test_create_method_desc_simple(self):
+ params = common.ExtensionParameter('foo')
+ desc = extensions._create_accepted_method_desc('foo',
+ params.get_parameters())
+ self.assertEqual('foo', desc)
+
+ def test_create_method_desc_with_parameters(self):
+ params = common.ExtensionParameter('foo')
+ params.add_parameter('x', 'Hello, World')
+ params.add_parameter('y', '10')
+ desc = extensions._create_accepted_method_desc('foo',
+ params.get_parameters())
+ self.assertEqual('foo; x="Hello, World"; y=10', desc)
+
+
+class DeflateFrameExtensionProcessorParsingTest(unittest.TestCase):
+ """A unittest for checking that DeflateFrameExtensionProcessor parses given
+ extension parameter correctly.
+ """
+
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('deflate-frame'))
+ self.assertIsInstance(processor,
+ extensions.DeflateFrameExtensionProcessor)
+
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('x-webkit-deflate-frame'))
+ self.assertIsInstance(processor,
+ extensions.DeflateFrameExtensionProcessor)
+
+ def test_minimal_offer(self):
+ processor = extensions.DeflateFrameExtensionProcessor(
+ common.ExtensionParameter('perframe-deflate'))
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(zlib.MAX_WBITS,
+ processor._rfc1979_deflater._window_bits)
+ self.assertFalse(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', '10')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(10, processor._rfc1979_deflater._window_bits)
+
+ def test_offer_with_out_of_range_max_window_bits(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', '0')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_max_window_bits_without_value(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('max_window_bits', None)
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_no_context_takeover(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('no_context_takeover', None)
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertTrue(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_no_context_takeover_with_value(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('no_context_takeover', 'foobar')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_unknown_parameter(self):
+ parameter = common.ExtensionParameter('perframe-deflate')
+ parameter.add_parameter('foo', 'bar')
+ processor = extensions.DeflateFrameExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('perframe-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+
+class PerMessageDeflateExtensionProcessorParsingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor parses
+ given extension parameter correctly.
+ """
+
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('permessage-deflate'))
+ self.assertIsInstance(processor,
+ extensions.PerMessageDeflateExtensionProcessor)
+
+ def test_minimal_offer(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+ self.assertEqual(zlib.MAX_WBITS,
+ processor._rfc1979_deflater._window_bits)
+ self.assertFalse(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '10')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_max_window_bits', '10')],
+ response.get_parameters())
+
+ self.assertEqual(10, processor._rfc1979_deflater._window_bits)
+
+ def test_offer_with_out_of_range_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', '0')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_max_window_bits_without_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_no_context_takeover(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('server_no_context_takeover', None)],
+ response.get_parameters())
+
+ self.assertTrue(processor._rfc1979_deflater._no_context_takeover)
+
+ def test_offer_with_no_context_takeover_with_value(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('server_no_context_takeover', 'foobar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+ def test_offer_with_unknown_parameter(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('foo', 'bar')
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+
+ self.assertIsNone(processor.get_extension_response())
+
+
+class PerMessageDeflateExtensionProcessorBuildingTest(unittest.TestCase):
+ """A unittest for checking that PerMessageDeflateExtensionProcessor builds
+ a response based on specified options correctly.
+ """
+
+ def test_response_with_max_window_bits(self):
+ parameter = common.ExtensionParameter('permessage-deflate')
+ parameter.add_parameter('client_max_window_bits', None)
+ processor = extensions.PerMessageDeflateExtensionProcessor(parameter)
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_max_window_bits', '10')],
+ response.get_parameters())
+
+ def test_response_with_max_window_bits_without_client_permission(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+ processor.set_client_max_window_bits(10)
+
+ response = processor.get_extension_response()
+ self.assertIsNone(response)
+
+ def test_response_with_true_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(True)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual([('client_no_context_takeover', None)],
+ response.get_parameters())
+
+ def test_response_with_false_for_no_context_takeover(self):
+ processor = extensions.PerMessageDeflateExtensionProcessor(
+ common.ExtensionParameter('permessage-deflate'))
+
+ processor.set_client_no_context_takeover(False)
+
+ response = processor.get_extension_response()
+ self.assertEqual('permessage-deflate', response.name())
+ self.assertEqual(0, len(response.get_parameters()))
+
+
+class PerMessageCompressExtensionProcessorTest(unittest.TestCase):
+ def test_registry(self):
+ processor = extensions.get_extension_processor(
+ common.ExtensionParameter('permessage-compress'))
+ self.assertIsInstance(processor,
+ extensions.PerMessageCompressExtensionProcessor)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py
new file mode 100755
index 000000000..aa78ac05e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake._base module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.common import ExtensionParameter
+from mod_pywebsocket.common import ExtensionParsingException
+from mod_pywebsocket.common import format_extensions
+from mod_pywebsocket.common import parse_extensions
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import validate_subprotocol
+
+
+class ValidateSubprotocolTest(unittest.TestCase):
+ """A unittest for validate_subprotocol method."""
+
+ def test_validate_subprotocol(self):
+ # Should succeed.
+ validate_subprotocol('sample')
+ validate_subprotocol('Sample')
+ validate_subprotocol('sample\x7eprotocol')
+
+ # Should fail.
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ '')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x09protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x19protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x20protocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ 'sample\x7fprotocol')
+ self.assertRaises(HandshakeException,
+ validate_subprotocol,
+ # "Japan" in Japanese
+ u'\u65e5\u672c')
+
+
+_TEST_TOKEN_EXTENSION_DATA = [
+ ('foo', [('foo', [])]),
+ ('foo; bar', [('foo', [('bar', None)])]),
+ ('foo; bar=baz', [('foo', [('bar', 'baz')])]),
+ ('foo; bar=baz; car=cdr', [('foo', [('bar', 'baz'), ('car', 'cdr')])]),
+ ('foo; bar=baz, car; cdr',
+ [('foo', [('bar', 'baz')]), ('car', [('cdr', None)])]),
+ ('a, b, c, d',
+ [('a', []), ('b', []), ('c', []), ('d', [])]),
+ ]
+
+
+_TEST_QUOTED_EXTENSION_DATA = [
+ ('foo; bar=""', [('foo', [('bar', '')])]),
+ ('foo; bar=" baz "', [('foo', [('bar', ' baz ')])]),
+ ('foo; bar=",baz;"', [('foo', [('bar', ',baz;')])]),
+ ('foo; bar="\\\r\\\nbaz"', [('foo', [('bar', '\r\nbaz')])]),
+ ('foo; bar="\\"baz"', [('foo', [('bar', '"baz')])]),
+ ('foo; bar="\xbbbaz"', [('foo', [('bar', '\xbbbaz')])]),
+ ]
+
+
+_TEST_REDUNDANT_TOKEN_EXTENSION_DATA = [
+ ('foo \t ', [('foo', [])]),
+ ('foo; \r\n bar', [('foo', [('bar', None)])]),
+ ('foo; bar=\r\n \r\n baz', [('foo', [('bar', 'baz')])]),
+ ('foo ;bar = baz ', [('foo', [('bar', 'baz')])]),
+ ('foo,bar,,baz', [('foo', []), ('bar', []), ('baz', [])]),
+ ]
+
+
+_TEST_REDUNDANT_QUOTED_EXTENSION_DATA = [
+ ('foo; bar="\r\n \r\n baz"', [('foo', [('bar', ' baz')])]),
+ ]
+
+
+class ExtensionsParserTest(unittest.TestCase):
+
+ def _verify_extension_list(self, expected_list, actual_list):
+ """Verifies that ExtensionParameter objects in actual_list have the
+ same members as extension definitions in expected_list. Extension
+ definition used in this test is a pair of an extension name and a
+ parameter dictionary.
+ """
+
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ (name, parameters) = expected
+ self.assertEqual(name, actual._name)
+ self.assertEqual(parameters, actual._parameters)
+
+ def test_parse(self):
+ for formatted_string, definition in _TEST_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_quoted_data(self):
+ for formatted_string, definition in _TEST_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_redundant_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_TOKEN_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_redundant_quoted_data(self):
+ for (formatted_string,
+ definition) in _TEST_REDUNDANT_QUOTED_EXTENSION_DATA:
+ self._verify_extension_list(
+ definition, parse_extensions(formatted_string))
+
+ def test_parse_bad_data(self):
+ _TEST_BAD_EXTENSION_DATA = [
+ ('foo; ; '),
+ ('foo; a a'),
+ ('foo foo'),
+ (',,,'),
+ ('foo; bar='),
+ ('foo; bar="hoge'),
+ ('foo; bar="a\r"'),
+ ('foo; bar="\\\xff"'),
+ ('foo; bar=\ra'),
+ ]
+
+ for formatted_string in _TEST_BAD_EXTENSION_DATA:
+ self.assertRaises(
+ ExtensionParsingException, parse_extensions, formatted_string)
+
+
+class FormatExtensionsTest(unittest.TestCase):
+
+ def test_format_extensions(self):
+ for formatted_string, definitions in _TEST_TOKEN_EXTENSION_DATA:
+ extensions = []
+ for definition in definitions:
+ (name, parameters) = definition
+ extension = ExtensionParameter(name)
+ extension._parameters = parameters
+ extensions.append(extension)
+ self.assertEqual(
+ formatted_string, format_extensions(extensions))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py
new file mode 100755
index 000000000..6c8713823
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+from mod_pywebsocket import common
+from mod_pywebsocket.handshake._base import AbortedByUserException
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake._base import VersionException
+from mod_pywebsocket.handshake.hybi import Handshaker
+
+import mock
+
+
+class RequestDefinition(object):
+ """A class for holding data for constructing opening handshake strings for
+ testing the opening handshake processor.
+ """
+
+ def __init__(self, method, uri, headers):
+ self.method = method
+ self.uri = uri
+ self.headers = headers
+
+
+def _create_good_request_def():
+ return RequestDefinition(
+ 'GET', '/demo',
+ {'Host': 'server.example.com',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': '13',
+ 'Origin': 'http://example.com'})
+
+
+def _create_request(request_def):
+ conn = mock.MockConn('')
+ return mock.MockRequest(
+ method=request_def.method,
+ uri=request_def.uri,
+ headers_in=request_def.headers,
+ connection=conn)
+
+
+def _create_handshaker(request):
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ return handshaker
+
+
+class SubprotocolChoosingDispatcher(object):
+ """A dispatcher for testing. This dispatcher sets the i-th subprotocol
+ of requested ones to ws_protocol where i is given on construction as index
+ argument. If index is negative, default_value will be set to ws_protocol.
+ """
+
+ def __init__(self, index, default_value=None):
+ self.index = index
+ self.default_value = default_value
+
+ def do_extra_handshake(self, conn_context):
+ if self.index >= 0:
+ conn_context.ws_protocol = conn_context.ws_requested_protocols[
+ self.index]
+ else:
+ conn_context.ws_protocol = self.default_value
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class HandshakeAbortedException(Exception):
+ pass
+
+
+class AbortingDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an exception in
+ do_extra_handshake to reject the request.
+ """
+
+ def do_extra_handshake(self, conn_context):
+ raise HandshakeAbortedException('An exception to reject the request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+class AbortedByUserDispatcher(object):
+ """A dispatcher for testing. This dispatcher raises an
+ AbortedByUserException in do_extra_handshake to reject the request.
+ """
+
+ def do_extra_handshake(self, conn_context):
+ raise AbortedByUserException('An AbortedByUserException to reject the '
+ 'request')
+
+ def transfer_data(self, conn_context):
+ pass
+
+
+_EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n')
+
+
+class HandshakerTest(unittest.TestCase):
+ """A unittest for draft-ietf-hybi-thewebsocketprotocol-06 and later
+ handshake processor.
+ """
+
+ def test_do_handshake(self):
+ request = _create_request(_create_good_request_def())
+ dispatcher = mock.MockDispatcher()
+ handshaker = Handshaker(request, dispatcher)
+ handshaker.do_handshake()
+
+ self.assertTrue(dispatcher.do_extra_handshake_called)
+
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('/demo', request.ws_resource)
+ self.assertEqual('http://example.com', request.ws_origin)
+ self.assertEqual(None, request.ws_protocol)
+ self.assertEqual(None, request.ws_extensions)
+ self.assertEqual(common.VERSION_HYBI_LATEST, request.ws_version)
+
+ def test_do_handshake_with_extra_headers(self):
+ request_def = _create_good_request_def()
+ # Add headers not related to WebSocket opening handshake.
+ request_def.headers['FooKey'] = 'BarValue'
+ request_def.headers['EmptyKey'] = ''
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_capitalized_value(self):
+ request_def = _create_good_request_def()
+ request_def.headers['upgrade'] = 'WEBSOCKET'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'UPGRADE'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_do_handshake_with_multiple_connection_values(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Upgrade, keep-alive, , '
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ _EXPECTED_RESPONSE, request.connection.written_data())
+
+ def test_aborting_handshake(self):
+ handshaker = Handshaker(
+ _create_request(_create_good_request_def()),
+ AbortingDispatcher())
+ # do_extra_handshake raises an exception. Check that it's not caught by
+ # do_handshake.
+ self.assertRaises(HandshakeAbortedException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, SubprotocolChoosingDispatcher(0))
+ handshaker.do_handshake()
+
+ EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ 'Sec-WebSocket-Protocol: chat\r\n\r\n')
+
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual('chat', request.ws_protocol)
+
+ def test_do_handshake_protocol_not_in_request_but_in_response(self):
+ request_def = _create_good_request_def()
+ request = _create_request(request_def)
+ handshaker = Handshaker(
+ request, SubprotocolChoosingDispatcher(-1, 'foobar'))
+ # No request has been made but ws_protocol is set. HandshakeException
+ # must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_protocol_no_protocol_selection(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'chat, superchat'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ # ws_protocol is not set. HandshakeException must be raised.
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+ def test_do_handshake_with_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate, unknown')
+
+ EXPECTED_RESPONSE = (
+ 'HTTP/1.1 101 Switching Protocols\r\n'
+ 'Upgrade: websocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
+ 'Sec-WebSocket-Extensions: permessage-compress; method=deflate\r\n'
+ '\r\n')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(EXPECTED_RESPONSE, request.connection.written_data())
+ self.assertEqual(1, len(request.ws_extensions))
+ extension = request.ws_extensions[0]
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ extension.name())
+ self.assertEqual(['method'], extension.get_parameter_names())
+ self.assertEqual('deflate', extension.get_parameter_value('method'))
+ self.assertEqual(1, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+
+ def test_do_handshake_with_permessage_compress(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate')
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(1, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+
+ def test_do_handshake_with_quoted_extensions(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ 'permessage-compress; method=deflate, , '
+ 'unknown; e = "mc^2"; ma="\r\n \\\rf "; pv=nrt')
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(2, len(request.ws_requested_extensions))
+ first_extension = request.ws_requested_extensions[0]
+ self.assertEqual('permessage-compress', first_extension.name())
+ self.assertEqual(['method'], first_extension.get_parameter_names())
+ self.assertEqual('deflate',
+ first_extension.get_parameter_value('method'))
+ second_extension = request.ws_requested_extensions[1]
+ self.assertEqual('unknown', second_extension.name())
+ self.assertEqual(
+ ['e', 'ma', 'pv'], second_extension.get_parameter_names())
+ self.assertEqual('mc^2', second_extension.get_parameter_value('e'))
+ self.assertEqual(' \rf ', second_extension.get_parameter_value('ma'))
+ self.assertEqual('nrt', second_extension.get_parameter_value('pv'))
+
+ def test_do_handshake_with_optional_headers(self):
+ request_def = _create_good_request_def()
+ request_def.headers['EmptyValue'] = ''
+ request_def.headers['AKey'] = 'AValue'
+
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ self.assertEqual(
+ 'AValue', request.headers_in['AKey'])
+ self.assertEqual(
+ '', request.headers_in['EmptyValue'])
+
+ def test_abort_extra_handshake(self):
+ handshaker = Handshaker(
+ _create_request(_create_good_request_def()),
+ AbortedByUserDispatcher())
+ # do_extra_handshake raises an AbortedByUserException. Check that it's
+ # not caught by do_handshake.
+ self.assertRaises(AbortedByUserException, handshaker.do_handshake)
+
+ def test_do_handshake_with_mux_and_deflate_frame(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % (
+ common.MUX_EXTENSION,
+ common.DEFLATE_FRAME_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux_processor'))
+
+ def test_do_handshake_with_deflate_frame_and_mux(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = ('%s, %s' % (
+ common.DEFLATE_FRAME_EXTENSION,
+ common.MUX_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ first_extension = request.ws_extensions[0]
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ first_extension.name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.DEFLATE_FRAME_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux'))
+
+ def test_do_handshake_with_permessage_compress_and_mux(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ '%s; method=deflate, %s' % (
+ common.PERMESSAGE_COMPRESSION_EXTENSION,
+ common.MUX_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+
+ self.assertEqual(1, len(request.ws_extensions))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extensions[0].name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertTrue(hasattr(request, 'mux_processor'))
+ self.assertTrue(request.mux_processor.is_active())
+ mux_extensions = request.mux_processor.extensions()
+ self.assertEqual(1, len(mux_extensions))
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ mux_extensions[0].name())
+
+ def test_do_handshake_with_mux_and_permessage_compress(self):
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Extensions'] = (
+ '%s, %s; method=deflate' % (
+ common.MUX_EXTENSION,
+ common.PERMESSAGE_COMPRESSION_EXTENSION))
+ request = _create_request(request_def)
+ handshaker = _create_handshaker(request)
+ handshaker.do_handshake()
+ # mux should be rejected.
+ self.assertEqual(1, len(request.ws_extensions))
+ first_extension = request.ws_extensions[0]
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ first_extension.name())
+ self.assertEqual(2, len(request.ws_extension_processors))
+ self.assertEqual(common.MUX_EXTENSION,
+ request.ws_extension_processors[0].name())
+ self.assertEqual(common.PERMESSAGE_COMPRESSION_EXTENSION,
+ request.ws_extension_processors[1].name())
+ self.assertFalse(hasattr(request, 'mux_processor'))
+
+ def test_bad_requests(self):
+ bad_cases = [
+ ('HTTP request',
+ RequestDefinition(
+ 'GET', '/demo',
+ {'Host': 'www.google.com',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;'
+ ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3'
+ ' GTB6 GTBA',
+ 'Accept':
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,'
+ '*/*;q=0.8',
+ 'Accept-Language': 'en-us,en;q=0.5',
+ 'Accept-Encoding': 'gzip,deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive': '300',
+ 'Connection': 'keep-alive'}), None, True)]
+
+ request_def = _create_good_request_def()
+ request_def.method = 'POST'
+ bad_cases.append(('Wrong method', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Host']
+ bad_cases.append(('Missing Host', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Upgrade']
+ bad_cases.append(('Missing Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Upgrade'] = 'nonwebsocket'
+ bad_cases.append(('Wrong Upgrade', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Connection']
+ bad_cases.append(('Missing Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Connection'] = 'Downgrade'
+ bad_cases.append(('Wrong Connection', request_def, None, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Key']
+ bad_cases.append(('Missing Sec-WebSocket-Key', request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==garbage')
+ bad_cases.append(('Wrong Sec-WebSocket-Key (with garbage on the tail)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = 'YQ==' # BASE64 of 'a'
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (decoded value is not 16 octets long)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ # The last character right before == must be any of A, Q, w and g.
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'AQIDBAUGBwgJCgsMDQ4PEC==')
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (padding bits are not zero)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Key'] = (
+ 'dGhlIHNhbXBsZSBub25jZQ==,dGhlIHNhbXBsZSBub25jZQ==')
+ bad_cases.append(
+ ('Wrong Sec-WebSocket-Key (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ del request_def.headers['Sec-WebSocket-Version']
+ bad_cases.append(('Missing Sec-WebSocket-Version', request_def, None,
+ True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '3'
+ bad_cases.append(('Wrong Sec-WebSocket-Version', request_def, None,
+ False))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Version'] = '13, 13'
+ bad_cases.append(('Wrong Sec-WebSocket-Version (multiple values)',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = 'illegal\x09protocol'
+ bad_cases.append(('Illegal Sec-WebSocket-Protocol',
+ request_def, 400, True))
+
+ request_def = _create_good_request_def()
+ request_def.headers['Sec-WebSocket-Protocol'] = ''
+ bad_cases.append(('Empty Sec-WebSocket-Protocol',
+ request_def, 400, True))
+
+ for (case_name, request_def, expected_status,
+ expect_handshake_exception) in bad_cases:
+ request = _create_request(request_def)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ try:
+ handshaker.do_handshake()
+ self.fail('No exception thrown for \'%s\' case' % case_name)
+ except HandshakeException, e:
+ self.assertTrue(expect_handshake_exception)
+ self.assertEqual(expected_status, e.status)
+ except VersionException, e:
+ self.assertFalse(expect_handshake_exception)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py
new file mode 100755
index 000000000..73f9f27ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_handshake_hybi00.py
@@ -0,0 +1,516 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for handshake.hybi00 module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.handshake._base import HandshakeException
+from mod_pywebsocket.handshake.hybi00 import Handshaker
+from mod_pywebsocket.handshake.hybi00 import _validate_subprotocol
+from test import mock
+
+
+_TEST_KEY1 = '4 @1 46546xW%0l 1 5'
+_TEST_KEY2 = '12998 5 Y3 1 .P00'
+_TEST_KEY3 = '^n:ds[4U'
+_TEST_CHALLENGE_RESPONSE = '8jKS\'y:G*Co,Wxa-'
+
+
+_GOOD_REQUEST = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'UPGRADE',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WEBSOCKET',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'hOsT': 'example.com',
+ 'cOnNeCtIoN': 'Upgrade',
+ 'sEc-wEbsOcKeT-kEy2': _TEST_KEY2,
+ 'sEc-wEbsOcKeT-pRoToCoL': 'sample',
+ 'uPgRaDe': 'WebSocket',
+ 'sEc-wEbsOcKeT-kEy1': _TEST_KEY1,
+ 'oRiGiN': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_DEFAULT_PORT = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_RESPONSE_SECURE = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: wss://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_NONDEFAULT_PORT = (
+ 8081,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com:8081',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_NONDEFAULT_PORT = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com:8081/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_RESPONSE_SECURE_NONDEF = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: wss://example.com:8081/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_NO_PROTOCOL = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_NO_PROTOCOL = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_GOOD_REQUEST_WITH_OPTIONAL_HEADERS = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'EmptyValue': '',
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'AKey': 'AValue',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+# TODO(tyoshino): Include \r \n in key3, challenge response.
+
+_GOOD_REQUEST_WITH_NONPRINTABLE_KEY = (
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': 'y R2 48 Q1O4 e|BV3 i5 1 u- 65',
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': '36 7 74 i 92 2\'m 9 0G',
+ 'Origin': 'http://example.com',
+ },
+ ''.join(map(chr, [0x01, 0xd1, 0xdd, 0x3b, 0xd1, 0x56, 0x63, 0xff])))
+
+_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ ''.join(map(chr, [0x0b, 0x99, 0xfa, 0x55, 0xbd, 0x01, 0x23, 0x7b,
+ 0x45, 0xa2, 0xf1, 0xd0, 0x87, 0x8a, 0xee, 0xeb])))
+
+_GOOD_REQUEST_WITH_QUERY_PART = (
+ 80,
+ 'GET',
+ '/demo?e=mc2',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3)
+
+_GOOD_RESPONSE_WITH_QUERY_PART = (
+ 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n'
+ 'Upgrade: WebSocket\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Sec-WebSocket-Location: ws://example.com/demo?e=mc2\r\n'
+ 'Sec-WebSocket-Origin: http://example.com\r\n'
+ 'Sec-WebSocket-Protocol: sample\r\n'
+ '\r\n' +
+ _TEST_CHALLENGE_RESPONSE)
+
+_BAD_REQUESTS = (
+ ( # HTTP request
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'www.google.com',
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5;'
+ ' en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3'
+ ' GTB6 GTBA',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,'
+ '*/*;q=0.8',
+ 'Accept-Language': 'en-us,en;q=0.5',
+ 'Accept-Encoding': 'gzip,deflate',
+ 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
+ 'Keep-Alive': '300',
+ 'Connection': 'keep-alive',
+ }),
+ ( # Wrong method
+ 80,
+ 'POST',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Missing Upgrade
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Wrong Upgrade
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'NonWebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Empty WebSocket-Protocol
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': '',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Wrong port number format
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com:0x50',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Header/connection port mismatch
+ 8080,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'sample',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+ ( # Illegal WebSocket-Protocol
+ 80,
+ 'GET',
+ '/demo',
+ {
+ 'Host': 'example.com',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key2': _TEST_KEY2,
+ 'Sec-WebSocket-Protocol': 'illegal\x09protocol',
+ 'Upgrade': 'WebSocket',
+ 'Sec-WebSocket-Key1': _TEST_KEY1,
+ 'Origin': 'http://example.com',
+ },
+ _TEST_KEY3),
+)
+
+
+def _create_request(request_def):
+ data = ''
+ if len(request_def) > 4:
+ data = request_def[4]
+ conn = mock.MockConn(data)
+ conn.local_addr = ('0.0.0.0', request_def[0])
+ return mock.MockRequest(
+ method=request_def[1],
+ uri=request_def[2],
+ headers_in=request_def[3],
+ connection=conn)
+
+
+def _create_get_memorized_lines(lines):
+ """Creates a function that returns the given string."""
+
+ def get_memorized_lines():
+ return lines
+ return get_memorized_lines
+
+
+def _create_requests_with_lines(request_lines_set):
+ requests = []
+ for lines in request_lines_set:
+ request = _create_request(_GOOD_REQUEST)
+ request.connection.get_memorized_lines = _create_get_memorized_lines(
+ lines)
+ requests.append(request)
+ return requests
+
+
+class HyBi00HandshakerTest(unittest.TestCase):
+
+ def test_good_request_default_port(self):
+ request = _create_request(_GOOD_REQUEST)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+ self.assertEqual('/demo', request.ws_resource)
+ self.assertEqual('http://example.com', request.ws_origin)
+ self.assertEqual('ws://example.com/demo', request.ws_location)
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_capitalized_header_values(self):
+ request = _create_request(_GOOD_REQUEST_CAPITALIZED_HEADER_VALUES)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+
+ def test_good_request_case_mixed_header_names(self):
+ request = _create_request(_GOOD_REQUEST_CASE_MIXED_HEADER_NAMES)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_DEFAULT_PORT,
+ request.connection.written_data())
+
+ def test_good_request_secure_default_port(self):
+ request = _create_request(_GOOD_REQUEST)
+ request.connection.local_addr = ('0.0.0.0', 443)
+ request.is_https_ = True
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_SECURE,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_nondefault_port(self):
+ request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT)
+ handshaker = Handshaker(request,
+ mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_NONDEFAULT_PORT,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_secure_non_default_port(self):
+ request = _create_request(_GOOD_REQUEST_NONDEFAULT_PORT)
+ request.is_https_ = True
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_SECURE_NONDEF,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_default_no_protocol(self):
+ request = _create_request(_GOOD_REQUEST_NO_PROTOCOL)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_NO_PROTOCOL,
+ request.connection.written_data())
+ self.assertEqual(None, request.ws_protocol)
+
+ def test_good_request_optional_headers(self):
+ request = _create_request(_GOOD_REQUEST_WITH_OPTIONAL_HEADERS)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual('AValue',
+ request.headers_in['AKey'])
+ self.assertEqual('',
+ request.headers_in['EmptyValue'])
+
+ def test_good_request_with_nonprintable_key(self):
+ request = _create_request(_GOOD_REQUEST_WITH_NONPRINTABLE_KEY)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_WITH_NONPRINTABLE_KEY,
+ request.connection.written_data())
+ self.assertEqual('sample', request.ws_protocol)
+
+ def test_good_request_with_query_part(self):
+ request = _create_request(_GOOD_REQUEST_WITH_QUERY_PART)
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ handshaker.do_handshake()
+ self.assertEqual(_GOOD_RESPONSE_WITH_QUERY_PART,
+ request.connection.written_data())
+ self.assertEqual('ws://example.com/demo?e=mc2', request.ws_location)
+
+ def test_bad_requests(self):
+ for request in map(_create_request, _BAD_REQUESTS):
+ handshaker = Handshaker(request, mock.MockDispatcher())
+ self.assertRaises(HandshakeException, handshaker.do_handshake)
+
+
+class HyBi00ValidateSubprotocolTest(unittest.TestCase):
+ def test_validate_subprotocol(self):
+ # should succeed.
+ _validate_subprotocol('sample')
+ _validate_subprotocol('Sample')
+ _validate_subprotocol('sample\x7eprotocol')
+ _validate_subprotocol('sample\x20protocol')
+
+ # should fail.
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ '')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ 'sample\x19protocol')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ 'sample\x7fprotocol')
+ self.assertRaises(HandshakeException,
+ _validate_subprotocol,
+ # "Japan" in Japanese
+ u'\u65e5\u672c')
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py
new file mode 100755
index 000000000..436dc57c3
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_http_header_util.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for http_header_util module."""
+
+
+import unittest
+
+from mod_pywebsocket import http_header_util
+
+
+class UnitTest(unittest.TestCase):
+ """A unittest for http_header_util module."""
+
+ def test_parse_relative_uri(self):
+ host, port, resource = http_header_util.parse_uri('/ws/test')
+ self.assertEqual(None, host)
+ self.assertEqual(None, port)
+ self.assertEqual('/ws/test', resource)
+
+ def test_parse_absolute_uri(self):
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://localhost:10080/ws/test')
+ self.assertEqual('localhost', host)
+ self.assertEqual(10080, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com/ws/test')
+ self.assertEqual('example.com', host)
+ self.assertEqual(80, port)
+ self.assertEqual('/ws/test', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'wss://example.com/')
+ self.assertEqual('example.com', host)
+ self.assertEqual(443, port)
+ self.assertEqual('/', resource)
+
+ host, port, resource = http_header_util.parse_uri(
+ 'ws://example.com:8080')
+ self.assertEqual('example.com', host)
+ self.assertEqual(8080, port)
+ self.assertEqual('/', resource)
+
+ def test_parse_invalid_uri(self):
+ host, port, resource = http_header_util.parse_uri('ws:///')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri('ws://localhost:')
+ self.assertEqual(None, resource)
+
+ host, port, resource = http_header_util.parse_uri('ws://localhost:/ws')
+ self.assertEqual(None, resource)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py
new file mode 100755
index 000000000..8f1b8eef4
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_memorizingfile.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for memorizingfile module."""
+
+
+import StringIO
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import memorizingfile
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for memorizingfile module."""
+
+ def check(self, memorizing_file, num_read, expected_list):
+ for unused in range(num_read):
+ memorizing_file.readline()
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ for expected, actual in zip(expected_list, actual_list):
+ self.assertEqual(expected, actual)
+
+ def check_with_size(self, memorizing_file, read_size, expected_list):
+ read_list = []
+ read_line = ''
+ while True:
+ line = memorizing_file.readline(read_size)
+ line_length = len(line)
+ self.assertTrue(line_length <= read_size)
+ if line_length == 0:
+ if read_line != '':
+ read_list.append(read_line)
+ break
+ read_line += line
+ if line[line_length - 1] == '\n':
+ read_list.append(read_line)
+ read_line = ''
+ actual_list = memorizing_file.get_memorized_lines()
+ self.assertEqual(len(expected_list), len(actual_list))
+ self.assertEqual(len(expected_list), len(read_list))
+ for expected, actual, read in zip(expected_list, actual_list,
+ read_list):
+ self.assertEqual(expected, actual)
+ self.assertEqual(expected, read)
+
+ def test_get_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'))
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n', 'Welcome'])
+
+ def test_get_memorized_lines_limit_memorized_lines(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'), 2)
+ self.check(memorizing_file, 3, ['Hello\n', 'World\n'])
+
+ def test_get_memorized_lines_empty_file(self):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ ''))
+ self.check(memorizing_file, 10, [])
+
+ def test_get_memorized_lines_with_size(self):
+ for size in range(1, 10):
+ memorizing_file = memorizingfile.MemorizingFile(StringIO.StringIO(
+ 'Hello\nWorld\nWelcome'))
+ self.check_with_size(memorizing_file, size,
+ ['Hello\n', 'World\n', 'Welcome'])
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py
new file mode 100755
index 000000000..7dc23a73d
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mock.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for mock module."""
+
+
+import Queue
+import threading
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from test import mock
+
+
+class MockConnTest(unittest.TestCase):
+ """A unittest for MockConn class."""
+
+ def setUp(self):
+ self._conn = mock.MockConn('ABC\r\nDEFG\r\n\r\nHIJK')
+
+ def test_readline(self):
+ self.assertEqual('ABC\r\n', self._conn.readline())
+ self.assertEqual('DEFG\r\n', self._conn.readline())
+ self.assertEqual('\r\n', self._conn.readline())
+ self.assertEqual('HIJK', self._conn.readline())
+ self.assertEqual('', self._conn.readline())
+
+ def test_read(self):
+ self.assertEqual('ABC\r\nD', self._conn.read(6))
+ self.assertEqual('EFG\r\n\r\nHI', self._conn.read(9))
+ self.assertEqual('JK', self._conn.read(10))
+ self.assertEqual('', self._conn.read(10))
+
+ def test_read_and_readline(self):
+ self.assertEqual('ABC\r\nD', self._conn.read(6))
+ self.assertEqual('EFG\r\n', self._conn.readline())
+ self.assertEqual('\r\nHIJK', self._conn.read(9))
+ self.assertEqual('', self._conn.readline())
+
+ def test_write(self):
+ self._conn.write('Hello\r\n')
+ self._conn.write('World\r\n')
+ self.assertEqual('Hello\r\nWorld\r\n', self._conn.written_data())
+
+
+class MockBlockingConnTest(unittest.TestCase):
+ """A unittest for MockBlockingConn class."""
+
+ def test_read(self):
+ """Tests that data put to MockBlockingConn by put_bytes method can be
+ read from it.
+ """
+
+ class LineReader(threading.Thread):
+ """A test class that launches a thread, calls readline on the
+ specified conn repeatedly and puts the read data to the specified
+ queue.
+ """
+
+ def __init__(self, conn, queue):
+ threading.Thread.__init__(self)
+ self._queue = queue
+ self._conn = conn
+ self.setDaemon(True)
+ self.start()
+
+ def run(self):
+ while True:
+ data = self._conn.readline()
+ self._queue.put(data)
+
+ conn = mock.MockBlockingConn()
+ queue = Queue.Queue()
+ reader = LineReader(conn, queue)
+ self.failUnless(queue.empty())
+ conn.put_bytes('Foo bar\r\n')
+ read = queue.get()
+ self.assertEqual('Foo bar\r\n', read)
+
+
+class MockTableTest(unittest.TestCase):
+ """A unittest for MockTable class."""
+
+ def test_create_from_dict(self):
+ table = mock.MockTable({'Key': 'Value'})
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_list(self):
+ table = mock.MockTable([('Key', 'Value')])
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_create_from_tuple(self):
+ table = mock.MockTable((('Key', 'Value'),))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['key'])
+
+ def test_set_and_get(self):
+ table = mock.MockTable()
+ self.assertEqual(None, table.get('Key'))
+ table['Key'] = 'Value'
+ self.assertEqual('Value', table.get('Key'))
+ self.assertEqual('Value', table.get('key'))
+ self.assertEqual('Value', table.get('KEY'))
+ self.assertEqual('Value', table['Key'])
+ self.assertEqual('Value', table['key'])
+ self.assertEqual('Value', table['KEY'])
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py
new file mode 100755
index 000000000..5fedcf92f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_msgutil.py
@@ -0,0 +1,1356 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for msgutil module."""
+
+
+import array
+import Queue
+import random
+import struct
+import unittest
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket.extensions import DeflateFrameExtensionProcessor
+from mod_pywebsocket.extensions import PerMessageCompressExtensionProcessor
+from mod_pywebsocket.extensions import PerMessageDeflateExtensionProcessor
+from mod_pywebsocket import msgutil
+from mod_pywebsocket.stream import InvalidUTF8Exception
+from mod_pywebsocket.stream import Stream
+from mod_pywebsocket.stream import StreamHixie75
+from mod_pywebsocket.stream import StreamOptions
+from mod_pywebsocket import util
+from test import mock
+
+
+# We use one fixed nonce for testing instead of cryptographically secure PRNG.
+_MASKING_NONCE = 'ABCD'
+
+
+def _mask_hybi(frame):
+ frame_key = map(ord, _MASKING_NONCE)
+ frame_key_len = len(frame_key)
+ result = array.array('B')
+ result.fromstring(frame)
+ count = 0
+ for i in xrange(len(result)):
+ result[i] ^= frame_key[count]
+ count = (count + 1) % frame_key_len
+ return _MASKING_NONCE + result.tostring()
+
+
+def _install_extension_processor(processor, request, stream_options):
+ response = processor.get_extension_response()
+ if response is not None:
+ processor.setup_stream_options(stream_options)
+ request.ws_extension_processors.append(processor)
+
+
+def _create_request_from_rawdata(
+ read_data,
+ deflate_frame_request=None,
+ permessage_compression_request=None,
+ permessage_deflate_request=None):
+ req = mock.MockRequest(connection=mock.MockConn(''.join(read_data)))
+ req.ws_version = common.VERSION_HYBI_LATEST
+ req.ws_extension_processors = []
+
+ processor = None
+ if deflate_frame_request is not None:
+ processor = DeflateFrameExtensionProcessor(deflate_frame_request)
+ elif permessage_compression_request is not None:
+ processor = PerMessageCompressExtensionProcessor(
+ permessage_compression_request)
+ elif permessage_deflate_request is not None:
+ processor = PerMessageDeflateExtensionProcessor(
+ permessage_deflate_request)
+
+ stream_options = StreamOptions()
+ if processor is not None:
+ _install_extension_processor(processor, req, stream_options)
+ req.ws_stream = Stream(req, stream_options)
+
+ return req
+
+
+def _create_request(*frames):
+ """Creates MockRequest using data given as frames.
+
+ frames will be returned on calling request.connection.read() where request
+ is MockRequest returned by this function.
+ """
+
+ read_data = []
+ for (header, body) in frames:
+ read_data.append(header + _mask_hybi(body))
+
+ return _create_request_from_rawdata(read_data)
+
+
+def _create_blocking_request():
+ """Creates MockRequest.
+
+ Data written to a MockRequest can be read out by calling
+ request.connection.written_data().
+ """
+
+ req = mock.MockRequest(connection=mock.MockBlockingConn())
+ req.ws_version = common.VERSION_HYBI_LATEST
+ stream_options = StreamOptions()
+ req.ws_stream = Stream(req, stream_options)
+ return req
+
+
+def _create_request_hixie75(read_data=''):
+ req = mock.MockRequest(connection=mock.MockConn(read_data))
+ req.ws_stream = StreamHixie75(req)
+ return req
+
+
+def _create_blocking_request_hixie75():
+ req = mock.MockRequest(connection=mock.MockBlockingConn())
+ req.ws_stream = StreamHixie75(req)
+ return req
+
+
+class BasicMessageTest(unittest.TestCase):
+ """Basic tests for Stream."""
+
+ def test_send_message(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello')
+ self.assertEqual('\x81\x05Hello', request.connection.written_data())
+
+ payload = 'a' * 125
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7d' + payload,
+ request.connection.written_data())
+
+ def test_send_medium_message(self):
+ payload = 'a' * 126
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7e\x00\x7e' + payload,
+ request.connection.written_data())
+
+ payload = 'a' * ((1 << 16) - 1)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7e\xff\xff' + payload,
+ request.connection.written_data())
+
+ def test_send_large_message(self):
+ payload = 'a' * (1 << 16)
+ request = _create_request()
+ msgutil.send_message(request, payload)
+ self.assertEqual('\x81\x7f\x00\x00\x00\x00\x00\x01\x00\x00' + payload,
+ request.connection.written_data())
+
+ def test_send_message_unicode(self):
+ request = _create_request()
+ msgutil.send_message(request, u'\u65e5')
+ # U+65e5 is encoded as e6,97,a5 in UTF-8
+ self.assertEqual('\x81\x03\xe6\x97\xa5',
+ request.connection.written_data())
+
+ def test_send_message_fragments(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello', False)
+ msgutil.send_message(request, ' ', False)
+ msgutil.send_message(request, 'World', False)
+ msgutil.send_message(request, '!', True)
+ self.assertEqual('\x01\x05Hello\x00\x01 \x00\x05World\x80\x01!',
+ request.connection.written_data())
+
+ def test_send_fragments_immediate_zero_termination(self):
+ request = _create_request()
+ msgutil.send_message(request, 'Hello World!', False)
+ msgutil.send_message(request, '', True)
+ self.assertEqual('\x01\x0cHello World!\x80\x00',
+ request.connection.written_data())
+
+ def test_receive_message(self):
+ request = _create_request(
+ ('\x81\x85', 'Hello'), ('\x81\x86', 'World!'))
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ payload = 'a' * 125
+ request = _create_request(('\x81\xfd', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_medium_message(self):
+ payload = 'a' * 126
+ request = _create_request(('\x81\xfe\x00\x7e', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ payload = 'a' * ((1 << 16) - 1)
+ request = _create_request(('\x81\xfe\xff\xff', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_large_message(self):
+ payload = 'a' * (1 << 16)
+ request = _create_request(
+ ('\x81\xff\x00\x00\x00\x00\x00\x01\x00\x00', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_length_not_encoded_using_minimal_number_of_bytes(self):
+ # Log warning on receiving bad payload length field that doesn't use
+ # minimal number of bytes but continue processing.
+
+ payload = 'a'
+ # 1 byte can be represented without extended payload length field.
+ request = _create_request(
+ ('\x81\xff\x00\x00\x00\x00\x00\x00\x00\x01', payload))
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ def test_receive_message_unicode(self):
+ request = _create_request(('\x81\x83', '\xe6\x9c\xac'))
+ # U+672c is encoded as e6,9c,ac in UTF-8
+ self.assertEqual(u'\u672c', msgutil.receive_message(request))
+
+ def test_receive_message_erroneous_unicode(self):
+ # \x80 and \x81 are invalid as UTF-8.
+ request = _create_request(('\x81\x82', '\x80\x81'))
+ # Invalid characters should raise InvalidUTF8Exception
+ self.assertRaises(InvalidUTF8Exception,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments(self):
+ request = _create_request(
+ ('\x01\x85', 'Hello'),
+ ('\x00\x81', ' '),
+ ('\x00\x85', 'World'),
+ ('\x80\x81', '!'))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_unicode(self):
+ # UTF-8 encodes U+6f22 into e6bca2 and U+5b57 into e5ad97.
+ request = _create_request(
+ ('\x01\x82', '\xe6\xbc'),
+ ('\x00\x82', '\xa2\xe5'),
+ ('\x80\x82', '\xad\x97'))
+ self.assertEqual(u'\u6f22\u5b57', msgutil.receive_message(request))
+
+ def test_receive_fragments_immediate_zero_termination(self):
+ request = _create_request(
+ ('\x01\x8c', 'Hello World!'), ('\x80\x80', ''))
+ self.assertEqual('Hello World!', msgutil.receive_message(request))
+
+ def test_receive_fragments_duplicate_start(self):
+ request = _create_request(
+ ('\x01\x85', 'Hello'), ('\x01\x85', 'World'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments_intermediate_but_not_started(self):
+ request = _create_request(('\x00\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_fragments_end_but_not_started(self):
+ request = _create_request(('\x80\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_message_discard(self):
+ request = _create_request(
+ ('\x8f\x86', 'IGNORE'), ('\x81\x85', 'Hello'),
+ ('\x8f\x89', 'DISREGARD'), ('\x81\x86', 'World!'))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertRaises(msgutil.UnsupportedFrameException,
+ msgutil.receive_message, request)
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ def test_receive_close(self):
+ request = _create_request(
+ ('\x88\x8a', struct.pack('!H', 1000) + 'Good bye'))
+ self.assertEqual(None, msgutil.receive_message(request))
+ self.assertEqual(1000, request.ws_close_code)
+ self.assertEqual('Good bye', request.ws_close_reason)
+
+ def test_send_longest_close(self):
+ reason = 'a' * 123
+ request = _create_request(
+ ('\x88\xfd',
+ struct.pack('!H', common.STATUS_NORMAL_CLOSURE) + reason))
+ request.ws_stream.close_connection(common.STATUS_NORMAL_CLOSURE,
+ reason)
+ self.assertEqual(request.ws_close_code, common.STATUS_NORMAL_CLOSURE)
+ self.assertEqual(request.ws_close_reason, reason)
+
+ def test_send_close_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection,
+ request.ws_stream,
+ common.STATUS_NORMAL_CLOSURE,
+ 'a' * 124)
+
+ def test_send_close_inconsistent_code_and_reason(self):
+ request = _create_request()
+ # reason parameter must not be specified when code is None.
+ self.assertRaises(msgutil.BadOperationException,
+ Stream.close_connection,
+ request.ws_stream,
+ None,
+ 'a')
+
+ def test_send_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'Hello World!')
+ self.assertEqual('\x89\x0cHello World!',
+ request.connection.written_data())
+
+ def test_send_longest_ping(self):
+ request = _create_request()
+ msgutil.send_ping(request, 'a' * 125)
+ self.assertEqual('\x89\x7d' + 'a' * 125,
+ request.connection.written_data())
+
+ def test_send_ping_too_long(self):
+ request = _create_request()
+ self.assertRaises(msgutil.BadOperationException,
+ msgutil.send_ping,
+ request,
+ 'a' * 126)
+
+ def test_receive_ping(self):
+ """Tests receiving a ping control frame."""
+
+ def handler(request, message):
+ request.called = True
+
+ # Stream automatically respond to ping with pong without any action
+ # by application layer.
+ request = _create_request(
+ ('\x89\x85', 'Hello'), ('\x81\x85', 'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual('\x8a\x05Hello',
+ request.connection.written_data())
+
+ request = _create_request(
+ ('\x89\x85', 'Hello'), ('\x81\x85', 'World'))
+ request.on_ping_handler = handler
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertTrue(request.called)
+
+ def test_receive_longest_ping(self):
+ request = _create_request(
+ ('\x89\xfd', 'a' * 125), ('\x81\x85', 'World'))
+ self.assertEqual('World', msgutil.receive_message(request))
+ self.assertEqual('\x8a\x7d' + 'a' * 125,
+ request.connection.written_data())
+
+ def test_receive_ping_too_long(self):
+ request = _create_request(('\x89\xfe\x00\x7e', 'a' * 126))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_receive_pong(self):
+ """Tests receiving a pong control frame."""
+
+ def handler(request, message):
+ request.called = True
+
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ request.on_pong_handler = handler
+ msgutil.send_ping(request, 'Hello')
+ self.assertEqual('\x89\x05Hello',
+ request.connection.written_data())
+ # Valid pong is received, but receive_message won't return for it.
+ self.assertEqual('World', msgutil.receive_message(request))
+ # Check that nothing was written after receive_message call.
+ self.assertEqual('\x89\x05Hello',
+ request.connection.written_data())
+
+ self.assertTrue(request.called)
+
+ def test_receive_unsolicited_pong(self):
+ # Unsolicited pong is allowed from HyBi 07.
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ msgutil.receive_message(request)
+
+ request = _create_request(
+ ('\x8a\x85', 'Hello'), ('\x81\x85', 'World'))
+ msgutil.send_ping(request, 'Jumbo')
+ # Body mismatch.
+ msgutil.receive_message(request)
+
+ def test_ping_cannot_be_fragmented(self):
+ request = _create_request(('\x09\x85', 'Hello'))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+ def test_ping_with_too_long_payload(self):
+ request = _create_request(('\x89\xfe\x01\x00', 'a' * 256))
+ self.assertRaises(msgutil.InvalidFrameException,
+ msgutil.receive_message,
+ request)
+
+
+class DeflateFrameTest(unittest.TestCase):
+ """Tests for checking deflate-frame extension."""
+
+ def test_send_message(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_bfinal(self):
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ deflate_frame_processor = request.ws_extension_processors[0]
+ deflate_frame_processor.set_bfinal(True)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ expected = ''
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_FINISH)
+ compressed_hello = compressed_hello + chr(0)
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_FINISH)
+ compressed_world = compressed_world + chr(0)
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_comp_bit(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ deflate_frame_processor = request.ws_extension_processors[0]
+ msgutil.send_message(request, 'Hello')
+ deflate_frame_processor.disable_outgoing_compression()
+ msgutil.send_message(request, 'Hello')
+ deflate_frame_processor.enable_outgoing_compression()
+ msgutil.send_message(request, 'Hello')
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ expected += '\x81\x05Hello'
+
+ compressed_2nd_hello = compress.compress('Hello')
+ compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_2nd_hello = compressed_2nd_hello[:-4]
+ expected += '\xc1%c' % len(compressed_2nd_hello)
+ expected += compressed_2nd_hello
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_no_context_takeover_parameter(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ extension.add_parameter('no_context_takeover', None)
+ request = _create_request_from_rawdata(
+ '', deflate_frame_request=extension)
+ for i in xrange(3):
+ msgutil.send_message(request, 'Hello')
+
+ compressed_message = compress.compress('Hello')
+ compressed_message += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_message = compressed_message[:-4]
+ expected = '\xc1%c' % len(compressed_message)
+ expected += compressed_message
+
+ self.assertEqual(
+ expected + expected + expected, request.connection.written_data())
+
+ def test_bad_request_parameters(self):
+ """Tests that if there's anything wrong with deflate-frame extension
+ request, deflate-frame is rejected.
+ """
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # max_window_bits less than 8 is illegal.
+ extension.add_parameter('max_window_bits', '7')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # max_window_bits greater than 15 is illegal.
+ extension.add_parameter('max_window_bits', '16')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # Non integer max_window_bits is illegal.
+ extension.add_parameter('max_window_bits', 'foobar')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ # no_context_takeover must not have any value.
+ extension.add_parameter('no_context_takeover', 'foobar')
+ processor = DeflateFrameExtensionProcessor(extension)
+ self.assertEqual(None, processor.get_extension_response())
+
+ def test_response_parameters(self):
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ processor = DeflateFrameExtensionProcessor(extension)
+ processor.set_response_window_bits(8)
+ response = processor.get_extension_response()
+ self.assertTrue(response.has_parameter('max_window_bits'))
+ self.assertEqual('8', response.get_parameter_value('max_window_bits'))
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ processor = DeflateFrameExtensionProcessor(extension)
+ processor.set_response_no_context_takeover(True)
+ response = processor.get_extension_response()
+ self.assertTrue(response.has_parameter('no_context_takeover'))
+ self.assertTrue(
+ response.get_parameter_value('no_context_takeover') is None)
+
+ def test_receive_message(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ compressed_websocket = compress.compress('WebSocket')
+ compressed_websocket += compress.flush(zlib.Z_FINISH)
+ compressed_websocket += '\x00'
+ data += '\xc1%c' % (len(compressed_websocket) | 0x80)
+ data += _mask_hybi(compressed_websocket)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('WebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_client_using_smaller_window(self):
+ """Test that frames coming from a client which is using smaller window
+ size that the server are correctly received.
+ """
+
+ # Using the smallest window bits of 8 for generating input frames.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -8)
+
+ data = ''
+
+ # Use a frame whose content is bigger than the clients' DEFLATE window
+ # size before compression. The content mainly consists of 'a' but
+ # repetition of 'b' is put at the head and tail so that if the window
+ # size is big, the head is back-referenced but if small, not.
+ payload = 'b' * 64 + 'a' * 1024 + 'b' * 64
+ compressed_hello = compress.compress(payload)
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_comp_bit(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ data += '\x81\x85' + _mask_hybi('Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_2nd_hello = compress.compress('Hello')
+ compressed_2nd_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_2nd_hello = compressed_2nd_hello[:-4]
+ data += '\xc1%c' % (len(compressed_2nd_hello) | 0x80)
+ data += _mask_hybi(compressed_2nd_hello)
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ for i in xrange(3):
+ self.assertEqual('Hello', msgutil.receive_message(request))
+
+ def test_receive_message_various_btype(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data += '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ compressed_websocket = compress.compress('WebSocket')
+ compressed_websocket += compress.flush(zlib.Z_FINISH)
+ compressed_websocket += '\x00'
+ data += '\xc1%c' % (len(compressed_websocket) | 0x80)
+ data += _mask_hybi(compressed_websocket)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(common.DEFLATE_FRAME_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, deflate_frame_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('WebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+
+class PerMessageDeflateTest(unittest.TestCase):
+ """Tests for permessage-deflate extension."""
+
+ def test_send_message(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected = '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_empty_message(self):
+ """Test that an empty message is compressed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '')
+
+ # Payload in binary: 0b00000010 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (01 that means fixed Huffman)
+ # - 7 bits of the first code (0000000 that is the code for the
+ # end-of-block)
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 3 bits of padding
+ self.assertEqual('\xc1\x02\x02\x00',
+ request.connection.written_data())
+
+ def test_send_message_with_null_character(self):
+ """Test that a simple payload (one null) is framed correctly."""
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+
+ msgutil.send_message(request, '\x00')
+
+ # Payload in binary: 0b01100010 0b00000000 0b00000000
+ # From LSB,
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (01 that means fixed Huffman)
+ # - 8 bits of the first code (00110000 that is the code for the literal
+ # alphabet 0x00)
+ # - 7 bits of the second code (0000000 that is the code for the
+ # end-of-block)
+ # - 1 bit of BFINAL (0)
+ # - 2 bits of BTYPE (no compression)
+ # - 2 bits of padding
+ self.assertEqual('\xc1\x03\x62\x00\x00',
+ request.connection.written_data())
+
+ def test_send_two_messages(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello')
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ expected = ''
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ expected += '\xc1%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\xc1%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Goodbye', end=False)
+ msgutil.send_message(request, 'World')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_goodbye = compress.compress('Goodbye')
+ compressed_goodbye += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected += '\x00%c' % len(compressed_goodbye)
+ expected += compressed_goodbye
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ expected += '\x80%c' % len(compressed_world)
+ expected += compressed_world
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_first_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, '', end=False)
+ msgutil.send_message(request, 'Hello')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress('Hello')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += '\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ print '%r' % expected
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_fragmented_empty_last_frame(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, '')
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+ compressed_empty = compress.compress('')
+ compressed_empty += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_empty = compressed_empty[:-4]
+ expected += '\x80%c' % len(compressed_empty)
+ expected += compressed_empty
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_send_message_using_small_window(self):
+ common_part = 'abcdefghijklmnopqrstuvwxyz'
+ test_message = common_part + '-' * 30000 + common_part
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_max_window_bits', '8')
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ msgutil.send_message(request, test_message)
+
+ expected_websocket_header_size = 2
+ expected_websocket_payload_size = 91
+
+ actual_frame = request.connection.written_data()
+ self.assertEqual(expected_websocket_header_size +
+ expected_websocket_payload_size,
+ len(actual_frame))
+ actual_header = actual_frame[0:expected_websocket_header_size]
+ actual_payload = actual_frame[expected_websocket_header_size:]
+
+ self.assertEqual(
+ '\xc1%c' % expected_websocket_payload_size, actual_header)
+ decompress = zlib.decompressobj(-8)
+ decompressed_message = decompress.decompress(
+ actual_payload + '\x00\x00\xff\xff')
+ decompressed_message += decompress.flush()
+ self.assertEqual(test_message, decompressed_message)
+ self.assertEqual(0, len(decompress.unused_data))
+ self.assertEqual(0, len(decompress.unconsumed_tail))
+
+ def test_send_message_no_context_takeover_parameter(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ extension.add_parameter('server_no_context_takeover', None)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ for i in xrange(3):
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'Hello', end=True)
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ first_hello = compress.compress('Hello')
+ first_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ expected = '\x41%c' % len(first_hello)
+ expected += first_hello
+ second_hello = compress.compress('Hello')
+ second_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ second_hello = second_hello[:-4]
+ expected += '\x80%c' % len(second_hello)
+ expected += second_hello
+
+ self.assertEqual(
+ expected + expected + expected,
+ request.connection.written_data())
+
+ def test_send_message_fragmented_bfinal(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ '', permessage_deflate_request=extension)
+ self.assertEquals(1, len(request.ws_extension_processors))
+ request.ws_extension_processors[0].set_bfinal(True)
+ msgutil.send_message(request, 'Hello', end=False)
+ msgutil.send_message(request, 'World', end=True)
+
+ expected = ''
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_FINISH)
+ compressed_hello = compressed_hello + chr(0)
+ expected += '\x41%c' % len(compressed_hello)
+ expected += compressed_hello
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_FINISH)
+ compressed_world = compressed_world + chr(0)
+ expected += '\x80%c' % len(compressed_world)
+ expected += compressed_world
+
+ self.assertEqual(expected, request.connection.written_data())
+
+ def test_receive_message_deflate(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ data = '\xc1%c' % (len(compressed_hello) | 0x80)
+ data += _mask_hybi(compressed_hello)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('Hello', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_random_section(self):
+ """Test that a compressed message fragmented into lots of chunks is
+ correctly received.
+ """
+
+ random.seed(a=0)
+ payload = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(1000)])
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_payload = compress.compress(payload)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ data = ''
+ frame_count = 0
+
+ chunk_sizes = []
+
+ while bytes_chunked < len(compressed_payload):
+ # Make sure that
+ # - the length of chunks are equal or less than 125 so that we can
+ # use 1 octet length header format for all frames.
+ # - at least 10 chunks are created.
+ chunk_size = random.randint(
+ 1, min(125,
+ len(compressed_payload) / 10,
+ len(compressed_payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = compressed_payload[
+ bytes_chunked:bytes_chunked + chunk_size]
+ bytes_chunked += chunk_size
+
+ first_octet = 0x00
+ if len(data) == 0:
+ first_octet = first_octet | 0x42
+ if bytes_chunked == len(compressed_payload):
+ first_octet = first_octet | 0x80
+
+ data += '%c%c' % (first_octet, chunk_size | 0x80)
+ data += _mask_hybi(chunk)
+
+ frame_count += 1
+
+ print "Chunk sizes: %r" % chunk_sizes
+ self.assertTrue(len(chunk_sizes) > 10)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_two_messages(self):
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ data = ''
+
+ compressed_hello = compress.compress('HelloWebSocket')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ split_position = len(compressed_hello) / 2
+ data += '\x41%c' % (split_position | 0x80)
+ data += _mask_hybi(compressed_hello[:split_position])
+
+ data += '\x80%c' % ((len(compressed_hello) - split_position) | 0x80)
+ data += _mask_hybi(compressed_hello[split_position:])
+
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+
+ compressed_world = compress.compress('World')
+ compressed_world += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_world = compressed_world[:-4]
+ data += '\xc1%c' % (len(compressed_world) | 0x80)
+ data += _mask_hybi(compressed_world)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual('HelloWebSocket', msgutil.receive_message(request))
+ self.assertEqual('World', msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+ def test_receive_message_mixed_btype(self):
+ """Test that a message compressed using lots of DEFLATE blocks with
+ various flush mode is correctly received.
+ """
+
+ random.seed(a=0)
+ payload = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(1000)])
+
+ compress = None
+
+ # Fragment the compressed payload into lots of frames.
+ bytes_chunked = 0
+ compressed_payload = ''
+
+ chunk_sizes = []
+ methods = []
+ sync_used = False
+ finish_used = False
+
+ while bytes_chunked < len(payload):
+ # Make sure at least 10 chunks are created.
+ chunk_size = random.randint(
+ 1, min(100, len(payload) - bytes_chunked))
+ chunk_sizes.append(chunk_size)
+ chunk = payload[bytes_chunked:bytes_chunked + chunk_size]
+
+ bytes_chunked += chunk_size
+
+ if compress is None:
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION,
+ zlib.DEFLATED,
+ -zlib.MAX_WBITS)
+
+ if bytes_chunked == len(payload):
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_payload = compressed_payload[:-4]
+ else:
+ method = random.randint(0, 1)
+ methods.append(method)
+ if method == 0:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_SYNC_FLUSH)
+ sync_used = True
+ else:
+ compressed_payload += compress.compress(chunk)
+ compressed_payload += compress.flush(zlib.Z_FINISH)
+ compress = None
+ finish_used = True
+
+ print "Chunk sizes: %r" % chunk_sizes
+ self.assertTrue(len(chunk_sizes) > 10)
+ print "Methods: %r" % methods
+ self.assertTrue(sync_used)
+ self.assertTrue(finish_used)
+
+ self.assertTrue(125 < len(compressed_payload))
+ self.assertTrue(len(compressed_payload) < 65536)
+ data = '\xc2\xfe' + struct.pack('!H', len(compressed_payload))
+ data += _mask_hybi(compressed_payload)
+
+ # Close frame
+ data += '\x88\x8a' + _mask_hybi(struct.pack('!H', 1000) + 'Good bye')
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_DEFLATE_EXTENSION)
+ request = _create_request_from_rawdata(
+ data, permessage_deflate_request=extension)
+ self.assertEqual(payload, msgutil.receive_message(request))
+
+ self.assertEqual(None, msgutil.receive_message(request))
+
+
+class PerMessageCompressTest(unittest.TestCase):
+ """Tests for checking permessage-compression extension."""
+
+ def test_deflate_response_parameters(self):
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ extension.add_parameter('method', 'deflate')
+ processor = PerMessageCompressExtensionProcessor(extension)
+ response = processor.get_extension_response()
+ self.assertEqual('deflate',
+ response.get_parameter_value('method'))
+
+ extension = common.ExtensionParameter(
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ extension.add_parameter('method', 'deflate')
+ processor = PerMessageCompressExtensionProcessor(extension)
+
+ def _compression_processor_hook(compression_processor):
+ compression_processor.set_client_max_window_bits(8)
+ compression_processor.set_client_no_context_takeover(True)
+ processor.set_compression_processor_hook(
+ _compression_processor_hook)
+ response = processor.get_extension_response()
+ self.assertEqual(
+ 'deflate; client_max_window_bits=8; client_no_context_takeover',
+ response.get_parameter_value('method'))
+
+
+class MessageTestHixie75(unittest.TestCase):
+ """Tests for draft-hixie-thewebsocketprotocol-76 stream class."""
+
+ def test_send_message(self):
+ request = _create_request_hixie75()
+ msgutil.send_message(request, 'Hello')
+ self.assertEqual('\x00Hello\xff', request.connection.written_data())
+
+ def test_send_message_unicode(self):
+ request = _create_request_hixie75()
+ msgutil.send_message(request, u'\u65e5')
+ # U+65e5 is encoded as e6,97,a5 in UTF-8
+ self.assertEqual('\x00\xe6\x97\xa5\xff',
+ request.connection.written_data())
+
+ def test_receive_message(self):
+ request = _create_request_hixie75('\x00Hello\xff\x00World!\xff')
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+ def test_receive_message_unicode(self):
+ request = _create_request_hixie75('\x00\xe6\x9c\xac\xff')
+ # U+672c is encoded as e6,9c,ac in UTF-8
+ self.assertEqual(u'\u672c', msgutil.receive_message(request))
+
+ def test_receive_message_erroneous_unicode(self):
+ # \x80 and \x81 are invalid as UTF-8.
+ request = _create_request_hixie75('\x00\x80\x81\xff')
+ # Invalid characters should be replaced with
+ # U+fffd REPLACEMENT CHARACTER
+ self.assertEqual(u'\ufffd\ufffd', msgutil.receive_message(request))
+
+ def test_receive_message_discard(self):
+ request = _create_request_hixie75('\x80\x06IGNORE\x00Hello\xff'
+ '\x01DISREGARD\xff\x00World!\xff')
+ self.assertEqual('Hello', msgutil.receive_message(request))
+ self.assertEqual('World!', msgutil.receive_message(request))
+
+
+class MessageReceiverTest(unittest.TestCase):
+ """Tests the Stream class using MessageReceiver."""
+
+ def test_queue(self):
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request)
+
+ self.assertEqual(None, receiver.receive_nowait())
+
+ request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!'))
+ self.assertEqual('Hello!', receiver.receive())
+
+ def test_onmessage(self):
+ onmessage_queue = Queue.Queue()
+
+ def onmessage_handler(message):
+ onmessage_queue.put(message)
+
+ request = _create_blocking_request()
+ receiver = msgutil.MessageReceiver(request, onmessage_handler)
+
+ request.connection.put_bytes('\x81\x86' + _mask_hybi('Hello!'))
+ self.assertEqual('Hello!', onmessage_queue.get())
+
+
+class MessageReceiverHixie75Test(unittest.TestCase):
+ """Tests the StreamHixie75 class using MessageReceiver."""
+
+ def test_queue(self):
+ request = _create_blocking_request_hixie75()
+ receiver = msgutil.MessageReceiver(request)
+
+ self.assertEqual(None, receiver.receive_nowait())
+
+ request.connection.put_bytes('\x00Hello!\xff')
+ self.assertEqual('Hello!', receiver.receive())
+
+ def test_onmessage(self):
+ onmessage_queue = Queue.Queue()
+
+ def onmessage_handler(message):
+ onmessage_queue.put(message)
+
+ request = _create_blocking_request_hixie75()
+ receiver = msgutil.MessageReceiver(request, onmessage_handler)
+
+ request.connection.put_bytes('\x00Hello!\xff')
+ self.assertEqual('Hello!', onmessage_queue.get())
+
+
+class MessageSenderTest(unittest.TestCase):
+ """Tests the Stream class using MessageSender."""
+
+ def test_send(self):
+ request = _create_blocking_request()
+ sender = msgutil.MessageSender(request)
+
+ sender.send('World')
+ self.assertEqual('\x81\x05World', request.connection.written_data())
+
+ def test_send_nowait(self):
+ # Use a queue to check the bytes written by MessageSender.
+ # request.connection.written_data() cannot be used here because
+ # MessageSender runs in a separate thread.
+ send_queue = Queue.Queue()
+
+ def write(bytes):
+ send_queue.put(bytes)
+
+ request = _create_blocking_request()
+ request.connection.write = write
+
+ sender = msgutil.MessageSender(request)
+
+ sender.send_nowait('Hello')
+ sender.send_nowait('World')
+ self.assertEqual('\x81\x05Hello', send_queue.get())
+ self.assertEqual('\x81\x05World', send_queue.get())
+
+
+class MessageSenderHixie75Test(unittest.TestCase):
+ """Tests the StreamHixie75 class using MessageSender."""
+
+ def test_send(self):
+ request = _create_blocking_request_hixie75()
+ sender = msgutil.MessageSender(request)
+
+ sender.send('World')
+ self.assertEqual('\x00World\xff', request.connection.written_data())
+
+ def test_send_nowait(self):
+ # Use a queue to check the bytes written by MessageSender.
+ # request.connection.written_data() cannot be used here because
+ # MessageSender runs in a separate thread.
+ send_queue = Queue.Queue()
+
+ def write(bytes):
+ send_queue.put(bytes)
+
+ request = _create_blocking_request_hixie75()
+ request.connection.write = write
+
+ sender = msgutil.MessageSender(request)
+
+ sender.send_nowait('Hello')
+ sender.send_nowait('World')
+ self.assertEqual('\x00Hello\xff', send_queue.get())
+ self.assertEqual('\x00World\xff', send_queue.get())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
new file mode 100644
index 000000000..d4598944e
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_mux.py
@@ -0,0 +1,2089 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for mux module."""
+
+import Queue
+import copy
+import logging
+import optparse
+import struct
+import sys
+import unittest
+import time
+import zlib
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import mux
+from mod_pywebsocket._stream_base import ConnectionTerminatedException
+from mod_pywebsocket._stream_base import UnsupportedFrameException
+from mod_pywebsocket._stream_hybi import Frame
+from mod_pywebsocket._stream_hybi import Stream
+from mod_pywebsocket._stream_hybi import StreamOptions
+from mod_pywebsocket._stream_hybi import create_binary_frame
+from mod_pywebsocket._stream_hybi import create_close_frame
+from mod_pywebsocket._stream_hybi import create_closing_handshake_body
+from mod_pywebsocket._stream_hybi import parse_frame
+from mod_pywebsocket.extensions import MuxExtensionProcessor
+
+
+import mock
+
+
+_TEST_HEADERS = {'Host': 'server.example.com',
+ 'Upgrade': 'websocket',
+ 'Connection': 'Upgrade',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': '13',
+ 'Origin': 'http://example.com'}
+
+
+class _OutgoingChannelData(object):
+ def __init__(self):
+ self.messages = []
+ self.control_messages = []
+
+ self.builder = mux._InnerMessageBuilder()
+
+class _MockMuxConnection(mock.MockBlockingConn):
+ """Mock class of mod_python connection for mux."""
+
+ def __init__(self):
+ mock.MockBlockingConn.__init__(self)
+ self._control_blocks = []
+ self._channel_data = {}
+
+ self._current_opcode = None
+ self._pending_fragments = []
+
+ self.server_close_code = None
+
+ def write(self, data):
+ """Override MockBlockingConn.write."""
+
+ self._current_data = data
+ self._position = 0
+
+ def _receive_bytes(length):
+ if self._position + length > len(self._current_data):
+ raise ConnectionTerminatedException(
+ 'Failed to receive %d bytes from encapsulated '
+ 'frame' % length)
+ data = self._current_data[self._position:self._position+length]
+ self._position += length
+ return data
+
+ # Parse physical frames and assemble a message if the message is
+ # fragmented.
+ opcode, payload, fin, rsv1, rsv2, rsv3 = (
+ parse_frame(_receive_bytes, unmask_receive=False))
+
+ self._pending_fragments.append(payload)
+
+ if self._current_opcode is None:
+ if opcode == common.OPCODE_CONTINUATION:
+ raise Exception('Sending invalid continuation opcode')
+ self._current_opcode = opcode
+ else:
+ if opcode != common.OPCODE_CONTINUATION:
+ raise Exception('Sending invalid opcode %d' % opcode)
+ if not fin:
+ return
+
+ inner_frame_data = ''.join(self._pending_fragments)
+ self._pending_fragments = []
+ self._current_opcode = None
+
+ # Handle a control message on the physical channel.
+ # TODO(bashi): Support other opcodes if needed.
+ if opcode == common.OPCODE_CLOSE:
+ if len(payload) >= 2:
+ self.server_close_code = struct.unpack('!H', payload[:2])[0]
+ close_body = create_closing_handshake_body(
+ common.STATUS_NORMAL_CLOSURE, '')
+ close_frame = create_close_frame(close_body, mask=True)
+ self.put_bytes(close_frame)
+ return
+
+ # Parse the payload of the message on physical channel.
+ parser = mux._MuxFramePayloadParser(inner_frame_data)
+ channel_id = parser.read_channel_id()
+ if channel_id == mux._CONTROL_CHANNEL_ID:
+ self._control_blocks.extend(list(parser.read_control_blocks()))
+ return
+
+ if not channel_id in self._channel_data:
+ self._channel_data[channel_id] = _OutgoingChannelData()
+ channel_data = self._channel_data[channel_id]
+
+ # Parse logical frames and assemble an inner (logical) message.
+ (inner_fin, inner_rsv1, inner_rsv2, inner_rsv3, inner_opcode,
+ inner_payload) = parser.read_inner_frame()
+ inner_frame = Frame(inner_fin, inner_rsv1, inner_rsv2, inner_rsv3,
+ inner_opcode, inner_payload)
+ message = channel_data.builder.build(inner_frame)
+ if message is None:
+ return
+
+ if (message.opcode == common.OPCODE_TEXT or
+ message.opcode == common.OPCODE_BINARY):
+ channel_data.messages.append(message.payload)
+
+ self.on_data_message(message.payload)
+ else:
+ channel_data.control_messages.append(
+ {'opcode': message.opcode,
+ 'message': message.payload})
+
+ def on_data_message(self, message):
+ pass
+
+ def get_written_control_blocks(self):
+ return self._control_blocks
+
+ def get_written_messages(self, channel_id):
+ return self._channel_data[channel_id].messages
+
+ def get_written_control_messages(self, channel_id):
+ return self._channel_data[channel_id].control_messages
+
+
+class _FailOnWriteConnection(_MockMuxConnection):
+ """Specicialized version of _MockMuxConnection. Its write() method raises
+ an exception for testing when a data message is written.
+ """
+
+ def on_data_message(self, message):
+ """Override to raise an exception."""
+
+ raise Exception('Intentional failure')
+
+
+class _ChannelEvent(object):
+ """A structure that records channel events."""
+
+ def __init__(self):
+ self.request = None
+ self.messages = []
+ self.exception = None
+ self.client_initiated_closing = False
+
+
+class _MuxMockDispatcher(object):
+ """Mock class of dispatch.Dispatcher for mux."""
+
+ def __init__(self):
+ self.channel_events = {}
+
+ def do_extra_handshake(self, request):
+ if request.ws_requested_protocols is not None:
+ request.ws_protocol = request.ws_requested_protocols[0]
+
+ def _do_echo(self, request, channel_events):
+ while True:
+ message = request.ws_stream.receive_message()
+ if message == None:
+ channel_events.client_initiated_closing = True
+ return
+ if message == 'Goodbye':
+ return
+ channel_events.messages.append(message)
+ # echo back
+ request.ws_stream.send_message(message)
+
+ def _do_ping(self, request, channel_events):
+ request.ws_stream.send_ping('Ping!')
+
+ def _do_ping_while_hello_world(self, request, channel_events):
+ request.ws_stream.send_message('Hello ', end=False)
+ request.ws_stream.send_ping('Ping!')
+ request.ws_stream.send_message('World!', end=True)
+
+ def _do_two_ping_while_hello_world(self, request, channel_events):
+ request.ws_stream.send_message('Hello ', end=False)
+ request.ws_stream.send_ping('Ping!')
+ request.ws_stream.send_ping('Pong!')
+ request.ws_stream.send_message('World!', end=True)
+
+ def transfer_data(self, request):
+ self.channel_events[request.channel_id] = _ChannelEvent()
+ self.channel_events[request.channel_id].request = request
+
+ try:
+ # Note: more handler will be added.
+ if request.uri.endswith('echo'):
+ self._do_echo(request,
+ self.channel_events[request.channel_id])
+ elif request.uri.endswith('ping'):
+ self._do_ping(request,
+ self.channel_events[request.channel_id])
+ elif request.uri.endswith('two_ping_while_hello_world'):
+ self._do_two_ping_while_hello_world(
+ request, self.channel_events[request.channel_id])
+ elif request.uri.endswith('ping_while_hello_world'):
+ self._do_ping_while_hello_world(
+ request, self.channel_events[request.channel_id])
+ else:
+ raise ValueError('Cannot handle path %r' % request.path)
+ if not request.server_terminated:
+ request.ws_stream.close_connection()
+ except ConnectionTerminatedException, e:
+ self.channel_events[request.channel_id].exception = e
+ except Exception, e:
+ self.channel_events[request.channel_id].exception = e
+ raise
+
+
+def _create_mock_request(connection=None, logical_channel_extensions=None):
+ if connection is None:
+ connection = _MockMuxConnection()
+
+ request = mock.MockRequest(uri='/echo',
+ headers_in=_TEST_HEADERS,
+ connection=connection)
+ request.ws_stream = Stream(request, options=StreamOptions())
+ request.mux_processor = MuxExtensionProcessor(
+ common.ExtensionParameter(common.MUX_EXTENSION))
+ if logical_channel_extensions is not None:
+ request.mux_processor.set_extensions(logical_channel_extensions)
+ request.mux_processor.set_quota(8 * 1024)
+ return request
+
+
+def _create_add_channel_request_frame(channel_id, encoding, encoded_handshake):
+ # Allow invalid encoding for testing.
+ first_byte = ((mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5) | encoding)
+ payload = (chr(first_byte) +
+ mux._encode_channel_id(channel_id) +
+ mux._encode_number(len(encoded_handshake)) +
+ encoded_handshake)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_drop_channel_frame(channel_id, code=None, message=''):
+ payload = mux._create_drop_channel(channel_id, code, message)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_flow_control_frame(channel_id, replenished_quota):
+ payload = mux._create_flow_control(channel_id, replenished_quota)
+ return create_binary_frame(
+ (mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + payload), mask=True)
+
+
+def _create_logical_frame(channel_id, message, opcode=common.OPCODE_BINARY,
+ fin=True, rsv1=False, rsv2=False, rsv3=False,
+ mask=True):
+ bits = chr((fin << 7) | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) | opcode)
+ payload = mux._encode_channel_id(channel_id) + bits + message
+ return create_binary_frame(payload, mask=True)
+
+
+def _create_request_header(path='/echo', extensions=None):
+ headers = (
+ 'GET %s HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n') % path
+ if extensions:
+ headers += '%s: %s' % (
+ common.SEC_WEBSOCKET_EXTENSIONS_HEADER, extensions)
+ return headers
+
+
+class MuxTest(unittest.TestCase):
+ """A unittest for mux module."""
+
+ def test_channel_id_decode(self):
+ data = '\x00\x01\xbf\xff\xdf\xff\xff\xff\xff\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(0, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 14 - 1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 21 - 1, channel_id)
+ channel_id = parser.read_channel_id()
+ self.assertEqual(2 ** 29 - 1, channel_id)
+ self.assertEqual(len(data), parser._read_position)
+
+ def test_channel_id_encode(self):
+ encoded = mux._encode_channel_id(0)
+ self.assertEqual('\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 14 - 1)
+ self.assertEqual('\xbf\xff', encoded)
+ encoded = mux._encode_channel_id(2 ** 14)
+ self.assertEqual('\xc0@\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 21 - 1)
+ self.assertEqual('\xdf\xff\xff', encoded)
+ encoded = mux._encode_channel_id(2 ** 21)
+ self.assertEqual('\xe0 \x00\x00', encoded)
+ encoded = mux._encode_channel_id(2 ** 29 - 1)
+ self.assertEqual('\xff\xff\xff\xff', encoded)
+ # channel_id is too large
+ self.assertRaises(ValueError,
+ mux._encode_channel_id,
+ 2 ** 29)
+
+ def test_read_multiple_control_blocks(self):
+ # Use AddChannelRequest because it can contain arbitrary length of data
+ data = ('\x00\x01\x01a'
+ '\x00\x02\x7d%s'
+ '\x00\x03\x7e\xff\xff%s'
+ '\x00\x04\x7f\x00\x00\x00\x00\x00\x01\x00\x00%s') % (
+ 'a' * 0x7d, 'b' * 0xffff, 'c' * 0x10000)
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(4, len(blocks))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode)
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(1, len(blocks[0].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[1].opcode)
+ self.assertEqual(2, blocks[1].channel_id)
+ self.assertEqual(0x7d, len(blocks[1].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[2].opcode)
+ self.assertEqual(3, blocks[2].channel_id)
+ self.assertEqual(0xffff, len(blocks[2].encoded_handshake))
+
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[3].opcode)
+ self.assertEqual(4, blocks[3].channel_id)
+ self.assertEqual(0x10000, len(blocks[3].encoded_handshake))
+
+ self.assertEqual(len(data), parser._read_position)
+
+ def test_read_add_channel_request(self):
+ data = '\x00\x01\x01a'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(mux._MUX_OPCODE_ADD_CHANNEL_REQUEST, blocks[0].opcode)
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(1, len(blocks[0].encoded_handshake))
+
+ def test_read_drop_channel(self):
+ data = '\x60\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode)
+ self.assertEqual(None, blocks[0].drop_code)
+ self.assertEqual(0, len(blocks[0].drop_message))
+
+ data = '\x60\x02\x09\x03\xe8Success'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(2, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_DROP_CHANNEL, blocks[0].opcode)
+ self.assertEqual(1000, blocks[0].drop_code)
+ self.assertEqual('Success', blocks[0].drop_message)
+
+ # Reason is too short.
+ data = '\x60\x01\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(mux.PhysicalConnectionError,
+ lambda: list(parser.read_control_blocks()))
+
+ def test_read_flow_control(self):
+ data = '\x40\x01\x02'
+ parser = mux._MuxFramePayloadParser(data)
+ blocks = list(parser.read_control_blocks())
+ self.assertEqual(1, len(blocks))
+ self.assertEqual(1, blocks[0].channel_id)
+ self.assertEqual(mux._MUX_OPCODE_FLOW_CONTROL, blocks[0].opcode)
+ self.assertEqual(2, blocks[0].send_quota)
+
+ def test_read_new_channel_slot(self):
+ data = '\x80\x01\x02\x02\x03'
+ parser = mux._MuxFramePayloadParser(data)
+ # TODO(bashi): Implement
+ self.assertRaises(mux.PhysicalConnectionError,
+ lambda: list(parser.read_control_blocks()))
+
+ def test_read_invalid_number_field_in_control_block(self):
+ # No number field.
+ data = ''
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The last two bytes are missing.
+ data = '\x7e'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Missing the last one byte.
+ data = '\x7f\x00\x00\x00\x00\x00\x01\x00'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The length of number field is too large.
+ data = '\x7f\xff\xff\xff\xff\xff\xff\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # The msb of the first byte is set.
+ data = '\x80'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Using 3 bytes encoding for 125.
+ data = '\x7e\x00\x7d'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ # Using 9 bytes encoding for 0xffff
+ data = '\x7f\x00\x00\x00\x00\x00\x00\xff\xff'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(ValueError, parser._read_number)
+
+ def test_read_invalid_size_and_contents(self):
+ # Only contain number field.
+ data = '\x01'
+ parser = mux._MuxFramePayloadParser(data)
+ self.assertRaises(mux.PhysicalConnectionError,
+ parser._read_size_and_contents)
+
+ def test_create_add_channel_response(self):
+ data = mux._create_add_channel_response(channel_id=1,
+ encoded_handshake='FooBar',
+ encoding=0,
+ rejected=False)
+ self.assertEqual('\x20\x01\x06FooBar', data)
+
+ data = mux._create_add_channel_response(channel_id=2,
+ encoded_handshake='Hello',
+ encoding=1,
+ rejected=True)
+ self.assertEqual('\x31\x02\x05Hello', data)
+
+ def test_create_drop_channel(self):
+ data = mux._create_drop_channel(channel_id=1)
+ self.assertEqual('\x60\x01\x00', data)
+
+ data = mux._create_drop_channel(channel_id=1,
+ code=2000,
+ message='error')
+ self.assertEqual('\x60\x01\x07\x07\xd0error', data)
+
+ # reason must be empty if code is None
+ self.assertRaises(ValueError,
+ mux._create_drop_channel,
+ 1, None, 'FooBar')
+
+ def test_parse_request_text(self):
+ request_text = _create_request_header()
+ command, path, version, headers = mux._parse_request_text(request_text)
+ self.assertEqual('GET', command)
+ self.assertEqual('/echo', path)
+ self.assertEqual('HTTP/1.1', version)
+ self.assertEqual(3, len(headers))
+ self.assertEqual('server.example.com', headers['Host'])
+ self.assertEqual('http://example.com', headers['Origin'])
+
+
+class MuxHandlerTest(unittest.TestCase):
+
+ def test_add_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ control_blocks = request.connection.get_written_control_blocks()
+ # There should be 8 control blocks:
+ # - 1 NewChannelSlot
+ # - 2 AddChannelResponses for channel id 2 and 3
+ # - 6 FlowControls for channel id 1 (initialize), 'Hello', 'World',
+ # and 3 'Goodbye's
+ self.assertEqual(9, len(control_blocks))
+
+ def test_physical_connection_write_failure(self):
+ # Use _FailOnWriteConnection.
+ request = _create_mock_request(connection=_FailOnWriteConnection())
+
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Let the worker echo back 'Hello'. It causes _FailOnWriteConnection
+ # raising an exception.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Hello'))
+
+ # Let the worker exit. This will be unnecessary when
+ # _LogicalConnection.write() is changed to throw an exception if
+ # woke up by on_writer_done.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # All threads should be done.
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ def test_send_blocked(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # On receiving this 'Hello', the server tries to echo back 'Hello',
+ # but it will be blocked since there's no send quota available for the
+ # channel 2.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+
+ # Wait until the worker is blocked due to send quota shortage.
+ time.sleep(1)
+
+ # Close the channel 2. The worker should be notified of the end of
+ # writer thread and stop waiting for send quota to be replenished.
+ drop_channel = _create_drop_channel_frame(channel_id=2)
+
+ request.connection.put_bytes(drop_channel)
+
+ # Make sure the channel 1 is also closed.
+ drop_channel = _create_drop_channel_frame(channel_id=1)
+ request.connection.put_bytes(drop_channel)
+
+ # All threads should be done.
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ def test_add_channel_delta_encoding(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+
+ def test_add_channel_delta_encoding_override(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Override Sec-WebSocket-Protocol.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ self.assertEqual('x-foo',
+ dispatcher.channel_events[2].request.ws_protocol)
+
+ def test_add_channel_delta_after_identity(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+ # Sec-WebSocket-Protocol is different from client's opening handshake
+ # of the physical connection.
+ # TODO(bashi): Remove Upgrade, Connection, Sec-WebSocket-Key and
+ # Sec-WebSocket-Version.
+ encoded_handshake = (
+ 'GET /echo HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ # Handshake base should be updated.
+ self.assertEqual(
+ 'x-foo',
+ mux_handler._handshake_base._headers['Sec-WebSocket-Protocol'])
+
+ def test_add_channel_delta_remove_header(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+ # Override handshake delta base.
+ encoded_handshake = (
+ 'GET /echo HTTP/1.1\r\n'
+ 'Host: server.example.com\r\n'
+ 'Sec-WebSocket-Protocol: x-foo\r\n'
+ 'Connection: Upgrade\r\n'
+ 'Origin: http://example.com\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ # Remove Sec-WebSocket-Protocol header.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Protocol:'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='World'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertEqual(['World'], dispatcher.channel_events[3].messages)
+ # Channel 2
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('Hello', messages[0])
+ # Channel 3
+ messages = request.connection.get_written_messages(3)
+ self.assertEqual(1, len(messages))
+ self.assertEqual('World', messages[0])
+ self.assertEqual(
+ 'x-foo',
+ dispatcher.channel_events[2].request.ws_protocol)
+ self.assertEqual(
+ None,
+ dispatcher.channel_events[3].request.ws_protocol)
+
+ def test_add_channel_delta_encoding_permessage_compress(self):
+ # Enable permessage compress extension on the implicitly opened channel.
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ delta = 'GET /echo HTTP/1.1\r\n\r\n'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed 'Hello' on logical channel 1 and 2.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message=compressed_hello,
+ rsv1=True))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello'], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ # Written 'Hello's should be compressed.
+ messages = request.connection.get_written_messages(1)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+
+ def test_add_channel_delta_encoding_remove_extensions(self):
+ # Enable permessage compress extension on the implicitly opened channel.
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Remove permessage compress extension.
+ delta = ('GET /echo HTTP/1.1\r\n'
+ 'Sec-WebSocket-Extensions:\r\n'
+ '\r\n')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=1, encoded_handshake=delta)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed message on logical channel 2. The message should
+ # be rejected (since rsv1 is set).
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('Hello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_NORMAL_CLOSURE, drop_channel.drop_code)
+ self.assertEqual(2, drop_channel.channel_id)
+ # UnsupportedFrameException should be raised on logical channel 2.
+ self.assertTrue(isinstance(dispatcher.channel_events[2].exception,
+ UnsupportedFrameException))
+
+ def test_add_channel_invalid_encoding(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=3,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_UNKNOWN_REQUEST_ENCODING,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_add_channel_incomplete_handshake(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ incomplete_encoded_handshake = 'GET /echo HTTP/1.1'
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=incomplete_encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertTrue(1 in dispatcher.channel_events)
+ self.assertTrue(not 2 in dispatcher.channel_events)
+
+ def test_add_channel_duplicate_channel_id(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_CHANNEL_ALREADY_EXISTS,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_receive_drop_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ drop_channel = _create_drop_channel_frame(channel_id=2)
+ request.connection.put_bytes(drop_channel)
+
+ # Terminate implicitly opened channel.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ exception = dispatcher.channel_events[2].exception
+ self.assertTrue(exception.__class__ == ConnectionTerminatedException)
+
+ def test_receive_ping_frame(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=13)
+ request.connection.put_bytes(flow_control)
+
+ ping_frame = _create_logical_frame(channel_id=2,
+ message='Hello World!',
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping_frame)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, messages[0]['opcode'])
+ self.assertEqual('Hello World!', messages[0]['message'])
+
+ def test_receive_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=13)
+ request.connection.put_bytes(flow_control)
+
+ # Send a ping with message 'Hello world!' in two fragmented frames.
+ ping_frame1 = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping_frame1)
+ ping_frame2 = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping_frame2)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, messages[0]['opcode'])
+ self.assertEqual('Hello World!', messages[0]['message'])
+
+ def test_receive_fragmented_ping_while_receiving_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=19)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented frame of message 'Hello '.
+ hello = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False)
+ request.connection.put_bytes(hello)
+
+ # Before sending the last fragmented frame of the message, send a
+ # fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+
+ # Send the last fragmented frame of the message.
+ world = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(world)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+
+ def test_receive_two_ping_while_receiving_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=25)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented frame of message 'Hello '.
+ hello = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False)
+ request.connection.put_bytes(hello)
+
+ # Before sending the last fragmented frame of the message, send a
+ # fragmented ping and a non-fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+ ping3 = _create_logical_frame(channel_id=2,
+ message='Pong!',
+ fin=True,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping3)
+
+ # Send the last fragmented frame of the message.
+ world = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(world)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PONG, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+ self.assertEqual(common.OPCODE_PONG, control_messages[1]['opcode'])
+ self.assertEqual('Pong!', control_messages[1]['message'])
+
+ def test_receive_message_while_receiving_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=19)
+ request.connection.put_bytes(flow_control)
+
+ # Send a fragmented ping.
+ ping1 = _create_logical_frame(channel_id=2,
+ message='Pi',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(ping1)
+
+ # Before sending the last fragmented ping, send a message.
+ # The logical channel (2) should be dropped.
+ message = _create_logical_frame(channel_id=2,
+ message='Hello world!',
+ fin=True)
+ request.connection.put_bytes(message)
+
+ # Send the last fragmented frame of the message.
+ ping2 = _create_logical_frame(channel_id=2,
+ message='ng!',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(ping2)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ # No message should be sent on channel 2.
+ self.assertRaises(KeyError,
+ request.connection.get_written_messages,
+ 2)
+ self.assertRaises(KeyError,
+ request.connection.get_written_control_messages,
+ 2)
+
+ def test_send_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_send_fragmented_ping(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish 3 bytes. This isn't enough to send the whole ping frame
+ # because the frame will have 5 bytes message('Ping!'). The frame
+ # should be fragmented.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ # Wait until the worker is blocked due to send quota shortage.
+ time.sleep(1)
+
+ # Replenish remaining 2 + 1 bytes (including extra cost).
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_send_fragmented_ping_while_sending_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(
+ path='/ping_while_hello_world')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Application will send:
+ # - text message 'Hello ' with fin=0
+ # - ping with 'Ping!' message
+ # - text message 'World!' with fin=1
+ # Replenish (6 + 1) + (2 + 1) bytes so that the ping will be
+ # fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=10)
+ request.connection.put_bytes(flow_control)
+
+ time.sleep(1)
+
+ # Replenish remaining 3 + 6 bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=9)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+
+ def test_send_fragmented_two_ping_while_sending_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(
+ path='/two_ping_while_hello_world')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Application will send:
+ # - text message 'Hello ' with fin=0
+ # - ping with 'Ping!' message
+ # - ping with 'Pong!' message
+ # - text message 'World!' with fin=1
+ # Replenish (6 + 1) + (2 + 1) bytes so that the first ping will be
+ # fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=10)
+ request.connection.put_bytes(flow_control)
+
+ time.sleep(1)
+
+ # Replenish remaining 3 + (5 + 1) + 6 bytes. The second ping won't
+ # be fragmented on the logical channel.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=15)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello World!'], messages)
+ control_messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, control_messages[0]['opcode'])
+ self.assertEqual('Ping!', control_messages[0]['message'])
+ self.assertEqual(common.OPCODE_PING, control_messages[1]['opcode'])
+ self.assertEqual('Pong!', control_messages[1]['message'])
+
+ def test_send_drop_channel(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # DropChannel for channel id 1 which doesn't have reason.
+ frame = create_binary_frame('\x00\x60\x01\x00', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_ACKNOWLEDGED,
+ drop_channel.drop_code)
+ self.assertEqual(1, drop_channel.channel_id)
+
+ def test_two_flow_control(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish 5 bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=5)
+ request.connection.put_bytes(flow_control)
+
+ # Send 10 bytes. The server will try echo back 10 bytes.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ # Replenish 5 + 1 (per-message extra cost) bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['HelloWorld'], messages)
+ received_flow_controls = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2]
+ # Replenishment for 'HelloWorld' + 1
+ self.assertEqual(11, received_flow_controls[0].send_quota)
+ # Replenishment for 'Goodbye' + 1
+ self.assertEqual(8, received_flow_controls[1].send_quota)
+
+ def test_no_send_quota_on_server(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # Just wait for 1 sec so that the server attempts to echo back
+ # 'HelloWorld'.
+ self.assertFalse(mux_handler.wait_until_done(timeout=1))
+
+ # No message should be sent on channel 2.
+ self.assertRaises(KeyError,
+ request.connection.get_written_messages,
+ 2)
+
+ def test_no_send_quota_on_server_for_permessage_extra_cost(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+ # Replenish only len('World') bytes.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=5)
+ request.connection.put_bytes(flow_control)
+ # Server should not callback for this message.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='World'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ # Just wait for 1 sec so that the server attempts to echo back
+ # 'World'.
+ self.assertFalse(mux_handler.wait_until_done(timeout=1))
+
+ # Only one message should be sent on channel 2.
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello'], messages)
+
+ def test_quota_violation_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 0)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='HelloWorld'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ control_blocks = request.connection.get_written_control_blocks()
+ self.assertEqual(5, len(control_blocks))
+ drop_channel = next(
+ b for b in control_blocks
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_consume_quota_empty_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ # Client has 1 byte quota.
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 1)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=2)
+ request.connection.put_bytes(flow_control)
+ # Send an empty message. Pywebsocket always replenishes 1 byte quota
+ # for empty message
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=''))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ # This message violates quota on channel id 2.
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(1, len(dispatcher.channel_events[2].messages))
+ self.assertEqual('', dispatcher.channel_events[2].messages[0])
+
+ received_flow_controls = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_FLOW_CONTROL and b.channel_id == 2]
+ self.assertEqual(1, len(received_flow_controls))
+ self.assertEqual(1, received_flow_controls[0].send_quota)
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_consume_quota_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ # Client has len('Hello') + len('Goodbye') + 2 bytes quota.
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS, 14)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='He', fin=False,
+ opcode=common.OPCODE_TEXT))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='llo', fin=True,
+ opcode=common.OPCODE_CONTINUATION))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(['Hello'], messages)
+
+ def test_fragmented_control_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/ping')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Replenish total 6 bytes in 3 FlowControls.
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=1)
+ request.connection.put_bytes(flow_control)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=2)
+ request.connection.put_bytes(flow_control)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=3)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ messages = request.connection.get_written_control_messages(2)
+ self.assertEqual(common.OPCODE_PING, messages[0]['opcode'])
+ self.assertEqual('Ping!', messages[0]['message'])
+
+ def test_channel_slot_violation_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(slots=1,
+ send_quota=mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Hello'))
+
+ # This request should be rejected.
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ flow_control = _create_flow_control_frame(channel_id=3,
+ replenished_quota=6)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=3, message='Hello'))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual([], dispatcher.channel_events[1].messages)
+ self.assertEqual(['Hello'], dispatcher.channel_events[2].messages)
+ self.assertFalse(dispatcher.channel_events.has_key(3))
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(3, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_NEW_CHANNEL_SLOT_VIOLATION,
+ drop_channel.drop_code)
+
+ def test_quota_overflow_by_client(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(slots=1,
+ send_quota=mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+ # Replenish 0x7FFFFFFFFFFFFFFF bytes twice.
+ flow_control = _create_flow_control_frame(
+ channel_id=2,
+ replenished_quota=0x7FFFFFFFFFFFFFFF)
+ request.connection.put_bytes(flow_control)
+ request.connection.put_bytes(flow_control)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(2, drop_channel.channel_id)
+ self.assertEqual(mux._DROP_CODE_SEND_QUOTA_OVERFLOW,
+ drop_channel.drop_code)
+
+ def test_invalid_encapsulated_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ first_byte = (mux._MUX_OPCODE_ADD_CHANNEL_REQUEST << 5)
+ block = (chr(first_byte) +
+ mux._encode_channel_id(1) +
+ mux._encode_number(0))
+ payload = mux._encode_channel_id(mux._CONTROL_CHANNEL_ID) + block
+ text_frame = create_binary_frame(payload, opcode=common.OPCODE_TEXT,
+ mask=True)
+ request.connection.put_bytes(text_frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_INVALID_ENCAPSULATING_MESSAGE,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_channel_id_truncated(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # The last byte of the channel id is missing.
+ frame = create_binary_frame('\x80', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_CHANNEL_ID_TRUNCATED,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_inner_frame_truncated(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Just contain channel id 1.
+ frame = create_binary_frame('\x01', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_unknown_mux_opcode(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # Undefined opcode 5
+ frame = create_binary_frame('\x00\xa0', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_UNKNOWN_MUX_OPCODE,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_invalid_mux_control_block(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+
+ # DropChannel contains 1 byte reason
+ frame = create_binary_frame('\x00\x60\x00\x01\x00', mask=True)
+ request.connection.put_bytes(frame)
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channel = next(
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL)
+ self.assertEqual(mux._DROP_CODE_INVALID_MUX_CONTROL_BLOCK,
+ drop_channel.drop_code)
+ self.assertEqual(common.STATUS_INTERNAL_ENDPOINT_ERROR,
+ request.connection.server_close_code)
+
+ def test_permessage_compress(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Enable permessage compress extension on logical channel 2.
+ extensions = '%s; method=deflate' % (
+ common.PERMESSAGE_COMPRESSION_EXTENSION)
+ encoded_handshake = _create_request_header(path='/echo',
+ extensions=extensions)
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ flow_control = _create_flow_control_frame(channel_id=2,
+ replenished_quota=20)
+ request.connection.put_bytes(flow_control)
+
+ # Send compressed 'Hello' twice.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello1 = compress.compress('Hello')
+ compressed_hello1 += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello1 = compressed_hello1[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello1,
+ rsv1=True))
+ compressed_hello2 = compress.compress('Hello')
+ compressed_hello2 += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello2 = compressed_hello2[:-4]
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message=compressed_hello2,
+ rsv1=True))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=2, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['Hello', 'Hello'],
+ dispatcher.channel_events[2].messages)
+ # Written 'Hello's should be compressed.
+ messages = request.connection.get_written_messages(2)
+ self.assertEqual(2, len(messages))
+ self.assertEqual(compressed_hello1, messages[0])
+ self.assertEqual(compressed_hello2, messages[1])
+
+
+ def test_permessage_compress_fragmented_message(self):
+ extensions = common.parse_extensions(
+ '%s; method=deflate' % common.PERMESSAGE_COMPRESSION_EXTENSION)
+ request = _create_mock_request(
+ logical_channel_extensions=extensions)
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ # Send compressed 'HelloHelloHello' as fragmented message.
+ compress = zlib.compressobj(
+ zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS)
+ compressed_hello = compress.compress('HelloHelloHello')
+ compressed_hello += compress.flush(zlib.Z_SYNC_FLUSH)
+ compressed_hello = compressed_hello[:-4]
+
+ m = len(compressed_hello) / 2
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1,
+ message=compressed_hello[:m],
+ fin=False, rsv1=True,
+ opcode=common.OPCODE_TEXT))
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1,
+ message=compressed_hello[m:],
+ fin=True, rsv1=False,
+ opcode=common.OPCODE_CONTINUATION))
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ self.assertEqual(['HelloHelloHello'],
+ dispatcher.channel_events[1].messages)
+ messages = request.connection.get_written_messages(1)
+ self.assertEqual(1, len(messages))
+ self.assertEqual(compressed_hello, messages[0])
+
+ def test_receive_bad_fragmented_message(self):
+ request = _create_mock_request()
+ dispatcher = _MuxMockDispatcher()
+ mux_handler = mux._MuxHandler(request, dispatcher)
+ mux_handler.start()
+ mux_handler.add_channel_slots(mux._INITIAL_NUMBER_OF_CHANNEL_SLOTS,
+ mux._INITIAL_QUOTA_FOR_CLIENT)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=2, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with fin=False, and then send a frame with
+ # opcode=TEXT (not CONTINUATION). Logical channel 2 should be dropped.
+ frame1 = _create_logical_frame(channel_id=2,
+ message='Hello ',
+ fin=False,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame1)
+ frame2 = _create_logical_frame(channel_id=2,
+ message='World!',
+ fin=True,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame2)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=3, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with opcode=CONTINUATION without a preceding frame
+ # the fin of which is not set. Logical channel 3 should be dropped.
+ frame3 = _create_logical_frame(channel_id=3,
+ message='Hello',
+ fin=True,
+ opcode=common.OPCODE_CONTINUATION)
+ request.connection.put_bytes(frame3)
+
+ encoded_handshake = _create_request_header(path='/echo')
+ add_channel_request = _create_add_channel_request_frame(
+ channel_id=4, encoding=0,
+ encoded_handshake=encoded_handshake)
+ request.connection.put_bytes(add_channel_request)
+
+ # Send a frame with opcode=PING and fin=False, and then send a frame
+ # with opcode=TEXT (not CONTINUATION). Logical channel 4 should be
+ # dropped.
+ frame4 = _create_logical_frame(channel_id=4,
+ message='Ping',
+ fin=False,
+ opcode=common.OPCODE_PING)
+ request.connection.put_bytes(frame4)
+ frame5 = _create_logical_frame(channel_id=4,
+ message='Hello',
+ fin=True,
+ opcode=common.OPCODE_TEXT)
+ request.connection.put_bytes(frame5)
+
+ request.connection.put_bytes(
+ _create_logical_frame(channel_id=1, message='Goodbye'))
+
+ self.assertTrue(mux_handler.wait_until_done(timeout=2))
+
+ drop_channels = [
+ b for b in request.connection.get_written_control_blocks()
+ if b.opcode == mux._MUX_OPCODE_DROP_CHANNEL]
+ self.assertEqual(3, len(drop_channels))
+ for d in drop_channels:
+ self.assertEqual(mux._DROP_CODE_BAD_FRAGMENTATION,
+ d.drop_code)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py
new file mode 100755
index 000000000..81acfeb04
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for stream module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import common
+from mod_pywebsocket import stream
+
+
+class StreamTest(unittest.TestCase):
+ """A unittest for stream module."""
+
+ def test_create_header(self):
+ # more, rsv1, ..., rsv4 are all true
+ header = stream.create_header(common.OPCODE_TEXT, 1, 1, 1, 1, 1, 1)
+ self.assertEqual('\xf1\x81', header)
+
+ # Maximum payload size
+ header = stream.create_header(
+ common.OPCODE_TEXT, (1 << 63) - 1, 0, 0, 0, 0, 0)
+ self.assertEqual('\x01\x7f\x7f\xff\xff\xff\xff\xff\xff\xff', header)
+
+ # Invalid opcode 0x10
+ self.assertRaises(ValueError,
+ stream.create_header,
+ 0x10, 0, 0, 0, 0, 0, 0)
+
+ # Invalid value 0xf passed to more parameter
+ self.assertRaises(ValueError,
+ stream.create_header,
+ common.OPCODE_TEXT, 0, 0xf, 0, 0, 0, 0)
+
+ # Too long payload_length
+ self.assertRaises(ValueError,
+ stream.create_header,
+ common.OPCODE_TEXT, 1 << 63, 0, 0, 0, 0, 0)
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py
new file mode 100755
index 000000000..ca9ac7130
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_stream_hixie75.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for stream module."""
+
+
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket.stream import StreamHixie75
+from test.test_msgutil import _create_request_hixie75
+
+
+class StreamHixie75Test(unittest.TestCase):
+ """A unittest for StreamHixie75 class."""
+
+ def test_payload_length(self):
+ for length, bytes in ((0, '\x00'), (0x7f, '\x7f'), (0x80, '\x81\x00'),
+ (0x1234, '\x80\xa4\x34')):
+ test_stream = StreamHixie75(_create_request_hixie75(bytes))
+ self.assertEqual(
+ length, test_stream._read_payload_length_hixie75())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py
new file mode 100755
index 000000000..20f4ab059
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/test_util.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python
+#
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Tests for util module."""
+
+
+import os
+import random
+import sys
+import unittest
+
+import set_sys_path # Update sys.path to locate mod_pywebsocket module.
+
+from mod_pywebsocket import util
+
+
+_TEST_DATA_DIR = os.path.join(os.path.split(__file__)[0], 'testdata')
+
+
+class UtilTest(unittest.TestCase):
+ """A unittest for util module."""
+
+ def test_get_stack_trace(self):
+ self.assertEqual('None\n', util.get_stack_trace())
+ try:
+ a = 1 / 0 # Intentionally raise exception.
+ except Exception:
+ trace = util.get_stack_trace()
+ self.failUnless(trace.startswith('Traceback'))
+ self.failUnless(trace.find('ZeroDivisionError') != -1)
+
+ def test_prepend_message_to_exception(self):
+ exc = Exception('World')
+ self.assertEqual('World', str(exc))
+ util.prepend_message_to_exception('Hello ', exc)
+ self.assertEqual('Hello World', str(exc))
+
+ def test_get_script_interp(self):
+ cygwin_path = 'c:\\cygwin\\bin'
+ cygwin_perl = os.path.join(cygwin_path, 'perl')
+ self.assertEqual(None, util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'README')))
+ self.assertEqual(None, util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'README'), cygwin_path))
+ self.assertEqual('/usr/bin/perl -wT', util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'hello.pl')))
+ self.assertEqual(cygwin_perl + ' -wT', util.get_script_interp(
+ os.path.join(_TEST_DATA_DIR, 'hello.pl'), cygwin_path))
+
+ def test_hexify(self):
+ self.assertEqual('61 7a 41 5a 30 39 20 09 0d 0a 00 ff',
+ util.hexify('azAZ09 \t\r\n\x00\xff'))
+
+
+class RepeatedXorMaskerTest(unittest.TestCase):
+ """A unittest for RepeatedXorMasker class."""
+
+ def test_mask(self):
+ # Sample input e6,97,a5 is U+65e5 in UTF-8
+ masker = util.RepeatedXorMasker('\xff\xff\xff\xff')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\x19\x68\x5a', result)
+
+ masker = util.RepeatedXorMasker('\x00\x00\x00\x00')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\xe6\x97\xa5', result)
+
+ masker = util.RepeatedXorMasker('\xe6\x97\xa5\x20')
+ result = masker.mask('\xe6\x97\xa5')
+ self.assertEqual('\x00\x00\x00', result)
+
+ def test_mask_twice(self):
+ masker = util.RepeatedXorMasker('\x00\x7f\xff\x20')
+ # mask[0], mask[1], ... will be used.
+ result = masker.mask('\x00\x00\x00\x00\x00')
+ self.assertEqual('\x00\x7f\xff\x20\x00', result)
+ # mask[2], mask[0], ... will be used for the next call.
+ result = masker.mask('\x00\x00\x00\x00\x00')
+ self.assertEqual('\x7f\xff\x20\x00\x7f', result)
+
+ def test_mask_large_data(self):
+ masker = util.RepeatedXorMasker('mASk')
+ original = ''.join([chr(i % 256) for i in xrange(1000)])
+ result = masker.mask(original)
+ expected = ''.join(
+ [chr((i % 256) ^ ord('mASk'[i % 4])) for i in xrange(1000)])
+ self.assertEqual(expected, result)
+
+ masker = util.RepeatedXorMasker('MaSk')
+ first_part = 'The WebSocket Protocol enables two-way communication.'
+ result = masker.mask(first_part)
+ self.assertEqual(
+ '\x19\t6K\x1a\x0418"\x028\x0e9A\x03\x19"\x15<\x08"\rs\x0e#'
+ '\x001\x07(\x12s\x1f:\x0e~\x1c,\x18s\x08"\x0c>\x1e#\x080\n9'
+ '\x08<\x05c',
+ result)
+ second_part = 'It has two parts: a handshake and the data transfer.'
+ result = masker.mask(second_part)
+ self.assertEqual(
+ "('K%\x00 K9\x16<K=\x00!\x1f>[s\nm\t2\x05)\x12;\n&\x04s\n#"
+ "\x05s\x1f%\x04s\x0f,\x152K9\x132\x05>\x076\x19c",
+ result)
+
+
+def get_random_section(source, min_num_chunks):
+ chunks = []
+ bytes_chunked = 0
+
+ while bytes_chunked < len(source):
+ chunk_size = random.randint(
+ 1,
+ min(len(source) / min_num_chunks, len(source) - bytes_chunked))
+ chunk = source[bytes_chunked:bytes_chunked + chunk_size]
+ chunks.append(chunk)
+ bytes_chunked += chunk_size
+
+ return chunks
+
+
+class InflaterDeflaterTest(unittest.TestCase):
+ """A unittest for _Inflater and _Deflater class."""
+
+ def test_inflate_deflate_default(self):
+ input = b'hello' + '-' * 30000 + b'hello'
+ inflater15 = util._Inflater(15)
+ deflater15 = util._Deflater(15)
+ inflater8 = util._Inflater(8)
+ deflater8 = util._Deflater(8)
+
+ compressed15 = deflater15.compress_and_finish(input)
+ compressed8 = deflater8.compress_and_finish(input)
+
+ inflater15.append(compressed15)
+ inflater8.append(compressed8)
+
+ self.assertNotEqual(compressed15, compressed8)
+ self.assertEqual(input, inflater15.decompress(-1))
+ self.assertEqual(input, inflater8.decompress(-1))
+
+ def test_random_section(self):
+ random.seed(a=0)
+ source = ''.join(
+ [chr(random.randint(0, 255)) for i in xrange(100 * 1024)])
+
+ chunked_input = get_random_section(source, 10)
+ print "Input chunk sizes: %r" % [len(c) for c in chunked_input]
+
+ deflater = util._Deflater(15)
+ compressed = []
+ for chunk in chunked_input:
+ compressed.append(deflater.compress(chunk))
+ compressed.append(deflater.compress_and_finish(''))
+
+ chunked_expectation = get_random_section(source, 10)
+ print ("Expectation chunk sizes: %r" %
+ [len(c) for c in chunked_expectation])
+
+ inflater = util._Inflater(15)
+ inflater.append(''.join(compressed))
+ for chunk in chunked_expectation:
+ decompressed = inflater.decompress(len(chunk))
+ self.assertEqual(chunk, decompressed)
+
+ self.assertEqual('', inflater.decompress(-1))
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
new file mode 100644
index 000000000..c001aa559
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/README
@@ -0,0 +1 @@
+Test data directory
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
new file mode 100644
index 000000000..367f9930f
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/abort_by_user_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from mod_pywebsocket import handshake
+
+
+def web_socket_do_extra_handshake(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+def web_socket_transfer_data(request):
+ raise handshake.AbortedByUserException("abort for test")
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
new file mode 100644
index 000000000..7f87c6af2
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/blank_wsh.py
@@ -0,0 +1,31 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# intentionally left blank
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
new file mode 100644
index 000000000..2c139fa17
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/origin_check_wsh.py
@@ -0,0 +1,42 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ if request.ws_origin == 'http://example.com':
+ return
+ raise ValueError('Unacceptable origin: %r' % request.ws_origin)
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('origin_check_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py
new file mode 100644
index 000000000..b982d0231
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/exception_in_transfer_wsh.py
@@ -0,0 +1,44 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Exception in web_socket_transfer_data().
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ raise Exception('Intentional Exception for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
new file mode 100644
index 000000000..17e7be180
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/no_wsh_at_the_end.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Correct signatures, wrong file name.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/no_wsh_at_the_end.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
new file mode 100644
index 000000000..26352eb4c
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/non_callable_wsh.py
@@ -0,0 +1,39 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Non-callable handlers.
+"""
+
+
+web_socket_do_extra_handshake = True
+web_socket_transfer_data = 1
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
new file mode 100644
index 000000000..db3ff6930
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/plain_wsh.py
@@ -0,0 +1,40 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write('sub/plain_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
new file mode 100644
index 000000000..6bf659bc9
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Wrong web_socket_do_extra_handshake signature.
+"""
+
+
+def no_web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/wrong_handshake_sig_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
new file mode 100644
index 000000000..e0e2e5507
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py
@@ -0,0 +1,45 @@
+# Copyright 2009, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""Wrong web_socket_transfer_data() signature.
+"""
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def no_web_socket_transfer_data(request):
+ request.connection.write(
+ 'sub/wrong_transfer_sig_wsh.py is called for %s, %s' %
+ (request.ws_resource, request.ws_protocol))
+
+
+# vi:sts=4 sw=4 et
diff --git a/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
new file mode 100644
index 000000000..882ef5a10
--- /dev/null
+++ b/testing/web-platform/tests/tools/pywebsocket/src/test/testdata/hello.pl
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -wT
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+print "Hello\n";