diff --git a/electrum/daemon.py b/electrum/daemon.py index a8fc2ceb1..b22c1d192 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -286,25 +286,31 @@ class AuthenticatedServer(Logger): class CommandsServer(AuthenticatedServer): - def __init__(self, daemon: 'Daemon', fd): + def __init__(self, daemon: 'Daemon', fd, *, only_minimal_jsonrpc: bool): rpc_user, rpc_password = get_rpc_credentials(daemon.config) AuthenticatedServer.__init__(self, rpc_user, rpc_password) self.daemon = daemon self.fd = fd + self._only_minimal_jsonrpc = only_minimal_jsonrpc self.config = daemon.config sockettype = self.config.RPC_SOCKET_TYPE self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config) self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config) self.host = self.config.RPC_HOST self.port = self.config.RPC_PORT + self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon) self.app = web.Application() self.app.router.add_post("/", self.handle) + # First add always-enabled commands that are also available for "minimal" rpc server. + # - "ping" RPC is needed for the lockfile fd to work. self.register_method('ping', self.ping) + # - "gui" RPC is needed for URI handling. (TODO restrict further: disallow opening arbitrary file paths) self.register_method('gui', self.gui) - self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon) - for cmdname in known_commands: - self.register_method(cmdname, getattr(self.cmd_runner, cmdname)) - self.register_method('run_cmdline', self.run_cmdline) + # Add other commands: + if not only_minimal_jsonrpc: + for cmdname in known_commands: + self.register_method(cmdname, getattr(self.cmd_runner, cmdname)) + self.register_method('run_cmdline', self.run_cmdline) def _socket_config_str(self) -> str: if self.socktype == 'unix': @@ -336,7 +342,9 @@ class CommandsServer(AuthenticatedServer): raise Exception(f"impossible socktype ({self.socktype!r})") os.write(self.fd, bytes(repr((self.socktype, addr, time.time())), 'utf8')) os.close(self.fd) - self.logger.info(f"now running and listening. socktype={self.socktype}, addr={addr}") + self.logger.info( + f"now running and listening. socktype={self.socktype}, addr={addr}. " + f"only_minimal_jsonrpc={self._only_minimal_jsonrpc}") async def ping(self): return True @@ -394,6 +402,7 @@ class Daemon(Logger): fd=None, *, listen_jsonrpc: bool = True, + only_minimal_jsonrpc: bool = True, start_network: bool = True, # setting to False allows customising network settings before starting it ): Logger.__init__(self) @@ -423,7 +432,7 @@ class Daemon(Logger): # Setup commands server self.commands_server = None if listen_jsonrpc: - self.commands_server = CommandsServer(self, fd) + self.commands_server = CommandsServer(self, fd, only_minimal_jsonrpc=only_minimal_jsonrpc) asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.commands_server.run()), self.asyncio_loop) @log_exceptions diff --git a/run_electrum b/run_electrum index df45836e1..e769ed474 100755 --- a/run_electrum +++ b/run_electrum @@ -123,6 +123,7 @@ from electrum.commands import get_parser, get_simple_parser, known_commands, Com from electrum import daemon from electrum.util import create_and_start_event_loop, UserFacingException, JsonRPCError from electrum.i18n import set_language +from electrum import constants if TYPE_CHECKING: import threading @@ -509,7 +510,9 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): configure_logging(config) fd = daemon.get_file_descriptor(config) if fd is not None: - d = daemon.Daemon(config, fd, start_network=False) + # When running in GUI mode, only start a limited minimal RPC server, to limit attack surface. + only_minimal_jsonrpc = not constants.net.TESTNET + d = daemon.Daemon(config, fd, start_network=False, only_minimal_jsonrpc=only_minimal_jsonrpc) try: d.run_gui() except BaseException as e: @@ -535,7 +538,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): fd = daemon.get_file_descriptor(config) if fd is not None: # run daemon - d = daemon.Daemon(config, fd) + d = daemon.Daemon(config, fd, only_minimal_jsonrpc=False) d.run_daemon() sys_exit(0) else: @@ -577,7 +580,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): except Exception as e: _logger.exception("error running command (with daemon)") sys_exit(1) - else: + else: # --offline if cmd.requires_network: print_msg("This command cannot be run offline") sys_exit(1)