Home | History | Annotate | Download | only in mod_pywebsocket
      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 
     30 
     31 """Dispatch WebSocket request.
     32 """
     33 
     34 
     35 import logging
     36 import os
     37 import re
     38 
     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
     45 
     46 
     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')
     53 
     54 
     55 class DispatchException(Exception):
     56     """Exception in dispatching WebSocket request."""
     57 
     58     def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND):
     59         super(DispatchException, self).__init__(name)
     60         self.status = status
     61 
     62 
     63 def _default_passive_closing_handshake_handler(request):
     64     """Default web_socket_passive_closing_handshake handler."""
     65 
     66     return common.STATUS_NORMAL_CLOSURE, ''
     67 
     68 
     69 def _normalize_path(path):
     70     """Normalize path.
     71 
     72     Args:
     73         path: the path to normalize.
     74 
     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.
     78     """
     79 
     80     path = path.replace('\\', os.path.sep)
     81     path = os.path.realpath(path)
     82     path = path.replace('\\', '/')
     83     return path
     84 
     85 
     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 '/'.
     91 
     92     Args:
     93         base_dir: the path to the base directory.
     94     """
     95 
     96     base_dir = _normalize_path(base_dir)
     97 
     98     base_len = len(base_dir)
     99     suffix_len = len(_SOURCE_SUFFIX)
    100 
    101     def converter(path):
    102         if not path.endswith(_SOURCE_SUFFIX):
    103             return None
    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):
    108             return None
    109         return path[base_len:-suffix_len]
    110 
    111     return converter
    112 
    113 
    114 def _enumerate_handler_file_paths(directory):
    115     """Returns a generator that enumerates WebSocket Handler source file names
    116     in the given directory.
    117     """
    118 
    119     for root, unused_dirs, files in os.walk(directory):
    120         for base in files:
    121             path = os.path.join(root, base)
    122             if _SOURCE_PATH_PATTERN.search(path):
    123                 yield path
    124 
    125 
    126 class _HandlerSuite(object):
    127     """A handler suite holder class."""
    128 
    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
    134 
    135 
    136 def _source_handler_file(handler_definition):
    137     """Source a handler definition string.
    138 
    139     Args:
    140         handler_definition: a string containing Python statements that define
    141                             handler functions.
    142     """
    143 
    144     global_dic = {}
    145     try:
    146         exec handler_definition in global_dic
    147     except Exception:
    148         raise DispatchException('Error in sourcing handler:' +
    149                                 util.get_stack_trace())
    150     passive_closing_handshake_handler = None
    151     try:
    152         passive_closing_handshake_handler = _extract_handler(
    153             global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME)
    154     except Exception:
    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)
    161 
    162 
    163 def _extract_handler(dic, name):
    164     """Extracts a callable with the specified name from the given dictionary
    165     dic.
    166     """
    167 
    168     if name not in dic:
    169         raise DispatchException('%s is not defined.' % name)
    170     handler = dic[name]
    171     if not callable(handler):
    172         raise DispatchException('%s is not callable.' % name)
    173     return handler
    174 
    175 
    176 class Dispatcher(object):
    177     """Dispatches WebSocket requests.
    178 
    179     This class maintains a map from resource name to handlers.
    180     """
    181 
    182     def __init__(
    183         self, root_dir, scan_dir=None,
    184         allow_handlers_outside_root_dir=True):
    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(
    211             root_dir, scan_dir, allow_handlers_outside_root_dir)
    212 
    213     def add_resource_path_alias(self,
    214                                 alias_resource_path, existing_resource_path):
    215         """Add resource path alias.
    216 
    217         Once added, request to alias_resource_path would be handled by
    218         handler registered for existing_resource_path.
    219 
    220         Args:
    221             alias_resource_path: alias resource path
    222             existing_resource_path: existing resource path
    223         """
    224         try:
    225             handler_suite = self._handler_suite_map[existing_resource_path]
    226             self._handler_suite_map[alias_resource_path] = handler_suite
    227         except KeyError:
    228             raise DispatchException('No handler for: %r' %
    229                                     existing_resource_path)
    230 
    231     def source_warnings(self):
    232         """Return warnings in sourcing handlers."""
    233 
    234         return self._source_warnings
    235 
    236     def do_extra_handshake(self, request):
    237         """Do extra checking in WebSocket handshake.
    238 
    239         Select a handler based on request.uri and call its
    240         web_socket_do_extra_handshake function.
    241 
    242         Args:
    243             request: mod_python request.
    244 
    245         Raises:
    246             DispatchException: when handler was not found
    247             AbortedByUserException: when user handler abort connection
    248             HandshakeException: when opening handshake failed
    249         """
    250 
    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
    255         try:
    256             do_extra_handshake_(request)
    257         except handshake.AbortedByUserException, e:
    258             raise
    259         except Exception, e:
    260             util.prepend_message_to_exception(
    261                     '%s raised exception for %s: ' % (
    262                             _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
    263                             request.ws_resource),
    264                     e)
    265             raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN)
    266 
    267     def transfer_data(self, request):
    268         """Let a handler transfer_data with a WebSocket client.
    269 
    270         Select a handler based on request.ws_resource and call its
    271         web_socket_transfer_data function.
    272 
    273         Args:
    274             request: mod_python request.
    275 
    276         Raises:
    277             DispatchException: when handler was not found
    278             AbortedByUserException: when user handler abort connection
    279         """
    280 
    281         # TODO(tyoshino): Terminate underlying TCP connection if possible.
    282         try:
    283             if mux.use_mux(request):
    284                 mux.start(request, self)
    285             else:
    286                 handler_suite = self.get_handler_suite(request.ws_resource)
    287                 if handler_suite is None:
    288                     raise DispatchException('No handler for: %r' %
    289                                             request.ws_resource)
    290                 transfer_data_ = handler_suite.transfer_data
    291                 transfer_data_(request)
    292 
    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)
    298             raise
    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)
    316         except Exception, e:
    317             util.prepend_message_to_exception(
    318                 '%s raised exception for %s: ' % (
    319                     _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
    320                 e)
    321             raise
    322 
    323     def passive_closing_handshake(self, request):
    324         """Prepare code and reason for responding client initiated closing
    325         handshake.
    326         """
    327 
    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)
    332 
    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.
    336         """
    337 
    338         fragment = None
    339         if '#' in resource:
    340             resource, fragment = resource.split('#', 1)
    341         if '?' in resource:
    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 '
    346                                     'WebSocket URIs',
    347                                     common.HTTP_STATUS_BAD_REQUEST)
    348         return handler_suite
    349 
    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.
    353 
    354         The resource path is determined relative to root_dir.
    355         """
    356 
    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.
    360 
    361         # Here we cannot use abspath. See
    362         # https://bugs.webkit.org/show_bug.cgi?id=31603
    363 
    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))):
    370                 self._logger.debug(
    371                     'Canonical path of %s is not under root directory' %
    372                     path)
    373                 continue
    374             try:
    375                 handler_suite = _source_handler_file(open(path).read())
    376             except DispatchException, e:
    377                 self._source_warnings.append('%s: %s' % (path, e))
    378                 continue
    379             resource = convert(path)
    380             if resource is None:
    381                 self._logger.debug(
    382                     'Path to resource conversion on %s failed' % path)
    383             else:
    384                 self._handler_suite_map[convert(path)] = handler_suite
    385 
    386 
    387 # vi:sts=4 sw=4 et
    388