tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

dispatch.py (15102B)


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