michael@0: # Copyright 2011, Google Inc. michael@0: # All rights reserved. michael@0: # michael@0: # Redistribution and use in source and binary forms, with or without michael@0: # modification, are permitted provided that the following conditions are michael@0: # met: michael@0: # michael@0: # * Redistributions of source code must retain the above copyright michael@0: # notice, this list of conditions and the following disclaimer. michael@0: # * Redistributions in binary form must reproduce the above michael@0: # copyright notice, this list of conditions and the following disclaimer michael@0: # in the documentation and/or other materials provided with the michael@0: # distribution. michael@0: # * Neither the name of Google Inc. nor the names of its michael@0: # contributors may be used to endorse or promote products derived from michael@0: # this software without specific prior written permission. michael@0: # michael@0: # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT michael@0: # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, michael@0: # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT michael@0: # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, michael@0: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY michael@0: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT michael@0: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE michael@0: # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: michael@0: michael@0: """This file provides a class for parsing/building frames of the WebSocket michael@0: protocol version HyBi 00 and Hixie 75. michael@0: michael@0: Specification: michael@0: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 michael@0: """ michael@0: michael@0: michael@0: from mod_pywebsocket import common michael@0: from mod_pywebsocket._stream_base import BadOperationException michael@0: from mod_pywebsocket._stream_base import ConnectionTerminatedException michael@0: from mod_pywebsocket._stream_base import InvalidFrameException michael@0: from mod_pywebsocket._stream_base import StreamBase michael@0: from mod_pywebsocket._stream_base import UnsupportedFrameException michael@0: from mod_pywebsocket import util michael@0: michael@0: michael@0: class StreamHixie75(StreamBase): michael@0: """A class for parsing/building frames of the WebSocket protocol version michael@0: HyBi 00 and Hixie 75. michael@0: """ michael@0: michael@0: def __init__(self, request, enable_closing_handshake=False): michael@0: """Construct an instance. michael@0: michael@0: Args: michael@0: request: mod_python request. michael@0: enable_closing_handshake: to let StreamHixie75 perform closing michael@0: handshake as specified in HyBi 00, set michael@0: this option to True. michael@0: """ michael@0: michael@0: StreamBase.__init__(self, request) michael@0: michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self._enable_closing_handshake = enable_closing_handshake michael@0: michael@0: self._request.client_terminated = False michael@0: self._request.server_terminated = False michael@0: michael@0: def send_message(self, message, end=True, binary=False): michael@0: """Send message. michael@0: michael@0: Args: michael@0: message: unicode string to send. michael@0: binary: not used in hixie75. michael@0: michael@0: Raises: michael@0: BadOperationException: when called on a server-terminated michael@0: connection. michael@0: """ michael@0: michael@0: if not end: michael@0: raise BadOperationException( michael@0: 'StreamHixie75 doesn\'t support send_message with end=False') michael@0: michael@0: if binary: michael@0: raise BadOperationException( michael@0: 'StreamHixie75 doesn\'t support send_message with binary=True') michael@0: michael@0: if self._request.server_terminated: michael@0: raise BadOperationException( michael@0: 'Requested send_message after sending out a closing handshake') michael@0: michael@0: self._write(''.join(['\x00', message.encode('utf-8'), '\xff'])) michael@0: michael@0: def _read_payload_length_hixie75(self): michael@0: """Reads a length header in a Hixie75 version frame with length. michael@0: michael@0: Raises: michael@0: ConnectionTerminatedException: when read returns empty string. michael@0: """ michael@0: michael@0: length = 0 michael@0: while True: michael@0: b_str = self._read(1) michael@0: b = ord(b_str) michael@0: length = length * 128 + (b & 0x7f) michael@0: if (b & 0x80) == 0: michael@0: break michael@0: return length michael@0: michael@0: def receive_message(self): michael@0: """Receive a WebSocket frame and return its payload an unicode string. michael@0: michael@0: Returns: michael@0: payload unicode string in a WebSocket frame. michael@0: michael@0: Raises: michael@0: ConnectionTerminatedException: when read returns empty michael@0: string. michael@0: BadOperationException: when called on a client-terminated michael@0: connection. michael@0: """ michael@0: michael@0: if self._request.client_terminated: michael@0: raise BadOperationException( michael@0: 'Requested receive_message after receiving a closing ' michael@0: 'handshake') michael@0: michael@0: while True: michael@0: # Read 1 byte. michael@0: # mp_conn.read will block if no bytes are available. michael@0: # Timeout is controlled by TimeOut directive of Apache. michael@0: frame_type_str = self.receive_bytes(1) michael@0: frame_type = ord(frame_type_str) michael@0: if (frame_type & 0x80) == 0x80: michael@0: # The payload length is specified in the frame. michael@0: # Read and discard. michael@0: length = self._read_payload_length_hixie75() michael@0: if length > 0: michael@0: _ = self.receive_bytes(length) michael@0: # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the michael@0: # /client terminated/ flag and abort these steps. michael@0: if not self._enable_closing_handshake: michael@0: continue michael@0: michael@0: if frame_type == 0xFF and length == 0: michael@0: self._request.client_terminated = True michael@0: michael@0: if self._request.server_terminated: michael@0: self._logger.debug( michael@0: 'Received ack for server-initiated closing ' michael@0: 'handshake') michael@0: return None michael@0: michael@0: self._logger.debug( michael@0: 'Received client-initiated closing handshake') michael@0: michael@0: self._send_closing_handshake() michael@0: self._logger.debug( michael@0: 'Sent ack for client-initiated closing handshake') michael@0: return None michael@0: else: michael@0: # The payload is delimited with \xff. michael@0: bytes = self._read_until('\xff') michael@0: # The WebSocket protocol section 4.4 specifies that invalid michael@0: # characters must be replaced with U+fffd REPLACEMENT michael@0: # CHARACTER. michael@0: message = bytes.decode('utf-8', 'replace') michael@0: if frame_type == 0x00: michael@0: return message michael@0: # Discard data of other types. michael@0: michael@0: def _send_closing_handshake(self): michael@0: if not self._enable_closing_handshake: michael@0: raise BadOperationException( michael@0: 'Closing handshake is not supported in Hixie 75 protocol') michael@0: michael@0: self._request.server_terminated = True michael@0: michael@0: # 5.3 the server may decide to terminate the WebSocket connection by michael@0: # running through the following steps: michael@0: # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the michael@0: # start of the closing handshake. michael@0: self._write('\xff\x00') michael@0: michael@0: def close_connection(self, unused_code='', unused_reason=''): michael@0: """Closes a WebSocket connection. michael@0: michael@0: Raises: michael@0: ConnectionTerminatedException: when closing handshake was michael@0: not successfull. michael@0: """ michael@0: michael@0: if self._request.server_terminated: michael@0: self._logger.debug( michael@0: 'Requested close_connection but server is already terminated') michael@0: return michael@0: michael@0: if not self._enable_closing_handshake: michael@0: self._request.server_terminated = True michael@0: self._logger.debug('Connection closed') michael@0: return michael@0: michael@0: self._send_closing_handshake() michael@0: self._logger.debug('Sent server-initiated closing handshake') michael@0: michael@0: # TODO(ukai): 2. wait until the /client terminated/ flag has been set, michael@0: # or until a server-defined timeout expires. michael@0: # michael@0: # For now, we expect receiving closing handshake right after sending michael@0: # out closing handshake, and if we couldn't receive non-handshake michael@0: # frame, we take it as ConnectionTerminatedException. michael@0: message = self.receive_message() michael@0: if message is not None: michael@0: raise ConnectionTerminatedException( michael@0: 'Didn\'t receive valid ack for closing handshake') michael@0: # TODO: 3. close the WebSocket connection. michael@0: # note: mod_python Connection (mp_conn) doesn't have close method. michael@0: michael@0: def send_ping(self, body): michael@0: raise BadOperationException( michael@0: 'StreamHixie75 doesn\'t support send_ping') michael@0: michael@0: michael@0: # vi:sts=4 sw=4 et