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