Home | History | Annotate | Download | only in mod_pywebsocket
      1 # Copyright 2011, Google Inc.
      2 # All rights reserved.
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 # notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 # copyright notice, this list of conditions and the following disclaimer
     12 # in the documentation and/or other materials provided with the
     13 # distribution.
     14 #     * Neither the name of Google Inc. nor the names of its
     15 # contributors may be used to endorse or promote products derived from
     16 # this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 
     31 """This file provides a class for parsing/building frames of the WebSocket
     32 protocol version HyBi 00 and Hixie 75.
     33 
     34 Specification:
     35 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
     36 """
     37 
     38 
     39 from mod_pywebsocket import common
     40 from mod_pywebsocket._stream_base import BadOperationException
     41 from mod_pywebsocket._stream_base import ConnectionTerminatedException
     42 from mod_pywebsocket._stream_base import InvalidFrameException
     43 from mod_pywebsocket._stream_base import StreamBase
     44 from mod_pywebsocket._stream_base import UnsupportedFrameException
     45 from mod_pywebsocket import util
     46 
     47 
     48 class StreamHixie75(StreamBase):
     49     """A class for parsing/building frames of the WebSocket protocol version
     50     HyBi 00 and Hixie 75.
     51     """
     52 
     53     def __init__(self, request, enable_closing_handshake=False):
     54         """Construct an instance.
     55 
     56         Args:
     57             request: mod_python request.
     58             enable_closing_handshake: to let StreamHixie75 perform closing
     59                                       handshake as specified in HyBi 00, set
     60                                       this option to True.
     61         """
     62 
     63         StreamBase.__init__(self, request)
     64 
     65         self._logger = util.get_class_logger(self)
     66 
     67         self._enable_closing_handshake = enable_closing_handshake
     68 
     69         self._request.client_terminated = False
     70         self._request.server_terminated = False
     71 
     72     def send_message(self, message, end=True, binary=False):
     73         """Send message.
     74 
     75         Args:
     76             message: unicode string to send.
     77             binary: not used in hixie75.
     78 
     79         Raises:
     80             BadOperationException: when called on a server-terminated
     81                 connection.
     82         """
     83 
     84         if not end:
     85             raise BadOperationException(
     86                 'StreamHixie75 doesn\'t support send_message with end=False')
     87 
     88         if binary:
     89             raise BadOperationException(
     90                 'StreamHixie75 doesn\'t support send_message with binary=True')
     91 
     92         if self._request.server_terminated:
     93             raise BadOperationException(
     94                 'Requested send_message after sending out a closing handshake')
     95 
     96         self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
     97 
     98     def _read_payload_length_hixie75(self):
     99         """Reads a length header in a Hixie75 version frame with length.
    100 
    101         Raises:
    102             ConnectionTerminatedException: when read returns empty string.
    103         """
    104 
    105         length = 0
    106         while True:
    107             b_str = self._read(1)
    108             b = ord(b_str)
    109             length = length * 128 + (b & 0x7f)
    110             if (b & 0x80) == 0:
    111                 break
    112         return length
    113 
    114     def receive_message(self):
    115         """Receive a WebSocket frame and return its payload an unicode string.
    116 
    117         Returns:
    118             payload unicode string in a WebSocket frame.
    119 
    120         Raises:
    121             ConnectionTerminatedException: when read returns empty
    122                 string.
    123             BadOperationException: when called on a client-terminated
    124                 connection.
    125         """
    126 
    127         if self._request.client_terminated:
    128             raise BadOperationException(
    129                 'Requested receive_message after receiving a closing '
    130                 'handshake')
    131 
    132         while True:
    133             # Read 1 byte.
    134             # mp_conn.read will block if no bytes are available.
    135             # Timeout is controlled by TimeOut directive of Apache.
    136             frame_type_str = self.receive_bytes(1)
    137             frame_type = ord(frame_type_str)
    138             if (frame_type & 0x80) == 0x80:
    139                 # The payload length is specified in the frame.
    140                 # Read and discard.
    141                 length = self._read_payload_length_hixie75()
    142                 if length > 0:
    143                     _ = self.receive_bytes(length)
    144                 # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
    145                 # /client terminated/ flag and abort these steps.
    146                 if not self._enable_closing_handshake:
    147                     continue
    148 
    149                 if frame_type == 0xFF and length == 0:
    150                     self._request.client_terminated = True
    151 
    152                     if self._request.server_terminated:
    153                         self._logger.debug(
    154                             'Received ack for server-initiated closing '
    155                             'handshake')
    156                         return None
    157 
    158                     self._logger.debug(
    159                         'Received client-initiated closing handshake')
    160 
    161                     self._send_closing_handshake()
    162                     self._logger.debug(
    163                         'Sent ack for client-initiated closing handshake')
    164                     return None
    165             else:
    166                 # The payload is delimited with \xff.
    167                 bytes = self._read_until('\xff')
    168                 # The WebSocket protocol section 4.4 specifies that invalid
    169                 # characters must be replaced with U+fffd REPLACEMENT
    170                 # CHARACTER.
    171                 message = bytes.decode('utf-8', 'replace')
    172                 if frame_type == 0x00:
    173                     return message
    174                 # Discard data of other types.
    175 
    176     def _send_closing_handshake(self):
    177         if not self._enable_closing_handshake:
    178             raise BadOperationException(
    179                 'Closing handshake is not supported in Hixie 75 protocol')
    180 
    181         self._request.server_terminated = True
    182 
    183         # 5.3 the server may decide to terminate the WebSocket connection by
    184         # running through the following steps:
    185         # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
    186         # start of the closing handshake.
    187         self._write('\xff\x00')
    188 
    189     def close_connection(self, unused_code='', unused_reason=''):
    190         """Closes a WebSocket connection.
    191 
    192         Raises:
    193             ConnectionTerminatedException: when closing handshake was
    194                 not successfull.
    195         """
    196 
    197         if self._request.server_terminated:
    198             self._logger.debug(
    199                 'Requested close_connection but server is already terminated')
    200             return
    201 
    202         if not self._enable_closing_handshake:
    203             self._request.server_terminated = True
    204             self._logger.debug('Connection closed')
    205             return
    206 
    207         self._send_closing_handshake()
    208         self._logger.debug('Sent server-initiated closing handshake')
    209 
    210         # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
    211         # or until a server-defined timeout expires.
    212         #
    213         # For now, we expect receiving closing handshake right after sending
    214         # out closing handshake, and if we couldn't receive non-handshake
    215         # frame, we take it as ConnectionTerminatedException.
    216         message = self.receive_message()
    217         if message is not None:
    218             raise ConnectionTerminatedException(
    219                 'Didn\'t receive valid ack for closing handshake')
    220         # TODO: 3. close the WebSocket connection.
    221         # note: mod_python Connection (mp_conn) doesn't have close method.
    222 
    223     def send_ping(self, body):
    224         raise BadOperationException(
    225             'StreamHixie75 doesn\'t support send_ping')
    226 
    227 
    228 # vi:sts=4 sw=4 et
    229