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 """WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75.""" 32 33 34 # Note: request.connection.write is used in this module, even though mod_python 35 # document says that it should be used only in connection handlers. 36 # Unfortunately, we have no other options. For example, request.write is not 37 # suitable because it doesn't allow direct raw bytes writing. 38 39 40 import logging 41 import re 42 43 from mod_pywebsocket import common 44 from mod_pywebsocket.stream import StreamHixie75 45 from mod_pywebsocket import util 46 from mod_pywebsocket.handshake._base import HandshakeException 47 from mod_pywebsocket.handshake._base import build_location 48 from mod_pywebsocket.handshake._base import validate_subprotocol 49 50 51 _MANDATORY_HEADERS = [ 52 # key, expected value or None 53 ['Upgrade', 'WebSocket'], 54 ['Connection', 'Upgrade'], 55 ['Host', None], 56 ['Origin', None], 57 ] 58 59 _FIRST_FIVE_LINES = map(re.compile, [ 60 r'^GET /[\S]* HTTP/1.1\r\n$', 61 r'^Upgrade: WebSocket\r\n$', 62 r'^Connection: Upgrade\r\n$', 63 r'^Host: [\S]+\r\n$', 64 r'^Origin: [\S]+\r\n$', 65 ]) 66 67 _SIXTH_AND_LATER = re.compile( 68 r'^' 69 r'(WebSocket-Protocol: [\x20-\x7e]+\r\n)?' 70 r'(Cookie: [^\r]*\r\n)*' 71 r'(Cookie2: [^\r]*\r\n)?' 72 r'(Cookie: [^\r]*\r\n)*' 73 r'\r\n') 74 75 76 class Handshaker(object): 77 """This class performs WebSocket handshake.""" 78 79 def __init__(self, request, dispatcher, strict=False): 80 """Construct an instance. 81 82 Args: 83 request: mod_python request. 84 dispatcher: Dispatcher (dispatch.Dispatcher). 85 strict: Strictly check handshake request. Default: False. 86 If True, request.connection must provide get_memorized_lines 87 method. 88 89 Handshaker will add attributes such as ws_resource in performing 90 handshake. 91 """ 92 93 self._logger = util.get_class_logger(self) 94 95 self._request = request 96 self._dispatcher = dispatcher 97 self._strict = strict 98 99 def do_handshake(self): 100 """Perform WebSocket Handshake. 101 102 On _request, we set 103 ws_resource, ws_origin, ws_location, ws_protocol 104 ws_challenge_md5: WebSocket handshake information. 105 ws_stream: Frame generation/parsing class. 106 ws_version: Protocol version. 107 """ 108 109 self._check_header_lines() 110 self._set_resource() 111 self._set_origin() 112 self._set_location() 113 self._set_subprotocol() 114 self._set_protocol_version() 115 116 self._dispatcher.do_extra_handshake(self._request) 117 118 self._send_handshake() 119 120 self._logger.debug('Sent opening handshake response') 121 122 def _set_resource(self): 123 self._request.ws_resource = self._request.uri 124 125 def _set_origin(self): 126 self._request.ws_origin = self._request.headers_in['Origin'] 127 128 def _set_location(self): 129 self._request.ws_location = build_location(self._request) 130 131 def _set_subprotocol(self): 132 subprotocol = self._request.headers_in.get('WebSocket-Protocol') 133 if subprotocol is not None: 134 validate_subprotocol(subprotocol, hixie=True) 135 self._request.ws_protocol = subprotocol 136 137 def _set_protocol_version(self): 138 self._logger.debug('IETF Hixie 75 protocol') 139 self._request.ws_version = common.VERSION_HIXIE75 140 self._request.ws_stream = StreamHixie75(self._request) 141 142 def _sendall(self, data): 143 self._request.connection.write(data) 144 145 def _send_handshake(self): 146 self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n') 147 self._sendall('Upgrade: WebSocket\r\n') 148 self._sendall('Connection: Upgrade\r\n') 149 self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin) 150 self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location) 151 if self._request.ws_protocol: 152 self._sendall( 153 'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol) 154 self._sendall('\r\n') 155 156 def _check_header_lines(self): 157 for key, expected_value in _MANDATORY_HEADERS: 158 actual_value = self._request.headers_in.get(key) 159 if not actual_value: 160 raise HandshakeException('Header %s is not defined' % key) 161 if expected_value: 162 if actual_value != expected_value: 163 raise HandshakeException( 164 'Expected %r for header %s but found %r' % 165 (expected_value, key, actual_value)) 166 if self._strict: 167 try: 168 lines = self._request.connection.get_memorized_lines() 169 except AttributeError, e: 170 raise AttributeError( 171 'Strict handshake is specified but the connection ' 172 'doesn\'t provide get_memorized_lines()') 173 self._check_first_lines(lines) 174 175 def _check_first_lines(self, lines): 176 if len(lines) < len(_FIRST_FIVE_LINES): 177 raise HandshakeException('Too few header lines: %d' % len(lines)) 178 for line, regexp in zip(lines, _FIRST_FIVE_LINES): 179 if not regexp.search(line): 180 raise HandshakeException( 181 'Unexpected header: %r doesn\'t match %r' 182 % (line, regexp.pattern)) 183 sixth_and_later = ''.join(lines[5:]) 184 if not _SIXTH_AND_LATER.search(sixth_and_later): 185 raise HandshakeException( 186 'Unexpected header: %r doesn\'t match %r' 187 % (sixth_and_later, _SIXTH_AND_LATER.pattern)) 188 189 190 # vi:sts=4 sw=4 et 191