From 5dd81a036367d664a4aba105fc74289b8a7f3534 Mon Sep 17 00:00:00 2001 From: Javier Cacheiro Date: Fri, 22 Apr 2022 13:38:49 +0200 Subject: Remove simplejson dependency: use json module from stdlib. Add missing redis dependency. --- setup.py | 2 +- test-requirements.txt | 1 + websockify/token_plugins.py | 13 ++++++------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index ac3f51a..c5c349a 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup(name=name, install_requires=[ 'numpy', 'requests', 'jwcrypto', - 'redis', 'simplejson', + 'redis', ], zip_safe=False, entry_points={ diff --git a/test-requirements.txt b/test-requirements.txt index 77a3f5f..4eeff97 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,5 @@ mock nose2 six +redis wrapt<=1.12.1;python_version<="3.4" diff --git a/websockify/token_plugins.py b/websockify/token_plugins.py index 19005d3..4e70e6f 100644 --- a/websockify/token_plugins.py +++ b/websockify/token_plugins.py @@ -3,6 +3,7 @@ import os import sys import time import re +import json logger = logging.getLogger(__name__) @@ -167,14 +168,13 @@ class TokenRedis(): 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: you have to install also the 'redis' module + 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(":") logger.info("TokenRedis backend initilized (%s:%s)" % (self._server, self._port)) @@ -183,15 +183,14 @@ class TokenRedis(): src) sys.exit() 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() 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) @@ -202,7 +201,7 @@ class TokenRedis(): else: responseStr = stuff.decode("utf-8") logger.debug("response from redis : %s" % responseStr) - combo = simplejson.loads(responseStr) + combo = json.loads(responseStr) (host, port) = combo["host"].split(':') logger.debug("host: %s, port: %s" % (host,port)) return [host, port] -- cgit v1.2.1 From 8121a5265a904e5e9f93a175bac973b2937cbf18 Mon Sep 17 00:00:00 2001 From: Javier Cacheiro Date: Fri, 22 Apr 2022 13:50:55 +0200 Subject: Token Redis source: add optional redis port, redis database and redis password --- tests/test_token_plugins.py | 32 ++++++++++++++++++++++++++ websockify/token_plugins.py | 55 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/tests/test_token_plugins.py b/tests/test_token_plugins.py index 3e1fd19..0d3b578 100644 --- a/tests/test_token_plugins.py +++ b/tests/test_token_plugins.py @@ -203,3 +203,35 @@ class TokenRedisTestCase(unittest.TestCase): self.assertIsNotNone(result) self.assertEqual(result[0], 'remote_host') self.assertEqual(result[1], 'remote_port') + + 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') diff --git a/websockify/token_plugins.py b/websockify/token_plugins.py index 4e70e6f..644b6a3 100644 --- a/websockify/token_plugins.py +++ b/websockify/token_plugins.py @@ -155,8 +155,25 @@ class JWTTokenApi(BasePlugin): logger.error("package jwcrypto not found, are you sure you've installed it correctly?") return None -class TokenRedis(): - """ + +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 and password are optional. + + 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 you will have to + specify also the port and db: + + my-redis-host:6379:0:verysecretpass + The TokenRedis plugin expects the format of the data in a form of json. Prepare data with: @@ -173,17 +190,34 @@ class TokenRedis(): """ def __init__(self, src): try: - # import those ahead of time so we provide error earlier import redis - 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 + elif len(fields) == 3: + self._server, self._port, self._db = fields + elif len(fields) == 4: + self._server, self._port, self._db, self._password = fields + 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 :" % - src) - sys.exit() - except ImportError: - logger.error("package redis not found, are you sure you've installed them correctly?") + logger.error("The provided --token-source='%s' is not in the " + "expected format [:[:[:]]]" % + src) sys.exit() def lookup(self, token): @@ -194,7 +228,8 @@ class TokenRedis(): 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 -- cgit v1.2.1 From e23d4e337cd3f4a83d160220af4b09e446eed61a Mon Sep 17 00:00:00 2001 From: Javier Cacheiro Date: Fri, 22 Apr 2022 13:54:27 +0200 Subject: Token Redis: Support both json and plain text tokens --- tests/test_token_plugins.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ websockify/token_plugins.py | 45 ++++++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/tests/test_token_plugins.py b/tests/test_token_plugins.py index 0d3b578..b3a1847 100644 --- a/tests/test_token_plugins.py +++ b/tests/test_token_plugins.py @@ -204,6 +204,60 @@ class TokenRedisTestCase(unittest.TestCase): 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') diff --git a/websockify/token_plugins.py b/websockify/token_plugins.py index 644b6a3..4e92c56 100644 --- a/websockify/token_plugins.py +++ b/websockify/token_plugins.py @@ -174,18 +174,32 @@ class TokenRedis(BasePlugin): my-redis-host:6379:0:verysecretpass - The TokenRedis plugin expects the format of the data in a form of json. + 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' module + 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): @@ -234,11 +248,26 @@ class TokenRedis(BasePlugin): 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 = json.loads(responseStr) - (host, port) = combo["host"].split(':') - logger.debug("host: %s, port: %s" % (host,port)) + 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] -- cgit v1.2.1 From 3d2e93aeb039d44850abe2bb7eecad0938be0c41 Mon Sep 17 00:00:00 2001 From: Javier Cacheiro Date: Wed, 25 May 2022 11:37:33 +0200 Subject: Allow empty options in redis token source string when using default values --- tests/test_token_plugins.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ websockify/token_plugins.py | 25 +++++++++++++++--- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/tests/test_token_plugins.py b/tests/test_token_plugins.py index b3a1847..d13d944 100644 --- a/tests/test_token_plugins.py +++ b/tests/test_token_plugins.py @@ -289,3 +289,67 @@ class TokenRedisTestCase(unittest.TestCase): 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) diff --git a/websockify/token_plugins.py b/websockify/token_plugins.py index 4e92c56..d42414e 100644 --- a/websockify/token_plugins.py +++ b/websockify/token_plugins.py @@ -163,16 +163,21 @@ class TokenRedis(BasePlugin): host[:port[:db[:password]]] - where port and password are optional. + 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 you will have to - specify also the port and db: + 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:6379:0:verysecretpass + 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: @@ -218,10 +223,22 @@ class TokenRedis(BasePlugin): 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) -- cgit v1.2.1