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'
105PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
106PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
107X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
108X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress'
109MUX_EXTENSION = 'mux_DO_NOT_USE'
110
111# Status codes
112# Code STATUS_NO_STATUS_RECEIVED, STATUS_ABNORMAL_CLOSURE, and
113# STATUS_TLS_HANDSHAKE are pseudo codes to indicate specific error cases.
114# Could not be used for codes in actual closing frames.
115# Application level errors must use codes in the range
116# STATUS_USER_REGISTERED_BASE to STATUS_USER_PRIVATE_MAX. The codes in the
117# range STATUS_USER_REGISTERED_BASE to STATUS_USER_REGISTERED_MAX are managed
118# by IANA. Usually application must define user protocol level errors in the
119# range STATUS_USER_PRIVATE_BASE to STATUS_USER_PRIVATE_MAX.
120STATUS_NORMAL_CLOSURE = 1000
121STATUS_GOING_AWAY = 1001
122STATUS_PROTOCOL_ERROR = 1002
123STATUS_UNSUPPORTED_DATA = 1003
124STATUS_NO_STATUS_RECEIVED = 1005
125STATUS_ABNORMAL_CLOSURE = 1006
126STATUS_INVALID_FRAME_PAYLOAD_DATA = 1007
127STATUS_POLICY_VIOLATION = 1008
128STATUS_MESSAGE_TOO_BIG = 1009
129STATUS_MANDATORY_EXTENSION = 1010
130STATUS_INTERNAL_ENDPOINT_ERROR = 1011
131STATUS_TLS_HANDSHAKE = 1015
132STATUS_USER_REGISTERED_BASE = 3000
133STATUS_USER_REGISTERED_MAX = 3999
134STATUS_USER_PRIVATE_BASE = 4000
135STATUS_USER_PRIVATE_MAX = 4999
136# Following definitions are aliases to keep compatibility. Applications must
137# not use these obsoleted definitions anymore.
138STATUS_NORMAL = STATUS_NORMAL_CLOSURE
139STATUS_UNSUPPORTED = STATUS_UNSUPPORTED_DATA
140STATUS_CODE_NOT_AVAILABLE = STATUS_NO_STATUS_RECEIVED
141STATUS_ABNORMAL_CLOSE = STATUS_ABNORMAL_CLOSURE
142STATUS_INVALID_FRAME_PAYLOAD = STATUS_INVALID_FRAME_PAYLOAD_DATA
143STATUS_MANDATORY_EXT = STATUS_MANDATORY_EXTENSION
144
145# HTTP status codes
146HTTP_STATUS_BAD_REQUEST = 400
147HTTP_STATUS_FORBIDDEN = 403
148HTTP_STATUS_NOT_FOUND = 404
149
150
151def is_control_opcode(opcode):
152    return (opcode >> 3) == 1
153
154
155class ExtensionParameter(object):
156    """Holds information about an extension which is exchanged on extension
157    negotiation in opening handshake.
158    """
159
160    def __init__(self, name):
161        self._name = name
162        # TODO(tyoshino): Change the data structure to more efficient one such
163        # as dict when the spec changes to say like
164        # - Parameter names must be unique
165        # - The order of parameters is not significant
166        self._parameters = []
167
168    def name(self):
169        return self._name
170
171    def add_parameter(self, name, value):
172        self._parameters.append((name, value))
173
174    def get_parameters(self):
175        return self._parameters
176
177    def get_parameter_names(self):
178        return [name for name, unused_value in self._parameters]
179
180    def has_parameter(self, name):
181        for param_name, param_value in self._parameters:
182            if param_name == name:
183                return True
184        return False
185
186    def get_parameter_value(self, name):
187        for param_name, param_value in self._parameters:
188            if param_name == name:
189                return param_value
190
191
192class ExtensionParsingException(Exception):
193    def __init__(self, name):
194        super(ExtensionParsingException, self).__init__(name)
195
196
197def _parse_extension_param(state, definition):
198    param_name = http_header_util.consume_token(state)
199
200    if param_name is None:
201        raise ExtensionParsingException('No valid parameter name found')
202
203    http_header_util.consume_lwses(state)
204
205    if not http_header_util.consume_string(state, '='):
206        definition.add_parameter(param_name, None)
207        return
208
209    http_header_util.consume_lwses(state)
210
211    # TODO(tyoshino): Add code to validate that parsed param_value is token
212    param_value = http_header_util.consume_token_or_quoted_string(state)
213    if param_value is None:
214        raise ExtensionParsingException(
215            'No valid parameter value found on the right-hand side of '
216            'parameter %r' % param_name)
217
218    definition.add_parameter(param_name, param_value)
219
220
221def _parse_extension(state):
222    extension_token = http_header_util.consume_token(state)
223    if extension_token is None:
224        return None
225
226    extension = ExtensionParameter(extension_token)
227
228    while True:
229        http_header_util.consume_lwses(state)
230
231        if not http_header_util.consume_string(state, ';'):
232            break
233
234        http_header_util.consume_lwses(state)
235
236        try:
237            _parse_extension_param(state, extension)
238        except ExtensionParsingException, e:
239            raise ExtensionParsingException(
240                'Failed to parse parameter for %r (%r)' %
241                (extension_token, e))
242
243    return extension
244
245
246def parse_extensions(data):
247    """Parses Sec-WebSocket-Extensions header value returns a list of
248    ExtensionParameter objects.
249
250    Leading LWSes must be trimmed.
251    """
252
253    state = http_header_util.ParsingState(data)
254
255    extension_list = []
256    while True:
257        extension = _parse_extension(state)
258        if extension is not None:
259            extension_list.append(extension)
260
261        http_header_util.consume_lwses(state)
262
263        if http_header_util.peek(state) is None:
264            break
265
266        if not http_header_util.consume_string(state, ','):
267            raise ExtensionParsingException(
268                'Failed to parse Sec-WebSocket-Extensions header: '
269                'Expected a comma but found %r' %
270                http_header_util.peek(state))
271
272        http_header_util.consume_lwses(state)
273
274    if len(extension_list) == 0:
275        raise ExtensionParsingException(
276            'No valid extension entry found')
277
278    return extension_list
279
280
281def format_extension(extension):
282    """Formats an ExtensionParameter object."""
283
284    formatted_params = [extension.name()]
285    for param_name, param_value in extension.get_parameters():
286        if param_value is None:
287            formatted_params.append(param_name)
288        else:
289            quoted_value = http_header_util.quote_if_necessary(param_value)
290            formatted_params.append('%s=%s' % (param_name, quoted_value))
291    return '; '.join(formatted_params)
292
293
294def format_extensions(extension_list):
295    """Formats a list of ExtensionParameter objects."""
296
297    formatted_extension_list = []
298    for extension in extension_list:
299        formatted_extension_list.append(format_extension(extension))
300    return ', '.join(formatted_extension_list)
301
302
303# vi:sts=4 sw=4 et
304