mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 13:51:41 +00:00
Bug 570789. Add WebSocket support to mochitest. r=ted
--HG-- extra : rebase_source : 2e33d054a13824d1a7e527777383820946684c3f
This commit is contained in:
parent
b51e1f6c6a
commit
e6fdd7ae87
@ -58,6 +58,8 @@ import automationutils
|
||||
_DEFAULT_WEB_SERVER = "127.0.0.1"
|
||||
_DEFAULT_HTTP_PORT = 8888
|
||||
_DEFAULT_SSL_PORT = 4443
|
||||
_DEFAULT_WEBSOCKET_PORT = 9999
|
||||
_DEFAULT_WEBSOCKET_PROXY_PORT = 7777
|
||||
|
||||
#expand _DIST_BIN = __XPC_BIN_PATH__
|
||||
#expand _IS_WIN32 = len("__WIN32__") != 0
|
||||
@ -151,15 +153,24 @@ class Automation(object):
|
||||
DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
|
||||
DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
|
||||
DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
|
||||
DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
|
||||
DEFAULT_WEBSOCKET_PROXY_PORT = _DEFAULT_WEBSOCKET_PROXY_PORT
|
||||
|
||||
def __init__(self):
|
||||
self.log = _log
|
||||
self.lastTestSeen = "automation.py"
|
||||
|
||||
def setServerInfo(self, webServer = _DEFAULT_WEB_SERVER, httpPort = _DEFAULT_HTTP_PORT, sslPort = _DEFAULT_SSL_PORT):
|
||||
def setServerInfo(self,
|
||||
webServer = _DEFAULT_WEB_SERVER,
|
||||
httpPort = _DEFAULT_HTTP_PORT,
|
||||
sslPort = _DEFAULT_SSL_PORT,
|
||||
webSocketPort = _DEFAULT_WEBSOCKET_PORT,
|
||||
webSocketProxyPort = _DEFAULT_WEBSOCKET_PROXY_PORT):
|
||||
self.webServer = webServer
|
||||
self.httpPort = httpPort
|
||||
self.sslPort = sslPort
|
||||
self.webSocketPort = webSocketPort
|
||||
self.webSocketProxyPort = webSocketProxyPort
|
||||
|
||||
@property
|
||||
def __all__(self):
|
||||
@ -375,12 +386,15 @@ function FindProxyForURL(url, host)
|
||||
return 'DIRECT';
|
||||
var isHttp = matches[1] == 'http';
|
||||
var isHttps = matches[1] == 'https';
|
||||
var isWebSocket = matches[1] == 'ws';
|
||||
if (!matches[3])
|
||||
{
|
||||
if (isHttp) matches[3] = '80';
|
||||
if (isHttp | isWebSocket) matches[3] = '80';
|
||||
if (isHttps) matches[3] = '443';
|
||||
}
|
||||
|
||||
if (isWebSocket)
|
||||
matches[1] = 'http';
|
||||
|
||||
var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
|
||||
if (origins.indexOf(origin) < 0)
|
||||
return 'DIRECT';
|
||||
@ -388,11 +402,14 @@ function FindProxyForURL(url, host)
|
||||
return 'PROXY %(remote)s:%(httpport)s';
|
||||
if (isHttps)
|
||||
return 'PROXY %(remote)s:%(sslport)s';
|
||||
if (isWebSocket)
|
||||
return 'PROXY %(remote)s:%(websocketproxyport)s';
|
||||
return 'DIRECT';
|
||||
}""" % { "origins": origins,
|
||||
"remote": self.webServer,
|
||||
"httpport":self.httpPort,
|
||||
"sslport": self.sslPort }
|
||||
"sslport": self.sslPort,
|
||||
"websocketproxyport": self.webSocketProxyPort }
|
||||
pacURL = "".join(pacURL.splitlines())
|
||||
|
||||
part += """
|
||||
@ -439,6 +456,8 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
|
||||
sslTunnelConfig.write("httpproxy:1\n")
|
||||
sslTunnelConfig.write("certdbdir:%s\n" % certPath)
|
||||
sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
|
||||
sslTunnelConfig.write("proxy:%s:%s:%s\n" %
|
||||
(self.webSocketProxyPort, self.webServer, self.webSocketPort))
|
||||
sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
|
||||
|
||||
# Configure automatic certificate and bind custom certificates, client authentication
|
||||
|
@ -394,6 +394,8 @@ _TEST_FILES2 = \
|
||||
test_html_colors_quirks.html \
|
||||
test_html_colors_standards.html \
|
||||
test_bug571390.xul \
|
||||
test_websocket_hello.html \
|
||||
file_websocket_hello_wsh.py \
|
||||
$(NULL)
|
||||
|
||||
# This test fails on the Mac for some reason
|
||||
|
@ -82,9 +82,37 @@ _SERV_FILES = \
|
||||
mozprefs.js \
|
||||
$(NULL)
|
||||
|
||||
_PYWEBSOCKET_FILES = \
|
||||
pywebsocket/standalone.py \
|
||||
$(NULL)
|
||||
|
||||
_MOD_PYWEBSOCKET_FILES = \
|
||||
pywebsocket/mod_pywebsocket/__init__.py \
|
||||
pywebsocket/mod_pywebsocket/dispatch.py \
|
||||
pywebsocket/mod_pywebsocket/util.py \
|
||||
pywebsocket/mod_pywebsocket/msgutil.py \
|
||||
pywebsocket/mod_pywebsocket/memorizingfile.py \
|
||||
pywebsocket/mod_pywebsocket/headerparserhandler.py \
|
||||
$(NULL)
|
||||
|
||||
_HANDSHAKE_FILES = \
|
||||
pywebsocket/mod_pywebsocket/handshake/__init__.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/_base.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/draft75.py \
|
||||
pywebsocket/mod_pywebsocket/handshake/handshake.py \
|
||||
$(NULL)
|
||||
|
||||
_DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir)
|
||||
|
||||
libs:: $(_PYWEBSOCKET_FILES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket
|
||||
|
||||
libs:: $(_MOD_PYWEBSOCKET_FILES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket/mod_pywebsocket
|
||||
|
||||
libs:: $(_HANDSHAKE_FILES)
|
||||
$(INSTALL) $(foreach f,$^,"$f") $(_DEST_DIR)/pywebsocket/mod_pywebsocket/handshake
|
||||
|
||||
runtests.py: runtests.py.in
|
||||
$(PYTHON) $(topsrcdir)/config/Preprocessor.py \
|
||||
$(DEFINES) $(ACDEFINES) $^ > $@
|
||||
|
28
testing/mochitest/pywebsocket/COPYING
Normal file
28
testing/mochitest/pywebsocket/COPYING
Normal file
@ -0,0 +1,28 @@
|
||||
Copyright 2009, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
1
testing/mochitest/pywebsocket/README
Normal file
1
testing/mochitest/pywebsocket/README
Normal file
@ -0,0 +1 @@
|
||||
This is mod_pywebsocket 0.5, from http://code.google.com/p/pywebsocket/
|
111
testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py
Normal file
111
testing/mochitest/pywebsocket/mod_pywebsocket/__init__.py
Normal file
@ -0,0 +1,111 @@
|
||||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket extension for Apache HTTP Server.
|
||||
|
||||
mod_pywebsocket is a Web Socket extension for Apache HTTP Server
|
||||
intended for testing or experimental purposes. mod_python is required.
|
||||
|
||||
Installation:
|
||||
|
||||
0. Prepare an Apache HTTP Server for which mod_python is enabled.
|
||||
|
||||
1. Specify the following Apache HTTP Server directives to suit your
|
||||
configuration.
|
||||
|
||||
If mod_pywebsocket is not in the Python path, specify the following.
|
||||
<websock_lib> is the directory where mod_pywebsocket is installed.
|
||||
|
||||
PythonPath "sys.path+['<websock_lib>']"
|
||||
|
||||
Always specify the following. <websock_handlers> is the directory where
|
||||
user-written Web Socket handlers are placed.
|
||||
|
||||
PythonOption mod_pywebsocket.handler_root <websock_handlers>
|
||||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
|
||||
To limit the search for Web Socket handlers to a directory <scan_dir>
|
||||
under <websock_handlers>, configure as follows:
|
||||
|
||||
PythonOption mod_pywebsocket.handler_scan <scan_dir>
|
||||
|
||||
<scan_dir> is useful in saving scan time when <websock_handlers>
|
||||
contains many non-Web Socket handler files.
|
||||
|
||||
If you want to support old handshake based on
|
||||
draft-hixie-thewebsocketprotocol-75:
|
||||
|
||||
PythonOption mod_pywebsocket.allow_draft75 On
|
||||
|
||||
|
||||
Example snippet of httpd.conf:
|
||||
(mod_pywebsocket is in /websock_lib, Web Socket handlers are in
|
||||
/websock_handlers, port is 80 for ws, 443 for wss.)
|
||||
|
||||
<IfModule python_module>
|
||||
PythonPath "sys.path+['/websock_lib']"
|
||||
PythonOption mod_pywebsocket.handler_root /websock_handlers
|
||||
PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
|
||||
</IfModule>
|
||||
|
||||
Writing Web Socket handlers:
|
||||
|
||||
When a Web Socket request comes in, the resource name
|
||||
specified in the handshake is considered as if it is a file path under
|
||||
<websock_handlers> and the handler defined in
|
||||
<websock_handlers>/<resource_name>_wsh.py is invoked.
|
||||
|
||||
For example, if the resource name is /example/chat, the handler defined in
|
||||
<websock_handlers>/example/chat_wsh.py is invoked.
|
||||
|
||||
A Web Socket handler is composed of the following two functions:
|
||||
|
||||
web_socket_do_extra_handshake(request)
|
||||
web_socket_transfer_data(request)
|
||||
|
||||
where:
|
||||
request: mod_python request.
|
||||
|
||||
web_socket_do_extra_handshake is called during the handshake after the
|
||||
headers are successfully parsed and Web Socket properties (ws_location,
|
||||
ws_origin, ws_protocol, and ws_resource) are added to request. A handler
|
||||
can reject the request by raising an exception.
|
||||
|
||||
web_socket_transfer_data is called after the handshake completed
|
||||
successfully. A handler can receive/send messages from/to the client
|
||||
using request. mod_pywebsocket.msgutil module provides utilities
|
||||
for data transfer.
|
||||
|
||||
A Web Socket handler must be thread-safe if the server (Apache or
|
||||
standalone.py) is configured to use threads.
|
||||
"""
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et tw=72
|
245
testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py
Normal file
245
testing/mochitest/pywebsocket/mod_pywebsocket/dispatch.py
Normal file
@ -0,0 +1,245 @@
|
||||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Dispatch Web Socket request.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from mod_pywebsocket import msgutil
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
_SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
|
||||
_SOURCE_SUFFIX = '_wsh.py'
|
||||
_DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
|
||||
_TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
|
||||
|
||||
|
||||
class DispatchError(Exception):
|
||||
"""Exception in dispatching Web Socket request."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_path(path):
|
||||
"""Normalize path.
|
||||
|
||||
Args:
|
||||
path: the path to normalize.
|
||||
|
||||
Path is converted to the absolute path.
|
||||
The input path can use either '\\' or '/' as the separator.
|
||||
The normalized path always uses '/' regardless of the platform.
|
||||
"""
|
||||
|
||||
path = path.replace('\\', os.path.sep)
|
||||
#path = os.path.realpath(path)
|
||||
path = path.replace('\\', '/')
|
||||
return path
|
||||
|
||||
|
||||
def _path_to_resource_converter(base_dir):
|
||||
base_dir = _normalize_path(base_dir)
|
||||
base_len = len(base_dir)
|
||||
suffix_len = len(_SOURCE_SUFFIX)
|
||||
def converter(path):
|
||||
if not path.endswith(_SOURCE_SUFFIX):
|
||||
return None
|
||||
path = _normalize_path(path)
|
||||
if not path.startswith(base_dir):
|
||||
return None
|
||||
return path[base_len:-suffix_len]
|
||||
return converter
|
||||
|
||||
|
||||
def _source_file_paths(directory):
|
||||
"""Yield Web Socket Handler source file names in the given directory."""
|
||||
|
||||
for root, unused_dirs, files in os.walk(directory):
|
||||
for base in files:
|
||||
path = os.path.join(root, base)
|
||||
if _SOURCE_PATH_PATTERN.search(path):
|
||||
yield path
|
||||
|
||||
|
||||
def _source(source_str):
|
||||
"""Source a handler definition string."""
|
||||
|
||||
global_dic = {}
|
||||
try:
|
||||
exec source_str in global_dic
|
||||
except Exception:
|
||||
raise DispatchError('Error in sourcing handler:' +
|
||||
util.get_stack_trace())
|
||||
return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
|
||||
_extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
|
||||
|
||||
|
||||
def _extract_handler(dic, name):
|
||||
if name not in dic:
|
||||
raise DispatchError('%s is not defined.' % name)
|
||||
handler = dic[name]
|
||||
if not callable(handler):
|
||||
raise DispatchError('%s is not callable.' % name)
|
||||
return handler
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
"""Dispatches Web Socket requests.
|
||||
|
||||
This class maintains a map from resource name to handlers.
|
||||
"""
|
||||
|
||||
def __init__(self, root_dir, scan_dir=None):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
root_dir: The directory where handler definition files are
|
||||
placed.
|
||||
scan_dir: The directory where handler definition files are
|
||||
searched. scan_dir must be a directory under root_dir,
|
||||
including root_dir itself. If scan_dir is None, root_dir
|
||||
is used as scan_dir. scan_dir can be useful in saving
|
||||
scan time when root_dir contains many subdirectories.
|
||||
"""
|
||||
|
||||
self._handlers = {}
|
||||
self._source_warnings = []
|
||||
if scan_dir is None:
|
||||
scan_dir = root_dir
|
||||
if not os.path.realpath(scan_dir).startswith(
|
||||
os.path.realpath(root_dir)):
|
||||
raise DispatchError('scan_dir:%s must be a directory under '
|
||||
'root_dir:%s.' % (scan_dir, root_dir))
|
||||
self._source_files_in_dir(root_dir, scan_dir)
|
||||
|
||||
def add_resource_path_alias(self,
|
||||
alias_resource_path, existing_resource_path):
|
||||
"""Add resource path alias.
|
||||
|
||||
Once added, request to alias_resource_path would be handled by
|
||||
handler registered for existing_resource_path.
|
||||
|
||||
Args:
|
||||
alias_resource_path: alias resource path
|
||||
existing_resource_path: existing resource path
|
||||
"""
|
||||
try:
|
||||
handler = self._handlers[existing_resource_path]
|
||||
self._handlers[alias_resource_path] = handler
|
||||
except KeyError:
|
||||
raise DispatchError('No handler for: %r' % existing_resource_path)
|
||||
|
||||
def source_warnings(self):
|
||||
"""Return warnings in sourcing handlers."""
|
||||
|
||||
return self._source_warnings
|
||||
|
||||
def do_extra_handshake(self, request):
|
||||
"""Do extra checking in Web Socket handshake.
|
||||
|
||||
Select a handler based on request.uri and call its
|
||||
web_socket_do_extra_handshake function.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
do_extra_handshake_, unused_transfer_data = self._handler(request)
|
||||
try:
|
||||
do_extra_handshake_(request)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_DO_EXTRA_HANDSHAKE_HANDLER_NAME,
|
||||
request.ws_resource),
|
||||
e)
|
||||
raise
|
||||
|
||||
def transfer_data(self, request):
|
||||
"""Let a handler transfer_data with a Web Socket client.
|
||||
|
||||
Select a handler based on request.ws_resource and call its
|
||||
web_socket_transfer_data function.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
|
||||
unused_do_extra_handshake, transfer_data_ = self._handler(request)
|
||||
try:
|
||||
try:
|
||||
request.client_terminated = False
|
||||
request.server_terminated = False
|
||||
transfer_data_(request)
|
||||
except msgutil.ConnectionTerminatedException, e:
|
||||
util.prepend_message_to_exception(
|
||||
'client initiated closing handshake for %s: ' % (
|
||||
request.ws_resource),
|
||||
e)
|
||||
raise
|
||||
except Exception, e:
|
||||
print 'exception: %s' % type(e)
|
||||
util.prepend_message_to_exception(
|
||||
'%s raised exception for %s: ' % (
|
||||
_TRANSFER_DATA_HANDLER_NAME, request.ws_resource),
|
||||
e)
|
||||
raise
|
||||
finally:
|
||||
msgutil.close_connection(request)
|
||||
|
||||
|
||||
def _handler(self, request):
|
||||
try:
|
||||
ws_resource_path = request.ws_resource.split('?', 1)[0]
|
||||
return self._handlers[ws_resource_path]
|
||||
except KeyError:
|
||||
raise DispatchError('No handler for: %r' % request.ws_resource)
|
||||
|
||||
def _source_files_in_dir(self, root_dir, scan_dir):
|
||||
"""Source all the handler source files in the scan_dir directory.
|
||||
|
||||
The resource path is determined relative to root_dir.
|
||||
"""
|
||||
|
||||
to_resource = _path_to_resource_converter(root_dir)
|
||||
for path in _source_file_paths(scan_dir):
|
||||
try:
|
||||
handlers = _source(open(path).read())
|
||||
except DispatchError, e:
|
||||
self._source_warnings.append('%s: %s' % (path, e))
|
||||
continue
|
||||
self._handlers[to_resource(path)] = handlers
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
@ -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
|
101
testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py
Normal file
101
testing/mochitest/pywebsocket/mod_pywebsocket/handshake/_base.py
Normal file
@ -0,0 +1,101 @@
|
||||
# Copyright 2010, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Socket handshaking.
|
||||
|
||||
Note: request.connection.write/read are used in this module, even though
|
||||
mod_python document says that they should be used only in connection handlers.
|
||||
Unfortunately, we have no other options. For example, request.write/read are
|
||||
not suitable because they don't allow direct raw bytes writing/reading.
|
||||
"""
|
||||
|
||||
|
||||
DEFAULT_WEB_SOCKET_PORT = 80
|
||||
DEFAULT_WEB_SOCKET_SECURE_PORT = 443
|
||||
WEB_SOCKET_SCHEME = 'ws'
|
||||
WEB_SOCKET_SECURE_SCHEME = 'wss'
|
||||
|
||||
|
||||
class HandshakeError(Exception):
|
||||
"""Exception in Web Socket Handshake."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def default_port(is_secure):
|
||||
if is_secure:
|
||||
return DEFAULT_WEB_SOCKET_SECURE_PORT
|
||||
else:
|
||||
return DEFAULT_WEB_SOCKET_PORT
|
||||
|
||||
|
||||
def validate_protocol(protocol):
|
||||
"""Validate WebSocket-Protocol string."""
|
||||
|
||||
if not protocol:
|
||||
raise HandshakeError('Invalid WebSocket-Protocol: empty')
|
||||
for c in protocol:
|
||||
if not 0x20 <= ord(c) <= 0x7e:
|
||||
raise HandshakeError('Illegal character in protocol: %r' % c)
|
||||
|
||||
|
||||
def parse_host_header(request):
|
||||
fields = request.headers_in['Host'].split(':', 1)
|
||||
if len(fields) == 1:
|
||||
return fields[0], default_port(request.is_https())
|
||||
try:
|
||||
return fields[0], int(fields[1])
|
||||
except ValueError, e:
|
||||
raise HandshakeError('Invalid port number format: %r' % e)
|
||||
|
||||
|
||||
def build_location(request):
|
||||
"""Build WebSocket location for request."""
|
||||
location_parts = []
|
||||
if request.is_https():
|
||||
location_parts.append(WEB_SOCKET_SECURE_SCHEME)
|
||||
else:
|
||||
location_parts.append(WEB_SOCKET_SCHEME)
|
||||
location_parts.append('://')
|
||||
host, port = parse_host_header(request)
|
||||
connection_port = request.connection.local_addr[1]
|
||||
if port != connection_port:
|
||||
raise HandshakeError('Header/connection port mismatch: %d/%d' %
|
||||
(port, connection_port))
|
||||
location_parts.append(host)
|
||||
if (port != default_port(request.is_https())):
|
||||
location_parts.append(':')
|
||||
location_parts.append(str(port))
|
||||
location_parts.append(request.uri)
|
||||
return ''.join(location_parts)
|
||||
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
@ -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
|
290
testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py
Normal file
290
testing/mochitest/pywebsocket/mod_pywebsocket/msgutil.py
Normal file
@ -0,0 +1,290 @@
|
||||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Message related utilities.
|
||||
|
||||
Note: request.connection.write/read are used in this module, even though
|
||||
mod_python document says that they should be used only in connection handlers.
|
||||
Unfortunately, we have no other options. For example, request.write/read are
|
||||
not suitable because they don't allow direct raw bytes writing/reading.
|
||||
"""
|
||||
|
||||
|
||||
import Queue
|
||||
import threading
|
||||
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
class MsgUtilException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionTerminatedException(MsgUtilException):
|
||||
pass
|
||||
|
||||
|
||||
def _read(request, length):
|
||||
bytes = request.connection.read(length)
|
||||
if not bytes:
|
||||
raise MsgUtilException(
|
||||
'Failed to receive message from %r' %
|
||||
(request.connection.remote_addr,))
|
||||
return bytes
|
||||
|
||||
|
||||
def _write(request, bytes):
|
||||
try:
|
||||
request.connection.write(bytes)
|
||||
except Exception, e:
|
||||
util.prepend_message_to_exception(
|
||||
'Failed to send message to %r: ' %
|
||||
(request.connection.remote_addr,),
|
||||
e)
|
||||
raise
|
||||
|
||||
|
||||
def close_connection(request):
|
||||
"""Close connection.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
if request.server_terminated:
|
||||
return
|
||||
# 5.3 the server may decide to terminate the WebSocket connection by
|
||||
# running through the following steps:
|
||||
# 1. send a 0xFF byte and a 0x00 byte to the client to indicate the start
|
||||
# of the closing handshake.
|
||||
_write(request, '\xff\x00')
|
||||
request.server_terminated = True
|
||||
# TODO(ukai): 2. wait until the /client terminated/ flag has been set, or
|
||||
# until a server-defined timeout expires.
|
||||
# TODO: 3. close the WebSocket connection.
|
||||
# note: mod_python Connection (mp_conn) doesn't have close method.
|
||||
|
||||
|
||||
def send_message(request, message):
|
||||
"""Send message.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
message: unicode string to send.
|
||||
Raises:
|
||||
ConnectionTerminatedException: when server already terminated.
|
||||
"""
|
||||
if request.server_terminated:
|
||||
raise ConnectionTerminatedException
|
||||
_write(request, '\x00' + message.encode('utf-8') + '\xff')
|
||||
|
||||
|
||||
def receive_message(request):
|
||||
"""Receive a Web Socket frame and return its payload as unicode string.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
Raises:
|
||||
ConnectionTerminatedException: when client already terminated.
|
||||
"""
|
||||
|
||||
if request.client_terminated:
|
||||
raise ConnectionTerminatedException
|
||||
while True:
|
||||
# Read 1 byte.
|
||||
# mp_conn.read will block if no bytes are available.
|
||||
# Timeout is controlled by TimeOut directive of Apache.
|
||||
frame_type_str = _read(request, 1)
|
||||
frame_type = ord(frame_type_str[0])
|
||||
if (frame_type & 0x80) == 0x80:
|
||||
# The payload length is specified in the frame.
|
||||
# Read and discard.
|
||||
length = _payload_length(request)
|
||||
_receive_bytes(request, length)
|
||||
# 5.3 3. 12. if /type/ is 0xFF and /length/ is 0, then set the
|
||||
# /client terminated/ flag and abort these steps.
|
||||
if frame_type == 0xFF and length == 0:
|
||||
request.client_terminated = True
|
||||
raise ConnectionTerminatedException
|
||||
else:
|
||||
# The payload is delimited with \xff.
|
||||
bytes = _read_until(request, '\xff')
|
||||
# The Web Socket protocol section 4.4 specifies that invalid
|
||||
# characters must be replaced with U+fffd REPLACEMENT CHARACTER.
|
||||
message = bytes.decode('utf-8', 'replace')
|
||||
if frame_type == 0x00:
|
||||
return message
|
||||
# Discard data of other types.
|
||||
|
||||
|
||||
def _payload_length(request):
|
||||
length = 0
|
||||
while True:
|
||||
b_str = _read(request, 1)
|
||||
b = ord(b_str[0])
|
||||
length = length * 128 + (b & 0x7f)
|
||||
if (b & 0x80) == 0:
|
||||
break
|
||||
return length
|
||||
|
||||
|
||||
def _receive_bytes(request, length):
|
||||
bytes = []
|
||||
while length > 0:
|
||||
new_bytes = _read(request, length)
|
||||
bytes.append(new_bytes)
|
||||
length -= len(new_bytes)
|
||||
return ''.join(bytes)
|
||||
|
||||
|
||||
def _read_until(request, delim_char):
|
||||
bytes = []
|
||||
while True:
|
||||
ch = _read(request, 1)
|
||||
if ch == delim_char:
|
||||
break
|
||||
bytes.append(ch)
|
||||
return ''.join(bytes)
|
||||
|
||||
|
||||
class MessageReceiver(threading.Thread):
|
||||
"""This class receives messages from the client.
|
||||
|
||||
This class provides three ways to receive messages: blocking, non-blocking,
|
||||
and via callback. Callback has the highest precedence.
|
||||
|
||||
Note: This class should not be used with the standalone server for wss
|
||||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
def __init__(self, request, onmessage=None):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
onmessage: a function to be called when a message is received.
|
||||
May be None. If not None, the function is called on
|
||||
another thread. In that case, MessageReceiver.receive
|
||||
and MessageReceiver.receive_nowait are useless because
|
||||
they will never return any messages.
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = Queue.Queue()
|
||||
self._onmessage = onmessage
|
||||
self._stop_requested = False
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while not self._stop_requested:
|
||||
message = receive_message(self._request)
|
||||
if self._onmessage:
|
||||
self._onmessage(message)
|
||||
else:
|
||||
self._queue.put(message)
|
||||
finally:
|
||||
close_connection(self._request)
|
||||
|
||||
def receive(self):
|
||||
""" Receive a message from the channel, blocking.
|
||||
|
||||
Returns:
|
||||
message as a unicode string.
|
||||
"""
|
||||
return self._queue.get()
|
||||
|
||||
def receive_nowait(self):
|
||||
""" Receive a message from the channel, non-blocking.
|
||||
|
||||
Returns:
|
||||
message as a unicode string if available. None otherwise.
|
||||
"""
|
||||
try:
|
||||
message = self._queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
message = None
|
||||
return message
|
||||
|
||||
def stop(self):
|
||||
"""Request to stop this instance.
|
||||
|
||||
The instance will be stopped after receiving the next message.
|
||||
This method may not be very useful, but there is no clean way
|
||||
in Python to forcefully stop a running thread.
|
||||
"""
|
||||
self._stop_requested = True
|
||||
|
||||
|
||||
class MessageSender(threading.Thread):
|
||||
"""This class sends messages to the client.
|
||||
|
||||
This class provides both synchronous and asynchronous ways to send
|
||||
messages.
|
||||
|
||||
Note: This class should not be used with the standalone server for wss
|
||||
because pyOpenSSL used by the server raises a fatal error if the socket
|
||||
is accessed from multiple threads.
|
||||
"""
|
||||
def __init__(self, request):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request: mod_python request.
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self._request = request
|
||||
self._queue = Queue.Queue()
|
||||
self.setDaemon(True)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
message, condition = self._queue.get()
|
||||
condition.acquire()
|
||||
send_message(self._request, message)
|
||||
condition.notify()
|
||||
condition.release()
|
||||
|
||||
def send(self, message):
|
||||
"""Send a message, blocking."""
|
||||
|
||||
condition = threading.Condition()
|
||||
condition.acquire()
|
||||
self._queue.put((message, condition))
|
||||
condition.wait()
|
||||
|
||||
def send_nowait(self, message):
|
||||
"""Send a message, non-blocking."""
|
||||
|
||||
self._queue.put((message, threading.Condition()))
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
121
testing/mochitest/pywebsocket/mod_pywebsocket/util.py
Normal file
121
testing/mochitest/pywebsocket/mod_pywebsocket/util.py
Normal file
@ -0,0 +1,121 @@
|
||||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Web Sockets utilities.
|
||||
"""
|
||||
|
||||
|
||||
import StringIO
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
|
||||
def get_stack_trace():
|
||||
"""Get the current stack trace as string.
|
||||
|
||||
This is needed to support Python 2.3.
|
||||
TODO: Remove this when we only support Python 2.4 and above.
|
||||
Use traceback.format_exc instead.
|
||||
"""
|
||||
|
||||
out = StringIO.StringIO()
|
||||
traceback.print_exc(file=out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def prepend_message_to_exception(message, exc):
|
||||
"""Prepend message to the exception."""
|
||||
|
||||
exc.args = (message + str(exc),)
|
||||
return
|
||||
|
||||
|
||||
def __translate_interp(interp, cygwin_path):
|
||||
"""Translate interp program path for Win32 python to run cygwin program
|
||||
(e.g. perl). Note that it doesn't support path that contains space,
|
||||
which is typically true for Unix, where #!-script is written.
|
||||
For Win32 python, cygwin_path is a directory of cygwin binaries.
|
||||
|
||||
Args:
|
||||
interp: interp command line
|
||||
cygwin_path: directory name of cygwin binary, or None
|
||||
Returns:
|
||||
translated interp command line.
|
||||
"""
|
||||
if not cygwin_path:
|
||||
return interp
|
||||
m = re.match("^[^ ]*/([^ ]+)( .*)?", interp)
|
||||
if m:
|
||||
cmd = os.path.join(cygwin_path, m.group(1))
|
||||
return cmd + m.group(2)
|
||||
return interp
|
||||
|
||||
|
||||
def get_script_interp(script_path, cygwin_path=None):
|
||||
"""Gets #!-interpreter command line from the script.
|
||||
|
||||
It also fixes command path. When Cygwin Python is used, e.g. in WebKit,
|
||||
it could run "/usr/bin/perl -wT hello.pl".
|
||||
When Win32 Python is used, e.g. in Chromium, it couldn't. So, fix
|
||||
"/usr/bin/perl" to "<cygwin_path>\perl.exe".
|
||||
|
||||
Args:
|
||||
script_path: pathname of the script
|
||||
cygwin_path: directory name of cygwin binary, or None
|
||||
Returns:
|
||||
#!-interpreter command line, or None if it is not #!-script.
|
||||
"""
|
||||
fp = open(script_path)
|
||||
line = fp.readline()
|
||||
fp.close()
|
||||
m = re.match("^#!(.*)", line)
|
||||
if m:
|
||||
return __translate_interp(m.group(1), cygwin_path)
|
||||
return None
|
||||
|
||||
def wrap_popen3_for_win(cygwin_path):
|
||||
"""Wrap popen3 to support #!-script on Windows.
|
||||
|
||||
Args:
|
||||
cygwin_path: path for cygwin binary if command path is needed to be
|
||||
translated. None if no translation required.
|
||||
"""
|
||||
__orig_popen3 = os.popen3
|
||||
def __wrap_popen3(cmd, mode='t', bufsize=-1):
|
||||
cmdline = cmd.split(' ')
|
||||
interp = get_script_interp(cmdline[0], cygwin_path)
|
||||
if interp:
|
||||
cmd = interp + " " + cmd
|
||||
return __orig_popen3(cmd, mode, bufsize)
|
||||
os.popen3 = __wrap_popen3
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
472
testing/mochitest/pywebsocket/standalone.py
Normal file
472
testing/mochitest/pywebsocket/standalone.py
Normal file
@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009, Google Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following disclaimer
|
||||
# in the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
# * Neither the name of Google Inc. nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
"""Standalone Web Socket server.
|
||||
|
||||
Use this server to run mod_pywebsocket without Apache HTTP Server.
|
||||
|
||||
Usage:
|
||||
python standalone.py [-p <ws_port>] [-w <websock_handlers>]
|
||||
[-s <scan_dir>]
|
||||
[-d <document_root>]
|
||||
[-m <websock_handlers_map_file>]
|
||||
... for other options, see _main below ...
|
||||
|
||||
<ws_port> is the port number to use for ws:// connection.
|
||||
|
||||
<document_root> is the path to the root directory of HTML files.
|
||||
|
||||
<websock_handlers> is the path to the root directory of Web Socket handlers.
|
||||
See __init__.py for details of <websock_handlers> and how to write Web Socket
|
||||
handlers. If this path is relative, <document_root> is used as the base.
|
||||
|
||||
<scan_dir> is a path under the root directory. If specified, only the handlers
|
||||
under scan_dir are scanned. This is useful in saving scan time.
|
||||
|
||||
Note:
|
||||
This server is derived from SocketServer.ThreadingMixIn. Hence a thread is
|
||||
used for each request.
|
||||
|
||||
SECURITY WARNING: This uses CGIHTTPServer and CGIHTTPServer is not secure.
|
||||
It may execute arbitrary Python code or external programs. It should not be
|
||||
used outside a firewall.
|
||||
"""
|
||||
|
||||
import BaseHTTPServer
|
||||
import CGIHTTPServer
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import logging
|
||||
import logging.handlers
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
_HAS_OPEN_SSL = False
|
||||
try:
|
||||
import OpenSSL.SSL
|
||||
_HAS_OPEN_SSL = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from mod_pywebsocket import dispatch
|
||||
from mod_pywebsocket import handshake
|
||||
from mod_pywebsocket import memorizingfile
|
||||
from mod_pywebsocket import util
|
||||
|
||||
|
||||
_LOG_LEVELS = {
|
||||
'debug': logging.DEBUG,
|
||||
'info': logging.INFO,
|
||||
'warn': logging.WARN,
|
||||
'error': logging.ERROR,
|
||||
'critical': logging.CRITICAL};
|
||||
|
||||
_DEFAULT_LOG_MAX_BYTES = 1024 * 256
|
||||
_DEFAULT_LOG_BACKUP_COUNT = 5
|
||||
|
||||
_DEFAULT_REQUEST_QUEUE_SIZE = 128
|
||||
|
||||
# 1024 is practically large enough to contain WebSocket handshake lines.
|
||||
_MAX_MEMORIZED_LINES = 1024
|
||||
|
||||
def _print_warnings_if_any(dispatcher):
|
||||
warnings = dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('mod_pywebsocket: %s' % warning)
|
||||
|
||||
|
||||
class _StandaloneConnection(object):
|
||||
"""Mimic mod_python mp_conn."""
|
||||
|
||||
def __init__(self, request_handler):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
self._request_handler = request_handler
|
||||
|
||||
def get_local_addr(self):
|
||||
"""Getter to mimic mp_conn.local_addr."""
|
||||
return (self._request_handler.server.server_name,
|
||||
self._request_handler.server.server_port)
|
||||
local_addr = property(get_local_addr)
|
||||
|
||||
def get_remote_addr(self):
|
||||
"""Getter to mimic mp_conn.remote_addr.
|
||||
|
||||
Setting the property in __init__ won't work because the request
|
||||
handler is not initialized yet there."""
|
||||
return self._request_handler.client_address
|
||||
remote_addr = property(get_remote_addr)
|
||||
|
||||
def write(self, data):
|
||||
"""Mimic mp_conn.write()."""
|
||||
return self._request_handler.wfile.write(data)
|
||||
|
||||
def read(self, length):
|
||||
"""Mimic mp_conn.read()."""
|
||||
return self._request_handler.rfile.read(length)
|
||||
|
||||
def get_memorized_lines(self):
|
||||
"""Get memorized lines."""
|
||||
return self._request_handler.rfile.get_memorized_lines()
|
||||
|
||||
|
||||
class _StandaloneRequest(object):
|
||||
"""Mimic mod_python request."""
|
||||
|
||||
def __init__(self, request_handler, use_tls):
|
||||
"""Construct an instance.
|
||||
|
||||
Args:
|
||||
request_handler: A WebSocketRequestHandler instance.
|
||||
"""
|
||||
self._request_handler = request_handler
|
||||
self.connection = _StandaloneConnection(request_handler)
|
||||
self._use_tls = use_tls
|
||||
|
||||
def get_uri(self):
|
||||
"""Getter to mimic request.uri."""
|
||||
return self._request_handler.path
|
||||
uri = property(get_uri)
|
||||
|
||||
def get_method(self):
|
||||
"""Getter to mimic request.method."""
|
||||
return self._request_handler.command
|
||||
method = property(get_method)
|
||||
|
||||
def get_headers_in(self):
|
||||
"""Getter to mimic request.headers_in."""
|
||||
return self._request_handler.headers
|
||||
headers_in = property(get_headers_in)
|
||||
|
||||
def is_https(self):
|
||||
"""Mimic request.is_https()."""
|
||||
return self._use_tls
|
||||
|
||||
|
||||
class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
"""HTTPServer specialized for Web Socket."""
|
||||
|
||||
SocketServer.ThreadingMixIn.daemon_threads = True
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass):
|
||||
"""Override SocketServer.BaseServer.__init__."""
|
||||
|
||||
SocketServer.BaseServer.__init__(
|
||||
self, server_address, RequestHandlerClass)
|
||||
self.socket = self._create_socket()
|
||||
self.server_bind()
|
||||
self.server_activate()
|
||||
|
||||
def _create_socket(self):
|
||||
socket_ = socket.socket(self.address_family, self.socket_type)
|
||||
if WebSocketServer.options.use_tls:
|
||||
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||
ctx.use_privatekey_file(WebSocketServer.options.private_key)
|
||||
ctx.use_certificate_file(WebSocketServer.options.certificate)
|
||||
socket_ = OpenSSL.SSL.Connection(ctx, socket_)
|
||||
return socket_
|
||||
|
||||
def handle_error(self, rquest, client_address):
|
||||
"""Override SocketServer.handle_error."""
|
||||
|
||||
logging.error(
|
||||
('Exception in processing request from: %r' % (client_address,)) +
|
||||
'\n' + util.get_stack_trace())
|
||||
# Note: client_address is a tuple. To match it against %r, we need the
|
||||
# trailing comma.
|
||||
|
||||
|
||||
class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
|
||||
"""CGIHTTPRequestHandler specialized for Web Socket."""
|
||||
|
||||
def setup(self):
|
||||
"""Override SocketServer.StreamRequestHandler.setup."""
|
||||
|
||||
self.connection = self.request
|
||||
self.rfile = memorizingfile.MemorizingFile(
|
||||
socket._fileobject(self.request, 'rb', self.rbufsize),
|
||||
max_memorized_lines=_MAX_MEMORIZED_LINES)
|
||||
self.wfile = socket._fileobject(self.request, 'wb', self.wbufsize)
|
||||
|
||||
def __init__(self, *args, **keywords):
|
||||
self._request = _StandaloneRequest(
|
||||
self, WebSocketRequestHandler.options.use_tls)
|
||||
self._dispatcher = WebSocketRequestHandler.options.dispatcher
|
||||
self._print_warnings_if_any()
|
||||
self._handshaker = handshake.Handshaker(
|
||||
self._request, self._dispatcher,
|
||||
allowDraft75=WebSocketRequestHandler.options.allow_draft75,
|
||||
strict=WebSocketRequestHandler.options.strict)
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.__init__(
|
||||
self, *args, **keywords)
|
||||
|
||||
def _print_warnings_if_any(self):
|
||||
warnings = self._dispatcher.source_warnings()
|
||||
if warnings:
|
||||
for warning in warnings:
|
||||
logging.warning('mod_pywebsocket: %s' % warning)
|
||||
|
||||
def parse_request(self):
|
||||
"""Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
|
||||
|
||||
Return True to continue processing for HTTP(S), False otherwise.
|
||||
"""
|
||||
result = CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self)
|
||||
if result:
|
||||
try:
|
||||
self._handshaker.do_handshake()
|
||||
try:
|
||||
self._dispatcher.transfer_data(self._request)
|
||||
except Exception, e:
|
||||
# Catch exception in transfer_data.
|
||||
# In this case, handshake has been successful, so just log
|
||||
# the exception and return False.
|
||||
logging.info('mod_pywebsocket: %s' % e)
|
||||
return False
|
||||
except handshake.HandshakeError, e:
|
||||
# Handshake for ws(s) failed. Assume http(s).
|
||||
logging.info('mod_pywebsocket: %s' % e)
|
||||
return True
|
||||
except dispatch.DispatchError, e:
|
||||
logging.warning('mod_pywebsocket: %s' % e)
|
||||
return False
|
||||
except Exception, e:
|
||||
logging.warning('mod_pywebsocket: %s' % e)
|
||||
logging.info('mod_pywebsocket: %s' % util.get_stack_trace())
|
||||
return False
|
||||
return result
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
"""Override BaseHTTPServer.log_request."""
|
||||
|
||||
logging.info('"%s" %s %s',
|
||||
self.requestline, str(code), str(size))
|
||||
|
||||
def log_error(self, *args):
|
||||
"""Override BaseHTTPServer.log_error."""
|
||||
|
||||
# Despite the name, this method is for warnings than for errors.
|
||||
# For example, HTTP status code is logged by this method.
|
||||
logging.warn('%s - %s' % (self.address_string(), (args[0] % args[1:])))
|
||||
|
||||
def is_cgi(self):
|
||||
"""Test whether self.path corresponds to a CGI script.
|
||||
|
||||
Add extra check that self.path doesn't contains ..
|
||||
Also check if the file is a executable file or not.
|
||||
If the file is not executable, it is handled as static file or dir
|
||||
rather than a CGI script.
|
||||
"""
|
||||
if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
|
||||
if '..' in self.path:
|
||||
return False
|
||||
# strip query parameter from request path
|
||||
resource_name = self.path.split('?', 2)[0]
|
||||
# convert resource_name into real path name in filesystem.
|
||||
scriptfile = self.translate_path(resource_name)
|
||||
if not os.path.isfile(scriptfile):
|
||||
return False
|
||||
if not self.is_executable(scriptfile):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _configure_logging(options):
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(_LOG_LEVELS[options.log_level])
|
||||
if options.log_file:
|
||||
handler = logging.handlers.RotatingFileHandler(
|
||||
options.log_file, 'a', options.log_max, options.log_count)
|
||||
else:
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"[%(asctime)s] [%(levelname)s] %(name)s: %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def _alias_handlers(dispatcher, websock_handlers_map_file):
|
||||
"""Set aliases specified in websock_handler_map_file in dispatcher.
|
||||
|
||||
Args:
|
||||
dispatcher: dispatch.Dispatcher instance
|
||||
websock_handler_map_file: alias map file
|
||||
"""
|
||||
fp = open(websock_handlers_map_file)
|
||||
try:
|
||||
for line in fp:
|
||||
if line[0] == '#' or line.isspace():
|
||||
continue
|
||||
m = re.match('(\S+)\s+(\S+)', line)
|
||||
if not m:
|
||||
logging.warning('Wrong format in map file:' + line)
|
||||
continue
|
||||
try:
|
||||
dispatcher.add_resource_path_alias(
|
||||
m.group(1), m.group(2))
|
||||
except dispatch.DispatchError, e:
|
||||
logging.error(str(e))
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
|
||||
def _main():
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-H', '--server-host', '--server_host',
|
||||
dest='server_host',
|
||||
default='',
|
||||
help='server hostname to listen to')
|
||||
parser.add_option('-p', '--port', dest='port', type='int',
|
||||
default=handshake.DEFAULT_WEB_SOCKET_PORT,
|
||||
help='port to listen to')
|
||||
parser.add_option('-w', '--websock-handlers', '--websock_handlers',
|
||||
dest='websock_handlers',
|
||||
default='.',
|
||||
help='Web Socket handlers root directory.')
|
||||
parser.add_option('-m', '--websock-handlers-map-file',
|
||||
'--websock_handlers_map_file',
|
||||
dest='websock_handlers_map_file',
|
||||
default=None,
|
||||
help=('Web Socket handlers map file. '
|
||||
'Each line consists of alias_resource_path and '
|
||||
'existing_resource_path, separated by spaces.'))
|
||||
parser.add_option('-s', '--scan-dir', '--scan_dir', dest='scan_dir',
|
||||
default=None,
|
||||
help=('Web Socket handlers scan directory. '
|
||||
'Must be a directory under websock_handlers.'))
|
||||
parser.add_option('-d', '--document-root', '--document_root',
|
||||
dest='document_root', default='.',
|
||||
help='Document root directory.')
|
||||
parser.add_option('-x', '--cgi-paths', '--cgi_paths', dest='cgi_paths',
|
||||
default=None,
|
||||
help=('CGI paths relative to document_root.'
|
||||
'Comma-separated. (e.g -x /cgi,/htbin) '
|
||||
'Files under document_root/cgi_path are handled '
|
||||
'as CGI programs. Must be executable.'))
|
||||
parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
|
||||
default=False, help='use TLS (wss://)')
|
||||
parser.add_option('-k', '--private-key', '--private_key',
|
||||
dest='private_key',
|
||||
default='', help='TLS private key file.')
|
||||
parser.add_option('-c', '--certificate', dest='certificate',
|
||||
default='', help='TLS certificate file.')
|
||||
parser.add_option('-l', '--log-file', '--log_file', dest='log_file',
|
||||
default='', help='Log file.')
|
||||
parser.add_option('--log-level', '--log_level', type='choice',
|
||||
dest='log_level', default='warn',
|
||||
choices=['debug', 'info', 'warn', 'error', 'critical'],
|
||||
help='Log level.')
|
||||
parser.add_option('--log-max', '--log_max', dest='log_max', type='int',
|
||||
default=_DEFAULT_LOG_MAX_BYTES,
|
||||
help='Log maximum bytes')
|
||||
parser.add_option('--log-count', '--log_count', dest='log_count',
|
||||
type='int', default=_DEFAULT_LOG_BACKUP_COUNT,
|
||||
help='Log backup count')
|
||||
parser.add_option('--allow-draft75', dest='allow_draft75',
|
||||
action='store_true', default=False,
|
||||
help='Allow draft 75 handshake')
|
||||
parser.add_option('--strict', dest='strict', action='store_true',
|
||||
default=False, help='Strictly check handshake request')
|
||||
parser.add_option('-q', '--queue', dest='request_queue_size', type='int',
|
||||
default=_DEFAULT_REQUEST_QUEUE_SIZE,
|
||||
help='request queue size')
|
||||
options = parser.parse_args()[0]
|
||||
|
||||
os.chdir(options.document_root)
|
||||
|
||||
_configure_logging(options)
|
||||
|
||||
SocketServer.TCPServer.request_queue_size = options.request_queue_size
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = []
|
||||
|
||||
if options.cgi_paths:
|
||||
CGIHTTPServer.CGIHTTPRequestHandler.cgi_directories = \
|
||||
options.cgi_paths.split(',')
|
||||
if sys.platform in ('cygwin', 'win32'):
|
||||
cygwin_path = None
|
||||
# For Win32 Python, it is expected that CYGWIN_PATH
|
||||
# is set to a directory of cygwin binaries.
|
||||
# For example, websocket_server.py in Chromium sets CYGWIN_PATH to
|
||||
# full path of third_party/cygwin/bin.
|
||||
if 'CYGWIN_PATH' in os.environ:
|
||||
cygwin_path = os.environ['CYGWIN_PATH']
|
||||
util.wrap_popen3_for_win(cygwin_path)
|
||||
def __check_script(scriptpath):
|
||||
return util.get_script_interp(scriptpath, cygwin_path)
|
||||
CGIHTTPServer.executable = __check_script
|
||||
|
||||
if options.use_tls:
|
||||
if not _HAS_OPEN_SSL:
|
||||
logging.critical('To use TLS, install pyOpenSSL.')
|
||||
sys.exit(1)
|
||||
if not options.private_key or not options.certificate:
|
||||
logging.critical(
|
||||
'To use TLS, specify private_key and certificate.')
|
||||
sys.exit(1)
|
||||
|
||||
if not options.scan_dir:
|
||||
options.scan_dir = options.websock_handlers
|
||||
|
||||
try:
|
||||
# Share a Dispatcher among request handlers to save time for
|
||||
# instantiation. Dispatcher can be shared because it is thread-safe.
|
||||
options.dispatcher = dispatch.Dispatcher(options.websock_handlers,
|
||||
options.scan_dir)
|
||||
if options.websock_handlers_map_file:
|
||||
_alias_handlers(options.dispatcher,
|
||||
options.websock_handlers_map_file)
|
||||
_print_warnings_if_any(options.dispatcher)
|
||||
|
||||
WebSocketRequestHandler.options = options
|
||||
WebSocketServer.options = options
|
||||
|
||||
server = WebSocketServer((options.server_host, options.port),
|
||||
WebSocketRequestHandler)
|
||||
server.serve_forever()
|
||||
except Exception, e:
|
||||
logging.critical(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main()
|
||||
|
||||
|
||||
# vi:sts=4 sw=4 et
|
@ -265,6 +265,8 @@ See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logg
|
||||
options.webServer = self._automation.DEFAULT_WEB_SERVER
|
||||
options.httpPort = self._automation.DEFAULT_HTTP_PORT
|
||||
options.sslPort = self._automation.DEFAULT_SSL_PORT
|
||||
options.webSocketPort = self._automation.DEFAULT_WEBSOCKET_PORT
|
||||
options.webSocketProxyPort = self._automation.DEFAULT_WEBSOCKET_PROXY_PORT
|
||||
|
||||
if options.vmwareRecording:
|
||||
if not self._automation.IS_WIN32:
|
||||
@ -343,6 +345,27 @@ class MochitestServer:
|
||||
except:
|
||||
self._process.kill()
|
||||
|
||||
class WebSocketServer(object):
|
||||
"Class which encapsulates the mod_pywebsocket server"
|
||||
|
||||
def __init__(self, automation, options, scriptdir):
|
||||
self.port = options.webSocketPort
|
||||
self._automation = automation
|
||||
self._scriptdir = scriptdir
|
||||
|
||||
def start(self):
|
||||
script = os.path.join(self._scriptdir, 'pywebsocket/standalone.py')
|
||||
cmd = [sys.executable, script, '-p', str(self.port), '-w', self._scriptdir, '-l', os.path.join(self._scriptdir, "websock.log"), '--log-level=debug']
|
||||
|
||||
self._process = self._automation.Process(cmd)
|
||||
pid = self._process.pid
|
||||
if pid < 0:
|
||||
print "Error starting websocket server."
|
||||
sys.exit(2)
|
||||
self._automation.log.info("INFO | runtests.py | Websocket server pid: %d", pid)
|
||||
|
||||
def stop(self):
|
||||
self._process.kill()
|
||||
|
||||
class Mochitest(object):
|
||||
# Path to the test script on the server
|
||||
@ -389,6 +412,20 @@ class Mochitest(object):
|
||||
testURL = "about:blank"
|
||||
return testURL
|
||||
|
||||
def startWebSocketServer(self, options):
|
||||
""" Launch the websocket server """
|
||||
if options.webServer != '127.0.0.1':
|
||||
return
|
||||
|
||||
self.wsserver = WebSocketServer(self.automation, options, self.SCRIPT_DIRECTORY)
|
||||
self.wsserver.start()
|
||||
|
||||
def stopWebSocketServer(self, options):
|
||||
if options.webServer != '127.0.0.1':
|
||||
return
|
||||
|
||||
self.wsserver.stop()
|
||||
|
||||
def startWebServer(self, options):
|
||||
if options.webServer != '127.0.0.1':
|
||||
return
|
||||
@ -547,7 +584,7 @@ class Mochitest(object):
|
||||
|
||||
manifest = self.buildProfile(options)
|
||||
self.startWebServer(options)
|
||||
|
||||
self.startWebSocketServer(options)
|
||||
|
||||
testURL = self.buildTestPath(options)
|
||||
self.buildURLOptions(options)
|
||||
@ -587,6 +624,7 @@ class Mochitest(object):
|
||||
self.stopVMwareRecording();
|
||||
|
||||
self.stopWebServer(options)
|
||||
self.stopWebSocketServer(options)
|
||||
processLeakLog(self.leak_report_file, options.leakThreshold)
|
||||
self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
|
||||
|
||||
@ -690,7 +728,11 @@ def main():
|
||||
if options.symbolsPath and not isURL(options.symbolsPath):
|
||||
options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
|
||||
|
||||
automation.setServerInfo(options.webServer, options.httpPort, options.sslPort)
|
||||
automation.setServerInfo(options.webServer,
|
||||
options.httpPort,
|
||||
options.sslPort,
|
||||
options.webSocketPort,
|
||||
options.webSocketProxyPort)
|
||||
sys.exit(mochitest.runTests(options))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -121,6 +121,14 @@ typedef struct {
|
||||
string cert_nickname;
|
||||
PLHashTable* host_cert_table;
|
||||
PLHashTable* host_clientauth_table;
|
||||
// If not empty, and this server is using HTTP CONNECT, connections
|
||||
// will be proxied to this address.
|
||||
PRNetAddr remote_addr;
|
||||
// True if no SSL should be used for this server's connections.
|
||||
bool http_proxy_only;
|
||||
// The original host in the Host: header for the initial connection is
|
||||
// stored here, for proxied connections.
|
||||
string original_host;
|
||||
} server_info_t;
|
||||
|
||||
typedef struct {
|
||||
@ -132,6 +140,7 @@ typedef struct {
|
||||
const PRInt32 BUF_SIZE = 16384;
|
||||
const PRInt32 BUF_MARGIN = 1024;
|
||||
const PRInt32 BUF_TOTAL = BUF_SIZE + BUF_MARGIN;
|
||||
const char HEADER_HOST[] = "Host:";
|
||||
|
||||
struct relayBuffer
|
||||
{
|
||||
@ -254,7 +263,7 @@ bool ReadConnectRequest(server_info_t* server_info,
|
||||
*(buffer.buffertail-1));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
printf(" parsing initial connect request, dump:\n%.*s\n", (int)buffer.present(), buffer.bufferhead);
|
||||
|
||||
*result = 400;
|
||||
@ -341,6 +350,100 @@ bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, string &cer
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function examines the buffer for a S5ec-WebSocket-Location: field,
|
||||
* and if it's present, it replaces the hostname in that field with the
|
||||
* value in the server's original_host field. This function works
|
||||
* in the reverse direction as AdjustHost(), replacing the real hostname
|
||||
* of a response with the potentially fake hostname that is expected
|
||||
* by the browser (e.g., mochi.test).
|
||||
*
|
||||
* @return true if the header was adjusted successfully, or not found, false
|
||||
* if the header is present but the url is not, which should indicate
|
||||
* that more data needs to be read from the socket
|
||||
*/
|
||||
bool AdjustWebSocketLocation(relayBuffer& buffer, server_info_t *si)
|
||||
{
|
||||
assert(buffer.margin());
|
||||
buffer.buffertail[1] = '\0';
|
||||
|
||||
char* wsloc = strstr(buffer.bufferhead, "Sec-WebSocket-Location:");
|
||||
if (!wsloc)
|
||||
return true;
|
||||
// advance pointer to the start of the hostname
|
||||
wsloc = strstr(wsloc, "ws://");
|
||||
if (!wsloc)
|
||||
return false;
|
||||
wsloc += 5;
|
||||
// find the end of the hostname
|
||||
char* wslocend = strchr(wsloc + 1, '/');
|
||||
if (!wslocend)
|
||||
return false;
|
||||
char *crlf = strstr(wsloc, "\r\n");
|
||||
if (!crlf)
|
||||
return false;
|
||||
if (si->original_host.empty())
|
||||
return true;
|
||||
|
||||
int diff = si->original_host.length() - (wslocend-wsloc);
|
||||
if (diff > 0)
|
||||
assert(size_t(diff) <= buffer.margin());
|
||||
memmove(wslocend + diff, wslocend, buffer.buffertail - wsloc - diff);
|
||||
buffer.buffertail += diff;
|
||||
|
||||
memcpy(wsloc, si->original_host.c_str(), si->original_host.length());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function examines the buffer for a Host: field, and if it's present,
|
||||
* it replaces the hostname in that field with the hostname in the server's
|
||||
* remote_addr field. This is needed because proxy requests may be coming
|
||||
* from mochitest with fake hosts, like mochi.test, and these need to be
|
||||
* replaced with the host that the destination server is actually running
|
||||
* on.
|
||||
*/
|
||||
bool AdjustHost(relayBuffer& buffer, server_info_t *si)
|
||||
{
|
||||
if (!si->remote_addr.inet.port)
|
||||
return false;
|
||||
|
||||
assert(buffer.margin());
|
||||
|
||||
// Cannot use strnchr so add a null char at the end. There is always some
|
||||
// space left because we preserve a margin.
|
||||
buffer.buffertail[1] = '\0';
|
||||
|
||||
char* host = strstr(buffer.bufferhead, HEADER_HOST);
|
||||
if (!host)
|
||||
return false;
|
||||
// advance pointer to beginning of hostname
|
||||
host += strlen(HEADER_HOST);
|
||||
host += strspn(host, " \t");
|
||||
|
||||
char* endhost = strstr(host, "\r\n");
|
||||
if (!endhost)
|
||||
return false;
|
||||
|
||||
// Save the original host, so we can use it later on responses from the
|
||||
// server.
|
||||
si->original_host.assign(host, endhost-host);
|
||||
|
||||
char newhost[40];
|
||||
PR_NetAddrToString(&si->remote_addr, newhost, sizeof(newhost));
|
||||
assert(strlen(newhost) < sizeof(newhost) - 7);
|
||||
sprintf(newhost, "%s:%d", newhost, PR_ntohs(si->remote_addr.inet.port));
|
||||
|
||||
int diff = strlen(newhost) - (endhost-host);
|
||||
if (diff > 0)
|
||||
assert(size_t(diff) <= buffer.margin());
|
||||
memmove(endhost + diff, endhost, buffer.buffertail - host - diff);
|
||||
buffer.buffertail += diff;
|
||||
|
||||
memcpy(host, newhost, strlen(newhost));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function prefixes Request-URI path with a full scheme-host-port
|
||||
* string.
|
||||
@ -429,7 +532,8 @@ void HandleConnection(void* data)
|
||||
|
||||
if (!do_http_proxy)
|
||||
{
|
||||
if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone))
|
||||
if (!ci->server_info->http_proxy_only &&
|
||||
!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone))
|
||||
client_error = true;
|
||||
else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
|
||||
client_error = true;
|
||||
@ -566,7 +670,10 @@ void HandleConnection(void* data)
|
||||
strcpy(buffers[s2].buffer, "HTTP/1.1 200 Connected\r\nConnection: keep-alive\r\n\r\n");
|
||||
buffers[s2].buffertail = buffers[s2].buffer + strlen(buffers[s2].buffer);
|
||||
|
||||
if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
|
||||
PRNetAddr* addr = &remote_addr;
|
||||
if (ci->server_info->remote_addr.inet.port > 0)
|
||||
addr = &ci->server_info->remote_addr;
|
||||
if (!ConnectSocket(other_sock, addr, connect_timeout))
|
||||
{
|
||||
printf(" could not open connection to the real server\n");
|
||||
client_error = true;
|
||||
@ -587,8 +694,18 @@ void HandleConnection(void* data)
|
||||
|
||||
if (ssl_updated)
|
||||
{
|
||||
if (s == 0 && expect_request_start)
|
||||
expect_request_start = !AdjustRequestURI(buffers[s], &fullHost);
|
||||
if (s == 0 && expect_request_start)
|
||||
{
|
||||
if (ci->server_info->http_proxy_only)
|
||||
expect_request_start = !AdjustHost(buffers[s], ci->server_info);
|
||||
else
|
||||
expect_request_start = !AdjustRequestURI(buffers[s], &fullHost);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!AdjustWebSocketLocation(buffers[s], ci->server_info))
|
||||
continue;
|
||||
}
|
||||
|
||||
in_flags2 |= PR_POLL_WRITE;
|
||||
printf(" telling the other socket to write");
|
||||
@ -619,7 +736,7 @@ void HandleConnection(void* data)
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(", writen %d bytes", bytesWrite);
|
||||
printf(", written %d bytes", bytesWrite);
|
||||
buffers[s2].buffertail[1] = '\0';
|
||||
printf(" dump:\n%.*s\n", bytesWrite, buffers[s2].bufferhead);
|
||||
|
||||
@ -636,7 +753,8 @@ void HandleConnection(void* data)
|
||||
printf(" proxy response sent to the client");
|
||||
// Proxy response has just been writen, update to ssl
|
||||
ssl_updated = true;
|
||||
if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, clientAuth))
|
||||
if (!ci->server_info->http_proxy_only &&
|
||||
!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, clientAuth))
|
||||
{
|
||||
printf(" but failed to config server socket\n");
|
||||
client_error = true;
|
||||
@ -793,6 +911,35 @@ int processConfigLine(char* configLine)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!strcmp(keyword, "proxy"))
|
||||
{
|
||||
server_info_t server;
|
||||
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
|
||||
server.host_clientauth_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, ClientAuthValueComparator, NULL, NULL);
|
||||
server.http_proxy_only = true;
|
||||
|
||||
char* listenport = strtok2(_caret, ":", &_caret);
|
||||
server.listen_port = atoi(listenport);
|
||||
if (server.listen_port <= 0) {
|
||||
fprintf(stderr, "Invalid listen port in proxy config: %s\n", listenport);
|
||||
return 1;
|
||||
}
|
||||
char* ipstring = strtok2(_caret, ":", &_caret);
|
||||
if (PR_StringToNetAddr(ipstring, &server.remote_addr) != PR_SUCCESS) {
|
||||
fprintf(stderr, "Invalid IP address in proxy config: %s\n", ipstring);
|
||||
return 1;
|
||||
}
|
||||
char* remoteport = strtok2(_caret, ":", &_caret);
|
||||
int port = atoi(remoteport);
|
||||
if (port <= 0) {
|
||||
fprintf(stderr, "Invalid remote port in proxy config: %s\n", remoteport);
|
||||
return 1;
|
||||
}
|
||||
server.remote_addr.inet.port = PR_htons(port);
|
||||
servers.push_back(server);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Configure all listen sockets and port+certificate bindings
|
||||
if (!strcmp(keyword, "listen"))
|
||||
{
|
||||
@ -832,8 +979,10 @@ int processConfigLine(char* configLine)
|
||||
else
|
||||
{
|
||||
server_info_t server;
|
||||
memset(&server.remote_addr, 0, sizeof(PRNetAddr));
|
||||
server.cert_nickname = certnick;
|
||||
server.listen_port = port;
|
||||
server.http_proxy_only = false;
|
||||
server.host_cert_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, PL_CompareStrings, NULL, NULL);
|
||||
if (!server.host_cert_table)
|
||||
{
|
||||
@ -1013,7 +1162,11 @@ int main(int argc, char** argv)
|
||||
" # in httpproxy mode and only after the 'listen' option has been\n"
|
||||
" # specified. You also have to specify the tunnel listen port.\n"
|
||||
" clientauth:requesting-client-cert.host.com:443:4443:request\n"
|
||||
" clientauth:requiring-client-cert.host.com:443:4443:require\n",
|
||||
" clientauth:requiring-client-cert.host.com:443:4443:require\n"
|
||||
" # Act as a simple proxy for incoming connections on port 7777,\n"
|
||||
" # tunneling them to the server at 127.0.0.1:9999. Not affected\n"
|
||||
" # by the 'forward' option.\n"
|
||||
" proxy:7777:127.0.0.1:9999\n",
|
||||
configFilePath);
|
||||
return 1;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user