mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 01:11:35 +00:00
daemon.py: Add authentication to Watchtower.
Define abstract class AuthenticatedServer
This commit is contained in:
parent
2fed218452
commit
d3fb68575d
2 changed files with 134 additions and 119 deletions
|
@ -140,13 +140,137 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
|
||||||
return rpc_user, rpc_password
|
return rpc_user, rpc_password
|
||||||
|
|
||||||
|
|
||||||
class WatchTowerServer(Logger):
|
class AuthenticationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthenticationInvalidOrMissing(AuthenticationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthenticationCredentialsInvalid(AuthenticationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthenticatedServer(Logger):
|
||||||
|
|
||||||
|
def __init__(self, rpc_user, rpc_password):
|
||||||
|
Logger.__init__(self)
|
||||||
|
self.rpc_user = rpc_user
|
||||||
|
self.rpc_password = rpc_password
|
||||||
|
self.auth_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def authenticate(self, headers):
|
||||||
|
if self.rpc_password == '':
|
||||||
|
# RPC authentication is disabled
|
||||||
|
return
|
||||||
|
auth_string = headers.get('Authorization', None)
|
||||||
|
if auth_string is None:
|
||||||
|
raise AuthenticationInvalidOrMissing('CredentialsMissing')
|
||||||
|
basic, _, encoded = auth_string.partition(' ')
|
||||||
|
if basic != 'Basic':
|
||||||
|
raise AuthenticationInvalidOrMissing('UnsupportedType')
|
||||||
|
encoded = to_bytes(encoded, 'utf8')
|
||||||
|
credentials = to_string(b64decode(encoded), 'utf8')
|
||||||
|
username, _, password = credentials.partition(':')
|
||||||
|
if not (constant_time_compare(username, self.rpc_user)
|
||||||
|
and constant_time_compare(password, self.rpc_password)):
|
||||||
|
await asyncio.sleep(0.050)
|
||||||
|
raise AuthenticationCredentialsInvalid('Invalid Credentials')
|
||||||
|
|
||||||
|
async def handle(self, request):
|
||||||
|
async with self.auth_lock:
|
||||||
|
try:
|
||||||
|
await self.authenticate(request.headers)
|
||||||
|
except AuthenticationInvalidOrMissing:
|
||||||
|
return web.Response(headers={"WWW-Authenticate": "Basic realm=Electrum"},
|
||||||
|
text='Unauthorized', status=401)
|
||||||
|
except AuthenticationCredentialsInvalid:
|
||||||
|
return web.Response(text='Forbidden', status=403)
|
||||||
|
request = await request.text()
|
||||||
|
response = await jsonrpcserver.async_dispatch(request, methods=self.methods)
|
||||||
|
if isinstance(response, jsonrpcserver.response.ExceptionResponse):
|
||||||
|
self.logger.error(f"error handling request: {request}", exc_info=response.exc)
|
||||||
|
# this exposes the error message to the client
|
||||||
|
response.message = str(response.exc)
|
||||||
|
if response.wanted:
|
||||||
|
return web.json_response(response.deserialized(), status=response.http_status)
|
||||||
|
else:
|
||||||
|
return web.Response()
|
||||||
|
|
||||||
|
|
||||||
|
class CommandsServer(AuthenticatedServer):
|
||||||
|
|
||||||
|
def __init__(self, daemon, fd):
|
||||||
|
rpc_user, rpc_password = get_rpc_credentials(daemon.config)
|
||||||
|
AuthenticatedServer.__init__(self, rpc_user, rpc_password)
|
||||||
|
self.daemon = daemon
|
||||||
|
self.fd = fd
|
||||||
|
self.config = daemon.config
|
||||||
|
self.host = self.config.get('rpchost', '127.0.0.1')
|
||||||
|
self.port = self.config.get('rpcport', 0)
|
||||||
|
self.app = web.Application()
|
||||||
|
self.app.router.add_post("/", self.handle)
|
||||||
|
self.methods = jsonrpcserver.methods.Methods()
|
||||||
|
self.methods.add(self.ping)
|
||||||
|
self.methods.add(self.gui)
|
||||||
|
self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon)
|
||||||
|
for cmdname in known_commands:
|
||||||
|
self.methods.add(getattr(self.cmd_runner, cmdname))
|
||||||
|
self.methods.add(self.run_cmdline)
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
self.runner = web.AppRunner(self.app)
|
||||||
|
await self.runner.setup()
|
||||||
|
site = web.TCPSite(self.runner, self.host, self.port)
|
||||||
|
await site.start()
|
||||||
|
socket = site._server.sockets[0]
|
||||||
|
os.write(self.fd, bytes(repr((socket.getsockname(), time.time())), 'utf8'))
|
||||||
|
os.close(self.fd)
|
||||||
|
|
||||||
|
async def ping(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def gui(self, config_options):
|
||||||
|
if self.daemon.gui_object:
|
||||||
|
if hasattr(self.daemon.gui_object, 'new_window'):
|
||||||
|
path = self.config.get_wallet_path(use_gui_last_wallet=True)
|
||||||
|
self.daemon.gui_object.new_window(path, config_options.get('url'))
|
||||||
|
response = "ok"
|
||||||
|
else:
|
||||||
|
response = "error: current GUI does not support multiple windows"
|
||||||
|
else:
|
||||||
|
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def run_cmdline(self, config_options):
|
||||||
|
cmdname = config_options['cmd']
|
||||||
|
cmd = known_commands[cmdname]
|
||||||
|
# arguments passed to function
|
||||||
|
args = [config_options.get(x) for x in cmd.params]
|
||||||
|
# decode json arguments
|
||||||
|
args = [json_decode(i) for i in args]
|
||||||
|
# options
|
||||||
|
kwargs = {}
|
||||||
|
for x in cmd.options:
|
||||||
|
kwargs[x] = config_options.get(x)
|
||||||
|
if cmd.requires_wallet:
|
||||||
|
kwargs['wallet_path'] = config_options.get('wallet_path')
|
||||||
|
func = getattr(self.cmd_runner, cmd.name)
|
||||||
|
# fixme: not sure how to retrieve message in jsonrpcclient
|
||||||
|
try:
|
||||||
|
result = await func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
result = {'error':str(e)}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class WatchTowerServer(AuthenticatedServer):
|
||||||
|
|
||||||
def __init__(self, network, netaddress):
|
def __init__(self, network, netaddress):
|
||||||
Logger.__init__(self)
|
|
||||||
self.addr = netaddress
|
self.addr = netaddress
|
||||||
self.config = network.config
|
self.config = network.config
|
||||||
self.network = network
|
self.network = network
|
||||||
|
watchtower_user = self.config.get('watchtower_user', '')
|
||||||
|
watchtower_password = self.config.get('watchtower_password', '')
|
||||||
|
AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
|
||||||
self.lnwatcher = network.local_watchtower
|
self.lnwatcher = network.local_watchtower
|
||||||
self.app = web.Application()
|
self.app = web.Application()
|
||||||
self.app.router.add_post("/", self.handle)
|
self.app.router.add_post("/", self.handle)
|
||||||
|
@ -154,15 +278,6 @@ class WatchTowerServer(Logger):
|
||||||
self.methods.add(self.get_ctn)
|
self.methods.add(self.get_ctn)
|
||||||
self.methods.add(self.add_sweep_tx)
|
self.methods.add(self.add_sweep_tx)
|
||||||
|
|
||||||
async def handle(self, request):
|
|
||||||
request = await request.text()
|
|
||||||
self.logger.info(f'{request}')
|
|
||||||
response = await jsonrpcserver.async_dispatch(request, methods=self.methods)
|
|
||||||
if response.wanted:
|
|
||||||
return web.json_response(response.deserialized(), status=response.http_status)
|
|
||||||
else:
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
self.runner = web.AppRunner(self.app)
|
self.runner = web.AppRunner(self.app)
|
||||||
await self.runner.setup()
|
await self.runner.setup()
|
||||||
|
@ -268,14 +383,6 @@ class PayServer(Logger):
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AuthenticationInvalidOrMissing(AuthenticationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AuthenticationCredentialsInvalid(AuthenticationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Daemon(Logger):
|
class Daemon(Logger):
|
||||||
|
|
||||||
|
@ -284,7 +391,6 @@ class Daemon(Logger):
|
||||||
@profiler
|
@profiler
|
||||||
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
|
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
|
||||||
Logger.__init__(self)
|
Logger.__init__(self)
|
||||||
self.auth_lock = asyncio.Lock()
|
|
||||||
self.running = False
|
self.running = False
|
||||||
self.running_lock = threading.Lock()
|
self.running_lock = threading.Lock()
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -301,10 +407,12 @@ class Daemon(Logger):
|
||||||
# path -> wallet; make sure path is standardized.
|
# path -> wallet; make sure path is standardized.
|
||||||
self._wallets = {} # type: Dict[str, Abstract_Wallet]
|
self._wallets = {} # type: Dict[str, Abstract_Wallet]
|
||||||
daemon_jobs = []
|
daemon_jobs = []
|
||||||
# Setup JSONRPC server
|
# Setup commands server
|
||||||
|
self.commands_server = None
|
||||||
if listen_jsonrpc:
|
if listen_jsonrpc:
|
||||||
daemon_jobs.append(self.start_jsonrpc(config, fd))
|
self.commands_server = CommandsServer(self, fd)
|
||||||
# request server
|
daemon_jobs.append(self.commands_server.run())
|
||||||
|
# pay server
|
||||||
self.pay_server = None
|
self.pay_server = None
|
||||||
payserver_address = self.config.get_netaddress('payserver_address')
|
payserver_address = self.config.get_netaddress('payserver_address')
|
||||||
if not config.get('offline') and payserver_address:
|
if not config.get('offline') and payserver_address:
|
||||||
|
@ -338,80 +446,6 @@ class Daemon(Logger):
|
||||||
finally:
|
finally:
|
||||||
self.logger.info("taskgroup stopped.")
|
self.logger.info("taskgroup stopped.")
|
||||||
|
|
||||||
async def authenticate(self, headers):
|
|
||||||
if self.rpc_password == '':
|
|
||||||
# RPC authentication is disabled
|
|
||||||
return
|
|
||||||
auth_string = headers.get('Authorization', None)
|
|
||||||
if auth_string is None:
|
|
||||||
raise AuthenticationInvalidOrMissing('CredentialsMissing')
|
|
||||||
basic, _, encoded = auth_string.partition(' ')
|
|
||||||
if basic != 'Basic':
|
|
||||||
raise AuthenticationInvalidOrMissing('UnsupportedType')
|
|
||||||
encoded = to_bytes(encoded, 'utf8')
|
|
||||||
credentials = to_string(b64decode(encoded), 'utf8')
|
|
||||||
username, _, password = credentials.partition(':')
|
|
||||||
if not (constant_time_compare(username, self.rpc_user)
|
|
||||||
and constant_time_compare(password, self.rpc_password)):
|
|
||||||
await asyncio.sleep(0.050)
|
|
||||||
raise AuthenticationCredentialsInvalid('Invalid Credentials')
|
|
||||||
|
|
||||||
async def handle(self, request):
|
|
||||||
async with self.auth_lock:
|
|
||||||
try:
|
|
||||||
await self.authenticate(request.headers)
|
|
||||||
except AuthenticationInvalidOrMissing:
|
|
||||||
return web.Response(headers={"WWW-Authenticate": "Basic realm=Electrum"},
|
|
||||||
text='Unauthorized', status=401)
|
|
||||||
except AuthenticationCredentialsInvalid:
|
|
||||||
return web.Response(text='Forbidden', status=403)
|
|
||||||
request = await request.text()
|
|
||||||
response = await jsonrpcserver.async_dispatch(request, methods=self.methods)
|
|
||||||
if isinstance(response, jsonrpcserver.response.ExceptionResponse):
|
|
||||||
self.logger.error(f"error handling request: {request}", exc_info=response.exc)
|
|
||||||
# this exposes the error message to the client
|
|
||||||
response.message = str(response.exc)
|
|
||||||
if response.wanted:
|
|
||||||
return web.json_response(response.deserialized(), status=response.http_status)
|
|
||||||
else:
|
|
||||||
return web.Response()
|
|
||||||
|
|
||||||
async def start_jsonrpc(self, config: SimpleConfig, fd):
|
|
||||||
self.app = web.Application()
|
|
||||||
self.app.router.add_post("/", self.handle)
|
|
||||||
self.rpc_user, self.rpc_password = get_rpc_credentials(config)
|
|
||||||
self.methods = jsonrpcserver.methods.Methods()
|
|
||||||
self.methods.add(self.ping)
|
|
||||||
self.methods.add(self.gui)
|
|
||||||
self.cmd_runner = Commands(config=self.config, network=self.network, daemon=self)
|
|
||||||
for cmdname in known_commands:
|
|
||||||
self.methods.add(getattr(self.cmd_runner, cmdname))
|
|
||||||
self.methods.add(self.run_cmdline)
|
|
||||||
self.host = config.get('rpchost', '127.0.0.1')
|
|
||||||
self.port = config.get('rpcport', 0)
|
|
||||||
self.runner = web.AppRunner(self.app)
|
|
||||||
await self.runner.setup()
|
|
||||||
site = web.TCPSite(self.runner, self.host, self.port)
|
|
||||||
await site.start()
|
|
||||||
socket = site._server.sockets[0]
|
|
||||||
os.write(fd, bytes(repr((socket.getsockname(), time.time())), 'utf8'))
|
|
||||||
os.close(fd)
|
|
||||||
|
|
||||||
async def ping(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def gui(self, config_options):
|
|
||||||
if self.gui_object:
|
|
||||||
if hasattr(self.gui_object, 'new_window'):
|
|
||||||
path = self.config.get_wallet_path(use_gui_last_wallet=True)
|
|
||||||
self.gui_object.new_window(path, config_options.get('url'))
|
|
||||||
response = "ok"
|
|
||||||
else:
|
|
||||||
response = "error: current GUI does not support multiple windows"
|
|
||||||
else:
|
|
||||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
|
||||||
return response
|
|
||||||
|
|
||||||
def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]:
|
def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]:
|
||||||
path = standardize_path(path)
|
path = standardize_path(path)
|
||||||
# wizard will be launched if we return
|
# wizard will be launched if we return
|
||||||
|
@ -466,27 +500,6 @@ class Daemon(Logger):
|
||||||
wallet.stop()
|
wallet.stop()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def run_cmdline(self, config_options):
|
|
||||||
cmdname = config_options['cmd']
|
|
||||||
cmd = known_commands[cmdname]
|
|
||||||
# arguments passed to function
|
|
||||||
args = [config_options.get(x) for x in cmd.params]
|
|
||||||
# decode json arguments
|
|
||||||
args = [json_decode(i) for i in args]
|
|
||||||
# options
|
|
||||||
kwargs = {}
|
|
||||||
for x in cmd.options:
|
|
||||||
kwargs[x] = config_options.get(x)
|
|
||||||
if cmd.requires_wallet:
|
|
||||||
kwargs['wallet_path'] = config_options.get('wallet_path')
|
|
||||||
func = getattr(self.cmd_runner, cmd.name)
|
|
||||||
# fixme: not sure how to retrieve message in jsonrpcclient
|
|
||||||
try:
|
|
||||||
result = await func(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
result = {'error':str(e)}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run_daemon(self):
|
def run_daemon(self):
|
||||||
self.running = True
|
self.running = True
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -318,8 +318,10 @@ fi
|
||||||
if [[ $1 == "configure_test_watchtower" ]]; then
|
if [[ $1 == "configure_test_watchtower" ]]; then
|
||||||
# carol is the watchtower of bob
|
# carol is the watchtower of bob
|
||||||
$carol setconfig -o run_local_watchtower true
|
$carol setconfig -o run_local_watchtower true
|
||||||
|
$carol setconfig -o watchtower_user wtuser
|
||||||
|
$carol setconfig -o watchtower_password wtpassword
|
||||||
$carol setconfig -o watchtower_address 127.0.0.1:12345
|
$carol setconfig -o watchtower_address 127.0.0.1:12345
|
||||||
$bob setconfig -o watchtower_url http://127.0.0.1:12345
|
$bob setconfig -o watchtower_url http://wtuser:wtpassword@127.0.0.1:12345
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $1 == "watchtower" ]]; then
|
if [[ $1 == "watchtower" ]]; then
|
||||||
|
|
Loading…
Add table
Reference in a new issue