Thu, 22 Jan 2015 13:21:57 +0100
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 |