forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			393 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			393 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2012, Google Inc.
 | 
						|
# All rights reserved.
 | 
						|
#
 | 
						|
# Redistribution and use in source and binary forms, with or without
 | 
						|
# modification, are permitted provided that the following conditions are
 | 
						|
# met:
 | 
						|
#
 | 
						|
#     * Redistributions of source code must retain the above copyright
 | 
						|
# notice, this list of conditions and the following disclaimer.
 | 
						|
#     * Redistributions in binary form must reproduce the above
 | 
						|
# copyright notice, this list of conditions and the following disclaimer
 | 
						|
# in the documentation and/or other materials provided with the
 | 
						|
# distribution.
 | 
						|
#     * Neither the name of Google Inc. nor the names of its
 | 
						|
# contributors may be used to endorse or promote products derived from
 | 
						|
# this software without specific prior written permission.
 | 
						|
#
 | 
						|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 | 
						|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 | 
						|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | 
						|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 | 
						|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 | 
						|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 | 
						|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 | 
						|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 | 
						|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | 
						|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 | 
						|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | 
						|
 | 
						|
 | 
						|
"""Dispatch WebSocket request.
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import re
 | 
						|
 | 
						|
from mod_pywebsocket import common
 | 
						|
from mod_pywebsocket import handshake
 | 
						|
from mod_pywebsocket import msgutil
 | 
						|
from mod_pywebsocket import mux
 | 
						|
from mod_pywebsocket import stream
 | 
						|
from mod_pywebsocket import util
 | 
						|
 | 
						|
 | 
						|
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
 | 
						|
_SOURCE_SUFFIX = '_wsh.py'
 | 
						|
_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
 | 
						|
_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
 | 
						|
_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = (
 | 
						|
    'web_socket_passive_closing_handshake')
 | 
						|
 | 
						|
 | 
						|
class DispatchException(Exception):
 | 
						|
    """Exception in dispatching WebSocket request."""
 | 
						|
 | 
						|
    def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
 | 
						|
        super(DispatchException, self).__init__(name)
 | 
						|
        self.status = status
 | 
						|
 | 
						|
 | 
						|
def _default_passive_closing_handshake_handler(request):
 | 
						|
    """Default web_socket_passive_closing_handshake handler."""
 | 
						|
 | 
						|
    return common.STATUS_NORMAL_CLOSURE, ''
 | 
						|
 | 
						|
 | 
						|
def _normalize_path(path):
 | 
						|
    """Normalize path.
 | 
						|
 | 
						|
    Args:
 | 
						|
        path: the path to normalize.
 | 
						|
 | 
						|
    Path is converted to the absolute path.
 | 
						|
    The input path can use either '\\' or '/' as the separator.
 | 
						|
    The normalized path always uses '/' regardless of the platform.
 | 
						|
    """
 | 
						|
 | 
						|
    path = path.replace('\\', os.path.sep)
 | 
						|
    path = os.path.realpath(path)
 | 
						|
    path = path.replace('\\', '/')
 | 
						|
    return path
 | 
						|
 | 
						|
 | 
						|
def _create_path_to_resource_converter(base_dir):
 | 
						|
    """Returns a function that converts the path of a WebSocket handler source
 | 
						|
    file to a resource string by removing the path to the base directory from
 | 
						|
    its head, removing _SOURCE_SUFFIX from its tail, and replacing path
 | 
						|
    separators in it with '/'.
 | 
						|
 | 
						|
    Args:
 | 
						|
        base_dir: the path to the base directory.
 | 
						|
    """
 | 
						|
 | 
						|
    base_dir = _normalize_path(base_dir)
 | 
						|
 | 
						|
    base_len = len(base_dir)
 | 
						|
    suffix_len = len(_SOURCE_SUFFIX)
 | 
						|
 | 
						|
    def converter(path):
 | 
						|
        if not path.endswith(_SOURCE_SUFFIX):
 | 
						|
            return None
 | 
						|
        # _normalize_path must not be used because resolving symlink breaks
 | 
						|
        # following path check.
 | 
						|
        path = path.replace('\\', '/')
 | 
						|
        if not path.startswith(base_dir):
 | 
						|
            return None
 | 
						|
        return path[base_len:-suffix_len]
 | 
						|
 | 
						|
    return converter
 | 
						|
 | 
						|
 | 
						|
def _enumerate_handler_file_paths(directory):
 | 
						|
    """Returns a generator that enumerates WebSocket Handler source file names
 | 
						|
    in the given directory.
 | 
						|
    """
 | 
						|
 | 
						|
    for root, unused_dirs, files in os.walk(directory):
 | 
						|
        for base in files:
 | 
						|
            path = os.path.join(root, base)
 | 
						|
            if _SOURCE_PATH_PATTERN.search(path):
 | 
						|
                yield path
 | 
						|
 | 
						|
 | 
						|
class _HandlerSuite(object):
 | 
						|
    """A handler suite holder class."""
 | 
						|
 | 
						|
    def __init__(self, do_extra_handshake, transfer_data,
 | 
						|
                 passive_closing_handshake):
 | 
						|
        self.do_extra_handshake = do_extra_handshake
 | 
						|
        self.transfer_data = transfer_data
 | 
						|
        self.passive_closing_handshake = passive_closing_handshake
 | 
						|
 | 
						|
 | 
						|
def _source_handler_file(handler_definition):
 | 
						|
    """Source a handler definition string.
 | 
						|
 | 
						|
    Args:
 | 
						|
        handler_definition: a string containing Python statements that define
 | 
						|
                            handler functions.
 | 
						|
    """
 | 
						|
 | 
						|
    global_dic = {}
 | 
						|
    try:
 | 
						|
        exec handler_definition in global_dic
 | 
						|
    except Exception:
 | 
						|
        raise DispatchException('Error in sourcing handler:' +
 | 
						|
                                util.get_stack_trace())
 | 
						|
    passive_closing_handshake_handler = None
 | 
						|
    try:
 | 
						|
        passive_closing_handshake_handler = _extract_handler(
 | 
						|
            global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
 | 
						|
    except Exception:
 | 
						|
        passive_closing_handshake_handler = (
 | 
						|
            _default_passive_closing_handshake_handler)
 | 
						|
    return _HandlerSuite(
 | 
						|
        _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
 | 
						|
        _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME),
 | 
						|
        passive_closing_handshake_handler)
 | 
						|
 | 
						|
 | 
						|
def _extract_handler(dic, name):
 | 
						|
    """Extracts a callable with the specified name from the given dictionary
 | 
						|
    dic.
 | 
						|
    """
 | 
						|
 | 
						|
    if name not in dic:
 | 
						|
        raise DispatchException('%s is not defined.' % name)
 | 
						|
    handler = dic[name]
 | 
						|
    if not callable(handler):
 | 
						|
        raise DispatchException('%s is not callable.' % name)
 | 
						|
    return handler
 | 
						|
 | 
						|
 | 
						|
class Dispatcher(object):
 | 
						|
    """Dispatches WebSocket requests.
 | 
						|
 | 
						|
    This class maintains a map from resource name to handlers.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self, root_dir, scan_dir=None,
 | 
						|
        allow_handlers_outside_root_dir=True):
 | 
						|
        """Construct an instance.
 | 
						|
 | 
						|
        Args:
 | 
						|
            root_dir: The directory where handler definition files are
 | 
						|
                      placed.
 | 
						|
            scan_dir: The directory where handler definition files are
 | 
						|
                      searched. scan_dir must be a directory under root_dir,
 | 
						|
                      including root_dir itself.  If scan_dir is None,
 | 
						|
                      root_dir is used as scan_dir. scan_dir can be useful
 | 
						|
                      in saving scan time when root_dir contains many
 | 
						|
                      subdirectories.
 | 
						|
            allow_handlers_outside_root_dir: Scans handler files even if their
 | 
						|
                      canonical path is not under root_dir.
 | 
						|
        """
 | 
						|
 | 
						|
        self._logger = util.get_class_logger(self)
 | 
						|
 | 
						|
        self._handler_suite_map = {}
 | 
						|
        self._source_warnings = []
 | 
						|
        if scan_dir is None:
 | 
						|
            scan_dir = root_dir
 | 
						|
        if not os.path.realpath(scan_dir).startswith(
 | 
						|
                os.path.realpath(root_dir)):
 | 
						|
            raise DispatchException('scan_dir:%s must be a directory under '
 | 
						|
                                    'root_dir:%s.' % (scan_dir, root_dir))
 | 
						|
        self._source_handler_files_in_dir(
 | 
						|
            root_dir, scan_dir, allow_handlers_outside_root_dir)
 | 
						|
 | 
						|
    def add_resource_path_alias(self,
 | 
						|
                                alias_resource_path, existing_resource_path):
 | 
						|
        """Add resource path alias.
 | 
						|
 | 
						|
        Once added, request to alias_resource_path would be handled by
 | 
						|
        handler registered for existing_resource_path.
 | 
						|
 | 
						|
        Args:
 | 
						|
            alias_resource_path: alias resource path
 | 
						|
            existing_resource_path: existing resource path
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            handler_suite = self._handler_suite_map[existing_resource_path]
 | 
						|
            self._handler_suite_map[alias_resource_path] = handler_suite
 | 
						|
        except KeyError:
 | 
						|
            raise DispatchException('No handler for: %r' %
 | 
						|
                                    existing_resource_path)
 | 
						|
 | 
						|
    def source_warnings(self):
 | 
						|
        """Return warnings in sourcing handlers."""
 | 
						|
 | 
						|
        return self._source_warnings
 | 
						|
 | 
						|
    def do_extra_handshake(self, request):
 | 
						|
        """Do extra checking in WebSocket handshake.
 | 
						|
 | 
						|
        Select a handler based on request.uri and call its
 | 
						|
        web_socket_do_extra_handshake function.
 | 
						|
 | 
						|
        Args:
 | 
						|
            request: mod_python request.
 | 
						|
 | 
						|
        Raises:
 | 
						|
            DispatchException: when handler was not found
 | 
						|
            AbortedByUserException: when user handler abort connection
 | 
						|
            HandshakeException: when opening handshake failed
 | 
						|
        """
 | 
						|
 | 
						|
        handler_suite = self.get_handler_suite(request.ws_resource)
 | 
						|
        if handler_suite is None:
 | 
						|
            raise DispatchException('No handler for: %r' % request.ws_resource)
 | 
						|
        do_extra_handshake_ = handler_suite.do_extra_handshake
 | 
						|
        try:
 | 
						|
            do_extra_handshake_(request)
 | 
						|
        except handshake.AbortedByUserException, e:
 | 
						|
            # Re-raise to tell the caller of this function to finish this
 | 
						|
            # connection without sending any error.
 | 
						|
            self._logger.debug('%s', util.get_stack_trace())
 | 
						|
            raise
 | 
						|
        except Exception, e:
 | 
						|
            util.prepend_message_to_exception(
 | 
						|
                    '%s raised exception for %s: ' % (
 | 
						|
                            _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
 | 
						|
                            request.ws_resource),
 | 
						|
                    e)
 | 
						|
            raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
 | 
						|
 | 
						|
    def transfer_data(self, request):
 | 
						|
        """Let a handler transfer_data with a WebSocket client.
 | 
						|
 | 
						|
        Select a handler based on request.ws_resource and call its
 | 
						|
        web_socket_transfer_data function.
 | 
						|
 | 
						|
        Args:
 | 
						|
            request: mod_python request.
 | 
						|
 | 
						|
        Raises:
 | 
						|
            DispatchException: when handler was not found
 | 
						|
            AbortedByUserException: when user handler abort connection
 | 
						|
        """
 | 
						|
 | 
						|
        # TODO(tyoshino): Terminate underlying TCP connection if possible.
 | 
						|
        try:
 | 
						|
            if mux.use_mux(request):
 | 
						|
                mux.start(request, self)
 | 
						|
            else:
 | 
						|
                handler_suite = self.get_handler_suite(request.ws_resource)
 | 
						|
                if handler_suite is None:
 | 
						|
                    raise DispatchException('No handler for: %r' %
 | 
						|
                                            request.ws_resource)
 | 
						|
                transfer_data_ = handler_suite.transfer_data
 | 
						|
                transfer_data_(request)
 | 
						|
 | 
						|
            if not request.server_terminated:
 | 
						|
                request.ws_stream.close_connection()
 | 
						|
        # Catch non-critical exceptions the handler didn't handle.
 | 
						|
        except handshake.AbortedByUserException, e:
 | 
						|
            self._logger.debug('%s', util.get_stack_trace())
 | 
						|
            raise
 | 
						|
        except msgutil.BadOperationException, e:
 | 
						|
            self._logger.debug('%s', e)
 | 
						|
            request.ws_stream.close_connection(
 | 
						|
                common.STATUS_INTERNAL_ENDPOINT_ERROR)
 | 
						|
        except msgutil.InvalidFrameException, e:
 | 
						|
            # InvalidFrameException must be caught before
 | 
						|
            # ConnectionTerminatedException that catches InvalidFrameException.
 | 
						|
            self._logger.debug('%s', e)
 | 
						|
            request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR)
 | 
						|
        except msgutil.UnsupportedFrameException, e:
 | 
						|
            self._logger.debug('%s', e)
 | 
						|
            request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA)
 | 
						|
        except stream.InvalidUTF8Exception, e:
 | 
						|
            self._logger.debug('%s', e)
 | 
						|
            request.ws_stream.close_connection(
 | 
						|
                common.STATUS_INVALID_FRAME_PAYLOAD_DATA)
 | 
						|
        except msgutil.ConnectionTerminatedException, e:
 | 
						|
            self._logger.debug('%s', e)
 | 
						|
        except Exception, e:
 | 
						|
            # Any other exceptions are forwarded to the caller of this
 | 
						|
            # function.
 | 
						|
            util.prepend_message_to_exception(
 | 
						|
                '%s raised exception for %s: ' % (
 | 
						|
                    _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
 | 
						|
                e)
 | 
						|
            raise
 | 
						|
 | 
						|
    def passive_closing_handshake(self, request):
 | 
						|
        """Prepare code and reason for responding client initiated closing
 | 
						|
        handshake.
 | 
						|
        """
 | 
						|
 | 
						|
        handler_suite = self.get_handler_suite(request.ws_resource)
 | 
						|
        if handler_suite is None:
 | 
						|
            return _default_passive_closing_handshake_handler(request)
 | 
						|
        return handler_suite.passive_closing_handshake(request)
 | 
						|
 | 
						|
    def get_handler_suite(self, resource):
 | 
						|
        """Retrieves two handlers (one for extra handshake processing, and one
 | 
						|
        for data transfer) for the given request as a HandlerSuite object.
 | 
						|
        """
 | 
						|
 | 
						|
        fragment = None
 | 
						|
        if '#' in resource:
 | 
						|
            resource, fragment = resource.split('#', 1)
 | 
						|
        if '?' in resource:
 | 
						|
            resource = resource.split('?', 1)[0]
 | 
						|
        handler_suite = self._handler_suite_map.get(resource)
 | 
						|
        if handler_suite and fragment:
 | 
						|
            raise DispatchException('Fragment identifiers MUST NOT be used on '
 | 
						|
                                    'WebSocket URIs',
 | 
						|
                                    common.HTTP_STATUS_BAD_REQUEST)
 | 
						|
        return handler_suite
 | 
						|
 | 
						|
    def _source_handler_files_in_dir(
 | 
						|
        self, root_dir, scan_dir, allow_handlers_outside_root_dir):
 | 
						|
        """Source all the handler source files in the scan_dir directory.
 | 
						|
 | 
						|
        The resource path is determined relative to root_dir.
 | 
						|
        """
 | 
						|
 | 
						|
        # We build a map from resource to handler code assuming that there's
 | 
						|
        # only one path from root_dir to scan_dir and it can be obtained by
 | 
						|
        # comparing realpath of them.
 | 
						|
 | 
						|
        # Here we cannot use abspath. See
 | 
						|
        # https://bugs.webkit.org/show_bug.cgi?id=31603
 | 
						|
 | 
						|
        convert = _create_path_to_resource_converter(root_dir)
 | 
						|
        scan_realpath = os.path.realpath(scan_dir)
 | 
						|
        root_realpath = os.path.realpath(root_dir)
 | 
						|
        for path in _enumerate_handler_file_paths(scan_realpath):
 | 
						|
            if (not allow_handlers_outside_root_dir and
 | 
						|
                (not os.path.realpath(path).startswith(root_realpath))):
 | 
						|
                self._logger.debug(
 | 
						|
                    'Canonical path of %s is not under root directory' %
 | 
						|
                    path)
 | 
						|
                continue
 | 
						|
            try:
 | 
						|
                handler_suite = _source_handler_file(open(path).read())
 | 
						|
            except DispatchException, e:
 | 
						|
                self._source_warnings.append('%s: %s' % (path, e))
 | 
						|
                continue
 | 
						|
            resource = convert(path)
 | 
						|
            if resource is None:
 | 
						|
                self._logger.debug(
 | 
						|
                    'Path to resource conversion on %s failed' % path)
 | 
						|
            else:
 | 
						|
                self._handler_suite_map[convert(path)] = handler_suite
 | 
						|
 | 
						|
 | 
						|
# vi:sts=4 sw=4 et
 |