26 Commits

Author SHA1 Message Date
Pierre Ossman 06e61fa4cc websockify 0.11.0 2022-12-16 13:11:04 +01:00
Pierre Ossman 4f9d496dfe Use local websockify when building container
It's very surprising to get some external copy of websockify when you
are building an image in your local websockify source tree. Make sure we
are using the local copy of everything.
2022-12-16 13:10:58 +01:00
Pierre Ossman 1979e92f0e Also require all token plugin requirements
Token plugins are technically optional, but if you are installing
websockify via pip then all of these are available anyway. So let's make
things simple for users.
2022-12-16 13:08:23 +01:00
Adam Tilghman d2affc73b5 Insert rebinder at the head of the (possibly empty) LD_PRELOAD pathlist,
required for use cases relying on other preloaded libraries e.g. nss_wrapper.
2022-11-16 16:35:39 -08:00
Pierre Ossman 27ee353401 Don't include default message to send_error()
Python can provide this for us, so avoid duplication.
2022-11-16 15:28:18 +01:00
Pierre Ossman 39c7cb0115 Merge branch 'http_api' of https://github.com/CendioOssman/websockify 2022-11-08 14:29:12 +01:00
Linn Mattsson be7b868518 Remove logging from handle_upgrade()
The logging should be handled directly in send_response() instead, which
is the default of Python's built-in send_response(). Remove this manual
logging to avoid logging the same call twice.
2022-11-08 14:24:46 +01:00
Linn Mattsson 4695f96728 Add new websocket class HttpWebSocket
This class acts as a glue between websocket and http functionality by
taking a 'request_handler' and using its functions for send_response(),
send_header() and end_headers().
2022-11-08 14:24:46 +01:00
Linn Mattsson 832ae23f00 Make websocket's API more intuitive
Functions connect() and accept() are using http functionality, like
sending requests and headers. Let's create separate functions with more
intuitive names for these calls. This allows subclasses to override
these functions, as well as makes the code easier to understand at a
glance.
2022-11-08 14:05:46 +01:00
Jokin c123bfbbff Add requests module 2022-10-20 11:28:34 +00:00
Manoj Ghosh 70579756f3 expose --file-only option to disable dir listing 2022-10-18 04:10:44 -07:00
Pierre Ossman e9c80aa32a Merge branch 'master' of https://github.com/msnatepg/websockify 2022-10-10 10:13:01 +02:00
Maximilian Sesterhenn caef680fff ensure that queues are empty when closing connections 2022-10-07 14:24:53 +02:00
Pierre Ossman 7133f85df6 Merge branch 'verbose_logging' of https://github.com/javicacheiro/websockify 2022-08-18 11:11:22 +02:00
Samuel Mannehed 33910d758d Merge pull request #521 from javicacheiro/fix_jwcrypto
Tests break with jwcrypto>=1.3
2022-05-26 15:59:39 +02:00
Javier Cacheiro 0f17500348 Support for jwcrypto>=1.3 2022-05-25 12:40:29 +02:00
Pierre Ossman 4b194636e2 Merge branch 'dockerfile' of https://github.com/javicacheiro/websockify 2022-05-11 14:23:40 +02:00
Javier Cacheiro ffa31d6c2c When using verbose set root log level to DEBUG 2022-05-03 09:58:50 +02:00
Javier Cacheiro dec26a6a2d Docker support 2022-04-22 13:21:57 +02:00
Pierre Ossman d96ecf0859 Add more alternatives to usage string
If you use a token plugin, or a Unix socket target, then you should no
longer specify a target on the command line. Add these to the usage
string to make this clear.
2022-04-22 10:19:54 +02:00
Pierre Ossman d0ad4a1b2a Merge branches 'use_logging' and 'fix_logging_configuration' of https://github.com/javicacheiro/websockify 2022-04-14 09:15:21 +02:00
Javier Cacheiro e1f903b9e8 Apply configuration to the root logger 2022-04-13 17:02:05 +02:00
Javier Cacheiro 62ac4aeb03 Use logging instead of directly printing messages to sys.stderr 2022-04-13 16:12:31 +02:00
Pierre Ossman e4cff3746d Explicitly install old wrapt on Python 3.4
Something is broken in pip so it installs a wrapt that doesn't support
Python 3.4. Work around this by manually request a version that is known
to work.
2022-04-13 12:50:25 +02:00
Javier Cacheiro d5e8d709d7 Add tests for TokenRedis 2022-04-12 10:58:42 +02:00
Pierre Ossman dc345815c0 Use RSA-OAEP instead of RSA1_5 for jwt tests
The latest version of jwcrypto has disabled RSA1_5 by default, making
the tests fail.
2021-07-23 09:38:58 +02:00
14 changed files with 301 additions and 78 deletions
+6
View File
@@ -1,6 +1,12 @@
Changes Changes
======= =======
0.11.0
------
* Command line now supports disabling directory listings
* Basic Dockerfile included
0.10.0 0.10.0
------ ------
+32
View File
@@ -168,3 +168,35 @@ before running `python3 setup.py install`.
Afterwards, websockify should be available in your path. Run Afterwards, websockify should be available in your path. Run
`websockify --help` to confirm it's installed correctly. `websockify --help` to confirm it's installed correctly.
### Running with Docker/Podman
You can also run websockify using Docker, Podman, Singularity, udocker or
your favourite container runtime that support OCI container images.
The entrypoint of the image is the `run` command.
To build the image:
```
./docker/build.sh
```
Once built you can just launch it with the same
arguments you would give to the `run` command and taking care of
assigning the port mappings:
```
docker run -it --rm -p <port>:<container_port> novnc/websockify <container_port> <run_arguments>
```
For example to forward traffic from local port 7000 to 10.1.1.1:5902
you can use:
```
docker run -it --rm -p 7000:80 novnc/websockify 80 10.1.1.1:5902
```
If you need to include files, like for example for the `--web` or `--cert`
options you can just mount the required files in the `/data` volume and then
you can reference them in the usual way:
```
docker run -it --rm -p 443:443 -v websockify-data:/data novnc/websockify --cert /data/self.pem --web /data/noVNC :443 --token-plugin TokenRedis --token-source myredis.local:6379 --ssl-only --ssl-version tlsv1_2
```
+16
View File
@@ -0,0 +1,16 @@
FROM python
COPY websockify-*.tar.gz /
RUN python3 -m pip install websockify-*.tar.gz
RUN rm -rf /websockify-* /root/.cache
VOLUME /data
EXPOSE 80
EXPOSE 443
WORKDIR /opt/websockify
ENTRYPOINT ["/usr/local/bin/websockify"]
CMD ["--help"]
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -e -x
cd "$(dirname "$0")"
(cd .. && python3 setup.py sdist --dist-dir docker/)
docker build -t novnc/websockify .
+6 -2
View File
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '0.10.0' version = '0.11.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"
@@ -29,7 +29,11 @@ setup(name=name,
packages=['websockify'], packages=['websockify'],
include_package_data=True, include_package_data=True,
install_requires=['numpy'], install_requires=[
'numpy', 'requests',
'jwcrypto',
'redis', 'simplejson',
],
zip_safe=False, zip_safe=False,
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
+1
View File
@@ -4,3 +4,4 @@ jwcrypto
six six
redis redis
simplejson simplejson
wrapt<=1.12.1;python_version<="3.4"
+38 -12
View File
@@ -4,9 +4,9 @@
import unittest import unittest
from unittest.mock import patch, mock_open, MagicMock from unittest.mock import patch, mock_open, MagicMock
from jwcrypto import jwt from jwcrypto import jwt, jwk
from websockify.token_plugins import ReadOnlyTokenFile, JWTTokenApi from websockify.token_plugins import ReadOnlyTokenFile, JWTTokenApi, TokenRedis
class ReadOnlyTokenFileTestCase(unittest.TestCase): class ReadOnlyTokenFileTestCase(unittest.TestCase):
patch('os.path.isdir', MagicMock(return_value=False)) patch('os.path.isdir', MagicMock(return_value=False))
@@ -56,7 +56,7 @@ class JWSTokenTestCase(unittest.TestCase):
def test_asymmetric_jws_token_plugin(self): def test_asymmetric_jws_token_plugin(self):
plugin = JWTTokenApi("./tests/fixtures/public.pem") plugin = JWTTokenApi("./tests/fixtures/public.pem")
key = jwt.JWK() key = jwk.JWK()
private_key = open("./tests/fixtures/private.pem", "rb").read() private_key = open("./tests/fixtures/private.pem", "rb").read()
key.import_from_pem(private_key) key.import_from_pem(private_key)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"})
@@ -71,7 +71,7 @@ class JWSTokenTestCase(unittest.TestCase):
def test_asymmetric_jws_token_plugin_with_illigal_key_exception(self): def test_asymmetric_jws_token_plugin_with_illigal_key_exception(self):
plugin = JWTTokenApi("wrong.pub") plugin = JWTTokenApi("wrong.pub")
key = jwt.JWK() key = jwk.JWK()
private_key = open("./tests/fixtures/private.pem", "rb").read() private_key = open("./tests/fixtures/private.pem", "rb").read()
key.import_from_pem(private_key) key.import_from_pem(private_key)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"})
@@ -85,7 +85,7 @@ class JWSTokenTestCase(unittest.TestCase):
def test_jwt_valid_time(self, mock_time): def test_jwt_valid_time(self, mock_time):
plugin = JWTTokenApi("./tests/fixtures/public.pem") plugin = JWTTokenApi("./tests/fixtures/public.pem")
key = jwt.JWK() key = jwk.JWK()
private_key = open("./tests/fixtures/private.pem", "rb").read() private_key = open("./tests/fixtures/private.pem", "rb").read()
key.import_from_pem(private_key) key.import_from_pem(private_key)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 })
@@ -102,7 +102,7 @@ class JWSTokenTestCase(unittest.TestCase):
def test_jwt_early_time(self, mock_time): def test_jwt_early_time(self, mock_time):
plugin = JWTTokenApi("./tests/fixtures/public.pem") plugin = JWTTokenApi("./tests/fixtures/public.pem")
key = jwt.JWK() key = jwk.JWK()
private_key = open("./tests/fixtures/private.pem", "rb").read() private_key = open("./tests/fixtures/private.pem", "rb").read()
key.import_from_pem(private_key) key.import_from_pem(private_key)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 })
@@ -117,7 +117,7 @@ class JWSTokenTestCase(unittest.TestCase):
def test_jwt_late_time(self, mock_time): def test_jwt_late_time(self, mock_time):
plugin = JWTTokenApi("./tests/fixtures/public.pem") plugin = JWTTokenApi("./tests/fixtures/public.pem")
key = jwt.JWK() key = jwk.JWK()
private_key = open("./tests/fixtures/private.pem", "rb").read() private_key = open("./tests/fixtures/private.pem", "rb").read()
key.import_from_pem(private_key) key.import_from_pem(private_key)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 }) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port", 'nbf': 100, 'exp': 200 })
@@ -132,7 +132,7 @@ class JWSTokenTestCase(unittest.TestCase):
plugin = JWTTokenApi("./tests/fixtures/symmetric.key") plugin = JWTTokenApi("./tests/fixtures/symmetric.key")
secret = open("./tests/fixtures/symmetric.key").read() secret = open("./tests/fixtures/symmetric.key").read()
key = jwt.JWK() key = jwk.JWK()
key.import_key(kty="oct",k=secret) key.import_key(kty="oct",k=secret)
jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"})
jwt_token.make_signed_token(key) jwt_token.make_signed_token(key)
@@ -147,7 +147,7 @@ class JWSTokenTestCase(unittest.TestCase):
plugin = JWTTokenApi("wrong_sauce") plugin = JWTTokenApi("wrong_sauce")
secret = open("./tests/fixtures/symmetric.key").read() secret = open("./tests/fixtures/symmetric.key").read()
key = jwt.JWK() key = jwk.JWK()
key.import_key(kty="oct",k=secret) key.import_key(kty="oct",k=secret)
jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"})
jwt_token.make_signed_token(key) jwt_token.make_signed_token(key)
@@ -159,15 +159,15 @@ class JWSTokenTestCase(unittest.TestCase):
def test_asymmetric_jwe_token_plugin(self): def test_asymmetric_jwe_token_plugin(self):
plugin = JWTTokenApi("./tests/fixtures/private.pem") plugin = JWTTokenApi("./tests/fixtures/private.pem")
private_key = jwt.JWK() private_key = jwk.JWK()
public_key = jwt.JWK() public_key = jwk.JWK()
private_key_data = open("./tests/fixtures/private.pem", "rb").read() private_key_data = open("./tests/fixtures/private.pem", "rb").read()
public_key_data = open("./tests/fixtures/public.pem", "rb").read() public_key_data = open("./tests/fixtures/public.pem", "rb").read()
private_key.import_from_pem(private_key_data) private_key.import_from_pem(private_key_data)
public_key.import_from_pem(public_key_data) public_key.import_from_pem(public_key_data)
jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"})
jwt_token.make_signed_token(private_key) jwt_token.make_signed_token(private_key)
jwe_token = jwt.JWT(header={"alg": "RSA1_5", "enc": "A256CBC-HS512"}, jwe_token = jwt.JWT(header={"alg": "RSA-OAEP", "enc": "A256CBC-HS512"},
claims=jwt_token.serialize()) claims=jwt_token.serialize())
jwe_token.make_encrypted_token(public_key) jwe_token.make_encrypted_token(public_key)
@@ -177,3 +177,29 @@ class JWSTokenTestCase(unittest.TestCase):
self.assertEqual(result[0], "remote_host") self.assertEqual(result[0], "remote_host")
self.assertEqual(result[1], "remote_port") self.assertEqual(result[1], "remote_port")
class TokenRedisTestCase(unittest.TestCase):
@patch('redis.Redis')
def test_empty(self, mock_redis):
plugin = TokenRedis('127.0.0.1:1234')
instance = mock_redis.return_value
instance.get.return_value = None
result = plugin.lookup('testhost')
instance.get.assert_called_once_with('testhost')
self.assertIsNone(result)
@patch('redis.Redis')
def test_simple(self, mock_redis):
plugin = TokenRedis('127.0.0.1:1234')
instance = mock_redis.return_value
instance.get.return_value = b'{"host": "remote_host:remote_port"}'
result = plugin.lookup('testhost')
instance.get.assert_called_once_with('testhost')
self.assertIsNotNone(result)
self.assertEqual(result[0], 'remote_host')
self.assertEqual(result[1], 'remote_port')
+69
View File
@@ -0,0 +1,69 @@
""" Unit tests for websocketserver """
import unittest
from unittest.mock import patch, MagicMock
from websockify.websocketserver import HttpWebSocket
class HttpWebSocketTest(unittest.TestCase):
@patch("websockify.websocketserver.WebSocket.__init__", autospec=True)
def test_constructor(self, websock):
# Given
req_obj = MagicMock()
# When
sock = HttpWebSocket(req_obj)
# Then
websock.assert_called_once_with(sock)
self.assertEqual(sock.request_handler, req_obj)
@patch("websockify.websocketserver.WebSocket.__init__", MagicMock(autospec=True))
def test_send_response(self):
# Given
req_obj = MagicMock()
sock = HttpWebSocket(req_obj)
# When
sock.send_response(200, "message")
# Then
req_obj.send_response.assert_called_once_with(200, "message")
@patch("websockify.websocketserver.WebSocket.__init__", MagicMock(autospec=True))
def test_send_response_default_message(self):
# Given
req_obj = MagicMock()
sock = HttpWebSocket(req_obj)
# When
sock.send_response(200)
# Then
req_obj.send_response.assert_called_once_with(200, None)
@patch("websockify.websocketserver.WebSocket.__init__", MagicMock(autospec=True))
def test_send_header(self):
# Given
req_obj = MagicMock()
sock = HttpWebSocket(req_obj)
# When
sock.send_header("keyword", "value")
# Then
req_obj.send_header.assert_called_once_with("keyword", "value")
@patch("websockify.websocketserver.WebSocket.__init__", MagicMock(autospec=True))
def test_end_headers(self):
# Given
req_obj = MagicMock()
sock = HttpWebSocket(req_obj)
# When
sock.end_headers()
# Then
req_obj.end_headers.assert_called_once_with()
+2 -2
View File
@@ -86,7 +86,7 @@ class WebSockifyRequestHandlerTestCase(unittest.TestCase):
FakeSocket(b'GET /tmp.txt HTTP/1.1'), '127.0.0.1', server) FakeSocket(b'GET /tmp.txt HTTP/1.1'), '127.0.0.1', server)
handler.do_GET() handler.do_GET()
send_error.assert_called_with(405, ANY) send_error.assert_called_with(405)
@patch('websockify.websockifyserver.WebSockifyRequestHandler.send_error') @patch('websockify.websockifyserver.WebSockifyRequestHandler.send_error')
def test_list_dir_with_file_only_returns_error(self, send_error): def test_list_dir_with_file_only_returns_error(self, send_error):
@@ -96,7 +96,7 @@ class WebSockifyRequestHandlerTestCase(unittest.TestCase):
handler.path = '/' handler.path = '/'
handler.do_GET() handler.do_GET()
send_error.assert_called_with(404, ANY) send_error.assert_called_with(404)
class WebSockifyServerTestCase(unittest.TestCase): class WebSockifyServerTestCase(unittest.TestCase):
+29 -25
View File
@@ -1,8 +1,12 @@
import logging
import os import os
import sys import sys
import time import time
import re import re
logger = logging.getLogger(__name__)
class BasePlugin(): class BasePlugin():
def __init__(self, src): def __init__(self, src):
self.source = src self.source = src
@@ -35,7 +39,7 @@ class ReadOnlyTokenFile(BasePlugin):
tok, target = re.split(':\s', line) tok, target = re.split(':\s', line)
self._targets[tok] = target.strip().rsplit(':', 1) self._targets[tok] = target.strip().rsplit(':', 1)
except ValueError: except ValueError:
print("Syntax error in %s on line %d" % (self.source, index), file=sys.stderr) logger.error("Syntax error in %s on line %d" % (self.source, index))
index += 1 index += 1
def lookup(self, token): def lookup(self, token):
@@ -99,16 +103,16 @@ class JWTTokenApi(BasePlugin):
def lookup(self, token): def lookup(self, token):
try: try:
from jwcrypto import jwt from jwcrypto import jwt, jwk
import json import json
key = jwt.JWK() key = jwk.JWK()
try: try:
with open(self.source, 'rb') as key_file: with open(self.source, 'rb') as key_file:
key_data = key_file.read() key_data = key_file.read()
except Exception as e: except Exception as e:
print("Error loading key file: %s" % str(e), file=sys.stderr) logger.error("Error loading key file: %s" % str(e))
return None return None
try: try:
@@ -117,7 +121,7 @@ class JWTTokenApi(BasePlugin):
try: try:
key.import_key(k=key_data.decode('utf-8'),kty='oct') key.import_key(k=key_data.decode('utf-8'),kty='oct')
except: except:
print('Failed to correctly parse key data!', file=sys.stderr) logger.error('Failed to correctly parse key data!')
return None return None
try: try:
@@ -129,40 +133,40 @@ class JWTTokenApi(BasePlugin):
token = jwt.JWT(key=key, jwt=token.claims) token = jwt.JWT(key=key, jwt=token.claims)
parsed = json.loads(token.claims) parsed = json.loads(token.claims)
if 'nbf' in parsed: if 'nbf' in parsed:
# Not Before is present, so we need to check it # Not Before is present, so we need to check it
if time.time() < parsed['nbf']: if time.time() < parsed['nbf']:
print('Token can not be used yet!', file=sys.stderr) logger.warning('Token can not be used yet!')
return None return None
if 'exp' in parsed: if 'exp' in parsed:
# Expiration time is present, so we need to check it # Expiration time is present, so we need to check it
if time.time() > parsed['exp']: if time.time() > parsed['exp']:
print('Token has expired!', file=sys.stderr) logger.warning('Token has expired!')
return None return None
return (parsed['host'], parsed['port']) return (parsed['host'], parsed['port'])
except Exception as e: except Exception as e:
print("Failed to parse token: %s" % str(e), file=sys.stderr) logger.error("Failed to parse token: %s" % str(e))
return None return None
except ImportError as e: except ImportError:
print("package jwcrypto not found, are you sure you've installed it correctly?", file=sys.stderr) logger.error("package jwcrypto not found, are you sure you've installed it correctly?")
return None return None
class TokenRedis(): class TokenRedis():
""" """
The TokenRedis plugin expects the format of the data in a form of json. The TokenRedis plugin expects the format of the data in a form of json.
Prepare data with: Prepare data with:
redis-cli set hello '{"host":"127.0.0.1:5000"}' redis-cli set hello '{"host":"127.0.0.1:5000"}'
Verify with: Verify with:
redis-cli --raw get hello redis-cli --raw get hello
Spawn a test "server" using netcat Spawn a test "server" using netcat
nc -l 5000 -v nc -l 5000 -v
Note: you have to install also the 'redis' and 'simplejson' modules Note: you have to install also the 'redis' and 'simplejson' modules
pip install redis simplejson pip install redis simplejson
""" """
@@ -172,14 +176,14 @@ class TokenRedis():
import redis import redis
import simplejson import simplejson
self._server, self._port = src.split(":") self._server, self._port = src.split(":")
print("TokenRedis backend initilized (%s:%s)" % logger.info("TokenRedis backend initilized (%s:%s)" %
(self._server, self._port), file=sys.stderr) (self._server, self._port))
except ValueError: except ValueError:
print("The provided --token-source='%s' is not in an expected format <host>:<port>" % logger.error("The provided --token-source='%s' is not in an expected format <host>:<port>" %
src, file=sys.stderr) src)
sys.exit() sys.exit()
except ImportError: except ImportError:
print("package redis or simplejson not found, are you sure you've installed them correctly?", file=sys.stderr) logger.error("package redis or simplejson not found, are you sure you've installed them correctly?")
sys.exit() sys.exit()
def lookup(self, token): def lookup(self, token):
@@ -187,20 +191,20 @@ class TokenRedis():
import redis import redis
import simplejson import simplejson
except ImportError: except ImportError:
print("package redis or simplejson not found, are you sure you've installed them correctly?", file=sys.stderr) logger.error("package redis or simplejson not found, are you sure you've installed them correctly?")
sys.exit() sys.exit()
print("resolving token '%s'" % token, file=sys.stderr) logger.info("resolving token '%s'" % token)
client = redis.Redis(host=self._server, port=self._port) client = redis.Redis(host=self._server, port=self._port)
stuff = client.get(token) stuff = client.get(token)
if stuff is None: if stuff is None:
return None return None
else: else:
responseStr = stuff.decode("utf-8") responseStr = stuff.decode("utf-8")
print("response from redis : %s" % responseStr, file=sys.stderr) logger.debug("response from redis : %s" % responseStr)
combo = simplejson.loads(responseStr) combo = simplejson.loads(responseStr)
(host, port) = combo["host"].split(':') (host, port) = combo["host"].split(':')
print("host: %s, port: %s" % (host,port), file=sys.stderr) logger.debug("host: %s, port: %s" % (host,port))
return [host, port] return [host, port]
@@ -228,5 +232,5 @@ class UnixDomainSocketDirectory(BasePlugin):
return [ 'unix_socket', uds_path ] return [ 'unix_socket', uds_path ]
except Exception as e: except Exception as e:
print("Error finding unix domain socket: %s" % str(e), file=sys.stderr) logger.error("Error finding unix domain socket: %s" % str(e))
return None return None
+27 -15
View File
@@ -158,19 +158,19 @@ class WebSocket(object):
if not path: if not path:
path = "/" path = "/"
self._queue_str("GET %s HTTP/1.1\r\n" % path) self.send_request("GET", path)
self._queue_str("Host: %s\r\n" % uri.hostname) self.send_header("Host", uri.hostname)
self._queue_str("Upgrade: websocket\r\n") self.send_header("Upgrade", "websocket")
self._queue_str("Connection: upgrade\r\n") self.send_header("Connection", "upgrade")
self._queue_str("Sec-WebSocket-Key: %s\r\n" % self._key) self.send_header("Sec-WebSocket-Key", self._key)
self._queue_str("Sec-WebSocket-Version: 13\r\n") self.send_header("Sec-WebSocket-Version", 13)
if origin is not None: if origin is not None:
self._queue_str("Origin: %s\r\n" % origin) self.send_header("Origin", origin)
if len(protocols) > 0: if len(protocols) > 0:
self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % ", ".join(protocols)) self.send_header("Sec-WebSocket-Protocol", ", ".join(protocols))
self._queue_str("\r\n") self.end_headers()
self._state = "send_headers" self._state = "send_headers"
@@ -283,15 +283,15 @@ class WebSocket(object):
if self.protocol not in protocols: if self.protocol not in protocols:
raise Exception('Invalid protocol selected') raise Exception('Invalid protocol selected')
self._queue_str("HTTP/1.1 101 Switching Protocols\r\n") self.send_response(101, "Switching Protocols")
self._queue_str("Upgrade: websocket\r\n") self.send_header("Upgrade", "websocket")
self._queue_str("Connection: Upgrade\r\n") self.send_header("Connection", "Upgrade")
self._queue_str("Sec-WebSocket-Accept: %s\r\n" % accept) self.send_header("Sec-WebSocket-Accept", accept)
if self.protocol: if self.protocol:
self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % self.protocol) self.send_header("Sec-WebSocket-Protocol", self.protocol)
self._queue_str("\r\n") self.end_headers()
self._state = "flush" self._state = "flush"
@@ -447,6 +447,18 @@ class WebSocket(object):
return len(msg) return len(msg)
def send_response(self, code, message):
self._queue_str("HTTP/1.1 %d %s\r\n" % (code, message))
def send_header(self, keyword, value):
self._queue_str("%s: %s\r\n" % (keyword, value))
def end_headers(self):
self._queue_str("\r\n")
def send_request(self, type, path):
self._queue_str("%s %s HTTP/1.1\r\n" % (type.upper(), path))
def ping(self, data=b''): def ping(self, data=b''):
"""Write a ping message to the WebSocket """Write a ping message to the WebSocket
+46 -13
View File
@@ -35,15 +35,15 @@ Traffic Legend:
< - Client send < - Client send
<. - Client send partial <. - Client send partial
""" """
def send_auth_error(self, ex): def send_auth_error(self, ex):
self.send_response(ex.code, ex.msg) self.send_response(ex.code, ex.msg)
self.send_header('Content-Type', 'text/html') self.send_header('Content-Type', 'text/html')
for name, val in ex.headers.items(): for name, val in ex.headers.items():
self.send_header(name, val) self.send_header(name, val)
self.end_headers() self.end_headers()
def validate_connection(self): def validate_connection(self):
if not self.server.token_plugin: if not self.server.token_plugin:
return return
@@ -72,7 +72,7 @@ Traffic Legend:
except (TypeError, AttributeError, KeyError): except (TypeError, AttributeError, KeyError):
# not a SSL connection or client presented no certificate with valid data # not a SSL connection or client presented no certificate with valid data
pass pass
try: try:
self.server.auth_plugin.authenticate( self.server.auth_plugin.authenticate(
headers=self.headers, target_host=self.server.target_host, headers=self.headers, target_host=self.server.target_host,
@@ -222,6 +222,18 @@ Traffic Legend:
tqueue.extend(bufs) tqueue.extend(bufs)
if closed: if closed:
while (len(tqueue) != 0):
# Send queued client data to the target
dat = tqueue.pop(0)
sent = target.send(dat)
if sent == len(dat):
self.print_traffic(">")
else:
# requeue the remaining data
tqueue.insert(0, dat[sent:])
self.print_traffic(".>")
# 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",
@@ -245,6 +257,16 @@ Traffic Legend:
# Receive target data, encode it and queue for client # Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size) buf = target.recv(self.buffer_size)
if len(buf) == 0: if len(buf) == 0:
# Target socket closed, flushing queues and closing client-side websocket
# Send queued target data to the client
if len(cqueue) != 0:
c_pend = True
while(c_pend):
c_pend = self.send_frames(cqueue)
cqueue = []
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)
@@ -303,8 +325,11 @@ class WebSocketProxy(websockifyserver.WebSockifyServer):
self.target_port = sock.getsockname()[1] self.target_port = sock.getsockname()[1]
sock.close() sock.close()
# Insert rebinder at the head of the (possibly empty) LD_PRELOAD pathlist
ld_preloads = filter(None, [ self.rebinder, os.environ.get("LD_PRELOAD", None) ])
os.environ.update({ os.environ.update({
"LD_PRELOAD": self.rebinder, "LD_PRELOAD": os.pathsep.join(ld_preloads),
"REBIND_OLD_PORT": str(kwargs['listen_port']), "REBIND_OLD_PORT": str(kwargs['listen_port']),
"REBIND_NEW_PORT": str(self.target_port)}) "REBIND_NEW_PORT": str(self.target_port)})
@@ -403,7 +428,7 @@ def select_ssl_version(version):
# It so happens that version names sorted lexicographically form a list # It so happens that version names sorted lexicographically form a list
# from the least to the most secure # from the least to the most secure
keys = list(SSL_OPTIONS.keys()) keys = list(SSL_OPTIONS.keys())
keys.sort() keys.sort()
fallback = keys[-1] fallback = keys[-1]
logger = logging.getLogger(WebSocketProxy.log_prefix) logger = logging.getLogger(WebSocketProxy.log_prefix)
logger.warn("TLS version %s unsupported. Falling back to %s", logger.warn("TLS version %s unsupported. Falling back to %s",
@@ -413,19 +438,22 @@ def select_ssl_version(version):
def websockify_init(): def websockify_init():
# Setup basic logging to stderr. # Setup basic logging to stderr.
logger = logging.getLogger(WebSocketProxy.log_prefix)
logger.propagate = False
logger.setLevel(logging.INFO)
stderr_handler = logging.StreamHandler() stderr_handler = logging.StreamHandler()
stderr_handler.setLevel(logging.DEBUG) stderr_handler.setLevel(logging.DEBUG)
log_formatter = logging.Formatter("%(message)s") log_formatter = logging.Formatter("%(message)s")
stderr_handler.setFormatter(log_formatter) stderr_handler.setFormatter(log_formatter)
logger.addHandler(stderr_handler) root = logging.getLogger()
root.addHandler(stderr_handler)
root.setLevel(logging.INFO)
# Setup optparse. # Setup optparse.
usage = "\n %prog [options]" usage = "\n %prog [options]"
usage += " [source_addr:]source_port [target_addr:target_port]" usage += " [source_addr:]source_port [target_addr:target_port]"
usage += "\n %prog [options]" usage += "\n %prog [options]"
usage += " --token-plugin=CLASS [source_addr:]source_port"
usage += "\n %prog [options]"
usage += " --unix-target=FILE [source_addr:]source_port"
usage += "\n %prog [options]"
usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
parser.add_option("--verbose", "-v", action="store_true", parser.add_option("--verbose", "-v", action="store_true",
@@ -517,6 +545,8 @@ def websockify_init():
parser.add_option("--legacy-syslog", action="store_true", parser.add_option("--legacy-syslog", action="store_true",
help="Use the old syslog protocol instead of RFC 5424. " help="Use the old syslog protocol instead of RFC 5424. "
"Use this if the messages produced by websockify seem abnormal.") "Use this if the messages produced by websockify seem abnormal.")
parser.add_option("--file-only", action="store_true",
help="use this to disable directory listings in web server.")
(opts, args) = parser.parse_args() (opts, args) = parser.parse_args()
@@ -552,7 +582,8 @@ def websockify_init():
log_file_handler = logging.FileHandler(opts.log_file) log_file_handler = logging.FileHandler(opts.log_file)
log_file_handler.setLevel(logging.DEBUG) log_file_handler.setLevel(logging.DEBUG)
log_file_handler.setFormatter(log_formatter) log_file_handler.setFormatter(log_formatter)
logger.addHandler(log_file_handler) root = logging.getLogger()
root.addHandler(log_file_handler)
del opts.log_file del opts.log_file
@@ -585,13 +616,15 @@ def websockify_init():
legacy=opts.legacy_syslog) legacy=opts.legacy_syslog)
syslog_handler.setLevel(logging.DEBUG) syslog_handler.setLevel(logging.DEBUG)
syslog_handler.setFormatter(log_formatter) syslog_handler.setFormatter(log_formatter)
logger.addHandler(syslog_handler) root = logging.getLogger()
root.addHandler(syslog_handler)
del opts.syslog del opts.syslog
del opts.legacy_syslog del opts.legacy_syslog
if opts.verbose: if opts.verbose:
logger.setLevel(logging.DEBUG) root = logging.getLogger()
root.setLevel(logging.DEBUG)
# Transform to absolute path as daemon may chdir # Transform to absolute path as daemon may chdir
+19 -4
View File
@@ -12,6 +12,23 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError
class HttpWebSocket(WebSocket):
"""Class to glue websocket and http request functionality together"""
def __init__(self, request_handler):
super().__init__()
self.request_handler = request_handler
def send_response(self, code, message=None):
self.request_handler.send_response(code, message)
def send_header(self, keyword, value):
self.request_handler.send_header(keyword, value)
def end_headers(self):
self.request_handler.end_headers()
class WebSocketRequestHandlerMixIn: class WebSocketRequestHandlerMixIn:
"""WebSocket request handler mix-in class """WebSocket request handler mix-in class
@@ -25,7 +42,7 @@ class WebSocketRequestHandlerMixIn:
use for the WebSocket connection. use for the WebSocket connection.
""" """
SocketClass = WebSocket SocketClass = HttpWebSocket
def handle_one_request(self): def handle_one_request(self):
"""Extended request handler """Extended request handler
@@ -59,7 +76,7 @@ class WebSocketRequestHandlerMixIn:
The WebSocket object will then replace the request object and The WebSocket object will then replace the request object and
handle_websocket() will be called. handle_websocket() will be called.
""" """
websocket = self.SocketClass() websocket = self.SocketClass(self)
try: try:
websocket.accept(self.request, self.headers) websocket.accept(self.request, self.headers)
except Exception: except Exception:
@@ -67,8 +84,6 @@ class WebSocketRequestHandlerMixIn:
self.send_error(400, str(exc)) self.send_error(400, str(exc))
return return
self.log_request(101)
self.request = websocket self.request = websocket
# Other requests cannot follow Websocket data # Other requests cannot follow Websocket data
+5 -5
View File
@@ -29,10 +29,10 @@ if sys.platform == 'win32':
# make sockets pickle-able/inheritable # make sockets pickle-able/inheritable
import multiprocessing.reduction import multiprocessing.reduction
from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError from websockify.websocket import WebSocketWantReadError, WebSocketWantWriteError
from websockify.websocketserver import WebSocketRequestHandlerMixIn from websockify.websocketserver import WebSocketRequestHandlerMixIn
class CompatibleWebSocket(WebSocket): class CompatibleWebSocket(WebSocketRequestHandlerMixIn.SocketClass):
def select_subprotocol(self, protocols): def select_subprotocol(self, protocols):
# Handle old websockify clients that still specify a sub-protocol # Handle old websockify clients that still specify a sub-protocol
if 'binary' in protocols: if 'binary' in protocols:
@@ -250,13 +250,13 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa
self.auth_connection() self.auth_connection()
if self.only_upgrade: if self.only_upgrade:
self.send_error(405, "Method Not Allowed") self.send_error(405)
else: else:
super().do_GET() super().do_GET()
def list_directory(self, path): def list_directory(self, path):
if self.file_only: if self.file_only:
self.send_error(404, "No such file") self.send_error(404)
else: else:
return super().list_directory(path) return super().list_directory(path)
@@ -277,7 +277,7 @@ class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHa
self.auth_connection() self.auth_connection()
if self.only_upgrade: if self.only_upgrade:
self.send_error(405, "Method Not Allowed") self.send_error(405)
else: else:
super().do_HEAD() super().do_HEAD()