1 # Copyright 2012, Google Inc.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
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
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.
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.
31 """Dispatch WebSocket request.
39 from mod_pywebsocket import common
40 from mod_pywebsocket import handshake
41 from mod_pywebsocket import msgutil
42 from mod_pywebsocket import mux
43 from mod_pywebsocket import stream
44 from mod_pywebsocket import util
47 _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
48 _SOURCE_SUFFIX = '_wsh.py'
49 _DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
50 _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
51 _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
52 'web_socket_passive_closing_handshake')
55 class DispatchException(Exception):
56 """Exception in dispatching WebSocket request."""
58 def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
59 super(DispatchException, self).__init__(name)
63 def _default_passive_closing_handshake_handler(request):
64 """Default web_socket_passive_closing_handshake handler."""
66 return common.STATUS_NORMAL_CLOSURE, ''
69 def _normalize_path(path):
73 path: the path to normalize.
75 Path is converted to the absolute path.
76 The input path can use either '\\' or '/' as the separator.
77 The normalized path always uses '/' regardless of the platform.
80 path = path.replace('\\', os.path.sep)
81 path = os.path.realpath(path)
82 path = path.replace('\\', '/')
86 def _create_path_to_resource_converter(base_dir):
87 """Returns a function that converts the path of a WebSocket handler source
88 file to a resource string by removing the path to the base directory from
89 its head, removing _SOURCE_SUFFIX from its tail, and replacing path
90 separators in it with '/'.
93 base_dir: the path to the base directory.
96 base_dir = _normalize_path(base_dir)
98 base_len = len(base_dir)
99 suffix_len = len(_SOURCE_SUFFIX)
102 if not path.endswith(_SOURCE_SUFFIX):
104 # _normalize_path must not be used because resolving symlink breaks
105 # following path check.
106 path = path.replace('\\', '/')
107 if not path.startswith(base_dir):
109 return path[base_len:-suffix_len]
114 def _enumerate_handler_file_paths(directory):
115 """Returns a generator that enumerates WebSocket Handler source file names
116 in the given directory.
119 for root, unused_dirs, files in os.walk(directory):
121 path = os.path.join(root, base)
122 if _SOURCE_PATH_PATTERN.search(path):
126 class _HandlerSuite(object):
127 """A handler suite holder class."""
129 def __init__(self, do_extra_handshake, transfer_data,
130 passive_closing_handshake):
131 self.do_extra_handshake = do_extra_handshake
132 self.transfer_data = transfer_data
133 self.passive_closing_handshake = passive_closing_handshake
136 def _source_handler_file(handler_definition):
137 """Source a handler definition string.
140 handler_definition: a string containing Python statements that define
146 exec handler_definition in global_dic
148 raise DispatchException('Error in sourcing handler:' +
149 util.get_stack_trace())
150 passive_closing_handshake_handler = None
152 passive_closing_handshake_handler = _extract_handler(
153 global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
155 passive_closing_handshake_handler = (
156 _default_passive_closing_handshake_handler)
157 return _HandlerSuite(
158 _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
159 _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
160 passive_closing_handshake_handler)
163 def _extract_handler(dic, name):
164 """Extracts a callable with the specified name from the given dictionary
169 raise DispatchException('%s is not defined.' % name)
171 if not callable(handler):
172 raise DispatchException('%s is not callable.' % name)
176 class Dispatcher(object):
177 """Dispatches WebSocket requests.
179 This class maintains a map from resource name to handlers.
183 self, root_dir, scan_dir=None,
184 allow_handlers_outside_root_dir=True):
185 """Construct an instance.
188 root_dir: The directory where handler definition files are
190 scan_dir: The directory where handler definition files are
191 searched. scan_dir must be a directory under root_dir,
192 including root_dir itself. If scan_dir is None,
193 root_dir is used as scan_dir. scan_dir can be useful
194 in saving scan time when root_dir contains many
196 allow_handlers_outside_root_dir: Scans handler files even if their
197 canonical path is not under root_dir.
200 self._logger = util.get_class_logger(self)
202 self._handler_suite_map = {}
203 self._source_warnings = []
206 if not os.path.realpath(scan_dir).startswith(
207 os.path.realpath(root_dir)):
208 raise DispatchException('scan_dir:%s must be a directory under '
209 'root_dir:%s.' % (scan_dir, root_dir))
210 self._source_handler_files_in_dir(
211 root_dir, scan_dir, allow_handlers_outside_root_dir)
213 def add_resource_path_alias(self,
214 alias_resource_path, existing_resource_path):
215 """Add resource path alias.
217 Once added, request to alias_resource_path would be handled by
218 handler registered for existing_resource_path.
221 alias_resource_path: alias resource path
222 existing_resource_path: existing resource path
225 handler_suite = self._handler_suite_map[existing_resource_path]
226 self._handler_suite_map[alias_resource_path] = handler_suite
228 raise DispatchException('No handler for: %r' %
229 existing_resource_path)
231 def source_warnings(self):
232 """Return warnings in sourcing handlers."""
234 return self._source_warnings
236 def do_extra_handshake(self, request):
237 """Do extra checking in WebSocket handshake.
239 Select a handler based on request.uri and call its
240 web_socket_do_extra_handshake function.
243 request: mod_python request.
246 DispatchException: when handler was not found
247 AbortedByUserException: when user handler abort connection
248 HandshakeException: when opening handshake failed
251 handler_suite = self.get_handler_suite(request.ws_resource)
252 if handler_suite is None:
253 raise DispatchException('No handler for: %r' % request.ws_resource)
254 do_extra_handshake_ = handler_suite.do_extra_handshake
256 do_extra_handshake_(request)
257 except handshake.AbortedByUserException, e:
260 util.prepend_message_to_exception(
261 '%s raised exception for %s: ' % (
262 _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
263 request.ws_resource),
265 raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
267 def transfer_data(self, request):
268 """Let a handler transfer_data with a WebSocket client.
270 Select a handler based on request.ws_resource and call its
271 web_socket_transfer_data function.
274 request: mod_python request.
277 DispatchException: when handler was not found
278 AbortedByUserException: when user handler abort connection
281 # TODO(tyoshino): Terminate underlying TCP connection if possible.
283 if mux.use_mux(request):
284 mux.start(request, self)
286 handler_suite = self.get_handler_suite(request.ws_resource)
287 if handler_suite is None:
288 raise DispatchException('No handler for: %r' %
290 transfer_data_ = handler_suite.transfer_data
291 transfer_data_(request)
293 if not request.server_terminated:
294 request.ws_stream.close_connection()
295 # Catch non-critical exceptions the handler didn't handle.
296 except handshake.AbortedByUserException, e:
297 self._logger.debug('%s', e)
299 except msgutil.BadOperationException, e:
300 self._logger.debug('%s', e)
301 request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE)
302 except msgutil.InvalidFrameException, e:
303 # InvalidFrameException must be caught before
304 # ConnectionTerminatedException that catches InvalidFrameException.
305 self._logger.debug('%s', e)
306 request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
307 except msgutil.UnsupportedFrameException, e:
308 self._logger.debug('%s', e)
309 request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
310 except stream.InvalidUTF8Exception, e:
311 self._logger.debug('%s', e)
312 request.ws_stream.close_connection(
313 common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
314 except msgutil.ConnectionTerminatedException, e:
315 self._logger.debug('%s', e)
317 util.prepend_message_to_exception(
318 '%s raised exception for %s: ' % (
319 _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
323 def passive_closing_handshake(self, request):
324 """Prepare code and reason for responding client initiated closing
328 handler_suite = self.get_handler_suite(request.ws_resource)
329 if handler_suite is None:
330 return _default_passive_closing_handshake_handler(request)
331 return handler_suite.passive_closing_handshake(request)
333 def get_handler_suite(self, resource):
334 """Retrieves two handlers (one for extra handshake processing, and one
335 for data transfer) for the given request as a HandlerSuite object.
340 resource, fragment = resource.split('#', 1)
342 resource = resource.split('?', 1)[0]
343 handler_suite = self._handler_suite_map.get(resource)
344 if handler_suite and fragment:
345 raise DispatchException('Fragment identifiers MUST NOT be used on '
347 common.HTTP_STATUS_BAD_REQUEST)
350 def _source_handler_files_in_dir(
351 self, root_dir, scan_dir, allow_handlers_outside_root_dir):
352 """Source all the handler source files in the scan_dir directory.
354 The resource path is determined relative to root_dir.
357 # We build a map from resource to handler code assuming that there's
358 # only one path from root_dir to scan_dir and it can be obtained by
359 # comparing realpath of them.
361 # Here we cannot use abspath. See
362 # https://bugs.webkit.org/show_bug.cgi?id=31603
364 convert = _create_path_to_resource_converter(root_dir)
365 scan_realpath = os.path.realpath(scan_dir)
366 root_realpath = os.path.realpath(root_dir)
367 for path in _enumerate_handler_file_paths(scan_realpath):
368 if (not allow_handlers_outside_root_dir and
369 (not os.path.realpath(path).startswith(root_realpath))):
371 'Canonical path of %s is not under root directory' %
375 handler_suite = _source_handler_file(open(path).read())
376 except DispatchException, e:
377 self._source_warnings.append('%s: %s' % (path, e))
379 resource = convert(path)
382 'Path to resource conversion on %s failed' % path)
384 self._handler_suite_map[convert(path)] = handler_suite