Bug 570789. Add WebSocket support to mochitest. r=ted

--HG--
extra : rebase_source : 2e33d054a13824d1a7e527777383820946684c3f
This commit is contained in:
Jonathan Griffin 2010-06-16 22:38:55 -07:00
parent b51e1f6c6a
commit e6fdd7ae87
18 changed files with 2317 additions and 14 deletions

View File

@ -58,6 +58,8 @@ import automationutils
_DEFAULT_WEB_SERVER = "127.0.0.1"
_DEFAULT_HTTP_PORT = 8888
_DEFAULT_SSL_PORT = 4443
_DEFAULT_WEBSOCKET_PORT = 9999
_DEFAULT_WEBSOCKET_PROXY_PORT = 7777
#expand _DIST_BIN = __XPC_BIN_PATH__
#expand _IS_WIN32 = len("__WIN32__") != 0
@ -151,15 +153,24 @@ class Automation(object):
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
DEFAULT_WEBSOCKET_PROXY_PORT = _DEFAULT_WEBSOCKET_PROXY_PORT
def __init__(self):
self.log = _log
self.lastTestSeen = "automation.py"
def setServerInfo(self, webServer = _DEFAULT_WEB_SERVER, httpPort = _DEFAULT_HTTP_PORT, sslPort = _DEFAULT_SSL_PORT):
def setServerInfo(self,
webServer = _DEFAULT_WEB_SERVER,
httpPort = _DEFAULT_HTTP_PORT,
sslPort = _DEFAULT_SSL_PORT,
webSocketPort = _DEFAULT_WEBSOCKET_PORT,
webSocketProxyPort = _DEFAULT_WEBSOCKET_PROXY_PORT):
self.webServer = webServer
self.httpPort = httpPort
self.sslPort = sslPort
self.webSocketPort = webSocketPort
self.webSocketProxyPort = webSocketProxyPort
@property
def __all__(self):
@ -375,11 +386,14 @@ function FindProxyForURL(url, host)
return 'DIRECT';
var isHttp = matches[1] == 'http';
var isHttps = matches[1] == 'https';
var isWebSocket = matches[1] == 'ws';
if (!matches[3])
{
if (isHttp) matches[3] = '80';
if (isHttp | isWebSocket) matches[3] = '80';
if (isHttps) matches[3] = '443';
}
if (isWebSocket)
matches[1] = 'http';
var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
if (origins.indexOf(origin) < 0)
@ -388,11 +402,14 @@ function FindProxyForURL(url, host)
return 'PROXY %(remote)s:%(httpport)s';
if (isHttps)
return 'PROXY %(remote)s:%(sslport)s';
if (isWebSocket)
return 'PROXY %(remote)s:%(websocketproxyport)s';
return 'DIRECT';
}""" % { "origins": origins,
"remote": self.webServer,
"httpport":self.httpPort,
"sslport": self.sslPort }
"sslport": self.sslPort,
"websocketproxyport": self.webSocketProxyPort }
pacURL = "".join(pacURL.splitlines())
part += """
@ -439,6 +456,8 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
sslTunnelConfig.write("httpproxy:1\n")
sslTunnelConfig.write("certdbdir:%s\n" % certPath)
sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
sslTunnelConfig.write("proxy:%s:%s:%s\n" %
(self.webSocketProxyPort, self.webServer, self.webSocketPort))
sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
# Configure automatic certificate and bind custom certificates, client authentication

View File

@ -394,6 +394,8 @@ _TEST_FILES2 = \
test_html_colors_quirks.html \
test_html_colors_standards.html \
test_bug571390.xul \
test_websocket_hello.html \
file_websocket_hello_wsh.py \
$(NULL)
# This test fails on the Mac for some reason

View File

@ -82,9 +82,37 @@ _SERV_FILES = \
mozprefs.js \
$(NULL)
_PYWEBSOCKET_FILES = \
pywebsocket/standalone.py \
$(NULL)
_MOD_PYWEBSOCKET_FILES = \
pywebsocket/mod_pywebsocket/__init__.py \
pywebsocket/mod_pywebsocket/dispatch.py \
pywebsocket/mod_pywebsocket/util.py \
pywebsocket/mod_pywebsocket/msgutil.py \
pywebsocket/mod_pywebsocket/memorizingfile.py \
pywebsocket/mod_pywebsocket/headerparserhandler.py \
$(NULL)
_HANDSHAKE_FILES = \
pywebsocket/mod_pywebsocket/handshake/__init__.py \
pywebsocket/mod_pywebsocket/handshake/_base.py \
pywebsocket/mod_pywebsocket/handshake/draft75.py \
pywebsocket/mod_pywebsocket/handshake/handshake.py \
$(NULL)
_DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir)
libs:: $(_PYWEBSOCKET_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket
libs:: $(_MOD_PYWEBSOCKET_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket/mod_pywebsocket
libs:: $(_HANDSHAKE_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket/mod_pywebsocket/handshake
runtests.py: runtests.py.in
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
$(DEFINES) $(ACDEFINES) $^ > $@

View File

@ -0,0 +1,28 @@
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.

View File

@ -0,0 +1 @@
This is mod_pywebsocket 0.5, from http://code.google.com/p/pywebsocket/

View File

@ -0,0 +1,111 @@
# 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
To limit the search for Web Socket handlers to a directory <scan_dir>
under <websock_handlers>, configure as follows:
PythonOption mod_pywebsocket.handler_scan <scan_dir>
<scan_dir> is useful in saving scan time when <websock_handlers>
contains many non-Web Socket handler files.
If you want to support old handshake based on
draft-hixie-thewebsocketprotocol-75:
PythonOption mod_pywebsocket.allow_draft75 On
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.
A Web Socket handler must be thread-safe if the server (Apache or
standalone.py) is configured to use threads.
"""
# vi:sts=4 sw=4 et tw=72

View File

@ -0,0 +1,245 @@
# 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
from mod_pywebsocket import msgutil
from mod_pywebsocket 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.realpath(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, scan_dir=None):
"""Construct an instance.
Args:
root_dir: The directory where handler definition files are
placed.
scan_dir: The directory where handler definition files are
searched. scan_dir must be a directory under root_dir,
including root_dir itself. If scan_dir is None, root_dir
is used as scan_dir. scan_dir can be useful in saving
scan time when root_dir contains many subdirectories.
"""
self._handlers = {}
self._source_warnings = []
if scan_dir is None:
scan_dir = root_dir
if not os.path.realpath(scan_dir).startswith(
os.path.realpath(root_dir)):
raise DispatchError('scan_dir:%s must be a directory under '
'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."""
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, e:
util.prepend_message_to_exception(
'%s raised exception for %s: ' % (
_DO_EXTRA_HANDSHAKE_HANDLER_NAME,
request.ws_resource),
e)
raise
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:
try:
request.client_terminated = False
request.server_terminated = False
transfer_data_(request)
except msgutil.ConnectionTerminatedException, e:
util.prepend_message_to_exception(
'client initiated closing handshake for %s: ' % (
request.ws_resource),
e)
raise
except Exception, e:
print 'exception: %s' % type(e)
util.prepend_message_to_exception(
'%s raised exception for %s: ' % (
_TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
e)
raise
finally:
msgutil.close_connection(request)
def _handler(self, request):
try:
ws_resource_path = request.ws_resource.split('?', 1)[0]
return self._handlers[ws_resource_path]
except KeyError:
raise DispatchError('No handler for: %r' % request.ws_resource)
def _source_files_in_dir(self, root_dir, scan_dir):
"""Source all the handler source files in the scan_dir directory.
The resource path is determined relative to root_dir.
"""
to_resource = _path_to_resource_converter(root_dir)
for path in _source_file_paths(scan_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

View File

@ -0,0 +1,95 @@
# Copyright 2010, 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 handshaking.
Note: request.connection.write/read are used in this module, even though
mod_python document says that they should be used only in connection handlers.
Unfortunately, we have no other options. For example, request.write/read are
not suitable because they don't allow direct raw bytes writing/reading.
"""
import logging
import re
from mod_pywebsocket.handshake import draft75
from mod_pywebsocket.handshake import handshake
from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_PORT
from mod_pywebsocket.handshake._base import DEFAULT_WEB_SOCKET_SECURE_PORT
from mod_pywebsocket.handshake._base import WEB_SOCKET_SCHEME
from mod_pywebsocket.handshake._base import WEB_SOCKET_SECURE_SCHEME
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import validate_protocol
class Handshaker(object):
"""This class performs Web Socket handshake."""
def __init__(self, request, dispatcher, allowDraft75=False, strict=False):
"""Construct an instance.
Args:
request: mod_python request.
dispatcher: Dispatcher (dispatch.Dispatcher).
allowDraft75: allow draft 75 handshake protocol.
strict: Strictly check handshake request in draft 75.
Default: False. If True, request.connection must provide
get_memorized_lines method.
Handshaker will add attributes such as ws_resource in performing
handshake.
"""
self._logger = logging.getLogger("mod_pywebsocket.handshake")
self._request = request
self._dispatcher = dispatcher
self._strict = strict
self._handshaker = handshake.Handshaker(request, dispatcher)
self._fallbackHandshaker = None
if allowDraft75:
self._fallbackHandshaker = draft75.Handshaker(
request, dispatcher, strict)
def do_handshake(self):
"""Perform Web Socket Handshake."""
try:
self._handshaker.do_handshake()
except HandshakeError, e:
self._logger.error('Handshake error: %s' % e)
if self._fallbackHandshaker:
self._logger.warning('fallback to old protocol')
self._fallbackHandshaker.do_handshake()
return
raise e
# vi:sts=4 sw=4 et

View File

@ -0,0 +1,101 @@
# Copyright 2010, 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 handshaking.
Note: request.connection.write/read are used in this module, even though
mod_python document says that they should be used only in connection handlers.
Unfortunately, we have no other options. For example, request.write/read are
not suitable because they don't allow direct raw bytes writing/reading.
"""
DEFAULT_WEB_SOCKET_PORT = 80
DEFAULT_WEB_SOCKET_SECURE_PORT = 443
WEB_SOCKET_SCHEME = 'ws'
WEB_SOCKET_SECURE_SCHEME = 'wss'
class HandshakeError(Exception):
"""Exception in Web Socket Handshake."""
pass
def default_port(is_secure):
if is_secure:
return DEFAULT_WEB_SOCKET_SECURE_PORT
else:
return DEFAULT_WEB_SOCKET_PORT
def validate_protocol(protocol):
"""Validate WebSocket-Protocol string."""
if not protocol:
raise HandshakeError('Invalid WebSocket-Protocol: empty')
for c in protocol:
if not 0x20 <= ord(c) <= 0x7e:
raise HandshakeError('Illegal character in protocol: %r' % c)
def parse_host_header(request):
fields = request.headers_in['Host'].split(':', 1)
if len(fields) == 1:
return fields[0], default_port(request.is_https())
try:
return fields[0], int(fields[1])
except ValueError, e:
raise HandshakeError('Invalid port number format: %r' % e)
def build_location(request):
"""Build WebSocket location for request."""
location_parts = []
if request.is_https():
location_parts.append(WEB_SOCKET_SECURE_SCHEME)
else:
location_parts.append(WEB_SOCKET_SCHEME)
location_parts.append('://')
host, port = parse_host_header(request)
connection_port = request.connection.local_addr[1]
if port != connection_port:
raise HandshakeError('Header/connection port mismatch: %d/%d' %
(port, connection_port))
location_parts.append(host)
if (port != default_port(request.is_https())):
location_parts.append(':')
location_parts.append(str(port))
location_parts.append(request.uri)
return ''.join(location_parts)
# vi:sts=4 sw=4 et

View File

@ -0,0 +1,168 @@
# Copyright 2010, 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 handshaking defined in draft-hixie-thewebsocketprotocol-75.
Note: request.connection.write/read are used in this module, even though
mod_python document says that they should be used only in connection handlers.
Unfortunately, we have no other options. For example, request.write/read are
not suitable because they don't allow direct raw bytes writing/reading.
"""
import re
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import build_location
from mod_pywebsocket.handshake._base import validate_protocol
_MANDATORY_HEADERS = [
# key, expected value or None
['Upgrade', 'WebSocket'],
['Connection', 'Upgrade'],
['Host', None],
['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')
class Handshaker(object):
"""This class performs Web Socket handshake."""
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.
"""
self._request = request
self._dispatcher = dispatcher
self._strict = strict
def do_handshake(self):
"""Perform Web Socket Handshake."""
self._check_header_lines()
self._set_resource()
self._set_origin()
self._set_location()
self._set_protocol()
self._dispatcher.do_extra_handshake(self._request)
self._send_handshake()
def _set_resource(self):
self._request.ws_resource = self._request.uri
def _set_origin(self):
self._request.ws_origin = self._request.headers_in['Origin']
def _set_location(self):
self._request.ws_location = build_location(self._request)
def _set_protocol(self):
protocol = self._request.headers_in.get('WebSocket-Protocol')
if protocol is not None:
validate_protocol(protocol)
self._request.ws_protocol = protocol
def _send_handshake(self):
self._request.connection.write(
'HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
self._request.connection.write('Upgrade: WebSocket\r\n')
self._request.connection.write('Connection: Upgrade\r\n')
self._request.connection.write('WebSocket-Origin: ')
self._request.connection.write(self._request.ws_origin)
self._request.connection.write('\r\n')
self._request.connection.write('WebSocket-Location: ')
self._request.connection.write(self._request.ws_location)
self._request.connection.write('\r\n')
if self._request.ws_protocol:
self._request.connection.write('WebSocket-Protocol: ')
self._request.connection.write(self._request.ws_protocol)
self._request.connection.write('\r\n')
self._request.connection.write('\r\n')
def _check_header_lines(self):
for key, expected_value in _MANDATORY_HEADERS:
actual_value = self._request.headers_in.get(key)
if not actual_value:
raise HandshakeError('Header %s is not defined' % key)
if expected_value:
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:
raise AttributeError(
'Strict handshake is specified but the connection '
'doesn\'t provide get_memorized_lines()')
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

View File

@ -0,0 +1,208 @@
# 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 handshaking.
Note: request.connection.write/read are used in this module, even though
mod_python document says that they should be used only in connection handlers.
Unfortunately, we have no other options. For example, request.write/read are
not suitable because they don't allow direct raw bytes writing/reading.
"""
import logging
from md5 import md5
import re
import struct
from mod_pywebsocket.handshake._base import HandshakeError
from mod_pywebsocket.handshake._base import build_location
from mod_pywebsocket.handshake._base import validate_protocol
_MANDATORY_HEADERS = [
# key, expected value or None
['Upgrade', 'WebSocket'],
['Connection', 'Upgrade'],
]
def _hexify(s):
return re.sub('.', lambda x: '%02x ' % ord(x.group(0)), s)
class Handshaker(object):
"""This class performs Web Socket handshake."""
def __init__(self, request, dispatcher):
"""Construct an instance.
Args:
request: mod_python request.
dispatcher: Dispatcher (dispatch.Dispatcher).
Handshaker will add attributes such as ws_resource in performing
handshake.
"""
self._logger = logging.getLogger("mod_pywebsocket.handshake")
self._request = request
self._dispatcher = dispatcher
def do_handshake(self):
"""Perform Web Socket Handshake."""
# 5.1 Reading the client's opening handshake.
# dispatcher sets it in self._request.
self._check_header_lines()
self._set_resource()
self._set_protocol()
self._set_location()
self._set_origin()
self._set_challenge_response()
self._dispatcher.do_extra_handshake(self._request)
self._send_handshake()
def _check_header_lines(self):
# 5.1 1. The three character UTF-8 string "GET".
# 5.1 2. A UTF-8-encoded U+0020 SPACE character (0x20 byte).
if self._request.method != 'GET':
raise HandshakeError('Method is not GET')
# The expected field names, and the meaning of their corresponding
# values, are as follows.
# |Upgrade| and |Connection|
for key, expected_value in _MANDATORY_HEADERS:
actual_value = self._request.headers_in.get(key)
if not actual_value:
raise HandshakeError('Header %s is not defined' % key)
if expected_value:
if actual_value != expected_value:
raise HandshakeError('Illegal value for header %s: %s' %
(key, actual_value))
def _set_resource(self):
self._request.ws_resource = self._request.uri
def _set_protocol(self):
# |Sec-WebSocket-Protocol|
protocol = self._request.headers_in.get('Sec-WebSocket-Protocol')
if protocol is not None:
validate_protocol(protocol)
self._request.ws_protocol = protocol
def _set_location(self):
# |Host|
host = self._request.headers_in.get('Host')
if host is not None:
self._request.ws_location = build_location(self._request)
# TODO(ukai): check host is this host.
def _set_origin(self):
# |Origin|
origin = self._request.headers_in['Origin']
if origin is not None:
self._request.ws_origin = origin
def _set_challenge_response(self):
# 5.2 4-8.
self._request.ws_challenge = self._get_challenge()
# 5.2 9. let /response/ be the MD5 finterprint of /challenge/
self._request.ws_challenge_md5 = md5(
self._request.ws_challenge).digest()
self._logger.debug("challenge: %s" % _hexify(
self._request.ws_challenge))
self._logger.debug("response: %s" % _hexify(
self._request.ws_challenge_md5))
def _get_key_value(self, key_field):
key_value = self._request.headers_in.get(key_field)
if key_value is None:
self._logger.debug("no %s" % key_value)
return None
try:
# 5.2 4. let /key-number_n/ be the digits (characters in the range
# U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_n/,
# interpreted as a base ten integer, ignoring all other characters
# in /key_n/
key_number = int(re.sub("\\D", "", key_value))
# 5.2 5. let /spaces_n/ be the number of U+0020 SPACE characters
# in /key_n/.
spaces = re.subn(" ", "", key_value)[1]
# 5.2 6. if /key-number_n/ is not an integral multiple of /spaces_n/
# then abort the WebSocket connection.
if key_number % spaces != 0:
raise handshakeError('key_number %d is not an integral '
'multiple of spaces %d' % (key_number,
spaces))
# 5.2 7. let /part_n/ be /key_number_n/ divided by /spaces_n/.
part = key_number / spaces
self._logger.debug("%s: %s => %d / %d => %d" % (
key_field, key_value, key_number, spaces, part))
return part
except:
return None
def _get_challenge(self):
# 5.2 4-7.
key1 = self._get_key_value('Sec-Websocket-Key1')
if not key1:
raise HandshakeError('Sec-WebSocket-Key1 not found')
key2 = self._get_key_value('Sec-Websocket-Key2')
if not key2:
raise HandshakeError('Sec-WebSocket-Key2 not found')
# 5.2 8. let /challenge/ be the concatenation of /part_1/,
challenge = ""
challenge += struct.pack("!I", key1) # network byteorder int
challenge += struct.pack("!I", key2) # network byteorder int
challenge += self._request.connection.read(8)
return challenge
def _send_handshake(self):
# 5.2 10. send the following line.
self._request.connection.write(
'HTTP/1.1 101 WebSocket Protocol Handshake\r\n')
# 5.2 11. send the following fields to the client.
self._request.connection.write('Upgrade: WebSocket\r\n')
self._request.connection.write('Connection: Upgrade\r\n')
self._request.connection.write('Sec-WebSocket-Location: ')
self._request.connection.write(self._request.ws_location)
self._request.connection.write('\r\n')
self._request.connection.write('Sec-WebSocket-Origin: ')
self._request.connection.write(self._request.ws_origin)
self._request.connection.write('\r\n')
if self._request.ws_protocol:
self._request.connection.write('Sec-WebSocket-Protocol: ')
self._request.connection.write(self._request.ws_protocol)
self._request.connection.write('\r\n')
# 5.2 12. send two bytes 0x0D 0x0A.
self._request.connection.write('\r\n')
# 5.2 13. send /response/
self._request.connection.write(self._request.ws_challenge_md5)
# vi:sts=4 sw=4 et

View File

@ -0,0 +1,138 @@
# 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.
"""PythonHeaderParserHandler for mod_pywebsocket.
Apache HTTP Server and mod_python must be configured such that this
function is called to handle Web Socket request.
"""
import logging
from mod_python import apache
from mod_pywebsocket import dispatch
from mod_pywebsocket import handshake
from mod_pywebsocket import util
# PythonOption to specify the handler root directory.
_PYOPT_HANDLER_ROOT = 'mod_pywebsocket.handler_root'
# PythonOption to specify the handler scan directory.
# This must be a directory under the root directory.
# The default is the root directory.
_PYOPT_HANDLER_SCAN = 'mod_pywebsocket.handler_scan'
# PythonOption to specify to allow draft75 handshake.
# The default is None (Off)
_PYOPT_ALLOW_DRAFT75 = 'mod_pywebsocket.allow_draft75'
class ApacheLogHandler(logging.Handler):
"""Wrapper logging.Handler to emit log message to apache's error.log"""
_LEVELS = {
logging.DEBUG: apache.APLOG_DEBUG,
logging.INFO: apache.APLOG_INFO,
logging.WARNING: apache.APLOG_WARNING,
logging.ERROR: apache.APLOG_ERR,
logging.CRITICAL: apache.APLOG_CRIT,
}
def __init__(self, request=None):
logging.Handler.__init__(self)
self.log_error = apache.log_error
if request is not None:
self.log_error = request.log_error
def emit(self, record):
apache_level = apache.APLOG_DEBUG
if record.levelno in ApacheLogHandler._LEVELS:
apache_level = ApacheLogHandler._LEVELS[record.levelno]
self.log_error(record.getMessage(), apache_level)
logging.getLogger("mod_pywebsocket").addHandler(ApacheLogHandler())
def _create_dispatcher():
_HANDLER_ROOT = apache.main_server.get_options().get(
_PYOPT_HANDLER_ROOT, None)
if not _HANDLER_ROOT:
raise Exception('PythonOption %s is not defined' % _PYOPT_HANDLER_ROOT,
apache.APLOG_ERR)
_HANDLER_SCAN = apache.main_server.get_options().get(
_PYOPT_HANDLER_SCAN, _HANDLER_ROOT)
dispatcher = dispatch.Dispatcher(_HANDLER_ROOT, _HANDLER_SCAN)
for warning in dispatcher.source_warnings():
apache.log_error('mod_pywebsocket: %s' % warning, apache.APLOG_WARNING)
return dispatcher
# Initialize
_dispatcher = _create_dispatcher()
def headerparserhandler(request):
"""Handle request.
Args:
request: mod_python request.
This function is named headerparserhandler because it is the default name
for a PythonHeaderParserHandler.
"""
try:
allowDraft75 = apache.main_server.get_options().get(
_PYOPT_ALLOW_DRAFT75, None)
handshaker = handshake.Handshaker(request, _dispatcher,
allowDraft75=allowDraft75)
handshaker.do_handshake()
request.log_error('mod_pywebsocket: resource: %r' % request.ws_resource,
apache.APLOG_DEBUG)
try:
_dispatcher.transfer_data(request)
except Exception, e:
# Catch exception in transfer_data.
# In this case, handshake has been successful, so just log the
# exception and return apache.DONE
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
except handshake.HandshakeError, e:
# Handshake for ws/wss failed.
# But the request can be valid http/https request.
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_INFO)
return apache.DECLINED
except dispatch.DispatchError, e:
request.log_error('mod_pywebsocket: %s' % e, apache.APLOG_WARNING)
return apache.DECLINED
return apache.DONE # Return DONE such that no other handlers are invoked.
# vi:sts=4 sw=4 et

View File

@ -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

View File

@ -0,0 +1,290 @@
# 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.
"""Message related utilities.
Note: request.connection.write/read are used in this module, even though
mod_python document says that they should be used only in connection handlers.
Unfortunately, we have no other options. For example, request.write/read are
not suitable because they don't allow direct raw bytes writing/reading.
"""
import Queue
import threading
from mod_pywebsocket import util
class MsgUtilException(Exception):
pass
class ConnectionTerminatedException(MsgUtilException):
pass
def _read(request, length):
bytes = request.connection.read(length)
if not bytes:
raise MsgUtilException(
'Failed to receive message from %r' %
(request.connection.remote_addr,))
return bytes
def _write(request, bytes):
try:
request.connection.write(bytes)
except Exception, e:
util.prepend_message_to_exception(
'Failed to send message to %r: ' %
(request.connection.remote_addr,),
e)
raise
def close_connection(request):
"""Close connection.
Args:
request: mod_python request.
"""
if request.server_terminated:
return
# 5.3 the server may decide to terminate the WebSocket connection by
# running through the following steps:
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the start
# of the closing handshake.
_write(request, '\xff\x00')
request.server_terminated = True
# TODO(ukai): 2. wait until the /client terminated/ flag has been set, or
# until a server-defined timeout expires.
# TODO: 3. close the WebSocket connection.
# note: mod_python Connection (mp_conn) doesn't have close method.
def send_message(request, message):
"""Send message.
Args:
request: mod_python request.
message: unicode string to send.
Raises:
ConnectionTerminatedException: when server already terminated.
"""
if request.server_terminated:
raise ConnectionTerminatedException
_write(request, '\x00' + message.encode('utf-8') + '\xff')
def receive_message(request):
"""Receive a Web Socket frame and return its payload as unicode string.
Args:
request: mod_python request.
Raises:
ConnectionTerminatedException: when client already terminated.
"""
if request.client_terminated:
raise ConnectionTerminatedException
while True:
# Read 1 byte.
# mp_conn.read will block if no bytes are available.
# Timeout is controlled by TimeOut directive of Apache.
frame_type_str = _read(request, 1)
frame_type = ord(frame_type_str[0])
if (frame_type & 0x80) == 0x80:
# The payload length is specified in the frame.
# Read and discard.
length = _payload_length(request)
_receive_bytes(request, length)
# 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
# /client terminated/ flag and abort these steps.
if frame_type == 0xFF and length == 0:
request.client_terminated = True
raise ConnectionTerminatedException
else:
# The payload is delimited with \xff.
bytes = _read_until(request, '\xff')
# The Web Socket protocol section 4.4 specifies that invalid
# characters must be replaced with U+fffd REPLACEMENT CHARACTER.
message = bytes.decode('utf-8', 'replace')
if frame_type == 0x00:
return message
# Discard data of other types.
def _payload_length(request):
length = 0
while True:
b_str = _read(request, 1)
b = ord(b_str[0])
length = length * 128 + (b & 0x7f)
if (b & 0x80) == 0:
break
return length
def _receive_bytes(request, length):
bytes = []
while length > 0:
new_bytes = _read(request, length)
bytes.append(new_bytes)
length -= len(new_bytes)
return ''.join(bytes)
def _read_until(request, delim_char):
bytes = []
while True:
ch = _read(request, 1)
if ch == delim_char:
break
bytes.append(ch)
return ''.join(bytes)
class MessageReceiver(threading.Thread):
"""This class receives messages from the client.
This class provides three ways to receive messages: blocking, non-blocking,
and via callback. Callback has the highest precedence.
Note: This class should not be used with the standalone server for wss
because pyOpenSSL used by the server raises a fatal error if the socket
is accessed from multiple threads.
"""
def __init__(self, request, onmessage=None):
"""Construct an instance.
Args:
request: mod_python request.
onmessage: a function to be called when a message is received.
May be None. If not None, the function is called on
another thread. In that case, MessageReceiver.receive
and MessageReceiver.receive_nowait are useless because
they will never return any messages.
"""
threading.Thread.__init__(self)
self._request = request
self._queue = Queue.Queue()
self._onmessage = onmessage
self._stop_requested = False
self.setDaemon(True)
self.start()
def run(self):
try:
while not self._stop_requested:
message = receive_message(self._request)
if self._onmessage:
self._onmessage(message)
else:
self._queue.put(message)
finally:
close_connection(self._request)
def receive(self):
""" Receive a message from the channel, blocking.
Returns:
message as a unicode string.
"""
return self._queue.get()
def receive_nowait(self):
""" Receive a message from the channel, non-blocking.
Returns:
message as a unicode string if available. None otherwise.
"""
try:
message = self._queue.get_nowait()
except Queue.Empty:
message = None
return message
def stop(self):
"""Request to stop this instance.
The instance will be stopped after receiving the next message.
This method may not be very useful, but there is no clean way
in Python to forcefully stop a running thread.
"""
self._stop_requested = True
class MessageSender(threading.Thread):
"""This class sends messages to the client.
This class provides both synchronous and asynchronous ways to send
messages.
Note: This class should not be used with the standalone server for wss
because pyOpenSSL used by the server raises a fatal error if the socket
is accessed from multiple threads.
"""
def __init__(self, request):
"""Construct an instance.
Args:
request: mod_python request.
"""
threading.Thread.__init__(self)
self._request = request
self._queue = Queue.Queue()
self.setDaemon(True)
self.start()
def run(self):
while True:
message, condition = self._queue.get()
condition.acquire()
send_message(self._request, message)
condition.notify()
condition.release()
def send(self, message):
"""Send a message, blocking."""
condition = threading.Condition()
condition.acquire()
self._queue.put((message, condition))
condition.wait()
def send_nowait(self, message):
"""Send a message, non-blocking."""
self._queue.put((message, threading.Condition()))
# vi:sts=4 sw=4 et

View File

@ -0,0 +1,121 @@
# 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 Sockets utilities.
"""
import StringIO
import os
import re
import traceback
def get_stack_trace():
"""Get the current stack trace as string.
This is needed to support Python 2.3.
TODO: Remove this when we only support Python 2.4 and above.
Use traceback.format_exc instead.
"""
out = StringIO.StringIO()
traceback.print_exc(file=out)
return out.getvalue()
def prepend_message_to_exception(message, exc):
"""Prepend message to the exception."""
exc.args = (message + str(exc),)
return
def __translate_interp(interp, cygwin_path):
"""Translate interp program path for Win32 python to run cygwin program
(e.g. perl). Note that it doesn't support path that contains space,
which is typically true for Unix, where #!-script is written.
For Win32 python, cygwin_path is a directory of cygwin binaries.
Args:
interp: interp command line
cygwin_path: directory name of cygwin binary, or None
Returns:
translated interp command line.
"""
if not cygwin_path:
return interp
m = re.match("^[^ ]*/([^ ]+)( .*)?", interp)
if m:
cmd = os.path.join(cygwin_path, m.group(1))
return cmd + m.group(2)
return interp
def get_script_interp(script_path, cygwin_path=None):
"""Gets #!-interpreter command line from the script.
It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
it could run "/usr/bin/perl -wT hello.pl".
When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
"/usr/bin/perl" to "<cygwin_path>\perl.exe".
Args:
script_path: pathname of the script
cygwin_path: directory name of cygwin binary, or None
Returns:
#!-interpreter command line, or None if it is not #!-script.
"""
fp = open(script_path)
line = fp.readline()
fp.close()
m = re.match("^#!(.*)", line)
if m:
return __translate_interp(m.group(1), cygwin_path)
return None
def wrap_popen3_for_win(cygwin_path):
"""Wrap popen3 to support #!-script on Windows.
Args:
cygwin_path: path for cygwin binary if command path is needed to be
translated. None if no translation required.
"""
__orig_popen3 = os.popen3
def __wrap_popen3(cmd, mode='t', bufsize=-1):
cmdline = cmd.split(' ')
interp = get_script_interp(cmdline[0], cygwin_path)
if interp:
cmd = interp + " " + cmd
return __orig_popen3(cmd, mode, bufsize)
os.popen3 = __wrap_popen3
# vi:sts=4 sw=4 et

View File

@ -0,0 +1,472 @@
#!/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.
"""Standalone Web Socket server.
Use this server to run mod_pywebsocket without Apache HTTP Server.
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.
<document_root> is the path to the root directory of HTML files.
<websock_handlers> is the path to the root directory of Web Socket handlers.
See __init__.py for details of <websock_handlers> and how to write Web Socket
handlers. If this path is relative, <document_root> is used as the base.
<scan_dir> is a path under the root directory. If specified, only the handlers
under scan_dir are scanned. This is useful in saving scan time.
Note:
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
used for each request.
SECURITY WARNING: This uses CGIHTTPServer and CGIHTTPServer is not secure.
It may execute arbitrary Python code or external programs. It should not be
used outside a firewall.
"""
import BaseHTTPServer
import CGIHTTPServer
import SimpleHTTPServer
import SocketServer
import logging
import logging.handlers
import optparse
import os
import re
import socket
import sys
_HAS_OPEN_SSL = False
try:
import OpenSSL.SSL
_HAS_OPEN_SSL = True
except ImportError:
pass
from mod_pywebsocket import dispatch
from mod_pywebsocket import handshake
from mod_pywebsocket import memorizingfile
from mod_pywebsocket import util
_LOG_LEVELS = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warn': logging.WARN,
'error': logging.ERROR,
'critical': logging.CRITICAL};
_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()
if warnings:
for warning in warnings:
logging.warning('mod_pywebsocket: %s' % warning)
class _StandaloneConnection(object):
"""Mimic mod_python mp_conn."""
def __init__(self, request_handler):
"""Construct an instance.
Args:
request_handler: A WebSocketRequestHandler instance.
"""
self._request_handler = request_handler
def get_local_addr(self):
"""Getter to mimic mp_conn.local_addr."""
return (self._request_handler.server.server_name,
self._request_handler.server.server_port)
local_addr = property(get_local_addr)
def get_remote_addr(self):
"""Getter to mimic mp_conn.remote_addr.
Setting the property in __init__ won't work because the request
handler is not initialized yet there."""
return self._request_handler.client_address
remote_addr = property(get_remote_addr)
def write(self, data):
"""Mimic mp_conn.write()."""
return self._request_handler.wfile.write(data)
def read(self, length):
"""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."""
def __init__(self, request_handler, use_tls):
"""Construct an instance.
Args:
request_handler: A WebSocketRequestHandler instance.
"""
self._request_handler = request_handler
self.connection = _StandaloneConnection(request_handler)
self._use_tls = use_tls
def get_uri(self):
"""Getter to mimic request.uri."""
return self._request_handler.path
uri = property(get_uri)
def get_method(self):
"""Getter to mimic request.method."""
return self._request_handler.command
method = property(get_method)
def get_headers_in(self):
"""Getter to mimic request.headers_in."""
return self._request_handler.headers
headers_in = property(get_headers_in)
def is_https(self):
"""Mimic request.is_https()."""
return self._use_tls
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
"""HTTPServer specialized for Web Socket."""
SocketServer.ThreadingMixIn.daemon_threads = True
def __init__(self, server_address, RequestHandlerClass):
"""Override SocketServer.BaseServer.__init__."""
SocketServer.BaseServer.__init__(
self, server_address, RequestHandlerClass)
self.socket = self._create_socket()
self.server_bind()
self.server_activate()
def _create_socket(self):
socket_ = socket.socket(self.address_family, self.socket_type)
if WebSocketServer.options.use_tls:
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
ctx.use_privatekey_file(WebSocketServer.options.private_key)
ctx.use_certificate_file(WebSocketServer.options.certificate)
socket_ = OpenSSL.SSL.Connection(ctx, socket_)
return socket_
def handle_error(self, rquest, client_address):
"""Override SocketServer.handle_error."""
logging.error(
('Exception in processing request from: %r' % (client_address,)) +
'\n' + util.get_stack_trace())
# Note: client_address is a tuple. To match it against %r, we need the
# trailing comma.
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
"""CGIHTTPRequestHandler specialized for Web Socket."""
def setup(self):
"""Override SocketServer.StreamRequestHandler.setup."""
self.connection = self.request
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):
self._request = _StandaloneRequest(
self, WebSocketRequestHandler.options.use_tls)
self._dispatcher = WebSocketRequestHandler.options.dispatcher
self._print_warnings_if_any()
self._handshaker = handshake.Handshaker(
self._request, self._dispatcher,
allowDraft75=WebSocketRequestHandler.options.allow_draft75,
strict=WebSocketRequestHandler.options.strict)
CGIHTTPServer.CGIHTTPRequestHandler.__init__(
self, *args, **keywords)
def _print_warnings_if_any(self):
warnings = self._dispatcher.source_warnings()
if warnings:
for warning in warnings:
logging.warning('mod_pywebsocket: %s' % warning)
def parse_request(self):
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
Return True to continue processing for HTTP(S), False otherwise.
"""
result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self)
if result:
try:
self._handshaker.do_handshake()
try:
self._dispatcher.transfer_data(self._request)
except Exception, e:
# Catch exception in transfer_data.
# In this case, handshake has been successful, so just log
# the exception and return False.
logging.info('mod_pywebsocket: %s' % e)
return False
except handshake.HandshakeError, e:
# Handshake for ws(s) failed. Assume http(s).
logging.info('mod_pywebsocket: %s' % e)
return True
except dispatch.DispatchError, e:
logging.warning('mod_pywebsocket: %s' % e)
return False
except Exception, e:
logging.warning('mod_pywebsocket: %s' % e)
logging.info('mod_pywebsocket: %s' % util.get_stack_trace())
return False
return result
def log_request(self, code='-', size='-'):
"""Override BaseHTTPServer.log_request."""
logging.info('"%s" %s %s',
self.requestline, str(code), str(size))
def log_error(self, *args):
"""Override BaseHTTPServer.log_error."""
# Despite the name, this method is for warnings than for errors.
# For example, HTTP status code is logged by this method.
logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
def is_cgi(self):
"""Test whether self.path corresponds to a CGI script.
Add extra check that self.path doesn't contains ..
Also check if the file is a executable file or not.
If the file is not executable, it is handled as static file or dir
rather than a CGI script.
"""
if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
if '..' in self.path:
return False
# strip query parameter from request path
resource_name = self.path.split('?', 2)[0]
# convert resource_name into real path name in filesystem.
scriptfile = self.translate_path(resource_name)
if not os.path.isfile(scriptfile):
return False
if not self.is_executable(scriptfile):
return False
return True
return False
def _configure_logging(options):
logger = logging.getLogger()
logger.setLevel(_LOG_LEVELS[options.log_level])
if options.log_file:
handler = logging.handlers.RotatingFileHandler(
options.log_file, 'a', options.log_max, options.log_count)
else:
handler = logging.StreamHandler()
formatter = logging.Formatter(
"[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
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()
parser.add_option('-H', '--server-host', '--server_host',
dest='server_host',
default='',
help='server hostname to listen to')
parser.add_option('-p', '--port', dest='port', type='int',
default=handshake.DEFAULT_WEB_SOCKET_PORT,
help='port to listen to')
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
dest='websock_handlers',
default='.',
help='Web Socket handlers root directory.')
parser.add_option('-m', '--websock-handlers-map-file',
'--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', '--scan_dir', dest='scan_dir',
default=None,
help=('Web Socket handlers scan directory. '
'Must be a directory under websock_handlers.'))
parser.add_option('-d', '--document-root', '--document_root',
dest='document_root', default='.',
help='Document root directory.')
parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
default=None,
help=('CGI paths relative to document_root.'
'Comma-separated. (e.g -x /cgi,/htbin) '
'Files under document_root/cgi_path are handled '
'as CGI programs. Must be executable.'))
parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
default=False, help='use TLS (wss://)')
parser.add_option('-k', '--private-key', '--private_key',
dest='private_key',
default='', help='TLS private key file.')
parser.add_option('-c', '--certificate', dest='certificate',
default='', help='TLS certificate file.')
parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
default='', help='Log file.')
parser.add_option('--log-level', '--log_level', type='choice',
dest='log_level', default='warn',
choices=['debug', 'info', 'warn', 'error', 'critical'],
help='Log level.')
parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
default=_DEFAULT_LOG_MAX_BYTES,
help='Log maximum bytes')
parser.add_option('--log-count', '--log_count', dest='log_count',
type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
help='Log backup count')
parser.add_option('--allow-draft75', dest='allow_draft75',
action='store_true', default=False,
help='Allow draft 75 handshake')
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
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
if options.cgi_paths:
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
options.cgi_paths.split(',')
if sys.platform in ('cygwin', 'win32'):
cygwin_path = None
# For Win32 Python, it is expected that CYGWIN_PATH
# is set to a directory of cygwin binaries.
# For example, websocket_server.py in Chromium sets CYGWIN_PATH to
# full path of third_party/cygwin/bin.
if 'CYGWIN_PATH' in os.environ:
cygwin_path = os.environ['CYGWIN_PATH']
util.wrap_popen3_for_win(cygwin_path)
def __check_script(scriptpath):
return util.get_script_interp(scriptpath, cygwin_path)
CGIHTTPServer.executable = __check_script
if options.use_tls:
if not _HAS_OPEN_SSL:
logging.critical('To use TLS, install pyOpenSSL.')
sys.exit(1)
if not options.private_key or not options.certificate:
logging.critical(
'To use TLS, specify private_key and certificate.')
sys.exit(1)
if not options.scan_dir:
options.scan_dir = options.websock_handlers
try:
# Share a Dispatcher among request handlers to save time for
# 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
WebSocketServer.options = options
server = WebSocketServer((options.server_host, options.port),
WebSocketRequestHandler)
server.serve_forever()
except Exception, e:
logging.critical(str(e))
sys.exit(1)
if __name__ == '__main__':
_main()
# vi:sts=4 sw=4 et

View File

@ -265,6 +265,8 @@ See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logg
options.webServer = self._automation.DEFAULT_WEB_SERVER
options.httpPort = self._automation.DEFAULT_HTTP_PORT
options.sslPort = self._automation.DEFAULT_SSL_PORT
options.webSocketPort = self._automation.DEFAULT_WEBSOCKET_PORT
options.webSocketProxyPort = self._automation.DEFAULT_WEBSOCKET_PROXY_PORT
if options.vmwareRecording:
if not self._automation.IS_WIN32:
@ -343,6 +345,27 @@ class MochitestServer:
except:
self._process.kill()
class WebSocketServer(object):
"Class which encapsulates the mod_pywebsocket server"
def __init__(self, automation, options, scriptdir):
self.port = options.webSocketPort
self._automation = automation
self._scriptdir = scriptdir
def start(self):
script = os.path.join(self._scriptdir, 'pywebsocket/standalone.py')
cmd = [sys.executable, script, '-p', str(self.port), '-w', self._scriptdir, '-l', os.path.join(self._scriptdir, "websock.log"), '--log-level=debug']
self._process = self._automation.Process(cmd)
pid = self._process.pid
if pid < 0:
print "Error starting websocket server."
sys.exit(2)
self._automation.log.info("INFO | runtests.py | Websocket server pid: %d", pid)
def stop(self):
self._process.kill()
class Mochitest(object):
# Path to the test script on the server
@ -389,6 +412,20 @@ class Mochitest(object):
testURL = "about:blank"
return testURL
def startWebSocketServer(self, options):
""" Launch the websocket server """
if options.webServer != '127.0.0.1':
return
self.wsserver = WebSocketServer(self.automation, options, self.SCRIPT_DIRECTORY)
self.wsserver.start()
def stopWebSocketServer(self, options):
if options.webServer != '127.0.0.1':
return
self.wsserver.stop()
def startWebServer(self, options):
if options.webServer != '127.0.0.1':
return
@ -547,7 +584,7 @@ class Mochitest(object):
manifest = self.buildProfile(options)
self.startWebServer(options)
self.startWebSocketServer(options)
testURL = self.buildTestPath(options)
self.buildURLOptions(options)
@ -587,6 +624,7 @@ class Mochitest(object):
self.stopVMwareRecording();
self.stopWebServer(options)
self.stopWebSocketServer(options)
processLeakLog(self.leak_report_file, options.leakThreshold)
self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
@ -690,7 +728,11 @@ def main():
if options.symbolsPath and not isURL(options.symbolsPath):
options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
automation.setServerInfo(options.webServer, options.httpPort, options.sslPort)
automation.setServerInfo(options.webServer,
options.httpPort,
options.sslPort,
options.webSocketPort,
options.webSocketProxyPort)
sys.exit(mochitest.runTests(options))
if __name__ == "__main__":

View File

@ -121,6 +121,14 @@ typedef struct {
string cert_nickname;
PLHashTable* host_cert_table;
PLHashTable* host_clientauth_table;
// If not empty, and this server is using HTTP CONNECT, connections
// will be proxied to this address.
PRNetAddr remote_addr;
// True if no SSL should be used for this server's connections.
bool http_proxy_only;
// The original host in the Host: header for the initial connection is
// stored here, for proxied connections.
string original_host;
} server_info_t;
typedef struct {
@ -132,6 +140,7 @@ typedef struct {
const PRInt32 BUF_SIZE = 16384;
const PRInt32 BUF_MARGIN = 1024;
const PRInt32 BUF_TOTAL = BUF_SIZE + BUF_MARGIN;
const char HEADER_HOST[] = "Host:";
struct relayBuffer
{
@ -341,6 +350,100 @@ bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, string &cer
return true;
}
/**
* This function examines the buffer for a S5ec-WebSocket-Location: field,
* and if it's present, it replaces the hostname in that field with the
* value in the server's original_host field. This function works
* in the reverse direction as AdjustHost(), replacing the real hostname
* of a response with the potentially fake hostname that is expected
* by the browser (e.g., mochi.test).
*
* @return true if the header was adjusted successfully, or not found, false
* if the header is present but the url is not, which should indicate
* that more data needs to be read from the socket
*/
bool AdjustWebSocketLocation(relayBuffer& buffer, server_info_t *si)
{
assert(buffer.margin());
buffer.buffertail[1] = '\0';
char* wsloc = strstr(buffer.bufferhead, "Sec-WebSocket-Location:");
if (!wsloc)
return true;
// advance pointer to the start of the hostname
wsloc = strstr(wsloc, "ws://");
if (!wsloc)
return false;
wsloc += 5;
// find the end of the hostname
char* wslocend = strchr(wsloc + 1, '/');
if (!wslocend)
return false;
char *crlf = strstr(wsloc, "\r\n");
if (!crlf)
return false;
if (si->original_host.empty())
return true;
int diff = si->original_host.length() - (wslocend-wsloc);
if (diff > 0)
assert(size_t(diff) <= buffer.margin());
memmove(wslocend + diff, wslocend, buffer.buffertail - wsloc - diff);
buffer.buffertail += diff;
memcpy(wsloc, si->original_host.c_str(), si->original_host.length());
return true;
}
/**
* This function examines the buffer for a Host: field, and if it's present,
* it replaces the hostname in that field with the hostname in the server's
* remote_addr field. This is needed because proxy requests may be coming
* from mochitest with fake hosts, like mochi.test, and these need to be
* replaced with the host that the destination server is actually running
* on.
*/
bool AdjustHost(relayBuffer& buffer, server_info_t *si)
{
if (!si->remote_addr.inet.port)
return false;
assert(buffer.margin());
// Cannot use strnchr so add a null char at the end. There is always some
// space left because we preserve a margin.
buffer.buffertail[1] = '\0';
char* host = strstr(buffer.bufferhead, HEADER_HOST);
if (!host)
return false;
// advance pointer to beginning of hostname
host += strlen(HEADER_HOST);
host += strspn(host, " \t");
char* endhost = strstr(host, "\r\n");
if (!endhost)
return false;
// Save the original host, so we can use it later on responses from the
// server.
si->original_host.assign(host, endhost-host);
char newhost[40];
PR_NetAddrToString(&si->remote_addr, newhost, sizeof(newhost));
assert(strlen(newhost) < sizeof(newhost) - 7);
sprintf(newhost, "%s:%d", newhost, PR_ntohs(si->remote_addr.inet.port));
int diff = strlen(newhost) - (endhost-host);
if (diff > 0)
assert(size_t(diff) <= buffer.margin());
memmove(endhost + diff, endhost, buffer.buffertail - host - diff);
buffer.buffertail += diff;
memcpy(host, newhost, strlen(newhost));
return true;
}
/**
* This function prefixes Request-URI path with a full scheme-host-port
* string.
@ -429,7 +532,8 @@ void HandleConnection(void* data)
if (!do_http_proxy)
{
if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone))
if (!ci->server_info->http_proxy_only &&
!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone))
client_error = true;
else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
client_error = true;
@ -566,7 +670,10 @@ void HandleConnection(void* data)
strcpy(buffers[s2].buffer, "HTTP/1.1 200 Connected\r\nConnection: keep-alive\r\n\r\n");
buffers[s2].buffertail = buffers[s2].buffer + strlen(buffers[s2].buffer);
if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
PRNetAddr* addr = &remote_addr;
if (ci->server_info->remote_addr.inet.port > 0)
addr = &ci->server_info->remote_addr;
if (!ConnectSocket(other_sock, addr, connect_timeout))
{
printf(" could not open connection to the real server\n");
client_error = true;
@ -588,7 +695,17 @@ void HandleConnection(void* data)
if (ssl_updated)
{
if (s == 0 && expect_request_start)
{
if (ci->server_info->http_proxy_only)
expect_request_start = !AdjustHost(buffers[s], ci->server_info);
else
expect_request_start = !AdjustRequestURI(buffers[s], &fullHost);
}
else
{
if (!AdjustWebSocketLocation(buffers[s], ci->server_info))
continue;
}
in_flags2 |= PR_POLL_WRITE;
printf(" telling the other socket to write");
@ -619,7 +736,7 @@ void HandleConnection(void* data)
}
else
{
printf(", writen %d bytes", bytesWrite);
printf(", written %d bytes", bytesWrite);
buffers[s2].buffertail[1] = '\0';
printf(" dump:\n%.*s\n", bytesWrite, buffers[s2].bufferhead);
@ -636,7 +753,8 @@ void HandleConnection(void* data)
printf(" proxy response sent to the client");
// Proxy response has just been writen, update to ssl
ssl_updated = true;
if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, clientAuth))
if (!ci->server_info->http_proxy_only &&
!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, clientAuth))
{
printf(" but failed to config server socket\n");
client_error = true;
@ -793,6 +911,35 @@ int processConfigLine(char* configLine)
return 0;
}
if (!strcmp(keyword, "proxy"))
{
server_info_t server;
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
server.host_clientauth_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, ClientAuthValueComparator, NULL, NULL);
server.http_proxy_only = true;
char* listenport = strtok2(_caret, ":", &_caret);
server.listen_port = atoi(listenport);
if (server.listen_port <= 0) {
fprintf(stderr, "Invalid listen port in proxy config: %s\n", listenport);
return 1;
}
char* ipstring = strtok2(_caret, ":", &_caret);
if (PR_StringToNetAddr(ipstring, &server.remote_addr) != PR_SUCCESS) {
fprintf(stderr, "Invalid IP address in proxy config: %s\n", ipstring);
return 1;
}
char* remoteport = strtok2(_caret, ":", &_caret);
int port = atoi(remoteport);
if (port <= 0) {
fprintf(stderr, "Invalid remote port in proxy config: %s\n", remoteport);
return 1;
}
server.remote_addr.inet.port = PR_htons(port);
servers.push_back(server);
return 0;
}
// Configure all listen sockets and port+certificate bindings
if (!strcmp(keyword, "listen"))
{
@ -832,8 +979,10 @@ int processConfigLine(char* configLine)
else
{
server_info_t server;
memset(&server.remote_addr, 0, sizeof(PRNetAddr));
server.cert_nickname = certnick;
server.listen_port = port;
server.http_proxy_only = false;
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
if (!server.host_cert_table)
{
@ -1013,7 +1162,11 @@ int main(int argc, char** argv)
" # in httpproxy mode and only after the 'listen' option has been\n"
" # specified. You also have to specify the tunnel listen port.\n"
" clientauth:requesting-client-cert.host.com:443:4443:request\n"
" clientauth:requiring-client-cert.host.com:443:4443:require\n",
" clientauth:requiring-client-cert.host.com:443:4443:require\n"
" # Act as a simple proxy for incoming connections on port 7777,\n"
" # tunneling them to the server at 127.0.0.1:9999. Not affected\n"
" # by the 'forward' option.\n"
" proxy:7777:127.0.0.1:9999\n",
configFilePath);
return 1;
}