don't use autoinstall to import pywebsocket but check it in WebKit directly.
[WebKit-https.git] / Tools / Scripts / webkitpy / thirdparty / mod_pywebsocket / dispatch.py
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