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 build_location 55 from mod_pywebsocket.handshake._base import check_header_lines 56 from mod_pywebsocket.handshake._base import format_header 57 from mod_pywebsocket.handshake._base import get_mandatory_header 58 from mod_pywebsocket.handshake._base import validate_subprotocol 59 60 61 _MANDATORY_HEADERS = [ 62 # key, expected value or None 63 [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75], 64 [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE], 65 ] 66 67 68 class Handshaker(object): 69 """Opening handshake processor for the WebSocket protocol version HyBi 00. 70 """ 71 72 def __init__(self, request, dispatcher): 73 """Construct an instance. 74 75 Args: 76 request: mod_python request. 77 dispatcher: Dispatcher (dispatch.Dispatcher). 78 79 Handshaker will add attributes such as ws_resource in performing 80 handshake. 81 """ 82 83 self._logger = util.get_class_logger(self) 84 85 self._request = request 86 self._dispatcher = dispatcher 87 88 def do_handshake(self): 89 """Perform WebSocket Handshake. 90 91 On _request, we set 92 ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge, 93 ws_challenge_md5: WebSocket handshake information. 94 ws_stream: Frame generation/parsing class. 95 ws_version: Protocol version. 96 97 Raises: 98 HandshakeException: when any error happened in parsing the opening 99 handshake request. 100 """ 101 102 # 5.1 Reading the client's opening handshake. 103 # dispatcher sets it in self._request. 104 check_header_lines(self._request, _MANDATORY_HEADERS) 105 self._set_resource() 106 self._set_subprotocol() 107 self._set_location() 108 self._set_origin() 109 self._set_challenge_response() 110 self._set_protocol_version() 111 112 self._dispatcher.do_extra_handshake(self._request) 113 114 self._send_handshake() 115 116 def _set_resource(self): 117 self._request.ws_resource = self._request.uri 118 119 def _set_subprotocol(self): 120 # |Sec-WebSocket-Protocol| 121 subprotocol = self._request.headers_in.get( 122 common.SEC_WEBSOCKET_PROTOCOL_HEADER) 123 if subprotocol is not None: 124 validate_subprotocol(subprotocol, hixie=True) 125 self._request.ws_protocol = subprotocol 126 127 def _set_location(self): 128 # |Host| 129 host = self._request.headers_in.get(common.HOST_HEADER) 130 if host is not None: 131 self._request.ws_location = build_location(self._request) 132 # TODO(ukai): check host is this host. 133 134 def _set_origin(self): 135 # |Origin| 136 origin = self._request.headers_in.get(common.ORIGIN_HEADER) 137 if origin is not None: 138 self._request.ws_origin = origin 139 140 def _set_protocol_version(self): 141 # |Sec-WebSocket-Draft| 142 draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER) 143 if draft is not None and draft != '0': 144 raise HandshakeException('Illegal value for %s: %s' % 145 (common.SEC_WEBSOCKET_DRAFT_HEADER, 146 draft)) 147 148 self._logger.debug('Protocol version is HyBi 00') 149 self._request.ws_version = common.VERSION_HYBI00 150 self._request.ws_stream = StreamHixie75(self._request, True) 151 152 def _set_challenge_response(self): 153 # 5.2 4-8. 154 self._request.ws_challenge = self._get_challenge() 155 # 5.2 9. let /response/ be the MD5 finterprint of /challenge/ 156 self._request.ws_challenge_md5 = util.md5_hash( 157 self._request.ws_challenge).digest() 158 self._logger.debug( 159 'Challenge: %r (%s)', 160 self._request.ws_challenge, 161 util.hexify(self._request.ws_challenge)) 162 self._logger.debug( 163 'Challenge response: %r (%s)', 164 self._request.ws_challenge_md5, 165 util.hexify(self._request.ws_challenge_md5)) 166 167 def _get_key_value(self, key_field): 168 key_value = get_mandatory_header(self._request, key_field) 169 170 self._logger.debug('%s: %r', key_field, key_value) 171 172 # 5.2 4. let /key-number_n/ be the digits (characters in the range 173 # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/, 174 # interpreted as a base ten integer, ignoring all other characters 175 # in /key_n/. 176 try: 177 key_number = int(re.sub("\\D", "", key_value)) 178 except: 179 raise HandshakeException('%s field contains no digit' % key_field) 180 # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters 181 # in /key_n/. 182 spaces = re.subn(" ", "", key_value)[1] 183 if spaces == 0: 184 raise HandshakeException('%s field contains no space' % key_field) 185 186 self._logger.debug( 187 '%s: Key-number is %d and number of spaces is %d', 188 key_field, key_number, spaces) 189 190 # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/ 191 # then abort the WebSocket connection. 192 if key_number % spaces != 0: 193 raise HandshakeException( 194 '%s: Key-number (%d) is not an integral multiple of spaces ' 195 '(%d)' % (key_field, key_number, spaces)) 196 # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/. 197 part = key_number / spaces 198 self._logger.debug('%s: Part is %d', key_field, part) 199 return part 200 201 def _get_challenge(self): 202 # 5.2 4-7. 203 key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER) 204 key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER) 205 # 5.2 8. let /challenge/ be the concatenation of /part_1/, 206 challenge = '' 207 challenge += struct.pack('!I', key1) # network byteorder int 208 challenge += struct.pack('!I', key2) # network byteorder int 209 challenge += self._request.connection.read(8) 210 return challenge 211 212 def _send_handshake(self): 213 response = [] 214 215 # 5.2 10. send the following line. 216 response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n') 217 218 # 5.2 11. send the following fields to the client. 219 response.append(format_header( 220 common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75)) 221 response.append(format_header( 222 common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) 223 response.append(format_header( 224 common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location)) 225 response.append(format_header( 226 common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin)) 227 if self._request.ws_protocol: 228 response.append(format_header( 229 common.SEC_WEBSOCKET_PROTOCOL_HEADER, 230 self._request.ws_protocol)) 231 # 5.2 12. send two bytes 0x0D 0x0A. 232 response.append('\r\n') 233 # 5.2 13. send /response/ 234 response.append(self._request.ws_challenge_md5) 235 236 raw_response = ''.join(response) 237 self._request.connection.write(raw_response) 238 self._logger.debug('Sent server\'s opening handshake: %r', 239 raw_response) 240 241 242 # vi:sts=4 sw=4 et 243