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 32protocol version HyBi 00 and Hixie 75. 33 34Specification: 35http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 36""" 37 38 39from mod_pywebsocket import common 40from mod_pywebsocket._stream_base import BadOperationException 41from mod_pywebsocket._stream_base import ConnectionTerminatedException 42from mod_pywebsocket._stream_base import InvalidFrameException 43from mod_pywebsocket._stream_base import StreamBase 44from mod_pywebsocket._stream_base import UnsupportedFrameException 45from mod_pywebsocket import util 46 47 48class 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