Home | History | Annotate | Download | only in mod_pywebsocket
      1 # Copyright 2009, 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 Web Socket request.
     32 """
     33 
     34 
     35 import os
     36 import re
     37 
     38 import util
     39 
     40 
     41 _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
     42 _SOURCE_SUFFIX = '_wsh.py'
     43 _DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
     44 _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
     45 
     46 
     47 class DispatchError(Exception):
     48     """Exception in dispatching Web Socket request."""
     49 
     50     pass
     51 
     52 
     53 def _normalize_path(path):
     54     """Normalize path.
     55 
     56     Args:
     57         path: the path to normalize.
     58 
     59     Path is converted to the absolute path.
     60     The input path can use either '\\' or '/' as the separator.
     61     The normalized path always uses '/' regardless of the platform.
     62     """
     63 
     64     path = path.replace('\\', os.path.sep)
     65     path = os.path.realpath(path)
     66     path = path.replace('\\', '/')
     67     return path
     68 
     69 
     70 def _path_to_resource_converter(base_dir):
     71     base_dir = _normalize_path(base_dir)
     72     base_len = len(base_dir)
     73     suffix_len = len(_SOURCE_SUFFIX)
     74     def converter(path):
     75         if not path.endswith(_SOURCE_SUFFIX):
     76             return None
     77         path = _normalize_path(path)
     78         if not path.startswith(base_dir):
     79             return None
     80         return path[base_len:-suffix_len]
     81     return converter
     82 
     83 
     84 def _source_file_paths(directory):
     85     """Yield Web Socket Handler source file names in the given directory."""
     86 
     87     for root, unused_dirs, files in os.walk(directory):
     88         for base in files:
     89             path = os.path.join(root, base)
     90             if _SOURCE_PATH_PATTERN.search(path):
     91                 yield path
     92 
     93 
     94 def _source(source_str):
     95     """Source a handler definition string."""
     96 
     97     global_dic = {}
     98     try:
     99         exec source_str in global_dic
    100     except Exception:
    101         raise DispatchError('Error in sourcing handler:' +
    102                             util.get_stack_trace())
    103     return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
    104             _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
    105 
    106 
    107 def _extract_handler(dic, name):
    108     if name not in dic:
    109         raise DispatchError('%s is not defined.' % name)
    110     handler = dic[name]
    111     if not callable(handler):
    112         raise DispatchError('%s is not callable.' % name)
    113     return handler
    114 
    115 
    116 class Dispatcher(object):
    117     """Dispatches Web Socket requests.
    118 
    119     This class maintains a map from resource name to handlers.
    120     """
    121 
    122     def __init__(self, root_dir, scan_dir=None):
    123         """Construct an instance.
    124 
    125         Args:
    126             root_dir: The directory where handler definition files are
    127                       placed.
    128             scan_dir: The directory where handler definition files are
    129                       searched. scan_dir must be a directory under root_dir,
    130                       including root_dir itself.  If scan_dir is None, root_dir
    131                       is used as scan_dir. scan_dir can be useful in saving
    132                       scan time when root_dir contains many subdirectories.
    133         """
    134 
    135         self._handlers = {}
    136         self._source_warnings = []
    137         if scan_dir is None:
    138             scan_dir = root_dir
    139         if not os.path.realpath(scan_dir).startswith(
    140                 os.path.realpath(root_dir)):
    141             raise DispatchError('scan_dir:%s must be a directory under '
    142                                 'root_dir:%s.' % (scan_dir, root_dir))
    143         self._source_files_in_dir(root_dir, scan_dir)
    144 
    145     def add_resource_path_alias(self,
    146                                 alias_resource_path, existing_resource_path):
    147         """Add resource path alias.
    148 
    149         Once added, request to alias_resource_path would be handled by
    150         handler registered for existing_resource_path.
    151 
    152         Args:
    153             alias_resource_path: alias resource path
    154             existing_resource_path: existing resource path
    155         """
    156         try:
    157             handler = self._handlers[existing_resource_path]
    158             self._handlers[alias_resource_path] = handler
    159         except KeyError:
    160             raise DispatchError('No handler for: %r' % existing_resource_path)
    161 
    162     def source_warnings(self):
    163         """Return warnings in sourcing handlers."""
    164 
    165         return self._source_warnings
    166 
    167     def do_extra_handshake(self, request):
    168         """Do extra checking in Web Socket handshake.
    169 
    170         Select a handler based on request.uri and call its
    171         web_socket_do_extra_handshake function.
    172 
    173         Args:
    174             request: mod_python request.
    175         """
    176 
    177         do_extra_handshake_, unused_transfer_data = self._handler(request)
    178         try:
    179             do_extra_handshake_(request)
    180         except Exception, e:
    181             util.prepend_message_to_exception(
    182                     '%s raised exception for %s: ' % (
    183                             _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
    184                             request.ws_resource),
    185                     e)
    186             raise
    187 
    188     def transfer_data(self, request):
    189         """Let a handler transfer_data with a Web Socket client.
    190 
    191         Select a handler based on request.ws_resource and call its
    192         web_socket_transfer_data function.
    193 
    194         Args:
    195             request: mod_python request.
    196         """
    197 
    198         unused_do_extra_handshake, transfer_data_ = self._handler(request)
    199         try:
    200             transfer_data_(request)
    201         except Exception, e:
    202             util.prepend_message_to_exception(
    203                     '%s raised exception for %s: ' % (
    204                             _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
    205                     e)
    206             raise
    207 
    208     def _handler(self, request):
    209         try:
    210             ws_resource_path = request.ws_resource.split('?', 1)[0]
    211             return self._handlers[ws_resource_path]
    212         except KeyError:
    213             raise DispatchError('No handler for: %r' % request.ws_resource)
    214 
    215     def _source_files_in_dir(self, root_dir, scan_dir):
    216         """Source all the handler source files in the scan_dir directory.
    217 
    218         The resource path is determined relative to root_dir.
    219         """
    220 
    221         to_resource = _path_to_resource_converter(root_dir)
    222         for path in _source_file_paths(scan_dir):
    223             try:
    224                 handlers = _source(open(path).read())
    225             except DispatchError, e:
    226                 self._source_warnings.append('%s: %s' % (path, e))
    227                 continue
    228             self._handlers[to_resource(path)] = handlers
    229 
    230 
    231 # vi:sts=4 sw=4 et
    232