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