michael@0: # Copyright 2011, Google Inc. michael@0: # All rights reserved. michael@0: # michael@0: # Redistribution and use in source and binary forms, with or without michael@0: # modification, are permitted provided that the following conditions are michael@0: # met: michael@0: # michael@0: # * Redistributions of source code must retain the above copyright michael@0: # notice, this list of conditions and the following disclaimer. michael@0: # * Redistributions in binary form must reproduce the above michael@0: # copyright notice, this list of conditions and the following disclaimer michael@0: # in the documentation and/or other materials provided with the michael@0: # distribution. michael@0: # * Neither the name of Google Inc. nor the names of its michael@0: # contributors may be used to endorse or promote products derived from michael@0: # this software without specific prior written permission. michael@0: # michael@0: # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT michael@0: # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, michael@0: # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT michael@0: # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, michael@0: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY michael@0: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT michael@0: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE michael@0: # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: michael@0: michael@0: """WebSocket handshaking defined in draft-hixie-thewebsocketprotocol-75.""" michael@0: michael@0: michael@0: # Note: request.connection.write is used in this module, even though mod_python michael@0: # document says that it should be used only in connection handlers. michael@0: # Unfortunately, we have no other options. For example, request.write is not michael@0: # suitable because it doesn't allow direct raw bytes writing. michael@0: michael@0: michael@0: import logging michael@0: import re michael@0: michael@0: from mod_pywebsocket import common michael@0: from mod_pywebsocket.stream import StreamHixie75 michael@0: from mod_pywebsocket import util michael@0: from mod_pywebsocket.handshake._base import HandshakeException michael@0: from mod_pywebsocket.handshake._base import build_location michael@0: from mod_pywebsocket.handshake._base import validate_subprotocol michael@0: michael@0: michael@0: _MANDATORY_HEADERS = [ michael@0: # key, expected value or None michael@0: ['Upgrade', 'WebSocket'], michael@0: ['Connection', 'Upgrade'], michael@0: ['Host', None], michael@0: ['Origin', None], michael@0: ] michael@0: michael@0: _FIRST_FIVE_LINES = map(re.compile, [ michael@0: r'^GET /[\S]* HTTP/1.1\r\n$', michael@0: r'^Upgrade: WebSocket\r\n$', michael@0: r'^Connection: Upgrade\r\n$', michael@0: r'^Host: [\S]+\r\n$', michael@0: r'^Origin: [\S]+\r\n$', michael@0: ]) michael@0: michael@0: _SIXTH_AND_LATER = re.compile( michael@0: r'^' michael@0: r'(WebSocket-Protocol: [\x20-\x7e]+\r\n)?' michael@0: r'(Cookie: [^\r]*\r\n)*' michael@0: r'(Cookie2: [^\r]*\r\n)?' michael@0: r'(Cookie: [^\r]*\r\n)*' michael@0: r'\r\n') michael@0: michael@0: michael@0: class Handshaker(object): michael@0: """This class performs WebSocket handshake.""" michael@0: michael@0: def __init__(self, request, dispatcher, strict=False): michael@0: """Construct an instance. michael@0: michael@0: Args: michael@0: request: mod_python request. michael@0: dispatcher: Dispatcher (dispatch.Dispatcher). michael@0: strict: Strictly check handshake request. Default: False. michael@0: If True, request.connection must provide get_memorized_lines michael@0: method. michael@0: michael@0: Handshaker will add attributes such as ws_resource in performing michael@0: handshake. michael@0: """ michael@0: michael@0: self._logger = util.get_class_logger(self) michael@0: michael@0: self._request = request michael@0: self._dispatcher = dispatcher michael@0: self._strict = strict michael@0: michael@0: def do_handshake(self): michael@0: """Perform WebSocket Handshake. michael@0: michael@0: On _request, we set michael@0: ws_resource, ws_origin, ws_location, ws_protocol michael@0: ws_challenge_md5: WebSocket handshake information. michael@0: ws_stream: Frame generation/parsing class. michael@0: ws_version: Protocol version. michael@0: """ michael@0: michael@0: self._check_header_lines() michael@0: self._set_resource() michael@0: self._set_origin() michael@0: self._set_location() michael@0: self._set_subprotocol() michael@0: self._set_protocol_version() michael@0: michael@0: self._dispatcher.do_extra_handshake(self._request) michael@0: michael@0: self._send_handshake() michael@0: michael@0: self._logger.debug('Sent opening handshake response') michael@0: michael@0: def _set_resource(self): michael@0: self._request.ws_resource = self._request.uri michael@0: michael@0: def _set_origin(self): michael@0: self._request.ws_origin = self._request.headers_in['Origin'] michael@0: michael@0: def _set_location(self): michael@0: self._request.ws_location = build_location(self._request) michael@0: michael@0: def _set_subprotocol(self): michael@0: subprotocol = self._request.headers_in.get('WebSocket-Protocol') michael@0: if subprotocol is not None: michael@0: validate_subprotocol(subprotocol, hixie=True) michael@0: self._request.ws_protocol = subprotocol michael@0: michael@0: def _set_protocol_version(self): michael@0: self._logger.debug('IETF Hixie 75 protocol') michael@0: self._request.ws_version = common.VERSION_HIXIE75 michael@0: self._request.ws_stream = StreamHixie75(self._request) michael@0: michael@0: def _sendall(self, data): michael@0: self._request.connection.write(data) michael@0: michael@0: def _send_handshake(self): michael@0: self._sendall('HTTP/1.1 101 Web Socket Protocol Handshake\r\n') michael@0: self._sendall('Upgrade: WebSocket\r\n') michael@0: self._sendall('Connection: Upgrade\r\n') michael@0: self._sendall('WebSocket-Origin: %s\r\n' % self._request.ws_origin) michael@0: self._sendall('WebSocket-Location: %s\r\n' % self._request.ws_location) michael@0: if self._request.ws_protocol: michael@0: self._sendall( michael@0: 'WebSocket-Protocol: %s\r\n' % self._request.ws_protocol) michael@0: self._sendall('\r\n') michael@0: michael@0: def _check_header_lines(self): michael@0: for key, expected_value in _MANDATORY_HEADERS: michael@0: actual_value = self._request.headers_in.get(key) michael@0: if not actual_value: michael@0: raise HandshakeException('Header %s is not defined' % key) michael@0: if expected_value: michael@0: if actual_value != expected_value: michael@0: raise HandshakeException( michael@0: 'Expected %r for header %s but found %r' % michael@0: (expected_value, key, actual_value)) michael@0: if self._strict: michael@0: try: michael@0: lines = self._request.connection.get_memorized_lines() michael@0: except AttributeError, e: michael@0: raise AttributeError( michael@0: 'Strict handshake is specified but the connection ' michael@0: 'doesn\'t provide get_memorized_lines()') michael@0: self._check_first_lines(lines) michael@0: michael@0: def _check_first_lines(self, lines): michael@0: if len(lines) < len(_FIRST_FIVE_LINES): michael@0: raise HandshakeException('Too few header lines: %d' % len(lines)) michael@0: for line, regexp in zip(lines, _FIRST_FIVE_LINES): michael@0: if not regexp.search(line): michael@0: raise HandshakeException( michael@0: 'Unexpected header: %r doesn\'t match %r' michael@0: % (line, regexp.pattern)) michael@0: sixth_and_later = ''.join(lines[5:]) michael@0: if not _SIXTH_AND_LATER.search(sixth_and_later): michael@0: raise HandshakeException( michael@0: 'Unexpected header: %r doesn\'t match %r' michael@0: % (sixth_and_later, _SIXTH_AND_LATER.pattern)) michael@0: michael@0: michael@0: # vi:sts=4 sw=4 et