Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | # Copyright 2012, Google Inc. |
michael@0 | 2 | # All rights reserved. |
michael@0 | 3 | # |
michael@0 | 4 | # Redistribution and use in source and binary forms, with or without |
michael@0 | 5 | # modification, are permitted provided that the following conditions are |
michael@0 | 6 | # met: |
michael@0 | 7 | # |
michael@0 | 8 | # * Redistributions of source code must retain the above copyright |
michael@0 | 9 | # notice, this list of conditions and the following disclaimer. |
michael@0 | 10 | # * Redistributions in binary form must reproduce the above |
michael@0 | 11 | # copyright notice, this list of conditions and the following disclaimer |
michael@0 | 12 | # in the documentation and/or other materials provided with the |
michael@0 | 13 | # distribution. |
michael@0 | 14 | # * Neither the name of Google Inc. nor the names of its |
michael@0 | 15 | # contributors may be used to endorse or promote products derived from |
michael@0 | 16 | # this software without specific prior written permission. |
michael@0 | 17 | # |
michael@0 | 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
michael@0 | 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
michael@0 | 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
michael@0 | 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
michael@0 | 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
michael@0 | 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
michael@0 | 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
michael@0 | 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
michael@0 | 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
michael@0 | 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
michael@0 | 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
michael@0 | 29 | |
michael@0 | 30 | |
michael@0 | 31 | """Dispatch WebSocket request. |
michael@0 | 32 | """ |
michael@0 | 33 | |
michael@0 | 34 | |
michael@0 | 35 | import logging |
michael@0 | 36 | import os |
michael@0 | 37 | import re |
michael@0 | 38 | |
michael@0 | 39 | from mod_pywebsocket import common |
michael@0 | 40 | from mod_pywebsocket import handshake |
michael@0 | 41 | from mod_pywebsocket import msgutil |
michael@0 | 42 | from mod_pywebsocket import stream |
michael@0 | 43 | from mod_pywebsocket import util |
michael@0 | 44 | |
michael@0 | 45 | |
michael@0 | 46 | _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$') |
michael@0 | 47 | _SOURCE_SUFFIX = '_wsh.py' |
michael@0 | 48 | _DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake' |
michael@0 | 49 | _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data' |
michael@0 | 50 | _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME = ( |
michael@0 | 51 | 'web_socket_passive_closing_handshake') |
michael@0 | 52 | |
michael@0 | 53 | |
michael@0 | 54 | class DispatchException(Exception): |
michael@0 | 55 | """Exception in dispatching WebSocket request.""" |
michael@0 | 56 | |
michael@0 | 57 | def __init__(self, name, status=common.HTTP_STATUS_NOT_FOUND): |
michael@0 | 58 | super(DispatchException, self).__init__(name) |
michael@0 | 59 | self.status = status |
michael@0 | 60 | |
michael@0 | 61 | |
michael@0 | 62 | def _default_passive_closing_handshake_handler(request): |
michael@0 | 63 | """Default web_socket_passive_closing_handshake handler.""" |
michael@0 | 64 | |
michael@0 | 65 | return common.STATUS_NORMAL_CLOSURE, '' |
michael@0 | 66 | |
michael@0 | 67 | |
michael@0 | 68 | def _normalize_path(path): |
michael@0 | 69 | """Normalize path. |
michael@0 | 70 | |
michael@0 | 71 | Args: |
michael@0 | 72 | path: the path to normalize. |
michael@0 | 73 | |
michael@0 | 74 | Path is converted to the absolute path. |
michael@0 | 75 | The input path can use either '\\' or '/' as the separator. |
michael@0 | 76 | The normalized path always uses '/' regardless of the platform. |
michael@0 | 77 | """ |
michael@0 | 78 | |
michael@0 | 79 | path = path.replace('\\', os.path.sep) |
michael@0 | 80 | path = os.path.realpath(path) |
michael@0 | 81 | path = path.replace('\\', '/') |
michael@0 | 82 | return path |
michael@0 | 83 | |
michael@0 | 84 | |
michael@0 | 85 | def _create_path_to_resource_converter(base_dir): |
michael@0 | 86 | """Returns a function that converts the path of a WebSocket handler source |
michael@0 | 87 | file to a resource string by removing the path to the base directory from |
michael@0 | 88 | its head, removing _SOURCE_SUFFIX from its tail, and replacing path |
michael@0 | 89 | separators in it with '/'. |
michael@0 | 90 | |
michael@0 | 91 | Args: |
michael@0 | 92 | base_dir: the path to the base directory. |
michael@0 | 93 | """ |
michael@0 | 94 | |
michael@0 | 95 | base_dir = _normalize_path(base_dir) |
michael@0 | 96 | |
michael@0 | 97 | base_len = len(base_dir) |
michael@0 | 98 | suffix_len = len(_SOURCE_SUFFIX) |
michael@0 | 99 | |
michael@0 | 100 | def converter(path): |
michael@0 | 101 | if not path.endswith(_SOURCE_SUFFIX): |
michael@0 | 102 | return None |
michael@0 | 103 | # _normalize_path must not be used because resolving symlink breaks |
michael@0 | 104 | # following path check. |
michael@0 | 105 | path = path.replace('\\', '/') |
michael@0 | 106 | if not path.startswith(base_dir): |
michael@0 | 107 | return None |
michael@0 | 108 | return path[base_len:-suffix_len] |
michael@0 | 109 | |
michael@0 | 110 | return converter |
michael@0 | 111 | |
michael@0 | 112 | |
michael@0 | 113 | def _enumerate_handler_file_paths(directory): |
michael@0 | 114 | """Returns a generator that enumerates WebSocket Handler source file names |
michael@0 | 115 | in the given directory. |
michael@0 | 116 | """ |
michael@0 | 117 | |
michael@0 | 118 | for root, unused_dirs, files in os.walk(directory): |
michael@0 | 119 | for base in files: |
michael@0 | 120 | path = os.path.join(root, base) |
michael@0 | 121 | if _SOURCE_PATH_PATTERN.search(path): |
michael@0 | 122 | yield path |
michael@0 | 123 | |
michael@0 | 124 | |
michael@0 | 125 | class _HandlerSuite(object): |
michael@0 | 126 | """A handler suite holder class.""" |
michael@0 | 127 | |
michael@0 | 128 | def __init__(self, do_extra_handshake, transfer_data, |
michael@0 | 129 | passive_closing_handshake): |
michael@0 | 130 | self.do_extra_handshake = do_extra_handshake |
michael@0 | 131 | self.transfer_data = transfer_data |
michael@0 | 132 | self.passive_closing_handshake = passive_closing_handshake |
michael@0 | 133 | |
michael@0 | 134 | |
michael@0 | 135 | def _source_handler_file(handler_definition): |
michael@0 | 136 | """Source a handler definition string. |
michael@0 | 137 | |
michael@0 | 138 | Args: |
michael@0 | 139 | handler_definition: a string containing Python statements that define |
michael@0 | 140 | handler functions. |
michael@0 | 141 | """ |
michael@0 | 142 | |
michael@0 | 143 | global_dic = {} |
michael@0 | 144 | try: |
michael@0 | 145 | exec handler_definition in global_dic |
michael@0 | 146 | except Exception: |
michael@0 | 147 | raise DispatchException('Error in sourcing handler:' + |
michael@0 | 148 | util.get_stack_trace()) |
michael@0 | 149 | passive_closing_handshake_handler = None |
michael@0 | 150 | try: |
michael@0 | 151 | passive_closing_handshake_handler = _extract_handler( |
michael@0 | 152 | global_dic, _PASSIVE_CLOSING_HANDSHAKE_HANDLER_NAME) |
michael@0 | 153 | except Exception: |
michael@0 | 154 | passive_closing_handshake_handler = ( |
michael@0 | 155 | _default_passive_closing_handshake_handler) |
michael@0 | 156 | return _HandlerSuite( |
michael@0 | 157 | _extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME), |
michael@0 | 158 | _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME), |
michael@0 | 159 | passive_closing_handshake_handler) |
michael@0 | 160 | |
michael@0 | 161 | |
michael@0 | 162 | def _extract_handler(dic, name): |
michael@0 | 163 | """Extracts a callable with the specified name from the given dictionary |
michael@0 | 164 | dic. |
michael@0 | 165 | """ |
michael@0 | 166 | |
michael@0 | 167 | if name not in dic: |
michael@0 | 168 | raise DispatchException('%s is not defined.' % name) |
michael@0 | 169 | handler = dic[name] |
michael@0 | 170 | if not callable(handler): |
michael@0 | 171 | raise DispatchException('%s is not callable.' % name) |
michael@0 | 172 | return handler |
michael@0 | 173 | |
michael@0 | 174 | |
michael@0 | 175 | class Dispatcher(object): |
michael@0 | 176 | """Dispatches WebSocket requests. |
michael@0 | 177 | |
michael@0 | 178 | This class maintains a map from resource name to handlers. |
michael@0 | 179 | """ |
michael@0 | 180 | |
michael@0 | 181 | def __init__( |
michael@0 | 182 | self, root_dir, scan_dir=None, |
michael@0 | 183 | allow_handlers_outside_root_dir=True): |
michael@0 | 184 | """Construct an instance. |
michael@0 | 185 | |
michael@0 | 186 | Args: |
michael@0 | 187 | root_dir: The directory where handler definition files are |
michael@0 | 188 | placed. |
michael@0 | 189 | scan_dir: The directory where handler definition files are |
michael@0 | 190 | searched. scan_dir must be a directory under root_dir, |
michael@0 | 191 | including root_dir itself. If scan_dir is None, |
michael@0 | 192 | root_dir is used as scan_dir. scan_dir can be useful |
michael@0 | 193 | in saving scan time when root_dir contains many |
michael@0 | 194 | subdirectories. |
michael@0 | 195 | allow_handlers_outside_root_dir: Scans handler files even if their |
michael@0 | 196 | canonical path is not under root_dir. |
michael@0 | 197 | """ |
michael@0 | 198 | |
michael@0 | 199 | self._logger = util.get_class_logger(self) |
michael@0 | 200 | |
michael@0 | 201 | self._handler_suite_map = {} |
michael@0 | 202 | self._source_warnings = [] |
michael@0 | 203 | if scan_dir is None: |
michael@0 | 204 | scan_dir = root_dir |
michael@0 | 205 | if not os.path.realpath(scan_dir).startswith( |
michael@0 | 206 | os.path.realpath(root_dir)): |
michael@0 | 207 | raise DispatchException('scan_dir:%s must be a directory under ' |
michael@0 | 208 | 'root_dir:%s.' % (scan_dir, root_dir)) |
michael@0 | 209 | self._source_handler_files_in_dir( |
michael@0 | 210 | root_dir, scan_dir, allow_handlers_outside_root_dir) |
michael@0 | 211 | |
michael@0 | 212 | def add_resource_path_alias(self, |
michael@0 | 213 | alias_resource_path, existing_resource_path): |
michael@0 | 214 | """Add resource path alias. |
michael@0 | 215 | |
michael@0 | 216 | Once added, request to alias_resource_path would be handled by |
michael@0 | 217 | handler registered for existing_resource_path. |
michael@0 | 218 | |
michael@0 | 219 | Args: |
michael@0 | 220 | alias_resource_path: alias resource path |
michael@0 | 221 | existing_resource_path: existing resource path |
michael@0 | 222 | """ |
michael@0 | 223 | try: |
michael@0 | 224 | handler_suite = self._handler_suite_map[existing_resource_path] |
michael@0 | 225 | self._handler_suite_map[alias_resource_path] = handler_suite |
michael@0 | 226 | except KeyError: |
michael@0 | 227 | raise DispatchException('No handler for: %r' % |
michael@0 | 228 | existing_resource_path) |
michael@0 | 229 | |
michael@0 | 230 | def source_warnings(self): |
michael@0 | 231 | """Return warnings in sourcing handlers.""" |
michael@0 | 232 | |
michael@0 | 233 | return self._source_warnings |
michael@0 | 234 | |
michael@0 | 235 | def do_extra_handshake(self, request): |
michael@0 | 236 | """Do extra checking in WebSocket handshake. |
michael@0 | 237 | |
michael@0 | 238 | Select a handler based on request.uri and call its |
michael@0 | 239 | web_socket_do_extra_handshake function. |
michael@0 | 240 | |
michael@0 | 241 | Args: |
michael@0 | 242 | request: mod_python request. |
michael@0 | 243 | |
michael@0 | 244 | Raises: |
michael@0 | 245 | DispatchException: when handler was not found |
michael@0 | 246 | AbortedByUserException: when user handler abort connection |
michael@0 | 247 | HandshakeException: when opening handshake failed |
michael@0 | 248 | """ |
michael@0 | 249 | |
michael@0 | 250 | handler_suite = self.get_handler_suite(request.ws_resource) |
michael@0 | 251 | if handler_suite is None: |
michael@0 | 252 | raise DispatchException('No handler for: %r' % request.ws_resource) |
michael@0 | 253 | do_extra_handshake_ = handler_suite.do_extra_handshake |
michael@0 | 254 | try: |
michael@0 | 255 | do_extra_handshake_(request) |
michael@0 | 256 | except handshake.AbortedByUserException, e: |
michael@0 | 257 | raise |
michael@0 | 258 | except Exception, e: |
michael@0 | 259 | util.prepend_message_to_exception( |
michael@0 | 260 | '%s raised exception for %s: ' % ( |
michael@0 | 261 | _DO_EXTRA_HANDSHAKE_HANDLER_NAME, |
michael@0 | 262 | request.ws_resource), |
michael@0 | 263 | e) |
michael@0 | 264 | raise handshake.HandshakeException(e, common.HTTP_STATUS_FORBIDDEN) |
michael@0 | 265 | |
michael@0 | 266 | def transfer_data(self, request): |
michael@0 | 267 | """Let a handler transfer_data with a WebSocket client. |
michael@0 | 268 | |
michael@0 | 269 | Select a handler based on request.ws_resource and call its |
michael@0 | 270 | web_socket_transfer_data function. |
michael@0 | 271 | |
michael@0 | 272 | Args: |
michael@0 | 273 | request: mod_python request. |
michael@0 | 274 | |
michael@0 | 275 | Raises: |
michael@0 | 276 | DispatchException: when handler was not found |
michael@0 | 277 | AbortedByUserException: when user handler abort connection |
michael@0 | 278 | """ |
michael@0 | 279 | |
michael@0 | 280 | handler_suite = self.get_handler_suite(request.ws_resource) |
michael@0 | 281 | if handler_suite is None: |
michael@0 | 282 | raise DispatchException('No handler for: %r' % request.ws_resource) |
michael@0 | 283 | transfer_data_ = handler_suite.transfer_data |
michael@0 | 284 | # TODO(tyoshino): Terminate underlying TCP connection if possible. |
michael@0 | 285 | try: |
michael@0 | 286 | transfer_data_(request) |
michael@0 | 287 | if not request.server_terminated: |
michael@0 | 288 | request.ws_stream.close_connection() |
michael@0 | 289 | # Catch non-critical exceptions the handler didn't handle. |
michael@0 | 290 | except handshake.AbortedByUserException, e: |
michael@0 | 291 | self._logger.debug('%s', e) |
michael@0 | 292 | raise |
michael@0 | 293 | except msgutil.BadOperationException, e: |
michael@0 | 294 | self._logger.debug('%s', e) |
michael@0 | 295 | request.ws_stream.close_connection(common.STATUS_ABNORMAL_CLOSURE) |
michael@0 | 296 | except msgutil.InvalidFrameException, e: |
michael@0 | 297 | # InvalidFrameException must be caught before |
michael@0 | 298 | # ConnectionTerminatedException that catches InvalidFrameException. |
michael@0 | 299 | self._logger.debug('%s', e) |
michael@0 | 300 | request.ws_stream.close_connection(common.STATUS_PROTOCOL_ERROR) |
michael@0 | 301 | except msgutil.UnsupportedFrameException, e: |
michael@0 | 302 | self._logger.debug('%s', e) |
michael@0 | 303 | request.ws_stream.close_connection(common.STATUS_UNSUPPORTED_DATA) |
michael@0 | 304 | except stream.InvalidUTF8Exception, e: |
michael@0 | 305 | self._logger.debug('%s', e) |
michael@0 | 306 | request.ws_stream.close_connection( |
michael@0 | 307 | common.STATUS_INVALID_FRAME_PAYLOAD_DATA) |
michael@0 | 308 | except msgutil.ConnectionTerminatedException, e: |
michael@0 | 309 | self._logger.debug('%s', e) |
michael@0 | 310 | except Exception, e: |
michael@0 | 311 | util.prepend_message_to_exception( |
michael@0 | 312 | '%s raised exception for %s: ' % ( |
michael@0 | 313 | _TRANSFER_DATA_HANDLER_NAME, request.ws_resource), |
michael@0 | 314 | e) |
michael@0 | 315 | raise |
michael@0 | 316 | |
michael@0 | 317 | def passive_closing_handshake(self, request): |
michael@0 | 318 | """Prepare code and reason for responding client initiated closing |
michael@0 | 319 | handshake. |
michael@0 | 320 | """ |
michael@0 | 321 | |
michael@0 | 322 | handler_suite = self.get_handler_suite(request.ws_resource) |
michael@0 | 323 | if handler_suite is None: |
michael@0 | 324 | return _default_passive_closing_handshake_handler(request) |
michael@0 | 325 | return handler_suite.passive_closing_handshake(request) |
michael@0 | 326 | |
michael@0 | 327 | def get_handler_suite(self, resource): |
michael@0 | 328 | """Retrieves two handlers (one for extra handshake processing, and one |
michael@0 | 329 | for data transfer) for the given request as a HandlerSuite object. |
michael@0 | 330 | """ |
michael@0 | 331 | |
michael@0 | 332 | fragment = None |
michael@0 | 333 | if '#' in resource: |
michael@0 | 334 | resource, fragment = resource.split('#', 1) |
michael@0 | 335 | if '?' in resource: |
michael@0 | 336 | resource = resource.split('?', 1)[0] |
michael@0 | 337 | handler_suite = self._handler_suite_map.get(resource) |
michael@0 | 338 | if handler_suite and fragment: |
michael@0 | 339 | raise DispatchException('Fragment identifiers MUST NOT be used on ' |
michael@0 | 340 | 'WebSocket URIs', |
michael@0 | 341 | common.HTTP_STATUS_BAD_REQUEST) |
michael@0 | 342 | return handler_suite |
michael@0 | 343 | |
michael@0 | 344 | def _source_handler_files_in_dir( |
michael@0 | 345 | self, root_dir, scan_dir, allow_handlers_outside_root_dir): |
michael@0 | 346 | """Source all the handler source files in the scan_dir directory. |
michael@0 | 347 | |
michael@0 | 348 | The resource path is determined relative to root_dir. |
michael@0 | 349 | """ |
michael@0 | 350 | |
michael@0 | 351 | # We build a map from resource to handler code assuming that there's |
michael@0 | 352 | # only one path from root_dir to scan_dir and it can be obtained by |
michael@0 | 353 | # comparing realpath of them. |
michael@0 | 354 | |
michael@0 | 355 | # Here we cannot use abspath. See |
michael@0 | 356 | # https://bugs.webkit.org/show_bug.cgi?id=31603 |
michael@0 | 357 | |
michael@0 | 358 | convert = _create_path_to_resource_converter(root_dir) |
michael@0 | 359 | scan_realpath = os.path.realpath(scan_dir) |
michael@0 | 360 | root_realpath = os.path.realpath(root_dir) |
michael@0 | 361 | for path in _enumerate_handler_file_paths(scan_realpath): |
michael@0 | 362 | if (not allow_handlers_outside_root_dir and |
michael@0 | 363 | (not os.path.realpath(path).startswith(root_realpath))): |
michael@0 | 364 | self._logger.debug( |
michael@0 | 365 | 'Canonical path of %s is not under root directory' % |
michael@0 | 366 | path) |
michael@0 | 367 | continue |
michael@0 | 368 | try: |
michael@0 | 369 | handler_suite = _source_handler_file(open(path).read()) |
michael@0 | 370 | except DispatchException, e: |
michael@0 | 371 | self._source_warnings.append('%s: %s' % (path, e)) |
michael@0 | 372 | continue |
michael@0 | 373 | resource = convert(path) |
michael@0 | 374 | if resource is None: |
michael@0 | 375 | self._logger.debug( |
michael@0 | 376 | 'Path to resource conversion on %s failed' % path) |
michael@0 | 377 | else: |
michael@0 | 378 | self._handler_suite_map[convert(path)] = handler_suite |
michael@0 | 379 | |
michael@0 | 380 | |
michael@0 | 381 | # vi:sts=4 sw=4 et |