зеркало из https://github.com/mozilla/gecko-dev.git
Bug 570789. Add WebSocket support to mochitest. r=ted
--HG-- extra : rebase_source : 2e33d054a13824d1a7e527777383820946684c3f
This commit is contained in:
Родитель
b51e1f6c6a
Коммит
e6fdd7ae87
|
@ -58,6 +58,8 @@ import automationutils
|
||||||
_DEFAULT_WEB_SERVER = "127.0.0.1"
|
_DEFAULT_WEB_SERVER = "127.0.0.1"
|
||||||
_DEFAULT_HTTP_PORT = 8888
|
_DEFAULT_HTTP_PORT = 8888
|
||||||
_DEFAULT_SSL_PORT = 4443
|
_DEFAULT_SSL_PORT = 4443
|
||||||
|
_DEFAULT_WEBSOCKET_PORT = 9999
|
||||||
|
_DEFAULT_WEBSOCKET_PROXY_PORT = 7777
|
||||||
|
|
||||||
#expand _DIST_BIN = __XPC_BIN_PATH__
|
#expand _DIST_BIN = __XPC_BIN_PATH__
|
||||||
#expand _IS_WIN32 = len("__WIN32__") != 0
|
#expand _IS_WIN32 = len("__WIN32__") != 0
|
||||||
|
@ -151,15 +153,24 @@ class Automation(object):
|
||||||
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
|
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
|
||||||
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
|
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
|
||||||
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
|
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
|
||||||
|
DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
|
||||||
|
DEFAULT_WEBSOCKET_PROXY_PORT = _DEFAULT_WEBSOCKET_PROXY_PORT
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.log = _log
|
self.log = _log
|
||||||
self.lastTestSeen = "automation.py"
|
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.webServer = webServer
|
||||||
self.httpPort = httpPort
|
self.httpPort = httpPort
|
||||||
self.sslPort = sslPort
|
self.sslPort = sslPort
|
||||||
|
self.webSocketPort = webSocketPort
|
||||||
|
self.webSocketProxyPort = webSocketProxyPort
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def __all__(self):
|
def __all__(self):
|
||||||
|
@ -375,11 +386,14 @@ function FindProxyForURL(url, host)
|
||||||
return 'DIRECT';
|
return 'DIRECT';
|
||||||
var isHttp = matches[1] == 'http';
|
var isHttp = matches[1] == 'http';
|
||||||
var isHttps = matches[1] == 'https';
|
var isHttps = matches[1] == 'https';
|
||||||
|
var isWebSocket = matches[1] == 'ws';
|
||||||
if (!matches[3])
|
if (!matches[3])
|
||||||
{
|
{
|
||||||
if (isHttp) matches[3] = '80';
|
if (isHttp | isWebSocket) matches[3] = '80';
|
||||||
if (isHttps) matches[3] = '443';
|
if (isHttps) matches[3] = '443';
|
||||||
}
|
}
|
||||||
|
if (isWebSocket)
|
||||||
|
matches[1] = 'http';
|
||||||
|
|
||||||
var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
|
var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
|
||||||
if (origins.indexOf(origin) < 0)
|
if (origins.indexOf(origin) < 0)
|
||||||
|
@ -388,11 +402,14 @@ function FindProxyForURL(url, host)
|
||||||
return 'PROXY %(remote)s:%(httpport)s';
|
return 'PROXY %(remote)s:%(httpport)s';
|
||||||
if (isHttps)
|
if (isHttps)
|
||||||
return 'PROXY %(remote)s:%(sslport)s';
|
return 'PROXY %(remote)s:%(sslport)s';
|
||||||
|
if (isWebSocket)
|
||||||
|
return 'PROXY %(remote)s:%(websocketproxyport)s';
|
||||||
return 'DIRECT';
|
return 'DIRECT';
|
||||||
}""" % { "origins": origins,
|
}""" % { "origins": origins,
|
||||||
"remote": self.webServer,
|
"remote": self.webServer,
|
||||||
"httpport":self.httpPort,
|
"httpport":self.httpPort,
|
||||||
"sslport": self.sslPort }
|
"sslport": self.sslPort,
|
||||||
|
"websocketproxyport": self.webSocketProxyPort }
|
||||||
pacURL = "".join(pacURL.splitlines())
|
pacURL = "".join(pacURL.splitlines())
|
||||||
|
|
||||||
part += """
|
part += """
|
||||||
|
@ -439,6 +456,8 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
|
||||||
sslTunnelConfig.write("httpproxy:1\n")
|
sslTunnelConfig.write("httpproxy:1\n")
|
||||||
sslTunnelConfig.write("certdbdir:%s\n" % certPath)
|
sslTunnelConfig.write("certdbdir:%s\n" % certPath)
|
||||||
sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
|
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)
|
sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
|
||||||
|
|
||||||
# Configure automatic certificate and bind custom certificates, client authentication
|
# Configure automatic certificate and bind custom certificates, client authentication
|
||||||
|
|
|
@ -394,6 +394,8 @@ _TEST_FILES2 = \
|
||||||
test_html_colors_quirks.html \
|
test_html_colors_quirks.html \
|
||||||
test_html_colors_standards.html \
|
test_html_colors_standards.html \
|
||||||
test_bug571390.xul \
|
test_bug571390.xul \
|
||||||
|
test_websocket_hello.html \
|
||||||
|
file_websocket_hello_wsh.py \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
# This test fails on the Mac for some reason
|
# This test fails on the Mac for some reason
|
||||||
|
|
|
@ -82,9 +82,37 @@ _SERV_FILES = \
|
||||||
mozprefs.js \
|
mozprefs.js \
|
||||||
$(NULL)
|
$(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)
|
_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
|
runtests.py: runtests.py.in
|
||||||
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
|
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
|
||||||
$(DEFINES) $(ACDEFINES) $^ > $@
|
$(DEFINES) $(ACDEFINES) $^ > $@
|
||||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
This is mod_pywebsocket 0.5, from http://code.google.com/p/pywebsocket/
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.webServer = self._automation.DEFAULT_WEB_SERVER
|
||||||
options.httpPort = self._automation.DEFAULT_HTTP_PORT
|
options.httpPort = self._automation.DEFAULT_HTTP_PORT
|
||||||
options.sslPort = self._automation.DEFAULT_SSL_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 options.vmwareRecording:
|
||||||
if not self._automation.IS_WIN32:
|
if not self._automation.IS_WIN32:
|
||||||
|
@ -343,6 +345,27 @@ class MochitestServer:
|
||||||
except:
|
except:
|
||||||
self._process.kill()
|
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):
|
class Mochitest(object):
|
||||||
# Path to the test script on the server
|
# Path to the test script on the server
|
||||||
|
@ -389,6 +412,20 @@ class Mochitest(object):
|
||||||
testURL = "about:blank"
|
testURL = "about:blank"
|
||||||
return testURL
|
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):
|
def startWebServer(self, options):
|
||||||
if options.webServer != '127.0.0.1':
|
if options.webServer != '127.0.0.1':
|
||||||
return
|
return
|
||||||
|
@ -547,7 +584,7 @@ class Mochitest(object):
|
||||||
|
|
||||||
manifest = self.buildProfile(options)
|
manifest = self.buildProfile(options)
|
||||||
self.startWebServer(options)
|
self.startWebServer(options)
|
||||||
|
self.startWebSocketServer(options)
|
||||||
|
|
||||||
testURL = self.buildTestPath(options)
|
testURL = self.buildTestPath(options)
|
||||||
self.buildURLOptions(options)
|
self.buildURLOptions(options)
|
||||||
|
@ -587,6 +624,7 @@ class Mochitest(object):
|
||||||
self.stopVMwareRecording();
|
self.stopVMwareRecording();
|
||||||
|
|
||||||
self.stopWebServer(options)
|
self.stopWebServer(options)
|
||||||
|
self.stopWebSocketServer(options)
|
||||||
processLeakLog(self.leak_report_file, options.leakThreshold)
|
processLeakLog(self.leak_report_file, options.leakThreshold)
|
||||||
self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
|
self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
|
||||||
|
|
||||||
|
@ -690,7 +728,11 @@ def main():
|
||||||
if options.symbolsPath and not isURL(options.symbolsPath):
|
if options.symbolsPath and not isURL(options.symbolsPath):
|
||||||
options.symbolsPath = mochitest.getFullPath(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))
|
sys.exit(mochitest.runTests(options))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -121,6 +121,14 @@ typedef struct {
|
||||||
string cert_nickname;
|
string cert_nickname;
|
||||||
PLHashTable* host_cert_table;
|
PLHashTable* host_cert_table;
|
||||||
PLHashTable* host_clientauth_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;
|
} server_info_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
@ -132,6 +140,7 @@ typedef struct {
|
||||||
const PRInt32 BUF_SIZE = 16384;
|
const PRInt32 BUF_SIZE = 16384;
|
||||||
const PRInt32 BUF_MARGIN = 1024;
|
const PRInt32 BUF_MARGIN = 1024;
|
||||||
const PRInt32 BUF_TOTAL = BUF_SIZE + BUF_MARGIN;
|
const PRInt32 BUF_TOTAL = BUF_SIZE + BUF_MARGIN;
|
||||||
|
const char HEADER_HOST[] = "Host:";
|
||||||
|
|
||||||
struct relayBuffer
|
struct relayBuffer
|
||||||
{
|
{
|
||||||
|
@ -341,6 +350,100 @@ bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, string &cer
|
||||||
return true;
|
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
|
* This function prefixes Request-URI path with a full scheme-host-port
|
||||||
* string.
|
* string.
|
||||||
|
@ -429,7 +532,8 @@ void HandleConnection(void* data)
|
||||||
|
|
||||||
if (!do_http_proxy)
|
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;
|
client_error = true;
|
||||||
else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
|
else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
|
||||||
client_error = true;
|
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");
|
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);
|
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");
|
printf(" could not open connection to the real server\n");
|
||||||
client_error = true;
|
client_error = true;
|
||||||
|
@ -588,7 +695,17 @@ void HandleConnection(void* data)
|
||||||
if (ssl_updated)
|
if (ssl_updated)
|
||||||
{
|
{
|
||||||
if (s == 0 && expect_request_start)
|
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);
|
expect_request_start = !AdjustRequestURI(buffers[s], &fullHost);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!AdjustWebSocketLocation(buffers[s], ci->server_info))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
in_flags2 |= PR_POLL_WRITE;
|
in_flags2 |= PR_POLL_WRITE;
|
||||||
printf(" telling the other socket to write");
|
printf(" telling the other socket to write");
|
||||||
|
@ -619,7 +736,7 @@ void HandleConnection(void* data)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
printf(", writen %d bytes", bytesWrite);
|
printf(", written %d bytes", bytesWrite);
|
||||||
buffers[s2].buffertail[1] = '\0';
|
buffers[s2].buffertail[1] = '\0';
|
||||||
printf(" dump:\n%.*s\n", bytesWrite, buffers[s2].bufferhead);
|
printf(" dump:\n%.*s\n", bytesWrite, buffers[s2].bufferhead);
|
||||||
|
|
||||||
|
@ -636,7 +753,8 @@ void HandleConnection(void* data)
|
||||||
printf(" proxy response sent to the client");
|
printf(" proxy response sent to the client");
|
||||||
// Proxy response has just been writen, update to ssl
|
// Proxy response has just been writen, update to ssl
|
||||||
ssl_updated = true;
|
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");
|
printf(" but failed to config server socket\n");
|
||||||
client_error = true;
|
client_error = true;
|
||||||
|
@ -793,6 +911,35 @@ int processConfigLine(char* configLine)
|
||||||
return 0;
|
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
|
// Configure all listen sockets and port+certificate bindings
|
||||||
if (!strcmp(keyword, "listen"))
|
if (!strcmp(keyword, "listen"))
|
||||||
{
|
{
|
||||||
|
@ -832,8 +979,10 @@ int processConfigLine(char* configLine)
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
server_info_t server;
|
server_info_t server;
|
||||||
|
memset(&server.remote_addr, 0, sizeof(PRNetAddr));
|
||||||
server.cert_nickname = certnick;
|
server.cert_nickname = certnick;
|
||||||
server.listen_port = port;
|
server.listen_port = port;
|
||||||
|
server.http_proxy_only = false;
|
||||||
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
|
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
|
||||||
if (!server.host_cert_table)
|
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"
|
" # in httpproxy mode and only after the 'listen' option has been\n"
|
||||||
" # specified. You also have to specify the tunnel listen port.\n"
|
" # specified. You also have to specify the tunnel listen port.\n"
|
||||||
" clientauth:requesting-client-cert.host.com:443:4443:request\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);
|
configFilePath);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче