• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
35import os
36import re
37
38import 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
47class DispatchError(Exception):
48    """Exception in dispatching Web Socket request."""
49
50    pass
51
52
53def _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
70def _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
84def _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
94def _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
107def _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
116class 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 add_resource_path_alias(self,
146                                alias_resource_path, existing_resource_path):
147        """Add resource path alias.
148
149        Once added, request to alias_resource_path would be handled by
150        handler registered for existing_resource_path.
151
152        Args:
153            alias_resource_path: alias resource path
154            existing_resource_path: existing resource path
155        """
156        try:
157            handler = self._handlers[existing_resource_path]
158            self._handlers[alias_resource_path] = handler
159        except KeyError:
160            raise DispatchError('No handler for: %r' % existing_resource_path)
161
162    def source_warnings(self):
163        """Return warnings in sourcing handlers."""
164
165        return self._source_warnings
166
167    def do_extra_handshake(self, request):
168        """Do extra checking in Web Socket handshake.
169
170        Select a handler based on request.uri and call its
171        web_socket_do_extra_handshake function.
172
173        Args:
174            request: mod_python request.
175        """
176
177        do_extra_handshake_, unused_transfer_data = self._handler(request)
178        try:
179            do_extra_handshake_(request)
180        except Exception, e:
181            util.prepend_message_to_exception(
182                    '%s raised exception for %s: ' % (
183                            _DO_EXTRA_HANDSHAKE_HANDLER_NAME,
184                            request.ws_resource),
185                    e)
186            raise
187
188    def transfer_data(self, request):
189        """Let a handler transfer_data with a Web Socket client.
190
191        Select a handler based on request.ws_resource and call its
192        web_socket_transfer_data function.
193
194        Args:
195            request: mod_python request.
196        """
197
198        unused_do_extra_handshake, transfer_data_ = self._handler(request)
199        try:
200            transfer_data_(request)
201        except Exception, e:
202            util.prepend_message_to_exception(
203                    '%s raised exception for %s: ' % (
204                            _TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
205                    e)
206            raise
207
208    def _handler(self, request):
209        try:
210            ws_resource_path = request.ws_resource.split('?', 1)[0]
211            return self._handlers[ws_resource_path]
212        except KeyError:
213            raise DispatchError('No handler for: %r' % request.ws_resource)
214
215    def _source_files_in_dir(self, root_dir, scan_dir):
216        """Source all the handler source files in the scan_dir directory.
217
218        The resource path is determined relative to root_dir.
219        """
220
221        to_resource = _path_to_resource_converter(root_dir)
222        for path in _source_file_paths(scan_dir):
223            try:
224                handlers = _source(open(path).read())
225            except DispatchError, e:
226                self._source_warnings.append('%s: %s' % (path, e))
227                continue
228            self._handlers[to_resource(path)] = handlers
229
230
231# vi:sts=4 sw=4 et
232