|
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 |