diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 9cd48b3..701bc20 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -12354,11 +12354,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.closeDB(cursor, commit=False) def getLockedState(self): - if self._is_encrypted is None or self._is_locked is None: - self._is_encrypted, self._is_locked = self.ci( - Coins.PART - ).isWalletEncryptedLocked() - return self._is_encrypted, self._is_locked + try: + if self._is_encrypted is None or self._is_locked is None: + self._is_encrypted, self._is_locked = self.ci( + Coins.PART + ).isWalletEncryptedLocked() + return self._is_encrypted, self._is_locked + except Exception as e: + self.log.warning(f"getLockedState failed: {e}") + return False, False def getExchangeName(self, coin_id: int, exchange_name: str) -> str: if coin_id == Coins.BCH: diff --git a/basicswap/http_server.py b/basicswap/http_server.py index e85c09a..ba30795 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -218,31 +218,34 @@ class HttpHandler(BaseHTTPRequestHandler): args_dict["debug_mode"] = True if swap_client.debug_ui: args_dict["debug_ui_mode"] = True - if swap_client.use_tor_proxy: - args_dict["use_tor_proxy"] = True + + is_authenticated = self.is_authenticated() or not swap_client.settings.get( + "client_auth_hash" + ) + + if is_authenticated: + if swap_client.use_tor_proxy: + args_dict["use_tor_proxy"] = True + try: + tor_state = get_tor_established_state(swap_client) + args_dict["tor_established"] = True if tor_state == "1" else False + except Exception: + args_dict["tor_established"] = False + + from .ui.page_amm import get_amm_status, get_amm_active_count + try: - tor_state = get_tor_established_state(swap_client) - args_dict["tor_established"] = True if tor_state == "1" else False - except Exception as e: - args_dict["tor_established"] = False - if swap_client.debug: - swap_client.log.error(f"Error getting Tor state: {str(e)}") - swap_client.log.error(traceback.format_exc()) + args_dict["current_status"] = get_amm_status() + args_dict["amm_active_count"] = get_amm_active_count(swap_client) + except Exception: + args_dict["current_status"] = "stopped" + args_dict["amm_active_count"] = 0 - from .ui.page_amm import get_amm_status, get_amm_active_count - - try: - args_dict["current_status"] = get_amm_status() - args_dict["amm_active_count"] = get_amm_active_count(swap_client) - except Exception as e: - args_dict["current_status"] = "stopped" + if swap_client._show_notifications: + args_dict["notifications"] = swap_client.getNotifications() + else: + args_dict["current_status"] = "unknown" args_dict["amm_active_count"] = 0 - if swap_client.debug: - swap_client.log.error(f"Error getting AMM state: {str(e)}") - swap_client.log.error(traceback.format_exc()) - - if swap_client._show_notifications: - args_dict["notifications"] = swap_client.getNotifications() if "messages" in args_dict: messages_with_ids = [] @@ -274,9 +277,19 @@ class HttpHandler(BaseHTTPRequestHandler): self.server.session_tokens["shutdown"] = shutdown_token args_dict["shutdown_token"] = shutdown_token - encrypted, locked = swap_client.getLockedState() - args_dict["encrypted"] = encrypted - args_dict["locked"] = locked + if is_authenticated: + try: + encrypted, locked = swap_client.getLockedState() + args_dict["encrypted"] = encrypted + args_dict["locked"] = locked + except Exception as e: + args_dict["encrypted"] = False + args_dict["locked"] = False + if swap_client.debug: + swap_client.log.warning(f"Could not get wallet locked state: {e}") + else: + args_dict["encrypted"] = args_dict.get("encrypted", False) + args_dict["locked"] = args_dict.get("locked", False) with self.server.msg_id_lock: if self.server.msg_id_counter >= 0x7FFFFFFF: diff --git a/basicswap/rpc.py b/basicswap/rpc.py index 9d77e03..9a400c4 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -9,6 +9,7 @@ import json import logging import traceback import urllib +import http.client from xmlrpc.client import ( Fault, Transport, @@ -26,6 +27,26 @@ def enable_rpc_pooling(settings): _rpc_pool_settings = settings +class TimeoutTransport(Transport): + def __init__(self, timeout=10, *args, **kwargs): + self.timeout = timeout + super().__init__(*args, **kwargs) + + def make_connection(self, host): + conn = http.client.HTTPConnection(host, timeout=self.timeout) + return conn + + +class TimeoutSafeTransport(SafeTransport): + def __init__(self, timeout=10, *args, **kwargs): + self.timeout = timeout + super().__init__(*args, **kwargs) + + def make_connection(self, host): + conn = http.client.HTTPSConnection(host, timeout=self.timeout) + return conn + + class Jsonrpc: # __getattr__ complicates extending ServerProxy def __init__( @@ -39,22 +60,40 @@ class Jsonrpc: use_builtin_types=False, *, context=None, + timeout=10, ): # establish a "logical" server connection - # get the url parsed = urllib.parse.urlparse(uri) if parsed.scheme not in ("http", "https"): raise OSError("unsupported XML-RPC protocol") - self.__host = parsed.netloc + + self.__auth = None + if "@" in parsed.netloc: + auth_part, host_port = parsed.netloc.rsplit("@", 1) + self.__host = host_port + if ":" in auth_part: + import base64 + + auth_bytes = auth_part.encode("utf-8") + auth_b64 = base64.b64encode(auth_bytes).decode("ascii") + self.__auth = f"Basic {auth_b64}" + else: + self.__host = parsed.netloc + + if not self.__host: + raise ValueError(f"Invalid or empty hostname in URI: {uri}") self.__handler = parsed.path if not self.__handler: self.__handler = "/RPC2" if transport is None: - handler = SafeTransport if parsed.scheme == "https" else Transport + handler = ( + TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport + ) extra_kwargs = {} transport = handler( + timeout=timeout, use_datetime=use_datetime, use_builtin_types=use_builtin_types, **extra_kwargs, @@ -72,6 +111,7 @@ class Jsonrpc: self.__transport.close() def json_request(self, method, params): + connection = None try: connection = self.__transport.make_connection(self.__host) headers = self.__transport._extra_headers[:] @@ -81,6 +121,10 @@ class Jsonrpc: connection.putrequest("POST", self.__handler) headers.append(("Content-Type", "application/json")) headers.append(("User-Agent", "jsonrpc")) + + if self.__auth: + headers.append(("Authorization", self.__auth)) + self.__transport.send_headers(connection, headers) self.__transport.send_content( connection, @@ -89,15 +133,23 @@ class Jsonrpc: self.__request_id += 1 resp = connection.getresponse() - return resp.read() + result = resp.read() + + connection.close() + + return result except Fault: raise except Exception: - # All unexpected errors leave connection in - # a strange state, so we clear it. self.__transport.close() raise + finally: + if connection is not None: + try: + connection.close() + except Exception: + pass def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"): @@ -114,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"): x.close() r = json.loads(v.decode("utf-8")) except Exception as ex: - traceback.print_exc() raise ValueError(f"RPC server error: {ex}, method: {method}") if "error" in r and r["error"] is not None: @@ -211,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"): def escape_rpcauth(auth_str: str) -> str: username, password = auth_str.split(":", 1) + username = urllib.parse.quote(username, safe="") password = urllib.parse.quote(password, safe="") return f"{username}:{password}" diff --git a/basicswap/rpc_pool.py b/basicswap/rpc_pool.py index d3c3e23..3042937 100644 --- a/basicswap/rpc_pool.py +++ b/basicswap/rpc_pool.py @@ -12,7 +12,7 @@ from basicswap.rpc import Jsonrpc class RPCConnectionPool: def __init__( - self, url, max_connections=5, timeout=30, logger=None, max_idle_time=300 + self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300 ): self.url = url self.max_connections = max_connections