12da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# Copyright 2011, Google Inc.
22da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# All rights reserved.
32da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#
42da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# Redistribution and use in source and binary forms, with or without
52da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# modification, are permitted provided that the following conditions are
62da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# met:
72da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#
82da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#     * Redistributions of source code must retain the above copyright
92da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# notice, this list of conditions and the following disclaimer.
102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#     * Redistributions in binary form must reproduce the above
112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# copyright notice, this list of conditions and the following disclaimer
122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# in the documentation and/or other materials provided with the
132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# distribution.
142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#     * Neither the name of Google Inc. nor the names of its
152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# contributors may be used to endorse or promote products derived from
162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# this software without specific prior written permission.
172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis#
182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
312da489cd246702bee5938545b18a6f710ed214bcJamie Gennis"""This file provides a class for parsing/building frames of the WebSocket
322da489cd246702bee5938545b18a6f710ed214bcJamie Gennisprotocol version HyBi 00 and Hixie 75.
332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
342da489cd246702bee5938545b18a6f710ed214bcJamie GennisSpecification:
352da489cd246702bee5938545b18a6f710ed214bcJamie Gennishttp://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis"""
372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
382da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
392da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket import common
402da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket._stream_base import BadOperationException
412da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket._stream_base import ConnectionTerminatedException
422da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket._stream_base import InvalidFrameException
432da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket._stream_base import StreamBase
442da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket._stream_base import UnsupportedFrameException
452da489cd246702bee5938545b18a6f710ed214bcJamie Gennisfrom mod_pywebsocket import util
462da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
472da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
482da489cd246702bee5938545b18a6f710ed214bcJamie Gennisclass StreamHixie75(StreamBase):
492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """A class for parsing/building frames of the WebSocket protocol version
502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    HyBi 00 and Hixie 75.
512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    """
522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def __init__(self, request, enable_closing_handshake=False):
542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Construct an instance.
552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
562da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Args:
572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            request: mod_python request.
582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            enable_closing_handshake: to let StreamHixie75 perform closing
592da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                                      handshake as specified in HyBi 00, set
602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                                      this option to True.
612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        StreamBase.__init__(self, request)
642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._logger = util.get_class_logger(self)
662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._enable_closing_handshake = enable_closing_handshake
682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._request.client_terminated = False
702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._request.server_terminated = False
712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
722da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def send_message(self, message, end=True, binary=False):
732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Send message.
742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Args:
762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            message: unicode string to send.
772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            binary: not used in hixie75.
782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Raises:
802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            BadOperationException: when called on a server-terminated
812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                connection.
822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if not end:
852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise BadOperationException(
862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'StreamHixie75 doesn\'t support send_message with end=False')
872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if binary:
892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise BadOperationException(
902da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'StreamHixie75 doesn\'t support send_message with binary=True')
912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
922da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if self._request.server_terminated:
932da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise BadOperationException(
942da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'Requested send_message after sending out a closing handshake')
952da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
962da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
972da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def _read_payload_length_hixie75(self):
992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Reads a length header in a Hixie75 version frame with length.
1002da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1012da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Raises:
1022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            ConnectionTerminatedException: when read returns empty string.
1032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
1042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        length = 0
1062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        while True:
1072da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            b_str = self._read(1)
1082da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            b = ord(b_str)
1092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            length = length * 128 + (b & 0x7f)
1102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            if (b & 0x80) == 0:
1112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                break
1122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        return length
1132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def receive_message(self):
1152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Receive a WebSocket frame and return its payload an unicode string.
1162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Returns:
1182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            payload unicode string in a WebSocket frame.
1192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Raises:
1212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            ConnectionTerminatedException: when read returns empty
1222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                string.
1232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            BadOperationException: when called on a client-terminated
1242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                connection.
1252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
1262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if self._request.client_terminated:
1282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise BadOperationException(
1292da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'Requested receive_message after receiving a closing '
1302da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'handshake')
1312da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1322da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        while True:
1332da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            # Read 1 byte.
1342da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            # mp_conn.read will block if no bytes are available.
1352da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            # Timeout is controlled by TimeOut directive of Apache.
1362da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            frame_type_str = self.receive_bytes(1)
1372da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            frame_type = ord(frame_type_str)
1382da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            if (frame_type & 0x80) == 0x80:
1392da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # The payload length is specified in the frame.
1402da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # Read and discard.
1412da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                length = self._read_payload_length_hixie75()
1422da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                if length > 0:
1432da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    _ = self.receive_bytes(length)
1442da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
1452da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # /client terminated/ flag and abort these steps.
1462da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                if not self._enable_closing_handshake:
1472da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    continue
1482da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1492da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                if frame_type == 0xFF and length == 0:
1502da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    self._request.client_terminated = True
1512da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1522da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    if self._request.server_terminated:
1532da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                        self._logger.debug(
1542da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                            'Received ack for server-initiated closing '
1552da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                            'handshake')
1562da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                        return None
1572da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1582da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    self._logger.debug(
1592da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                        'Received client-initiated closing handshake')
1602da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1612da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    self._send_closing_handshake()
1622da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    self._logger.debug(
1632da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                        'Sent ack for client-initiated closing handshake')
1642da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    return None
1652da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            else:
1662da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # The payload is delimited with \xff.
1672da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                bytes = self._read_until('\xff')
1682da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # The WebSocket protocol section 4.4 specifies that invalid
1692da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # characters must be replaced with U+fffd REPLACEMENT
1702da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # CHARACTER.
1712da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                message = bytes.decode('utf-8', 'replace')
1722da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                if frame_type == 0x00:
1732da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                    return message
1742da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                # Discard data of other types.
1752da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1762da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def _send_closing_handshake(self):
1772da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if not self._enable_closing_handshake:
1782da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise BadOperationException(
1792da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'Closing handshake is not supported in Hixie 75 protocol')
1802da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1812da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._request.server_terminated = True
1822da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1832da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # 5.3 the server may decide to terminate the WebSocket connection by
1842da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # running through the following steps:
1852da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
1862da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # start of the closing handshake.
1872da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._write('\xff\x00')
1882da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1892da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def close_connection(self, unused_code='', unused_reason=''):
1902da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """Closes a WebSocket connection.
1912da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1922da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        Raises:
1932da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            ConnectionTerminatedException: when closing handshake was
1942da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                not successfull.
1952da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        """
1962da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
1972da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if self._request.server_terminated:
1982da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            self._logger.debug(
1992da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'Requested close_connection but server is already terminated')
2002da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            return
2012da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2022da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if not self._enable_closing_handshake:
2032da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            self._request.server_terminated = True
2042da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            self._logger.debug('Connection closed')
2052da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            return
2062da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2072da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._send_closing_handshake()
2082da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        self._logger.debug('Sent server-initiated closing handshake')
2092da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2102da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
2112da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # or until a server-defined timeout expires.
2122da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        #
2132da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # For now, we expect receiving closing handshake right after sending
2142da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # out closing handshake, and if we couldn't receive non-handshake
2152da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # frame, we take it as ConnectionTerminatedException.
2162da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        message = self.receive_message()
2172da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        if message is not None:
2182da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            raise ConnectionTerminatedException(
2192da489cd246702bee5938545b18a6f710ed214bcJamie Gennis                'Didn\'t receive valid ack for closing handshake')
2202da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # TODO: 3. close the WebSocket connection.
2212da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        # note: mod_python Connection (mp_conn) doesn't have close method.
2222da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2232da489cd246702bee5938545b18a6f710ed214bcJamie Gennis    def send_ping(self, body):
2242da489cd246702bee5938545b18a6f710ed214bcJamie Gennis        raise BadOperationException(
2252da489cd246702bee5938545b18a6f710ed214bcJamie Gennis            'StreamHixie75 doesn\'t support send_ping')
2262da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2272da489cd246702bee5938545b18a6f710ed214bcJamie Gennis
2282da489cd246702bee5938545b18a6f710ed214bcJamie Gennis# vi:sts=4 sw=4 et
229