testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,381 @@
     1.4 +# Copyright 2012, Google Inc.
     1.5 +# All rights reserved.
     1.6 +#
     1.7 +# Redistribution and use in source and binary forms, with or without
     1.8 +# modification, are permitted provided that the following conditions are
     1.9 +# met:
    1.10 +#
    1.11 +#     * Redistributions of source code must retain the above copyright
    1.12 +# notice, this list of conditions and the following disclaimer.
    1.13 +#     * Redistributions in binary form must reproduce the above
    1.14 +# copyright notice, this list of conditions and the following disclaimer
    1.15 +# in the documentation and/or other materials provided with the
    1.16 +# distribution.
    1.17 +#     * Neither the name of Google Inc. nor the names of its
    1.18 +# contributors may be used to endorse or promote products derived from
    1.19 +# this software without specific prior written permission.
    1.20 +#
    1.21 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    1.22 +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    1.23 +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    1.24 +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    1.25 +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    1.26 +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    1.27 +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    1.28 +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    1.29 +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    1.30 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    1.31 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    1.32 +
    1.33 +
    1.34 +"""Dispatch WebSocket request.
    1.35 +"""
    1.36 +
    1.37 +
    1.38 +import logging
    1.39 +import os
    1.40 +import re
    1.41 +
    1.42 +from mod_pywebsocket import common
    1.43 +from mod_pywebsocket import handshake
    1.44 +from mod_pywebsocket import msgutil
    1.45 +from mod_pywebsocket import stream
    1.46 +from mod_pywebsocket import util
    1.47 +
    1.48 +
    1.49 +_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
    1.50 +_SOURCE_SUFFIX = '_wsh.py'
    1.51 +_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
    1.52 +_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
    1.53 +_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
    1.54 +    'web_socket_passive_closing_handshake')
    1.55 +
    1.56 +
    1.57 +class DispatchException(Exception):
    1.58 +    """Exception in dispatching WebSocket request."""
    1.59 +
    1.60 +    def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
    1.61 +        super(DispatchException, self).__init__(name)
    1.62 +        self.status = status
    1.63 +
    1.64 +
    1.65 +def _default_passive_closing_handshake_handler(request):
    1.66 +    """Default web_socket_passive_closing_handshake handler."""
    1.67 +
    1.68 +    return common.STATUS_NORMAL_CLOSURE, ''
    1.69 +
    1.70 +
    1.71 +def _normalize_path(path):
    1.72 +    """Normalize path.
    1.73 +
    1.74 +    Args:
    1.75 +        path: the path to normalize.
    1.76 +
    1.77 +    Path is converted to the absolute path.
    1.78 +    The input path can use either '\\' or '/' as the separator.
    1.79 +    The normalized path always uses '/' regardless of the platform.
    1.80 +    """
    1.81 +
    1.82 +    path = path.replace('\\', os.path.sep)
    1.83 +    path = os.path.realpath(path)
    1.84 +    path = path.replace('\\', '/')
    1.85 +    return path
    1.86 +
    1.87 +
    1.88 +def _create_path_to_resource_converter(base_dir):
    1.89 +    """Returns a function that converts the path of a WebSocket handler source
    1.90 +    file to a resource string by removing the path to the base directory from
    1.91 +    its head, removing _SOURCE_SUFFIX from its tail, and replacing path
    1.92 +    separators in it with '/'.
    1.93 +
    1.94 +    Args:
    1.95 +        base_dir: the path to the base directory.
    1.96 +    """
    1.97 +
    1.98 +    base_dir = _normalize_path(base_dir)
    1.99 +
   1.100 +    base_len = len(base_dir)
   1.101 +    suffix_len = len(_SOURCE_SUFFIX)
   1.102 +
   1.103 +    def converter(path):
   1.104 +        if not path.endswith(_SOURCE_SUFFIX):
   1.105 +            return None
   1.106 +        # _normalize_path must not be used because resolving symlink breaks
   1.107 +        # following path check.
   1.108 +        path = path.replace('\\', '/')
   1.109 +        if not path.startswith(base_dir):
   1.110 +            return None
   1.111 +        return path[base_len:-suffix_len]
   1.112 +
   1.113 +    return converter
   1.114 +
   1.115 +
   1.116 +def _enumerate_handler_file_paths(directory):
   1.117 +    """Returns a generator that enumerates WebSocket Handler source file names
   1.118 +    in the given directory.
   1.119 +    """
   1.120 +
   1.121 +    for root, unused_dirs, files in os.walk(directory):
   1.122 +        for base in files:
   1.123 +            path = os.path.join(root, base)
   1.124 +            if _SOURCE_PATH_PATTERN.search(path):
   1.125 +                yield path
   1.126 +
   1.127 +
   1.128 +class _HandlerSuite(object):
   1.129 +    """A handler suite holder class."""
   1.130 +
   1.131 +    def __init__(self, do_extra_handshake, transfer_data,
   1.132 +                 passive_closing_handshake):
   1.133 +        self.do_extra_handshake = do_extra_handshake
   1.134 +        self.transfer_data = transfer_data
   1.135 +        self.passive_closing_handshake = passive_closing_handshake
   1.136 +
   1.137 +
   1.138 +def _source_handler_file(handler_definition):
   1.139 +    """Source a handler definition string.
   1.140 +
   1.141 +    Args:
   1.142 +        handler_definition: a string containing Python statements that define
   1.143 +                            handler functions.
   1.144 +    """
   1.145 +
   1.146 +    global_dic = {}
   1.147 +    try:
   1.148 +        exec handler_definition in global_dic
   1.149 +    except Exception:
   1.150 +        raise DispatchException('Error in sourcing handler:' +
   1.151 +                                util.get_stack_trace())
   1.152 +    passive_closing_handshake_handler = None
   1.153 +    try:
   1.154 +        passive_closing_handshake_handler = _extract_handler(
   1.155 +            global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
   1.156 +    except Exception:
   1.157 +        passive_closing_handshake_handler = (
   1.158 +            _default_passive_closing_handshake_handler)
   1.159 +    return _HandlerSuite(
   1.160 +        _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
   1.161 +        _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
   1.162 +        passive_closing_handshake_handler)
   1.163 +
   1.164 +
   1.165 +def _extract_handler(dic, name):
   1.166 +    """Extracts a callable with the specified name from the given dictionary
   1.167 +    dic.
   1.168 +    """
   1.169 +
   1.170 +    if name not in dic:
   1.171 +        raise DispatchException('%s is not defined.' % name)
   1.172 +    handler = dic[name]
   1.173 +    if not callable(handler):
   1.174 +        raise DispatchException('%s is not callable.' % name)
   1.175 +    return handler
   1.176 +
   1.177 +
   1.178 +class Dispatcher(object):
   1.179 +    """Dispatches WebSocket requests.
   1.180 +
   1.181 +    This class maintains a map from resource name to handlers.
   1.182 +    """
   1.183 +
   1.184 +    def __init__(
   1.185 +        self, root_dir, scan_dir=None,
   1.186 +        allow_handlers_outside_root_dir=True):
   1.187 +        """Construct an instance.
   1.188 +
   1.189 +        Args:
   1.190 +            root_dir: The directory where handler definition files are
   1.191 +                      placed.
   1.192 +            scan_dir: The directory where handler definition files are
   1.193 +                      searched. scan_dir must be a directory under root_dir,
   1.194 +                      including root_dir itself.  If scan_dir is None,
   1.195 +                      root_dir is used as scan_dir. scan_dir can be useful
   1.196 +                      in saving scan time when root_dir contains many
   1.197 +                      subdirectories.
   1.198 +            allow_handlers_outside_root_dir: Scans handler files even if their
   1.199 +                      canonical path is not under root_dir.
   1.200 +        """
   1.201 +
   1.202 +        self._logger = util.get_class_logger(self)
   1.203 +
   1.204 +        self._handler_suite_map = {}
   1.205 +        self._source_warnings = []
   1.206 +        if scan_dir is None:
   1.207 +            scan_dir = root_dir
   1.208 +        if not os.path.realpath(scan_dir).startswith(
   1.209 +                os.path.realpath(root_dir)):
   1.210 +            raise DispatchException('scan_dir:%s must be a directory under '
   1.211 +                                    'root_dir:%s.' % (scan_dir, root_dir))
   1.212 +        self._source_handler_files_in_dir(
   1.213 +            root_dir, scan_dir, allow_handlers_outside_root_dir)
   1.214 +
   1.215 +    def add_resource_path_alias(self,
   1.216 +                                alias_resource_path, existing_resource_path):
   1.217 +        """Add resource path alias.
   1.218 +
   1.219 +        Once added, request to alias_resource_path would be handled by
   1.220 +        handler registered for existing_resource_path.
   1.221 +
   1.222 +        Args:
   1.223 +            alias_resource_path: alias resource path
   1.224 +            existing_resource_path: existing resource path
   1.225 +        """
   1.226 +        try:
   1.227 +            handler_suite = self._handler_suite_map[existing_resource_path]
   1.228 +            self._handler_suite_map[alias_resource_path] = handler_suite
   1.229 +        except KeyError:
   1.230 +            raise DispatchException('No handler for: %r' %
   1.231 +                                    existing_resource_path)
   1.232 +
   1.233 +    def source_warnings(self):
   1.234 +        """Return warnings in sourcing handlers."""
   1.235 +
   1.236 +        return self._source_warnings
   1.237 +
   1.238 +    def do_extra_handshake(self, request):
   1.239 +        """Do extra checking in WebSocket handshake.
   1.240 +
   1.241 +        Select a handler based on request.uri and call its
   1.242 +        web_socket_do_extra_handshake function.
   1.243 +
   1.244 +        Args:
   1.245 +            request: mod_python request.
   1.246 +
   1.247 +        Raises:
   1.248 +            DispatchException: when handler was not found
   1.249 +            AbortedByUserException: when user handler abort connection
   1.250 +            HandshakeException: when opening handshake failed
   1.251 +        """
   1.252 +
   1.253 +        handler_suite = self.get_handler_suite(request.ws_resource)
   1.254 +        if handler_suite is None:
   1.255 +            raise DispatchException('No handler for: %r' % request.ws_resource)
   1.256 +        do_extra_handshake_ = handler_suite.do_extra_handshake
   1.257 +        try:
   1.258 +            do_extra_handshake_(request)
   1.259 +        except handshake.AbortedByUserException, e:
   1.260 +            raise
   1.261 +        except Exception, e:
   1.262 +            util.prepend_message_to_exception(
   1.263 +                    '%s raised exception for %s: ' % (
   1.264 +                            _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
   1.265 +                            request.ws_resource),
   1.266 +                    e)
   1.267 +            raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
   1.268 +
   1.269 +    def transfer_data(self, request):
   1.270 +        """Let a handler transfer_data with a WebSocket client.
   1.271 +
   1.272 +        Select a handler based on request.ws_resource and call its
   1.273 +        web_socket_transfer_data function.
   1.274 +
   1.275 +        Args:
   1.276 +            request: mod_python request.
   1.277 +
   1.278 +        Raises:
   1.279 +            DispatchException: when handler was not found
   1.280 +            AbortedByUserException: when user handler abort connection
   1.281 +        """
   1.282 +
   1.283 +        handler_suite = self.get_handler_suite(request.ws_resource)
   1.284 +        if handler_suite is None:
   1.285 +            raise DispatchException('No handler for: %r' % request.ws_resource)
   1.286 +        transfer_data_ = handler_suite.transfer_data
   1.287 +        # TODO(tyoshino): Terminate underlying TCP connection if possible.
   1.288 +        try:
   1.289 +            transfer_data_(request)
   1.290 +            if not request.server_terminated:
   1.291 +                request.ws_stream.close_connection()
   1.292 +        # Catch non-critical exceptions the handler didn't handle.
   1.293 +        except handshake.AbortedByUserException, e:
   1.294 +            self._logger.debug('%s', e)
   1.295 +            raise
   1.296 +        except msgutil.BadOperationException, e:
   1.297 +            self._logger.debug('%s', e)
   1.298 +            request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE)
   1.299 +        except msgutil.InvalidFrameException, e:
   1.300 +            # InvalidFrameException must be caught before
   1.301 +            # ConnectionTerminatedException that catches InvalidFrameException.
   1.302 +            self._logger.debug('%s', e)
   1.303 +            request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
   1.304 +        except msgutil.UnsupportedFrameException, e:
   1.305 +            self._logger.debug('%s', e)
   1.306 +            request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
   1.307 +        except stream.InvalidUTF8Exception, e:
   1.308 +            self._logger.debug('%s', e)
   1.309 +            request.ws_stream.close_connection(
   1.310 +                common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
   1.311 +        except msgutil.ConnectionTerminatedException, e:
   1.312 +            self._logger.debug('%s', e)
   1.313 +        except Exception, e:
   1.314 +            util.prepend_message_to_exception(
   1.315 +                '%s raised exception for %s: ' % (
   1.316 +                    _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
   1.317 +                e)
   1.318 +            raise
   1.319 +
   1.320 +    def passive_closing_handshake(self, request):
   1.321 +        """Prepare code and reason for responding client initiated closing
   1.322 +        handshake.
   1.323 +        """
   1.324 +
   1.325 +        handler_suite = self.get_handler_suite(request.ws_resource)
   1.326 +        if handler_suite is None:
   1.327 +            return _default_passive_closing_handshake_handler(request)
   1.328 +        return handler_suite.passive_closing_handshake(request)
   1.329 +
   1.330 +    def get_handler_suite(self, resource):
   1.331 +        """Retrieves two handlers (one for extra handshake processing, and one
   1.332 +        for data transfer) for the given request as a HandlerSuite object.
   1.333 +        """
   1.334 +
   1.335 +        fragment = None
   1.336 +        if '#' in resource:
   1.337 +            resource, fragment = resource.split('#', 1)
   1.338 +        if '?' in resource:
   1.339 +            resource = resource.split('?', 1)[0]
   1.340 +        handler_suite = self._handler_suite_map.get(resource)
   1.341 +        if handler_suite and fragment:
   1.342 +            raise DispatchException('Fragment identifiers MUST NOT be used on '
   1.343 +                                    'WebSocket URIs',
   1.344 +                                    common.HTTP_STATUS_BAD_REQUEST)
   1.345 +        return handler_suite
   1.346 +
   1.347 +    def _source_handler_files_in_dir(
   1.348 +        self, root_dir, scan_dir, allow_handlers_outside_root_dir):
   1.349 +        """Source all the handler source files in the scan_dir directory.
   1.350 +
   1.351 +        The resource path is determined relative to root_dir.
   1.352 +        """
   1.353 +
   1.354 +        # We build a map from resource to handler code assuming that there's
   1.355 +        # only one path from root_dir to scan_dir and it can be obtained by
   1.356 +        # comparing realpath of them.
   1.357 +
   1.358 +        # Here we cannot use abspath. See
   1.359 +        # https://bugs.webkit.org/show_bug.cgi?id=31603
   1.360 +
   1.361 +        convert = _create_path_to_resource_converter(root_dir)
   1.362 +        scan_realpath = os.path.realpath(scan_dir)
   1.363 +        root_realpath = os.path.realpath(root_dir)
   1.364 +        for path in _enumerate_handler_file_paths(scan_realpath):
   1.365 +            if (not allow_handlers_outside_root_dir and
   1.366 +                (not os.path.realpath(path).startswith(root_realpath))):
   1.367 +                self._logger.debug(
   1.368 +                    'Canonical path of %s is not under root directory' %
   1.369 +                    path)
   1.370 +                continue
   1.371 +            try:
   1.372 +                handler_suite = _source_handler_file(open(path).read())
   1.373 +            except DispatchException, e:
   1.374 +                self._source_warnings.append('%s: %s' % (path, e))
   1.375 +                continue
   1.376 +            resource = convert(path)
   1.377 +            if resource is None:
   1.378 +                self._logger.debug(
   1.379 +                    'Path to resource conversion on %s failed' % path)
   1.380 +            else:
   1.381 +                self._handler_suite_map[convert(path)] = handler_suite
   1.382 +
   1.383 +
   1.384 +# vi:sts=4 sw=4 et

mercurial