41 Commits

Author SHA1 Message Date
Pierre Ossman 43c371fc7a Don't fake a close code in close response
If the peer doesn't send us a close code, then don't send any code back
in the response. Sending 1005 is explicitly wrong as the specification
states that code should only be used locally and never be sent over the
network.
2023-09-06 12:25:19 +02:00
Pierre Ossman 354668143f Use Ubuntu 20.04 runner for tests
GitHub has deprecated 18.04, so we can't continue running tests on that
old distribution. Unfortunately, we can't switch to the latest 22.04
since GitHub doesn't package anything older than Python 3.7 for that
runner.

And unfortunately the oldest version in 20.04 is Python 3.5, so we have
to remove the test runs for Python 3.4.
2023-08-23 08:49:17 +02:00
Pierre Ossman e28b7c8c75 Fix usage syntax in regular TCP case
You always need to specify the target on the command line as the system
has no default target address.
2023-08-21 16:28:58 +02:00
Pierre Ossman a1346552fb Merge branch 'token_redis_improvements' of https://github.com/javicacheiro/websockify 2023-01-20 17:01:32 +01:00
Javier Cacheiro 3d2e93aeb0 Allow empty options in redis token source string when using default values 2023-01-19 17:19:30 +01:00
Javier Cacheiro e23d4e337c Token Redis: Support both json and plain text tokens 2023-01-19 17:19:27 +01:00
Javier Cacheiro 8121a5265a Token Redis source: add optional redis port, redis database and redis password 2023-01-19 17:17:07 +01:00
Javier Cacheiro 5dd81a0363 Remove simplejson dependency: use json module from stdlib.
Add missing redis dependency.
2023-01-19 16:35:53 +01:00
don bright 7f53e9c22c Update README.md
adding WSS exceptions for dummies
2023-01-07 16:34:17 -06:00
Pierre Ossman d54020538d Merge branch 'master' of https://github.com/shiomax/websockify 2022-12-22 14:15:46 +01:00
Pierre Ossman ac74ade2ca Test current versions of Python 2022-12-16 14:15:23 +01:00
Pierre Ossman 9ac3272d2b Switch to nose2 for tests
The original nosetests is long abandoned, and doesn't work properly on
newer versions of Python.
2022-12-16 14:14:54 +01:00
Pierre Ossman 5d17281187 Remove redundant test requirements
These should get pulled in via setup.py.
2022-12-16 14:14:54 +01:00
Pierre Ossman 789b80f719 Explicitly install dependencies
It is very buggy if we let setuptools do it for some reason, so have
this as an explicit step instead.
2022-12-16 14:14:23 +01:00
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
max 63eb24dd8e Add option to listen to unix socket 2022-12-14 20:51:29 +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
15 changed files with 642 additions and 137 deletions
+8 -4
View File
@@ -4,16 +4,17 @@ on: [push, pull_request]
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
python-version: python-version:
- 3.4
- 3.5 - 3.5
- 3.6 - 3.6
- 3.7 - 3.7
- 3.8 - 3.8
- 3.9 - 3.9
- "3.10"
- 3.11
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -21,10 +22,13 @@ jobs:
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Update pip and setuptools
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install setuptools python -m pip install setuptools
- name: Install dependencies
run: |
python -m pip install -e .
python -m pip install -r test-requirements.txt python -m pip install -r test-requirements.txt
- name: Install old numpy - name: Install old numpy
run: | run: |
@@ -32,4 +36,4 @@ jobs:
if: ${{ matrix.python-version >= '3.4' && matrix.python-version < '3.7' }} if: ${{ matrix.python-version >= '3.4' && matrix.python-version < '3.7' }}
- name: Run tests - name: Run tests
run: | run: |
python setup.py nosetests --verbosity=3 python -m nose2 --verbosity=3
+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
------ ------
+38
View File
@@ -57,6 +57,12 @@ 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 by opening a WSS socket with invalid certificate, hence you need to have it
accept it by either of those two methods. accept it by either of those two methods.
The ports may be considered as distinguishing connections by the browser,
for example, if your website url is https://my.local:8443 and your WebSocket
url is wss://my.local:8001, first browse to https://my.local:8001, add the
exception, then browse to https://my.local:8443 and add another exception.
Then an html page served over :8443 will be able to open WSS to :8001
If you have a commercial/valid SSL certificate with one or more intermediate If you have a commercial/valid SSL certificate with one or more intermediate
certificates, concat them into one file, server certificate first, then the 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 intermediate(s) from the CA, etc. Point to this file with the `--cert` option
@@ -168,3 +174,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',
],
zip_safe=False, zip_safe=False,
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
+2 -3
View File
@@ -1,6 +1,5 @@
mock mock
nose nose2
jwcrypto
six six
redis redis
simplejson wrapt<=1.12.1;python_version<="3.4"
+188 -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,179 @@ 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')
@patch('redis.Redis')
def test_json_token_with_spaces(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')
@patch('redis.Redis')
def test_text_token(self, mock_redis):
plugin = TokenRedis('127.0.0.1:1234')
instance = mock_redis.return_value
instance.get.return_value = b'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')
@patch('redis.Redis')
def test_text_token_with_spaces(self, mock_redis):
plugin = TokenRedis('127.0.0.1:1234')
instance = mock_redis.return_value
instance.get.return_value = b' 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')
@patch('redis.Redis')
def test_invalid_token(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.assertIsNone(result)
def test_src_only_host(self):
plugin = TokenRedis('127.0.0.1')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, None)
def test_src_with_host_port(self):
plugin = TokenRedis('127.0.0.1:1234')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 1234)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, None)
def test_src_with_host_port_db(self):
plugin = TokenRedis('127.0.0.1:1234:2')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 1234)
self.assertEqual(plugin._db, 2)
self.assertEqual(plugin._password, None)
def test_src_with_host_port_db_pass(self):
plugin = TokenRedis('127.0.0.1:1234:2:verysecret')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 1234)
self.assertEqual(plugin._db, 2)
self.assertEqual(plugin._password, 'verysecret')
def test_src_with_host_empty_port_empty_db_pass(self):
plugin = TokenRedis('127.0.0.1:::verysecret')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, 'verysecret')
def test_src_with_host_empty_port_empty_db_empty_pass(self):
plugin = TokenRedis('127.0.0.1:::')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, None)
def test_src_with_host_empty_port_empty_db_no_pass(self):
plugin = TokenRedis('127.0.0.1::')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, None)
def test_src_with_host_empty_port_no_db_no_pass(self):
plugin = TokenRedis('127.0.0.1:')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, None)
def test_src_with_host_empty_port_db_no_pass(self):
plugin = TokenRedis('127.0.0.1::2')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 2)
self.assertEqual(plugin._password, None)
def test_src_with_host_port_empty_db_pass(self):
plugin = TokenRedis('127.0.0.1:1234::verysecret')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 1234)
self.assertEqual(plugin._db, 0)
self.assertEqual(plugin._password, 'verysecret')
def test_src_with_host_empty_port_db_pass(self):
plugin = TokenRedis('127.0.0.1::2:verysecret')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 2)
self.assertEqual(plugin._password, 'verysecret')
def test_src_with_host_empty_port_db_empty_pass(self):
plugin = TokenRedis('127.0.0.1::2:')
self.assertEqual(plugin._server, '127.0.0.1')
self.assertEqual(plugin._port, 6379)
self.assertEqual(plugin._db, 2)
self.assertEqual(plugin._password, None)
+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):
+121 -37
View File
@@ -1,7 +1,12 @@
import logging
import os import os
import sys import sys
import time import time
import re import re
import json
logger = logging.getLogger(__name__)
class BasePlugin(): class BasePlugin():
def __init__(self, src): def __init__(self, src):
@@ -35,7 +40,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 +104,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 +122,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:
@@ -133,74 +138,153 @@ class JWTTokenApi(BasePlugin):
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(BasePlugin):
The TokenRedis plugin expects the format of the data in a form of json. """Token plugin based on the Redis in-memory data store.
The token source is in the format:
host[:port[:db[:password]]]
where port, db and password are optional. If port or db are left empty
they will take its default value, ie. 6379 and 0 respectively.
If your redis server is using the default port (6379) then you can use:
my-redis-host
In case you need to authenticate with the redis server and you are using
the default database and port you can use:
my-redis-host:::verysecretpass
In the more general case you will use:
my-redis-host:6380:1:verysecretpass
The TokenRedis plugin expects the format of the target in one of these two
formats:
- JSON
{"host": "target-host:target-port"}
- Plain text
target-host:target-port
Prepare data with: Prepare data with:
redis-cli set hello '{"host":"127.0.0.1:5000"}'
redis-cli set my-token '{"host": "127.0.0.1:5000"}'
Verify with: Verify with:
redis-cli --raw get hello
redis-cli --raw get my-token
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: This Token Plugin depends on the 'redis' module, so you have
pip install redis simplejson to install it before using this plugin:
pip install redis
""" """
def __init__(self, src): def __init__(self, src):
try: try:
# import those ahead of time so we provide error earlier
import redis import redis
import simplejson
self._server, self._port = src.split(":")
print("TokenRedis backend initilized (%s:%s)" %
(self._server, self._port), file=sys.stderr)
except ValueError:
print("The provided --token-source='%s' is not in an expected format <host>:<port>" %
src, file=sys.stderr)
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("Unable to load redis module")
sys.exit()
# Default values
self._port = 6379
self._db = 0
self._password = None
try:
fields = src.split(":")
if len(fields) == 1:
self._server = fields[0]
elif len(fields) == 2:
self._server, self._port = fields
if not self._port:
self._port = 6379
elif len(fields) == 3:
self._server, self._port, self._db = fields
if not self._port:
self._port = 6379
if not self._db:
self._db = 0
elif len(fields) == 4:
self._server, self._port, self._db, self._password = fields
if not self._port:
self._port = 6379
if not self._db:
self._db = 0
if not self._password:
self._password = None
else:
raise ValueError
self._port = int(self._port)
self._db = int(self._db)
logger.info("TokenRedis backend initilized (%s:%s)" %
(self._server, self._port))
except ValueError:
logger.error("The provided --token-source='%s' is not in the "
"expected format <host>[:<port>[:<db>[:<password>]]]" %
src)
sys.exit() sys.exit()
def lookup(self, token): def lookup(self, token):
try: try:
import redis import redis
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 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,
db=self._db, password=self._password)
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").strip()
print("response from redis : %s" % responseStr, file=sys.stderr) logger.debug("response from redis : %s" % responseStr)
combo = simplejson.loads(responseStr) if responseStr.startswith("{"):
(host, port) = combo["host"].split(':') try:
print("host: %s, port: %s" % (host,port), file=sys.stderr) combo = json.loads(responseStr)
host, port = combo["host"].split(":")
except ValueError:
logger.error("Unable to decode JSON token: %s" %
responseStr)
return None
except KeyError:
logger.error("Unable to find 'host' key in JSON token: %s" %
responseStr)
return None
elif re.match(r'\S+:\S+', responseStr):
host, port = responseStr.split(":")
else:
logger.error("Unable to parse token: %s" % responseStr)
return None
logger.debug("host: %s, port: %s" % (host, port))
return [host, port] return [host, port]
@@ -228,5 +312,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
+28 -16
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
@@ -654,7 +666,7 @@ class WebSocket(object):
continue continue
if code is None: if code is None:
self.close_code = code = 1005 self.close_code = 1005
self.close_reason = "No close status code specified by peer" self.close_reason = "No close status code specified by peer"
else: else:
self.close_code = code self.close_code = code
+60 -11
View File
@@ -11,7 +11,7 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
''' '''
import signal, socket, optparse, time, os, sys, subprocess, logging, errno, ssl import signal, socket, optparse, time, os, sys, subprocess, logging, errno, ssl, stat
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from http.server import HTTPServer from http.server import HTTPServer
@@ -112,7 +112,9 @@ Traffic Legend:
self.server.target_host, self.server.target_port, e) self.server.target_host, self.server.target_port, e)
raise self.CClose(1011, "Failed to connect to downstream server") raise self.CClose(1011, "Failed to connect to downstream server")
self.request.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) # Option unavailable when listening to unix socket
if not self.server.unix_listen:
self.request.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
if not self.server.wrap_cmd and not self.server.unix_target: if not self.server.wrap_cmd and not self.server.unix_target:
tsock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) tsock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
@@ -222,6 +224,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 +259,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 +327,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)})
@@ -413,18 +440,21 @@ 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 += " --token-plugin=CLASS [source_addr:]source_port"
usage += "\n %prog [options]"
usage += " --unix-target=FILE [source_addr:]source_port"
usage += "\n %prog [options]" 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)
@@ -467,6 +497,10 @@ def websockify_init():
parser.add_option("--ssl-ciphers", action="store", parser.add_option("--ssl-ciphers", action="store",
help="list of ciphers allowed for connection. For a list of " help="list of ciphers allowed for connection. For a list of "
"supported ciphers run `openssl ciphers`") "supported ciphers run `openssl ciphers`")
parser.add_option("--unix-listen",
help="listen to unix socket", metavar="FILE", default=None)
parser.add_option("--unix-listen-mode", default=None,
help="specify mode for unix socket (defaults to 0600)")
parser.add_option("--unix-target", parser.add_option("--unix-target",
help="connect to unix socket target", metavar="FILE") help="connect to unix socket target", metavar="FILE")
parser.add_option("--inetd", parser.add_option("--inetd",
@@ -517,6 +551,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 +588,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 +622,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
@@ -617,6 +656,16 @@ def websockify_init():
if opts.inetd: if opts.inetd:
opts.listen_fd = sys.stdin.fileno() opts.listen_fd = sys.stdin.fileno()
elif opts.unix_listen:
if opts.unix_listen_mode:
try:
# Parse octal notation (like 750)
opts.unix_listen_mode = int(opts.unix_listen_mode, 8)
except ValueError:
parser.error("Error parsing listen unix socket mode")
else:
# Default to 0600 (Owner Read/Write)
opts.unix_listen_mode = stat.S_IREAD | stat.S_IWRITE
else: else:
if len(args) < 1: if len(args) < 1:
parser.error("Too few arguments") parser.error("Too few arguments")
+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
+63 -35
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()
@@ -325,37 +325,40 @@ class WebSockifyServer():
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, ssl_ciphers=None, ssl_options=0): tcp_keepintvl=None, ssl_ciphers=None, ssl_options=0,
unix_listen=None, unix_listen_mode=None):
# settings # settings
self.RequestHandlerClass = RequestHandlerClass self.RequestHandlerClass = RequestHandlerClass
self.verbose = verbose self.verbose = verbose
self.listen_fd = listen_fd self.listen_fd = listen_fd
self.listen_host = listen_host self.unix_listen = unix_listen
self.listen_port = listen_port self.unix_listen_mode = unix_listen_mode
self.prefer_ipv6 = source_is_ipv6 self.listen_host = listen_host
self.ssl_only = ssl_only self.listen_port = listen_port
self.ssl_ciphers = ssl_ciphers self.prefer_ipv6 = source_is_ipv6
self.ssl_options = ssl_options self.ssl_only = ssl_only
self.verify_client = verify_client self.ssl_ciphers = ssl_ciphers
self.daemon = daemon self.ssl_options = ssl_options
self.run_once = run_once self.verify_client = verify_client
self.timeout = timeout self.daemon = daemon
self.idle_timeout = idle_timeout self.run_once = run_once
self.traffic = traffic self.timeout = timeout
self.file_only = file_only self.idle_timeout = idle_timeout
self.web_auth = web_auth self.traffic = traffic
self.file_only = file_only
self.web_auth = web_auth
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
self.terminating = False self.terminating = False
self.logger = self.get_logger() self.logger = self.get_logger()
self.tcp_keepalive = tcp_keepalive self.tcp_keepalive = tcp_keepalive
self.tcp_keepcnt = tcp_keepcnt self.tcp_keepcnt = tcp_keepcnt
self.tcp_keepidle = tcp_keepidle self.tcp_keepidle = tcp_keepidle
self.tcp_keepintvl = tcp_keepintvl self.tcp_keepintvl = tcp_keepintvl
# keyfile path must be None if not specified # keyfile path must be None if not specified
self.key = None self.key = None
@@ -387,6 +390,8 @@ class WebSockifyServer():
self.msg("WebSocket server settings:") self.msg("WebSocket server settings:")
if self.listen_fd != None: if self.listen_fd != None:
self.msg(" - Listen for inetd connections") self.msg(" - Listen for inetd connections")
elif self.unix_listen != None:
self.msg(" - Listen on unix socket %s", self.unix_listen)
else: else:
self.msg(" - Listen on %s:%s", self.msg(" - Listen on %s:%s",
self.listen_host, self.listen_port) self.listen_host, self.listen_port)
@@ -421,8 +426,9 @@ class WebSockifyServer():
@staticmethod @staticmethod
def socket(host, port=None, connect=False, prefer_ipv6=False, def socket(host, port=None, connect=False, prefer_ipv6=False,
unix_socket=None, use_ssl=False, tcp_keepalive=True, unix_socket=None, unix_socket_mode=None, unix_socket_listen=False,
tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None): use_ssl=False, tcp_keepalive=True, tcp_keepcnt=None,
tcp_keepidle=None, tcp_keepintvl=None):
""" Resolve a host (and optional port) to an IPv4 or IPv6 """ Resolve a host (and optional port) to an IPv4 or IPv6
address. Create a socket. Bind to it if listen is set, address. Create a socket. Bind to it if listen is set,
otherwise connect to it. Return the socket. otherwise connect to it. Return the socket.
@@ -470,8 +476,22 @@ class WebSockifyServer():
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) if unix_socket_listen:
sock.connect(unix_socket) # Make sure the socket does not already exist
try:
os.unlink(unix_socket)
except FileNotFoundError:
pass
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
oldmask = os.umask(0o777 ^ unix_socket_mode)
try:
sock.bind(unix_socket)
finally:
os.umask(oldmask)
sock.listen(100)
else:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(unix_socket)
return sock return sock
@@ -700,6 +720,11 @@ class WebSockifyServer():
if self.listen_fd != None: if self.listen_fd != None:
lsock = socket.fromfd(self.listen_fd, socket.AF_INET, socket.SOCK_STREAM) lsock = socket.fromfd(self.listen_fd, socket.AF_INET, socket.SOCK_STREAM)
elif self.unix_listen != None:
lsock = self.socket(host=None,
unix_socket=self.unix_listen,
unix_socket_mode=self.unix_listen_mode,
unix_socket_listen=True)
else: else:
lsock = self.socket(self.listen_host, self.listen_port, False, lsock = self.socket(self.listen_host, self.listen_port, False,
self.prefer_ipv6, self.prefer_ipv6,
@@ -766,6 +791,9 @@ class WebSockifyServer():
ready = select.select([lsock], [], [], 1)[0] ready = select.select([lsock], [], [], 1)[0]
if lsock in ready: if lsock in ready:
startsock, address = lsock.accept() startsock, address = lsock.accept()
# Unix Socket will not report address (empty string), but address[0] is logged a bunch
if self.unix_listen != None:
address = [ self.unix_listen ]
else: else:
continue continue
except self.Terminate: except self.Terminate: