testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py

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

mercurial