1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,323 @@ 1.4 +# Copyright 2011, Google Inc. 1.5 +# All rights reserved. 1.6 +# 1.7 +# Redistribution and use in source and binary forms, with or without 1.8 +# modification, are permitted provided that the following conditions are 1.9 +# met: 1.10 +# 1.11 +# * Redistributions of source code must retain the above copyright 1.12 +# notice, this list of conditions and the following disclaimer. 1.13 +# * Redistributions in binary form must reproduce the above 1.14 +# copyright notice, this list of conditions and the following disclaimer 1.15 +# in the documentation and/or other materials provided with the 1.16 +# distribution. 1.17 +# * Neither the name of Google Inc. nor the names of its 1.18 +# contributors may be used to endorse or promote products derived from 1.19 +# this software without specific prior written permission. 1.20 +# 1.21 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 1.22 +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 1.23 +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 1.24 +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 1.25 +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 1.26 +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 1.27 +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 1.28 +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 1.29 +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 1.30 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 1.31 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1.32 + 1.33 + 1.34 +"""Common functions and exceptions used by WebSocket opening handshake 1.35 +processors. 1.36 +""" 1.37 + 1.38 + 1.39 +from mod_pywebsocket import common 1.40 +from mod_pywebsocket import http_header_util 1.41 + 1.42 + 1.43 +class AbortedByUserException(Exception): 1.44 + """Exception for aborting a connection intentionally. 1.45 + 1.46 + If this exception is raised in do_extra_handshake handler, the connection 1.47 + will be abandoned. No other WebSocket or HTTP(S) handler will be invoked. 1.48 + 1.49 + If this exception is raised in transfer_data_handler, the connection will 1.50 + be closed without closing handshake. No other WebSocket or HTTP(S) handler 1.51 + will be invoked. 1.52 + """ 1.53 + 1.54 + pass 1.55 + 1.56 + 1.57 +class HandshakeException(Exception): 1.58 + """This exception will be raised when an error occurred while processing 1.59 + WebSocket initial handshake. 1.60 + """ 1.61 + 1.62 + def __init__(self, name, status=None): 1.63 + super(HandshakeException, self).__init__(name) 1.64 + self.status = status 1.65 + 1.66 + 1.67 +class VersionException(Exception): 1.68 + """This exception will be raised when a version of client request does not 1.69 + match with version the server supports. 1.70 + """ 1.71 + 1.72 + def __init__(self, name, supported_versions=''): 1.73 + """Construct an instance. 1.74 + 1.75 + Args: 1.76 + supported_version: a str object to show supported hybi versions. 1.77 + (e.g. '8, 13') 1.78 + """ 1.79 + super(VersionException, self).__init__(name) 1.80 + self.supported_versions = supported_versions 1.81 + 1.82 + 1.83 +def get_default_port(is_secure): 1.84 + if is_secure: 1.85 + return common.DEFAULT_WEB_SOCKET_SECURE_PORT 1.86 + else: 1.87 + return common.DEFAULT_WEB_SOCKET_PORT 1.88 + 1.89 + 1.90 +def validate_subprotocol(subprotocol, hixie): 1.91 + """Validate a value in subprotocol fields such as WebSocket-Protocol, 1.92 + Sec-WebSocket-Protocol. 1.93 + 1.94 + See 1.95 + - RFC 6455: Section 4.1., 4.2.2., and 4.3. 1.96 + - HyBi 00: Section 4.1. Opening handshake 1.97 + - Hixie 75: Section 4.1. Handshake 1.98 + """ 1.99 + 1.100 + if not subprotocol: 1.101 + raise HandshakeException('Invalid subprotocol name: empty') 1.102 + if hixie: 1.103 + # Parameter should be in the range U+0020 to U+007E. 1.104 + for c in subprotocol: 1.105 + if not 0x20 <= ord(c) <= 0x7e: 1.106 + raise HandshakeException( 1.107 + 'Illegal character in subprotocol name: %r' % c) 1.108 + else: 1.109 + # Parameter should be encoded HTTP token. 1.110 + state = http_header_util.ParsingState(subprotocol) 1.111 + token = http_header_util.consume_token(state) 1.112 + rest = http_header_util.peek(state) 1.113 + # If |rest| is not None, |subprotocol| is not one token or invalid. If 1.114 + # |rest| is None, |token| must not be None because |subprotocol| is 1.115 + # concatenation of |token| and |rest| and is not None. 1.116 + if rest is not None: 1.117 + raise HandshakeException('Invalid non-token string in subprotocol ' 1.118 + 'name: %r' % rest) 1.119 + 1.120 + 1.121 +def parse_host_header(request): 1.122 + fields = request.headers_in['Host'].split(':', 1) 1.123 + if len(fields) == 1: 1.124 + return fields[0], get_default_port(request.is_https()) 1.125 + try: 1.126 + return fields[0], int(fields[1]) 1.127 + except ValueError, e: 1.128 + raise HandshakeException('Invalid port number format: %r' % e) 1.129 + 1.130 + 1.131 +def format_header(name, value): 1.132 + return '%s: %s\r\n' % (name, value) 1.133 + 1.134 + 1.135 +def build_location(request): 1.136 + """Build WebSocket location for request.""" 1.137 + location_parts = [] 1.138 + if request.is_https(): 1.139 + location_parts.append(common.WEB_SOCKET_SECURE_SCHEME) 1.140 + else: 1.141 + location_parts.append(common.WEB_SOCKET_SCHEME) 1.142 + location_parts.append('://') 1.143 + host, port = parse_host_header(request) 1.144 + connection_port = request.connection.local_addr[1] 1.145 + if port != connection_port: 1.146 + raise HandshakeException('Header/connection port mismatch: %d/%d' % 1.147 + (port, connection_port)) 1.148 + location_parts.append(host) 1.149 + if (port != get_default_port(request.is_https())): 1.150 + location_parts.append(':') 1.151 + location_parts.append(str(port)) 1.152 + location_parts.append(request.uri) 1.153 + return ''.join(location_parts) 1.154 + 1.155 + 1.156 +def get_mandatory_header(request, key): 1.157 + value = request.headers_in.get(key) 1.158 + if value is None: 1.159 + raise HandshakeException('Header %s is not defined' % key) 1.160 + return value 1.161 + 1.162 + 1.163 +def validate_mandatory_header(request, key, expected_value, fail_status=None): 1.164 + value = get_mandatory_header(request, key) 1.165 + 1.166 + if value.lower() != expected_value.lower(): 1.167 + raise HandshakeException( 1.168 + 'Expected %r for header %s but found %r (case-insensitive)' % 1.169 + (expected_value, key, value), status=fail_status) 1.170 + 1.171 + 1.172 +def check_request_line(request): 1.173 + # 5.1 1. The three character UTF-8 string "GET". 1.174 + # 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). 1.175 + if request.method != 'GET': 1.176 + raise HandshakeException('Method is not GET') 1.177 + 1.178 + 1.179 +def check_header_lines(request, mandatory_headers): 1.180 + check_request_line(request) 1.181 + 1.182 + # The expected field names, and the meaning of their corresponding 1.183 + # values, are as follows. 1.184 + # |Upgrade| and |Connection| 1.185 + for key, expected_value in mandatory_headers: 1.186 + validate_mandatory_header(request, key, expected_value) 1.187 + 1.188 + 1.189 +def parse_token_list(data): 1.190 + """Parses a header value which follows 1#token and returns parsed elements 1.191 + as a list of strings. 1.192 + 1.193 + Leading LWSes must be trimmed. 1.194 + """ 1.195 + 1.196 + state = http_header_util.ParsingState(data) 1.197 + 1.198 + token_list = [] 1.199 + 1.200 + while True: 1.201 + token = http_header_util.consume_token(state) 1.202 + if token is not None: 1.203 + token_list.append(token) 1.204 + 1.205 + http_header_util.consume_lwses(state) 1.206 + 1.207 + if http_header_util.peek(state) is None: 1.208 + break 1.209 + 1.210 + if not http_header_util.consume_string(state, ','): 1.211 + raise HandshakeException( 1.212 + 'Expected a comma but found %r' % http_header_util.peek(state)) 1.213 + 1.214 + http_header_util.consume_lwses(state) 1.215 + 1.216 + if len(token_list) == 0: 1.217 + raise HandshakeException('No valid token found') 1.218 + 1.219 + return token_list 1.220 + 1.221 + 1.222 +def _parse_extension_param(state, definition, allow_quoted_string): 1.223 + param_name = http_header_util.consume_token(state) 1.224 + 1.225 + if param_name is None: 1.226 + raise HandshakeException('No valid parameter name found') 1.227 + 1.228 + http_header_util.consume_lwses(state) 1.229 + 1.230 + if not http_header_util.consume_string(state, '='): 1.231 + definition.add_parameter(param_name, None) 1.232 + return 1.233 + 1.234 + http_header_util.consume_lwses(state) 1.235 + 1.236 + if allow_quoted_string: 1.237 + # TODO(toyoshim): Add code to validate that parsed param_value is token 1.238 + param_value = http_header_util.consume_token_or_quoted_string(state) 1.239 + else: 1.240 + param_value = http_header_util.consume_token(state) 1.241 + if param_value is None: 1.242 + raise HandshakeException( 1.243 + 'No valid parameter value found on the right-hand side of ' 1.244 + 'parameter %r' % param_name) 1.245 + 1.246 + definition.add_parameter(param_name, param_value) 1.247 + 1.248 + 1.249 +def _parse_extension(state, allow_quoted_string): 1.250 + extension_token = http_header_util.consume_token(state) 1.251 + if extension_token is None: 1.252 + return None 1.253 + 1.254 + extension = common.ExtensionParameter(extension_token) 1.255 + 1.256 + while True: 1.257 + http_header_util.consume_lwses(state) 1.258 + 1.259 + if not http_header_util.consume_string(state, ';'): 1.260 + break 1.261 + 1.262 + http_header_util.consume_lwses(state) 1.263 + 1.264 + try: 1.265 + _parse_extension_param(state, extension, allow_quoted_string) 1.266 + except HandshakeException, e: 1.267 + raise HandshakeException( 1.268 + 'Failed to parse Sec-WebSocket-Extensions header: ' 1.269 + 'Failed to parse parameter for %r (%r)' % 1.270 + (extension_token, e)) 1.271 + 1.272 + return extension 1.273 + 1.274 + 1.275 +def parse_extensions(data, allow_quoted_string=False): 1.276 + """Parses Sec-WebSocket-Extensions header value returns a list of 1.277 + common.ExtensionParameter objects. 1.278 + 1.279 + Leading LWSes must be trimmed. 1.280 + """ 1.281 + 1.282 + state = http_header_util.ParsingState(data) 1.283 + 1.284 + extension_list = [] 1.285 + while True: 1.286 + extension = _parse_extension(state, allow_quoted_string) 1.287 + if extension is not None: 1.288 + extension_list.append(extension) 1.289 + 1.290 + http_header_util.consume_lwses(state) 1.291 + 1.292 + if http_header_util.peek(state) is None: 1.293 + break 1.294 + 1.295 + if not http_header_util.consume_string(state, ','): 1.296 + raise HandshakeException( 1.297 + 'Failed to parse Sec-WebSocket-Extensions header: ' 1.298 + 'Expected a comma but found %r' % 1.299 + http_header_util.peek(state)) 1.300 + 1.301 + http_header_util.consume_lwses(state) 1.302 + 1.303 + if len(extension_list) == 0: 1.304 + raise HandshakeException( 1.305 + 'Sec-WebSocket-Extensions header contains no valid extension') 1.306 + 1.307 + return extension_list 1.308 + 1.309 + 1.310 +def format_extensions(extension_list): 1.311 + formatted_extension_list = [] 1.312 + for extension in extension_list: 1.313 + formatted_params = [extension.name()] 1.314 + for param_name, param_value in extension.get_parameters(): 1.315 + if param_value is None: 1.316 + formatted_params.append(param_name) 1.317 + else: 1.318 + quoted_value = http_header_util.quote_if_necessary(param_value) 1.319 + formatted_params.append('%s=%s' % (param_name, quoted_value)) 1.320 + 1.321 + formatted_extension_list.append('; '.join(formatted_params)) 1.322 + 1.323 + return ', '.join(formatted_extension_list) 1.324 + 1.325 + 1.326 +# vi:sts=4 sw=4 et