1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,381 @@ 1.4 +# Copyright 2012, Google Inc. 1.5 +# All rights reserved. 1.6 +# 1.7 +# Redistribution and use in source and binary forms, with or without 1.8 +# modification, are permitted provided that the following conditions are 1.9 +# met: 1.10 +# 1.11 +# * Redistributions of source code must retain the above copyright 1.12 +# notice, this list of conditions and the following disclaimer. 1.13 +# * Redistributions in binary form must reproduce the above 1.14 +# copyright notice, this list of conditions and the following disclaimer 1.15 +# in the documentation and/or other materials provided with the 1.16 +# distribution. 1.17 +# * Neither the name of Google Inc. nor the names of its 1.18 +# contributors may be used to endorse or promote products derived from 1.19 +# this software without specific prior written permission. 1.20 +# 1.21 +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 1.22 +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 1.23 +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 1.24 +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 1.25 +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 1.26 +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 1.27 +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 1.28 +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 1.29 +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 1.30 +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 1.31 +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1.32 + 1.33 + 1.34 +"""Dispatch WebSocket request. 1.35 +""" 1.36 + 1.37 + 1.38 +import logging 1.39 +import os 1.40 +import re 1.41 + 1.42 +from mod_pywebsocket import common 1.43 +from mod_pywebsocket import handshake 1.44 +from mod_pywebsocket import msgutil 1.45 +from mod_pywebsocket import stream 1.46 +from mod_pywebsocket import util 1.47 + 1.48 + 1.49 +_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') 1.50 +_SOURCE_SUFFIX = '_wsh.py' 1.51 +_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake' 1.52 +_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' 1.53 +_PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( 1.54 + 'web_socket_passive_closing_handshake') 1.55 + 1.56 + 1.57 +class DispatchException(Exception): 1.58 + """Exception in dispatching WebSocket request.""" 1.59 + 1.60 + def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): 1.61 + super(DispatchException, self).__init__(name) 1.62 + self.status = status 1.63 + 1.64 + 1.65 +def _default_passive_closing_handshake_handler(request): 1.66 + """Default web_socket_passive_closing_handshake handler.""" 1.67 + 1.68 + return common.STATUS_NORMAL_CLOSURE, '' 1.69 + 1.70 + 1.71 +def _normalize_path(path): 1.72 + """Normalize path. 1.73 + 1.74 + Args: 1.75 + path: the path to normalize. 1.76 + 1.77 + Path is converted to the absolute path. 1.78 + The input path can use either '\\' or '/' as the separator. 1.79 + The normalized path always uses '/' regardless of the platform. 1.80 + """ 1.81 + 1.82 + path = path.replace('\\', os.path.sep) 1.83 + path = os.path.realpath(path) 1.84 + path = path.replace('\\', '/') 1.85 + return path 1.86 + 1.87 + 1.88 +def _create_path_to_resource_converter(base_dir): 1.89 + """Returns a function that converts the path of a WebSocket handler source 1.90 + file to a resource string by removing the path to the base directory from 1.91 + its head, removing _SOURCE_SUFFIX from its tail, and replacing path 1.92 + separators in it with '/'. 1.93 + 1.94 + Args: 1.95 + base_dir: the path to the base directory. 1.96 + """ 1.97 + 1.98 + base_dir = _normalize_path(base_dir) 1.99 + 1.100 + base_len = len(base_dir) 1.101 + suffix_len = len(_SOURCE_SUFFIX) 1.102 + 1.103 + def converter(path): 1.104 + if not path.endswith(_SOURCE_SUFFIX): 1.105 + return None 1.106 + # _normalize_path must not be used because resolving symlink breaks 1.107 + # following path check. 1.108 + path = path.replace('\\', '/') 1.109 + if not path.startswith(base_dir): 1.110 + return None 1.111 + return path[base_len:-suffix_len] 1.112 + 1.113 + return converter 1.114 + 1.115 + 1.116 +def _enumerate_handler_file_paths(directory): 1.117 + """Returns a generator that enumerates WebSocket Handler source file names 1.118 + in the given directory. 1.119 + """ 1.120 + 1.121 + for root, unused_dirs, files in os.walk(directory): 1.122 + for base in files: 1.123 + path = os.path.join(root, base) 1.124 + if _SOURCE_PATH_PATTERN.search(path): 1.125 + yield path 1.126 + 1.127 + 1.128 +class _HandlerSuite(object): 1.129 + """A handler suite holder class.""" 1.130 + 1.131 + def __init__(self, do_extra_handshake, transfer_data, 1.132 + passive_closing_handshake): 1.133 + self.do_extra_handshake = do_extra_handshake 1.134 + self.transfer_data = transfer_data 1.135 + self.passive_closing_handshake = passive_closing_handshake 1.136 + 1.137 + 1.138 +def _source_handler_file(handler_definition): 1.139 + """Source a handler definition string. 1.140 + 1.141 + Args: 1.142 + handler_definition: a string containing Python statements that define 1.143 + handler functions. 1.144 + """ 1.145 + 1.146 + global_dic = {} 1.147 + try: 1.148 + exec handler_definition in global_dic 1.149 + except Exception: 1.150 + raise DispatchException('Error in sourcing handler:' + 1.151 + util.get_stack_trace()) 1.152 + passive_closing_handshake_handler = None 1.153 + try: 1.154 + passive_closing_handshake_handler = _extract_handler( 1.155 + global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME) 1.156 + except Exception: 1.157 + passive_closing_handshake_handler = ( 1.158 + _default_passive_closing_handshake_handler) 1.159 + return _HandlerSuite( 1.160 + _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), 1.161 + _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME), 1.162 + passive_closing_handshake_handler) 1.163 + 1.164 + 1.165 +def _extract_handler(dic, name): 1.166 + """Extracts a callable with the specified name from the given dictionary 1.167 + dic. 1.168 + """ 1.169 + 1.170 + if name not in dic: 1.171 + raise DispatchException('%s is not defined.' % name) 1.172 + handler = dic[name] 1.173 + if not callable(handler): 1.174 + raise DispatchException('%s is not callable.' % name) 1.175 + return handler 1.176 + 1.177 + 1.178 +class Dispatcher(object): 1.179 + """Dispatches WebSocket requests. 1.180 + 1.181 + This class maintains a map from resource name to handlers. 1.182 + """ 1.183 + 1.184 + def __init__( 1.185 + self, root_dir, scan_dir=None, 1.186 + allow_handlers_outside_root_dir=True): 1.187 + """Construct an instance. 1.188 + 1.189 + Args: 1.190 + root_dir: The directory where handler definition files are 1.191 + placed. 1.192 + scan_dir: The directory where handler definition files are 1.193 + searched. scan_dir must be a directory under root_dir, 1.194 + including root_dir itself. If scan_dir is None, 1.195 + root_dir is used as scan_dir. scan_dir can be useful 1.196 + in saving scan time when root_dir contains many 1.197 + subdirectories. 1.198 + allow_handlers_outside_root_dir: Scans handler files even if their 1.199 + canonical path is not under root_dir. 1.200 + """ 1.201 + 1.202 + self._logger = util.get_class_logger(self) 1.203 + 1.204 + self._handler_suite_map = {} 1.205 + self._source_warnings = [] 1.206 + if scan_dir is None: 1.207 + scan_dir = root_dir 1.208 + if not os.path.realpath(scan_dir).startswith( 1.209 + os.path.realpath(root_dir)): 1.210 + raise DispatchException('scan_dir:%s must be a directory under ' 1.211 + 'root_dir:%s.' % (scan_dir, root_dir)) 1.212 + self._source_handler_files_in_dir( 1.213 + root_dir, scan_dir, allow_handlers_outside_root_dir) 1.214 + 1.215 + def add_resource_path_alias(self, 1.216 + alias_resource_path, existing_resource_path): 1.217 + """Add resource path alias. 1.218 + 1.219 + Once added, request to alias_resource_path would be handled by 1.220 + handler registered for existing_resource_path. 1.221 + 1.222 + Args: 1.223 + alias_resource_path: alias resource path 1.224 + existing_resource_path: existing resource path 1.225 + """ 1.226 + try: 1.227 + handler_suite = self._handler_suite_map[existing_resource_path] 1.228 + self._handler_suite_map[alias_resource_path] = handler_suite 1.229 + except KeyError: 1.230 + raise DispatchException('No handler for: %r' % 1.231 + existing_resource_path) 1.232 + 1.233 + def source_warnings(self): 1.234 + """Return warnings in sourcing handlers.""" 1.235 + 1.236 + return self._source_warnings 1.237 + 1.238 + def do_extra_handshake(self, request): 1.239 + """Do extra checking in WebSocket handshake. 1.240 + 1.241 + Select a handler based on request.uri and call its 1.242 + web_socket_do_extra_handshake function. 1.243 + 1.244 + Args: 1.245 + request: mod_python request. 1.246 + 1.247 + Raises: 1.248 + DispatchException: when handler was not found 1.249 + AbortedByUserException: when user handler abort connection 1.250 + HandshakeException: when opening handshake failed 1.251 + """ 1.252 + 1.253 + handler_suite = self.get_handler_suite(request.ws_resource) 1.254 + if handler_suite is None: 1.255 + raise DispatchException('No handler for: %r' % request.ws_resource) 1.256 + do_extra_handshake_ = handler_suite.do_extra_handshake 1.257 + try: 1.258 + do_extra_handshake_(request) 1.259 + except handshake.AbortedByUserException, e: 1.260 + raise 1.261 + except Exception, e: 1.262 + util.prepend_message_to_exception( 1.263 + '%s raised exception for %s: ' % ( 1.264 + _DO_EXTRA_HANDSHAKE_HANDLER_NAME, 1.265 + request.ws_resource), 1.266 + e) 1.267 + raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) 1.268 + 1.269 + def transfer_data(self, request): 1.270 + """Let a handler transfer_data with a WebSocket client. 1.271 + 1.272 + Select a handler based on request.ws_resource and call its 1.273 + web_socket_transfer_data function. 1.274 + 1.275 + Args: 1.276 + request: mod_python request. 1.277 + 1.278 + Raises: 1.279 + DispatchException: when handler was not found 1.280 + AbortedByUserException: when user handler abort connection 1.281 + """ 1.282 + 1.283 + handler_suite = self.get_handler_suite(request.ws_resource) 1.284 + if handler_suite is None: 1.285 + raise DispatchException('No handler for: %r' % request.ws_resource) 1.286 + transfer_data_ = handler_suite.transfer_data 1.287 + # TODO(tyoshino): Terminate underlying TCP connection if possible. 1.288 + try: 1.289 + transfer_data_(request) 1.290 + if not request.server_terminated: 1.291 + request.ws_stream.close_connection() 1.292 + # Catch non-critical exceptions the handler didn't handle. 1.293 + except handshake.AbortedByUserException, e: 1.294 + self._logger.debug('%s', e) 1.295 + raise 1.296 + except msgutil.BadOperationException, e: 1.297 + self._logger.debug('%s', e) 1.298 + request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE) 1.299 + except msgutil.InvalidFrameException, e: 1.300 + # InvalidFrameException must be caught before 1.301 + # ConnectionTerminatedException that catches InvalidFrameException. 1.302 + self._logger.debug('%s', e) 1.303 + request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) 1.304 + except msgutil.UnsupportedFrameException, e: 1.305 + self._logger.debug('%s', e) 1.306 + request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) 1.307 + except stream.InvalidUTF8Exception, e: 1.308 + self._logger.debug('%s', e) 1.309 + request.ws_stream.close_connection( 1.310 + common.STATUS_INVALID_FRAME_PAYLOAD_DATA) 1.311 + except msgutil.ConnectionTerminatedException, e: 1.312 + self._logger.debug('%s', e) 1.313 + except Exception, e: 1.314 + util.prepend_message_to_exception( 1.315 + '%s raised exception for %s: ' % ( 1.316 + _TRANSFER_DATA_HANDLER_NAME, request.ws_resource), 1.317 + e) 1.318 + raise 1.319 + 1.320 + def passive_closing_handshake(self, request): 1.321 + """Prepare code and reason for responding client initiated closing 1.322 + handshake. 1.323 + """ 1.324 + 1.325 + handler_suite = self.get_handler_suite(request.ws_resource) 1.326 + if handler_suite is None: 1.327 + return _default_passive_closing_handshake_handler(request) 1.328 + return handler_suite.passive_closing_handshake(request) 1.329 + 1.330 + def get_handler_suite(self, resource): 1.331 + """Retrieves two handlers (one for extra handshake processing, and one 1.332 + for data transfer) for the given request as a HandlerSuite object. 1.333 + """ 1.334 + 1.335 + fragment = None 1.336 + if '#' in resource: 1.337 + resource, fragment = resource.split('#', 1) 1.338 + if '?' in resource: 1.339 + resource = resource.split('?', 1)[0] 1.340 + handler_suite = self._handler_suite_map.get(resource) 1.341 + if handler_suite and fragment: 1.342 + raise DispatchException('Fragment identifiers MUST NOT be used on ' 1.343 + 'WebSocket URIs', 1.344 + common.HTTP_STATUS_BAD_REQUEST) 1.345 + return handler_suite 1.346 + 1.347 + def _source_handler_files_in_dir( 1.348 + self, root_dir, scan_dir, allow_handlers_outside_root_dir): 1.349 + """Source all the handler source files in the scan_dir directory. 1.350 + 1.351 + The resource path is determined relative to root_dir. 1.352 + """ 1.353 + 1.354 + # We build a map from resource to handler code assuming that there's 1.355 + # only one path from root_dir to scan_dir and it can be obtained by 1.356 + # comparing realpath of them. 1.357 + 1.358 + # Here we cannot use abspath. See 1.359 + # https://bugs.webkit.org/show_bug.cgi?id=31603 1.360 + 1.361 + convert = _create_path_to_resource_converter(root_dir) 1.362 + scan_realpath = os.path.realpath(scan_dir) 1.363 + root_realpath = os.path.realpath(root_dir) 1.364 + for path in _enumerate_handler_file_paths(scan_realpath): 1.365 + if (not allow_handlers_outside_root_dir and 1.366 + (not os.path.realpath(path).startswith(root_realpath))): 1.367 + self._logger.debug( 1.368 + 'Canonical path of %s is not under root directory' % 1.369 + path) 1.370 + continue 1.371 + try: 1.372 + handler_suite = _source_handler_file(open(path).read()) 1.373 + except DispatchException, e: 1.374 + self._source_warnings.append('%s: %s' % (path, e)) 1.375 + continue 1.376 + resource = convert(path) 1.377 + if resource is None: 1.378 + self._logger.debug( 1.379 + 'Path to resource conversion on %s failed' % path) 1.380 + else: 1.381 + self._handler_suite_map[convert(path)] = handler_suite 1.382 + 1.383 + 1.384 +# vi:sts=4 sw=4 et