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