Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f132f9d84 | |||
| 4e6938e9c2 | |||
| 68b1587ada | |||
| 0b906a1c3e | |||
| 6f0e06772a | |||
| 8a13c48428 | |||
| b2a93bf709 | |||
| 9bc002dcf9 | |||
| dc979f042c | |||
| 72b629838a | |||
| d947fb6e30 | |||
| 8dacbc974c | |||
| c415521214 | |||
| 5bd6554651 | |||
| 50cc65f1e4 | |||
| 3c04cf8c45 | |||
| 62c3a59192 | |||
| e401701311 | |||
| 714aa34e4e | |||
| 05cac26257 | |||
| 487db5f7c9 | |||
| f693871c58 | |||
| b445296816 | |||
| 1e2b5c2256 | |||
| 6c1543c05b | |||
| b7684e0914 | |||
| 12432c9b06 | |||
| 2051c2419f | |||
| cb6b309dad | |||
| addc2e6b20 | |||
| 69c04c819c | |||
| 558a6439f1 | |||
| 2b9e0a5794 | |||
| 3371090909 | |||
| 1f960d9f3c | |||
| 1221960baa | |||
| 2e57ee3159 | |||
| 04fd789a67 | |||
| d5216c3166 | |||
| 11f9a6cf10 | |||
| df10501615 | |||
| d94b944f93 | |||
| 52adf957b3 | |||
| 731fd20796 | |||
| 52c2e62535 | |||
| ac9d357c87 | |||
| ce07749223 | |||
| af10f458a1 | |||
| 69a8b928aa | |||
| 19b558e4cd | |||
| dcdf0a0b6c | |||
| 23045cb212 | |||
| acd276e1a2 | |||
| 999a133bc5 | |||
| f36877c684 | |||
| 303a71310c | |||
| 63209f84ca | |||
| d37e729587 | |||
| 12191d266b | |||
| 67a12a2394 | |||
| 6c526fd645 | |||
| 6c1a2e9032 |
+3
-1
@@ -4,8 +4,10 @@
|
|||||||
other/.lein-deps-sum
|
other/.lein-deps-sum
|
||||||
other/classes
|
other/classes
|
||||||
other/lib
|
other/lib
|
||||||
other/node_modules
|
other/js/node_modules
|
||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
target.cfg
|
target.cfg
|
||||||
target.cfg.d
|
target.cfg.d
|
||||||
|
.tox
|
||||||
|
*.egg-info
|
||||||
|
|||||||
+10
@@ -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
|
||||||
+28
@@ -1,6 +1,34 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
0.8.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Make websockify properly terminate children on SIGTERM (#226)
|
||||||
|
* Remove logging in signal handlers (this can cause Python to hang under certain conditions) (#219)
|
||||||
|
* Make it easier to log to a file (#205)
|
||||||
|
* Add support for IPv6 addresses in tokens in the TokenFile token plugins (#197)
|
||||||
|
* Improve auth plugin framework to enable better support for HTTP auth (#194, #201)
|
||||||
|
* Fix bug in JSONTokenAPI token plugin (#192)
|
||||||
|
* Fix a missing variable in the exception handler (#178)
|
||||||
|
|
||||||
|
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
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
* **PATCH RELEASE**: Fixes a bug causing file_only to not be passed properly
|
||||||
|
|
||||||
0.6.0 - Feb 18, 2014
|
0.6.0 - Feb 18, 2014
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
@@ -47,17 +47,29 @@ which is why the negotiation is necessary.
|
|||||||
|
|
||||||
### Encrypted WebSocket connections (wss://)
|
### Encrypted WebSocket connections (wss://)
|
||||||
|
|
||||||
To encrypt the traffic using the WebSocket 'wss://' URI scheme you
|
To encrypt the traffic using the WebSocket 'wss://' URI scheme you need to
|
||||||
need to generate a certificate for websockify to load. By default websockify
|
generate a certificate and key for Websockify to load. By default, Websockify
|
||||||
loads a certificate file name `self.pem` but the `--cert=CERT` option can
|
loads a certificate file name `self.pem` but the `--cert=CERT` and `--key=KEY`
|
||||||
override the file name. You can generate a self-signed certificate using
|
options can override the file name. You can generate a self-signed certificate
|
||||||
openssl. When asked for the common name, use the hostname of the server where
|
using openssl. When asked for the common name, use the hostname of the server
|
||||||
the proxy will be running:
|
where the proxy will be running:
|
||||||
|
|
||||||
```
|
```
|
||||||
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
|
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a self-signed certificate to work, you need to make your client/browser
|
||||||
|
understand it. You can do this by installing it as accepted certificate, or by
|
||||||
|
using that same certificate for a HTTPS connection to which you navigate first
|
||||||
|
and approve. Browsers generally don't give you the "trust certificate?" prompt
|
||||||
|
by opening a WSS socket with invalid certificate, hence you need to have it
|
||||||
|
acccept it by either of those two methods.
|
||||||
|
|
||||||
|
If you have a commercial/valid SSL certificate with one ore more intermediate
|
||||||
|
certificates, concat them into one file, server certificate first, then the
|
||||||
|
intermediate(s) from the CA, etc. Point to this file with the `--cert` option
|
||||||
|
and then also to the key with `--key`. Finally, use `--ssl-only` as needed.
|
||||||
|
|
||||||
|
|
||||||
### Websock Javascript library
|
### Websock Javascript library
|
||||||
|
|
||||||
@@ -103,11 +115,14 @@ These are not necessary for the basic operation.
|
|||||||
|
|
||||||
* Mini-webserver: websockify can detect and respond to normal web
|
* Mini-webserver: websockify can detect and respond to normal web
|
||||||
requests on the same port as the WebSockets proxy and Flash security
|
requests on the same port as the WebSockets proxy and Flash security
|
||||||
policy. This functionality is activate with the `--web DIR` option
|
policy. This functionality is activated with the `--web DIR` option
|
||||||
where DIR is the root of the web directory to serve.
|
where DIR is the root of the web directory to serve.
|
||||||
|
|
||||||
* Wrap a program: see the "Wrap a Program" section below.
|
* Wrap a program: see the "Wrap a Program" section below.
|
||||||
|
|
||||||
|
* Log files: websockify can save all logging information in a file.
|
||||||
|
This functionality is activated with the `--log-file FILE` option
|
||||||
|
where FILE is the file where the logs should be saved.
|
||||||
|
|
||||||
### Implementations of websockify
|
### Implementations of websockify
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -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,13 +2,13 @@
|
|||||||
"author": "Joel Martin <github@martintribe.org> (http://github.com/kanaka)",
|
"author": "Joel Martin <github@martintribe.org> (http://github.com/kanaka)",
|
||||||
"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.0",
|
||||||
"version": "0.6.0",
|
"version": "0.8.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"
|
||||||
},
|
},
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": ">=0.4.27",
|
"ws": ">=0.4.27",
|
||||||
"base64": "latest",
|
|
||||||
"optimist": "latest",
|
"optimist": "latest",
|
||||||
"policyfile": "latest"
|
"policyfile": "latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import websockify
|
import websockify
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
version = '0.6.0'
|
version = '0.8.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',
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mox
|
||||||
|
nose
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
""" Unit tests for Authentication plugins"""
|
||||||
|
|
||||||
|
from websockify.auth_plugins import BasicHTTPAuth, AuthenticationError
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class BasicHTTPAuthTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.plugin = BasicHTTPAuth('Aladdin:open sesame')
|
||||||
|
|
||||||
|
def test_no_auth(self):
|
||||||
|
headers = {}
|
||||||
|
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
|
||||||
|
|
||||||
|
def test_invalid_password(self):
|
||||||
|
headers = {'Authorization': 'Basic QWxhZGRpbjpzZXNhbWUgc3RyZWV0'}
|
||||||
|
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
|
||||||
|
|
||||||
|
def test_valid_password(self):
|
||||||
|
headers = {'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}
|
||||||
|
self.plugin.authenticate(headers, 'localhost', '1234')
|
||||||
|
|
||||||
|
def test_garbage_auth(self):
|
||||||
|
headers = {'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
|
||||||
|
self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234')
|
||||||
+338
-151
@@ -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')
|
||||||
|
|||||||
@@ -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, 'send_auth_error',
|
||||||
web_socket_proxy.started()
|
staticmethod(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.validate_connection()
|
||||||
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(response_msg="some_error")
|
||||||
|
|
||||||
def mock_select(*args, **kwargs):
|
self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error',
|
||||||
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.validate_connection)
|
||||||
|
|
||||||
|
self.handler.server.target_host = "someotherhost"
|
||||||
|
self.handler.validate_connection()
|
||||||
|
|
||||||
self.stubs.Set(select, 'select', mock_select)
|
|
||||||
self.assertRaises(Exception, web_socket_proxy.new_websocket_client)
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
from websocket import *
|
from websockify.websocket import *
|
||||||
from websocketproxy import *
|
from websockify.websocketproxy import *
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
class BasePlugin(object):
|
||||||
|
def __init__(self, src=None):
|
||||||
|
self.source = src
|
||||||
|
|
||||||
|
def authenticate(self, headers, target_host, target_port):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationError(Exception):
|
||||||
|
def __init__(self, log_msg=None, response_code=403, response_headers={}, response_msg=None):
|
||||||
|
self.code = response_code
|
||||||
|
self.headers = response_headers
|
||||||
|
self.msg = response_msg
|
||||||
|
|
||||||
|
if log_msg is None:
|
||||||
|
log_msg = response_msg
|
||||||
|
|
||||||
|
super(AuthenticationError, self).__init__('%s %s' % (self.code, log_msg))
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidOriginError(AuthenticationError):
|
||||||
|
def __init__(self, expected, actual):
|
||||||
|
self.expected_origin = expected
|
||||||
|
self.actual_origin = actual
|
||||||
|
|
||||||
|
super(InvalidOriginError, self).__init__(
|
||||||
|
response_msg='Invalid Origin',
|
||||||
|
log_msg="Invalid Origin Header: Expected one of "
|
||||||
|
"%s, got '%s'" % (expected, actual))
|
||||||
|
|
||||||
|
|
||||||
|
class BasicHTTPAuth(object):
|
||||||
|
"""Verifies Basic Auth headers. Specify src as username:password"""
|
||||||
|
|
||||||
|
def __init__(self, src=None):
|
||||||
|
self.src = src
|
||||||
|
|
||||||
|
def authenticate(self, headers, target_host, target_port):
|
||||||
|
import base64
|
||||||
|
auth_header = headers.get('Authorization')
|
||||||
|
if auth_header:
|
||||||
|
if not auth_header.startswith('Basic '):
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_pass_raw = base64.b64decode(auth_header[6:])
|
||||||
|
except TypeError:
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication
|
||||||
|
user_pass_as_text = user_pass_raw.decode('ISO-8859-1')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
user_pass = user_pass_as_text.split(':', 1)
|
||||||
|
if len(user_pass) != 2:
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
if not self.validate_creds(*user_pass):
|
||||||
|
raise AuthenticationError(response_code=403)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise AuthenticationError(response_code=401,
|
||||||
|
response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'})
|
||||||
|
|
||||||
|
def validate_creds(self, username, password):
|
||||||
|
if '%s:%s' % (username, password) == self.src:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
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.get('Origin', None)
|
||||||
|
if origin is None or origin not in self.source:
|
||||||
|
raise InvalidOriginError(expected=self.source, actual=origin)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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().rsplit(':', 1)
|
||||||
|
|
||||||
|
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):
|
||||||
|
resp_json = resp.json()
|
||||||
|
return (resp_json['host'], resp_json['port'])
|
||||||
+120
-41
@@ -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,13 +104,18 @@ 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)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
self.logger.info("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), format % args))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def unmask(buf, hlen, plen):
|
def unmask(buf, hlen, plen):
|
||||||
pstart = hlen + 4
|
pstart = hlen + 4
|
||||||
@@ -118,20 +123,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 +181,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 +247,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 +359,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 +374,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 +411,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,18 +468,22 @@ 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'):
|
||||||
|
|
||||||
|
# ensure connection is authorized, and determine the target
|
||||||
|
self.validate_connection()
|
||||||
|
|
||||||
if not self.do_websocket_handshake():
|
if not self.do_websocket_handshake():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -468,21 +505,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 +532,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,11 +551,15 @@ 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")
|
||||||
|
|
||||||
|
def validate_connection(self):
|
||||||
|
""" Ensure that the connection is a valid connection, and set the target. """
|
||||||
|
pass
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
if self.only_upgrade:
|
if self.only_upgrade:
|
||||||
self.send_error(405, "Method Not Allowed")
|
self.send_error(405, "Method Not Allowed")
|
||||||
@@ -560,14 +601,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
|
||||||
@@ -581,7 +622,9 @@ class WebSocketServer(object):
|
|||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.idle_timeout = idle_timeout
|
self.idle_timeout = idle_timeout
|
||||||
self.traffic = traffic
|
self.traffic = traffic
|
||||||
|
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
|
||||||
self.handler_id = 1
|
self.handler_id = 1
|
||||||
@@ -592,6 +635,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 = ''
|
||||||
@@ -618,7 +662,10 @@ class WebSocketServer(object):
|
|||||||
self.listen_host, self.listen_port)
|
self.listen_host, self.listen_port)
|
||||||
self.msg(" - Flash security policy server")
|
self.msg(" - Flash security policy server")
|
||||||
if self.web:
|
if self.web:
|
||||||
self.msg(" - Web server. Web root: %s", self.web)
|
if self.file_only:
|
||||||
|
self.msg(" - Web server (no directory listings). Web root: %s", self.web)
|
||||||
|
else:
|
||||||
|
self.msg(" - Web server. Web root: %s", self.web)
|
||||||
if ssl:
|
if ssl:
|
||||||
if os.path.exists(self.cert):
|
if os.path.exists(self.cert):
|
||||||
self.msg(" - SSL/TLS support")
|
self.msg(" - SSL/TLS support")
|
||||||
@@ -662,7 +709,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)
|
||||||
@@ -693,7 +740,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)
|
||||||
|
|
||||||
@@ -701,6 +748,10 @@ class WebSocketServer(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def daemonize(keepfd=None, chdir='/'):
|
def daemonize(keepfd=None, chdir='/'):
|
||||||
|
|
||||||
|
if keepfd is None:
|
||||||
|
keepfd = []
|
||||||
|
|
||||||
os.umask(0)
|
os.umask(0)
|
||||||
if chdir:
|
if chdir:
|
||||||
os.chdir(chdir)
|
os.chdir(chdir)
|
||||||
@@ -723,7 +774,7 @@ class WebSocketServer(object):
|
|||||||
if maxfd == resource.RLIM_INFINITY: maxfd = 256
|
if maxfd == resource.RLIM_INFINITY: maxfd = 256
|
||||||
for fd in reversed(range(maxfd)):
|
for fd in reversed(range(maxfd)):
|
||||||
try:
|
try:
|
||||||
if fd != keepfd:
|
if fd not in keepfd:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
except OSError:
|
except OSError:
|
||||||
_, exc, _ = sys.exc_info()
|
_, exc, _ = sys.exc_info()
|
||||||
@@ -753,7 +804,7 @@ class WebSocketServer(object):
|
|||||||
"""
|
"""
|
||||||
ready = select.select([sock], [], [], 3)[0]
|
ready = select.select([sock], [], [], 3)[0]
|
||||||
|
|
||||||
|
|
||||||
if not ready:
|
if not ready:
|
||||||
raise self.EClose("ignoring socket not ready")
|
raise self.EClose("ignoring socket not ready")
|
||||||
# Peek, but do not read the data so that we have a opportunity
|
# Peek, but do not read the data so that we have a opportunity
|
||||||
@@ -761,7 +812,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/>")):
|
||||||
@@ -844,11 +895,14 @@ 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()))
|
# TODO: figure out a way to actually log this information without
|
||||||
|
# calling `log` in the signal handlers
|
||||||
|
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)
|
||||||
self.vmsg("Got SIGCHLD, reaping zombies")
|
# TODO: figure out a way to actually log this information without
|
||||||
|
# calling `log` in the signal handlers
|
||||||
try:
|
try:
|
||||||
result = os.waitpid(-1, os.WNOHANG)
|
result = os.waitpid(-1, os.WNOHANG)
|
||||||
while result[0]:
|
while result[0]:
|
||||||
@@ -858,16 +912,18 @@ class WebSocketServer(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def do_SIGINT(self, sig, stack):
|
def do_SIGINT(self, sig, stack):
|
||||||
self.msg("Got SIGINT, exiting")
|
# TODO: figure out a way to actually log this information without
|
||||||
|
# calling `log` in the signal handlers
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|
||||||
def do_SIGTERM(self, sig, stack):
|
def do_SIGTERM(self, sig, stack):
|
||||||
self.msg("Got SIGTERM, exiting")
|
# TODO: figure out a way to actually log this information without
|
||||||
|
# calling `log` in the signal handlers
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|
||||||
def top_new_client(self, startsock, address):
|
def top_new_client(self, startsock, address):
|
||||||
""" Do something with a WebSockets client connection. """
|
""" Do something with a WebSockets client connection. """
|
||||||
# handler process
|
# handler process
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@@ -890,6 +946,18 @@ class WebSocketServer(object):
|
|||||||
# Original socket closed by caller
|
# Original socket closed by caller
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
def get_log_fd(self):
|
||||||
|
"""
|
||||||
|
Get file descriptors for the loggers.
|
||||||
|
They should not be closed when the process is forked.
|
||||||
|
"""
|
||||||
|
descriptors = []
|
||||||
|
for handler in self.logger.parent.handlers:
|
||||||
|
if isinstance(handler, logging.FileHandler):
|
||||||
|
descriptors.append(handler.stream.fileno())
|
||||||
|
|
||||||
|
return descriptors
|
||||||
|
|
||||||
def start_server(self):
|
def start_server(self):
|
||||||
"""
|
"""
|
||||||
Daemonize if requested. Listen for for connections. Run
|
Daemonize if requested. Listen for for connections. Run
|
||||||
@@ -905,7 +973,9 @@ class WebSocketServer(object):
|
|||||||
tcp_keepintvl=self.tcp_keepintvl)
|
tcp_keepintvl=self.tcp_keepintvl)
|
||||||
|
|
||||||
if self.daemon:
|
if self.daemon:
|
||||||
self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
|
keepfd = self.get_log_fd()
|
||||||
|
keepfd.append(lsock.fileno())
|
||||||
|
self.daemonize(keepfd=keepfd, chdir=self.web)
|
||||||
|
|
||||||
self.started() # Some things need to happen after daemonizing
|
self.started() # Some things need to happen after daemonizing
|
||||||
|
|
||||||
@@ -1009,8 +1079,17 @@ class WebSocketServer(object):
|
|||||||
|
|
||||||
except (self.Terminate, SystemExit, KeyboardInterrupt):
|
except (self.Terminate, SystemExit, KeyboardInterrupt):
|
||||||
self.msg("In exit")
|
self.msg("In exit")
|
||||||
|
# terminate all child processes
|
||||||
|
if multiprocessing and not self.run_once:
|
||||||
|
children = multiprocessing.active_children()
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
self.msg("Terminating child %s" % child.pid)
|
||||||
|
child.terminate()
|
||||||
|
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
|
exc = sys.exc_info()[1]
|
||||||
self.msg("handler exception: %s", str(exc))
|
self.msg("handler exception: %s", str(exc))
|
||||||
self.vmsg("exception", exc_info=True)
|
self.vmsg("exception", exc_info=True)
|
||||||
|
|
||||||
|
|||||||
+167
-57
@@ -11,13 +11,14 @@ 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
|
||||||
|
from websockify import auth_plugins as auth
|
||||||
try:
|
try:
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
except:
|
except:
|
||||||
@@ -37,15 +38,34 @@ Traffic Legend:
|
|||||||
< - Client send
|
< - Client send
|
||||||
<. - Client send partial
|
<. - Client send partial
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def send_auth_error(self, ex):
|
||||||
|
self.send_response(ex.code, ex.msg)
|
||||||
|
self.send_header('Content-Type', 'text/html')
|
||||||
|
for name, val in ex.headers.items():
|
||||||
|
self.send_header(name, val)
|
||||||
|
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def validate_connection(self):
|
||||||
|
if self.server.token_plugin:
|
||||||
|
(self.server.target_host, self.server.target_port) = self.get_target(self.server.token_plugin, self.path)
|
||||||
|
|
||||||
|
if self.server.auth_plugin:
|
||||||
|
try:
|
||||||
|
self.server.auth_plugin.authenticate(
|
||||||
|
headers=self.headers, target_host=self.server.target_host,
|
||||||
|
target_port=self.server.target_port)
|
||||||
|
except auth.AuthenticationError:
|
||||||
|
ex = sys.exc_info()[1]
|
||||||
|
self.send_auth_error(ex)
|
||||||
|
raise
|
||||||
|
|
||||||
def new_websocket_client(self):
|
def new_websocket_client(self):
|
||||||
"""
|
"""
|
||||||
Called after a new WebSocket connection has been established.
|
Called after a new WebSocket connection has been established.
|
||||||
"""
|
"""
|
||||||
# Checks if we receive a token, and look
|
# Checking for a token is done in validate_connection()
|
||||||
# for a valid target for it then
|
|
||||||
if self.server.target_cfg:
|
|
||||||
(self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path)
|
|
||||||
|
|
||||||
# Connect to the target
|
# Connect to the target
|
||||||
if self.server.wrap_cmd:
|
if self.server.wrap_cmd:
|
||||||
@@ -73,15 +93,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 +110,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 +131,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 +177,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 +200,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 +225,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 +285,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 +386,69 @@ 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")
|
||||||
|
parser.add_option("--log-file", metavar="FILE",
|
||||||
|
dest="log_file",
|
||||||
|
help="File where logs will be saved")
|
||||||
|
|
||||||
|
|
||||||
(opts, args) = parser.parse_args()
|
(opts, args) = parser.parse_args()
|
||||||
|
|
||||||
|
if opts.log_file:
|
||||||
|
opts.log_file = os.path.abspath(opts.log_file)
|
||||||
|
handler = logging.FileHandler(opts.log_file)
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
handler.setFormatter(logging.Formatter("%(message)s"))
|
||||||
|
logging.getLogger(WebSocketProxy.log_prefix).addHandler(handler)
|
||||||
|
|
||||||
|
del opts.log_file
|
||||||
|
|
||||||
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 +473,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 +485,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 +539,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 +566,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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user