Skip to content

Commit 417210f

Browse files
committed
Merge branch 'master' of https://github.com/so-saf/websockify
2 parents 245fd08 + 0af3404 commit 417210f

File tree

2 files changed

+163
-16
lines changed

2 files changed

+163
-16
lines changed

tests/test_token_plugins.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,29 @@
77
from unittest.mock import patch, mock_open, MagicMock
88
from jwcrypto import jwt, jwk
99

10-
from websockify.token_plugins import ReadOnlyTokenFile, JWTTokenApi, TokenRedis
10+
from websockify.token_plugins import parse_source_args, ReadOnlyTokenFile, JWTTokenApi, TokenRedis
11+
12+
class ParseSourceArgumentsTestCase(unittest.TestCase):
13+
def test_parameterized(self):
14+
params = [
15+
('', ['']),
16+
(':', ['', '']),
17+
('::', ['', '', '']),
18+
('"', ['"']),
19+
('""', ['""']),
20+
('"""', ['"""']),
21+
('"localhost"', ['localhost']),
22+
('"localhost":', ['localhost', '']),
23+
('"localhost"::', ['localhost', '', '']),
24+
('"local:host"', ['local:host']),
25+
('"local:host:"pass"', ['"local', 'host', "pass"]),
26+
('"local":"host"', ['local', 'host']),
27+
('"local":host"', ['local', 'host"']),
28+
('localhost:6379:1:pass"word:"my-app-namespace:dev"',
29+
['localhost', '6379', '1', 'pass"word', 'my-app-namespace:dev']),
30+
]
31+
for src, args in params:
32+
self.assertEqual(args, parse_source_args(src))
1133

1234
class ReadOnlyTokenFileTestCase(unittest.TestCase):
1335
patch('os.path.isdir', MagicMock(return_value=False))
@@ -267,13 +289,50 @@ def test_invalid_token(self, mock_redis):
267289
instance.get.assert_called_once_with('testhost')
268290
self.assertIsNone(result)
269291

292+
@patch('redis.Redis')
293+
def test_token_without_namespace(self, mock_redis):
294+
plugin = TokenRedis('127.0.0.1:1234')
295+
token = 'testhost'
296+
297+
def mock_redis_get(key):
298+
self.assertEqual(key, token)
299+
return b'remote_host:remote_port'
300+
301+
instance = mock_redis.return_value
302+
instance.get = mock_redis_get
303+
304+
result = plugin.lookup(token)
305+
306+
self.assertIsNotNone(result)
307+
self.assertEqual(result[0], 'remote_host')
308+
self.assertEqual(result[1], 'remote_port')
309+
310+
@patch('redis.Redis')
311+
def test_token_with_namespace(self, mock_redis):
312+
plugin = TokenRedis('127.0.0.1:1234:::namespace')
313+
token = 'testhost'
314+
315+
def mock_redis_get(key):
316+
self.assertEqual(key, "namespace:" + token)
317+
return b'remote_host:remote_port'
318+
319+
instance = mock_redis.return_value
320+
instance.get = mock_redis_get
321+
322+
result = plugin.lookup(token)
323+
324+
self.assertIsNotNone(result)
325+
self.assertEqual(result[0], 'remote_host')
326+
self.assertEqual(result[1], 'remote_port')
327+
270328
def test_src_only_host(self):
271329
plugin = TokenRedis('127.0.0.1')
272330

273331
self.assertEqual(plugin._server, '127.0.0.1')
274332
self.assertEqual(plugin._port, 6379)
275333
self.assertEqual(plugin._db, 0)
276334
self.assertEqual(plugin._password, None)
335+
self.assertEqual(plugin._namespace, "")
277336

278337
def test_src_with_host_port(self):
279338
plugin = TokenRedis('127.0.0.1:1234')
@@ -282,6 +341,7 @@ def test_src_with_host_port(self):
282341
self.assertEqual(plugin._port, 1234)
283342
self.assertEqual(plugin._db, 0)
284343
self.assertEqual(plugin._password, None)
344+
self.assertEqual(plugin._namespace, "")
285345

286346
def test_src_with_host_port_db(self):
287347
plugin = TokenRedis('127.0.0.1:1234:2')
@@ -290,6 +350,7 @@ def test_src_with_host_port_db(self):
290350
self.assertEqual(plugin._port, 1234)
291351
self.assertEqual(plugin._db, 2)
292352
self.assertEqual(plugin._password, None)
353+
self.assertEqual(plugin._namespace, "")
293354

294355
def test_src_with_host_port_db_pass(self):
295356
plugin = TokenRedis('127.0.0.1:1234:2:verysecret')
@@ -298,67 +359,112 @@ def test_src_with_host_port_db_pass(self):
298359
self.assertEqual(plugin._port, 1234)
299360
self.assertEqual(plugin._db, 2)
300361
self.assertEqual(plugin._password, 'verysecret')
362+
self.assertEqual(plugin._namespace, "")
301363

302-
def test_src_with_host_empty_port_empty_db_pass(self):
364+
def test_src_with_host_port_db_pass_namespace(self):
365+
plugin = TokenRedis('127.0.0.1:1234:2:verysecret:namespace')
366+
367+
self.assertEqual(plugin._server, '127.0.0.1')
368+
self.assertEqual(plugin._port, 1234)
369+
self.assertEqual(plugin._db, 2)
370+
self.assertEqual(plugin._password, 'verysecret')
371+
self.assertEqual(plugin._namespace, "namespace:")
372+
373+
def test_src_with_host_empty_port_empty_db_pass_no_namespace(self):
303374
plugin = TokenRedis('127.0.0.1:::verysecret')
304375

305376
self.assertEqual(plugin._server, '127.0.0.1')
306377
self.assertEqual(plugin._port, 6379)
307378
self.assertEqual(plugin._db, 0)
308379
self.assertEqual(plugin._password, 'verysecret')
380+
self.assertEqual(plugin._namespace, "")
381+
382+
def test_src_with_host_empty_port_empty_db_empty_pass_empty_namespace(self):
383+
plugin = TokenRedis('127.0.0.1::::')
309384

310-
def test_src_with_host_empty_port_empty_db_empty_pass(self):
385+
self.assertEqual(plugin._server, '127.0.0.1')
386+
self.assertEqual(plugin._port, 6379)
387+
self.assertEqual(plugin._db, 0)
388+
self.assertEqual(plugin._password, None)
389+
self.assertEqual(plugin._namespace, "")
390+
391+
def test_src_with_host_empty_port_empty_db_empty_pass_no_namespace(self):
311392
plugin = TokenRedis('127.0.0.1:::')
312393

313394
self.assertEqual(plugin._server, '127.0.0.1')
314395
self.assertEqual(plugin._port, 6379)
315396
self.assertEqual(plugin._db, 0)
316397
self.assertEqual(plugin._password, None)
398+
self.assertEqual(plugin._namespace, "")
317399

318-
def test_src_with_host_empty_port_empty_db_no_pass(self):
400+
def test_src_with_host_empty_port_empty_db_no_pass_no_namespace(self):
319401
plugin = TokenRedis('127.0.0.1::')
320402

321403
self.assertEqual(plugin._server, '127.0.0.1')
322404
self.assertEqual(plugin._port, 6379)
323405
self.assertEqual(plugin._db, 0)
324406
self.assertEqual(plugin._password, None)
407+
self.assertEqual(plugin._namespace, "")
325408

326-
def test_src_with_host_empty_port_no_db_no_pass(self):
409+
def test_src_with_host_empty_port_no_db_no_pass_no_namespace(self):
327410
plugin = TokenRedis('127.0.0.1:')
328411

329412
self.assertEqual(plugin._server, '127.0.0.1')
330413
self.assertEqual(plugin._port, 6379)
331414
self.assertEqual(plugin._db, 0)
332415
self.assertEqual(plugin._password, None)
416+
self.assertEqual(plugin._namespace, "")
417+
418+
def test_src_with_host_empty_port_empty_db_empty_pass_namespace(self):
419+
plugin = TokenRedis('127.0.0.1::::namespace')
420+
421+
self.assertEqual(plugin._server, '127.0.0.1')
422+
self.assertEqual(plugin._port, 6379)
423+
self.assertEqual(plugin._db, 0)
424+
self.assertEqual(plugin._password, None)
425+
self.assertEqual(plugin._namespace, "namespace:")
426+
427+
def test_src_with_host_empty_port_empty_db_empty_pass_nested_namespace(self):
428+
plugin = TokenRedis('127.0.0.1::::"ns1:ns2"')
429+
430+
self.assertEqual(plugin._server, '127.0.0.1')
431+
self.assertEqual(plugin._port, 6379)
432+
self.assertEqual(plugin._db, 0)
433+
self.assertEqual(plugin._password, None)
434+
self.assertEqual(plugin._namespace, "ns1:ns2:")
333435

334-
def test_src_with_host_empty_port_db_no_pass(self):
436+
def test_src_with_host_empty_port_db_no_pass_no_namespace(self):
335437
plugin = TokenRedis('127.0.0.1::2')
336438

337439
self.assertEqual(plugin._server, '127.0.0.1')
338440
self.assertEqual(plugin._port, 6379)
339441
self.assertEqual(plugin._db, 2)
340442
self.assertEqual(plugin._password, None)
443+
self.assertEqual(plugin._namespace, "")
341444

342-
def test_src_with_host_port_empty_db_pass(self):
445+
def test_src_with_host_port_empty_db_pass_no_namespace(self):
343446
plugin = TokenRedis('127.0.0.1:1234::verysecret')
344447

345448
self.assertEqual(plugin._server, '127.0.0.1')
346449
self.assertEqual(plugin._port, 1234)
347450
self.assertEqual(plugin._db, 0)
348451
self.assertEqual(plugin._password, 'verysecret')
452+
self.assertEqual(plugin._namespace, "")
349453

350-
def test_src_with_host_empty_port_db_pass(self):
454+
def test_src_with_host_empty_port_db_pass_no_namespace(self):
351455
plugin = TokenRedis('127.0.0.1::2:verysecret')
352456

353457
self.assertEqual(plugin._server, '127.0.0.1')
354458
self.assertEqual(plugin._port, 6379)
355459
self.assertEqual(plugin._db, 2)
356460
self.assertEqual(plugin._password, 'verysecret')
461+
self.assertEqual(plugin._namespace, "")
357462

358-
def test_src_with_host_empty_port_db_empty_pass(self):
463+
def test_src_with_host_empty_port_db_empty_pass_no_namespace(self):
359464
plugin = TokenRedis('127.0.0.1::2:')
360465

361466
self.assertEqual(plugin._server, '127.0.0.1')
362467
self.assertEqual(plugin._port, 6379)
363468
self.assertEqual(plugin._db, 2)
364469
self.assertEqual(plugin._password, None)
470+
self.assertEqual(plugin._namespace, "")

websockify/token_plugins.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77

88
logger = logging.getLogger(__name__)
99

10+
_SOURCE_SPLIT_REGEX = re.compile(
11+
r'(?<=^)"([^"]+)"(?=:|$)'
12+
r'|(?<=:)"([^"]+)"(?=:|$)'
13+
r'|(?<=^)([^:]*)(?=:|$)'
14+
r'|(?<=:)([^:]*)(?=:|$)',
15+
)
16+
17+
18+
def parse_source_args(src):
19+
"""It works like src.split(":") but with the ability to use a colon
20+
if you wrap the word in quotation marks.
21+
22+
a:b:c:d -> ['a', 'b', 'c', 'd'
23+
a:"b:c":c -> ['a', 'b:c', 'd']
24+
"""
25+
matches = _SOURCE_SPLIT_REGEX.findall(src)
26+
return [m[0] or m[1] or m[2] or m[3] for m in matches]
27+
1028

1129
class BasePlugin():
1230
def __init__(self, src):
@@ -178,9 +196,9 @@ class TokenRedis(BasePlugin):
178196
179197
The token source is in the format:
180198
181-
host[:port[:db[:password]]]
199+
host[:port[:db[:password[:namespace]]]]
182200
183-
where port, db and password are optional. If port or db are left empty
201+
where port, db, password and namespace are optional. If port or db are left empty
184202
they will take its default value, ie. 6379 and 0 respectively.
185203
186204
If your redis server is using the default port (6379) then you can use:
@@ -192,9 +210,18 @@ class TokenRedis(BasePlugin):
192210
193211
my-redis-host:::verysecretpass
194212
213+
You can also specify a namespace. In this case, the tokens
214+
will be stored in the format '{namespace}:{token}'
215+
216+
my-redis-host::::my-app-namespace
217+
218+
Or if your namespace is nested, you can wrap it in quotes:
219+
220+
my-redis-host::::"first-ns:second-ns"
221+
195222
In the more general case you will use:
196223
197-
my-redis-host:6380:1:verysecretpass
224+
my-redis-host:6380:1:verysecretpass:my-app-namespace
198225
199226
The TokenRedis plugin expects the format of the target in one of these two
200227
formats:
@@ -234,8 +261,9 @@ def __init__(self, src):
234261
self._port = 6379
235262
self._db = 0
236263
self._password = None
264+
self._namespace = ""
237265
try:
238-
fields = src.split(":")
266+
fields = parse_source_args(src)
239267
if len(fields) == 1:
240268
self._server = fields[0]
241269
elif len(fields) == 2:
@@ -256,15 +284,28 @@ def __init__(self, src):
256284
self._db = 0
257285
if not self._password:
258286
self._password = None
287+
elif len(fields) == 5:
288+
self._server, self._port, self._db, self._password, self._namespace = fields
289+
if not self._port:
290+
self._port = 6379
291+
if not self._db:
292+
self._db = 0
293+
if not self._password:
294+
self._password = None
295+
if not self._namespace:
296+
self._namespace = ""
259297
else:
260298
raise ValueError
261299
self._port = int(self._port)
262300
self._db = int(self._db)
263-
logger.info("TokenRedis backend initilized (%s:%s)" %
301+
if self._namespace:
302+
self._namespace += ":"
303+
304+
logger.info("TokenRedis backend initialized (%s:%s)" %
264305
(self._server, self._port))
265306
except ValueError:
266307
logger.error("The provided --token-source='%s' is not in the "
267-
"expected format <host>[:<port>[:<db>[:<password>]]]" %
308+
"expected format <host>[:<port>[:<db>[:<password>[:<namespace>]]]]" %
268309
src)
269310
sys.exit()
270311

@@ -278,7 +319,7 @@ def lookup(self, token):
278319
logger.info("resolving token '%s'" % token)
279320
client = redis.Redis(host=self._server, port=self._port,
280321
db=self._db, password=self._password)
281-
stuff = client.get(token)
322+
stuff = client.get(self._namespace + token)
282323
if stuff is None:
283324
return None
284325
else:

0 commit comments

Comments
 (0)