Commit b66f65b2 authored by eric@webkit.org's avatar eric@webkit.org

2009-10-15 Yuzo Fujishima <yuzo@google.com>

        Reviewed by David Levin.

        Add mod_pywebsocket to test Web Sockets.
        http://code.google.com/p/pywebsocket/
        https://bugs.webkit.org/show_bug.cgi?id=27490

        * pywebsocket/COPYING: Added.
        * pywebsocket/MANIFEST.in: Added.
        * pywebsocket/README: Added.
        * pywebsocket/example/echo_client.py: Added.
        * pywebsocket/example/echo_wsh.py: Added.
        * pywebsocket/mod_pywebsocket/__init__.py: Added.
        * pywebsocket/mod_pywebsocket/dispatch.py: Added.
        * pywebsocket/mod_pywebsocket/handshake.py: Added.
        * pywebsocket/mod_pywebsocket/headerparserhandler.py: Added.
        * pywebsocket/mod_pywebsocket/msgutil.py: Added.
        * pywebsocket/mod_pywebsocket/standalone.py: Added.
        * pywebsocket/mod_pywebsocket/util.py: Added.
        * pywebsocket/setup.py: Added.
        * pywebsocket/test/config.py: Added.
        * pywebsocket/test/mock.py: Added.
        * pywebsocket/test/run_all.py: Added.
        * pywebsocket/test/test_dispatch.py: Added.
        * pywebsocket/test/test_handshake.py: Added.
        * pywebsocket/test/test_mock.py: Added.
        * pywebsocket/test/test_msgutil.py: Added.
        * pywebsocket/test/test_util.py: Added.
        * pywebsocket/test/testdata/handlers/blank_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/origin_check_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/sub/exception_in_transfer_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/sub/no_wsh_at_the_end.py: Added.
        * pywebsocket/test/testdata/handlers/sub/non_callable_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/sub/plain_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py: Added.
        * pywebsocket/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@49672 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent c69b85e9
2009-10-15 Yuzo Fujishima <yuzo@google.com>
Reviewed by David Levin.
Add mod_pywebsocket to test Web Sockets.
http://code.google.com/p/pywebsocket/
https://bugs.webkit.org/show_bug.cgi?id=27490
* pywebsocket/COPYING: Added.
* pywebsocket/MANIFEST.in: Added.
* pywebsocket/README: Added.
* pywebsocket/example/echo_client.py: Added.
* pywebsocket/example/echo_wsh.py: Added.
* pywebsocket/mod_pywebsocket/__init__.py: Added.
* pywebsocket/mod_pywebsocket/dispatch.py: Added.
* pywebsocket/mod_pywebsocket/handshake.py: Added.
* pywebsocket/mod_pywebsocket/headerparserhandler.py: Added.
* pywebsocket/mod_pywebsocket/msgutil.py: Added.
* pywebsocket/mod_pywebsocket/standalone.py: Added.
* pywebsocket/mod_pywebsocket/util.py: Added.
* pywebsocket/setup.py: Added.
* pywebsocket/test/config.py: Added.
* pywebsocket/test/mock.py: Added.
* pywebsocket/test/run_all.py: Added.
* pywebsocket/test/test_dispatch.py: Added.
* pywebsocket/test/test_handshake.py: Added.
* pywebsocket/test/test_mock.py: Added.
* pywebsocket/test/test_msgutil.py: Added.
* pywebsocket/test/test_util.py: Added.
* pywebsocket/test/testdata/handlers/blank_wsh.py: Added.
* pywebsocket/test/testdata/handlers/origin_check_wsh.py: Added.
* pywebsocket/test/testdata/handlers/sub/exception_in_transfer_wsh.py: Added.
* pywebsocket/test/testdata/handlers/sub/no_wsh_at_the_end.py: Added.
* pywebsocket/test/testdata/handlers/sub/non_callable_wsh.py: Added.
* pywebsocket/test/testdata/handlers/sub/plain_wsh.py: Added.
* pywebsocket/test/testdata/handlers/sub/wrong_handshake_sig_wsh.py: Added.
* pywebsocket/test/testdata/handlers/sub/wrong_transfer_sig_wsh.py: Added.
2009-10-15 James Robinson <jamesr@google.com>
Reviewed by David Levin.
......
Copyright 2009, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
include COPYING
include MANIFEST.in
include README
recursive-include example *.py
recursive-include mod_pywebsocket *.py
recursive-include test *.py
Install this package by:
$ python setup.py build
$ sudo python setup.py install
Then read document by:
$ pydoc mod_pywebsocket
#!/usr/bin/env python
#
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Web Socket Echo client.
This is an example Web Socket client that talks with echo_wsh.py.
This may be useful for checking mod_pywebsocket installation.
Note:
This code is far from robust, e.g., we cut corners in handshake.
"""
import codecs
from optparse import OptionParser
import socket
import sys
_DEFAULT_PORT = 80
_DEFAULT_SECURE_PORT = 443
_UNDEFINED_PORT = -1
_UPGRADE_HEADER = 'Upgrade: WebSocket\r\n'
_CONNECTION_HEADER = 'Connection: Upgrade\r\n'
_EXPECTED_RESPONSE = (
'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
_UPGRADE_HEADER +
_CONNECTION_HEADER)
def _method_line(resource):
return 'GET %s HTTP/1.1\r\n' % resource
def _origin_header(origin):
return 'Origin: %s\r\n' % origin
class _TLSSocket(object):
"""Wrapper for a TLS connection."""
def __init__(self, raw_socket):
self._ssl = socket.ssl(raw_socket)
def send(self, bytes):
return self._ssl.write(bytes)
def recv(self, size=-1):
return self._ssl.read(size)
def close(self):
# Nothing to do.
pass
class EchoClient(object):
"""Web Socket echo client."""
def __init__(self, options):
self._options = options
self._socket = None
def run(self):
"""Run the client.
Shake hands and then repeat sending message and receiving its echo.
"""
self._socket = socket.socket()
try:
self._socket.connect((self._options.server_host,
self._options.server_port))
if self._options.use_tls:
self._socket = _TLSSocket(self._socket)
self._handshake()
for line in self._options.message.split(','):
frame = '\x00' + line.encode('utf-8') + '\xff'
self._socket.send(frame)
if self._options.verbose:
print 'Send: %s' % line
received = self._socket.recv(len(frame))
if received != frame:
raise Exception('Incorrect echo: %r' % received)
if self._options.verbose:
print 'Recv: %s' % received[1:-1].decode('utf-8')
finally:
self._socket.close()
def _handshake(self):
self._socket.send(_method_line(self._options.resource))
self._socket.send(_UPGRADE_HEADER)
self._socket.send(_CONNECTION_HEADER)
self._socket.send(self._format_host_header())
self._socket.send(_origin_header(self._options.origin))
self._socket.send('\r\n')
for expected_char in _EXPECTED_RESPONSE:
received = self._socket.recv(1)[0]
if expected_char != received:
raise Exception('Handshake failure')
# We cut corners and skip other headers.
self._skip_headers()
def _skip_headers(self):
terminator = '\r\n\r\n'
pos = 0
while pos < len(terminator):
received = self._socket.recv(1)[0]
if received == terminator[pos]:
pos += 1
elif received == terminator[0]:
pos = 1
else:
pos = 0
def _format_host_header(self):
host = 'Host: ' + self._options.server_host
if ((not self._options.use_tls and
self._options.server_port != _DEFAULT_PORT) or
(self._options.use_tls and
self._options.server_port != _DEFAULT_SECURE_PORT)):
host += ':' + str(self._options.server_port)
host += '\r\n'
return host
def main():
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
parser = OptionParser()
parser.add_option('-s', '--server_host', dest='server_host', type='string',
default='localhost', help='server host')
parser.add_option('-p', '--server_port', dest='server_port', type='int',
default=_UNDEFINED_PORT, help='server port')
parser.add_option('-o', '--origin', dest='origin', type='string',
default='http://localhost/', help='origin')
parser.add_option('-r', '--resource', dest='resource', type='string',
default='/echo', help='resource path')
parser.add_option('-m', '--message', dest='message', type='string',
help='comma-separated messages to send')
parser.add_option('-q', '--quiet', dest='verbose', action='store_false',
default=True, help='suppress messages')
parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
default=False, help='use TLS (wss://)')
(options, unused_args) = parser.parse_args()
# Default port number depends on whether TLS is used.
if options.server_port == _UNDEFINED_PORT:
if options.use_tls:
options.server_port = _DEFAULT_SECURE_PORT
else:
options.server_port = _DEFAULT_PORT
# optparse doesn't seem to handle non-ascii default values.
# Set default message here.
if not options.message:
options.message = u'Hello,\u65e5\u672c' # "Japan" in Japanese
EchoClient(options).run()
if __name__ == '__main__':
main()
# vi:sts=4 sw=4 et
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from mod_pywebsocket import msgutil
def web_socket_do_extra_handshake(request):
pass # Always accept.
def web_socket_transfer_data(request):
while True:
line = msgutil.receive_message(request)
msgutil.send_message(request, line)
# vi:sts=4 sw=4 et
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Web Socket extension for Apache HTTP Server.
mod_pywebsocket is a Web Socket extension for Apache HTTP Server
intended for testing or experimental purposes. mod_python is required.
Installation:
0. Prepare an Apache HTTP Server for which mod_python is enabled.
1. Specify the following Apache HTTP Server directives to suit your
configuration.
If mod_pywebsocket is not in the Python path, specify the following.
<websock_lib> is the directory where mod_pywebsocket is installed.
PythonPath "sys.path+['<websock_lib>']"
Always specify the following. <websock_handlers> is the directory where
user-written Web Socket handlers are placed.
PythonOption mod_pywebsocket.handler_root <websock_handlers>
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
Example snippet of httpd.conf:
(mod_pywebsocket is in /websock_lib, Web Socket handlers are in
/websock_handlers, port is 80 for ws, 443 for wss.)
<IfModule python_module>
PythonPath "sys.path+['/websock_lib']"
PythonOption mod_pywebsocket.handler_root /websock_handlers
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
</IfModule>
Writing Web Socket handlers:
When a Web Socket request comes in, the resource name
specified in the handshake is considered as if it is a file path under
<websock_handlers> and the handler defined in
<websock_handlers>/<resource_name>_wsh.py is invoked.
For example, if the resource name is /example/chat, the handler defined in
<websock_handlers>/example/chat_wsh.py is invoked.
A Web Socket handler is composed of the following two functions:
web_socket_do_extra_handshake(request)
web_socket_transfer_data(request)
where:
request: mod_python request.
web_socket_do_extra_handshake is called during the handshake after the
headers are successfully parsed and Web Socket properties (ws_location,
ws_origin, ws_protocol, and ws_resource) are added to request. A handler
can reject the request by raising an exception.
web_socket_transfer_data is called after the handshake completed
successfully. A handler can receive/send messages from/to the client
using request. mod_pywebsocket.msgutil module provides utilities
for data transfer.
"""
# vi:sts=4 sw=4 et tw=72
# Copyright 2009, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Dispatch Web Socket request.
"""
import os
import re
import util
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
_SOURCE_SUFFIX = '_wsh.py'
_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
class DispatchError(Exception):
"""Exception in dispatching Web Socket request."""
pass
def _normalize_path(path):
"""Normalize path.
Args:
path: the path to normalize.
Path is converted to the absolute path.
The input path can use either '\\' or '/' as the separator.
The normalized path always uses '/' regardless of the platform.
"""
path = path.replace('\\', os.path.sep)
path = os.path.abspath(path)
path = path.replace('\\', '/')
return path
def _path_to_resource_converter(base_dir):
base_dir = _normalize_path(base_dir)
base_len = len(base_dir)
suffix_len = len(_SOURCE_SUFFIX)
def converter(path):
if not path.endswith(_SOURCE_SUFFIX):
return None
path = _normalize_path(path)
if not path.startswith(base_dir):
return None
return path[base_len:-suffix_len]
return converter
def _source_file_paths(directory):
"""Yield Web Socket Handler source file names in the given directory."""
for root, unused_dirs, files in os.walk(directory):
for base in files:
path = os.path.join(root, base)
if _SOURCE_PATH_PATTERN.search(path):
yield path
def _source(source_str):
"""Source a handler definition string."""
global_dic = {}
try:
exec source_str in global_dic
except Exception:
raise DispatchError('Error in sourcing handler:' +
util.get_stack_trace())
return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
def _extract_handler(dic, name):
if name not in dic:
raise DispatchError('%s is not defined.' % name)
handler = dic[name]
if not callable(handler):
raise DispatchError('%s is not callable.' % name)
return handler
class Dispatcher(object):
"""Dispatches Web Socket requests.
This class maintains a map from resource name to handlers.
"""
def __init__(self, root_dir):
"""Construct an instance.
Args:
root_dir: The directory where handler definition files are
placed.
"""
self._handlers = {}
self._source_warnings = []
self._source_files_in_dir(root_dir)
def source_warnings(self):
"""Return warnings in sourcing handlers."""
return self._source_warnings
def do_extra_handshake(self, request):
"""Do extra checking in Web Socket handshake.
Select a handler based on request.uri and call its
web_socket_do_extra_handshake function.
Args:
request: mod_python request.
"""
do_extra_handshake_, unused_transfer_data = self._handler(request)
try:
do_extra_handshake_(request)
except Exception:
raise DispatchError('%s raised exception: %s' %
(_DO_EXTRA_HANDSHAKE_HANDLER_NAME, util.get_stack_trace()))
def transfer_data(self, request):
"""Let a handler transfer_data with a Web Socket client.
Select a handler based on request.ws_resource and call its
web_socket_transfer_data function.
Args:
request: mod_python request.
"""
unused_do_extra_handshake, transfer_data_ = self._handler(request)
try:
transfer_data_(request)
except Exception:
raise DispatchError('%s raised exception: %s' %
(_TRANSFER_DATA_HANDLER_NAME, util.get_stack_trace()))
def _handler(self, request):
try:
return self._handlers[request.ws_resource]
except KeyError:
raise DispatchError('No handler for: %r' % request.ws_resource)
def _source_files_in_dir(self, root_dir):
"""Source all the handler source files in the directory."""
to_resource = _path_to_resource_converter(root_dir)
for path in _source_file_paths(root_dir):
try:
handlers = _source(open(path).read())
except DispatchError, e:
self._source_warnings.append('%s: %s' % (path, e))
continue
self._handlers[to_resource(path)] = handlers
# vi:sts=4 sw=4 et