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 check_request_line
     55 from mod_pywebsocket.handshake._base import format_header
     56 from mod_pywebsocket.handshake._base import get_default_port
     57 from mod_pywebsocket.handshake._base import get_mandatory_header
     58 from mod_pywebsocket.handshake._base import parse_host_header
     59 from mod_pywebsocket.handshake._base import validate_mandatory_header
     60 
     61 
     62 _MANDATORY_HEADERS = [
     63     # key, expected value or None
     64     [common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75],
     65     [common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE],
     66 ]
     67 
     68 
     69 def _validate_subprotocol(subprotocol):
     70     """Checks if characters in subprotocol are in range between U+0020 and
     71     U+007E. A value in the Sec-WebSocket-Protocol field need to satisfy this
     72     requirement.
     73 
     74     See the Section 4.1. Opening handshake of the spec.
     75     """
     76 
     77     if not subprotocol:
     78         raise HandshakeException('Invalid subprotocol name: empty')
     79 
     80     # Parameter should be in the range U+0020 to U+007E.
     81     for c in subprotocol:
     82         if not 0x20 <= ord(c) <= 0x7e:
     83             raise HandshakeException(
     84                 'Illegal character in subprotocol name: %r' % c)
     85 
     86 
     87 def _check_header_lines(request, mandatory_headers):
     88     check_request_line(request)
     89 
     90     # The expected field names, and the meaning of their corresponding
     91     # values, are as follows.
     92     #  |Upgrade| and |Connection|
     93     for key, expected_value in mandatory_headers:
     94         validate_mandatory_header(request, key, expected_value)
     95 
     96 
     97 def _build_location(request):
     98     """Build WebSocket location for request."""
     99 
    100     location_parts = []
    101     if request.is_https():
    102         location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
    103     else:
    104         location_parts.append(common.WEB_SOCKET_SCHEME)
    105     location_parts.append('://')
    106     host, port = parse_host_header(request)
    107     connection_port = request.connection.local_addr[1]
    108     if port != connection_port:
    109         raise HandshakeException('Header/connection port mismatch: %d/%d' %
    110                                  (port, connection_port))
    111     location_parts.append(host)
    112     if (port != get_default_port(request.is_https())):
    113         location_parts.append(':')
    114         location_parts.append(str(port))
    115     location_parts.append(request.unparsed_uri)
    116     return ''.join(location_parts)
    117 
    118 
    119 class Handshaker(object):
    120     """Opening handshake processor for the WebSocket protocol version HyBi 00.
    121     """
    122 
    123     def __init__(self, request, dispatcher):
    124         """Construct an instance.
    125 
    126         Args:
    127             request: mod_python request.
    128             dispatcher: Dispatcher (dispatch.Dispatcher).
    129 
    130         Handshaker will add attributes such as ws_resource in performing
    131         handshake.
    132         """
    133 
    134         self._logger = util.get_class_logger(self)
    135 
    136         self._request = request
    137         self._dispatcher = dispatcher
    138 
    139     def do_handshake(self):
    140         """Perform WebSocket Handshake.
    141 
    142         On _request, we set
    143             ws_resource, ws_protocol, ws_location, ws_origin, ws_challenge,
    144             ws_challenge_md5: WebSocket handshake information.
    145             ws_stream: Frame generation/parsing class.
    146             ws_version: Protocol version.
    147 
    148         Raises:
    149             HandshakeException: when any error happened in parsing the opening
    150                                 handshake request.
    151         """
    152 
    153         # 5.1 Reading the client's opening handshake.
    154         # dispatcher sets it in self._request.
    155         _check_header_lines(self._request, _MANDATORY_HEADERS)
    156         self._set_resource()
    157         self._set_subprotocol()
    158         self._set_location()
    159         self._set_origin()
    160         self._set_challenge_response()
    161         self._set_protocol_version()
    162 
    163         self._dispatcher.do_extra_handshake(self._request)
    164 
    165         self._send_handshake()
    166 
    167     def _set_resource(self):
    168         self._request.ws_resource = self._request.uri
    169 
    170     def _set_subprotocol(self):
    171         # |Sec-WebSocket-Protocol|
    172         subprotocol = self._request.headers_in.get(
    173             common.SEC_WEBSOCKET_PROTOCOL_HEADER)
    174         if subprotocol is not None:
    175             _validate_subprotocol(subprotocol)
    176         self._request.ws_protocol = subprotocol
    177 
    178     def _set_location(self):
    179         # |Host|
    180         host = self._request.headers_in.get(common.HOST_HEADER)
    181         if host is not None:
    182             self._request.ws_location = _build_location(self._request)
    183         # TODO(ukai): check host is this host.
    184 
    185     def _set_origin(self):
    186         # |Origin|
    187         origin = self._request.headers_in.get(common.ORIGIN_HEADER)
    188         if origin is not None:
    189             self._request.ws_origin = origin
    190 
    191     def _set_protocol_version(self):
    192         # |Sec-WebSocket-Draft|
    193         draft = self._request.headers_in.get(common.SEC_WEBSOCKET_DRAFT_HEADER)
    194         if draft is not None and draft != '0':
    195             raise HandshakeException('Illegal value for %s: %s' %
    196                                      (common.SEC_WEBSOCKET_DRAFT_HEADER,
    197                                       draft))
    198 
    199         self._logger.debug('Protocol version is HyBi 00')
    200         self._request.ws_version = common.VERSION_HYBI00
    201         self._request.ws_stream = StreamHixie75(self._request, True)
    202 
    203     def _set_challenge_response(self):
    204         # 5.2 4-8.
    205         self._request.ws_challenge = self._get_challenge()
    206         # 5.2 9. let /response/ be the MD5 finterprint of /challenge/
    207         self._request.ws_challenge_md5 = util.md5_hash(
    208             self._request.ws_challenge).digest()
    209         self._logger.debug(
    210             'Challenge: %r (%s)',
    211             self._request.ws_challenge,
    212             util.hexify(self._request.ws_challenge))
    213         self._logger.debug(
    214             'Challenge response: %r (%s)',
    215             self._request.ws_challenge_md5,
    216             util.hexify(self._request.ws_challenge_md5))
    217 
    218     def _get_key_value(self, key_field):
    219         key_value = get_mandatory_header(self._request, key_field)
    220 
    221         self._logger.debug('%s: %r', key_field, key_value)
    222 
    223         # 5.2 4. let /key-number_n/ be the digits (characters in the range
    224         # U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
    225         # interpreted as a base ten integer, ignoring all other characters
    226         # in /key_n/.
    227         try:
    228             key_number = int(re.sub("\\D", "", key_value))
    229         except:
    230             raise HandshakeException('%s field contains no digit' % key_field)
    231         # 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
    232         # in /key_n/.
    233         spaces = re.subn(" ", "", key_value)[1]
    234         if spaces == 0:
    235             raise HandshakeException('%s field contains no space' % key_field)
    236 
    237         self._logger.debug(
    238             '%s: Key-number is %d and number of spaces is %d',
    239             key_field, key_number, spaces)
    240 
    241         # 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/
    242         # then abort the WebSocket connection.
    243         if key_number % spaces != 0:
    244             raise HandshakeException(
    245                 '%s: Key-number (%d) is not an integral multiple of spaces '
    246                 '(%d)' % (key_field, key_number, spaces))
    247         # 5.2 7. let /part_n/ be /key-number_n/ divided by /spaces_n/.
    248         part = key_number / spaces
    249         self._logger.debug('%s: Part is %d', key_field, part)
    250         return part
    251 
    252     def _get_challenge(self):
    253         # 5.2 4-7.
    254         key1 = self._get_key_value(common.SEC_WEBSOCKET_KEY1_HEADER)
    255         key2 = self._get_key_value(common.SEC_WEBSOCKET_KEY2_HEADER)
    256         # 5.2 8. let /challenge/ be the concatenation of /part_1/,
    257         challenge = ''
    258         challenge += struct.pack('!I', key1)  # network byteorder int
    259         challenge += struct.pack('!I', key2)  # network byteorder int
    260         challenge += self._request.connection.read(8)
    261         return challenge
    262 
    263     def _send_handshake(self):
    264         response = []
    265 
    266         # 5.2 10. send the following line.
    267         response.append('HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
    268 
    269         # 5.2 11. send the following fields to the client.
    270         response.append(format_header(
    271             common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE_HIXIE75))
    272         response.append(format_header(
    273             common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE))
    274         response.append(format_header(
    275             common.SEC_WEBSOCKET_LOCATION_HEADER, self._request.ws_location))
    276         response.append(format_header(
    277             common.SEC_WEBSOCKET_ORIGIN_HEADER, self._request.ws_origin))
    278         if self._request.ws_protocol:
    279             response.append(format_header(
    280                 common.SEC_WEBSOCKET_PROTOCOL_HEADER,
    281                 self._request.ws_protocol))
    282         # 5.2 12. send two bytes 0x0D 0x0A.
    283         response.append('\r\n')
    284         # 5.2 13. send /response/
    285         response.append(self._request.ws_challenge_md5)
    286 
    287         raw_response = ''.join(response)
    288         self._request.connection.write(raw_response)
    289         self._logger.debug('Sent server\'s opening handshake: %r',
    290                            raw_response)
    291 
    292 
    293 # vi:sts=4 sw=4 et
    294