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 - HyBi 00 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
     36 - Hixie 75 http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
     37 """
     38 
     39 
     40 from mod_pywebsocket import common
     41 from mod_pywebsocket._stream_base import BadOperationException
     42 from mod_pywebsocket._stream_base import ConnectionTerminatedException
     43 from mod_pywebsocket._stream_base import InvalidFrameException
     44 from mod_pywebsocket._stream_base import StreamBase
     45 from mod_pywebsocket._stream_base import UnsupportedFrameException
     46 from mod_pywebsocket import util
     47 
     48 
     49 class StreamHixie75(StreamBase):
     50     """A class for parsing/building frames of the WebSocket protocol version
     51     HyBi 00 and Hixie 75.
     52     """
     53 
     54     def __init__(self, request, enable_closing_handshake=False):
     55         """Construct an instance.
     56 
     57         Args:
     58             request: mod_python request.
     59             enable_closing_handshake: to let StreamHixie75 perform closing
     60                                       handshake as specified in HyBi 00, set
     61                                       this option to True.
     62         """
     63 
     64         StreamBase.__init__(self, request)
     65 
     66         self._logger = util.get_class_logger(self)
     67 
     68         self._enable_closing_handshake = enable_closing_handshake
     69 
     70         self._request.client_terminated = False
     71         self._request.server_terminated = False
     72 
     73     def send_message(self, message, end=True, binary=False):
     74         """Send message.
     75 
     76         Args:
     77             message: unicode string to send.
     78             binary: not used in hixie75.
     79 
     80         Raises:
     81             BadOperationException: when called on a server-terminated
     82                 connection.
     83         """
     84 
     85         if not end:
     86             raise BadOperationException(
     87                 'StreamHixie75 doesn\'t support send_message with end=False')
     88 
     89         if binary:
     90             raise BadOperationException(
     91                 'StreamHixie75 doesn\'t support send_message with binary=True')
     92 
     93         if self._request.server_terminated:
     94             raise BadOperationException(
     95                 'Requested send_message after sending out a closing handshake')
     96 
     97         self._write(''.join(['\x00', message.encode('utf-8'), '\xff']))
     98 
     99     def _read_payload_length_hixie75(self):
    100         """Reads a length header in a Hixie75 version frame with length.
    101 
    102         Raises:
    103             ConnectionTerminatedException: when read returns empty string.
    104         """
    105 
    106         length = 0
    107         while True:
    108             b_str = self._read(1)
    109             b = ord(b_str)
    110             length = length * 128 + (b & 0x7f)
    111             if (b & 0x80) == 0:
    112                 break
    113         return length
    114 
    115     def receive_message(self):
    116         """Receive a WebSocket frame and return its payload an unicode string.
    117 
    118         Returns:
    119             payload unicode string in a WebSocket frame.
    120 
    121         Raises:
    122             ConnectionTerminatedException: when read returns empty
    123                 string.
    124             BadOperationException: when called on a client-terminated
    125                 connection.
    126         """
    127 
    128         if self._request.client_terminated:
    129             raise BadOperationException(
    130                 'Requested receive_message after receiving a closing '
    131                 'handshake')
    132 
    133         while True:
    134             # Read 1 byte.
    135             # mp_conn.read will block if no bytes are available.
    136             # Timeout is controlled by TimeOut directive of Apache.
    137             frame_type_str = self.receive_bytes(1)
    138             frame_type = ord(frame_type_str)
    139             if (frame_type & 0x80) == 0x80:
    140                 # The payload length is specified in the frame.
    141                 # Read and discard.
    142                 length = self._read_payload_length_hixie75()
    143                 if length > 0:
    144                     _ = self.receive_bytes(length)
    145                 # 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
    146                 # /client terminated/ flag and abort these steps.
    147                 if not self._enable_closing_handshake:
    148                     continue
    149 
    150                 if frame_type == 0xFF and length == 0:
    151                     self._request.client_terminated = True
    152 
    153                     if self._request.server_terminated:
    154                         self._logger.debug(
    155                             'Received ack for server-initiated closing '
    156                             'handshake')
    157                         return None
    158 
    159                     self._logger.debug(
    160                         'Received client-initiated closing handshake')
    161 
    162                     self._send_closing_handshake()
    163                     self._logger.debug(
    164                         'Sent ack for client-initiated closing handshake')
    165                     return None
    166             else:
    167                 # The payload is delimited with \xff.
    168                 bytes = self._read_until('\xff')
    169                 # The WebSocket protocol section 4.4 specifies that invalid
    170                 # characters must be replaced with U+fffd REPLACEMENT
    171                 # CHARACTER.
    172                 message = bytes.decode('utf-8', 'replace')
    173                 if frame_type == 0x00:
    174                     return message
    175                 # Discard data of other types.
    176 
    177     def _send_closing_handshake(self):
    178         if not self._enable_closing_handshake:
    179             raise BadOperationException(
    180                 'Closing handshake is not supported in Hixie 75 protocol')
    181 
    182         self._request.server_terminated = True
    183 
    184         # 5.3 the server may decide to terminate the WebSocket connection by
    185         # running through the following steps:
    186         # 1. send a 0xFF byte and a 0x00 byte to the client to indicate the
    187         # start of the closing handshake.
    188         self._write('\xff\x00')
    189 
    190     def close_connection(self, unused_code='', unused_reason=''):
    191         """Closes a WebSocket connection.
    192 
    193         Raises:
    194             ConnectionTerminatedException: when closing handshake was
    195                 not successfull.
    196         """
    197 
    198         if self._request.server_terminated:
    199             self._logger.debug(
    200                 'Requested close_connection but server is already terminated')
    201             return
    202 
    203         if not self._enable_closing_handshake:
    204             self._request.server_terminated = True
    205             self._logger.debug('Connection closed')
    206             return
    207 
    208         self._send_closing_handshake()
    209         self._logger.debug('Sent server-initiated closing handshake')
    210 
    211         # TODO(ukai): 2. wait until the /client terminated/ flag has been set,
    212         # or until a server-defined timeout expires.
    213         #
    214         # For now, we expect receiving closing handshake right after sending
    215         # out closing handshake, and if we couldn't receive non-handshake
    216         # frame, we take it as ConnectionTerminatedException.
    217         message = self.receive_message()
    218         if message is not None:
    219             raise ConnectionTerminatedException(
    220                 'Didn\'t receive valid ack for closing handshake')
    221         # TODO: 3. close the WebSocket connection.
    222         # note: mod_python Connection (mp_conn) doesn't have close method.
    223 
    224     def send_ping(self, body):
    225         raise BadOperationException(
    226             'StreamHixie75 doesn\'t support send_ping')
    227 
    228 
    229 # vi:sts=4 sw=4 et
    230