From 253150cb36e820f0a8262ef559d3295f7c8fe8d1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 2 Jan 2023 11:58:16 +0000 Subject: [PATCH] qt network dialog: don't poll Tor socks proxy, but scan on interaction Polling is introduces spam in Tor logs. Also, Tor Browser 12.0 apparently has a bug where our polling renders the socks proxy unusuable after some time. see https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/41549 Instead of trying to detect a Tor socks proxy every 10 seconds, we now run detection when the Qt network dialog gets opened, and also when the user switches to the "Proxy" tab in the dialog. fixes https://github.com/spesmilo/electrum/issues/7317 --- electrum/gui/qt/network_dialog.py | 54 +++++++++++++++---------------- electrum/util.py | 27 ++++++++++++++++ 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index cb2d4297c..1d523f0ca 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -40,6 +40,7 @@ from electrum import constants, blockchain, util from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL from electrum.network import Network from electrum.logging import get_logger +from electrum.util import detect_tor_socks_proxy from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit) @@ -66,6 +67,11 @@ class NetworkDialog(QDialog, QtEventListener): self.register_callbacks() self._cleaned_up = False + def show(self): + super().show() + if td := self.nlayout.td: + td.trigger_rescan() + @qt_event_listener def on_event_network_updated(self): self.nlayout.update() @@ -203,10 +209,11 @@ class NetworkChoiceLayout(object): self.tor_proxy = None self.tabs = tabs = QTabWidget() - proxy_tab = QWidget() + self._proxy_tab = proxy_tab = QWidget() blockchain_tab = QWidget() tabs.addTab(blockchain_tab, _('Overview')) tabs.addTab(proxy_tab, _('Proxy')) + tabs.currentChanged.connect(self._on_tab_changed) fixed_width_hostname = 24 * char_width_in_lineedit() fixed_width_port = 6 * char_width_in_lineedit() @@ -417,6 +424,10 @@ class NetworkChoiceLayout(object): net_params = net_params._replace(proxy=proxy) self.network.run_from_another_thread(self.network.set_parameters(net_params)) + def _on_tab_changed(self): + if self.tabs.currentWidget() is self._proxy_tab: + self.td.trigger_rescan() + def suggest_proxy(self, found_proxy): if found_proxy is None: self.tor_cb.hide() @@ -457,38 +468,25 @@ class TorDetector(QThread): def __init__(self): QThread.__init__(self) - self._stop_event = threading.Event() + self._work_to_do_evt = threading.Event() + self._stopping = False def run(self): - # Probable ports for Tor to listen at - ports = [9050, 9150] while True: - for p in ports: - net_addr = ("127.0.0.1", p) - if TorDetector.is_tor_port(net_addr): - self.found_proxy.emit(net_addr) - break - else: - self.found_proxy.emit(None) - stopping = self._stop_event.wait(10) - if stopping: + # do rescan + net_addr = detect_tor_socks_proxy() + self.found_proxy.emit(net_addr) + # wait until triggered + self._work_to_do_evt.wait() + self._work_to_do_evt.clear() + if self._stopping: return + def trigger_rescan(self) -> None: + self._work_to_do_evt.set() + def stop(self): - self._stop_event.set() + self._stopping = True + self._work_to_do_evt.set() self.exit() self.wait() - - @staticmethod - def is_tor_port(net_addr: Tuple[str, int]) -> bool: - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(0.1) - s.connect(net_addr) - # Tor responds uniquely to HTTP-like requests - s.send(b"GET\n") - if b"Tor is not an HTTP Proxy" in s.recv(1024): - return True - except socket.error: - pass - return False diff --git a/electrum/util.py b/electrum/util.py index 55ffb0b58..a4d05d8d9 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -47,6 +47,7 @@ import random import secrets import functools from abc import abstractmethod, ABC +import socket import attr import aiohttp @@ -1472,6 +1473,32 @@ class NetworkJobOnDefaultServer(Logger, ABC): return s +def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]: + # Probable ports for Tor to listen at + candidates = [ + ("127.0.0.1", 9050), + ("127.0.0.1", 9150), + ] + for net_addr in candidates: + if is_tor_socks_port(*net_addr): + return net_addr + return None + + +def is_tor_socks_port(host: str, port: int) -> bool: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.1) + s.connect((host, port)) + # Tor responds uniquely to HTTP-like requests + s.send(b"GET\n") + if b"Tor is not an HTTP Proxy" in s.recv(1024): + return True + except socket.error: + pass + return False + + _asyncio_event_loop = None # type: Optional[asyncio.AbstractEventLoop] def get_asyncio_loop() -> asyncio.AbstractEventLoop: """Returns the global asyncio event loop we use."""