diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/tools/pywebsocket/src/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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')
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 Binary files differnew file mode 100644 index 000000000..14e139927 --- /dev/null +++ b/testing/web-platform/tests/tools/pywebsocket/src/test/cert/client_cert.p12 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"; |