1# Copyright 2012, 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"""This file provides the opening handshake processor for the WebSocket 30protocol (RFC 6455). 31 32Specification: 33http://tools.ietf.org/html/rfc6455 34""" 35 36from __future__ import absolute_import 37import base64 38import re 39from hashlib import sha1 40 41from mod_pywebsocket import common 42from mod_pywebsocket.handshake.base import get_mandatory_header 43from mod_pywebsocket.handshake.base import HandshakeException 44from mod_pywebsocket.handshake.base import parse_token_list 45from mod_pywebsocket.handshake.base import validate_mandatory_header 46from mod_pywebsocket.handshake.base import HandshakerBase 47from mod_pywebsocket import util 48 49# Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648 50# disallows non-zero padding, so the character right before == must be any of 51# A, Q, g and w. 52_SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$') 53 54 55def check_request_line(request): 56 # 5.1 1. The three character UTF-8 string "GET". 57 # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). 58 if request.method != u'GET': 59 raise HandshakeException('Method is not GET: %r' % request.method) 60 61 if request.protocol != u'HTTP/1.1': 62 raise HandshakeException('Version is not HTTP/1.1: %r' % 63 request.protocol) 64 65 66def compute_accept(key): 67 """Computes value for the Sec-WebSocket-Accept header from value of the 68 Sec-WebSocket-Key header. 69 """ 70 71 accept_binary = sha1(key + common.WEBSOCKET_ACCEPT_UUID).digest() 72 accept = base64.b64encode(accept_binary) 73 74 return accept 75 76 77def compute_accept_from_unicode(unicode_key): 78 """A wrapper function for compute_accept which takes a unicode string as an 79 argument, and encodes it to byte string. It then passes it on to 80 compute_accept. 81 """ 82 83 key = unicode_key.encode('UTF-8') 84 return compute_accept(key) 85 86 87def format_header(name, value): 88 return u'%s: %s\r\n' % (name, value) 89 90 91class Handshaker(HandshakerBase): 92 """Opening handshake processor for the WebSocket protocol (RFC 6455).""" 93 def __init__(self, request, dispatcher): 94 """Construct an instance. 95 96 Args: 97 request: mod_python request. 98 dispatcher: Dispatcher (dispatch.Dispatcher). 99 100 Handshaker will add attributes such as ws_resource during handshake. 101 """ 102 super(Handshaker, self).__init__(request, dispatcher) 103 104 def _transform_header(self, header): 105 return header 106 107 def _protocol_rfc(self): 108 return 'RFC 6455' 109 110 def _validate_connection_header(self): 111 connection = get_mandatory_header(self._request, 112 common.CONNECTION_HEADER) 113 114 try: 115 connection_tokens = parse_token_list(connection) 116 except HandshakeException as e: 117 raise HandshakeException('Failed to parse %s: %s' % 118 (common.CONNECTION_HEADER, e)) 119 120 connection_is_valid = False 121 for token in connection_tokens: 122 if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower(): 123 connection_is_valid = True 124 break 125 if not connection_is_valid: 126 raise HandshakeException( 127 '%s header doesn\'t contain "%s"' % 128 (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) 129 130 def _validate_request(self): 131 check_request_line(self._request) 132 validate_mandatory_header(self._request, common.UPGRADE_HEADER, 133 common.WEBSOCKET_UPGRADE_TYPE) 134 self._validate_connection_header() 135 unused_host = get_mandatory_header(self._request, common.HOST_HEADER) 136 137 def _set_accept(self): 138 # Key validation, response generation. 139 key = self._get_key() 140 accept = compute_accept(key) 141 self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_ACCEPT_HEADER, 142 accept, util.hexify(base64.b64decode(accept))) 143 self._request._accept = accept 144 145 def _validate_key(self, key): 146 if key.find(',') >= 0: 147 raise HandshakeException('Request has multiple %s header lines or ' 148 'contains illegal character \',\': %r' % 149 (common.SEC_WEBSOCKET_KEY_HEADER, key)) 150 151 # Validate 152 key_is_valid = False 153 try: 154 # Validate key by quick regex match before parsing by base64 155 # module. Because base64 module skips invalid characters, we have 156 # to do this in advance to make this server strictly reject illegal 157 # keys. 158 if _SEC_WEBSOCKET_KEY_REGEX.match(key): 159 decoded_key = base64.b64decode(key) 160 if len(decoded_key) == 16: 161 key_is_valid = True 162 except TypeError as e: 163 pass 164 165 if not key_is_valid: 166 raise HandshakeException('Illegal value for header %s: %r' % 167 (common.SEC_WEBSOCKET_KEY_HEADER, key)) 168 169 return decoded_key 170 171 def _get_key(self): 172 key = get_mandatory_header(self._request, 173 common.SEC_WEBSOCKET_KEY_HEADER) 174 175 decoded_key = self._validate_key(key) 176 177 self._logger.debug('%s: %r (%s)', common.SEC_WEBSOCKET_KEY_HEADER, key, 178 util.hexify(decoded_key)) 179 180 return key.encode('UTF-8') 181 182 def _create_handshake_response(self, accept): 183 response = [] 184 185 response.append(u'HTTP/1.1 101 Switching Protocols\r\n') 186 187 # WebSocket headers 188 response.append( 189 format_header(common.UPGRADE_HEADER, 190 common.WEBSOCKET_UPGRADE_TYPE)) 191 response.append( 192 format_header(common.CONNECTION_HEADER, 193 common.UPGRADE_CONNECTION_TYPE)) 194 response.append( 195 format_header(common.SEC_WEBSOCKET_ACCEPT_HEADER, 196 accept.decode('UTF-8'))) 197 if self._request.ws_protocol is not None: 198 response.append( 199 format_header(common.SEC_WEBSOCKET_PROTOCOL_HEADER, 200 self._request.ws_protocol)) 201 if (self._request.ws_extensions is not None 202 and len(self._request.ws_extensions) != 0): 203 response.append( 204 format_header( 205 common.SEC_WEBSOCKET_EXTENSIONS_HEADER, 206 common.format_extensions(self._request.ws_extensions))) 207 208 # Headers not specific for WebSocket 209 for name, value in self._request.extra_headers: 210 response.append(format_header(name, value)) 211 212 response.append(u'\r\n') 213 214 return u''.join(response) 215 216 def _send_handshake(self): 217 raw_response = self._create_handshake_response(self._request._accept) 218 self._request.connection.write(raw_response.encode('UTF-8')) 219 self._logger.debug('Sent server\'s opening handshake: %r', 220 raw_response) 221 222 223# vi:sts=4 sw=4 et 224