From 4ff34a416976106ce00a115cd0fa6b64f5d1e219 Mon Sep 17 00:00:00 2001 From: Patrick McManus Date: Sat, 21 May 2011 21:27:52 -0400 Subject: [PATCH] bug 640003 websockets - update incorporated pywebsockets to support -07 r=biesi --- testing/mochitest/Makefile.in | 15 +- testing/mochitest/pywebsocket/README | 30 +- .../pywebsocket/mod_pywebsocket/__init__.py | 83 ++- .../mod_pywebsocket/_stream_base.py | 151 ++++++ .../mod_pywebsocket/_stream_hixie75.py | 218 ++++++++ .../mod_pywebsocket/_stream_hybi06.py | 510 ++++++++++++++++++ .../pywebsocket/mod_pywebsocket/common.py | 74 +++ .../pywebsocket/mod_pywebsocket/dispatch.py | 129 +++-- .../mod_pywebsocket/handshake/__init__.py | 61 ++- .../mod_pywebsocket/handshake/_base.py | 77 ++- .../mod_pywebsocket/handshake/draft75.py | 83 +-- .../mod_pywebsocket/handshake/handshake.py | 208 ------- .../mod_pywebsocket/handshake/hybi00.py | 232 ++++++++ .../mod_pywebsocket/handshake/hybi06.py | 250 +++++++++ .../mod_pywebsocket/headerparserhandler.py | 22 +- .../mod_pywebsocket/memorizingfile.py | 2 +- .../pywebsocket/mod_pywebsocket/msgutil.py | 146 +---- .../pywebsocket/mod_pywebsocket/standalone.py | 476 ++++++++++++++++ .../pywebsocket/mod_pywebsocket/stream.py | 53 ++ .../pywebsocket/mod_pywebsocket/util.py | 216 +++++++- testing/mochitest/pywebsocket/standalone.py | 60 ++- 21 files changed, 2573 insertions(+), 523 deletions(-) create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi06.py create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/common.py delete mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/handshake/handshake.py create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py create mode 100755 testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi06.py create mode 100755 testing/mochitest/pywebsocket/mod_pywebsocket/standalone.py create mode 100644 testing/mochitest/pywebsocket/mod_pywebsocket/stream.py diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in index 290eda6edfc4..ab5482f257cd 100644 --- a/testing/mochitest/Makefile.in +++ b/testing/mochitest/Makefile.in @@ -108,18 +108,25 @@ _PYWEBSOCKET_FILES = \ _MOD_PYWEBSOCKET_FILES = \ pywebsocket/mod_pywebsocket/__init__.py \ + pywebsocket/mod_pywebsocket/common.py \ pywebsocket/mod_pywebsocket/dispatch.py \ - pywebsocket/mod_pywebsocket/util.py \ - pywebsocket/mod_pywebsocket/msgutil.py \ - pywebsocket/mod_pywebsocket/memorizingfile.py \ pywebsocket/mod_pywebsocket/headerparserhandler.py \ + pywebsocket/mod_pywebsocket/memorizingfile.py \ + pywebsocket/mod_pywebsocket/util.py \ + pywebsocket/mod_pywebsocket/stream.py \ + pywebsocket/mod_pywebsocket/_stream_hixie75.py \ + pywebsocket/mod_pywebsocket/msgutil.py \ + pywebsocket/mod_pywebsocket/_stream_hybi06.py \ + pywebsocket/mod_pywebsocket/standalone.py \ + pywebsocket/mod_pywebsocket/_stream_base.py \ $(NULL) _HANDSHAKE_FILES = \ pywebsocket/mod_pywebsocket/handshake/__init__.py \ + pywebsocket/mod_pywebsocket/handshake/hybi00.py \ pywebsocket/mod_pywebsocket/handshake/_base.py \ pywebsocket/mod_pywebsocket/handshake/draft75.py \ - pywebsocket/mod_pywebsocket/handshake/handshake.py \ + pywebsocket/mod_pywebsocket/handshake/hybi06.py \ $(NULL) _DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir) diff --git a/testing/mochitest/pywebsocket/README b/testing/mochitest/pywebsocket/README index 6c93a907ee0b..b7d197338469 100644 --- a/testing/mochitest/pywebsocket/README +++ b/testing/mochitest/pywebsocket/README @@ -1 +1,29 @@ -This is mod_pywebsocket 0.5, from http://code.google.com/p/pywebsocket/ +mod_pywebsocket http://pywebsocket.googlecode.com/svn +version 470 +supporting ietf-07 + +includes the following minor patch:: + +diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py +--- a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py ++++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py +@@ -60,17 +60,18 @@ def _normalize_path(path): + path: the path to normalize. + + Path is converted to the absolute path. + The input path can use either '\\' or '/' as the separator. + The normalized path always uses '/' regardless of the platform. + """ + + path = path.replace('\\', os.path.sep) +- path = os.path.realpath(path) ++ # do not normalize away symlinks in mochitest ++ # path = os.path.realpath(path) + path = path.replace('\\', '/') + return path + + + def _create_path_to_resource_converter(base_dir): + base_dir = _normalize_path(base_dir) + + base_len = len(base_dir) diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py index d947128e9738..e33bc4cd91f4 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py @@ -28,11 +28,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Web Socket extension for Apache HTTP Server. +"""WebSocket extension for Apache HTTP Server. -mod_pywebsocket is a Web Socket extension for Apache HTTP Server +mod_pywebsocket is a WebSocket extension for Apache HTTP Server intended for testing or experimental purposes. mod_python is required. + Installation: 0. Prepare an Apache HTTP Server for which mod_python is enabled. @@ -46,27 +47,26 @@ Installation: PythonPath "sys.path+['']" Always specify the following. is the directory where - user-written Web Socket handlers are placed. + user-written WebSocket handlers are placed. PythonOption mod_pywebsocket.handler_root PythonHeaderParserHandler mod_pywebsocket.headerparserhandler - To limit the search for Web Socket handlers to a directory + To limit the search for WebSocket handlers to a directory under , configure as follows: PythonOption mod_pywebsocket.handler_scan is useful in saving scan time when - contains many non-Web Socket handler files. + contains many non-WebSocket handler files. If you want to support old handshake based on draft-hixie-thewebsocketprotocol-75: PythonOption mod_pywebsocket.allow_draft75 On - Example snippet of httpd.conf: - (mod_pywebsocket is in /websock_lib, Web Socket handlers are in + (mod_pywebsocket is in /websock_lib, WebSocket handlers are in /websock_handlers, port is 80 for ws, 443 for wss.) @@ -75,9 +75,17 @@ Installation: PythonHeaderParserHandler mod_pywebsocket.headerparserhandler -Writing Web Socket handlers: +2. Tune Apache parameters for serving WebSocket. We'd like to note that at + least TimeOut directive from core features and RequestReadTimeout directive + from mod_reqtimeout should be modified not to kill connections in only a few + seconds of idle time. -When a Web Socket request comes in, the resource name +3. Verify installation. You can use example/console.html to poke the server. + + +Writing WebSocket handlers: + +When a WebSocket request comes in, the resource name specified in the handshake is considered as if it is a file path under and the handler defined in /_wsh.py is invoked. @@ -85,7 +93,7 @@ specified in the handshake is considered as if it is a file path under For example, if the resource name is /example/chat, the handler defined in /example/chat_wsh.py is invoked. -A Web Socket handler is composed of the following two functions: +A WebSocket handler is composed of the following two functions: web_socket_do_extra_handshake(request) web_socket_transfer_data(request) @@ -94,16 +102,65 @@ where: request: mod_python request. web_socket_do_extra_handshake is called during the handshake after the -headers are successfully parsed and Web Socket properties (ws_location, -ws_origin, ws_protocol, and ws_resource) are added to request. A handler +headers are successfully parsed and WebSocket properties (ws_location, +ws_origin, and ws_resource) are added to request. A handler can reject the request by raising an exception. +A request object has the following properties that you can use during the extra +handshake (web_socket_do_extra_handshake): +- ws_resource +- ws_origin +- ws_version +- ws_location (Hixie 75 and HyBi 00 only) +- ws_extensions (Hybi 06 and later) +- ws_deflate (HyBi 06 and later) +- ws_protocol +- ws_requested_protocols (HyBi 06 and later) + +The last two are a bit tricky. + +For HyBi 06 and later, ws_protocol is always set to None when +web_socket_do_extra_handshake is called. If ws_requested_protocols is not +None, you must choose one subprotocol from this list and set it to ws_protocol. + +For Hixie 75 and HyBi 00, when web_socket_do_extra_handshake is called, +ws_protocol is set to the value given by the client in Sec-WebSocket-Protocol +(WebSocket-Protocol for Hixie 75) header or None if such header was not found +in the opening handshake request. Finish extra handshake with ws_protocol +untouched to accept the request subprotocol. Then, Sec-WebSocket-Protocol +(or WebSocket-Protocol) header will be sent to the client in response with the +same value as requested. Raise an exception in web_socket_do_extra_handshake to +reject the requested subprotocol. + web_socket_transfer_data is called after the handshake completed successfully. A handler can receive/send messages from/to the client using request. mod_pywebsocket.msgutil module provides utilities for data transfer. -A Web Socket handler must be thread-safe if the server (Apache or +You can receive a message by the following statement. + + message = request.ws_stream.receive_message() + +This call blocks until any complete text frame arrives, and the payload data of +the incoming frame will be stored into message. When you're using IETF HyBi 00 +or later protocol, receive_message() will return None on receiving +client-initiated closing handshake. When any error occurs, receive_message() +will raise some exception. + +You can send a message by the following statement. + + request.ws_stream.send_message(message) + +Executing the following statement or just return-ing from +web_socket_transfer_data cause connection close. + + request.ws_stream.close_connection() + +When you're using IETF HyBi 00 or later protocol, close_connection will wait +for closing handshake acknowledgement coming from the client. When it couldn't +receive a valid acknowledgement, raises an exception. + +A WebSocket handler must be thread-safe if the server (Apache or standalone.py) is configured to use threads. """ diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py new file mode 100644 index 000000000000..eb17d7f89bc9 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_base.py @@ -0,0 +1,151 @@ +# 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. + + +"""Base stream class. +""" + + +# Note: request.connection.write/read are used in this module, even though +# mod_python document says that they should be used only in connection +# handlers. Unfortunately, we have no other options. For example, +# request.write/read are not suitable because they don't allow direct raw bytes +# writing/reading. + + +from mod_pywebsocket import util + + +# Exceptions +class ConnectionTerminatedException(Exception): + """This exception will be raised when a connection is terminated + unexpectedly. + """ + pass + + +class InvalidFrameException(ConnectionTerminatedException): + """This exception will be raised when we received an invalid frame we + cannot parse. + """ + pass + + +class BadOperationException(RuntimeError): + """This exception will be raised when send_message() is called on + server-terminated connection or receive_message() is called on + client-terminated connection. + """ + pass + + +class UnsupportedFrameException(RuntimeError): + """This exception will be raised when we receive a frame with flag, opcode + we cannot handle. Handlers can just catch and ignore this exception and + call receive_message() again to continue processing the next frame. + """ + pass + + +class StreamBase(object): + """Base stream class.""" + + def __init__(self, request): + """Construct an instance. + + Args: + request: mod_python request. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + + def _read(self, length): + """Reads length bytes from connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + bytes = self._request.connection.read(length) + if not bytes: + raise ConnectionTerminatedException( + 'Receiving %d byte failed. Peer (%r) closed connection' % + (length, (self._request.connection.remote_addr,))) + return bytes + + def _write(self, bytes): + """Writes given bytes to connection. In case we catch any exception, + prepends remote address to the exception message and raise again. + """ + + try: + self._request.connection.write(bytes) + except Exception, e: + util.prepend_message_to_exception( + 'Failed to send message to %r: ' % + (self._request.connection.remote_addr,), + e) + raise + + def receive_bytes(self, length): + """Receives multiple bytes. Retries read when we couldn't receive the + specified amount. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + bytes = [] + while length > 0: + new_bytes = self._read(length) + bytes.append(new_bytes) + length -= len(new_bytes) + return ''.join(bytes) + + def _read_until(self, delim_char): + """Reads bytes until we encounter delim_char. The result will not + contain delim_char. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + bytes = [] + while True: + ch = self._read(1) + if ch == delim_char: + break + bytes.append(ch) + return ''.join(bytes) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py new file mode 100644 index 000000000000..429c899d8926 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hixie75.py @@ -0,0 +1,218 @@ +# 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. + + +"""Stream of WebSocket protocol with the framing used by IETF HyBi 00 and +Hixie 75. For Hixie 75 this stream doesn't perform closing handshake. +""" + + +from mod_pywebsocket import common +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket import util + + +class StreamHixie75(StreamBase): + """Stream of WebSocket messages.""" + + def __init__(self, request, enable_closing_handshake=False): + """Construct an instance. + + Args: + request: mod_python request. + enable_closing_handshake: to let StreamHixie75 perform closing + handshake as specified in HyBi 00, set + this option to True. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._enable_closing_handshake = enable_closing_handshake + + self._request.client_terminated = False + self._request.server_terminated = False + + def send_message(self, message, end=True): + """Send message. + + Args: + message: unicode string to send. + + Raises: + BadOperationException: when called on a server-terminated + connection. + """ + + if not end: + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_message with end=False') + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + self._write(''.join(['\x00', message.encode('utf-8'), '\xff'])) + + def _read_payload_length_hixie75(self): + """Reads a length header in a Hixie75 version frame with length. + + Raises: + ConnectionTerminatedException: when read returns empty string. + """ + + length = 0 + while True: + b_str = self._read(1) + b = ord(b_str) + length = length * 128 + (b & 0x7f) + if (b & 0x80) == 0: + break + return length + + def receive_message(self): + """Receive a WebSocket frame and return its payload an unicode string. + + Returns: + payload unicode string in a WebSocket frame. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + BadOperationException: when called on a client-terminated + connection. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing ' + 'handshake') + + while True: + # Read 1 byte. + # mp_conn.read will block if no bytes are available. + # Timeout is controlled by TimeOut directive of Apache. + frame_type_str = self.receive_bytes(1) + frame_type = ord(frame_type_str) + if (frame_type & 0x80) == 0x80: + # The payload length is specified in the frame. + # Read and discard. + length = self._read_payload_length_hixie75() + if length > 0: + _ = self.receive_bytes(length) + # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the + # /client terminated/ flag and abort these steps. + if not self._enable_closing_handshake: + continue + + if frame_type == 0xFF and length == 0: + self._request.client_terminated = True + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing ' + 'handshake') + return None + + self._logger.debug( + 'Received client-initiated closing handshake') + + self._send_closing_handshake() + self._logger.debug( + 'Sent ack for client-initiated closing handshake') + return None + else: + # The payload is delimited with \xff. + bytes = self._read_until('\xff') + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + message = bytes.decode('utf-8', 'replace') + if frame_type == 0x00: + return message + # Discard data of other types. + + def _send_closing_handshake(self): + if not self._enable_closing_handshake: + raise BadOperationException( + 'Closing handshake is not supported in Hixie 75 protocol') + + self._request.server_terminated = True + + # 5.3 the server may decide to terminate the WebSocket connection by + # running through the following steps: + # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the + # start of the closing handshake. + self._write('\xff\x00') + + def close_connection(self, unused_code='', unused_reason=''): + """Closes a WebSocket connection. + + Raises: + ConnectionTerminatedException: when closing handshake was + not successfull. + """ + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + if not self._enable_closing_handshake: + self._request.server_terminated = True + self._logger.debug('Connection closed') + return + + self._send_closing_handshake() + self._logger.debug('Sent server-initiated closing handshake') + + # TODO(ukai): 2. wait until the /client terminated/ flag has been set, + # or until a server-defined timeout expires. + # + # For now, we expect receiving closing handshake right after sending + # out closing handshake, and if we couldn't receive non-handshake + # frame, we take it as ConnectionTerminatedException. + message = self.receive_message() + if message is not None: + raise ConnectionTerminatedException( + 'Didn\'t receive valid ack for closing handshake') + # TODO: 3. close the WebSocket connection. + # note: mod_python Connection (mp_conn) doesn't have close method. + + def send_ping(self, body): + raise BadOperationException( + 'StreamHixie75 doesn\'t support send_ping') + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi06.py b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi06.py new file mode 100644 index 000000000000..649556d2aaf7 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/_stream_hybi06.py @@ -0,0 +1,510 @@ +# 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. + + +"""Stream class for IETF HyBi 07 WebSocket protocol. +""" + + +from collections import deque +import os +import struct + +from mod_pywebsocket import common +from mod_pywebsocket import util +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import StreamBase +from mod_pywebsocket._stream_base import UnsupportedFrameException + + +def is_control_opcode(opcode): + return (opcode >> 3) == 1 + + +_NOOP_MASKER = util.NoopMasker() + + +# Helper functions made public to be used for writing unittests for WebSocket +# clients. +def create_length_header(length, mask): + """Creates a length header. + + Args: + length: Frame length. Must be less than 2^63. + mask: Mask bit. Must be boolean. + + Raises: + ValueError: when bad data is given. + """ + + if mask: + mask_bit = 1 << 7 + else: + mask_bit = 0 + + if length < 0: + raise ValueError('length must be non negative integer') + elif length <= 125: + return chr(mask_bit | length) + elif length < (1 << 16): + return chr(mask_bit | 126) + struct.pack('!H', length) + elif length < (1 << 63): + return chr(mask_bit | 127) + struct.pack('!Q', length) + else: + raise ValueError('Payload is too big for one frame') + + +def create_header(opcode, payload_length, fin, rsv1, rsv2, rsv3, mask): + """Creates a frame header. + + Raises: + Exception: when bad data is given. + """ + + if opcode < 0 or 0xf < opcode: + raise ValueError('Opcode out of range') + + if payload_length < 0 or (1 << 63) <= payload_length: + raise ValueError('payload_length out of range') + + if (fin | rsv1 | rsv2 | rsv3) & ~1: + raise ValueError('FIN bit and Reserved bit parameter must be 0 or 1') + + header = '' + + first_byte = ((fin << 7) + | (rsv1 << 6) | (rsv2 << 5) | (rsv3 << 4) + | opcode) + header += chr(first_byte) + header += create_length_header(payload_length, mask) + + return header + + +def _build_frame(header, body, mask): + if not mask: + return header + body + + masking_nonce = os.urandom(4) + masker = util.RepeatedXorMasker(masking_nonce) + + return header + masking_nonce + masker.mask(body) + + +def create_text_frame(message, opcode=common.OPCODE_TEXT, fin=1, mask=False): + """Creates a simple text frame with no extension, reserved bit.""" + + encoded_message = message.encode('utf-8') + header = create_header(opcode, len(encoded_message), fin, 0, 0, 0, mask) + return _build_frame(header, encoded_message, mask) + + +class FragmentedTextFrameBuilder(object): + """A stateful class to send a message as fragments.""" + + def __init__(self, mask): + """Constructs an instance.""" + + self._mask = mask + + self._started = False + + def build(self, message, end): + if self._started: + opcode = common.OPCODE_CONTINUATION + else: + opcode = common.OPCODE_TEXT + + if end: + self._started = False + fin = 1 + else: + self._started = True + fin = 0 + + return create_text_frame(message, opcode, fin, self._mask) + + +def create_ping_frame(body, mask=False): + header = create_header(common.OPCODE_PING, len(body), 1, 0, 0, 0, mask) + return _build_frame(header, body, mask) + + +def create_pong_frame(body, mask=False): + header = create_header(common.OPCODE_PONG, len(body), 1, 0, 0, 0, mask) + return _build_frame(header, body, mask) + + +def create_close_frame(body, mask=False): + header = create_header(common.OPCODE_CLOSE, len(body), 1, 0, 0, 0, mask) + return _build_frame(header, body, mask) + + +class StreamOptions(object): + def __init__(self): + self.deflate = False + self.mask_send = False + self.unmask_receive = True + + +class Stream(StreamBase): + """Stream of WebSocket messages.""" + + def __init__(self, request, options): + """Constructs an instance. + + Args: + request: mod_python request. + """ + + StreamBase.__init__(self, request) + + self._logger = util.get_class_logger(self) + + self._options = options + + if self._options.deflate: + self._logger.debug('Deflated stream') + self._request = util.DeflateRequest(self._request) + + self._request.client_terminated = False + self._request.server_terminated = False + + # Holds body of received fragments. + self._received_fragments = [] + # Holds the opcode of the first fragment. + self._original_opcode = None + + self._writer = FragmentedTextFrameBuilder(self._options.mask_send) + + self._ping_queue = deque() + + def _receive_frame(self): + """Receives a frame and return data in the frame as a tuple containing + each header field and payload separately. + + Raises: + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid data. + """ + + received = self.receive_bytes(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 == 1) != self._options.unmask_receive: + raise InvalidFrameException( + 'Mask bit on the received frame did\'nt match masking ' + 'configuration for received frames') + + if payload_length == 127: + extended_payload_length = self.receive_bytes(8) + payload_length = struct.unpack( + '!Q', extended_payload_length)[0] + if payload_length > 0x7FFFFFFFFFFFFFFF: + raise InvalidFrameException( + 'Extended payload length >= 2^63') + elif payload_length == 126: + extended_payload_length = self.receive_bytes(2) + payload_length = struct.unpack( + '!H', extended_payload_length)[0] + + if mask == 1: + masking_nonce = self.receive_bytes(4) + masker = util.RepeatedXorMasker(masking_nonce) + else: + masker = _NOOP_MASKER + + bytes = masker.mask(self.receive_bytes(payload_length)) + + return opcode, bytes, fin, rsv1, rsv2, rsv3 + + def send_message(self, message, end=True): + """Send message. + + Args: + message: unicode string to send. + + Raises: + BadOperationException: when called on a server-terminated + connection. + """ + + if self._request.server_terminated: + raise BadOperationException( + 'Requested send_message after sending out a closing handshake') + + self._write(self._writer.build(message, end)) + + def receive_message(self): + """Receive a WebSocket frame and return its payload an unicode string. + + Returns: + payload unicode string in a WebSocket frame. None iff received + closing handshake. + Raises: + BadOperationException: when called on a client-terminated + connection. + ConnectionTerminatedException: when read returns empty + string. + InvalidFrameException: when the frame contains invalid + data. + UnsupportedFrameException: when the received frame has + flags, opcode we cannot handle. You can ignore this exception + and continue receiving the next frame. + """ + + if self._request.client_terminated: + raise BadOperationException( + 'Requested receive_message after receiving a closing handshake') + + while True: + # mp_conn.read will block if no bytes are available. + # Timeout is controlled by TimeOut directive of Apache. + + opcode, bytes, fin, rsv1, rsv2, rsv3 = self._receive_frame() + if rsv1 or rsv2 or rsv3: + raise UnsupportedFrameException( + 'Unsupported flag is set (rsv = %d%d%d)' % + (rsv1, rsv2, rsv3)) + + if opcode == common.OPCODE_CONTINUATION: + if not self._received_fragments: + if fin: + raise InvalidFrameException( + 'Received a termination frame but fragmentation ' + 'not started') + else: + raise InvalidFrameException( + 'Received an intermediate frame but ' + 'fragmentation not started') + + if fin: + # End of fragmentation frame + self._received_fragments.append(bytes) + message = ''.join(self._received_fragments) + self._received_fragments = [] + else: + # Intermediate frame + self._received_fragments.append(bytes) + continue + else: + if self._received_fragments: + if fin: + raise InvalidFrameException( + 'Received an unfragmented frame without ' + 'terminating existing fragmentation') + else: + raise InvalidFrameException( + 'New fragmentation started without terminating ' + 'existing fragmentation') + + if fin: + # Unfragmented frame + self._original_opcode = opcode + message = bytes + + if is_control_opcode(opcode) and len(message) > 125: + raise InvalidFrameException( + 'Application data size of control frames must be ' + '125 bytes or less') + else: + # Start of fragmentation frame + + if is_control_opcode(opcode): + raise InvalidFrameException( + 'Control frames must not be fragmented') + + self._original_opcode = opcode + self._received_fragments.append(bytes) + continue + + if self._original_opcode == common.OPCODE_TEXT: + # The WebSocket protocol section 4.4 specifies that invalid + # characters must be replaced with U+fffd REPLACEMENT + # CHARACTER. + return message.decode('utf-8', 'replace') + elif self._original_opcode == common.OPCODE_CLOSE: + self._request.client_terminated = True + + # Status code is optional. We can have status reason only if we + # have status code. Status reason can be empty string. So, + # allowed cases are + # - no application data: no code no reason + # - 2 octet of application data: has code but no reason + # - 3 or more octet of application data: both code and reason + if len(message) == 1: + raise InvalidFrameException( + 'If a close frame has status code, the length of ' + 'status code must be 2 octet') + elif len(message) >= 2: + self._request.ws_close_code = struct.unpack( + '!H', message[0:2])[0] + self._request.ws_close_reason = message[2:].decode( + 'utf-8', 'replace') + + if self._request.server_terminated: + self._logger.debug( + 'Received ack for server-initiated closing ' + 'handshake') + return None + + self._logger.debug( + 'Received client-initiated closing handshake') + + self._send_closing_handshake(common.STATUS_NORMAL, '') + self._logger.debug( + 'Sent ack for client-initiated closing handshake') + return None + elif self._original_opcode == common.OPCODE_PING: + try: + handler = self._request.on_ping_handler + if handler: + handler(self._request, message) + continue + except AttributeError, e: + pass + self._send_pong(message) + elif self._original_opcode == common.OPCODE_PONG: + # TODO(tyoshino): Add ping timeout handling. + + inflight_pings = deque() + + while True: + try: + expected_body = self._ping_queue.popleft() + if expected_body == message: + # inflight_pings contains pings ignored by the + # other peer. Just forget them. + self._logger.debug( + 'Ping %r is acked (%d pings were ignored)' % + (expected_body, len(inflight_pings))) + break + else: + inflight_pings.append(expected_body) + except IndexError, e: + # The received pong was unsolicited pong. Keep the + # ping queue as is. + self._ping_queue = inflight_pings + self._logger.debug('Received a unsolicited pong') + break + + try: + handler = self._request.on_pong_handler + if handler: + handler(self._request, message) + continue + except AttributeError, e: + pass + + continue + else: + raise UnsupportedFrameException( + 'Opcode %d is not supported' % self._original_opcode) + + def _send_closing_handshake(self, code, reason): + if code >= (1 << 16) or code < 0: + raise BadOperationException('Status code is out of range') + + encoded_reason = reason.encode('utf-8') + if len(encoded_reason) + 2 > 125: + raise BadOperationException( + 'Application data size of close frames must be 125 bytes or ' + 'less') + + frame = create_close_frame( + struct.pack('!H', code) + encoded_reason, self._options.mask_send) + + self._request.server_terminated = True + + self._write(frame) + + def close_connection(self, code=common.STATUS_NORMAL, reason=''): + """Closes a WebSocket connection.""" + + if self._request.server_terminated: + self._logger.debug( + 'Requested close_connection but server is already terminated') + return + + self._send_closing_handshake(code, reason) + self._logger.debug('Sent server-initiated closing handshake') + + if (code == common.STATUS_GOING_AWAY or + code == common.STATUS_PROTOCOL_ERROR): + # It doesn't make sense to wait for a close frame if the reason is + # protocol error or that the server is going away. For some of other + # reasons, it might not make sense to wait for a close frame, but + # it's not clear, yet. + return + + # TODO(ukai): 2. wait until the /client terminated/ flag has been set, + # or until a server-defined timeout expires. + # + # For now, we expect receiving closing handshake right after sending + # out closing handshake. + message = self.receive_message() + if message is not None: + raise ConnectionTerminatedException( + 'Didn\'t receive valid ack for closing handshake') + # TODO: 3. close the WebSocket connection. + # note: mod_python Connection (mp_conn) doesn't have close method. + + def send_ping(self, body=''): + if len(body) > 125: + raise ValueError( + 'Application data size of control frames must be 125 bytes or ' + 'less') + frame = create_ping_frame(body, self._options.mask_send) + self._write(frame) + + self._ping_queue.append(body) + + def _send_pong(self, body): + if len(body) > 125: + raise ValueError( + 'Application data size of control frames must be 125 bytes or ' + 'less') + frame = create_pong_frame(body, self._options.mask_send) + self._write(frame) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/common.py b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py new file mode 100644 index 000000000000..e76b70ecac77 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/common.py @@ -0,0 +1,74 @@ +# 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. + + +# Constants indicating WebSocket protocol version. +VERSION_HYBI07 = 7 +VERSION_HYBI00 = 0 +VERSION_HIXIE75 = -1 + +# Port numbers +DEFAULT_WEB_SOCKET_PORT = 80 +DEFAULT_WEB_SOCKET_SECURE_PORT = 443 + +# Schemes +WEB_SOCKET_SCHEME = 'ws' +WEB_SOCKET_SECURE_SCHEME = 'wss' + +# Frame opcodes defined in the spec. +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xa + +# UUIDs used by HyBi 07 opening handshake and frame masking. +WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +# Opening handshake header names and expected values. +UPGRADE_HEADER = 'Upgrade' +WEBSOCKET_UPGRADE_TYPE = 'websocket' +WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket' +CONNECTION_HEADER = 'Connection' +UPGRADE_CONNECTION_TYPE = 'Upgrade' +HOST_HEADER = 'Host' +SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin' +SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key' +SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept' +SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version' +SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol' +SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions' + +# Status codes +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED = 1003 +STATUS_TOO_LARGE = 1004 diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py index 42c48fd8dea8..4a13194853db 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py @@ -1,4 +1,4 @@ -# Copyright 2009, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,13 +28,15 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Dispatch Web Socket request. +"""Dispatch WebSocket request. """ +import logging import os import re +from mod_pywebsocket import common from mod_pywebsocket import msgutil from mod_pywebsocket import util @@ -46,7 +48,7 @@ _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' class DispatchError(Exception): - """Exception in dispatching Web Socket request.""" + """Exception in dispatching WebSocket request.""" pass @@ -63,15 +65,18 @@ def _normalize_path(path): """ path = path.replace('\\', os.path.sep) - #path = os.path.realpath(path) + # do not normalize away symlinks in mochitest + # path = os.path.realpath(path) path = path.replace('\\', '/') return path -def _path_to_resource_converter(base_dir): +def _create_path_to_resource_converter(base_dir): base_dir = _normalize_path(base_dir) + base_len = len(base_dir) suffix_len = len(_SOURCE_SUFFIX) + def converter(path): if not path.endswith(_SOURCE_SUFFIX): return None @@ -79,11 +84,14 @@ def _path_to_resource_converter(base_dir): if not path.startswith(base_dir): return None return path[base_len:-suffix_len] + return converter -def _source_file_paths(directory): - """Yield Web Socket Handler source file names in the given directory.""" +def _enumerate_handler_file_paths(directory): + """Returns a generator that enumerates WebSocket Handler source file names + in the given directory. + """ for root, unused_dirs, files in os.walk(directory): for base in files: @@ -92,20 +100,38 @@ def _source_file_paths(directory): yield path -def _source(source_str): - """Source a handler definition string.""" +class _HandlerSuite(object): + """A handler suite holder class.""" + + def __init__(self, do_extra_handshake, transfer_data): + self.do_extra_handshake = do_extra_handshake + self.transfer_data = transfer_data + + +def _source_handler_file(handler_definition): + """Source a handler definition string. + + Args: + handler_definition: a string containing Python statements that define + handler functions. + """ global_dic = {} try: - exec source_str in global_dic + exec handler_definition in global_dic except Exception: raise DispatchError('Error in sourcing handler:' + util.get_stack_trace()) - return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), - _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME)) + return _HandlerSuite( + _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), + _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME)) def _extract_handler(dic, name): + """Extracts a callable with the specified name from the given dictionary + dic. + """ + if name not in dic: raise DispatchError('%s is not defined.' % name) handler = dic[name] @@ -115,7 +141,7 @@ def _extract_handler(dic, name): class Dispatcher(object): - """Dispatches Web Socket requests. + """Dispatches WebSocket requests. This class maintains a map from resource name to handlers. """ @@ -133,7 +159,9 @@ class Dispatcher(object): scan time when root_dir contains many subdirectories. """ - self._handlers = {} + self._logger = util.get_class_logger(self) + + self._handler_suite_map = {} self._source_warnings = [] if scan_dir is None: scan_dir = root_dir @@ -141,7 +169,7 @@ class Dispatcher(object): os.path.realpath(root_dir)): raise DispatchError('scan_dir:%s must be a directory under ' 'root_dir:%s.' % (scan_dir, root_dir)) - self._source_files_in_dir(root_dir, scan_dir) + self._source_handler_files_in_dir(root_dir, scan_dir) def add_resource_path_alias(self, alias_resource_path, existing_resource_path): @@ -155,8 +183,8 @@ class Dispatcher(object): existing_resource_path: existing resource path """ try: - handler = self._handlers[existing_resource_path] - self._handlers[alias_resource_path] = handler + handler_suite = self._handler_suite_map[existing_resource_path] + self._handler_suite_map[alias_resource_path] = handler_suite except KeyError: raise DispatchError('No handler for: %r' % existing_resource_path) @@ -166,7 +194,7 @@ class Dispatcher(object): return self._source_warnings def do_extra_handshake(self, request): - """Do extra checking in Web Socket handshake. + """Do extra checking in WebSocket handshake. Select a handler based on request.uri and call its web_socket_do_extra_handshake function. @@ -175,7 +203,8 @@ class Dispatcher(object): request: mod_python request. """ - do_extra_handshake_, unused_transfer_data = self._handler(request) + do_extra_handshake_ = self._get_handler_suite( + request).do_extra_handshake try: do_extra_handshake_(request) except Exception, e: @@ -187,7 +216,7 @@ class Dispatcher(object): raise def transfer_data(self, request): - """Let a handler transfer_data with a Web Socket client. + """Let a handler transfer_data with a WebSocket client. Select a handler based on request.ws_resource and call its web_socket_transfer_data function. @@ -196,50 +225,58 @@ class Dispatcher(object): request: mod_python request. """ - unused_do_extra_handshake, transfer_data_ = self._handler(request) + transfer_data_ = self._get_handler_suite(request).transfer_data + # TODO(tyoshino): Terminate underlying TCP connection if possible. try: - try: - request.client_terminated = False - request.server_terminated = False - transfer_data_(request) - except msgutil.ConnectionTerminatedException, e: - util.prepend_message_to_exception( - 'client initiated closing handshake for %s: ' % ( - request.ws_resource), - e) - raise - except Exception, e: - print 'exception: %s' % type(e) - util.prepend_message_to_exception( - '%s raised exception for %s: ' % ( + transfer_data_(request) + if not request.server_terminated: + request.ws_stream.close_connection() + # Catch non-critical exceptions the handler didn't handle. + except msgutil.BadOperationException, e: + self._logger.debug(str(e)) + request.ws_stream.close_connection(common.STATUS_GOING_AWAY) + except msgutil.InvalidFrameException, e: + # InvalidFrameException must be caught before + # ConnectionTerminatedException that catches InvalidFrameException. + self._logger.debug(str(e)) + request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) + except msgutil.UnsupportedFrameException, e: + self._logger.debug(str(e)) + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED) + except msgutil.ConnectionTerminatedException, e: + self._logger.debug(str(e)) + except Exception, e: + util.prepend_message_to_exception( + '%s raised exception for %s: ' % ( _TRANSFER_DATA_HANDLER_NAME, request.ws_resource), - e) - raise - finally: - msgutil.close_connection(request) + e) + raise + def _get_handler_suite(self, request): + """Retrieves two handlers (one for extra handshake processing, and one + for data transfer) for the given request as a HandlerSuite object. + """ - def _handler(self, request): try: ws_resource_path = request.ws_resource.split('?', 1)[0] - return self._handlers[ws_resource_path] + return self._handler_suite_map[ws_resource_path] except KeyError: raise DispatchError('No handler for: %r' % request.ws_resource) - def _source_files_in_dir(self, root_dir, scan_dir): + def _source_handler_files_in_dir(self, root_dir, scan_dir): """Source all the handler source files in the scan_dir directory. The resource path is determined relative to root_dir. """ - to_resource = _path_to_resource_converter(root_dir) - for path in _source_file_paths(scan_dir): + convert = _create_path_to_resource_converter(root_dir) + for path in _enumerate_handler_file_paths(scan_dir): try: - handlers = _source(open(path).read()) + handler_suite = _source_handler_file(open(path).read()) except DispatchError, e: self._source_warnings.append('%s: %s' % (path, e)) continue - self._handlers[to_resource(path)] = handlers + self._handler_suite_map[convert(path)] = handler_suite # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py index 237dbde6d886..2e6758ee5f76 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2010, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,30 +28,23 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Web Socket handshaking. - -Note: request.connection.write/read are used in this module, even though -mod_python document says that they should be used only in connection handlers. -Unfortunately, we have no other options. For example, request.write/read are -not suitable because they don't allow direct raw bytes writing/reading. +"""WebSocket opening handshake processor. This class try to apply available +opening handshake processors for each protocol version until a connection is +successfully established. """ import logging -import re +from mod_pywebsocket import util from mod_pywebsocket.handshake import draft75 -from mod_pywebsocket.handshake import handshake -from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_PORT -from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_SECURE_PORT -from mod_pywebsocket.handshake._base import WEB_SOCKET_SCHEME -from mod_pywebsocket.handshake._base import WEB_SOCKET_SECURE_SCHEME +from mod_pywebsocket.handshake import hybi00 +from mod_pywebsocket.handshake import hybi06 from mod_pywebsocket.handshake._base import HandshakeError -from mod_pywebsocket.handshake._base import validate_protocol class Handshaker(object): - """This class performs Web Socket handshake.""" + """This class performs WebSocket handshake.""" def __init__(self, request, dispatcher, allowDraft75=False, strict=False): """Construct an instance. @@ -68,28 +61,38 @@ class Handshaker(object): handshake. """ - self._logger = logging.getLogger("mod_pywebsocket.handshake") + self._logger = util.get_class_logger(self) + self._request = request self._dispatcher = dispatcher self._strict = strict - self._handshaker = handshake.Handshaker(request, dispatcher) - self._fallbackHandshaker = None + self._hybi07Handshaker = hybi06.Handshaker(request, dispatcher) + self._hybi00Handshaker = hybi00.Handshaker(request, dispatcher) + self._hixie75Handshaker = None if allowDraft75: - self._fallbackHandshaker = draft75.Handshaker( + self._hixie75Handshaker = draft75.Handshaker( request, dispatcher, strict) def do_handshake(self): - """Perform Web Socket Handshake.""" + """Perform WebSocket Handshake.""" - try: - self._handshaker.do_handshake() - except HandshakeError, e: - self._logger.error('Handshake error: %s' % e) - if self._fallbackHandshaker: - self._logger.warning('fallback to old protocol') - self._fallbackHandshaker.do_handshake() - return - raise e + self._logger.debug( + 'Opening handshake headers: %s' % self._request.headers_in) + handshakers = [ + ('HyBi 07', self._hybi07Handshaker), + ('HyBi 00', self._hybi00Handshaker), + ('Hixie 75', self._hixie75Handshaker)] + last_error = HandshakeError('No handshaker available') + for name, handshaker in handshakers: + if handshaker: + self._logger.info('Trying %s protocol' % name) + try: + handshaker.do_handshake() + return + except HandshakeError, e: + self._logger.info('%s handshake failed: %s' % (name, e)) + last_error = e + raise last_error # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py index 6232471de3ce..94104008604f 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py @@ -1,4 +1,4 @@ -# Copyright 2010, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,48 +28,51 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Web Socket handshaking. - -Note: request.connection.write/read are used in this module, even though -mod_python document says that they should be used only in connection handlers. -Unfortunately, we have no other options. For example, request.write/read are -not suitable because they don't allow direct raw bytes writing/reading. +"""Common functions and exceptions used by WebSocket opening handshake +processors. """ -DEFAULT_WEB_SOCKET_PORT = 80 -DEFAULT_WEB_SOCKET_SECURE_PORT = 443 -WEB_SOCKET_SCHEME = 'ws' -WEB_SOCKET_SECURE_SCHEME = 'wss' +from mod_pywebsocket import common class HandshakeError(Exception): - """Exception in Web Socket Handshake.""" - + """This exception will be raised when an error occurred while processing + WebSocket initial handshake. + """ pass -def default_port(is_secure): +def get_default_port(is_secure): if is_secure: - return DEFAULT_WEB_SOCKET_SECURE_PORT + return common.DEFAULT_WEB_SOCKET_SECURE_PORT else: - return DEFAULT_WEB_SOCKET_PORT + return common.DEFAULT_WEB_SOCKET_PORT -def validate_protocol(protocol): - """Validate WebSocket-Protocol string.""" +# TODO(tyoshino): Have stricter validator for HyBi 07. +def validate_subprotocol(subprotocol): + """Validate a value in subprotocol fields such as WebSocket-Protocol, + Sec-WebSocket-Protocol. - if not protocol: - raise HandshakeError('Invalid WebSocket-Protocol: empty') - for c in protocol: + See + - HyBi 06: Section 5.2.2. + - HyBi 00: Section 4.1. Opening handshake + - Hixie 75: Section 4.1. Handshake + """ + + if not subprotocol: + raise HandshakeError('Invalid subprotocol name: empty') + for c in subprotocol: if not 0x20 <= ord(c) <= 0x7e: - raise HandshakeError('Illegal character in protocol: %r' % c) + raise HandshakeError( + 'Illegal character in subprotocol name: %r' % c) def parse_host_header(request): fields = request.headers_in['Host'].split(':', 1) if len(fields) == 1: - return fields[0], default_port(request.is_https()) + return fields[0], get_default_port(request.is_https()) try: return fields[0], int(fields[1]) except ValueError, e: @@ -80,9 +83,9 @@ def build_location(request): """Build WebSocket location for request.""" location_parts = [] if request.is_https(): - location_parts.append(WEB_SOCKET_SECURE_SCHEME) + location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) else: - location_parts.append(WEB_SOCKET_SCHEME) + location_parts.append(common.WEB_SOCKET_SCHEME) location_parts.append('://') host, port = parse_host_header(request) connection_port = request.connection.local_addr[1] @@ -90,12 +93,34 @@ def build_location(request): raise HandshakeError('Header/connection port mismatch: %d/%d' % (port, connection_port)) location_parts.append(host) - if (port != default_port(request.is_https())): + if (port != get_default_port(request.is_https())): location_parts.append(':') location_parts.append(str(port)) location_parts.append(request.uri) return ''.join(location_parts) +def get_mandatory_header(request, key, expected_value=None): + value = request.headers_in.get(key) + if value is None: + raise HandshakeError('Header %s is not defined' % key) + if expected_value is not None and expected_value != value: + raise HandshakeError( + 'Illegal value for header %s: %s (expected: %s)' % + (key, value, expected_value)) + return value + + +def check_header_lines(request, mandatory_headers): + # 5.1 1. The three character UTF-8 string "GET". + # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). + if request.method != 'GET': + raise HandshakeError('Method is not GET') + # The expected field names, and the meaning of their corresponding + # values, are as follows. + # |Upgrade| and |Connection| + for key, expected_value in mandatory_headers: + get_mandatory_header(request, key, expected_value) + # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/draft75.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/draft75.py index b132dbb1c39a..e56def7f4683 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/draft75.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/draft75.py @@ -1,4 +1,4 @@ -# Copyright 2010, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,20 +28,24 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Web Socket handshaking defined in draft-hixie-thewebsocketprotocol-75. - -Note: request.connection.write/read are used in this module, even though -mod_python document says that they should be used only in connection handlers. -Unfortunately, we have no other options. For example, request.write/read are -not suitable because they don't allow direct raw bytes writing/reading. -""" +"""WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75.""" +# Note: request.connection.write is used in this module, even though mod_python +# document says that it should be used only in connection handlers. +# Unfortunately, we have no other options. For example, request.write is not +# suitable because it doesn't allow direct raw bytes writing. + + +import logging import re +from mod_pywebsocket import common +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket import util from mod_pywebsocket.handshake._base import HandshakeError from mod_pywebsocket.handshake._base import build_location -from mod_pywebsocket.handshake._base import validate_protocol +from mod_pywebsocket.handshake._base import validate_subprotocol _MANDATORY_HEADERS = [ @@ -70,7 +74,7 @@ _SIXTH_AND_LATER = re.compile( class Handshaker(object): - """This class performs Web Socket handshake.""" + """This class performs WebSocket handshake.""" def __init__(self, request, dispatcher, strict=False): """Construct an instance. @@ -86,18 +90,28 @@ class Handshaker(object): handshake. """ + self._logger = util.get_class_logger(self) + self._request = request self._dispatcher = dispatcher self._strict = strict def do_handshake(self): - """Perform Web Socket Handshake.""" + """Perform WebSocket Handshake. + + On _request, we set + ws_resource, ws_origin, ws_location, ws_protocol + ws_challenge_md5: WebSocket handshake information. + ws_stream: Frame generation/parsing class. + ws_version: Protocol version. + """ self._check_header_lines() self._set_resource() self._set_origin() self._set_location() - self._set_protocol() + self._set_subprotocol() + self._set_protocol_version() self._dispatcher.do_extra_handshake(self._request) self._send_handshake() @@ -110,28 +124,30 @@ class Handshaker(object): def _set_location(self): self._request.ws_location = build_location(self._request) - def _set_protocol(self): - protocol = self._request.headers_in.get('WebSocket-Protocol') - if protocol is not None: - validate_protocol(protocol) - self._request.ws_protocol = protocol + def _set_subprotocol(self): + subprotocol = self._request.headers_in.get('WebSocket-Protocol') + if subprotocol is not None: + validate_subprotocol(subprotocol) + self._request.ws_protocol = subprotocol + + def _set_protocol_version(self): + self._logger.debug('IETF Hixie 75 protocol') + self._request.ws_version = common.VERSION_HIXIE75 + self._request.ws_stream = StreamHixie75(self._request) + + def _sendall(self, data): + self._request.connection.write(data) def _send_handshake(self): - self._request.connection.write( - 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n') - self._request.connection.write('Upgrade: WebSocket\r\n') - self._request.connection.write('Connection: Upgrade\r\n') - self._request.connection.write('WebSocket-Origin: ') - self._request.connection.write(self._request.ws_origin) - self._request.connection.write('\r\n') - self._request.connection.write('WebSocket-Location: ') - self._request.connection.write(self._request.ws_location) - self._request.connection.write('\r\n') + self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n') + self._sendall('Upgrade: WebSocket\r\n') + self._sendall('Connection: Upgrade\r\n') + self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin) + self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location) if self._request.ws_protocol: - self._request.connection.write('WebSocket-Protocol: ') - self._request.connection.write(self._request.ws_protocol) - self._request.connection.write('\r\n') - self._request.connection.write('\r\n') + self._sendall( + 'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol) + self._sendall('\r\n') def _check_header_lines(self): for key, expected_value in _MANDATORY_HEADERS: @@ -140,8 +156,9 @@ class Handshaker(object): raise HandshakeError('Header %s is not defined' % key) if expected_value: if actual_value != expected_value: - raise HandshakeError('Illegal value for header %s: %s' % - (key, actual_value)) + raise HandshakeError( + 'Expected %r for header %s but found %r' % + (expected_value, key, actual_value)) if self._strict: try: lines = self._request.connection.get_memorized_lines() diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/handshake.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/handshake.py deleted file mode 100644 index 8348bcb30582..000000000000 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/handshake.py +++ /dev/null @@ -1,208 +0,0 @@ -# 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. - - -"""Web Socket handshaking. - -Note: request.connection.write/read are used in this module, even though -mod_python document says that they should be used only in connection handlers. -Unfortunately, we have no other options. For example, request.write/read are -not suitable because they don't allow direct raw bytes writing/reading. -""" - - -import logging -from md5 import md5 -import re -import struct - -from mod_pywebsocket.handshake._base import HandshakeError -from mod_pywebsocket.handshake._base import build_location -from mod_pywebsocket.handshake._base import validate_protocol - - -_MANDATORY_HEADERS = [ - # key, expected value or None - ['Upgrade', 'WebSocket'], - ['Connection', 'Upgrade'], -] - -def _hexify(s): - return re.sub('.', lambda x: '%02x ' % ord(x.group(0)), s) - -class Handshaker(object): - """This class performs Web Socket handshake.""" - - def __init__(self, request, dispatcher): - """Construct an instance. - - Args: - request: mod_python request. - dispatcher: Dispatcher (dispatch.Dispatcher). - - Handshaker will add attributes such as ws_resource in performing - handshake. - """ - - self._logger = logging.getLogger("mod_pywebsocket.handshake") - self._request = request - self._dispatcher = dispatcher - - def do_handshake(self): - """Perform Web Socket Handshake.""" - - # 5.1 Reading the client's opening handshake. - # dispatcher sets it in self._request. - self._check_header_lines() - self._set_resource() - self._set_protocol() - self._set_location() - self._set_origin() - self._set_challenge_response() - self._dispatcher.do_extra_handshake(self._request) - self._send_handshake() - - def _check_header_lines(self): - # 5.1 1. The three character UTF-8 string "GET". - # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). - if self._request.method != 'GET': - raise HandshakeError('Method is not GET') - # The expected field names, and the meaning of their corresponding - # values, are as follows. - # |Upgrade| and |Connection| - for key, expected_value in _MANDATORY_HEADERS: - actual_value = self._request.headers_in.get(key) - if not actual_value: - raise HandshakeError('Header %s is not defined' % key) - if expected_value: - if actual_value != expected_value: - raise HandshakeError('Illegal value for header %s: %s' % - (key, actual_value)) - - def _set_resource(self): - self._request.ws_resource = self._request.uri - - def _set_protocol(self): - # |Sec-WebSocket-Protocol| - protocol = self._request.headers_in.get('Sec-WebSocket-Protocol') - if protocol is not None: - validate_protocol(protocol) - self._request.ws_protocol = protocol - - def _set_location(self): - # |Host| - host = self._request.headers_in.get('Host') - if host is not None: - self._request.ws_location = build_location(self._request) - # TODO(ukai): check host is this host. - - def _set_origin(self): - # |Origin| - origin = self._request.headers_in['Origin'] - if origin is not None: - self._request.ws_origin = origin - - def _set_challenge_response(self): - # 5.2 4-8. - self._request.ws_challenge = self._get_challenge() - # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ - self._request.ws_challenge_md5 = md5( - self._request.ws_challenge).digest() - self._logger.debug("challenge: %s" % _hexify( - self._request.ws_challenge)) - self._logger.debug("response: %s" % _hexify( - self._request.ws_challenge_md5)) - - def _get_key_value(self, key_field): - key_value = self._request.headers_in.get(key_field) - if key_value is None: - self._logger.debug("no %s" % key_value) - return None - try: - # 5.2 4. let /key-number_n/ be the digits (characters in the range - # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, - # interpreted as a base ten integer, ignoring all other characters - # in /key_n/ - key_number = int(re.sub("\\D", "", key_value)) - # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters - # in /key_n/. - spaces = re.subn(" ", "", key_value)[1] - # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ - # then abort the WebSocket connection. - if key_number % spaces != 0: - raise handshakeError('key_number %d is not an integral ' - 'multiple of spaces %d' % (key_number, - spaces)) - # 5.2 7. let /part_n/ be /key_number_n/ divided by /spaces_n/. - part = key_number / spaces - self._logger.debug("%s: %s => %d / %d => %d" % ( - key_field, key_value, key_number, spaces, part)) - return part - except: - return None - - def _get_challenge(self): - # 5.2 4-7. - key1 = self._get_key_value('Sec-Websocket-Key1') - if not key1: - raise HandshakeError('Sec-WebSocket-Key1 not found') - key2 = self._get_key_value('Sec-Websocket-Key2') - if not key2: - raise HandshakeError('Sec-WebSocket-Key2 not found') - # 5.2 8. let /challenge/ be the concatenation of /part_1/, - challenge = "" - challenge += struct.pack("!I", key1) # network byteorder int - challenge += struct.pack("!I", key2) # network byteorder int - challenge += self._request.connection.read(8) - return challenge - - def _send_handshake(self): - # 5.2 10. send the following line. - self._request.connection.write( - 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n') - # 5.2 11. send the following fields to the client. - self._request.connection.write('Upgrade: WebSocket\r\n') - self._request.connection.write('Connection: Upgrade\r\n') - self._request.connection.write('Sec-WebSocket-Location: ') - self._request.connection.write(self._request.ws_location) - self._request.connection.write('\r\n') - self._request.connection.write('Sec-WebSocket-Origin: ') - self._request.connection.write(self._request.ws_origin) - self._request.connection.write('\r\n') - if self._request.ws_protocol: - self._request.connection.write('Sec-WebSocket-Protocol: ') - self._request.connection.write(self._request.ws_protocol) - self._request.connection.write('\r\n') - # 5.2 12. send two bytes 0x0D 0x0A. - self._request.connection.write('\r\n') - # 5.2 13. send /response/ - self._request.connection.write(self._request.ws_challenge_md5) - - -# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py new file mode 100644 index 000000000000..ca8f56cb8b04 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi00.py @@ -0,0 +1,232 @@ +# 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. + + +"""WebSocket initial handshake hander for HyBi 00 protocol.""" + + +# Note: request.connection.write/read are used in this module, even though +# mod_python document says that they should be used only in connection +# handlers. Unfortunately, we have no other options. For example, +# request.write/read are not suitable because they don't allow direct raw bytes +# writing/reading. + + +import logging +import re +import struct + +from mod_pywebsocket import common +from mod_pywebsocket.stream import StreamHixie75 +from mod_pywebsocket import util +from mod_pywebsocket.handshake._base import HandshakeError +from mod_pywebsocket.handshake._base import build_location +from mod_pywebsocket.handshake._base import check_header_lines +from mod_pywebsocket.handshake._base import get_mandatory_header +from mod_pywebsocket.handshake._base import validate_subprotocol + + +_MANDATORY_HEADERS = [ + # key, expected value or None + [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], + [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], +] + + +class Handshaker(object): + """This class performs WebSocket handshake.""" + + def __init__(self, request, dispatcher): + """Construct an instance. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource in performing + handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def do_handshake(self): + """Perform WebSocket Handshake. + + On _request, we set + ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, + ws_challenge_md5: WebSocket handshake information. + ws_stream: Frame generation/parsing class. + ws_version: Protocol version. + """ + + # 5.1 Reading the client's opening handshake. + # dispatcher sets it in self._request. + check_header_lines(self._request, _MANDATORY_HEADERS) + self._set_resource() + self._set_subprotocol() + self._set_location() + self._set_origin() + self._set_challenge_response() + self._set_protocol_version() + self._dispatcher.do_extra_handshake(self._request) + self._send_handshake() + + def _set_resource(self): + self._request.ws_resource = self._request.uri + + def _set_subprotocol(self): + # |Sec-WebSocket-Protocol| + subprotocol = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + if subprotocol is not None: + validate_subprotocol(subprotocol) + self._request.ws_protocol = subprotocol + + def _set_location(self): + # |Host| + host = self._request.headers_in.get(common.HOST_HEADER) + if host is not None: + self._request.ws_location = build_location(self._request) + # TODO(ukai): check host is this host. + + def _set_origin(self): + # |Origin| + origin = self._request.headers_in['Origin'] + if origin is not None: + self._request.ws_origin = origin + + def _set_protocol_version(self): + # |Sec-WebSocket-Draft| + draft = self._request.headers_in.get('Sec-WebSocket-Draft') + if draft is not None: + try: + draft_int = int(draft) + + # Draft value 2 is used by HyBi 02 and 03 which we no longer + # support. draft >= 3 and <= 1 are never defined in the spec. + # 0 might be used to mean HyBi 00 by somebody. 1 might be used + # to mean HyBi 01 by somebody but we no longer support it. + + if draft_int == 1 or draft_int == 2: + raise HandshakeError('HyBi 01-03 are not supported') + elif draft_int != 0: + raise ValueError + except ValueError, e: + raise HandshakeError( + 'Illegal value for Sec-WebSocket-Draft: %s' % draft) + + self._logger.debug('IETF HyBi 00 protocol') + self._request.ws_version = common.VERSION_HYBI00 + self._request.ws_stream = StreamHixie75(self._request, True) + + def _set_challenge_response(self): + # 5.2 4-8. + self._request.ws_challenge = self._get_challenge() + # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ + self._request.ws_challenge_md5 = util.md5_hash( + self._request.ws_challenge).digest() + self._logger.debug( + 'Challenge: %r (%s)' % + (self._request.ws_challenge, + util.hexify(self._request.ws_challenge))) + self._logger.debug( + 'Challenge response: %r (%s)' % + (self._request.ws_challenge_md5, + util.hexify(self._request.ws_challenge_md5))) + + def _get_key_value(self, key_field): + key_value = get_mandatory_header(self._request, key_field) + + # 5.2 4. let /key-number_n/ be the digits (characters in the range + # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, + # interpreted as a base ten integer, ignoring all other characters + # in /key_n/. + try: + key_number = int(re.sub("\\D", "", key_value)) + except: + raise HandshakeError('%s field contains no digit' % key_field) + # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters + # in /key_n/. + spaces = re.subn(" ", "", key_value)[1] + if spaces == 0: + raise HandshakeError('%s field contains no space' % key_field) + # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ + # then abort the WebSocket connection. + if key_number % spaces != 0: + raise HandshakeError('Key-number %d is not an integral ' + 'multiple of spaces %d' % (key_number, + spaces)) + # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. + part = key_number / spaces + self._logger.debug('%s: %s => %d / %d => %d' % ( + key_field, key_value, key_number, spaces, part)) + return part + + def _get_challenge(self): + # 5.2 4-7. + key1 = self._get_key_value('Sec-WebSocket-Key1') + key2 = self._get_key_value('Sec-WebSocket-Key2') + # 5.2 8. let /challenge/ be the concatenation of /part_1/, + challenge = '' + challenge += struct.pack('!I', key1) # network byteorder int + challenge += struct.pack('!I', key2) # network byteorder int + challenge += self._request.connection.read(8) + return challenge + + def _sendall(self, data): + self._request.connection.write(data) + + def _send_header(self, name, value): + self._sendall('%s: %s\r\n' % (name, value)) + + def _send_handshake(self): + # 5.2 10. send the following line. + self._sendall('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') + # 5.2 11. send the following fields to the client. + self._send_header( + common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75) + self._send_header( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE) + self._send_header('Sec-WebSocket-Location', self._request.ws_location) + self._send_header( + common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin) + if self._request.ws_protocol: + self._send_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol) + # 5.2 12. send two bytes 0x0D 0x0A. + self._sendall('\r\n') + # 5.2 13. send /response/ + self._sendall(self._request.ws_challenge_md5) + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi06.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi06.py new file mode 100755 index 000000000000..449fcc79cd9a --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi06.py @@ -0,0 +1,250 @@ +# 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. + + +"""WebSocket HyBi 07 opening handshake processor.""" + + +# Note: request.connection.write is used in this module, even though mod_python +# document says that it should be used only in connection handlers. +# Unfortunately, we have no other options. For example, request.write is not +# suitable because it doesn't allow direct raw bytes writing. + + +import base64 +import logging +import os +import re + +from mod_pywebsocket import common +from mod_pywebsocket.stream import Stream +from mod_pywebsocket.stream import StreamOptions +from mod_pywebsocket import util +from mod_pywebsocket.handshake._base import HandshakeError +from mod_pywebsocket.handshake._base import check_header_lines +from mod_pywebsocket.handshake._base import get_mandatory_header + + +_MANDATORY_HEADERS = [ + # key, expected value or None + [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE], + [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], +] + +_BASE64_REGEX = re.compile('^[+/0-9A-Za-z]*=*$') + + +def compute_accept(key): + """Computes value for the Sec-WebSocket-Accept header from value of the + Sec-WebSocket-Key header. + """ + + accept_binary = util.sha1_hash( + key + common.WEBSOCKET_ACCEPT_UUID).digest() + accept = base64.b64encode(accept_binary) + + return (accept, accept_binary) + + +class Handshaker(object): + """This class performs WebSocket handshake.""" + + def __init__(self, request, dispatcher): + """Construct an instance. + + Args: + request: mod_python request. + dispatcher: Dispatcher (dispatch.Dispatcher). + + Handshaker will add attributes such as ws_resource during handshake. + """ + + self._logger = util.get_class_logger(self) + + self._request = request + self._dispatcher = dispatcher + + def do_handshake(self): + check_header_lines(self._request, _MANDATORY_HEADERS) + self._request.ws_resource = self._request.uri + + unused_host = get_mandatory_header(self._request, common.HOST_HEADER) + + self._get_origin() + self._check_version() + self._set_protocol() + self._set_extensions() + + key = self._get_key() + (accept, accept_binary) = compute_accept(key) + self._logger.debug('Sec-WebSocket-Accept: %r (%s)' % + (accept, util.hexify(accept_binary))) + + self._logger.debug('IETF HyBi 07 protocol') + self._request.ws_version = common.VERSION_HYBI07 + stream_options = StreamOptions() + stream_options.deflate = self._request.ws_deflate + self._request.ws_stream = Stream(self._request, stream_options) + + self._request.ws_close_code = None + self._request.ws_close_reason = None + + self._dispatcher.do_extra_handshake(self._request) + + if self._request.ws_requested_protocols is not None: + if self._request.ws_protocol is None: + raise HandshakeError( + 'do_extra_handshake must choose one subprotocol from ' + 'ws_requested_protocols and set it to ws_protocol') + + # TODO(tyoshino): Validate selected subprotocol value. + + self._logger.debug( + 'Subprotocol accepted: %r', + self._request.ws_protocol) + else: + if self._request.ws_protocol is not None: + raise HandshakeError( + 'ws_protocol must be None when the client didn\'t request ' + 'any subprotocol') + + self._send_handshake(accept) + + def _get_origin(self): + origin = self._request.headers_in.get( + common.SEC_WEBSOCKET_ORIGIN_HEADER) + self._request.ws_origin = origin + + def _check_version(self): + unused_value = get_mandatory_header( + self._request, common.SEC_WEBSOCKET_VERSION_HEADER, '7') + + def _set_protocol(self): + self._request.ws_protocol = None + + protocol_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_PROTOCOL_HEADER) + + if not protocol_header: + self._request.ws_requested_protocols = None + return + + # TODO(tyoshino): Validate the header value. + + requested_protocols = protocol_header.split(',') + self._request.ws_requested_protocols = [ + s.strip() for s in requested_protocols] + + self._logger.debug('Subprotocols requested: %r', requested_protocols) + + def _set_extensions(self): + self._request.ws_deflate = False + + extensions_header = self._request.headers_in.get( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER) + if not extensions_header: + self._request.ws_extensions = None + return + + self._request.ws_extensions = [] + + requested_extensions = extensions_header.split(',') + # TODO(tyoshino): Follow the ABNF in the spec. + requested_extensions = [s.strip() for s in requested_extensions] + + for extension in requested_extensions: + # We now support only deflate-stream extension. Any other + # extension requests are just ignored for now. + if extension == 'deflate-stream': + self._request.ws_extensions.append(extension) + self._request.ws_deflate = True + + self._logger.debug('Extensions requested: %r', requested_extensions) + self._logger.debug( + 'Extensions accepted: %r', self._request.ws_extensions) + + def _validate_key(self, key): + # Validate + key_is_valid = False + try: + # Validate key by quick regex match before parsing by base64 + # module. Because base64 module skips invalid characters, we have + # to do this in advance to make this server strictly reject illegal + # keys. + if _BASE64_REGEX.match(key): + decoded_key = base64.b64decode(key) + if len(decoded_key) == 16: + key_is_valid = True + except TypeError, e: + pass + + if not key_is_valid: + raise HandshakeError( + 'Illegal value for header %s: %r' % + (common.SEC_WEBSOCKET_KEY_HEADER, key)) + + return decoded_key + + def _get_key(self): + key = get_mandatory_header( + self._request, common.SEC_WEBSOCKET_KEY_HEADER) + + decoded_key = self._validate_key(key) + + self._logger.debug('Sec-WebSocket-Key: %r (%s)' % + (key, util.hexify(decoded_key))) + + return key + + def _sendall(self, data): + self._request.connection.write(data) + + def _send_header(self, name, value): + self._sendall('%s: %s\r\n' % (name, value)) + + def _send_handshake(self, accept): + self._sendall('HTTP/1.1 101 Switching Protocols\r\n') + self._send_header(common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE) + self._send_header( + common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE) + self._send_header(common.SEC_WEBSOCKET_ACCEPT_HEADER, accept) + # TODO(tyoshino): Encode value of protocol and extensions if any + # special character that we have to encode by some manner. + if self._request.ws_protocol is not None: + self._send_header( + common.SEC_WEBSOCKET_PROTOCOL_HEADER, + self._request.ws_protocol) + if self._request.ws_extensions is not None: + self._send_header( + common.SEC_WEBSOCKET_EXTENSIONS_HEADER, + ', '.join(self._request.ws_extensions)) + self._sendall('\r\n') + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py index a62a448b4198..1b12869a6fc3 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/headerparserhandler.py @@ -1,4 +1,4 @@ -# Copyright 2009, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -31,9 +31,10 @@ """PythonHeaderParserHandler for mod_pywebsocket. Apache HTTP Server and mod_python must be configured such that this -function is called to handle Web Socket request. +function is called to handle WebSocket request. """ + import logging from mod_python import apache @@ -57,7 +58,8 @@ _PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75' class ApacheLogHandler(logging.Handler): - """Wrapper logging.Handler to emit log message to apache's error.log""" + """Wrapper logging.Handler to emit log message to apache's error.log.""" + _LEVELS = { logging.DEBUG: apache.APLOG_DEBUG, logging.INFO: apache.APLOG_INFO, @@ -65,11 +67,12 @@ class ApacheLogHandler(logging.Handler): logging.ERROR: apache.APLOG_ERR, logging.CRITICAL: apache.APLOG_CRIT, } + def __init__(self, request=None): logging.Handler.__init__(self) self.log_error = apache.log_error if request is not None: - self.log_error = request.log_error + self.log_error = request.log_error def emit(self, record): apache_level = apache.APLOG_DEBUG @@ -78,7 +81,7 @@ class ApacheLogHandler(logging.Handler): self.log_error(record.getMessage(), apache_level) -logging.getLogger("mod_pywebsocket").addHandler(ApacheLogHandler()) +logging.getLogger('mod_pywebsocket').addHandler(ApacheLogHandler()) def _create_dispatcher(): @@ -111,12 +114,13 @@ def headerparserhandler(request): try: allowDraft75 = apache.main_server.get_options().get( - _PYOPT_ALLOW_DRAFT75, None) + _PYOPT_ALLOW_DRAFT75, None) handshaker = handshake.Handshaker(request, _dispatcher, allowDraft75=allowDraft75) handshaker.do_handshake() - request.log_error('mod_pywebsocket: resource: %r' % request.ws_resource, - apache.APLOG_DEBUG) + request.log_error( + 'mod_pywebsocket: resource: %r' % request.ws_resource, + apache.APLOG_DEBUG) try: _dispatcher.transfer_data(request) except Exception, e: @@ -132,6 +136,8 @@ def headerparserhandler(request): except dispatch.DispatchError, e: request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING) return apache.DECLINED + # Set assbackwards to suppress response header generation by Apache. + request.assbackwards = 1 return apache.DONE # Return DONE such that no other handlers are invoked. diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py b/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py index 2f8a54e456d6..9c188a5c9d82 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/memorizingfile.py @@ -53,7 +53,7 @@ class MemorizingFile(object): file_: the file object to wrap. max_memorized_lines: the maximum number of lines to memorize. Only the first max_memorized_lines are memorized. - Default: sys.maxint. + Default: sys.maxint. """ self._file = file_ self._memorized_lines = [] diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py b/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py index 7aae2479e4ce..4ea1e94bc970 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py @@ -1,4 +1,4 @@ -# Copyright 2009, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -40,153 +40,52 @@ not suitable because they don't allow direct raw bytes writing/reading. import Queue import threading -from mod_pywebsocket import util -from time import time,sleep + +# Export Exception symbols from msgutil for backward compatibility +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import UnsupportedFrameException -class MsgUtilException(Exception): - pass - - -class ConnectionTerminatedException(MsgUtilException): - pass - - -def _read(request, length): - bytes = request.connection.read(length) - if not bytes: - raise MsgUtilException( - 'Failed to receive message from %r' % - (request.connection.remote_addr,)) - return bytes - - -def _write(request, bytes): - try: - request.connection.write(bytes) - except Exception, e: - util.prepend_message_to_exception( - 'Failed to send message to %r: ' % - (request.connection.remote_addr,), - e) - raise - - -def close_connection(request, abort=False): +# An API for handler to send/receive WebSocket messages. +def close_connection(request): """Close connection. Args: request: mod_python request. """ - if request.server_terminated: - return - # 5.3 the server may decide to terminate the WebSocket connection by - # running through the following steps: - # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the start - # of the closing handshake. - got_exception = False - if not abort: - _write(request, '\xff\x00') - # timeout of 20 seconds to get the client's close frame ack - initial_time = time() - end_time = initial_time + 20 - while time() < end_time: - try: - receive_message(request) - except ConnectionTerminatedException, e: - got_exception = True - sleep(1) - request.server_terminated = True - if got_exception: - util.prepend_message_to_exception( - 'client initiated closing handshake for %s: ' % ( - request.ws_resource), - e) - raise ConnectionTerminatedException - # TODO: 3. close the WebSocket connection. - # note: mod_python Connection (mp_conn) doesn't have close method. + request.ws_stream.close_connection() -def send_message(request, message): +def send_message(request, message, end=True): """Send message. Args: request: mod_python request. message: unicode string to send. + end: False to send message as a fragment. All messages until the first + call with end=True (inclusive) will be delivered to the client + in separate frames but as one WebSocket message. Raises: - ConnectionTerminatedException: when server already terminated. + BadOperationException: when server already terminated. """ - if request.server_terminated: - raise ConnectionTerminatedException - _write(request, '\x00' + message.encode('utf-8') + '\xff') + request.ws_stream.send_message(message, end) def receive_message(request): - """Receive a Web Socket frame and return its payload as unicode string. + """Receive a WebSocket frame and return its payload as unicode string. Args: request: mod_python request. Raises: - ConnectionTerminatedException: when client already terminated. + BadOperationException: when client already terminated. """ - - if request.client_terminated: - raise ConnectionTerminatedException - while True: - # Read 1 byte. - # mp_conn.read will block if no bytes are available. - # Timeout is controlled by TimeOut directive of Apache. - frame_type_str = _read(request, 1) - frame_type = ord(frame_type_str[0]) - if (frame_type & 0x80) == 0x80: - # The payload length is specified in the frame. - # Read and discard. - length = _payload_length(request) - _receive_bytes(request, length) - # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the - # /client terminated/ flag and abort these steps. - if frame_type == 0xFF and length == 0: - request.client_terminated = True - raise ConnectionTerminatedException - else: - # The payload is delimited with \xff. - bytes = _read_until(request, '\xff') - # The Web Socket protocol section 4.4 specifies that invalid - # characters must be replaced with U+fffd REPLACEMENT CHARACTER. - message = bytes.decode('utf-8', 'replace') - if frame_type == 0x00: - return message - # Discard data of other types. + return request.ws_stream.receive_message() -def _payload_length(request): - length = 0 - while True: - b_str = _read(request, 1) - b = ord(b_str[0]) - length = length * 128 + (b & 0x7f) - if (b & 0x80) == 0: - break - return length - - -def _receive_bytes(request, length): - bytes = [] - while length > 0: - new_bytes = _read(request, length) - bytes.append(new_bytes) - length -= len(new_bytes) - return ''.join(bytes) - - -def _read_until(request, delim_char): - bytes = [] - while True: - ch = _read(request, 1) - if ch == delim_char: - break - bytes.append(ch) - return ''.join(bytes) +def send_ping(request, body=''): + request.ws_stream.send_ping(body) class MessageReceiver(threading.Thread): @@ -199,6 +98,7 @@ class MessageReceiver(threading.Thread): because pyOpenSSL used by the server raises a fatal error if the socket is accessed from multiple threads. """ + def __init__(self, request, onmessage=None): """Construct an instance. @@ -210,6 +110,7 @@ class MessageReceiver(threading.Thread): and MessageReceiver.receive_nowait are useless because they will never return any messages. """ + threading.Thread.__init__(self) self._request = request self._queue = Queue.Queue() @@ -269,6 +170,7 @@ class MessageSender(threading.Thread): because pyOpenSSL used by the server raises a fatal error if the socket is accessed from multiple threads. """ + def __init__(self, request): """Construct an instance. diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/standalone.py b/testing/mochitest/pywebsocket/mod_pywebsocket/standalone.py new file mode 100755 index 000000000000..38dddf27be25 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/standalone.py @@ -0,0 +1,476 @@ +#!/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. + + +"""Standalone WebSocket server. + +Use this server to run mod_pywebsocket without Apache HTTP Server. + +Usage: + python standalone.py [-p ] [-w ] + [-s ] + [-d ] + [-m ] + ... for other options, see _main below ... + + is the port number to use for ws:// connection. + + is the path to the root directory of HTML files. + + is the path to the root directory of WebSocket handlers. +See __init__.py for details of and how to write WebSocket +handlers. If this path is relative, is used as the base. + + is a path under the root directory. If specified, only the handlers +under scan_dir are scanned. This is useful in saving scan time. + +Note: +This server is derived from SocketServer.ThreadingMixIn. Hence a thread is +used for each request. + +SECURITY WARNING: This uses CGIHTTPServer and CGIHTTPServer is not secure. +It may execute arbitrary Python code or external programs. It should not be +used outside a firewall. +""" + +import BaseHTTPServer +import CGIHTTPServer +import SimpleHTTPServer +import SocketServer +import logging +import logging.handlers +import optparse +import os +import re +import socket +import sys + +_HAS_OPEN_SSL = False +try: + import OpenSSL.SSL + _HAS_OPEN_SSL = True +except ImportError: + pass + +from mod_pywebsocket import common +from mod_pywebsocket import dispatch +from mod_pywebsocket import handshake +from mod_pywebsocket import memorizingfile +from mod_pywebsocket import util + + +_DEFAULT_LOG_MAX_BYTES = 1024 * 256 +_DEFAULT_LOG_BACKUP_COUNT = 5 + +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 + + +def _print_warnings_if_any(dispatcher): + warnings = dispatcher.source_warnings() + if warnings: + for warning in warnings: + logging.warning('mod_pywebsocket: %s' % warning) + + +class _StandaloneConnection(object): + """Mimic mod_python mp_conn.""" + + def __init__(self, request_handler): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + self._request_handler = request_handler + + def get_local_addr(self): + """Getter to mimic mp_conn.local_addr.""" + return (self._request_handler.server.server_name, + self._request_handler.server.server_port) + local_addr = property(get_local_addr) + + def get_remote_addr(self): + """Getter to mimic mp_conn.remote_addr. + + Setting the property in __init__ won't work because the request + handler is not initialized yet there.""" + return self._request_handler.client_address + remote_addr = property(get_remote_addr) + + def write(self, data): + """Mimic mp_conn.write().""" + return self._request_handler.wfile.write(data) + + def read(self, length): + """Mimic mp_conn.read().""" + return self._request_handler.rfile.read(length) + + def get_memorized_lines(self): + """Get memorized lines.""" + return self._request_handler.rfile.get_memorized_lines() + + +class _StandaloneRequest(object): + """Mimic mod_python request.""" + + def __init__(self, request_handler, use_tls): + """Construct an instance. + + Args: + request_handler: A WebSocketRequestHandler instance. + """ + self._request_handler = request_handler + self.connection = _StandaloneConnection(request_handler) + self._use_tls = use_tls + + def get_uri(self): + """Getter to mimic request.uri.""" + return self._request_handler.path + uri = property(get_uri) + + def get_method(self): + """Getter to mimic request.method.""" + return self._request_handler.command + method = property(get_method) + + def get_headers_in(self): + """Getter to mimic request.headers_in.""" + return self._request_handler.headers + headers_in = property(get_headers_in) + + def is_https(self): + """Mimic request.is_https().""" + return self._use_tls + + +class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + """HTTPServer specialized for WebSocket.""" + + SocketServer.ThreadingMixIn.daemon_threads = True + SocketServer.TCPServer.allow_reuse_address = True + + def __init__(self, server_address, RequestHandlerClass): + """Override SocketServer.BaseServer.__init__.""" + + SocketServer.BaseServer.__init__( + self, server_address, RequestHandlerClass) + self.socket = self._create_socket() + self.server_bind() + self.server_activate() + + def _create_socket(self): + socket_ = socket.socket(self.address_family, self.socket_type) + if WebSocketServer.options.use_tls: + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + ctx.use_privatekey_file(WebSocketServer.options.private_key) + ctx.use_certificate_file(WebSocketServer.options.certificate) + socket_ = OpenSSL.SSL.Connection(ctx, socket_) + return socket_ + + def handle_error(self, rquest, client_address): + """Override SocketServer.handle_error.""" + + logging.error( + ('Exception in processing request from: %r' % (client_address,)) + + '\n' + util.get_stack_trace()) + # Note: client_address is a tuple. To match it against %r, we need the + # trailing comma. + + +class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): + """CGIHTTPRequestHandler specialized for WebSocket.""" + + def setup(self): + """Override SocketServer.StreamRequestHandler.setup to wrap rfile with + MemorizingFile. + """ + + # Call superclass's setup to prepare rfile, wfile, etc. See setup + # definition on the root class SocketServer.StreamRequestHandler to + # understand what this does. + CGIHTTPServer.CGIHTTPRequestHandler.setup(self) + + self.rfile = memorizingfile.MemorizingFile( + self.rfile, + max_memorized_lines=_MAX_MEMORIZED_LINES) + + def __init__(self, *args, **keywords): + self._request = _StandaloneRequest( + self, WebSocketRequestHandler.options.use_tls) + self._dispatcher = WebSocketRequestHandler.options.dispatcher + self._print_warnings_if_any() + self._handshaker = handshake.Handshaker( + self._request, self._dispatcher, + allowDraft75=WebSocketRequestHandler.options.allow_draft75, + strict=WebSocketRequestHandler.options.strict) + CGIHTTPServer.CGIHTTPRequestHandler.__init__( + self, *args, **keywords) + + def _print_warnings_if_any(self): + warnings = self._dispatcher.source_warnings() + if warnings: + for warning in warnings: + logging.warning('mod_pywebsocket: %s' % warning) + + def parse_request(self): + """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request. + + Return True to continue processing for HTTP(S), False otherwise. + """ + result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self) + if result: + try: + self._handshaker.do_handshake() + try: + self._dispatcher.transfer_data(self._request) + except Exception, e: + # Catch exception in transfer_data. + # In this case, handshake has been successful, so just log + # the exception and return False. + logging.info('mod_pywebsocket: %s' % e) + logging.info('mod_pywebsocket: %s' % util.get_stack_trace()) + return False + except handshake.HandshakeError, e: + # Handshake for ws(s) failed. Assume http(s). + logging.info('mod_pywebsocket: %s' % e) + return True + except dispatch.DispatchError, e: + logging.warning('mod_pywebsocket: %s' % e) + return False + except Exception, e: + logging.warning('mod_pywebsocket: %s' % e) + logging.warning('mod_pywebsocket: %s' % util.get_stack_trace()) + return False + return result + + def log_request(self, code='-', size='-'): + """Override BaseHTTPServer.log_request.""" + + logging.info('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, *args): + """Override BaseHTTPServer.log_error.""" + + # Despite the name, this method is for warnings than for errors. + # For example, HTTP status code is logged by this method. + logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:]))) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Add extra check that self.path doesn't contains .. + Also check if the file is a executable file or not. + If the file is not executable, it is handled as static file or dir + rather than a CGI script. + """ + if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self): + if '..' in self.path: + return False + # strip query parameter from request path + resource_name = self.path.split('?', 2)[0] + # convert resource_name into real path name in filesystem. + scriptfile = self.translate_path(resource_name) + if not os.path.isfile(scriptfile): + return False + if not self.is_executable(scriptfile): + return False + return True + return False + + +def _configure_logging(options): + logger = logging.getLogger() + logger.setLevel(logging.getLevelName(options.log_level.upper())) + if options.log_file: + handler = logging.handlers.RotatingFileHandler( + options.log_file, 'a', options.log_max, options.log_count) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + +def _alias_handlers(dispatcher, websock_handlers_map_file): + """Set aliases specified in websock_handler_map_file in dispatcher. + + Args: + dispatcher: dispatch.Dispatcher instance + websock_handler_map_file: alias map file + """ + fp = open(websock_handlers_map_file) + try: + for line in fp: + if line[0] == '#' or line.isspace(): + continue + m = re.match('(\S+)\s+(\S+)', line) + if not m: + logging.warning('Wrong format in map file:' + line) + continue + try: + dispatcher.add_resource_path_alias( + m.group(1), m.group(2)) + except dispatch.DispatchError, e: + logging.error(str(e)) + finally: + fp.close() + + +def _main(): + parser = optparse.OptionParser() + parser.add_option('-H', '--server-host', '--server_host', + dest='server_host', + default='', + help='server hostname to listen to') + parser.add_option('-p', '--port', dest='port', type='int', + default=common.DEFAULT_WEB_SOCKET_PORT, + help='port to listen to') + parser.add_option('-w', '--websock-handlers', '--websock_handlers', + dest='websock_handlers', + default='.', + help='WebSocket handlers root directory.') + parser.add_option('-m', '--websock-handlers-map-file', + '--websock_handlers_map_file', + dest='websock_handlers_map_file', + default=None, + help=('WebSocket handlers map file. ' + 'Each line consists of alias_resource_path and ' + 'existing_resource_path, separated by spaces.')) + parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', + default=None, + help=('WebSocket handlers scan directory. ' + 'Must be a directory under websock_handlers.')) + parser.add_option('-d', '--document-root', '--document_root', + dest='document_root', default='.', + help='Document root directory.') + parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths', + default=None, + help=('CGI paths relative to document_root.' + 'Comma-separated. (e.g -x /cgi,/htbin) ' + 'Files under document_root/cgi_path are handled ' + 'as CGI programs. Must be executable.')) + parser.add_option('-t', '--tls', dest='use_tls', action='store_true', + default=False, help='use TLS (wss://)') + parser.add_option('-k', '--private-key', '--private_key', + dest='private_key', + default='', help='TLS private key file.') + parser.add_option('-c', '--certificate', dest='certificate', + default='', help='TLS certificate file.') + parser.add_option('-l', '--log-file', '--log_file', dest='log_file', + default='', help='Log file.') + parser.add_option('--log-level', '--log_level', type='choice', + dest='log_level', default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level.') + parser.add_option('--log-max', '--log_max', dest='log_max', type='int', + default=_DEFAULT_LOG_MAX_BYTES, + help='Log maximum bytes') + parser.add_option('--log-count', '--log_count', dest='log_count', + type='int', default=_DEFAULT_LOG_BACKUP_COUNT, + help='Log backup count') + parser.add_option('--allow-draft75', dest='allow_draft75', + action='store_true', default=False, + help='Allow draft 75 handshake') + parser.add_option('--strict', dest='strict', action='store_true', + default=False, help='Strictly check handshake request') + parser.add_option('-q', '--queue', dest='request_queue_size', type='int', + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') + options = parser.parse_args()[0] + + os.chdir(options.document_root) + + _configure_logging(options) + + SocketServer.TCPServer.request_queue_size = options.request_queue_size + CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = [] + + if options.cgi_paths: + CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \ + options.cgi_paths.split(',') + if sys.platform in ('cygwin', 'win32'): + cygwin_path = None + # For Win32 Python, it is expected that CYGWIN_PATH + # is set to a directory of cygwin binaries. + # For example, websocket_server.py in Chromium sets CYGWIN_PATH to + # full path of third_party/cygwin/bin. + if 'CYGWIN_PATH' in os.environ: + cygwin_path = os.environ['CYGWIN_PATH'] + util.wrap_popen3_for_win(cygwin_path) + def __check_script(scriptpath): + return util.get_script_interp(scriptpath, cygwin_path) + CGIHTTPServer.executable = __check_script + + if options.use_tls: + if not _HAS_OPEN_SSL: + logging.critical('To use TLS, install pyOpenSSL.') + sys.exit(1) + if not options.private_key or not options.certificate: + logging.critical( + 'To use TLS, specify private_key and certificate.') + sys.exit(1) + + if not options.scan_dir: + options.scan_dir = options.websock_handlers + + try: + # Share a Dispatcher among request handlers to save time for + # instantiation. Dispatcher can be shared because it is thread-safe. + options.dispatcher = dispatch.Dispatcher(options.websock_handlers, + options.scan_dir) + if options.websock_handlers_map_file: + _alias_handlers(options.dispatcher, + options.websock_handlers_map_file) + _print_warnings_if_any(options.dispatcher) + + WebSocketRequestHandler.options = options + WebSocketServer.options = options + + server = WebSocketServer((options.server_host, options.port), + WebSocketRequestHandler) + server.serve_forever() + except Exception, e: + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) + sys.exit(1) + + +if __name__ == '__main__': + _main() + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py b/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py new file mode 100644 index 000000000000..93c4025ff971 --- /dev/null +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/stream.py @@ -0,0 +1,53 @@ +# Copyright 2010, 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. + + +"""This file exports public symbols. +""" + + +from mod_pywebsocket._stream_base import BadOperationException +from mod_pywebsocket._stream_base import ConnectionTerminatedException +from mod_pywebsocket._stream_base import InvalidFrameException +from mod_pywebsocket._stream_base import UnsupportedFrameException +from mod_pywebsocket._stream_hixie75 import StreamHixie75 +from mod_pywebsocket._stream_hybi06 import Stream +from mod_pywebsocket._stream_hybi06 import StreamOptions + +# These methods are intended to be used by WebSocket client developers to have +# their implementations receive broken data in tests. +from mod_pywebsocket._stream_hybi06 import create_close_frame +from mod_pywebsocket._stream_hybi06 import create_header +from mod_pywebsocket._stream_hybi06 import create_length_header +from mod_pywebsocket._stream_hybi06 import create_ping_frame +from mod_pywebsocket._stream_hybi06 import create_pong_frame +from mod_pywebsocket._stream_hybi06 import create_text_frame + + +# vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/util.py b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py index 8ec9dca0ab45..6b3bed6db8ab 100644 --- a/testing/mochitest/pywebsocket/mod_pywebsocket/util.py +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/util.py @@ -28,14 +28,31 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Web Sockets utilities. +"""WebSocket utilities. """ +import array + +# Import hash classes from a module available and recommended for each Python +# version and re-export those symbol. Use sha and md5 module in Python 2.4, and +# hashlib module in Python 2.6. +try: + import hashlib + md5_hash = hashlib.md5 + sha1_hash = hashlib.sha1 +except ImportError: + import md5 + import sha + md5_hash = md5.md5 + sha1_hash = sha.sha + import StringIO +import logging import os import re import traceback +import zlib def get_stack_trace(): @@ -72,7 +89,7 @@ def __translate_interp(interp, cygwin_path): """ if not cygwin_path: return interp - m = re.match("^[^ ]*/([^ ]+)( .*)?", interp) + m = re.match('^[^ ]*/([^ ]+)( .*)?', interp) if m: cmd = os.path.join(cygwin_path, m.group(1)) return cmd + m.group(2) @@ -96,7 +113,7 @@ def get_script_interp(script_path, cygwin_path=None): fp = open(script_path) line = fp.readline() fp.close() - m = re.match("^#!(.*)", line) + m = re.match('^#!(.*)', line) if m: return __translate_interp(m.group(1), cygwin_path) return None @@ -113,9 +130,200 @@ def wrap_popen3_for_win(cygwin_path): cmdline = cmd.split(' ') interp = get_script_interp(cmdline[0], cygwin_path) if interp: - cmd = interp + " " + cmd + cmd = interp + ' ' + cmd return __orig_popen3(cmd, mode, bufsize) os.popen3 = __wrap_popen3 +def hexify(s): + return ' '.join(map(lambda x: '%02x' % ord(x), s)) + + +def get_class_logger(o): + return logging.getLogger( + '%s.%s' % (o.__class__.__module__, o.__class__.__name__)) + + +class NoopMasker(object): + def __init__(self): + pass + + def mask(self, s): + return s + + +class RepeatedXorMasker(object): + """A masking object that applies XOR on the string given to mask method + with the masking bytes given to the constructor repeatedly. This object + remembers the position in the masking bytes the last mask method call ended + and resumes from that point on the next mask method call. + """ + + def __init__(self, mask): + self._mask = map(ord, mask) + self._mask_size = len(self._mask) + self._count = 0 + + def mask(self, s): + result = array.array('B') + result.fromstring(s) + for i in xrange(len(result)): + result[i] ^= self._mask[self._count] + self._count = (self._count + 1) % self._mask_size + return result.tostring() + + +class DeflateRequest(object): + """A wrapper class for request object to intercept send and recv to perform + deflate compression and decompression transparently. + """ + + def __init__(self, request): + self._request = request + self.connection = DeflateConnection(request.connection) + + def __getattribute__(self, name): + if name in ('_request', 'connection'): + return object.__getattribute__(self, name) + return self._request.__getattribute__(name) + + def __setattr__(self, name, value): + if name in ('_request', 'connection'): + return object.__setattr__(self, name, value) + return self._request.__setattr__(name, value) + + +# By making wbits option negative, we can suppress CMF/FLG (2 octet) and +# ADLER32 (4 octet) fields of zlib so that we can use zlib module just as +# deflate library. DICTID won't be added as far as we don't set dictionary. +# LZ77 window of 32K will be used for both compression and decompression. +# For decompression, we can just use 32K to cover any windows size. For +# compression, we use 32K so receivers must use 32K. +# +# Compression level is Z_DEFAULT_COMPRESSION. We don't have to match level +# to decode. +# +# See zconf.h, deflate.cc, inflate.cc of zlib library, and zlibmodule.c of +# Python. See also RFC1950 (ZLIB 3.3). +class DeflateSocket(object): + """A wrapper class for socket object to intercept send and recv to perform + deflate compression and decompression transparently. + """ + + # Size of the buffer passed to recv to receive compressed data. + _RECV_SIZE = 4096 + + def __init__(self, socket): + self._socket = socket + + self._logger = logging.getLogger( + 'mod_pywebsocket.util.DeflateSocket') + + self._compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + self._decompress = zlib.decompressobj(-zlib.MAX_WBITS) + self._unconsumed = '' + + def recv(self, size): + # TODO(tyoshino): Allow call with size=0. It should block until any + # decompressed data is available. + if size <= 0: + raise Exception('Non-positive size passed') + data = '' + while True: + data += self._decompress.decompress( + self._unconsumed, size - len(data)) + self._unconsumed = self._decompress.unconsumed_tail + if self._decompress.unused_data: + raise Exception('Non-decompressible data found: %r' % + self._decompress.unused_data) + if len(data) != 0: + break + + read_data = self._socket.recv(DeflateSocket._RECV_SIZE) + self._logger.debug('Received compressed: %r' % read_data) + if not read_data: + break + self._unconsumed += read_data + self._logger.debug('Received: %r' % data) + return data + + def sendall(self, bytes): + self.send(bytes) + + def send(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH) + self._socket.sendall(compressed_bytes) + self._logger.debug('Wrote: %r' % bytes) + self._logger.debug('Wrote compressed: %r' % compressed_bytes) + return len(bytes) + + +class DeflateConnection(object): + """A wrapper class for request object to intercept write and read to + perform deflate compression and decompression transparently. + """ + + # Size of the buffer passed to recv to receive compressed data. + _RECV_SIZE = 4096 + + def __init__(self, connection): + self._connection = connection + + self._logger = logging.getLogger( + 'mod_pywebsocket.util.DeflateConnection') + + self._compress = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS) + self._decompress = zlib.decompressobj(-zlib.MAX_WBITS) + self._unconsumed = '' + + def put_bytes(self, bytes): + self.write(bytes) + + def read(self, size=-1): + # TODO(tyoshino): Allow call with size=0. + if size == 0 or size < -1: + raise Exception('size must be -1 or positive') + + data = '' + while True: + if size < 0: + data += self._decompress.decompress(self._unconsumed) + else: + data += self._decompress.decompress( + self._unconsumed, size - len(data)) + self._unconsumed = self._decompress.unconsumed_tail + if self._decompress.unused_data: + raise Exception('Non-decompressible data found: %r' % + self._decompress.unused_data) + + if size >= 0 and len(data) != 0: + break + + # TODO(tyoshino): Make this read efficient by some workaround. + # + # In 3.0.3 and prior of mod_python, read blocks until length bytes + # was read. We don't know the exact size to read while using + # deflate, so read byte-by-byte. + # + # _StandaloneRequest.read that ultimately performs + # socket._fileobject.read also blocks until length bytes was read + read_data = self._connection.read(1) + self._logger.debug('Read compressed: %r' % read_data) + if not read_data: + break + self._unconsumed += read_data + self._logger.debug('Read: %r' % data) + return data + + def write(self, bytes): + compressed_bytes = self._compress.compress(bytes) + compressed_bytes += self._compress.flush(zlib.Z_SYNC_FLUSH) + self._logger.debug('Wrote compressed: %r' % compressed_bytes) + self._logger.debug('Wrote: %r' % bytes) + self._connection.write(compressed_bytes) + + # vi:sts=4 sw=4 et diff --git a/testing/mochitest/pywebsocket/standalone.py b/testing/mochitest/pywebsocket/standalone.py index c3e68680197a..38dddf27be25 100644 --- a/testing/mochitest/pywebsocket/standalone.py +++ b/testing/mochitest/pywebsocket/standalone.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2009, Google Inc. +# Copyright 2011, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -30,7 +30,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Standalone Web Socket server. +"""Standalone WebSocket server. Use this server to run mod_pywebsocket without Apache HTTP Server. @@ -45,8 +45,8 @@ Usage: is the path to the root directory of HTML files. - is the path to the root directory of Web Socket handlers. -See __init__.py for details of and how to write Web Socket + is the path to the root directory of WebSocket handlers. +See __init__.py for details of and how to write WebSocket handlers. If this path is relative, is used as the base. is a path under the root directory. If specified, only the handlers @@ -80,19 +80,13 @@ try: except ImportError: pass +from mod_pywebsocket import common from mod_pywebsocket import dispatch from mod_pywebsocket import handshake from mod_pywebsocket import memorizingfile from mod_pywebsocket import util -_LOG_LEVELS = { - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warn': logging.WARN, - 'error': logging.ERROR, - 'critical': logging.CRITICAL}; - _DEFAULT_LOG_MAX_BYTES = 1024 * 256 _DEFAULT_LOG_BACKUP_COUNT = 5 @@ -101,6 +95,7 @@ _DEFAULT_REQUEST_QUEUE_SIZE = 128 # 1024 is practically large enough to contain WebSocket handshake lines. _MAX_MEMORIZED_LINES = 1024 + def _print_warnings_if_any(dispatcher): warnings = dispatcher.source_warnings() if warnings: @@ -180,9 +175,10 @@ class _StandaloneRequest(object): class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): - """HTTPServer specialized for Web Socket.""" + """HTTPServer specialized for WebSocket.""" SocketServer.ThreadingMixIn.daemon_threads = True + SocketServer.TCPServer.allow_reuse_address = True def __init__(self, server_address, RequestHandlerClass): """Override SocketServer.BaseServer.__init__.""" @@ -213,16 +209,21 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): - """CGIHTTPRequestHandler specialized for Web Socket.""" + """CGIHTTPRequestHandler specialized for WebSocket.""" def setup(self): - """Override SocketServer.StreamRequestHandler.setup.""" + """Override SocketServer.StreamRequestHandler.setup to wrap rfile with + MemorizingFile. + """ + + # Call superclass's setup to prepare rfile, wfile, etc. See setup + # definition on the root class SocketServer.StreamRequestHandler to + # understand what this does. + CGIHTTPServer.CGIHTTPRequestHandler.setup(self) - self.connection = self.request self.rfile = memorizingfile.MemorizingFile( - socket._fileobject(self.request, 'rb', self.rbufsize), - max_memorized_lines=_MAX_MEMORIZED_LINES) - self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) + self.rfile, + max_memorized_lines=_MAX_MEMORIZED_LINES) def __init__(self, *args, **keywords): self._request = _StandaloneRequest( @@ -258,6 +259,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): # In this case, handshake has been successful, so just log # the exception and return False. logging.info('mod_pywebsocket: %s' % e) + logging.info('mod_pywebsocket: %s' % util.get_stack_trace()) return False except handshake.HandshakeError, e: # Handshake for ws(s) failed. Assume http(s). @@ -268,7 +270,7 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): return False except Exception, e: logging.warning('mod_pywebsocket: %s' % e) - logging.info('mod_pywebsocket: %s' % util.get_stack_trace()) + logging.warning('mod_pywebsocket: %s' % util.get_stack_trace()) return False return result @@ -310,17 +312,18 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): def _configure_logging(options): logger = logging.getLogger() - logger.setLevel(_LOG_LEVELS[options.log_level]) + logger.setLevel(logging.getLevelName(options.log_level.upper())) if options.log_file: handler = logging.handlers.RotatingFileHandler( options.log_file, 'a', options.log_max, options.log_count) else: handler = logging.StreamHandler() formatter = logging.Formatter( - "[%(asctime)s] [%(levelname)s] %(name)s: %(message)s") + '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) + def _alias_handlers(dispatcher, websock_handlers_map_file): """Set aliases specified in websock_handler_map_file in dispatcher. @@ -346,7 +349,6 @@ def _alias_handlers(dispatcher, websock_handlers_map_file): fp.close() - def _main(): parser = optparse.OptionParser() parser.add_option('-H', '--server-host', '--server_host', @@ -354,22 +356,22 @@ def _main(): default='', help='server hostname to listen to') parser.add_option('-p', '--port', dest='port', type='int', - default=handshake.DEFAULT_WEB_SOCKET_PORT, + default=common.DEFAULT_WEB_SOCKET_PORT, help='port to listen to') parser.add_option('-w', '--websock-handlers', '--websock_handlers', dest='websock_handlers', default='.', - help='Web Socket handlers root directory.') + help='WebSocket handlers root directory.') parser.add_option('-m', '--websock-handlers-map-file', '--websock_handlers_map_file', dest='websock_handlers_map_file', default=None, - help=('Web Socket handlers map file. ' + help=('WebSocket handlers map file. ' 'Each line consists of alias_resource_path and ' 'existing_resource_path, separated by spaces.')) parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir', default=None, - help=('Web Socket handlers scan directory. ' + help=('WebSocket handlers scan directory. ' 'Must be a directory under websock_handlers.')) parser.add_option('-d', '--document-root', '--document_root', dest='document_root', default='.', @@ -391,7 +393,8 @@ def _main(): default='', help='Log file.') parser.add_option('--log-level', '--log_level', type='choice', dest='log_level', default='warn', - choices=['debug', 'info', 'warn', 'error', 'critical'], + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical'], help='Log level.') parser.add_option('--log-max', '--log_max', dest='log_max', type='int', default=_DEFAULT_LOG_MAX_BYTES, @@ -461,7 +464,8 @@ def _main(): WebSocketRequestHandler) server.serve_forever() except Exception, e: - logging.critical(str(e)) + logging.critical('mod_pywebsocket: %s' % e) + logging.critical('mod_pywebsocket: %s' % util.get_stack_trace()) sys.exit(1)