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