Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
import os
|
2017-10-10 09:10:10 +01:00
|
|
|
import sys
|
2021-01-27 14:16:08 +00:00
|
|
|
import time
|
2020-07-28 13:40:42 +01:00
|
|
|
import re
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
|
2020-12-14 08:13:11 +00:00
|
|
|
class BasePlugin():
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
def __init__(self, src):
|
|
|
|
self.source = src
|
|
|
|
|
|
|
|
def lookup(self, token):
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class ReadOnlyTokenFile(BasePlugin):
|
|
|
|
# source is a token file with lines like
|
|
|
|
# token: host:port
|
|
|
|
# or a directory of such files
|
2015-04-28 22:01:01 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
2020-12-14 08:13:11 +00:00
|
|
|
super().__init__(*args, **kwargs)
|
2015-04-28 22:01:01 +01:00
|
|
|
self._targets = None
|
|
|
|
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
def _load_targets(self):
|
|
|
|
if os.path.isdir(self.source):
|
|
|
|
cfg_files = [os.path.join(self.source, f) for
|
|
|
|
f in os.listdir(self.source)]
|
|
|
|
else:
|
|
|
|
cfg_files = [self.source]
|
|
|
|
|
|
|
|
self._targets = {}
|
2017-10-10 09:10:10 +01:00
|
|
|
index = 1
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
for f in cfg_files:
|
|
|
|
for line in [l.strip() for l in open(f).readlines()]:
|
|
|
|
if line and not line.startswith('#'):
|
2017-10-10 09:10:10 +01:00
|
|
|
try:
|
2020-07-28 13:40:42 +01:00
|
|
|
tok, target = re.split(':\s', line)
|
2017-10-10 09:10:10 +01:00
|
|
|
self._targets[tok] = target.strip().rsplit(':', 1)
|
2017-11-10 10:18:32 +00:00
|
|
|
except ValueError:
|
2020-07-28 13:35:49 +01:00
|
|
|
print("Syntax error in %s on line %d" % (self.source, index), file=sys.stderr)
|
2017-10-10 09:10:10 +01:00
|
|
|
index += 1
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
|
|
|
|
def lookup(self, token):
|
|
|
|
if self._targets is None:
|
|
|
|
self._load_targets()
|
|
|
|
|
|
|
|
if token in self._targets:
|
|
|
|
return self._targets[token]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# the above one is probably more efficient, but this one is
|
|
|
|
# more backwards compatible (although in most cases
|
|
|
|
# ReadOnlyTokenFile should suffice)
|
|
|
|
class TokenFile(ReadOnlyTokenFile):
|
|
|
|
# source is a token file with lines like
|
|
|
|
# token: host:port
|
|
|
|
# or a directory of such files
|
|
|
|
def lookup(self, token):
|
|
|
|
self._load_targets()
|
|
|
|
|
2020-12-14 08:13:11 +00:00
|
|
|
return super().lookup(token)
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BaseTokenAPI(BasePlugin):
|
|
|
|
# source is a url with a '%s' in it where the token
|
|
|
|
# should go
|
|
|
|
|
|
|
|
# we import things on demand so that other plugins
|
2019-07-03 11:29:02 +01:00
|
|
|
# in this file can be used w/o unnecessary dependencies
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
|
|
|
|
def process_result(self, resp):
|
2019-07-03 11:29:02 +01:00
|
|
|
host, port = resp.text.split(':')
|
|
|
|
port = port.encode('ascii','ignore')
|
|
|
|
return [ host, port ]
|
Introduce Token Plugins
Token plugins provide a generic interface for transforming a token
into a `(host, port)` tuple.
The plugin name is specified using the '--token-plugin' option,
and may either be the name of a class from `websockify.token_plugins`,
or a fully qualified python path to the token plugin class (see below).
An optional plugin parameter can be specified using the '--token-source'
option (a value of `None` will be used if no '--token-source' option is
passed).
Token plugins should inherit from `websockify.token_plugins.BasePlugin`,
and should implement the `lookup(token)` method. The value of the
'--token-source' option is available as `self.source`.
Several plugins are included by default. The `ReadOnlyTokenFile`
and `TokenFile` plugins implement functionality from '--target-config'
(with the former only reading the file(s) once, and the latter reading
them every time). The 'BaseTokenAPI' plugin fetches the value from
an API, returning the result of `process_result(response_object)`.
By default, `process_result` simply returns the text of the response,
but may be overriden. The `JSONTokenAPI` does just this, returning
the 'host' and 'port' values from the response JSON object.
The old '--target-config' option is now deprecated, and maps to the
`TokenFile` plugin under the hood.
Also-Authored-By: James Portman (@james-portman)
Closes #157
2015-03-26 20:01:57 +00:00
|
|
|
|
|
|
|
def lookup(self, token):
|
|
|
|
import requests
|
|
|
|
|
|
|
|
resp = requests.get(self.source % token)
|
|
|
|
|
|
|
|
if resp.ok:
|
|
|
|
return self.process_result(resp)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class JSONTokenApi(BaseTokenAPI):
|
|
|
|
# source is a url with a '%s' in it where the token
|
|
|
|
# should go
|
|
|
|
|
|
|
|
def process_result(self, resp):
|
2015-08-22 13:09:58 +01:00
|
|
|
resp_json = resp.json()
|
|
|
|
return (resp_json['host'], resp_json['port'])
|
2019-01-17 13:53:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
class JWTTokenApi(BasePlugin):
|
|
|
|
# source is a JWT-token, with hostname and port included
|
|
|
|
# Both JWS as JWE tokens are accepted. With regards to JWE tokens, the key is re-used for both validation and decryption.
|
|
|
|
|
|
|
|
def lookup(self, token):
|
|
|
|
try:
|
|
|
|
from jwcrypto import jwt
|
|
|
|
import json
|
|
|
|
|
|
|
|
key = jwt.JWK()
|
2020-03-12 03:49:37 +00:00
|
|
|
|
2019-01-17 13:53:01 +00:00
|
|
|
try:
|
|
|
|
with open(self.source, 'rb') as key_file:
|
|
|
|
key_data = key_file.read()
|
|
|
|
except Exception as e:
|
|
|
|
print("Error loading key file: %s" % str(e), file=sys.stderr)
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
key.import_from_pem(key_data)
|
|
|
|
except:
|
|
|
|
try:
|
|
|
|
key.import_key(k=key_data.decode('utf-8'),kty='oct')
|
|
|
|
except:
|
|
|
|
print('Failed to correctly parse key data!', file=sys.stderr)
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
token = jwt.JWT(key=key, jwt=token)
|
|
|
|
parsed_header = json.loads(token.header)
|
|
|
|
|
|
|
|
if 'enc' in parsed_header:
|
|
|
|
# Token is encrypted, so we need to decrypt by passing the claims to a new instance
|
|
|
|
token = jwt.JWT(key=key, jwt=token.claims)
|
|
|
|
|
|
|
|
parsed = json.loads(token.claims)
|
2021-01-27 14:16:08 +00:00
|
|
|
|
2021-01-27 15:50:19 +00:00
|
|
|
if 'nbf' in parsed:
|
|
|
|
# Not Before is present, so we need to check it
|
|
|
|
if time.time() < parsed['nbf']:
|
|
|
|
print('Token can not be used yet!', file=sys.stderr)
|
|
|
|
return None
|
|
|
|
|
2021-01-27 14:16:08 +00:00
|
|
|
if 'exp' in parsed:
|
|
|
|
# Expiration time is present, so we need to check it
|
|
|
|
if time.time() > parsed['exp']:
|
|
|
|
print('Token has expired!', file=sys.stderr)
|
|
|
|
return None
|
2019-01-17 13:53:01 +00:00
|
|
|
|
|
|
|
return (parsed['host'], parsed['port'])
|
|
|
|
except Exception as e:
|
|
|
|
print("Failed to parse token: %s" % str(e), file=sys.stderr)
|
|
|
|
return None
|
|
|
|
except ImportError as e:
|
|
|
|
print("package jwcrypto not found, are you sure you've installed it correctly?", file=sys.stderr)
|
|
|
|
return None
|
2019-04-04 12:53:17 +01:00
|
|
|
|
2020-12-14 08:13:11 +00:00
|
|
|
class TokenRedis():
|
2021-02-07 19:39:02 +00:00
|
|
|
"""
|
|
|
|
The TokenRedis plugin expects the format of the data in a form of json.
|
|
|
|
|
|
|
|
Prepare data with:
|
|
|
|
redis-cli set hello '{"host":"127.0.0.1:5000"}'
|
|
|
|
|
|
|
|
Verify with:
|
|
|
|
redis-cli --raw get hello
|
|
|
|
|
|
|
|
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
|
|
|
|
"""
|
2019-05-24 11:56:23 +01:00
|
|
|
def __init__(self, src):
|
2021-02-07 19:39:02 +00:00
|
|
|
try:
|
|
|
|
# import those ahead of time so we provide error earlier
|
|
|
|
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:
|
|
|
|
print("package redis or simplejson not found, are you sure you've installed them correctly?", file=sys.stderr)
|
|
|
|
sys.exit()
|
2019-04-04 12:53:17 +01:00
|
|
|
|
2019-05-24 11:56:23 +01:00
|
|
|
def lookup(self, token):
|
|
|
|
try:
|
|
|
|
import redis
|
|
|
|
import simplejson
|
2021-02-07 19:39:02 +00:00
|
|
|
except ImportError:
|
2019-05-24 11:56:23 +01:00
|
|
|
print("package redis or simplejson not found, are you sure you've installed them correctly?", file=sys.stderr)
|
2021-02-07 19:39:02 +00:00
|
|
|
sys.exit()
|
2019-04-04 12:53:17 +01:00
|
|
|
|
2021-02-07 19:39:02 +00:00
|
|
|
print("resolving token '%s'" % token, file=sys.stderr)
|
|
|
|
client = redis.Redis(host=self._server, port=self._port)
|
2019-05-24 11:56:23 +01:00
|
|
|
stuff = client.get(token)
|
|
|
|
if stuff is None:
|
|
|
|
return None
|
|
|
|
else:
|
2021-02-07 19:39:02 +00:00
|
|
|
responseStr = stuff.decode("utf-8")
|
|
|
|
print("response from redis : %s" % responseStr, file=sys.stderr)
|
|
|
|
combo = simplejson.loads(responseStr)
|
2020-02-27 14:55:35 +00:00
|
|
|
(host, port) = combo["host"].split(':')
|
2021-02-07 19:39:02 +00:00
|
|
|
print("host: %s, port: %s" % (host,port), file=sys.stderr)
|
|
|
|
return [host, port]
|
2020-03-12 03:49:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
class UnixDomainSocketDirectory(BasePlugin):
|
|
|
|
def __init__(self, *args, **kwargs):
|
2020-12-14 08:13:11 +00:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-03-12 03:49:37 +00:00
|
|
|
self._dir_path = os.path.abspath(self.source)
|
|
|
|
|
|
|
|
def lookup(self, token):
|
|
|
|
try:
|
|
|
|
import stat
|
|
|
|
|
|
|
|
if not os.path.isdir(self._dir_path):
|
|
|
|
return None
|
|
|
|
|
|
|
|
uds_path = os.path.abspath(os.path.join(self._dir_path, token))
|
|
|
|
if not uds_path.startswith(self._dir_path):
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not os.path.exists(uds_path):
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not stat.S_ISSOCK(os.stat(uds_path).st_mode):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return [ 'unix_socket', uds_path ]
|
|
|
|
except Exception as e:
|
|
|
|
print("Error finding unix domain socket: %s" % str(e), file=sys.stderr)
|
|
|
|
return None
|