Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43c371fc7a | |||
| 354668143f | |||
| e28b7c8c75 | |||
| a1346552fb | |||
| 3d2e93aeb0 | |||
| e23d4e337c | |||
| 8121a5265a | |||
| 5dd81a0363 | |||
| 7f53e9c22c | |||
| d54020538d | |||
| ac74ade2ca | |||
| 9ac3272d2b | |||
| 5d17281187 | |||
| 789b80f719 | |||
| 63eb24dd8e |
@@ -4,16 +4,17 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8
|
||||
- 3.9
|
||||
- "3.10"
|
||||
- 3.11
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -21,10 +22,13 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
- name: Update pip and setuptools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install setuptools
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install -e .
|
||||
python -m pip install -r test-requirements.txt
|
||||
- name: Install old numpy
|
||||
run: |
|
||||
@@ -32,4 +36,4 @@ jobs:
|
||||
if: ${{ matrix.python-version >= '3.4' && matrix.python-version < '3.7' }}
|
||||
- name: Run tests
|
||||
run: |
|
||||
python setup.py nosetests --verbosity=3
|
||||
python -m nose2 --verbosity=3
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -32,7 +32,7 @@ setup(name=name,
|
||||
install_requires=[
|
||||
'numpy', 'requests',
|
||||
'jwcrypto',
|
||||
'redis', 'simplejson',
|
||||
'redis',
|
||||
],
|
||||
zip_safe=False,
|
||||
entry_points={
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
mock
|
||||
nose
|
||||
jwcrypto
|
||||
nose2
|
||||
six
|
||||
redis
|
||||
simplejson
|
||||
wrapt<=1.12.1;python_version<="3.4"
|
||||
|
||||
@@ -203,3 +203,153 @@ class TokenRedisTestCase(unittest.TestCase):
|
||||
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)
|
||||
|
||||
+100
-20
@@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -154,56 +155,135 @@ class JWTTokenApi(BasePlugin):
|
||||
logger.error("package jwcrypto not found, are you sure you've installed it correctly?")
|
||||
return None
|
||||
|
||||
class TokenRedis():
|
||||
"""
|
||||
The TokenRedis plugin expects the format of the data in a form of json.
|
||||
|
||||
class TokenRedis(BasePlugin):
|
||||
"""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:
|
||||
redis-cli set hello '{"host":"127.0.0.1:5000"}'
|
||||
|
||||
redis-cli set my-token '{"host": "127.0.0.1:5000"}'
|
||||
|
||||
Verify with:
|
||||
redis-cli --raw get hello
|
||||
|
||||
redis-cli --raw get my-token
|
||||
|
||||
Spawn a test "server" using netcat
|
||||
|
||||
nc -l 5000 -v
|
||||
|
||||
Note: you have to install also the 'redis' and 'simplejson' modules
|
||||
pip install redis simplejson
|
||||
Note: This Token Plugin depends on the 'redis' module, so you have
|
||||
to install it before using this plugin:
|
||||
|
||||
pip install redis
|
||||
"""
|
||||
def __init__(self, src):
|
||||
try:
|
||||
# import those ahead of time so we provide error earlier
|
||||
import redis
|
||||
import simplejson
|
||||
self._server, self._port = src.split(":")
|
||||
except ImportError:
|
||||
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 an expected format <host>:<port>" %
|
||||
logger.error("The provided --token-source='%s' is not in the "
|
||||
"expected format <host>[:<port>[:<db>[:<password>]]]" %
|
||||
src)
|
||||
sys.exit()
|
||||
except ImportError:
|
||||
logger.error("package redis or simplejson not found, are you sure you've installed them correctly?")
|
||||
sys.exit()
|
||||
|
||||
def lookup(self, token):
|
||||
try:
|
||||
import redis
|
||||
import simplejson
|
||||
except ImportError:
|
||||
logger.error("package redis or simplejson not found, are you sure you've installed them correctly?")
|
||||
logger.error("package redis not found, are you sure you've installed them correctly?")
|
||||
sys.exit()
|
||||
|
||||
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)
|
||||
if stuff is None:
|
||||
return None
|
||||
else:
|
||||
responseStr = stuff.decode("utf-8")
|
||||
responseStr = stuff.decode("utf-8").strip()
|
||||
logger.debug("response from redis : %s" % responseStr)
|
||||
combo = simplejson.loads(responseStr)
|
||||
(host, port) = combo["host"].split(':')
|
||||
if responseStr.startswith("{"):
|
||||
try:
|
||||
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]
|
||||
|
||||
|
||||
@@ -666,7 +666,7 @@ class WebSocket(object):
|
||||
continue
|
||||
|
||||
if code is None:
|
||||
self.close_code = code = 1005
|
||||
self.close_code = 1005
|
||||
self.close_reason = "No close status code specified by peer"
|
||||
else:
|
||||
self.close_code = code
|
||||
|
||||
@@ -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 http.server import HTTPServer
|
||||
|
||||
@@ -112,6 +112,8 @@ Traffic Legend:
|
||||
self.server.target_host, self.server.target_port, e)
|
||||
raise self.CClose(1011, "Failed to connect to downstream server")
|
||||
|
||||
# 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:
|
||||
tsock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
|
||||
@@ -448,7 +450,7 @@ def websockify_init():
|
||||
|
||||
# Setup optparse.
|
||||
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]"
|
||||
@@ -495,6 +497,10 @@ def websockify_init():
|
||||
parser.add_option("--ssl-ciphers", action="store",
|
||||
help="list of ciphers allowed for connection. For a list of "
|
||||
"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",
|
||||
help="connect to unix socket target", metavar="FILE")
|
||||
parser.add_option("--inetd",
|
||||
@@ -650,6 +656,16 @@ def websockify_init():
|
||||
|
||||
if opts.inetd:
|
||||
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:
|
||||
if len(args) < 1:
|
||||
parser.error("Too few arguments")
|
||||
|
||||
@@ -325,12 +325,15 @@ class WebSockifyServer():
|
||||
file_only=False,
|
||||
run_once=False, timeout=0, idle_timeout=0, traffic=False,
|
||||
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
|
||||
self.RequestHandlerClass = RequestHandlerClass
|
||||
self.verbose = verbose
|
||||
self.listen_fd = listen_fd
|
||||
self.unix_listen = unix_listen
|
||||
self.unix_listen_mode = unix_listen_mode
|
||||
self.listen_host = listen_host
|
||||
self.listen_port = listen_port
|
||||
self.prefer_ipv6 = source_is_ipv6
|
||||
@@ -387,6 +390,8 @@ class WebSockifyServer():
|
||||
self.msg("WebSocket server settings:")
|
||||
if self.listen_fd != None:
|
||||
self.msg(" - Listen for inetd connections")
|
||||
elif self.unix_listen != None:
|
||||
self.msg(" - Listen on unix socket %s", self.unix_listen)
|
||||
else:
|
||||
self.msg(" - Listen on %s:%s",
|
||||
self.listen_host, self.listen_port)
|
||||
@@ -421,8 +426,9 @@ class WebSockifyServer():
|
||||
|
||||
@staticmethod
|
||||
def socket(host, port=None, connect=False, prefer_ipv6=False,
|
||||
unix_socket=None, use_ssl=False, tcp_keepalive=True,
|
||||
tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None):
|
||||
unix_socket=None, unix_socket_mode=None, unix_socket_listen=False,
|
||||
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
|
||||
address. Create a socket. Bind to it if listen is set,
|
||||
otherwise connect to it. Return the socket.
|
||||
@@ -469,6 +475,20 @@ class WebSockifyServer():
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(addrs[0][4])
|
||||
sock.listen(100)
|
||||
else:
|
||||
if unix_socket_listen:
|
||||
# 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)
|
||||
@@ -700,6 +720,11 @@ class WebSockifyServer():
|
||||
|
||||
if self.listen_fd != None:
|
||||
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:
|
||||
lsock = self.socket(self.listen_host, self.listen_port, False,
|
||||
self.prefer_ipv6,
|
||||
@@ -766,6 +791,9 @@ class WebSockifyServer():
|
||||
ready = select.select([lsock], [], [], 1)[0]
|
||||
if lsock in ready:
|
||||
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:
|
||||
continue
|
||||
except self.Terminate:
|
||||
|
||||
Reference in New Issue
Block a user