testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py

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

mercurial