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 the opening handshake processor for the WebSocket 32 protocol version HyBi 00. 33 34 Specification: 35 http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 36 """ 37 38 39 # Note: request.connection.write/read are used in this module, even though 40 # mod_python document says that they should be used only in connection 41 # handlers. Unfortunately, we have no other options. For example, 42 # request.write/read are not suitable because they don't allow direct raw bytes 43 # writing/reading. 44 45 46 import logging 47 import re 48 import struct 49 50 from mod_pywebsocket import common 51 from mod_pywebsocket.stream import StreamHixie75 52 from mod_pywebsocket import util 53 from mod_pywebsocket.handshake._base import HandshakeException 54 from mod_pywebsocket.handshake._base import check_request_line 55 from mod_pywebsocket.handshake._base import format_header 56 from mod_pywebsocket.handshake._base import get_default_port 57 from mod_pywebsocket.handshake._base import get_mandatory_header 58 from mod_pywebsocket.handshake._base import parse_host_header 59 from mod_pywebsocket.handshake._base import validate_mandatory_header 60 61 62 _MANDATORY_HEADERS = [ 63 # key, expected value or None 64 [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], 65 [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], 66 ] 67 68 69 def _validate_subprotocol(subprotocol): 70 """Checks if characters in subprotocol are in range between U+0020 and 71 U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this 72 requirement. 73 74 See the Section 4.1. Opening handshake of the spec. 75 """ 76 77 if not subprotocol: 78 raise HandshakeException('Invalid subprotocol name: empty') 79 80 # Parameter should be in the range U+0020 to U+007E. 81 for c in subprotocol: 82 if not 0x20 <= ord(c) <= 0x7e: 83 raise HandshakeException( 84 'Illegal character in subprotocol name: %r' % c) 85 86 87 def _check_header_lines(request, mandatory_headers): 88 check_request_line(request) 89 90 # The expected field names, and the meaning of their corresponding 91 # values, are as follows. 92 # |Upgrade| and |Connection| 93 for key, expected_value in mandatory_headers: 94 validate_mandatory_header(request, key, expected_value) 95 96 97 def _build_location(request): 98 """Build WebSocket location for request.""" 99 100 location_parts = [] 101 if request.is_https(): 102 location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) 103 else: 104 location_parts.append(common.WEB_SOCKET_SCHEME) 105 location_parts.append('://') 106 host, port = parse_host_header(request) 107 connection_port = request.connection.local_addr[1] 108 if port != connection_port: 109 raise HandshakeException('Header/connection port mismatch: %d/%d' % 110 (port, connection_port)) 111 location_parts.append(host) 112 if (port != get_default_port(request.is_https())): 113 location_parts.append(':') 114 location_parts.append(str(port)) 115 location_parts.append(request.unparsed_uri) 116 return ''.join(location_parts) 117 118 119 class Handshaker(object): 120 """Opening handshake processor for the WebSocket protocol version HyBi 00. 121 """ 122 123 def __init__(self, request, dispatcher): 124 """Construct an instance. 125 126 Args: 127 request: mod_python request. 128 dispatcher: Dispatcher (dispatch.Dispatcher). 129 130 Handshaker will add attributes such as ws_resource in performing 131 handshake. 132 """ 133 134 self._logger = util.get_class_logger(self) 135 136 self._request = request 137 self._dispatcher = dispatcher 138 139 def do_handshake(self): 140 """Perform WebSocket Handshake. 141 142 On _request, we set 143 ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, 144 ws_challenge_md5: WebSocket handshake information. 145 ws_stream: Frame generation/parsing class. 146 ws_version: Protocol version. 147 148 Raises: 149 HandshakeException: when any error happened in parsing the opening 150 handshake request. 151 """ 152 153 # 5.1 Reading the client's opening handshake. 154 # dispatcher sets it in self._request. 155 _check_header_lines(self._request, _MANDATORY_HEADERS) 156 self._set_resource() 157 self._set_subprotocol() 158 self._set_location() 159 self._set_origin() 160 self._set_challenge_response() 161 self._set_protocol_version() 162 163 self._dispatcher.do_extra_handshake(self._request) 164 165 self._send_handshake() 166 167 def _set_resource(self): 168 self._request.ws_resource = self._request.uri 169 170 def _set_subprotocol(self): 171 # |Sec-WebSocket-Protocol| 172 subprotocol = self._request.headers_in.get( 173 common.SEC_WEBSOCKET_PROTOCOL_HEADER) 174 if subprotocol is not None: 175 _validate_subprotocol(subprotocol) 176 self._request.ws_protocol = subprotocol 177 178 def _set_location(self): 179 # |Host| 180 host = self._request.headers_in.get(common.HOST_HEADER) 181 if host is not None: 182 self._request.ws_location = _build_location(self._request) 183 # TODO(ukai): check host is this host. 184 185 def _set_origin(self): 186 # |Origin| 187 origin = self._request.headers_in.get(common.ORIGIN_HEADER) 188 if origin is not None: 189 self._request.ws_origin = origin 190 191 def _set_protocol_version(self): 192 # |Sec-WebSocket-Draft| 193 draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER) 194 if draft is not None and draft != '0': 195 raise HandshakeException('Illegal value for %s: %s' % 196 (common.SEC_WEBSOCKET_DRAFT_HEADER, 197 draft)) 198 199 self._logger.debug('Protocol version is HyBi 00') 200 self._request.ws_version = common.VERSION_HYBI00 201 self._request.ws_stream = StreamHixie75(self._request, True) 202 203 def _set_challenge_response(self): 204 # 5.2 4-8. 205 self._request.ws_challenge = self._get_challenge() 206 # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ 207 self._request.ws_challenge_md5 = util.md5_hash( 208 self._request.ws_challenge).digest() 209 self._logger.debug( 210 'Challenge: %r (%s)', 211 self._request.ws_challenge, 212 util.hexify(self._request.ws_challenge)) 213 self._logger.debug( 214 'Challenge response: %r (%s)', 215 self._request.ws_challenge_md5, 216 util.hexify(self._request.ws_challenge_md5)) 217 218 def _get_key_value(self, key_field): 219 key_value = get_mandatory_header(self._request, key_field) 220 221 self._logger.debug('%s: %r', key_field, key_value) 222 223 # 5.2 4. let /key-number_n/ be the digits (characters in the range 224 # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, 225 # interpreted as a base ten integer, ignoring all other characters 226 # in /key_n/. 227 try: 228 key_number = int(re.sub("\\D", "", key_value)) 229 except: 230 raise HandshakeException('%s field contains no digit' % key_field) 231 # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters 232 # in /key_n/. 233 spaces = re.subn(" ", "", key_value)[1] 234 if spaces == 0: 235 raise HandshakeException('%s field contains no space' % key_field) 236 237 self._logger.debug( 238 '%s: Key-number is %d and number of spaces is %d', 239 key_field, key_number, spaces) 240 241 # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ 242 # then abort the WebSocket connection. 243 if key_number % spaces != 0: 244 raise HandshakeException( 245 '%s: Key-number (%d) is not an integral multiple of spaces ' 246 '(%d)' % (key_field, key_number, spaces)) 247 # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. 248 part = key_number / spaces 249 self._logger.debug('%s: Part is %d', key_field, part) 250 return part 251 252 def _get_challenge(self): 253 # 5.2 4-7. 254 key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER) 255 key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER) 256 # 5.2 8. let /challenge/ be the concatenation of /part_1/, 257 challenge = '' 258 challenge += struct.pack('!I', key1) # network byteorder int 259 challenge += struct.pack('!I', key2) # network byteorder int 260 challenge += self._request.connection.read(8) 261 return challenge 262 263 def _send_handshake(self): 264 response = [] 265 266 # 5.2 10. send the following line. 267 response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') 268 269 # 5.2 11. send the following fields to the client. 270 response.append(format_header( 271 common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)) 272 response.append(format_header( 273 common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) 274 response.append(format_header( 275 common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location)) 276 response.append(format_header( 277 common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)) 278 if self._request.ws_protocol: 279 response.append(format_header( 280 common.SEC_WEBSOCKET_PROTOCOL_HEADER, 281 self._request.ws_protocol)) 282 # 5.2 12. send two bytes 0x0D 0x0A. 283 response.append('\r\n') 284 # 5.2 13. send /response/ 285 response.append(self._request.ws_challenge_md5) 286 287 raw_response = ''.join(response) 288 self._request.connection.write(raw_response) 289 self._logger.debug('Sent server\'s opening handshake: %r', 290 raw_response) 291 292 293 # vi:sts=4 sw=4 et 294