Home | History | Annotate | Download | only in handshake
      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