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 | """This file provides the opening handshake processor for the WebSocket |
michael@0 | 32 | protocol (RFC 6455). |
michael@0 | 33 | |
michael@0 | 34 | Specification: |
michael@0 | 35 | http://tools.ietf.org/html/rfc6455 |
michael@0 | 36 | """ |
michael@0 | 37 | |
michael@0 | 38 | |
michael@0 | 39 | # Note: request.connection.write is used in this module, even though mod_python |
michael@0 | 40 | # document says that it should be used only in connection handlers. |
michael@0 | 41 | # Unfortunately, we have no other options. For example, request.write is not |
michael@0 | 42 | # suitable because it doesn't allow direct raw bytes writing. |
michael@0 | 43 | |
michael@0 | 44 | |
michael@0 | 45 | import base64 |
michael@0 | 46 | import logging |
michael@0 | 47 | import os |
michael@0 | 48 | import re |
michael@0 | 49 | |
michael@0 | 50 | from mod_pywebsocket import common |
michael@0 | 51 | from mod_pywebsocket.extensions import get_extension_processor |
michael@0 | 52 | from mod_pywebsocket.handshake._base import check_request_line |
michael@0 | 53 | from mod_pywebsocket.handshake._base import format_extensions |
michael@0 | 54 | from mod_pywebsocket.handshake._base import format_header |
michael@0 | 55 | from mod_pywebsocket.handshake._base import get_mandatory_header |
michael@0 | 56 | from mod_pywebsocket.handshake._base import HandshakeException |
michael@0 | 57 | from mod_pywebsocket.handshake._base import parse_extensions |
michael@0 | 58 | from mod_pywebsocket.handshake._base import parse_token_list |
michael@0 | 59 | from mod_pywebsocket.handshake._base import validate_mandatory_header |
michael@0 | 60 | from mod_pywebsocket.handshake._base import validate_subprotocol |
michael@0 | 61 | from mod_pywebsocket.handshake._base import VersionException |
michael@0 | 62 | from mod_pywebsocket.stream import Stream |
michael@0 | 63 | from mod_pywebsocket.stream import StreamOptions |
michael@0 | 64 | from mod_pywebsocket import util |
michael@0 | 65 | |
michael@0 | 66 | |
michael@0 | 67 | # Used to validate the value in the Sec-WebSocket-Key header strictly. RFC 4648 |
michael@0 | 68 | # disallows non-zero padding, so the character right before == must be any of |
michael@0 | 69 | # A, Q, g and w. |
michael@0 | 70 | _SEC_WEBSOCKET_KEY_REGEX = re.compile('^[+/0-9A-Za-z]{21}[AQgw]==$') |
michael@0 | 71 | |
michael@0 | 72 | # Defining aliases for values used frequently. |
michael@0 | 73 | _VERSION_HYBI08 = common.VERSION_HYBI08 |
michael@0 | 74 | _VERSION_HYBI08_STRING = str(_VERSION_HYBI08) |
michael@0 | 75 | _VERSION_LATEST = common.VERSION_HYBI_LATEST |
michael@0 | 76 | _VERSION_LATEST_STRING = str(_VERSION_LATEST) |
michael@0 | 77 | _SUPPORTED_VERSIONS = [ |
michael@0 | 78 | _VERSION_LATEST, |
michael@0 | 79 | _VERSION_HYBI08, |
michael@0 | 80 | ] |
michael@0 | 81 | |
michael@0 | 82 | |
michael@0 | 83 | def compute_accept(key): |
michael@0 | 84 | """Computes value for the Sec-WebSocket-Accept header from value of the |
michael@0 | 85 | Sec-WebSocket-Key header. |
michael@0 | 86 | """ |
michael@0 | 87 | |
michael@0 | 88 | accept_binary = util.sha1_hash( |
michael@0 | 89 | key + common.WEBSOCKET_ACCEPT_UUID).digest() |
michael@0 | 90 | accept = base64.b64encode(accept_binary) |
michael@0 | 91 | |
michael@0 | 92 | return (accept, accept_binary) |
michael@0 | 93 | |
michael@0 | 94 | |
michael@0 | 95 | class Handshaker(object): |
michael@0 | 96 | """Opening handshake processor for the WebSocket protocol (RFC 6455).""" |
michael@0 | 97 | |
michael@0 | 98 | def __init__(self, request, dispatcher): |
michael@0 | 99 | """Construct an instance. |
michael@0 | 100 | |
michael@0 | 101 | Args: |
michael@0 | 102 | request: mod_python request. |
michael@0 | 103 | dispatcher: Dispatcher (dispatch.Dispatcher). |
michael@0 | 104 | |
michael@0 | 105 | Handshaker will add attributes such as ws_resource during handshake. |
michael@0 | 106 | """ |
michael@0 | 107 | |
michael@0 | 108 | self._logger = util.get_class_logger(self) |
michael@0 | 109 | |
michael@0 | 110 | self._request = request |
michael@0 | 111 | self._dispatcher = dispatcher |
michael@0 | 112 | |
michael@0 | 113 | def _validate_connection_header(self): |
michael@0 | 114 | connection = get_mandatory_header( |
michael@0 | 115 | self._request, common.CONNECTION_HEADER) |
michael@0 | 116 | |
michael@0 | 117 | try: |
michael@0 | 118 | connection_tokens = parse_token_list(connection) |
michael@0 | 119 | except HandshakeException, e: |
michael@0 | 120 | raise HandshakeException( |
michael@0 | 121 | 'Failed to parse %s: %s' % (common.CONNECTION_HEADER, e)) |
michael@0 | 122 | |
michael@0 | 123 | connection_is_valid = False |
michael@0 | 124 | for token in connection_tokens: |
michael@0 | 125 | if token.lower() == common.UPGRADE_CONNECTION_TYPE.lower(): |
michael@0 | 126 | connection_is_valid = True |
michael@0 | 127 | break |
michael@0 | 128 | if not connection_is_valid: |
michael@0 | 129 | raise HandshakeException( |
michael@0 | 130 | '%s header doesn\'t contain "%s"' % |
michael@0 | 131 | (common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) |
michael@0 | 132 | |
michael@0 | 133 | def do_handshake(self): |
michael@0 | 134 | self._request.ws_close_code = None |
michael@0 | 135 | self._request.ws_close_reason = None |
michael@0 | 136 | |
michael@0 | 137 | # Parsing. |
michael@0 | 138 | |
michael@0 | 139 | check_request_line(self._request) |
michael@0 | 140 | |
michael@0 | 141 | validate_mandatory_header( |
michael@0 | 142 | self._request, |
michael@0 | 143 | common.UPGRADE_HEADER, |
michael@0 | 144 | common.WEBSOCKET_UPGRADE_TYPE) |
michael@0 | 145 | |
michael@0 | 146 | self._validate_connection_header() |
michael@0 | 147 | |
michael@0 | 148 | self._request.ws_resource = self._request.uri |
michael@0 | 149 | |
michael@0 | 150 | unused_host = get_mandatory_header(self._request, common.HOST_HEADER) |
michael@0 | 151 | |
michael@0 | 152 | self._request.ws_version = self._check_version() |
michael@0 | 153 | |
michael@0 | 154 | # This handshake must be based on latest hybi. We are responsible to |
michael@0 | 155 | # fallback to HTTP on handshake failure as latest hybi handshake |
michael@0 | 156 | # specifies. |
michael@0 | 157 | try: |
michael@0 | 158 | self._get_origin() |
michael@0 | 159 | self._set_protocol() |
michael@0 | 160 | self._parse_extensions() |
michael@0 | 161 | |
michael@0 | 162 | # Key validation, response generation. |
michael@0 | 163 | |
michael@0 | 164 | key = self._get_key() |
michael@0 | 165 | (accept, accept_binary) = compute_accept(key) |
michael@0 | 166 | self._logger.debug( |
michael@0 | 167 | '%s: %r (%s)', |
michael@0 | 168 | common.SEC_WEBSOCKET_ACCEPT_HEADER, |
michael@0 | 169 | accept, |
michael@0 | 170 | util.hexify(accept_binary)) |
michael@0 | 171 | |
michael@0 | 172 | self._logger.debug('Protocol version is RFC 6455') |
michael@0 | 173 | |
michael@0 | 174 | # Setup extension processors. |
michael@0 | 175 | |
michael@0 | 176 | processors = [] |
michael@0 | 177 | if self._request.ws_requested_extensions is not None: |
michael@0 | 178 | for extension_request in self._request.ws_requested_extensions: |
michael@0 | 179 | processor = get_extension_processor(extension_request) |
michael@0 | 180 | # Unknown extension requests are just ignored. |
michael@0 | 181 | if processor is not None: |
michael@0 | 182 | processors.append(processor) |
michael@0 | 183 | self._request.ws_extension_processors = processors |
michael@0 | 184 | |
michael@0 | 185 | # Extra handshake handler may modify/remove processors. |
michael@0 | 186 | self._dispatcher.do_extra_handshake(self._request) |
michael@0 | 187 | |
michael@0 | 188 | stream_options = StreamOptions() |
michael@0 | 189 | |
michael@0 | 190 | self._request.ws_extensions = None |
michael@0 | 191 | for processor in self._request.ws_extension_processors: |
michael@0 | 192 | if processor is None: |
michael@0 | 193 | # Some processors may be removed by extra handshake |
michael@0 | 194 | # handler. |
michael@0 | 195 | continue |
michael@0 | 196 | |
michael@0 | 197 | extension_response = processor.get_extension_response() |
michael@0 | 198 | if extension_response is None: |
michael@0 | 199 | # Rejected. |
michael@0 | 200 | continue |
michael@0 | 201 | |
michael@0 | 202 | if self._request.ws_extensions is None: |
michael@0 | 203 | self._request.ws_extensions = [] |
michael@0 | 204 | self._request.ws_extensions.append(extension_response) |
michael@0 | 205 | |
michael@0 | 206 | processor.setup_stream_options(stream_options) |
michael@0 | 207 | |
michael@0 | 208 | if self._request.ws_extensions is not None: |
michael@0 | 209 | self._logger.debug( |
michael@0 | 210 | 'Extensions accepted: %r', |
michael@0 | 211 | map(common.ExtensionParameter.name, |
michael@0 | 212 | self._request.ws_extensions)) |
michael@0 | 213 | |
michael@0 | 214 | self._request.ws_stream = Stream(self._request, stream_options) |
michael@0 | 215 | |
michael@0 | 216 | if self._request.ws_requested_protocols is not None: |
michael@0 | 217 | if self._request.ws_protocol is None: |
michael@0 | 218 | raise HandshakeException( |
michael@0 | 219 | 'do_extra_handshake must choose one subprotocol from ' |
michael@0 | 220 | 'ws_requested_protocols and set it to ws_protocol') |
michael@0 | 221 | validate_subprotocol(self._request.ws_protocol, hixie=False) |
michael@0 | 222 | |
michael@0 | 223 | self._logger.debug( |
michael@0 | 224 | 'Subprotocol accepted: %r', |
michael@0 | 225 | self._request.ws_protocol) |
michael@0 | 226 | else: |
michael@0 | 227 | if self._request.ws_protocol is not None: |
michael@0 | 228 | raise HandshakeException( |
michael@0 | 229 | 'ws_protocol must be None when the client didn\'t ' |
michael@0 | 230 | 'request any subprotocol') |
michael@0 | 231 | |
michael@0 | 232 | self._send_handshake(accept) |
michael@0 | 233 | except HandshakeException, e: |
michael@0 | 234 | if not e.status: |
michael@0 | 235 | # Fallback to 400 bad request by default. |
michael@0 | 236 | e.status = common.HTTP_STATUS_BAD_REQUEST |
michael@0 | 237 | raise e |
michael@0 | 238 | |
michael@0 | 239 | def _get_origin(self): |
michael@0 | 240 | if self._request.ws_version is _VERSION_HYBI08: |
michael@0 | 241 | origin_header = common.SEC_WEBSOCKET_ORIGIN_HEADER |
michael@0 | 242 | else: |
michael@0 | 243 | origin_header = common.ORIGIN_HEADER |
michael@0 | 244 | origin = self._request.headers_in.get(origin_header) |
michael@0 | 245 | if origin is None: |
michael@0 | 246 | self._logger.debug('Client request does not have origin header') |
michael@0 | 247 | self._request.ws_origin = origin |
michael@0 | 248 | |
michael@0 | 249 | def _check_version(self): |
michael@0 | 250 | version = get_mandatory_header(self._request, |
michael@0 | 251 | common.SEC_WEBSOCKET_VERSION_HEADER) |
michael@0 | 252 | if version == _VERSION_HYBI08_STRING: |
michael@0 | 253 | return _VERSION_HYBI08 |
michael@0 | 254 | if version == _VERSION_LATEST_STRING: |
michael@0 | 255 | return _VERSION_LATEST |
michael@0 | 256 | |
michael@0 | 257 | if version.find(',') >= 0: |
michael@0 | 258 | raise HandshakeException( |
michael@0 | 259 | 'Multiple versions (%r) are not allowed for header %s' % |
michael@0 | 260 | (version, common.SEC_WEBSOCKET_VERSION_HEADER), |
michael@0 | 261 | status=common.HTTP_STATUS_BAD_REQUEST) |
michael@0 | 262 | raise VersionException( |
michael@0 | 263 | 'Unsupported version %r for header %s' % |
michael@0 | 264 | (version, common.SEC_WEBSOCKET_VERSION_HEADER), |
michael@0 | 265 | supported_versions=', '.join(map(str, _SUPPORTED_VERSIONS))) |
michael@0 | 266 | |
michael@0 | 267 | def _set_protocol(self): |
michael@0 | 268 | self._request.ws_protocol = None |
michael@0 | 269 | # MOZILLA |
michael@0 | 270 | self._request.sts = None |
michael@0 | 271 | # /MOZILLA |
michael@0 | 272 | |
michael@0 | 273 | protocol_header = self._request.headers_in.get( |
michael@0 | 274 | common.SEC_WEBSOCKET_PROTOCOL_HEADER) |
michael@0 | 275 | |
michael@0 | 276 | if not protocol_header: |
michael@0 | 277 | self._request.ws_requested_protocols = None |
michael@0 | 278 | return |
michael@0 | 279 | |
michael@0 | 280 | self._request.ws_requested_protocols = parse_token_list( |
michael@0 | 281 | protocol_header) |
michael@0 | 282 | self._logger.debug('Subprotocols requested: %r', |
michael@0 | 283 | self._request.ws_requested_protocols) |
michael@0 | 284 | |
michael@0 | 285 | def _parse_extensions(self): |
michael@0 | 286 | extensions_header = self._request.headers_in.get( |
michael@0 | 287 | common.SEC_WEBSOCKET_EXTENSIONS_HEADER) |
michael@0 | 288 | if not extensions_header: |
michael@0 | 289 | self._request.ws_requested_extensions = None |
michael@0 | 290 | return |
michael@0 | 291 | |
michael@0 | 292 | if self._request.ws_version is common.VERSION_HYBI08: |
michael@0 | 293 | allow_quoted_string=False |
michael@0 | 294 | else: |
michael@0 | 295 | allow_quoted_string=True |
michael@0 | 296 | self._request.ws_requested_extensions = parse_extensions( |
michael@0 | 297 | extensions_header, allow_quoted_string=allow_quoted_string) |
michael@0 | 298 | |
michael@0 | 299 | self._logger.debug( |
michael@0 | 300 | 'Extensions requested: %r', |
michael@0 | 301 | map(common.ExtensionParameter.name, |
michael@0 | 302 | self._request.ws_requested_extensions)) |
michael@0 | 303 | |
michael@0 | 304 | def _validate_key(self, key): |
michael@0 | 305 | if key.find(',') >= 0: |
michael@0 | 306 | raise HandshakeException('Request has multiple %s header lines or ' |
michael@0 | 307 | 'contains illegal character \',\': %r' % |
michael@0 | 308 | (common.SEC_WEBSOCKET_KEY_HEADER, key)) |
michael@0 | 309 | |
michael@0 | 310 | # Validate |
michael@0 | 311 | key_is_valid = False |
michael@0 | 312 | try: |
michael@0 | 313 | # Validate key by quick regex match before parsing by base64 |
michael@0 | 314 | # module. Because base64 module skips invalid characters, we have |
michael@0 | 315 | # to do this in advance to make this server strictly reject illegal |
michael@0 | 316 | # keys. |
michael@0 | 317 | if _SEC_WEBSOCKET_KEY_REGEX.match(key): |
michael@0 | 318 | decoded_key = base64.b64decode(key) |
michael@0 | 319 | if len(decoded_key) == 16: |
michael@0 | 320 | key_is_valid = True |
michael@0 | 321 | except TypeError, e: |
michael@0 | 322 | pass |
michael@0 | 323 | |
michael@0 | 324 | if not key_is_valid: |
michael@0 | 325 | raise HandshakeException( |
michael@0 | 326 | 'Illegal value for header %s: %r' % |
michael@0 | 327 | (common.SEC_WEBSOCKET_KEY_HEADER, key)) |
michael@0 | 328 | |
michael@0 | 329 | return decoded_key |
michael@0 | 330 | |
michael@0 | 331 | def _get_key(self): |
michael@0 | 332 | key = get_mandatory_header( |
michael@0 | 333 | self._request, common.SEC_WEBSOCKET_KEY_HEADER) |
michael@0 | 334 | |
michael@0 | 335 | decoded_key = self._validate_key(key) |
michael@0 | 336 | |
michael@0 | 337 | self._logger.debug( |
michael@0 | 338 | '%s: %r (%s)', |
michael@0 | 339 | common.SEC_WEBSOCKET_KEY_HEADER, |
michael@0 | 340 | key, |
michael@0 | 341 | util.hexify(decoded_key)) |
michael@0 | 342 | |
michael@0 | 343 | return key |
michael@0 | 344 | |
michael@0 | 345 | def _send_handshake(self, accept): |
michael@0 | 346 | response = [] |
michael@0 | 347 | |
michael@0 | 348 | response.append('HTTP/1.1 101 Switching Protocols\r\n') |
michael@0 | 349 | |
michael@0 | 350 | response.append(format_header( |
michael@0 | 351 | common.UPGRADE_HEADER, common.WEBSOCKET_UPGRADE_TYPE)) |
michael@0 | 352 | response.append(format_header( |
michael@0 | 353 | common.CONNECTION_HEADER, common.UPGRADE_CONNECTION_TYPE)) |
michael@0 | 354 | response.append(format_header( |
michael@0 | 355 | common.SEC_WEBSOCKET_ACCEPT_HEADER, accept)) |
michael@0 | 356 | if self._request.ws_protocol is not None: |
michael@0 | 357 | response.append(format_header( |
michael@0 | 358 | common.SEC_WEBSOCKET_PROTOCOL_HEADER, |
michael@0 | 359 | self._request.ws_protocol)) |
michael@0 | 360 | if (self._request.ws_extensions is not None and |
michael@0 | 361 | len(self._request.ws_extensions) != 0): |
michael@0 | 362 | response.append(format_header( |
michael@0 | 363 | common.SEC_WEBSOCKET_EXTENSIONS_HEADER, |
michael@0 | 364 | format_extensions(self._request.ws_extensions))) |
michael@0 | 365 | # MOZILLA: Add HSTS header if requested to |
michael@0 | 366 | if self._request.sts is not None: |
michael@0 | 367 | response.append(format_header("Strict-Transport-Security", |
michael@0 | 368 | self._request.sts)) |
michael@0 | 369 | # /MOZILLA |
michael@0 | 370 | response.append('\r\n') |
michael@0 | 371 | |
michael@0 | 372 | raw_response = ''.join(response) |
michael@0 | 373 | self._request.connection.write(raw_response) |
michael@0 | 374 | self._logger.debug('Sent server\'s opening handshake: %r', |
michael@0 | 375 | raw_response) |
michael@0 | 376 | |
michael@0 | 377 | |
michael@0 | 378 | # vi:sts=4 sw=4 et |