testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial