| # Copyright 2012, Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| """This file must not depend on any module specific to the WebSocket protocol. |
| """ |
| |
| from __future__ import absolute_import |
| from mod_pywebsocket import http_header_util |
| |
| # Additional log level definitions. |
| LOGLEVEL_FINE = 9 |
| |
| # Constants indicating WebSocket protocol version. |
| VERSION_HYBI13 = 13 |
| VERSION_HYBI14 = 13 |
| VERSION_HYBI15 = 13 |
| VERSION_HYBI16 = 13 |
| VERSION_HYBI17 = 13 |
| |
| # Constants indicating WebSocket protocol latest version. |
| VERSION_HYBI_LATEST = VERSION_HYBI13 |
| |
| # Port numbers |
| DEFAULT_WEB_SOCKET_PORT = 80 |
| DEFAULT_WEB_SOCKET_SECURE_PORT = 443 |
| |
| # Schemes |
| WEB_SOCKET_SCHEME = 'ws' |
| WEB_SOCKET_SECURE_SCHEME = 'wss' |
| |
| # Frame opcodes defined in the spec. |
| OPCODE_CONTINUATION = 0x0 |
| OPCODE_TEXT = 0x1 |
| OPCODE_BINARY = 0x2 |
| OPCODE_CLOSE = 0x8 |
| OPCODE_PING = 0x9 |
| OPCODE_PONG = 0xa |
| |
| # UUID for the opening handshake and frame masking. |
| WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' |
| |
| # Opening handshake header names and expected values. |
| UPGRADE_HEADER = 'Upgrade' |
| WEBSOCKET_UPGRADE_TYPE = 'websocket' |
| CONNECTION_HEADER = 'Connection' |
| UPGRADE_CONNECTION_TYPE = 'Upgrade' |
| HOST_HEADER = 'Host' |
| ORIGIN_HEADER = 'Origin' |
| SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key' |
| SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept' |
| SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version' |
| SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol' |
| SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions' |
| |
| # Extensions |
| PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' |
| |
| # Status codes |
| # Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and |
| # STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases. |
| # Could not be used for codes in actual closing frames. |
| # Application level errors must use codes in the range |
| # STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the |
| # range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed |
| # by IANA. Usually application must define user protocol level errors in the |
| # range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX. |
| STATUS_NORMAL_CLOSURE = 1000 |
| STATUS_GOING_AWAY = 1001 |
| STATUS_PROTOCOL_ERROR = 1002 |
| STATUS_UNSUPPORTED_DATA = 1003 |
| STATUS_NO_STATUS_RECEIVED = 1005 |
| STATUS_ABNORMAL_CLOSURE = 1006 |
| STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007 |
| STATUS_POLICY_VIOLATION = 1008 |
| STATUS_MESSAGE_TOO_BIG = 1009 |
| STATUS_MANDATORY_EXTENSION = 1010 |
| STATUS_INTERNAL_ENDPOINT_ERROR = 1011 |
| STATUS_TLS_HANDSHAKE = 1015 |
| STATUS_USER_REGISTERED_BASE = 3000 |
| STATUS_USER_REGISTERED_MAX = 3999 |
| STATUS_USER_PRIVATE_BASE = 4000 |
| STATUS_USER_PRIVATE_MAX = 4999 |
| # Following definitions are aliases to keep compatibility. Applications must |
| # not use these obsoleted definitions anymore. |
| STATUS_NORMAL = STATUS_NORMAL_CLOSURE |
| STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA |
| STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED |
| STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE |
| STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA |
| STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION |
| |
| # HTTP status codes |
| HTTP_STATUS_BAD_REQUEST = 400 |
| HTTP_STATUS_FORBIDDEN = 403 |
| HTTP_STATUS_NOT_FOUND = 404 |
| |
| |
| def is_control_opcode(opcode): |
| return (opcode >> 3) == 1 |
| |
| |
| class ExtensionParameter(object): |
| """This is exchanged on extension negotiation in opening handshake.""" |
| def __init__(self, name): |
| self._name = name |
| # TODO(tyoshino): Change the data structure to more efficient one such |
| # as dict when the spec changes to say like |
| # - Parameter names must be unique |
| # - The order of parameters is not significant |
| self._parameters = [] |
| |
| def name(self): |
| """Return the extension name.""" |
| return self._name |
| |
| def add_parameter(self, name, value): |
| """Add a parameter.""" |
| self._parameters.append((name, value)) |
| |
| def get_parameters(self): |
| """Return the parameters.""" |
| return self._parameters |
| |
| def get_parameter_names(self): |
| """Return the names of the parameters.""" |
| return [name for name, unused_value in self._parameters] |
| |
| def has_parameter(self, name): |
| """Test if a parameter exists.""" |
| for param_name, param_value in self._parameters: |
| if param_name == name: |
| return True |
| return False |
| |
| def get_parameter_value(self, name): |
| """Get the value of a specific parameter.""" |
| for param_name, param_value in self._parameters: |
| if param_name == name: |
| return param_value |
| |
| |
| class ExtensionParsingException(Exception): |
| """Exception to handle errors in extension parsing.""" |
| def __init__(self, name): |
| super(ExtensionParsingException, self).__init__(name) |
| |
| |
| def _parse_extension_param(state, definition): |
| param_name = http_header_util.consume_token(state) |
| |
| if param_name is None: |
| raise ExtensionParsingException('No valid parameter name found') |
| |
| http_header_util.consume_lwses(state) |
| |
| if not http_header_util.consume_string(state, '='): |
| definition.add_parameter(param_name, None) |
| return |
| |
| http_header_util.consume_lwses(state) |
| |
| # TODO(tyoshino): Add code to validate that parsed param_value is token |
| param_value = http_header_util.consume_token_or_quoted_string(state) |
| if param_value is None: |
| raise ExtensionParsingException( |
| 'No valid parameter value found on the right-hand side of ' |
| 'parameter %r' % param_name) |
| |
| definition.add_parameter(param_name, param_value) |
| |
| |
| def _parse_extension(state): |
| extension_token = http_header_util.consume_token(state) |
| if extension_token is None: |
| return None |
| |
| extension = ExtensionParameter(extension_token) |
| |
| while True: |
| http_header_util.consume_lwses(state) |
| |
| if not http_header_util.consume_string(state, ';'): |
| break |
| |
| http_header_util.consume_lwses(state) |
| |
| try: |
| _parse_extension_param(state, extension) |
| except ExtensionParsingException as e: |
| raise ExtensionParsingException( |
| 'Failed to parse parameter for %r (%r)' % (extension_token, e)) |
| |
| return extension |
| |
| |
| def parse_extensions(data): |
| """Parse Sec-WebSocket-Extensions header value. |
| |
| Returns a list of ExtensionParameter objects. |
| Leading LWSes must be trimmed. |
| """ |
| state = http_header_util.ParsingState(data) |
| |
| extension_list = [] |
| while True: |
| extension = _parse_extension(state) |
| if extension is not None: |
| extension_list.append(extension) |
| |
| http_header_util.consume_lwses(state) |
| |
| if http_header_util.peek(state) is None: |
| break |
| |
| if not http_header_util.consume_string(state, ','): |
| raise ExtensionParsingException( |
| 'Failed to parse Sec-WebSocket-Extensions header: ' |
| 'Expected a comma but found %r' % http_header_util.peek(state)) |
| |
| http_header_util.consume_lwses(state) |
| |
| if len(extension_list) == 0: |
| raise ExtensionParsingException('No valid extension entry found') |
| |
| return extension_list |
| |
| |
| def format_extension(extension): |
| """Format an ExtensionParameter object.""" |
| formatted_params = [extension.name()] |
| for param_name, param_value in extension.get_parameters(): |
| if param_value is None: |
| formatted_params.append(param_name) |
| else: |
| quoted_value = http_header_util.quote_if_necessary(param_value) |
| formatted_params.append('%s=%s' % (param_name, quoted_value)) |
| return '; '.join(formatted_params) |
| |
| |
| def format_extensions(extension_list): |
| """Format a list of ExtensionParameter objects.""" |
| formatted_extension_list = [] |
| for extension in extension_list: |
| formatted_extension_list.append(format_extension(extension)) |
| return ', '.join(formatted_extension_list) |
| |
| |
| # vi:sts=4 sw=4 et |