1# Copyright 2012, Google Inc.
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8#     * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10#     * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14#     * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29"""This file must not depend on any module specific to the WebSocket protocol.
30"""
31
32from __future__ import absolute_import
33from mod_pywebsocket import http_header_util
34
35# Additional log level definitions.
36LOGLEVEL_FINE = 9
37
38# Constants indicating WebSocket protocol version.
39VERSION_HYBI13 = 13
40VERSION_HYBI14 = 13
41VERSION_HYBI15 = 13
42VERSION_HYBI16 = 13
43VERSION_HYBI17 = 13
44
45# Constants indicating WebSocket protocol latest version.
46VERSION_HYBI_LATEST = VERSION_HYBI13
47
48# Port numbers
49DEFAULT_WEB_SOCKET_PORT = 80
50DEFAULT_WEB_SOCKET_SECURE_PORT = 443
51
52# Schemes
53WEB_SOCKET_SCHEME = 'ws'
54WEB_SOCKET_SECURE_SCHEME = 'wss'
55
56# Frame opcodes defined in the spec.
57OPCODE_CONTINUATION = 0x0
58OPCODE_TEXT = 0x1
59OPCODE_BINARY = 0x2
60OPCODE_CLOSE = 0x8
61OPCODE_PING = 0x9
62OPCODE_PONG = 0xa
63
64# UUID for the opening handshake and frame masking.
65WEBSOCKET_ACCEPT_UUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
66
67# Opening handshake header names and expected values.
68UPGRADE_HEADER = 'Upgrade'
69WEBSOCKET_UPGRADE_TYPE = 'websocket'
70CONNECTION_HEADER = 'Connection'
71UPGRADE_CONNECTION_TYPE = 'Upgrade'
72HOST_HEADER = 'Host'
73ORIGIN_HEADER = 'Origin'
74SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
75SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
76SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
77SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
78SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
79
80# Extensions
81PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
82
83# Status codes
84# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
85# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
86# Could not be used for codes in actual closing frames.
87# Application level errors must use codes in the range
88# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
89# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
90# by IANA. Usually application must define user protocol level errors in the
91# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
92STATUS_NORMAL_CLOSURE = 1000
93STATUS_GOING_AWAY = 1001
94STATUS_PROTOCOL_ERROR = 1002
95STATUS_UNSUPPORTED_DATA = 1003
96STATUS_NO_STATUS_RECEIVED = 1005
97STATUS_ABNORMAL_CLOSURE = 1006
98STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
99STATUS_POLICY_VIOLATION = 1008
100STATUS_MESSAGE_TOO_BIG = 1009
101STATUS_MANDATORY_EXTENSION = 1010
102STATUS_INTERNAL_ENDPOINT_ERROR = 1011
103STATUS_TLS_HANDSHAKE = 1015
104STATUS_USER_REGISTERED_BASE = 3000
105STATUS_USER_REGISTERED_MAX = 3999
106STATUS_USER_PRIVATE_BASE = 4000
107STATUS_USER_PRIVATE_MAX = 4999
108# Following definitions are aliases to keep compatibility. Applications must
109# not use these obsoleted definitions anymore.
110STATUS_NORMAL = STATUS_NORMAL_CLOSURE
111STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
112STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
113STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
114STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
115STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
116
117# HTTP status codes
118HTTP_STATUS_BAD_REQUEST = 400
119HTTP_STATUS_FORBIDDEN = 403
120HTTP_STATUS_NOT_FOUND = 404
121
122
123def is_control_opcode(opcode):
124    return (opcode >> 3) == 1
125
126
127class ExtensionParameter(object):
128    """This is exchanged on extension negotiation in opening handshake."""
129    def __init__(self, name):
130        self._name = name
131        # TODO(tyoshino): Change the data structure to more efficient one such
132        # as dict when the spec changes to say like
133        # - Parameter names must be unique
134        # - The order of parameters is not significant
135        self._parameters = []
136
137    def name(self):
138        """Return the extension name."""
139        return self._name
140
141    def add_parameter(self, name, value):
142        """Add a parameter."""
143        self._parameters.append((name, value))
144
145    def get_parameters(self):
146        """Return the parameters."""
147        return self._parameters
148
149    def get_parameter_names(self):
150        """Return the names of the parameters."""
151        return [name for name, unused_value in self._parameters]
152
153    def has_parameter(self, name):
154        """Test if a parameter exists."""
155        for param_name, param_value in self._parameters:
156            if param_name == name:
157                return True
158        return False
159
160    def get_parameter_value(self, name):
161        """Get the value of a specific parameter."""
162        for param_name, param_value in self._parameters:
163            if param_name == name:
164                return param_value
165
166
167class ExtensionParsingException(Exception):
168    """Exception to handle errors in extension parsing."""
169    def __init__(self, name):
170        super(ExtensionParsingException, self).__init__(name)
171
172
173def _parse_extension_param(state, definition):
174    param_name = http_header_util.consume_token(state)
175
176    if param_name is None:
177        raise ExtensionParsingException('No valid parameter name found')
178
179    http_header_util.consume_lwses(state)
180
181    if not http_header_util.consume_string(state, '='):
182        definition.add_parameter(param_name, None)
183        return
184
185    http_header_util.consume_lwses(state)
186
187    # TODO(tyoshino): Add code to validate that parsed param_value is token
188    param_value = http_header_util.consume_token_or_quoted_string(state)
189    if param_value is None:
190        raise ExtensionParsingException(
191            'No valid parameter value found on the right-hand side of '
192            'parameter %r' % param_name)
193
194    definition.add_parameter(param_name, param_value)
195
196
197def _parse_extension(state):
198    extension_token = http_header_util.consume_token(state)
199    if extension_token is None:
200        return None
201
202    extension = ExtensionParameter(extension_token)
203
204    while True:
205        http_header_util.consume_lwses(state)
206
207        if not http_header_util.consume_string(state, ';'):
208            break
209
210        http_header_util.consume_lwses(state)
211
212        try:
213            _parse_extension_param(state, extension)
214        except ExtensionParsingException as e:
215            raise ExtensionParsingException(
216                'Failed to parse parameter for %r (%r)' % (extension_token, e))
217
218    return extension
219
220
221def parse_extensions(data):
222    """Parse Sec-WebSocket-Extensions header value.
223
224    Returns a list of ExtensionParameter objects.
225    Leading LWSes must be trimmed.
226    """
227    state = http_header_util.ParsingState(data)
228
229    extension_list = []
230    while True:
231        extension = _parse_extension(state)
232        if extension is not None:
233            extension_list.append(extension)
234
235        http_header_util.consume_lwses(state)
236
237        if http_header_util.peek(state) is None:
238            break
239
240        if not http_header_util.consume_string(state, ','):
241            raise ExtensionParsingException(
242                'Failed to parse Sec-WebSocket-Extensions header: '
243                'Expected a comma but found %r' % http_header_util.peek(state))
244
245        http_header_util.consume_lwses(state)
246
247    if len(extension_list) == 0:
248        raise ExtensionParsingException('No valid extension entry found')
249
250    return extension_list
251
252
253def format_extension(extension):
254    """Format an ExtensionParameter object."""
255    formatted_params = [extension.name()]
256    for param_name, param_value in extension.get_parameters():
257        if param_value is None:
258            formatted_params.append(param_name)
259        else:
260            quoted_value = http_header_util.quote_if_necessary(param_value)
261            formatted_params.append('%s=%s' % (param_name, quoted_value))
262    return '; '.join(formatted_params)
263
264
265def format_extensions(extension_list):
266    """Format a list of ExtensionParameter objects."""
267    formatted_extension_list = []
268    for extension in extension_list:
269        formatted_extension_list.append(format_extension(extension))
270    return ', '.join(formatted_extension_list)
271
272
273# vi:sts=4 sw=4 et
274