2009-11-25 Yuzo Fujishima <yuzo@google.com>
[WebKit-https.git] / WebKitTools / pywebsocket / mod_pywebsocket / dispatch.py
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
35 import os
36 import re
37
38 import 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
47 class DispatchError(Exception):
48     """Exception in dispatching Web Socket request."""
49
50     pass
51
52
53 def _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
70 def _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
84 def _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
94 def _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
107 def _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
116 class 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 source_warnings(self):
146         """Return warnings in sourcing handlers."""
147
148         return self._source_warnings
149
150     def do_extra_handshake(self, request):
151         """Do extra checking in Web Socket handshake.
152
153         Select a handler based on request.uri and call its
154         web_socket_do_extra_handshake function.
155
156         Args:
157             request: mod_python request.
158         """
159
160         do_extra_handshake_, unused_transfer_data = self._handler(request)
161         try:
162             do_extra_handshake_(request)
163         except Exception:
164             raise DispatchError('%s raised exception: %s' %
165                     (_DO_EXTRA_HANDSHAKE_HANDLER_NAME, util.get_stack_trace()))
166
167     def transfer_data(self, request):
168         """Let a handler transfer_data with a Web Socket client.
169
170         Select a handler based on request.ws_resource and call its
171         web_socket_transfer_data function.
172
173         Args:
174             request: mod_python request.
175         """
176
177         unused_do_extra_handshake, transfer_data_ = self._handler(request)
178         try:
179             transfer_data_(request)
180         except Exception:
181             raise DispatchError('%s raised exception: %s' %
182                     (_TRANSFER_DATA_HANDLER_NAME, util.get_stack_trace()))
183
184     def _handler(self, request):
185         try:
186             ws_resource_path = request.ws_resource.split('?', 1)[0]
187             return self._handlers[ws_resource_path]
188         except KeyError:
189             raise DispatchError('No handler for: %r' % request.ws_resource)
190
191     def _source_files_in_dir(self, root_dir, scan_dir):
192         """Source all the handler source files in the scan_dir directory.
193
194         The resource path is determined relative to root_dir.
195         """
196
197         to_resource = _path_to_resource_converter(root_dir)
198         for path in _source_file_paths(scan_dir):
199             try:
200                 handlers = _source(open(path).read())
201             except DispatchError, e:
202                 self._source_warnings.append('%s: %s' % (path, e))
203                 continue
204             self._handlers[to_resource(path)] = handlers
205
206
207 # vi:sts=4 sw=4 et