diff options
Diffstat (limited to 'WebKitTools/pywebsocket/mod_pywebsocket')
4 files changed, 202 insertions, 7 deletions
diff --git a/WebKitTools/pywebsocket/mod_pywebsocket/dispatch.py b/WebKitTools/pywebsocket/mod_pywebsocket/dispatch.py index bf9a856..c52e9eb 100644 --- a/WebKitTools/pywebsocket/mod_pywebsocket/dispatch.py +++ b/WebKitTools/pywebsocket/mod_pywebsocket/dispatch.py @@ -142,6 +142,23 @@ class Dispatcher(object): 'root_dir:%s.' % (scan_dir, root_dir)) self._source_files_in_dir(root_dir, scan_dir) + def add_resource_path_alias(self, + alias_resource_path, existing_resource_path): + """Add resource path alias. + + Once added, request to alias_resource_path would be handled by + handler registered for existing_resource_path. + + Args: + alias_resource_path: alias resource path + existing_resource_path: existing resource path + """ + try: + handler = self._handlers[existing_resource_path] + self._handlers[alias_resource_path] = handler + except KeyError: + raise DispatchError('No handler for: %r' % existing_resource_path) + def source_warnings(self): """Return warnings in sourcing handlers.""" diff --git a/WebKitTools/pywebsocket/mod_pywebsocket/handshake.py b/WebKitTools/pywebsocket/mod_pywebsocket/handshake.py index a67aadd..b86278e 100644 --- a/WebKitTools/pywebsocket/mod_pywebsocket/handshake.py +++ b/WebKitTools/pywebsocket/mod_pywebsocket/handshake.py @@ -39,14 +39,14 @@ not suitable because they don't allow direct raw bytes writing/reading. import re +import util + _DEFAULT_WEB_SOCKET_PORT = 80 _DEFAULT_WEB_SOCKET_SECURE_PORT = 443 _WEB_SOCKET_SCHEME = 'ws' _WEB_SOCKET_SECURE_SCHEME = 'wss' -_METHOD_LINE = re.compile(r'^GET ([^ ]+) HTTP/1.1\r\n$') - _MANDATORY_HEADERS = [ # key, expected value or None ['Upgrade', 'WebSocket'], @@ -55,6 +55,22 @@ _MANDATORY_HEADERS = [ ['Origin', None], ] +_FIRST_FIVE_LINES = map(re.compile, [ + r'^GET /[\S]* HTTP/1.1\r\n$', + r'^Upgrade: WebSocket\r\n$', + r'^Connection: Upgrade\r\n$', + r'^Host: [\S]+\r\n$', + r'^Origin: [\S]+\r\n$', +]) + +_SIXTH_AND_LATER = re.compile( + r'^' + r'(WebSocket-Protocol: [\x20-\x7e]+\r\n)?' + r'(Cookie: [^\r]*\r\n)*' + r'(Cookie2: [^\r]*\r\n)?' + r'(Cookie: [^\r]*\r\n)*' + r'\r\n') + def _default_port(is_secure): if is_secure: @@ -75,19 +91,22 @@ def _validate_protocol(protocol): if not protocol: raise HandshakeError('Invalid WebSocket-Protocol: empty') for c in protocol: - if not 0x21 <= ord(c) <= 0x7e: + if not 0x20 <= ord(c) <= 0x7e: raise HandshakeError('Illegal character in protocol: %r' % c) class Handshaker(object): """This class performs Web Socket handshake.""" - def __init__(self, request, dispatcher): + def __init__(self, request, dispatcher, strict=False): """Construct an instance. Args: request: mod_python request. dispatcher: Dispatcher (dispatch.Dispatcher). + strict: Strictly check handshake request. Default: False. + If True, request.connection must provide get_memorized_lines + method. Handshaker will add attributes such as ws_resource in performing handshake. @@ -95,6 +114,7 @@ class Handshaker(object): self._request = request self._dispatcher = dispatcher + self._strict = strict def do_handshake(self): """Perform Web Socket Handshake.""" @@ -173,6 +193,28 @@ class Handshaker(object): if actual_value != expected_value: raise HandshakeError('Illegal value for header %s: %s' % (key, actual_value)) + if self._strict: + try: + lines = self._request.connection.get_memorized_lines() + except AttributeError, e: + util.prepend_message_to_exception( + 'Strict handshake is specified but the connection ' + 'doesn\'t provide get_memorized_lines()', e) + raise + self._check_first_lines(lines) + + def _check_first_lines(self, lines): + if len(lines) < len(_FIRST_FIVE_LINES): + raise HandshakeError('Too few header lines: %d' % len(lines)) + for line, regexp in zip(lines, _FIRST_FIVE_LINES): + if not regexp.search(line): + raise HandshakeError('Unexpected header: %r doesn\'t match %r' + % (line, regexp.pattern)) + sixth_and_later = ''.join(lines[5:]) + if not _SIXTH_AND_LATER.search(sixth_and_later): + raise HandshakeError('Unexpected header: %r doesn\'t match %r' + % (sixth_and_later, + _SIXTH_AND_LATER.pattern)) # vi:sts=4 sw=4 et diff --git a/WebKitTools/pywebsocket/mod_pywebsocket/memorizingfile.py b/WebKitTools/pywebsocket/mod_pywebsocket/memorizingfile.py new file mode 100644 index 0000000..2f8a54e --- /dev/null +++ b/WebKitTools/pywebsocket/mod_pywebsocket/memorizingfile.py @@ -0,0 +1,81 @@ +#!/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. + + +"""Memorizing file. + +A memorizing file wraps a file and memorizes lines read by readline. +""" + + +import sys + + +class MemorizingFile(object): + """MemorizingFile wraps a file and memorizes lines read by readline. + + Note that data read by other methods are not memorized. This behavior + is good enough for memorizing lines SimpleHTTPServer reads before + the control reaches WebSocketRequestHandler. + """ + def __init__(self, file_, max_memorized_lines=sys.maxint): + """Construct an instance. + + Args: + file_: the file object to wrap. + max_memorized_lines: the maximum number of lines to memorize. + Only the first max_memorized_lines are memorized. + Default: sys.maxint. + """ + self._file = file_ + self._memorized_lines = [] + self._max_memorized_lines = max_memorized_lines + + def __getattribute__(self, name): + if name in ('_file', '_memorized_lines', '_max_memorized_lines', + 'readline', 'get_memorized_lines'): + return object.__getattribute__(self, name) + return self._file.__getattribute__(name) + + def readline(self): + """Override file.readline and memorize the line read.""" + + line = self._file.readline() + if line and len(self._memorized_lines) < self._max_memorized_lines: + self._memorized_lines.append(line) + return line + + def get_memorized_lines(self): + """Get lines memorized so far.""" + return self._memorized_lines + + +# vi:sts=4 sw=4 et diff --git a/WebKitTools/pywebsocket/mod_pywebsocket/standalone.py b/WebKitTools/pywebsocket/mod_pywebsocket/standalone.py index 6217585..0e6a349 100644 --- a/WebKitTools/pywebsocket/mod_pywebsocket/standalone.py +++ b/WebKitTools/pywebsocket/mod_pywebsocket/standalone.py @@ -38,6 +38,7 @@ Usage: python standalone.py [-p <ws_port>] [-w <websock_handlers>] [-s <scan_dir>] [-d <document_root>] + [-m <websock_handlers_map_file>] ... for other options, see _main below ... <ws_port> is the port number to use for ws:// connection. @@ -63,6 +64,7 @@ import logging import logging.handlers import optparse import os +import re import socket import sys @@ -75,6 +77,7 @@ except ImportError: import dispatch import handshake +import memorizingfile import util @@ -88,6 +91,10 @@ _LOG_LEVELS = { _DEFAULT_LOG_MAX_BYTES = 1024 * 256 _DEFAULT_LOG_BACKUP_COUNT = 5 +_DEFAULT_REQUEST_QUEUE_SIZE = 128 + +# 1024 is practically large enough to contain WebSocket handshake lines. +_MAX_MEMORIZED_LINES = 1024 def _print_warnings_if_any(dispatcher): warnings = dispatcher.source_warnings() @@ -129,6 +136,10 @@ class _StandaloneConnection(object): """Mimic mp_conn.read().""" return self._request_handler.rfile.read(length) + def get_memorized_lines(self): + """Get memorized lines.""" + return self._request_handler.rfile.get_memorized_lines() + class _StandaloneRequest(object): """Mimic mod_python request.""" @@ -198,7 +209,9 @@ class WebSocketRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): """Override SocketServer.StreamRequestHandler.setup.""" self.connection = self.request - self.rfile = socket._fileobject(self.request, 'rb', self.rbufsize) + self.rfile = memorizingfile.MemorizingFile( + socket._fileobject(self.request, 'rb', self.rbufsize), + max_memorized_lines=_MAX_MEMORIZED_LINES) self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize) def __init__(self, *args, **keywords): @@ -206,8 +219,9 @@ class WebSocketRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): self, WebSocketRequestHandler.options.use_tls) self._dispatcher = WebSocketRequestHandler.options.dispatcher self._print_warnings_if_any() - self._handshaker = handshake.Handshaker(self._request, - self._dispatcher) + self._handshaker = handshake.Handshaker( + self._request, self._dispatcher, + WebSocketRequestHandler.options.strict) SimpleHTTPServer.SimpleHTTPRequestHandler.__init__( self, *args, **keywords) @@ -268,6 +282,31 @@ def _configure_logging(options): handler.setFormatter(formatter) logger.addHandler(handler) +def _alias_handlers(dispatcher, websock_handlers_map_file): + """Set aliases specified in websock_handler_map_file in dispatcher. + + Args: + dispatcher: dispatch.Dispatcher instance + websock_handler_map_file: alias map file + """ + fp = open(websock_handlers_map_file) + try: + for line in fp: + if line[0] == '#' or line.isspace(): + continue + m = re.match('(\S+)\s+(\S+)', line) + if not m: + logging.warning('Wrong format in map file:' + line) + continue + try: + dispatcher.add_resource_path_alias( + m.group(1), m.group(2)) + except dispatch.DispatchError, e: + logging.error(str(e)) + finally: + fp.close() + + def _main(): parser = optparse.OptionParser() @@ -277,6 +316,12 @@ def _main(): parser.add_option('-w', '--websock_handlers', dest='websock_handlers', default='.', help='Web Socket handlers root directory.') + parser.add_option('-m', '--websock_handlers_map_file', + dest='websock_handlers_map_file', + default=None, + help=('Web Socket handlers map file. ' + 'Each line consists of alias_resource_path and ' + 'existing_resource_path, separated by spaces.')) parser.add_option('-s', '--scan_dir', dest='scan_dir', default=None, help=('Web Socket handlers scan directory. ' @@ -302,12 +347,19 @@ def _main(): parser.add_option('--log_count', dest='log_count', type='int', default=_DEFAULT_LOG_BACKUP_COUNT, help='Log backup count') + parser.add_option('--strict', dest='strict', action='store_true', + default=False, help='Strictly check handshake request') + parser.add_option('-q', '--queue', dest='request_queue_size', type='int', + default=_DEFAULT_REQUEST_QUEUE_SIZE, + help='request queue size') options = parser.parse_args()[0] os.chdir(options.document_root) _configure_logging(options) + SocketServer.TCPServer.request_queue_size = options.request_queue_size + if options.use_tls: if not _HAS_OPEN_SSL: logging.critical('To use TLS, install pyOpenSSL.') @@ -325,6 +377,9 @@ def _main(): # instantiation. Dispatcher can be shared because it is thread-safe. options.dispatcher = dispatch.Dispatcher(options.websock_handlers, options.scan_dir) + if options.websock_handlers_map_file: + _alias_handlers(options.dispatcher, + options.websock_handlers_map_file) _print_warnings_if_any(options.dispatcher) WebSocketRequestHandler.options = options |
