testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 # Copyright 2011, Google Inc.
michael@0 2 # All rights reserved.
michael@0 3 #
michael@0 4 # Redistribution and use in source and binary forms, with or without
michael@0 5 # modification, are permitted provided that the following conditions are
michael@0 6 # met:
michael@0 7 #
michael@0 8 # * Redistributions of source code must retain the above copyright
michael@0 9 # notice, this list of conditions and the following disclaimer.
michael@0 10 # * Redistributions in binary form must reproduce the above
michael@0 11 # copyright notice, this list of conditions and the following disclaimer
michael@0 12 # in the documentation and/or other materials provided with the
michael@0 13 # distribution.
michael@0 14 # * Neither the name of Google Inc. nor the names of its
michael@0 15 # contributors may be used to endorse or promote products derived from
michael@0 16 # this software without specific prior written permission.
michael@0 17 #
michael@0 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
michael@0 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
michael@0 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
michael@0 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
michael@0 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
michael@0 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
michael@0 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
michael@0 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
michael@0 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
michael@0 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
michael@0 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
michael@0 29
michael@0 30
michael@0 31 """Common functions and exceptions used by WebSocket opening handshake
michael@0 32 processors.
michael@0 33 """
michael@0 34
michael@0 35
michael@0 36 from mod_pywebsocket import common
michael@0 37 from mod_pywebsocket import http_header_util
michael@0 38
michael@0 39
michael@0 40 class AbortedByUserException(Exception):
michael@0 41 """Exception for aborting a connection intentionally.
michael@0 42
michael@0 43 If this exception is raised in do_extra_handshake handler, the connection
michael@0 44 will be abandoned. No other WebSocket or HTTP(S) handler will be invoked.
michael@0 45
michael@0 46 If this exception is raised in transfer_data_handler, the connection will
michael@0 47 be closed without closing handshake. No other WebSocket or HTTP(S) handler
michael@0 48 will be invoked.
michael@0 49 """
michael@0 50
michael@0 51 pass
michael@0 52
michael@0 53
michael@0 54 class HandshakeException(Exception):
michael@0 55 """This exception will be raised when an error occurred while processing
michael@0 56 WebSocket initial handshake.
michael@0 57 """
michael@0 58
michael@0 59 def __init__(self, name, status=None):
michael@0 60 super(HandshakeException, self).__init__(name)
michael@0 61 self.status = status
michael@0 62
michael@0 63
michael@0 64 class VersionException(Exception):
michael@0 65 """This exception will be raised when a version of client request does not
michael@0 66 match with version the server supports.
michael@0 67 """
michael@0 68
michael@0 69 def __init__(self, name, supported_versions=''):
michael@0 70 """Construct an instance.
michael@0 71
michael@0 72 Args:
michael@0 73 supported_version: a str object to show supported hybi versions.
michael@0 74 (e.g. '8, 13')
michael@0 75 """
michael@0 76 super(VersionException, self).__init__(name)
michael@0 77 self.supported_versions = supported_versions
michael@0 78
michael@0 79
michael@0 80 def get_default_port(is_secure):
michael@0 81 if is_secure:
michael@0 82 return common.DEFAULT_WEB_SOCKET_SECURE_PORT
michael@0 83 else:
michael@0 84 return common.DEFAULT_WEB_SOCKET_PORT
michael@0 85
michael@0 86
michael@0 87 def validate_subprotocol(subprotocol, hixie):
michael@0 88 """Validate a value in subprotocol fields such as WebSocket-Protocol,
michael@0 89 Sec-WebSocket-Protocol.
michael@0 90
michael@0 91 See
michael@0 92 - RFC 6455: Section 4.1., 4.2.2., and 4.3.
michael@0 93 - HyBi 00: Section 4.1. Opening handshake
michael@0 94 - Hixie 75: Section 4.1. Handshake
michael@0 95 """
michael@0 96
michael@0 97 if not subprotocol:
michael@0 98 raise HandshakeException('Invalid subprotocol name: empty')
michael@0 99 if hixie:
michael@0 100 # Parameter should be in the range U+0020 to U+007E.
michael@0 101 for c in subprotocol:
michael@0 102 if not 0x20 <= ord(c) <= 0x7e:
michael@0 103 raise HandshakeException(
michael@0 104 'Illegal character in subprotocol name: %r' % c)
michael@0 105 else:
michael@0 106 # Parameter should be encoded HTTP token.
michael@0 107 state = http_header_util.ParsingState(subprotocol)
michael@0 108 token = http_header_util.consume_token(state)
michael@0 109 rest = http_header_util.peek(state)
michael@0 110 # If |rest| is not None, |subprotocol| is not one token or invalid. If
michael@0 111 # |rest| is None, |token| must not be None because |subprotocol| is
michael@0 112 # concatenation of |token| and |rest| and is not None.
michael@0 113 if rest is not None:
michael@0 114 raise HandshakeException('Invalid non-token string in subprotocol '
michael@0 115 'name: %r' % rest)
michael@0 116
michael@0 117
michael@0 118 def parse_host_header(request):
michael@0 119 fields = request.headers_in['Host'].split(':', 1)
michael@0 120 if len(fields) == 1:
michael@0 121 return fields[0], get_default_port(request.is_https())
michael@0 122 try:
michael@0 123 return fields[0], int(fields[1])
michael@0 124 except ValueError, e:
michael@0 125 raise HandshakeException('Invalid port number format: %r' % e)
michael@0 126
michael@0 127
michael@0 128 def format_header(name, value):
michael@0 129 return '%s: %s\r\n' % (name, value)
michael@0 130
michael@0 131
michael@0 132 def build_location(request):
michael@0 133 """Build WebSocket location for request."""
michael@0 134 location_parts = []
michael@0 135 if request.is_https():
michael@0 136 location_parts.append(common.WEB_SOCKET_SECURE_SCHEME)
michael@0 137 else:
michael@0 138 location_parts.append(common.WEB_SOCKET_SCHEME)
michael@0 139 location_parts.append('://')
michael@0 140 host, port = parse_host_header(request)
michael@0 141 connection_port = request.connection.local_addr[1]
michael@0 142 if port != connection_port:
michael@0 143 raise HandshakeException('Header/connection port mismatch: %d/%d' %
michael@0 144 (port, connection_port))
michael@0 145 location_parts.append(host)
michael@0 146 if (port != get_default_port(request.is_https())):
michael@0 147 location_parts.append(':')
michael@0 148 location_parts.append(str(port))
michael@0 149 location_parts.append(request.uri)
michael@0 150 return ''.join(location_parts)
michael@0 151
michael@0 152
michael@0 153 def get_mandatory_header(request, key):
michael@0 154 value = request.headers_in.get(key)
michael@0 155 if value is None:
michael@0 156 raise HandshakeException('Header %s is not defined' % key)
michael@0 157 return value
michael@0 158
michael@0 159
michael@0 160 def validate_mandatory_header(request, key, expected_value, fail_status=None):
michael@0 161 value = get_mandatory_header(request, key)
michael@0 162
michael@0 163 if value.lower() != expected_value.lower():
michael@0 164 raise HandshakeException(
michael@0 165 'Expected %r for header %s but found %r (case-insensitive)' %
michael@0 166 (expected_value, key, value), status=fail_status)
michael@0 167
michael@0 168
michael@0 169 def check_request_line(request):
michael@0 170 # 5.1 1. The three character UTF-8 string "GET".
michael@0 171 # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
michael@0 172 if request.method != 'GET':
michael@0 173 raise HandshakeException('Method is not GET')
michael@0 174
michael@0 175
michael@0 176 def check_header_lines(request, mandatory_headers):
michael@0 177 check_request_line(request)
michael@0 178
michael@0 179 # The expected field names, and the meaning of their corresponding
michael@0 180 # values, are as follows.
michael@0 181 # |Upgrade| and |Connection|
michael@0 182 for key, expected_value in mandatory_headers:
michael@0 183 validate_mandatory_header(request, key, expected_value)
michael@0 184
michael@0 185
michael@0 186 def parse_token_list(data):
michael@0 187 """Parses a header value which follows 1#token and returns parsed elements
michael@0 188 as a list of strings.
michael@0 189
michael@0 190 Leading LWSes must be trimmed.
michael@0 191 """
michael@0 192
michael@0 193 state = http_header_util.ParsingState(data)
michael@0 194
michael@0 195 token_list = []
michael@0 196
michael@0 197 while True:
michael@0 198 token = http_header_util.consume_token(state)
michael@0 199 if token is not None:
michael@0 200 token_list.append(token)
michael@0 201
michael@0 202 http_header_util.consume_lwses(state)
michael@0 203
michael@0 204 if http_header_util.peek(state) is None:
michael@0 205 break
michael@0 206
michael@0 207 if not http_header_util.consume_string(state, ','):
michael@0 208 raise HandshakeException(
michael@0 209 'Expected a comma but found %r' % http_header_util.peek(state))
michael@0 210
michael@0 211 http_header_util.consume_lwses(state)
michael@0 212
michael@0 213 if len(token_list) == 0:
michael@0 214 raise HandshakeException('No valid token found')
michael@0 215
michael@0 216 return token_list
michael@0 217
michael@0 218
michael@0 219 def _parse_extension_param(state, definition, allow_quoted_string):
michael@0 220 param_name = http_header_util.consume_token(state)
michael@0 221
michael@0 222 if param_name is None:
michael@0 223 raise HandshakeException('No valid parameter name found')
michael@0 224
michael@0 225 http_header_util.consume_lwses(state)
michael@0 226
michael@0 227 if not http_header_util.consume_string(state, '='):
michael@0 228 definition.add_parameter(param_name, None)
michael@0 229 return
michael@0 230
michael@0 231 http_header_util.consume_lwses(state)
michael@0 232
michael@0 233 if allow_quoted_string:
michael@0 234 # TODO(toyoshim): Add code to validate that parsed param_value is token
michael@0 235 param_value = http_header_util.consume_token_or_quoted_string(state)
michael@0 236 else:
michael@0 237 param_value = http_header_util.consume_token(state)
michael@0 238 if param_value is None:
michael@0 239 raise HandshakeException(
michael@0 240 'No valid parameter value found on the right-hand side of '
michael@0 241 'parameter %r' % param_name)
michael@0 242
michael@0 243 definition.add_parameter(param_name, param_value)
michael@0 244
michael@0 245
michael@0 246 def _parse_extension(state, allow_quoted_string):
michael@0 247 extension_token = http_header_util.consume_token(state)
michael@0 248 if extension_token is None:
michael@0 249 return None
michael@0 250
michael@0 251 extension = common.ExtensionParameter(extension_token)
michael@0 252
michael@0 253 while True:
michael@0 254 http_header_util.consume_lwses(state)
michael@0 255
michael@0 256 if not http_header_util.consume_string(state, ';'):
michael@0 257 break
michael@0 258
michael@0 259 http_header_util.consume_lwses(state)
michael@0 260
michael@0 261 try:
michael@0 262 _parse_extension_param(state, extension, allow_quoted_string)
michael@0 263 except HandshakeException, e:
michael@0 264 raise HandshakeException(
michael@0 265 'Failed to parse Sec-WebSocket-Extensions header: '
michael@0 266 'Failed to parse parameter for %r (%r)' %
michael@0 267 (extension_token, e))
michael@0 268
michael@0 269 return extension
michael@0 270
michael@0 271
michael@0 272 def parse_extensions(data, allow_quoted_string=False):
michael@0 273 """Parses Sec-WebSocket-Extensions header value returns a list of
michael@0 274 common.ExtensionParameter objects.
michael@0 275
michael@0 276 Leading LWSes must be trimmed.
michael@0 277 """
michael@0 278
michael@0 279 state = http_header_util.ParsingState(data)
michael@0 280
michael@0 281 extension_list = []
michael@0 282 while True:
michael@0 283 extension = _parse_extension(state, allow_quoted_string)
michael@0 284 if extension is not None:
michael@0 285 extension_list.append(extension)
michael@0 286
michael@0 287 http_header_util.consume_lwses(state)
michael@0 288
michael@0 289 if http_header_util.peek(state) is None:
michael@0 290 break
michael@0 291
michael@0 292 if not http_header_util.consume_string(state, ','):
michael@0 293 raise HandshakeException(
michael@0 294 'Failed to parse Sec-WebSocket-Extensions header: '
michael@0 295 'Expected a comma but found %r' %
michael@0 296 http_header_util.peek(state))
michael@0 297
michael@0 298 http_header_util.consume_lwses(state)
michael@0 299
michael@0 300 if len(extension_list) == 0:
michael@0 301 raise HandshakeException(
michael@0 302 'Sec-WebSocket-Extensions header contains no valid extension')
michael@0 303
michael@0 304 return extension_list
michael@0 305
michael@0 306
michael@0 307 def format_extensions(extension_list):
michael@0 308 formatted_extension_list = []
michael@0 309 for extension in extension_list:
michael@0 310 formatted_params = [extension.name()]
michael@0 311 for param_name, param_value in extension.get_parameters():
michael@0 312 if param_value is None:
michael@0 313 formatted_params.append(param_name)
michael@0 314 else:
michael@0 315 quoted_value = http_header_util.quote_if_necessary(param_value)
michael@0 316 formatted_params.append('%s=%s' % (param_name, quoted_value))
michael@0 317
michael@0 318 formatted_extension_list.append('; '.join(formatted_params))
michael@0 319
michael@0 320 return ', '.join(formatted_extension_list)
michael@0 321
michael@0 322
michael@0 323 # vi:sts=4 sw=4 et

mercurial