|
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 """Dispatch WebSocket request. |
|
32 """ |
|
33 |
|
34 |
|
35 import logging |
|
36 import os |
|
37 import re |
|
38 |
|
39 from mod_pywebsocket import common |
|
40 from mod_pywebsocket import handshake |
|
41 from mod_pywebsocket import msgutil |
|
42 from mod_pywebsocket import stream |
|
43 from mod_pywebsocket import util |
|
44 |
|
45 |
|
46 _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') |
|
47 _SOURCE_SUFFIX = '_wsh.py' |
|
48 _DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake' |
|
49 _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' |
|
50 _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( |
|
51 'web_socket_passive_closing_handshake') |
|
52 |
|
53 |
|
54 class DispatchException(Exception): |
|
55 """Exception in dispatching WebSocket request.""" |
|
56 |
|
57 def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): |
|
58 super(DispatchException, self).__init__(name) |
|
59 self.status = status |
|
60 |
|
61 |
|
62 def _default_passive_closing_handshake_handler(request): |
|
63 """Default web_socket_passive_closing_handshake handler.""" |
|
64 |
|
65 return common.STATUS_NORMAL_CLOSURE, '' |
|
66 |
|
67 |
|
68 def _normalize_path(path): |
|
69 """Normalize path. |
|
70 |
|
71 Args: |
|
72 path: the path to normalize. |
|
73 |
|
74 Path is converted to the absolute path. |
|
75 The input path can use either '\\' or '/' as the separator. |
|
76 The normalized path always uses '/' regardless of the platform. |
|
77 """ |
|
78 |
|
79 path = path.replace('\\', os.path.sep) |
|
80 path = os.path.realpath(path) |
|
81 path = path.replace('\\', '/') |
|
82 return path |
|
83 |
|
84 |
|
85 def _create_path_to_resource_converter(base_dir): |
|
86 """Returns a function that converts the path of a WebSocket handler source |
|
87 file to a resource string by removing the path to the base directory from |
|
88 its head, removing _SOURCE_SUFFIX from its tail, and replacing path |
|
89 separators in it with '/'. |
|
90 |
|
91 Args: |
|
92 base_dir: the path to the base directory. |
|
93 """ |
|
94 |
|
95 base_dir = _normalize_path(base_dir) |
|
96 |
|
97 base_len = len(base_dir) |
|
98 suffix_len = len(_SOURCE_SUFFIX) |
|
99 |
|
100 def converter(path): |
|
101 if not path.endswith(_SOURCE_SUFFIX): |
|
102 return None |
|
103 # _normalize_path must not be used because resolving symlink breaks |
|
104 # following path check. |
|
105 path = path.replace('\\', '/') |
|
106 if not path.startswith(base_dir): |
|
107 return None |
|
108 return path[base_len:-suffix_len] |
|
109 |
|
110 return converter |
|
111 |
|
112 |
|
113 def _enumerate_handler_file_paths(directory): |
|
114 """Returns a generator that enumerates WebSocket Handler source file names |
|
115 in the given directory. |
|
116 """ |
|
117 |
|
118 for root, unused_dirs, files in os.walk(directory): |
|
119 for base in files: |
|
120 path = os.path.join(root, base) |
|
121 if _SOURCE_PATH_PATTERN.search(path): |
|
122 yield path |
|
123 |
|
124 |
|
125 class _HandlerSuite(object): |
|
126 """A handler suite holder class.""" |
|
127 |
|
128 def __init__(self, do_extra_handshake, transfer_data, |
|
129 passive_closing_handshake): |
|
130 self.do_extra_handshake = do_extra_handshake |
|
131 self.transfer_data = transfer_data |
|
132 self.passive_closing_handshake = passive_closing_handshake |
|
133 |
|
134 |
|
135 def _source_handler_file(handler_definition): |
|
136 """Source a handler definition string. |
|
137 |
|
138 Args: |
|
139 handler_definition: a string containing Python statements that define |
|
140 handler functions. |
|
141 """ |
|
142 |
|
143 global_dic = {} |
|
144 try: |
|
145 exec handler_definition in global_dic |
|
146 except Exception: |
|
147 raise DispatchException('Error in sourcing handler:' + |
|
148 util.get_stack_trace()) |
|
149 passive_closing_handshake_handler = None |
|
150 try: |
|
151 passive_closing_handshake_handler = _extract_handler( |
|
152 global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME) |
|
153 except Exception: |
|
154 passive_closing_handshake_handler = ( |
|
155 _default_passive_closing_handshake_handler) |
|
156 return _HandlerSuite( |
|
157 _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), |
|
158 _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME), |
|
159 passive_closing_handshake_handler) |
|
160 |
|
161 |
|
162 def _extract_handler(dic, name): |
|
163 """Extracts a callable with the specified name from the given dictionary |
|
164 dic. |
|
165 """ |
|
166 |
|
167 if name not in dic: |
|
168 raise DispatchException('%s is not defined.' % name) |
|
169 handler = dic[name] |
|
170 if not callable(handler): |
|
171 raise DispatchException('%s is not callable.' % name) |
|
172 return handler |
|
173 |
|
174 |
|
175 class Dispatcher(object): |
|
176 """Dispatches WebSocket requests. |
|
177 |
|
178 This class maintains a map from resource name to handlers. |
|
179 """ |
|
180 |
|
181 def __init__( |
|
182 self, root_dir, scan_dir=None, |
|
183 allow_handlers_outside_root_dir=True): |
|
184 """Construct an instance. |
|
185 |
|
186 Args: |
|
187 root_dir: The directory where handler definition files are |
|
188 placed. |
|
189 scan_dir: The directory where handler definition files are |
|
190 searched. scan_dir must be a directory under root_dir, |
|
191 including root_dir itself. If scan_dir is None, |
|
192 root_dir is used as scan_dir. scan_dir can be useful |
|
193 in saving scan time when root_dir contains many |
|
194 subdirectories. |
|
195 allow_handlers_outside_root_dir: Scans handler files even if their |
|
196 canonical path is not under root_dir. |
|
197 """ |
|
198 |
|
199 self._logger = util.get_class_logger(self) |
|
200 |
|
201 self._handler_suite_map = {} |
|
202 self._source_warnings = [] |
|
203 if scan_dir is None: |
|
204 scan_dir = root_dir |
|
205 if not os.path.realpath(scan_dir).startswith( |
|
206 os.path.realpath(root_dir)): |
|
207 raise DispatchException('scan_dir:%s must be a directory under ' |
|
208 'root_dir:%s.' % (scan_dir, root_dir)) |
|
209 self._source_handler_files_in_dir( |
|
210 root_dir, scan_dir, allow_handlers_outside_root_dir) |
|
211 |
|
212 def add_resource_path_alias(self, |
|
213 alias_resource_path, existing_resource_path): |
|
214 """Add resource path alias. |
|
215 |
|
216 Once added, request to alias_resource_path would be handled by |
|
217 handler registered for existing_resource_path. |
|
218 |
|
219 Args: |
|
220 alias_resource_path: alias resource path |
|
221 existing_resource_path: existing resource path |
|
222 """ |
|
223 try: |
|
224 handler_suite = self._handler_suite_map[existing_resource_path] |
|
225 self._handler_suite_map[alias_resource_path] = handler_suite |
|
226 except KeyError: |
|
227 raise DispatchException('No handler for: %r' % |
|
228 existing_resource_path) |
|
229 |
|
230 def source_warnings(self): |
|
231 """Return warnings in sourcing handlers.""" |
|
232 |
|
233 return self._source_warnings |
|
234 |
|
235 def do_extra_handshake(self, request): |
|
236 """Do extra checking in WebSocket handshake. |
|
237 |
|
238 Select a handler based on request.uri and call its |
|
239 web_socket_do_extra_handshake function. |
|
240 |
|
241 Args: |
|
242 request: mod_python request. |
|
243 |
|
244 Raises: |
|
245 DispatchException: when handler was not found |
|
246 AbortedByUserException: when user handler abort connection |
|
247 HandshakeException: when opening handshake failed |
|
248 """ |
|
249 |
|
250 handler_suite = self.get_handler_suite(request.ws_resource) |
|
251 if handler_suite is None: |
|
252 raise DispatchException('No handler for: %r' % request.ws_resource) |
|
253 do_extra_handshake_ = handler_suite.do_extra_handshake |
|
254 try: |
|
255 do_extra_handshake_(request) |
|
256 except handshake.AbortedByUserException, e: |
|
257 raise |
|
258 except Exception, e: |
|
259 util.prepend_message_to_exception( |
|
260 '%s raised exception for %s: ' % ( |
|
261 _DO_EXTRA_HANDSHAKE_HANDLER_NAME, |
|
262 request.ws_resource), |
|
263 e) |
|
264 raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) |
|
265 |
|
266 def transfer_data(self, request): |
|
267 """Let a handler transfer_data with a WebSocket client. |
|
268 |
|
269 Select a handler based on request.ws_resource and call its |
|
270 web_socket_transfer_data function. |
|
271 |
|
272 Args: |
|
273 request: mod_python request. |
|
274 |
|
275 Raises: |
|
276 DispatchException: when handler was not found |
|
277 AbortedByUserException: when user handler abort connection |
|
278 """ |
|
279 |
|
280 handler_suite = self.get_handler_suite(request.ws_resource) |
|
281 if handler_suite is None: |
|
282 raise DispatchException('No handler for: %r' % request.ws_resource) |
|
283 transfer_data_ = handler_suite.transfer_data |
|
284 # TODO(tyoshino): Terminate underlying TCP connection if possible. |
|
285 try: |
|
286 transfer_data_(request) |
|
287 if not request.server_terminated: |
|
288 request.ws_stream.close_connection() |
|
289 # Catch non-critical exceptions the handler didn't handle. |
|
290 except handshake.AbortedByUserException, e: |
|
291 self._logger.debug('%s', e) |
|
292 raise |
|
293 except msgutil.BadOperationException, e: |
|
294 self._logger.debug('%s', e) |
|
295 request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE) |
|
296 except msgutil.InvalidFrameException, e: |
|
297 # InvalidFrameException must be caught before |
|
298 # ConnectionTerminatedException that catches InvalidFrameException. |
|
299 self._logger.debug('%s', e) |
|
300 request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) |
|
301 except msgutil.UnsupportedFrameException, e: |
|
302 self._logger.debug('%s', e) |
|
303 request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) |
|
304 except stream.InvalidUTF8Exception, e: |
|
305 self._logger.debug('%s', e) |
|
306 request.ws_stream.close_connection( |
|
307 common.STATUS_INVALID_FRAME_PAYLOAD_DATA) |
|
308 except msgutil.ConnectionTerminatedException, e: |
|
309 self._logger.debug('%s', e) |
|
310 except Exception, e: |
|
311 util.prepend_message_to_exception( |
|
312 '%s raised exception for %s: ' % ( |
|
313 _TRANSFER_DATA_HANDLER_NAME, request.ws_resource), |
|
314 e) |
|
315 raise |
|
316 |
|
317 def passive_closing_handshake(self, request): |
|
318 """Prepare code and reason for responding client initiated closing |
|
319 handshake. |
|
320 """ |
|
321 |
|
322 handler_suite = self.get_handler_suite(request.ws_resource) |
|
323 if handler_suite is None: |
|
324 return _default_passive_closing_handshake_handler(request) |
|
325 return handler_suite.passive_closing_handshake(request) |
|
326 |
|
327 def get_handler_suite(self, resource): |
|
328 """Retrieves two handlers (one for extra handshake processing, and one |
|
329 for data transfer) for the given request as a HandlerSuite object. |
|
330 """ |
|
331 |
|
332 fragment = None |
|
333 if '#' in resource: |
|
334 resource, fragment = resource.split('#', 1) |
|
335 if '?' in resource: |
|
336 resource = resource.split('?', 1)[0] |
|
337 handler_suite = self._handler_suite_map.get(resource) |
|
338 if handler_suite and fragment: |
|
339 raise DispatchException('Fragment identifiers MUST NOT be used on ' |
|
340 'WebSocket URIs', |
|
341 common.HTTP_STATUS_BAD_REQUEST) |
|
342 return handler_suite |
|
343 |
|
344 def _source_handler_files_in_dir( |
|
345 self, root_dir, scan_dir, allow_handlers_outside_root_dir): |
|
346 """Source all the handler source files in the scan_dir directory. |
|
347 |
|
348 The resource path is determined relative to root_dir. |
|
349 """ |
|
350 |
|
351 # We build a map from resource to handler code assuming that there's |
|
352 # only one path from root_dir to scan_dir and it can be obtained by |
|
353 # comparing realpath of them. |
|
354 |
|
355 # Here we cannot use abspath. See |
|
356 # https://bugs.webkit.org/show_bug.cgi?id=31603 |
|
357 |
|
358 convert = _create_path_to_resource_converter(root_dir) |
|
359 scan_realpath = os.path.realpath(scan_dir) |
|
360 root_realpath = os.path.realpath(root_dir) |
|
361 for path in _enumerate_handler_file_paths(scan_realpath): |
|
362 if (not allow_handlers_outside_root_dir and |
|
363 (not os.path.realpath(path).startswith(root_realpath))): |
|
364 self._logger.debug( |
|
365 'Canonical path of %s is not under root directory' % |
|
366 path) |
|
367 continue |
|
368 try: |
|
369 handler_suite = _source_handler_file(open(path).read()) |
|
370 except DispatchException, e: |
|
371 self._source_warnings.append('%s: %s' % (path, e)) |
|
372 continue |
|
373 resource = convert(path) |
|
374 if resource is None: |
|
375 self._logger.debug( |
|
376 'Path to resource conversion on %s failed' % path) |
|
377 else: |
|
378 self._handler_suite_map[convert(path)] = handler_suite |
|
379 |
|
380 |
|
381 # vi:sts=4 sw=4 et |