32 Commits

Author SHA1 Message Date
Solly Ross 69c04c819c Update to 0.7.0
See CHANGES.txt for details.
2015-07-23 21:02:37 -04:00
Solly 558a6439f1 Merge pull request #176 from crodjer/venv-with-run
Make `run` respect virtualenv configuration
2015-06-02 15:39:03 -04:00
Rohan Jain 2b9e0a5794 Make run respect virtualenv configuration
Signed-off-by: Rohan Jain <crodjer@gmail.com>
2015-05-15 16:54:38 +05:30
Solly 3371090909 Merge pull request #175 from kanaka/bug/catch-interrupted-syscall
Catch interrupted system call
2015-05-13 16:52:31 -04:00
Bennett Kanuka 1f960d9f3c Catch interrupted system call
Closes #166
2015-05-13 16:15:25 -04:00
Solly Ross 1221960baa Introduce strict mode
This commit introduces strict mode, which is on by default.  Currently
strict mode only enforces client-to-server frame masking.  However,
in the future, it might enforce other parts of the RFC as well.

Closes #164
2015-05-13 16:03:57 -04:00
Solly Ross 2e57ee3159 Enable Travis
This commit enables running the unit tests on Travis for Python 2.6,
2.7, 3.3, and 3.4.  Note that Travis does not support Python 2.4, so
we cannot test there.
2015-05-13 16:03:57 -04:00
Solly Ross 04fd789a67 Update Tests and Test Plugins
This commit updates the unit tests to work with the current code
and adds in tests for the auth and token plugin functionality.
2015-05-13 16:03:57 -04:00
Solly Ross d5216c3166 Process plugin parameters in main
Previously, we just passed the values of '--*-plugin' and
'--*-source' directly to `LibProxyServer` and `WebSocketProxy`,
which handled turning that into an instance of the plugin class.
Now, that's done in main, and the classes receive an instance
directly.
2015-05-13 16:03:56 -04:00
Solly Ross 11f9a6cf10 Fix bug in ReadOnlyTokenFile
ReadOnlyTokenFile didn't initialize `self._targets` to `None`,
causing issues.
2015-05-13 16:03:37 -04:00
Solly Ross df10501615 Introduce Auth Plugins
Auth plugins provide a generic interface for authenticating requests.

The plugin name is specified using the '--auth-plugin' option, and
may either be the name of a class from `websockify.auth_plugins`,
or a fully qualified python path to the auth plugin class (see below).

An optional plugin parameter can be specified using the '--auth-source'
option (a value of `None` will be used if no '--auth-source' option is
specified).

Auth plugins should inherit from `websockify.auth_plugins.BasePlugin`,
and should implement the `authenticate(headers, target_host, target_port)`
method.  The value of the '--auth-source' option is available as
`self.source`.

One plugin is currently included: `ExpectOrigin`.  The `ExpectOrigin`
plugin checks that the 'Origin' header is an expected value.  The list
of acceptable origins is passed using the plugin source, as a
space-separated list.
2015-05-13 16:03:37 -04:00
Solly Ross d94b944f93 Restore functionaltiy to file_only.
There was a bug in WebSocketServer that prevented the `file_only`
parameter from actually being set as an instance property, causing
directory listings to appear even with `file_only=True`.  This
commit fixes that.

See-Also: https://bugs.launchpad.net/nova/+bug/1447675
2015-05-13 16:03:37 -04:00
Aric Stewart 52adf957b3 Add ping heartbeat option
With this option we will ping the client with a heartbeat.
Helpful for connection through firewalls that may disable inactive
connections.
2015-04-14 16:09:26 -05:00
Aric Stewart 731fd20796 Add option to turn on auto-pong
This allows the websockify server to respond to heart beat ping
messages sent by clients
2015-04-13 14:57:24 -05:00
Solly Ross 52c2e62535 Fix bug in token_plugin/target_cfg support
Previously, if no `target_cfg` flag was used, the `target_cfg` option
would not get removed from the opts dict, causing an error when it got
passed through to `WebSocketProxy`.  Now we always remove it.

Fixes #168.
2015-04-09 11:43:20 -04:00
Solly ac9d357c87 Merge pull request #161 from rafaelfolco/bug/150-websocket-bigendian
BUGFIX:  Websocket frame corruption on big-endian #150
2015-04-07 12:51:52 -04:00
Solly ce07749223 Merge pull request #162 from kanaka/feature/token-plugins
Introduce Token Plugins
2015-04-07 12:51:02 -04:00
Rafael Folco af10f458a1 BUGFIX: Websocket frame corruption on big-endian #150
This patch fixes noVNC endianess problem on handshake.
It affects noVNC sessions on Big Endian platforms.

Fixes #150
2015-04-07 09:36:21 -03:00
Solly Ross 69a8b928aa Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.

The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).

An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).

Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method.  The value of the
'--token-source' option is available as `self.source`.

Several plugins are included by default.  The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time).  The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden.  The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.

The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.

Also-Authored-By: James Portman (@james-portman)

Closes #157
2015-03-26 16:21:25 -04:00
Solly 19b558e4cd Merge pull request #159 from vaibhavsagar/python3-trove
Add Python 3 trove classifiers.
2015-03-17 10:32:40 -04:00
Vaibhav Sagar dcdf0a0b6c Add Python 3 trove classifiers.
This makes <http://py3readiness.org> display the status of this package correctly :).
2015-03-17 07:20:26 +11:00
Solly 23045cb212 Merge pull request #155 from kanaka/bug/154-no-implicit-relative-imports
Don't use implicit relative imports
2015-02-06 11:29:57 -05:00
Solly Ross acd276e1a2 Don't use implicit relative imports
Implicit relative imports don't work in Python 3.  This converts
implicit relative imports into absolute imports
(e.g. `import websocket` becomes `from websockify import websocket`).

Fixes #154
2015-02-03 16:52:29 -05:00
Solly 999a133bc5 Merge pull request #128 from ags131/patch-1
Added 'websockify.js' to package.json files list
2015-01-05 12:15:56 -05:00
Joel Martin f36877c684 Merge pull request #140 from rpodgorny/python3
python3 compatibility fixes
2014-10-02 10:02:11 -05:00
Radek Podgorny 303a71310c python3 compatibility fixes 2014-10-02 11:42:17 +02:00
samhed 63209f84ca sync with commit from PR #347 in noVNC 2014-08-19 14:13:00 +02:00
Joel Martin d37e729587 Merge pull request #129 from mmb/master
Fix typo in reaping zombies message.
2014-05-20 18:01:00 -05:00
Matthew M. Boedicker 12191d266b Fix typo in reaping zombies message. 2014-05-20 14:43:35 -07:00
ags131 67a12a2394 Fixed Package.json
Added websockify.js to the files section, this fixes #90 and allows NPM to properly install
2014-05-16 11:56:24 -05:00
samhed 6c526fd645 remove some trailing whitespaces 2014-04-14 14:20:00 +02:00
samhed 6c1a2e9032 Avoid using the %-operator, URL-escaped strings can contain extra %'s which can cause bugs. 2014-03-18 15:21:13 +01:00
16 changed files with 800 additions and 344 deletions
+2
View File
@@ -9,3 +9,5 @@ other/node_modules
.pydevproject .pydevproject
target.cfg target.cfg
target.cfg.d target.cfg.d
.tox
*.egg-info
+10
View File
@@ -0,0 +1,10 @@
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
install: pip install -r test-requirements.txt
script: python setup.py nosetests --verbosity=3
+12
View File
@@ -1,6 +1,18 @@
Changes Changes
======= =======
0.7.0
-----
* Python 3 support fixes (#140, #155, #159)
* Generic token-parsing plugins support (#162)
* Generic authentication plugins support (#172)
* Fixed frame corruption on big-endian systems (#161)
* Support heartbeats (via PING) and automatic responses to PONG (#169)
* Automatically reject unmasked client frames by default (strict mode) (#174)
* Automatically restart interrupted select calls (#175)
* Make 'run' respect environment settings (including virtualenv) (#176)
0.6.1 - May 11, 2015 0.6.1 - May 11, 2015
-------------------- --------------------
+9 -7
View File
@@ -262,7 +262,7 @@ function on(evt, handler) {
eventHandlers[evt] = handler; eventHandlers[evt] = handler;
} }
function init(protocols) { function init(protocols, ws_schema) {
rQ = []; rQ = [];
rQi = 0; rQi = 0;
sQ = []; sQ = [];
@@ -277,12 +277,13 @@ function init(protocols) {
('set' in Uint8Array.prototype)) { ('set' in Uint8Array.prototype)) {
bt = true; bt = true;
} }
// Check for full binary type support in WebSocket
// Check for full binary type support in WebSockets // Inspired by:
// TODO: this sucks, the property should exist on the prototype // https://github.com/Modernizr/Modernizr/issues/370
// but it does not. // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
try { try {
if (bt && ('binaryType' in (new WebSocket("ws://localhost:17523")))) { if (bt && ('binaryType' in WebSocket.prototype ||
!!(new WebSocket(ws_schema + '://.').binaryType))) {
Util.Info("Detected binaryType support in WebSockets"); Util.Info("Detected binaryType support in WebSockets");
wsbt = true; wsbt = true;
} }
@@ -325,7 +326,8 @@ function init(protocols) {
} }
function open(uri, protocols) { function open(uri, protocols) {
protocols = init(protocols); var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
protocols = init(protocols, ws_schema);
if (test_mode) { if (test_mode) {
websocket = {}; websocket = {};
+2 -2
View File
@@ -3,12 +3,12 @@
"name": "websockify", "name": "websockify",
"description": "websockify is a WebSocket-to-TCP proxy/bridge", "description": "websockify is a WebSocket-to-TCP proxy/bridge",
"license": "LGPL-3", "license": "LGPL-3",
"version": "0.6.1", "version": "0.7.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/kanaka/websockify.git" "url": "git://github.com/kanaka/websockify.git"
}, },
"files": ["../../docs/LICENSE.LGPL-3"], "files": ["../../docs/LICENSE.LGPL-3","websockify.js"],
"bin": { "bin": {
"websockify": "./websockify.js" "websockify": "./websockify.js"
}, },
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/env python
import websockify import websockify
+7 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '0.6.1' version = '0.7.0'
name = 'websockify' name = 'websockify'
long_description = open("README.md").read() + "\n" + \ long_description = open("README.md").read() + "\n" + \
open("CHANGES.txt").read() + "\n" open("CHANGES.txt").read() + "\n"
@@ -11,6 +11,12 @@ setup(name=name,
long_description=long_description, long_description=long_description,
classifiers=[ classifiers=[
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4"
], ],
data_files=[('share/websockify/include', data_files=[('share/websockify/include',
['include/util.js', ['include/util.js',
+2
View File
@@ -0,0 +1,2 @@
mox
nose
+338 -151
View File
@@ -26,201 +26,303 @@ import stubout
import sys import sys
import tempfile import tempfile
import unittest import unittest
from ssl import SSLError import socket
from websockify import websocket as websocket import signal
from SimpleHTTPServer import SimpleHTTPRequestHandler from websockify import websocket
try:
from SimpleHTTPServer import SimpleHTTPRequestHandler
except ImportError:
from http.server import SimpleHTTPRequestHandler
try:
from StringIO import StringIO
BytesIO = StringIO
except ImportError:
from io import StringIO
from io import BytesIO
class MockConnection(object):
def __init__(self, path):
self.path = path
def makefile(self, mode='r', bufsize=-1):
return open(self.path, mode, bufsize)
class WebSocketTestCase(unittest.TestCase): def raise_oserror(*args, **kwargs):
raise OSError('fake error')
def _init_logger(self, tmpdir):
name = 'websocket-unittest'
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.propagate = True
filename = "%s.log" % (name)
handler = logging.FileHandler(filename)
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)
class FakeSocket(object):
def __init__(self, data=''):
if isinstance(data, bytes):
self._data = data
else:
self._data = data.encode('latin_1')
def recv(self, amt, flags=None):
res = self._data[0:amt]
if not (flags & socket.MSG_PEEK):
self._data = self._data[amt:]
return res
def makefile(self, mode='r', buffsize=None):
if 'b' in mode:
return BytesIO(self._data)
else:
return StringIO(self._data.decode('latin_1'))
class WebSocketRequestHandlerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
"""Called automatically before each test.""" super(WebSocketRequestHandlerTestCase, self).setUp()
super(WebSocketTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
# Temporary dir for test data self.tmpdir = tempfile.mkdtemp('-websockify-tests')
self.tmpdir = tempfile.mkdtemp()
# Put log somewhere persistent
self._init_logger('./')
# Mock this out cause it screws tests up # Mock this out cause it screws tests up
self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None) self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None)
self.server = self._get_websockserver(daemon=True, self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
ssl_only=False) lambda *args, **kwargs: None)
self.soc = self.server.socket('localhost')
def tearDown(self): def tearDown(self):
"""Called automatically after each test.""" """Called automatically after each test."""
self.stubs.UnsetAll() self.stubs.UnsetAll()
shutil.rmtree(self.tmpdir) os.rmdir(self.tmpdir)
super(WebSocketTestCase, self).tearDown() super(WebSocketRequestHandlerTestCase, self).tearDown()
def _get_websockserver(self, **kwargs): def _get_server(self, handler_class=websocket.WebSocketRequestHandler,
return websocket.WebSocketServer(listen_host='localhost', **kwargs):
listen_port=80, web = kwargs.pop('web', self.tmpdir)
key=self.tmpdir, return websocket.WebSocketServer(
web=self.tmpdir, handler_class, listen_host='localhost',
record=self.tmpdir, listen_port=80, key=self.tmpdir, web=web,
**kwargs) record=self.tmpdir, daemon=False, ssl_only=0, idle_timeout=1,
**kwargs)
def _mock_os_open_oserror(self, file, flags): def test_normal_get_with_only_upgrade_returns_error(self):
raise OSError('') server = self._get_server(web=None)
handler = websocket.WebSocketRequestHandler(
FakeSocket('GET /tmp.txt HTTP/1.1'), '127.0.0.1', server)
def _mock_os_close_oserror(self, fd): def fake_send_response(self, code, message=None):
raise OSError('') self.last_code = code
def _mock_os_close_oserror_EBADF(self, fd): self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
raise OSError(errno.EBADF, '') fake_send_response)
def _mock_socket(self, *args, **kwargs): handler.do_GET()
return self.soc self.assertEqual(handler.last_code, 405)
def _mock_select(self, rlist, wlist, xlist, timeout=None): def test_list_dir_with_file_only_returns_error(self):
return '_mock_select' server = self._get_server(file_only=True)
handler = websocket.WebSocketRequestHandler(
FakeSocket('GET / HTTP/1.1'), '127.0.0.1', server)
def _mock_select_exception(self, rlist, wlist, xlist, timeout=None): def fake_send_response(self, code, message=None):
raise Exception self.last_code = code
def _mock_select_keyboardinterrupt(self, rlist, wlist, self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
xlist, timeout=None): fake_send_response)
raise KeyboardInterrupt
def _mock_select_systemexit(self, rlist, wlist, xlist, timeout=None): handler.path = '/'
sys.exit() handler.do_GET()
self.assertEqual(handler.last_code, 404)
def test_daemonize_error(self):
soc = self._get_websockserver(daemon=True, ssl_only=1, idle_timeout=1) class WebSocketServerTestCase(unittest.TestCase):
self.stubs.Set(os, 'fork', lambda *args: None) def setUp(self):
super(WebSocketServerTestCase, self).setUp()
self.stubs = stubout.StubOutForTesting()
self.tmpdir = tempfile.mkdtemp('-websockify-tests')
# Mock this out cause it screws tests up
self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None)
def tearDown(self):
"""Called automatically after each test."""
self.stubs.UnsetAll()
os.rmdir(self.tmpdir)
super(WebSocketServerTestCase, self).tearDown()
def _get_server(self, handler_class=websocket.WebSocketRequestHandler,
**kwargs):
return websocket.WebSocketServer(
handler_class, listen_host='localhost',
listen_port=80, key=self.tmpdir, web=self.tmpdir,
record=self.tmpdir, **kwargs)
def test_daemonize_raises_error_while_closing_fds(self):
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
self.stubs.Set(os, 'fork', lambda *args: 0)
self.stubs.Set(signal, 'signal', lambda *args: None)
self.stubs.Set(os, 'setsid', lambda *args: None) self.stubs.Set(os, 'setsid', lambda *args: None)
self.stubs.Set(os, 'close', self._mock_os_close_oserror) self.stubs.Set(os, 'close', raise_oserror)
self.assertRaises(OSError, soc.daemonize, keepfd=None, chdir='./') self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./')
def test_daemonize_EBADF_error(self): def test_daemonize_ignores_ebadf_error_while_closing_fds(self):
soc = self._get_websockserver(daemon=True, ssl_only=1, idle_timeout=1) def raise_oserror_ebadf(fd):
self.stubs.Set(os, 'fork', lambda *args: None) raise OSError(errno.EBADF, 'fake error')
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
self.stubs.Set(os, 'fork', lambda *args: 0)
self.stubs.Set(os, 'setsid', lambda *args: None) self.stubs.Set(os, 'setsid', lambda *args: None)
self.stubs.Set(os, 'close', self._mock_os_close_oserror_EBADF) self.stubs.Set(signal, 'signal', lambda *args: None)
self.stubs.Set(os, 'open', self._mock_os_open_oserror) self.stubs.Set(os, 'close', raise_oserror_ebadf)
self.assertRaises(OSError, soc.daemonize, keepfd=None, chdir='./') self.stubs.Set(os, 'open', raise_oserror)
self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./')
def test_decode_hybi(self): def test_handshake_fails_on_not_ready(self):
soc = self._get_websockserver(daemon=False, ssl_only=1, idle_timeout=1) server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
self.assertRaises(Exception, soc.decode_hybi, 'a' * 128,
base64=True)
def test_do_websocket_handshake(self): def fake_select(rlist, wlist, xlist, timeout=None):
soc = self._get_websockserver(daemon=True, ssl_only=0, idle_timeout=1) return ([], [], [])
soc.scheme = 'scheme'
headers = {'Sec-WebSocket-Protocol': 'binary',
'Sec-WebSocket-Version': '7',
'Sec-WebSocket-Key': 'foo'}
soc.do_websocket_handshake(headers, '127.0.0.1')
def test_do_handshake(self): self.stubs.Set(select, 'select', fake_select)
soc = self._get_websockserver(daemon=True, ssl_only=0, idle_timeout=1) self.assertRaises(
self.stubs.Set(select, 'select', self._mock_select) websocket.WebSocketServer.EClose, server.do_handshake,
self.stubs.Set(socket._socketobject, 'recv', lambda *args: 'mock_recv') FakeSocket(), '127.0.0.1')
self.assertRaises(Exception, soc.do_handshake, self.soc, '127.0.0.1')
def test_do_handshake_ssl_error(self): def test_empty_handshake_fails(self):
soc = self._get_websockserver(daemon=True, ssl_only=0, idle_timeout=1) server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
def _mock_wrap_socket(*args, **kwargs): sock = FakeSocket('')
from ssl import SSLError
raise SSLError('unit test exception')
self.stubs.Set(select, 'select', self._mock_select) def fake_select(rlist, wlist, xlist, timeout=None):
self.stubs.Set(socket._socketobject, 'recv', lambda *args: '\x16') return ([sock], [], [])
self.stubs.Set(ssl, 'wrap_socket', _mock_wrap_socket)
self.assertRaises(SSLError, soc.do_handshake, self.soc, '127.0.0.1')
def test_fallback_SIGCHILD(self): self.stubs.Set(select, 'select', fake_select)
soc = self._get_websockserver(daemon=True, ssl_only=0, idle_timeout=1) self.assertRaises(
soc.fallback_SIGCHLD(None, None) websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_start_server_Exception(self): def test_handshake_policy_request(self):
soc = self._get_websockserver(daemon=False, ssl_only=1, idle_timeout=1) # TODO(directxman12): implement
self.stubs.Set(websocket.WebSocketServer, 'socket', self._mock_socket) pass
def test_handshake_ssl_only_without_ssl_raises_error(self):
server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1)
sock = FakeSocket('some initial data')
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_do_handshake_no_ssl(self):
class FakeHandler(object):
CALLED = False
def __init__(self, *args, **kwargs):
type(self).CALLED = True
FakeHandler.CALLED = False
server = self._get_server(
handler_class=FakeHandler, daemon=True,
ssl_only=0, idle_timeout=1)
sock = FakeSocket('some initial data')
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertEqual(server.do_handshake(sock, '127.0.0.1'), sock)
self.assertTrue(FakeHandler.CALLED, True)
def test_do_handshake_ssl(self):
# TODO(directxman12): implement this
pass
def test_do_handshake_ssl_without_ssl_raises_error(self):
# TODO(directxman12): implement this
pass
def test_do_handshake_ssl_without_cert_raises_error(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1,
cert='afdsfasdafdsafdsafdsafdas')
sock = FakeSocket("\x16some ssl data")
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
self.stubs.Set(select, 'select', fake_select)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_do_handshake_ssl_error_eof_raises_close_error(self):
server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1)
sock = FakeSocket("\x16some ssl data")
def fake_select(rlist, wlist, xlist, timeout=None):
return ([sock], [], [])
def fake_wrap_socket(*args, **kwargs):
raise ssl.SSLError(ssl.SSL_ERROR_EOF)
self.stubs.Set(select, 'select', fake_select)
self.stubs.Set(ssl, 'wrap_socket', fake_wrap_socket)
self.assertRaises(
websocket.WebSocketServer.EClose, server.do_handshake,
sock, '127.0.0.1')
def test_fallback_sigchld_handler(self):
# TODO(directxman12): implement this
pass
def test_start_server_error(self):
server = self._get_server(daemon=False, ssl_only=1, idle_timeout=1)
sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
raise Exception("fake error")
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize', self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None) lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', self._mock_select_exception) self.stubs.Set(select, 'select', fake_select)
self.assertEqual(None, soc.start_server()) server.start_server()
def test_start_server_KeyboardInterrupt(self): def test_start_server_keyboardinterrupt(self):
soc = self._get_websockserver(daemon=False, ssl_only=1, idle_timeout=1) server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
self.stubs.Set(websocket.WebSocketServer, 'socket', self._mock_socket) sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
raise KeyboardInterrupt
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize', self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None) lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', self._mock_select_keyboardinterrupt) self.stubs.Set(select, 'select', fake_select)
self.assertEqual(None, soc.start_server()) server.start_server()
def test_start_server_systemexit(self): def test_start_server_systemexit(self):
websocket.ssl = None server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
self.stubs.Set(websocket.WebSocketServer, 'socket', self._mock_socket) sock = server.socket('localhost')
def fake_select(rlist, wlist, xlist, timeout=None):
sys.exit()
self.stubs.Set(websocket.WebSocketServer, 'socket',
lambda *args, **kwargs: sock)
self.stubs.Set(websocket.WebSocketServer, 'daemonize', self.stubs.Set(websocket.WebSocketServer, 'daemonize',
lambda *args, **kwargs: None) lambda *args, **kwargs: None)
self.stubs.Set(select, 'select', self._mock_select_systemexit) self.stubs.Set(select, 'select', fake_select)
soc = self._get_websockserver(daemon=True, ssl_only=0, idle_timeout=1, server.start_server()
verbose=True)
self.assertEqual(None, soc.start_server())
def test_WSRequestHandle_do_GET_nofile(self): def test_socket_set_keepalive_options(self):
request = 'GET /tmp.txt HTTP/0.9'
with tempfile.NamedTemporaryFile() as test_file:
test_file.write(request)
test_file.flush()
test_file.seek(0)
con = MockConnection(test_file.name)
soc = websocket.WSRequestHandler(con, "127.0.0.1", file_only=True)
soc.path = ''
soc.headers = {'upgrade': ''}
self.stubs.Set(SimpleHTTPRequestHandler, 'send_response',
lambda *args: None)
soc.do_GET()
self.assertEqual(404, soc.last_code)
def test_WSRequestHandle_do_GET_hidden_resource(self):
request = 'GET /tmp.txt HTTP/0.9'
with tempfile.NamedTemporaryFile() as test_file:
test_file.write(request)
test_file.flush()
test_file.seek(0)
con = MockConnection(test_file.name)
soc = websocket.WSRequestHandler(con, '127.0.0.1', no_parent=True)
soc.path = test_file.name + '?'
soc.headers = {'upgrade': ''}
soc.webroot = 'no match startswith'
self.stubs.Set(SimpleHTTPRequestHandler,
'send_response',
lambda *args: None)
soc.do_GET()
self.assertEqual(403, soc.last_code)
def testsocket_set_keepalive_options(self):
keepcnt = 12 keepcnt = 12
keepidle = 34 keepidle = 34
keepintvl = 56 keepintvl = 56
sock = self.server.socket('localhost', server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1)
tcp_keepcnt=keepcnt, sock = server.socket('localhost',
tcp_keepidle=keepidle, tcp_keepcnt=keepcnt,
tcp_keepintvl=keepintvl) tcp_keepidle=keepidle,
tcp_keepintvl=keepintvl)
self.assertEqual(sock.getsockopt(socket.SOL_TCP, self.assertEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPCNT), keepcnt) socket.TCP_KEEPCNT), keepcnt)
@@ -229,11 +331,11 @@ class WebSocketTestCase(unittest.TestCase):
self.assertEqual(sock.getsockopt(socket.SOL_TCP, self.assertEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPINTVL), keepintvl) socket.TCP_KEEPINTVL), keepintvl)
sock = self.server.socket('localhost', sock = server.socket('localhost',
tcp_keepalive=False, tcp_keepalive=False,
tcp_keepcnt=keepcnt, tcp_keepcnt=keepcnt,
tcp_keepidle=keepidle, tcp_keepidle=keepidle,
tcp_keepintvl=keepintvl) tcp_keepintvl=keepintvl)
self.assertNotEqual(sock.getsockopt(socket.SOL_TCP, self.assertNotEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPCNT), keepcnt) socket.TCP_KEEPCNT), keepcnt)
@@ -241,3 +343,88 @@ class WebSocketTestCase(unittest.TestCase):
socket.TCP_KEEPIDLE), keepidle) socket.TCP_KEEPIDLE), keepidle)
self.assertNotEqual(sock.getsockopt(socket.SOL_TCP, self.assertNotEqual(sock.getsockopt(socket.SOL_TCP,
socket.TCP_KEEPINTVL), keepintvl) socket.TCP_KEEPINTVL), keepintvl)
class HyBiEncodeDecodeTestCase(unittest.TestCase):
def test_decode_hybi_text(self):
buf = b'\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58'
res = websocket.WebSocketRequestHandler.decode_hybi(buf)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x1)
self.assertEqual(res['masked'], True)
self.assertEqual(res['length'], 5)
self.assertEqual(res['payload'], b'Hello')
self.assertEqual(res['left'], 0)
def test_decode_hybi_binary(self):
buf = b'\x82\x04\x01\x02\x03\x04'
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 4)
self.assertEqual(res['payload'], b'\x01\x02\x03\x04')
self.assertEqual(res['left'], 0)
def test_decode_hybi_extended_16bit_binary(self):
data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260
buf = b'\x82\x7e\x01\x04' + data
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 260)
self.assertEqual(res['payload'], data)
self.assertEqual(res['left'], 0)
def test_decode_hybi_extended_64bit_binary(self):
data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260
buf = b'\x82\x7f\x00\x00\x00\x00\x00\x00\x01\x04' + data
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x2)
self.assertEqual(res['length'], 260)
self.assertEqual(res['payload'], data)
self.assertEqual(res['left'], 0)
def test_decode_hybi_multi(self):
buf1 = b'\x01\x03\x48\x65\x6c'
buf2 = b'\x80\x02\x6c\x6f'
res1 = websocket.WebSocketRequestHandler.decode_hybi(buf1, strict=False)
self.assertEqual(res1['fin'], 0)
self.assertEqual(res1['opcode'], 0x1)
self.assertEqual(res1['length'], 3)
self.assertEqual(res1['payload'], b'Hel')
self.assertEqual(res1['left'], 0)
res2 = websocket.WebSocketRequestHandler.decode_hybi(buf2, strict=False)
self.assertEqual(res2['fin'], 1)
self.assertEqual(res2['opcode'], 0x0)
self.assertEqual(res2['length'], 2)
self.assertEqual(res2['payload'], b'lo')
self.assertEqual(res2['left'], 0)
def test_encode_hybi_basic(self):
res = websocket.WebSocketRequestHandler.encode_hybi(b'Hello', 0x1)
expected = (b'\x81\x05\x48\x65\x6c\x6c\x6f', 2, 0)
self.assertEqual(res, expected)
def test_strict_mode_refuses_unmasked_client_frames(self):
buf = b'\x81\x05\x48\x65\x6c\x6c\x6f'
self.assertRaises(websocket.WebSocketRequestHandler.CClose,
websocket.WebSocketRequestHandler.decode_hybi,
buf)
def test_no_strict_mode_accepts_unmasked_client_frames(self):
buf = b'\x81\x05\x48\x65\x6c\x6c\x6f'
res = websocket.WebSocketRequestHandler.decode_hybi(buf, strict=False)
self.assertEqual(res['fin'], 1)
self.assertEqual(res['opcode'], 0x1)
self.assertEqual(res['masked'], False)
self.assertEqual(res['length'], 5)
self.assertEqual(res['payload'], b'Hello')
+95 -86
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright(c)2013 NTT corp. All Rights Reserved. # Copyright(c) 2015 Red Hat, Inc All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain # not use this file except in compliance with the License. You may obtain
@@ -15,113 +15,122 @@
# under the License. # under the License.
""" Unit tests for websocketproxy """ """ Unit tests for websocketproxy """
import os
import logging
import select
import shutil
import stubout
import subprocess
import tempfile
import time
import unittest import unittest
import unittest
import socket
import stubout
from websockify import websocket
from websockify import websocketproxy from websockify import websocketproxy
from websockify import token_plugins
from websockify import auth_plugins
try:
from StringIO import StringIO
BytesIO = StringIO
except ImportError:
from io import StringIO
from io import BytesIO
class MockSocket(object): class FakeSocket(object):
def __init__(*args, **kwargs): def __init__(self, data=''):
if isinstance(data, bytes):
self._data = data
else:
self._data = data.encode('latin_1')
def recv(self, amt, flags=None):
res = self._data[0:amt]
if not (flags & socket.MSG_PEEK):
self._data = self._data[amt:]
return res
def makefile(self, mode='r', buffsize=None):
if 'b' in mode:
return BytesIO(self._data)
else:
return StringIO(self._data.decode('latin_1'))
class FakeServer(object):
class EClose(Exception):
pass pass
def shutdown(*args): def __init__(self):
pass self.token_plugin = None
self.auth_plugin = None
def close(*args): self.wrap_cmd = None
pass self.ssl_target = None
self.unix_target = None
class WebSocketProxyTest(unittest.TestCase):
def _init_logger(self, tmpdir):
name = 'websocket-unittest'
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.propagate = True
filename = "%s.log" % (name)
handler = logging.FileHandler(filename)
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)
class ProxyRequestHandlerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
"""Called automatically before each test.""" super(ProxyRequestHandlerTestCase, self).setUp()
super(WebSocketProxyTest, self).setUp()
self.soc = ''
self.stubs = stubout.StubOutForTesting() self.stubs = stubout.StubOutForTesting()
# Temporary dir for test data self.handler = websocketproxy.ProxyRequestHandler(
self.tmpdir = tempfile.mkdtemp() FakeSocket(''), "127.0.0.1", FakeServer())
# Put log somewhere persistent self.handler.path = "https://localhost:6080/websockify?token=blah"
self._init_logger('./') self.handler.headers = None
# Mock this out cause it screws tests up self.stubs.Set(websocket.WebSocketServer, 'socket',
self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None) staticmethod(lambda *args, **kwargs: None))
def tearDown(self): def tearDown(self):
"""Called automatically after each test."""
self.stubs.UnsetAll() self.stubs.UnsetAll()
shutil.rmtree(self.tmpdir) super(ProxyRequestHandlerTestCase, self).tearDown()
super(WebSocketProxyTest, self).tearDown()
def _get_websockproxy(self, **kwargs): def test_get_target(self):
return websocketproxy.WebSocketProxy(key=self.tmpdir, class TestPlugin(token_plugins.BasePlugin):
web=self.tmpdir, def lookup(self, token):
record=self.tmpdir, return ("some host", "some port")
**kwargs)
def test_run_wrap_cmd(self): host, port = self.handler.get_target(
web_socket_proxy = self._get_websockproxy() TestPlugin(None), self.handler.path)
web_socket_proxy.__dict__["wrap_cmd"] = "wrap_cmd"
def mock_Popen(*args, **kwargs): self.assertEqual(host, "some host")
return '_mock_cmd' self.assertEqual(port, "some port")
self.stubs.Set(subprocess, 'Popen', mock_Popen) def test_get_target_raises_error_on_unknown_token(self):
web_socket_proxy.run_wrap_cmd() class TestPlugin(token_plugins.BasePlugin):
self.assertEquals(web_socket_proxy.spawn_message, True) def lookup(self, token):
return None
def test_started(self): self.assertRaises(FakeServer.EClose, self.handler.get_target,
web_socket_proxy = self._get_websockproxy() TestPlugin(None), "https://localhost:6080/websockify?token=blah")
web_socket_proxy.__dict__["spawn_message"] = False
web_socket_proxy.__dict__["wrap_cmd"] = "wrap_cmd"
def mock_run_wrap_cmd(*args, **kwargs): def test_token_plugin(self):
web_socket_proxy.__dict__["spawn_message"] = True class TestPlugin(token_plugins.BasePlugin):
def lookup(self, token):
return (self.source + token).split(',')
self.stubs.Set(web_socket_proxy, 'run_wrap_cmd', mock_run_wrap_cmd) self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
web_socket_proxy.started() lambda *args, **kwargs: None)
self.assertEquals(web_socket_proxy.__dict__["spawn_message"], True)
def test_poll(self): self.handler.server.token_plugin = TestPlugin("somehost,")
web_socket_proxy = self._get_websockproxy() self.handler.new_websocket_client()
web_socket_proxy.__dict__["wrap_cmd"] = "wrap_cmd"
web_socket_proxy.__dict__["wrap_mode"] = "respawn"
web_socket_proxy.__dict__["wrap_times"] = [99999999]
web_socket_proxy.__dict__["spawn_message"] = True
web_socket_proxy.__dict__["cmd"] = None
self.stubs.Set(time, 'time', lambda: 100000000.000)
web_socket_proxy.poll()
self.assertEquals(web_socket_proxy.spawn_message, False)
def test_new_client(self): self.assertEqual(self.handler.server.target_host, "somehost")
web_socket_proxy = self._get_websockproxy() self.assertEqual(self.handler.server.target_port, "blah")
web_socket_proxy.__dict__["verbose"] = "verbose"
web_socket_proxy.__dict__["daemon"] = None
web_socket_proxy.__dict__["client"] = "client"
self.stubs.Set(web_socket_proxy, 'socket', MockSocket) def test_auth_plugin(self):
class TestPlugin(auth_plugins.BasePlugin):
def authenticate(self, headers, target_host, target_port):
if target_host == self.source:
raise auth_plugins.AuthenticationError("some error")
def mock_select(*args, **kwargs): self.stubs.Set(websocketproxy.ProxyRequestHandler, 'do_proxy',
ins = None staticmethod(lambda *args, **kwargs: None))
outs = None
excepts = "excepts" self.handler.server.auth_plugin = TestPlugin("somehost")
return ins, outs, excepts self.handler.server.target_host = "somehost"
self.handler.server.target_port = "someport"
self.assertRaises(auth_plugins.AuthenticationError,
self.handler.new_websocket_client)
self.handler.server.target_host = "someotherhost"
self.handler.new_websocket_client()
self.stubs.Set(select, 'select', mock_select)
self.assertRaises(Exception, web_socket_proxy.new_websocket_client)
+4 -7
View File
@@ -4,17 +4,14 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = py24,py25,py26,py27,py30 envlist = py24,py26,py27,py33,py34
setupdir = ../
[testenv] [testenv]
commands = nosetests {posargs} commands = nosetests {posargs}
deps = deps = -r{toxinidir}/test-requirements.txt
mox
nose
# At some point we should enable this since tox epdctes it to exist but # At some point we should enable this since tox expects it to exist but
# the code will need pep8ising first. # the code will need pep8ising first.
#[testenv:pep8] #[testenv:pep8]
#commands = flake8 #commands = flake8
#dep = flake8 #dep = flake8
+2 -2
View File
@@ -1,2 +1,2 @@
from websocket import * from websockify.websocket import *
from websocketproxy import * from websockify.websocketproxy import *
+33
View File
@@ -0,0 +1,33 @@
class BasePlugin(object):
def __init__(self, src=None):
self.source = src
def authenticate(self, headers, target_host, target_port):
pass
class AuthenticationError(Exception):
pass
class InvalidOriginError(AuthenticationError):
def __init__(self, expected, actual):
self.expected_origin = expected
self.actual_origin = actual
super(InvalidOriginError, self).__init__(
"Invalid Origin Header: Expected one of "
"%s, got '%s'" % (expected, actual))
class ExpectOrigin(object):
def __init__(self, src=None):
if src is None:
self.source = []
else:
self.source = src.split()
def authenticate(self, headers, target_host, target_port):
origin = headers.getheader('Origin', None)
if origin is None or origin not in self.source:
raise InvalidOriginError(expected=self.source, actual=origin)
+82
View File
@@ -0,0 +1,82 @@
import os
class BasePlugin(object):
def __init__(self, src):
self.source = src
def lookup(self, token):
return None
class ReadOnlyTokenFile(BasePlugin):
# source is a token file with lines like
# token: host:port
# or a directory of such files
def __init__(self, *args, **kwargs):
super(ReadOnlyTokenFile, self).__init__(*args, **kwargs)
self._targets = None
def _load_targets(self):
if os.path.isdir(self.source):
cfg_files = [os.path.join(self.source, f) for
f in os.listdir(self.source)]
else:
cfg_files = [self.source]
self._targets = {}
for f in cfg_files:
for line in [l.strip() for l in open(f).readlines()]:
if line and not line.startswith('#'):
tok, target = line.split(': ')
self._targets[tok] = target.strip().split(':')
def lookup(self, token):
if self._targets is None:
self._load_targets()
if token in self._targets:
return self._targets[token]
else:
return None
# the above one is probably more efficient, but this one is
# more backwards compatible (although in most cases
# ReadOnlyTokenFile should suffice)
class TokenFile(ReadOnlyTokenFile):
# source is a token file with lines like
# token: host:port
# or a directory of such files
def lookup(self, token):
self._load_targets()
return super(TokenFile, self).lookup(token)
class BaseTokenAPI(BasePlugin):
# source is a url with a '%s' in it where the token
# should go
# we import things on demand so that other plugins
# in this file can be used w/o unecessary dependencies
def process_result(self, resp):
return resp.text.split(':')
def lookup(self, token):
import requests
resp = requests.get(self.source % token)
if resp.ok:
return self.process_result(resp)
else:
return None
class JSONTokenApi(BaseTokenAPI):
# source is a url with a '%s' in it where the token
# should go
def process_result(self, resp):
return (resp.json['host'], resp.json['port'])
+64 -32
View File
@@ -74,8 +74,8 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
attributes on the server object: attributes on the server object:
* only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled, * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled,
only websocket is allowed. only websocket is allowed.
* verbose: If true, verbose logging is activated. * verbose: If true, verbose logging is activated.
* daemon: Running as daemon, do not write to console etc * daemon: Running as daemon, do not write to console etc
* record: Record raw frame data as JavaScript array into specified filename * record: Record raw frame data as JavaScript array into specified filename
* run_once: Handle a single request * run_once: Handle a single request
@@ -104,11 +104,13 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
self.handler_id = getattr(server, "handler_id", False) self.handler_id = getattr(server, "handler_id", False)
self.file_only = getattr(server, "file_only", False) self.file_only = getattr(server, "file_only", False)
self.traffic = getattr(server, "traffic", False) self.traffic = getattr(server, "traffic", False)
self.auto_pong = getattr(server, "auto_pong", False)
self.strict_mode = getattr(server, "strict_mode", True)
self.logger = getattr(server, "logger", None) self.logger = getattr(server, "logger", None)
if self.logger is None: if self.logger is None:
self.logger = WebSocketServer.get_logger() self.logger = WebSocketServer.get_logger()
SimpleHTTPRequestHandler.__init__(self, req, addr, server) SimpleHTTPRequestHandler.__init__(self, req, addr, server)
@staticmethod @staticmethod
@@ -118,20 +120,24 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
if numpy: if numpy:
b = c = s2b('') b = c = s2b('')
if plen >= 4: if plen >= 4:
mask = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'), dtype=numpy.dtype('<u4')
offset=hlen, count=1) if sys.byteorder == 'big':
data = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'), dtype = dtype.newbyteorder('>')
offset=pstart, count=int(plen / 4)) mask = numpy.frombuffer(buf, dtype, offset=hlen, count=1)
data = numpy.frombuffer(buf, dtype, offset=pstart,
count=int(plen / 4))
#b = numpy.bitwise_xor(data, mask).data #b = numpy.bitwise_xor(data, mask).data
b = numpy.bitwise_xor(data, mask).tostring() b = numpy.bitwise_xor(data, mask).tostring()
if plen % 4: if plen % 4:
#self.msg("Partial unmask") #self.msg("Partial unmask")
mask = numpy.frombuffer(buf, dtype=numpy.dtype('B'), dtype=numpy.dtype('B')
offset=hlen, count=(plen % 4)) if sys.byteorder == 'big':
data = numpy.frombuffer(buf, dtype=numpy.dtype('B'), dtype = dtype.newbyteorder('>')
offset=pend - (plen % 4), mask = numpy.frombuffer(buf, dtype, offset=hlen,
count=(plen % 4)) count=(plen % 4))
data = numpy.frombuffer(buf, dtype,
offset=pend - (plen % 4), count=(plen % 4))
c = numpy.bitwise_xor(data, mask).tostring() c = numpy.bitwise_xor(data, mask).tostring()
return b + c return b + c
else: else:
@@ -172,7 +178,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
return header + buf, len(header), 0 return header + buf, len(header), 0
@staticmethod @staticmethod
def decode_hybi(buf, base64=False, logger=None): def decode_hybi(buf, base64=False, logger=None, strict=True):
""" Decode HyBi style WebSocket packets. """ Decode HyBi style WebSocket packets.
Returns: Returns:
{'fin' : 0_or_1, {'fin' : 0_or_1,
@@ -238,6 +244,10 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
f['length']) f['length'])
else: else:
logger.debug("Unmasked frame: %s" % repr(buf)) logger.debug("Unmasked frame: %s" % repr(buf))
if strict:
raise WebSocketRequestHandler.CClose(1002, "The client sent an unmasked frame.")
f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len] f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len]
if base64 and f['opcode'] in [1, 2]: if base64 and f['opcode'] in [1, 2]:
@@ -346,7 +356,8 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
while buf: while buf:
frame = self.decode_hybi(buf, base64=self.base64, frame = self.decode_hybi(buf, base64=self.base64,
logger=self.logger) logger=self.logger,
strict=self.strict_mode)
#self.msg("Received buf: %s, frame: %s", repr(buf), frame) #self.msg("Received buf: %s, frame: %s", repr(buf), frame)
if frame['payload'] == None: if frame['payload'] == None:
@@ -360,6 +371,15 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
closed = {'code': frame['close_code'], closed = {'code': frame['close_code'],
'reason': frame['close_reason']} 'reason': frame['close_reason']}
break break
elif self.auto_pong and frame['opcode'] == 0x9: # ping
self.print_traffic("} ping %s\n" %
repr(frame['payload']))
self.send_pong(frame['payload'])
return [], False
elif frame['opcode'] == 0xA: # pong
self.print_traffic("} pong %s\n" %
repr(frame['payload']))
return [], False
self.print_traffic("}") self.print_traffic("}")
@@ -388,10 +408,20 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
def send_close(self, code=1000, reason=''): def send_close(self, code=1000, reason=''):
""" Send a WebSocket orderly close frame. """ """ Send a WebSocket orderly close frame. """
msg = pack(">H%ds" % len(reason), code, reason) msg = pack(">H%ds" % len(reason), code, s2b(reason))
buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False) buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)
self.request.send(buf) self.request.send(buf)
def send_pong(self, data=''):
""" Send a WebSocket pong frame. """
buf, h, t = self.encode_hybi(s2b(data), opcode=0x0A, base64=False)
self.request.send(buf)
def send_ping(self, data=''):
""" Send a WebSocket ping frame. """
buf, h, t = self.encode_hybi(s2b(data), opcode=0x09, base64=False)
self.request.send(buf)
def do_websocket_handshake(self): def do_websocket_handshake(self):
h = self.headers h = self.headers
@@ -435,16 +465,16 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
self.send_header("Sec-WebSocket-Protocol", "binary") self.send_header("Sec-WebSocket-Protocol", "binary")
self.end_headers() self.end_headers()
return True return True
else: else:
self.send_error(400, "Missing Sec-WebSocket-Version header. Hixie protocols not supported.") self.send_error(400, "Missing Sec-WebSocket-Version header. Hixie protocols not supported.")
return False return False
def handle_websocket(self): def handle_websocket(self):
"""Upgrade a connection to Websocket, if requested. If this succeeds, """Upgrade a connection to Websocket, if requested. If this succeeds,
new_websocket_client() will be called. Otherwise, False is returned. new_websocket_client() will be called. Otherwise, False is returned.
""" """
if (self.headers.get('upgrade') and if (self.headers.get('upgrade') and
self.headers.get('upgrade').lower() == 'websocket'): self.headers.get('upgrade').lower() == 'websocket'):
if not self.do_websocket_handshake(): if not self.do_websocket_handshake():
@@ -468,21 +498,21 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
if is_ssl: if is_ssl:
self.stype = "SSL/TLS (wss://)" self.stype = "SSL/TLS (wss://)"
else: else:
self.stype = "Plain non-SSL (ws://)" self.stype = "Plain non-SSL (ws://)"
self.log_message("%s: %s WebSocket connection" % (client_addr, self.log_message("%s: %s WebSocket connection", client_addr,
self.stype)) self.stype)
self.log_message("%s: Version %s, base64: '%s'" % (client_addr, self.log_message("%s: Version %s, base64: '%s'", client_addr,
self.version, self.base64)) self.version, self.base64)
if self.path != '/': if self.path != '/':
self.log_message("%s: Path: '%s'" % (client_addr, self.path)) self.log_message("%s: Path: '%s'", client_addr, self.path)
if self.record: if self.record:
# Record raw frame data as JavaScript array # Record raw frame data as JavaScript array
fname = "%s.%s" % (self.record, fname = "%s.%s" % (self.record,
self.handler_id) self.handler_id)
self.log_message("opening record file: %s" % fname) self.log_message("opening record file: %s", fname)
self.rec = open(fname, 'w+') self.rec = open(fname, 'w+')
encoding = "binary" encoding = "binary"
if self.base64: encoding = "base64" if self.base64: encoding = "base64"
@@ -495,7 +525,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
except self.CClose: except self.CClose:
# Close the client # Close the client
_, exc, _ = sys.exc_info() _, exc, _ = sys.exc_info()
self.send_close(exc.args[0], exc.args[1]) self.send_close(exc.args[0], exc.args[1])
return True return True
else: else:
return False return False
@@ -514,7 +544,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler):
self.send_error(404, "No such file") self.send_error(404, "No such file")
else: else:
return SimpleHTTPRequestHandler.list_directory(self, path) return SimpleHTTPRequestHandler.list_directory(self, path)
def new_websocket_client(self): def new_websocket_client(self):
""" Do something with a WebSockets client connection. """ """ Do something with a WebSockets client connection. """
raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded")
@@ -560,14 +590,14 @@ class WebSocketServer(object):
class Terminate(Exception): class Terminate(Exception):
pass pass
def __init__(self, RequestHandlerClass, listen_host='', def __init__(self, RequestHandlerClass, listen_host='',
listen_port=None, source_is_ipv6=False, listen_port=None, source_is_ipv6=False,
verbose=False, cert='', key='', ssl_only=None, verbose=False, cert='', key='', ssl_only=None,
daemon=False, record='', web='', daemon=False, record='', web='',
file_only=False, file_only=False,
run_once=False, timeout=0, idle_timeout=0, traffic=False, run_once=False, timeout=0, idle_timeout=0, traffic=False,
tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None, tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None,
tcp_keepintvl=None): tcp_keepintvl=None, auto_pong=False, strict_mode=True):
# settings # settings
self.RequestHandlerClass = RequestHandlerClass self.RequestHandlerClass = RequestHandlerClass
@@ -582,6 +612,7 @@ class WebSocketServer(object):
self.idle_timeout = idle_timeout self.idle_timeout = idle_timeout
self.traffic = traffic self.traffic = traffic
self.file_only = file_only self.file_only = file_only
self.strict_mode = strict_mode
self.launch_time = time.time() self.launch_time = time.time()
self.ws_connection = False self.ws_connection = False
@@ -593,6 +624,7 @@ class WebSocketServer(object):
self.tcp_keepidle = tcp_keepidle self.tcp_keepidle = tcp_keepidle
self.tcp_keepintvl = tcp_keepintvl self.tcp_keepintvl = tcp_keepintvl
self.auto_pong = auto_pong
# Make paths settings absolute # Make paths settings absolute
self.cert = os.path.abspath(cert) self.cert = os.path.abspath(cert)
self.key = self.web = self.record = '' self.key = self.web = self.record = ''
@@ -666,7 +698,7 @@ class WebSocketServer(object):
raise Exception("SSL only supported in connect mode (for now)") raise Exception("SSL only supported in connect mode (for now)")
if not connect: if not connect:
flags = flags | socket.AI_PASSIVE flags = flags | socket.AI_PASSIVE
if not unix_socket: if not unix_socket:
addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
socket.IPPROTO_TCP, flags) socket.IPPROTO_TCP, flags)
@@ -697,7 +729,7 @@ class WebSocketServer(object):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addrs[0][4]) sock.bind(addrs[0][4])
sock.listen(100) sock.listen(100)
else: else:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(unix_socket) sock.connect(unix_socket)
@@ -765,7 +797,7 @@ class WebSocketServer(object):
handshake = sock.recv(1024, socket.MSG_PEEK) handshake = sock.recv(1024, socket.MSG_PEEK)
#self.msg("Handshake [%s]" % handshake) #self.msg("Handshake [%s]" % handshake)
if handshake == "": if not handshake:
raise self.EClose("ignoring empty handshake") raise self.EClose("ignoring empty handshake")
elif handshake.startswith(s2b("<policy-file-request/>")): elif handshake.startswith(s2b("<policy-file-request/>")):
@@ -848,7 +880,7 @@ class WebSocketServer(object):
raise self.Terminate() raise self.Terminate()
def multiprocessing_SIGCHLD(self, sig, stack): def multiprocessing_SIGCHLD(self, sig, stack):
self.vmsg('Reaing zombies, active child count is %s', len(multiprocessing.active_children())) self.vmsg('Reaping zombies, active child count is %s', len(multiprocessing.active_children()))
def fallback_SIGCHLD(self, sig, stack): def fallback_SIGCHLD(self, sig, stack):
# Reap zombies when using os.fork() (python 2.4) # Reap zombies when using os.fork() (python 2.4)
+137 -55
View File
@@ -11,13 +11,13 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
''' '''
import signal, socket, optparse, time, os, sys, subprocess, logging import signal, socket, optparse, time, os, sys, subprocess, logging, errno
try: from socketserver import ForkingMixIn try: from socketserver import ForkingMixIn
except: from SocketServer import ForkingMixIn except: from SocketServer import ForkingMixIn
try: from http.server import HTTPServer try: from http.server import HTTPServer
except: from BaseHTTPServer import HTTPServer except: from BaseHTTPServer import HTTPServer
from select import select import select
import websocket from websockify import websocket
try: try:
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
except: except:
@@ -44,8 +44,13 @@ Traffic Legend:
""" """
# Checks if we receive a token, and look # Checks if we receive a token, and look
# for a valid target for it then # for a valid target for it then
if self.server.target_cfg: if self.server.token_plugin:
(self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path) (self.server.target_host, self.server.target_port) = self.get_target(self.server.token_plugin, self.path)
if self.server.auth_plugin:
self.server.auth_plugin.authenticate(
headers=self.headers, target_host=self.server.target_host,
target_port=self.server.target_port)
# Connect to the target # Connect to the target
if self.server.wrap_cmd: if self.server.wrap_cmd:
@@ -73,15 +78,15 @@ Traffic Legend:
if tsock: if tsock:
tsock.shutdown(socket.SHUT_RDWR) tsock.shutdown(socket.SHUT_RDWR)
tsock.close() tsock.close()
if self.verbose: if self.verbose:
self.log_message("%s:%s: Closed target" %( self.log_message("%s:%s: Closed target",
self.server.target_host, self.server.target_port)) self.server.target_host, self.server.target_port)
raise raise
def get_target(self, target_cfg, path): def get_target(self, target_plugin, path):
""" """
Parses the path, extracts a token, and looks for a valid Parses the path, extracts a token, and looks up a target
target for that token in the configuration file(s). Sets for that token using the token plugin. Sets
target_host and target_port if successful target_host and target_port if successful
""" """
# The files in targets contain the lines # The files in targets contain the lines
@@ -90,32 +95,17 @@ Traffic Legend:
# Extract the token parameter from url # Extract the token parameter from url
args = parse_qs(urlparse(path)[4]) # 4 is the query from url args = parse_qs(urlparse(path)[4]) # 4 is the query from url
if not args.has_key('token') or not len(args['token']): if not 'token' in args or not len(args['token']):
raise self.EClose("Token not present") raise self.server.EClose("Token not present")
token = args['token'][0].rstrip('\n') token = args['token'][0].rstrip('\n')
# target_cfg can be a single config file or directory of result_pair = target_plugin.lookup(token)
# config files
if os.path.isdir(target_cfg): if result_pair is not None:
cfg_files = [os.path.join(target_cfg, f) return result_pair
for f in os.listdir(target_cfg)]
else: else:
cfg_files = [target_cfg] raise self.server.EClose("Token '%s' not found" % token)
targets = {}
for f in cfg_files:
for line in [l.strip() for l in file(f).readlines()]:
if line and not line.startswith('#'):
ttoken, target = line.split(': ')
targets[ttoken] = target.strip()
self.vmsg("Target config: %s" % repr(targets))
if targets.has_key(token):
return targets[token].split(':')
else:
raise self.EClose("Token '%s' not found" % token)
def do_proxy(self, target): def do_proxy(self, target):
""" """
@@ -126,12 +116,37 @@ Traffic Legend:
tqueue = [] tqueue = []
rlist = [self.request, target] rlist = [self.request, target]
if self.server.heartbeat:
now = time.time()
self.heartbeat = now + self.server.heartbeat
else:
self.heartbeat = None
while True: while True:
wlist = [] wlist = []
if self.heartbeat is not None:
now = time.time()
if now > self.heartbeat:
self.heartbeat = now + self.server.heartbeat
self.send_ping()
if tqueue: wlist.append(target) if tqueue: wlist.append(target)
if cqueue or c_pend: wlist.append(self.request) if cqueue or c_pend: wlist.append(self.request)
ins, outs, excepts = select(rlist, wlist, [], 1) try:
ins, outs, excepts = select.select(rlist, wlist, [], 1)
except (select.error, OSError):
exc = sys.exc_info()[1]
if hasattr(exc, 'errno'):
err = exc.errno
else:
err = exc[0]
if err != errno.EINTR:
raise
else:
continue
if excepts: raise Exception("Socket exception") if excepts: raise Exception("Socket exception")
if self.request in outs: if self.request in outs:
@@ -147,9 +162,9 @@ Traffic Legend:
if closed: if closed:
# TODO: What about blocking on client socket? # TODO: What about blocking on client socket?
if self.verbose: if self.verbose:
self.log_message("%s:%s: Client closed connection" %( self.log_message("%s:%s: Client closed connection",
self.server.target_host, self.server.target_port)) self.server.target_host, self.server.target_port)
raise self.CClose(closed['code'], closed['reason']) raise self.CClose(closed['code'], closed['reason'])
@@ -170,8 +185,8 @@ Traffic Legend:
buf = target.recv(self.buffer_size) buf = target.recv(self.buffer_size)
if len(buf) == 0: if len(buf) == 0:
if self.verbose: if self.verbose:
self.log_message("%s:%s: Target closed connection" %( self.log_message("%s:%s: Target closed connection",
self.server.target_host, self.server.target_port)) self.server.target_host, self.server.target_port)
raise self.CClose(1000, "Target closed") raise self.CClose(1000, "Target closed")
cqueue.append(buf) cqueue.append(buf)
@@ -195,7 +210,11 @@ class WebSocketProxy(websocket.WebSocketServer):
self.wrap_mode = kwargs.pop('wrap_mode', None) self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None) self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None) self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None) self.heartbeat = kwargs.pop('heartbeat', None)
self.token_plugin = kwargs.pop('token_plugin', None)
self.auth_plugin = kwargs.pop('auth_plugin', None)
# Last 3 timestamps command was run # Last 3 timestamps command was run
self.wrap_times = [0, 0, 0] self.wrap_times = [0, 0, 0]
@@ -251,9 +270,9 @@ class WebSocketProxy(websocket.WebSocketServer):
else: else:
dst_string = "%s:%s" % (self.target_host, self.target_port) dst_string = "%s:%s" % (self.target_host, self.target_port)
if self.target_cfg: if self.token_plugin:
msg = " - proxying from %s:%s to targets in %s" % ( msg = " - proxying from %s:%s to targets generated by %s" % (
self.listen_host, self.listen_port, self.target_cfg) self.listen_host, self.listen_port, type(self.token_plugin).__name__)
else: else:
msg = " - proxying from %s:%s to %s" % ( msg = " - proxying from %s:%s to %s" % (
self.listen_host, self.listen_port, dst_string) self.listen_host, self.listen_port, dst_string)
@@ -352,20 +371,56 @@ def websockify_init():
parser.add_option("--prefer-ipv6", "-6", parser.add_option("--prefer-ipv6", "-6",
action="store_true", dest="source_is_ipv6", action="store_true", dest="source_is_ipv6",
help="prefer IPv6 when resolving source_addr") help="prefer IPv6 when resolving source_addr")
parser.add_option("--libserver", action="store_true",
help="use Python library SocketServer engine")
parser.add_option("--target-config", metavar="FILE", parser.add_option("--target-config", metavar="FILE",
dest="target_cfg", dest="target_cfg",
help="Configuration file containing valid targets " help="Configuration file containing valid targets "
"in the form 'token: host:port' or, alternatively, a " "in the form 'token: host:port' or, alternatively, a "
"directory containing configuration files of this form") "directory containing configuration files of this form "
parser.add_option("--libserver", action="store_true", "(DEPRECATED: use `--token-plugin TokenFile --token-source "
help="use Python library SocketServer engine") " path/to/token/file` instead)")
parser.add_option("--token-plugin", default=None, metavar="PLUGIN",
help="use the given Python class to process tokens "
"into host:port pairs")
parser.add_option("--token-source", default=None, metavar="ARG",
help="an argument to be passed to the token plugin"
"on instantiation")
parser.add_option("--auth-plugin", default=None, metavar="PLUGIN",
help="use the given Python class to determine if "
"a connection is allowed")
parser.add_option("--auth-source", default=None, metavar="ARG",
help="an argument to be passed to the auth plugin"
"on instantiation")
parser.add_option("--auto-pong", action="store_true",
help="Automatically respond to ping frames with a pong")
parser.add_option("--heartbeat", type=int, default=0,
help="send a ping to the client every HEARTBEAT seconds")
(opts, args) = parser.parse_args() (opts, args) = parser.parse_args()
if opts.verbose: if opts.verbose:
logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG) logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG)
if opts.token_source and not opts.token_plugin:
parser.error("You must use --token-plugin to use --token-source")
if opts.auth_source and not opts.auth_plugin:
parser.error("You must use --auth-plugin to use --auth-source")
# Transform to absolute path as daemon may chdir
if opts.target_cfg:
opts.target_cfg = os.path.abspath(opts.target_cfg)
if opts.target_cfg:
opts.token_plugin = 'TokenFile'
opts.token_source = opts.target_cfg
del opts.target_cfg
# Sanity checks # Sanity checks
if len(args) < 2 and not (opts.target_cfg or opts.unix_target): if len(args) < 2 and not (opts.token_plugin or opts.unix_target):
parser.error("Too few arguments") parser.error("Too few arguments")
if sys.argv.count('--'): if sys.argv.count('--'):
opts.wrap_cmd = args[1:] opts.wrap_cmd = args[1:]
@@ -390,7 +445,7 @@ def websockify_init():
try: opts.listen_port = int(opts.listen_port) try: opts.listen_port = int(opts.listen_port)
except: parser.error("Error parsing listen port") except: parser.error("Error parsing listen port")
if opts.wrap_cmd or opts.unix_target or opts.target_cfg: if opts.wrap_cmd or opts.unix_target or opts.token_plugin:
opts.target_host = None opts.target_host = None
opts.target_port = None opts.target_port = None
else: else:
@@ -402,9 +457,32 @@ def websockify_init():
try: opts.target_port = int(opts.target_port) try: opts.target_port = int(opts.target_port)
except: parser.error("Error parsing target port") except: parser.error("Error parsing target port")
# Transform to absolute path as daemon may chdir if opts.token_plugin is not None:
if opts.target_cfg: if '.' not in opts.token_plugin:
opts.target_cfg = os.path.abspath(opts.target_cfg) opts.token_plugin = (
'websockify.token_plugins.%s' % opts.token_plugin)
token_plugin_module, token_plugin_cls = opts.token_plugin.rsplit('.', 1)
__import__(token_plugin_module)
token_plugin_cls = getattr(sys.modules[token_plugin_module], token_plugin_cls)
opts.token_plugin = token_plugin_cls(opts.token_source)
del opts.token_source
if opts.auth_plugin is not None:
if '.' not in opts.auth_plugin:
opts.auth_plugin = 'websockify.auth_plugins.%s' % opts.auth_plugin
auth_plugin_module, auth_plugin_cls = opts.auth_plugin.rsplit('.', 1)
__import__(auth_plugin_module)
auth_plugin_cls = getattr(sys.modules[auth_plugin_module], auth_plugin_cls)
opts.auth_plugin = auth_plugin_cls(opts.auth_source)
del opts.auth_source
# Create and start the WebSockets proxy # Create and start the WebSockets proxy
libserver = opts.libserver libserver = opts.libserver
@@ -433,9 +511,13 @@ class LibProxyServer(ForkingMixIn, HTTPServer):
self.wrap_mode = kwargs.pop('wrap_mode', None) self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None) self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None) self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None) self.token_plugin = kwargs.pop('token_plugin', None)
self.auth_plugin = kwargs.pop('auth_plugin', None)
self.heartbeat = kwargs.pop('heartbeat', None)
self.token_plugin = None
self.auth_plugin = None
self.daemon = False self.daemon = False
self.target_cfg = None
# Server configuration # Server configuration
listen_host = kwargs.pop('listen_host', '') listen_host = kwargs.pop('listen_host', '')
@@ -456,8 +538,8 @@ class LibProxyServer(ForkingMixIn, HTTPServer):
if web: if web:
os.chdir(web) os.chdir(web)
HTTPServer.__init__(self, (listen_host, listen_port), HTTPServer.__init__(self, (listen_host, listen_port),
RequestHandlerClass) RequestHandlerClass)