| # 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. |
| |
| |
| """This file provides a class for parsing/building frames of the WebSocket |
| protocol version HyBi 00 and Hixie 75. |
| |
| Specification: |
| - HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 |
| - Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 |
| """ |
| |
| |
| 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): |
| """A class for parsing/building frames of the WebSocket protocol version |
| HyBi 00 and Hixie 75. |
| """ |
| |
| 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, binary=False): |
| """Send message. |
| |
| Args: |
| message: unicode string to send. |
| binary: not used in hixie75. |
| |
| Raises: |
| BadOperationException: when called on a server-terminated |
| connection. |
| """ |
| |
| if not end: |
| raise BadOperationException( |
| 'StreamHixie75 doesn\'t support send_message with end=False') |
| |
| if binary: |
| raise BadOperationException( |
| 'StreamHixie75 doesn\'t support send_message with binary=True') |
| |
| 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 |