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 35import os 36import re 37 38import 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 47class DispatchError(Exception): 48 """Exception in dispatching Web Socket request.""" 49 50 pass 51 52 53def _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 70def _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 84def _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 94def _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 107def _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 116class 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