URI: 
       tdaemon.py: Add authentication to Watchtower. Define abstract class AuthenticatedServer - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit d3fb68575d17ec3e58ad72a1325b38e5267d5b26
   DIR parent 2fed2184526e16fda81dc9d9675788426c35543c
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 12 May 2020 10:02:22 +0200
       
       daemon.py: Add authentication to Watchtower.
       Define abstract class AuthenticatedServer
       
       Diffstat:
         M electrum/daemon.py                  |     249 ++++++++++++++++---------------
         M electrum/tests/regtest/regtest.sh   |       4 +++-
       
       2 files changed, 134 insertions(+), 119 deletions(-)
       ---
   DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
       t@@ -140,13 +140,137 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
            return rpc_user, rpc_password
        
        
       -class WatchTowerServer(Logger):
       +class AuthenticationError(Exception):
       +    pass
        
       -    def __init__(self, network, netaddress):
       +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):
                self.addr = netaddress
                self.config = network.config
                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.app = web.Application()
                self.app.router.add_post("/", self.handle)
       t@@ -154,15 +278,6 @@ class WatchTowerServer(Logger):
                self.methods.add(self.get_ctn)
                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):
                self.runner = web.AppRunner(self.app)
                await self.runner.setup()
       t@@ -268,14 +383,6 @@ class PayServer(Logger):
                return ws
        
        
       -class AuthenticationError(Exception):
       -    pass
       -
       -class AuthenticationInvalidOrMissing(AuthenticationError):
       -    pass
       -
       -class AuthenticationCredentialsInvalid(AuthenticationError):
       -    pass
        
        class Daemon(Logger):
        
       t@@ -284,7 +391,6 @@ class Daemon(Logger):
            @profiler
            def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
                Logger.__init__(self)
       -        self.auth_lock = asyncio.Lock()
                self.running = False
                self.running_lock = threading.Lock()
                self.config = config
       t@@ -301,10 +407,12 @@ class Daemon(Logger):
                # path -> wallet;   make sure path is standardized.
                self._wallets = {}  # type: Dict[str, Abstract_Wallet]
                daemon_jobs = []
       -        # Setup JSONRPC server
       +        # Setup commands server
       +        self.commands_server = None
                if listen_jsonrpc:
       -            daemon_jobs.append(self.start_jsonrpc(config, fd))
       -        # request server
       +            self.commands_server = CommandsServer(self, fd)
       +            daemon_jobs.append(self.commands_server.run())
       +        # pay server
                self.pay_server = None
                payserver_address = self.config.get_netaddress('payserver_address')
                if not config.get('offline') and payserver_address:
       t@@ -338,80 +446,6 @@ class Daemon(Logger):
                finally:
                    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]:
                path = standardize_path(path)
                # wizard will be launched if we return
       t@@ -466,27 +500,6 @@ class Daemon(Logger):
                wallet.stop()
                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):
                self.running = True
                try:
   DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
       t@@ -318,8 +318,10 @@ fi
        if [[ $1 == "configure_test_watchtower" ]]; then
            # carol is the watchtower of bob
            $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
       -    $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
        
        if [[ $1 == "watchtower" ]]; then