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
30
31"""This file must not depend on any module specific to the WebSocket protocol.
32"""
33
34
35from mod_pywebsocket import http_header_util
36
37
38# Additional log level definitions.
39LOGLEVEL_FINE = 9
40
41# Constants indicating WebSocket protocol version.
42VERSION_HIXIE75 = -1
43VERSION_HYBI00 = 0
44VERSION_HYBI01 = 1
45VERSION_HYBI02 = 2
46VERSION_HYBI03 = 2
47VERSION_HYBI04 = 4
48VERSION_HYBI05 = 5
49VERSION_HYBI06 = 6
50VERSION_HYBI07 = 7
51VERSION_HYBI08 = 8
52VERSION_HYBI09 = 8
53VERSION_HYBI10 = 8
54VERSION_HYBI11 = 8
55VERSION_HYBI12 = 8
56VERSION_HYBI13 = 13
57VERSION_HYBI14 = 13
58VERSION_HYBI15 = 13
59VERSION_HYBI16 = 13
60VERSION_HYBI17 = 13
61
62# Constants indicating WebSocket protocol latest version.
63VERSION_HYBI_LATEST = VERSION_HYBI13
64
65# Port numbers
66DEFAULT_WEB_SOCKET_PORT = 80
67DEFAULT_WEB_SOCKET_SECURE_PORT = 443
68
69# Schemes
70WEB_SOCKET_SCHEME = 'ws'
71WEB_SOCKET_SECURE_SCHEME = 'wss'
72
73# Frame opcodes defined in the spec.
74OPCODE_CONTINUATION = 0x0
75OPCODE_TEXT = 0x1
76OPCODE_BINARY = 0x2
77OPCODE_CLOSE = 0x8
78OPCODE_PING = 0x9
79OPCODE_PONG = 0xa
80
81# UUIDs used by HyBi 04 and later opening handshake and frame masking.
82WEBSOCKET_ACCEPT_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
83
84# Opening handshake header names and expected values.
85UPGRADE_HEADER = 'Upgrade'
86WEBSOCKET_UPGRADE_TYPE = 'websocket'
87WEBSOCKET_UPGRADE_TYPE_HIXIE75 = 'WebSocket'
88CONNECTION_HEADER = 'Connection'
89UPGRADE_CONNECTION_TYPE = 'Upgrade'
90HOST_HEADER = 'Host'
91ORIGIN_HEADER = 'Origin'
92SEC_WEBSOCKET_ORIGIN_HEADER = 'Sec-WebSocket-Origin'
93SEC_WEBSOCKET_KEY_HEADER = 'Sec-WebSocket-Key'
94SEC_WEBSOCKET_ACCEPT_HEADER = 'Sec-WebSocket-Accept'
95SEC_WEBSOCKET_VERSION_HEADER = 'Sec-WebSocket-Version'
96SEC_WEBSOCKET_PROTOCOL_HEADER = 'Sec-WebSocket-Protocol'
97SEC_WEBSOCKET_EXTENSIONS_HEADER = 'Sec-WebSocket-Extensions'
98SEC_WEBSOCKET_DRAFT_HEADER = 'Sec-WebSocket-Draft'
99SEC_WEBSOCKET_KEY1_HEADER = 'Sec-WebSocket-Key1'
100SEC_WEBSOCKET_KEY2_HEADER = 'Sec-WebSocket-Key2'
101SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
102
103# Extensions
104DEFLATE_FRAME_EXTENSION = 'deflate-frame'
105PERFRAME_COMPRESSION_EXTENSION = 'perframe-compress'
106PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
107PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
108X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
109X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress'
110MUX_EXTENSION = 'mux_DO_NOT_USE'
111
112# Status codes
113# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
114# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
115# Could not be used for codes in actual closing frames.
116# Application level errors must use codes in the range
117# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
118# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
119# by IANA. Usually application must define user protocol level errors in the
120# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
121STATUS_NORMAL_CLOSURE = 1000
122STATUS_GOING_AWAY = 1001
123STATUS_PROTOCOL_ERROR = 1002
124STATUS_UNSUPPORTED_DATA = 1003
125STATUS_NO_STATUS_RECEIVED = 1005
126STATUS_ABNORMAL_CLOSURE = 1006
127STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
128STATUS_POLICY_VIOLATION = 1008
129STATUS_MESSAGE_TOO_BIG = 1009
130STATUS_MANDATORY_EXTENSION = 1010
131STATUS_INTERNAL_ENDPOINT_ERROR = 1011
132STATUS_TLS_HANDSHAKE = 1015
133STATUS_USER_REGISTERED_BASE = 3000
134STATUS_USER_REGISTERED_MAX = 3999
135STATUS_USER_PRIVATE_BASE = 4000
136STATUS_USER_PRIVATE_MAX = 4999
137# Following definitions are aliases to keep compatibility. Applications must
138# not use these obsoleted definitions anymore.
139STATUS_NORMAL = STATUS_NORMAL_CLOSURE
140STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
141STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
142STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
143STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
144STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
145
146# HTTP status codes
147HTTP_STATUS_BAD_REQUEST = 400
148HTTP_STATUS_FORBIDDEN = 403
149HTTP_STATUS_NOT_FOUND = 404
150
151
152def is_control_opcode(opcode):
153    return (opcode >> 3) == 1
154
155
156class ExtensionParameter(object):
157    """Holds information about an extension which is exchanged on extension
158    negotiation in opening handshake.
159    """
160
161    def __init__(self, name):
162        self._name = name
163        # TODO(tyoshino): Change the data structure to more efficient one such
164        # as dict when the spec changes to say like
165        # - Parameter names must be unique
166        # - The order of parameters is not significant
167        self._parameters = []
168
169    def name(self):
170        return self._name
171
172    def add_parameter(self, name, value):
173        self._parameters.append((name, value))
174
175    def get_parameters(self):
176        return self._parameters
177
178    def get_parameter_names(self):
179        return [name for name, unused_value in self._parameters]
180
181    def has_parameter(self, name):
182        for param_name, param_value in self._parameters:
183            if param_name == name:
184                return True
185        return False
186
187    def get_parameter_value(self, name):
188        for param_name, param_value in self._parameters:
189            if param_name == name:
190                return param_value
191
192
193class ExtensionParsingException(Exception):
194    def __init__(self, name):
195        super(ExtensionParsingException, self).__init__(name)
196
197
198def _parse_extension_param(state, definition, allow_quoted_string):
199    param_name = http_header_util.consume_token(state)
200
201    if param_name is None:
202        raise ExtensionParsingException('No valid parameter name found')
203
204    http_header_util.consume_lwses(state)
205
206    if not http_header_util.consume_string(state, '='):
207        definition.add_parameter(param_name, None)
208        return
209
210    http_header_util.consume_lwses(state)
211
212    if allow_quoted_string:
213        # TODO(toyoshim): Add code to validate that parsed param_value is token
214        param_value = http_header_util.consume_token_or_quoted_string(state)
215    else:
216        param_value = http_header_util.consume_token(state)
217    if param_value is None:
218        raise ExtensionParsingException(
219            'No valid parameter value found on the right-hand side of '
220            'parameter %r' % param_name)
221
222    definition.add_parameter(param_name, param_value)
223
224
225def _parse_extension(state, allow_quoted_string):
226    extension_token = http_header_util.consume_token(state)
227    if extension_token is None:
228        return None
229
230    extension = ExtensionParameter(extension_token)
231
232    while True:
233        http_header_util.consume_lwses(state)
234
235        if not http_header_util.consume_string(state, ';'):
236            break
237
238        http_header_util.consume_lwses(state)
239
240        try:
241            _parse_extension_param(state, extension, allow_quoted_string)
242        except ExtensionParsingException, e:
243            raise ExtensionParsingException(
244                'Failed to parse parameter for %r (%r)' %
245                (extension_token, e))
246
247    return extension
248
249
250def parse_extensions(data, allow_quoted_string=False):
251    """Parses Sec-WebSocket-Extensions header value returns a list of
252    ExtensionParameter objects.
253
254    Leading LWSes must be trimmed.
255    """
256
257    state = http_header_util.ParsingState(data)
258
259    extension_list = []
260    while True:
261        extension = _parse_extension(state, allow_quoted_string)
262        if extension is not None:
263            extension_list.append(extension)
264
265        http_header_util.consume_lwses(state)
266
267        if http_header_util.peek(state) is None:
268            break
269
270        if not http_header_util.consume_string(state, ','):
271            raise ExtensionParsingException(
272                'Failed to parse Sec-WebSocket-Extensions header: '
273                'Expected a comma but found %r' %
274                http_header_util.peek(state))
275
276        http_header_util.consume_lwses(state)
277
278    if len(extension_list) == 0:
279        raise ExtensionParsingException(
280            'No valid extension entry found')
281
282    return extension_list
283
284
285def format_extension(extension):
286    """Formats an ExtensionParameter object."""
287
288    formatted_params = [extension.name()]
289    for param_name, param_value in extension.get_parameters():
290        if param_value is None:
291            formatted_params.append(param_name)
292        else:
293            quoted_value = http_header_util.quote_if_necessary(param_value)
294            formatted_params.append('%s=%s' % (param_name, quoted_value))
295    return '; '.join(formatted_params)
296
297
298def format_extensions(extension_list):
299    """Formats a list of ExtensionParameter objects."""
300
301    formatted_extension_list = []
302    for extension in extension_list:
303        formatted_extension_list.append(format_extension(extension))
304    return ', '.join(formatted_extension_list)
305
306
307# vi:sts=4 sw=4 et
308