Skip to content

Commit

Permalink
Merge pull request #1941 from EchterAgo/improve_tor_msg
Browse files Browse the repository at this point in the history
Tor: Make the messages refer to a specific Tor binary
  • Loading branch information
cculianu committed Aug 11, 2020
2 parents e99580a + d50ced9 commit ef7d53a
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 46 deletions.
63 changes: 40 additions & 23 deletions gui/qt/network_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,23 +496,16 @@ def hideEvent(slf, e):
"In general, connections routed through Tor hide your IP address from servers, at the expense of "
"performance and network throughput.") + "\n\n" +
_("For the average user, it's recommended that you leave this option "
"disabled and only leave the 'integrated Tor client' option enabled.") )
"disabled and only leave the 'Start Tor client' option enabled.") )
self.tor_cb.setToolTip(tor_proxy_tooltip)

self.tor_enabled = QCheckBox(_("Start integrated Tor client"))
self.tor_enabled = QCheckBox()
self.tor_enabled.setIcon(QIcon(":icons/tor_logo.svg"))
self.tor_enabled.clicked.connect(self.set_tor_enabled)
self.tor_enabled.setChecked(self.network.tor_controller.is_enabled())
tor_enabled_tooltip = _("This will enable the integrated Tor proxy.")
self.tor_enabled.setToolTip(tor_enabled_tooltip)
tor_enabled_help = (
tor_enabled_tooltip + "\n\n"
+ _("If unsure, it's safe to enable this feature, and leave 'Use Tor Proxy' disabled. "
"In that situation, only certain plugins (such as CashFusion) will use Tor, but your "
"regular SPV server connections will remain unaffected.") )
self.tor_enabled_help = HelpButton('')

self.tor_custom_port_cb = QCheckBox(_("Custom port"))
self.tor_custom_port_cb.setEnabled(self.tor_enabled.isChecked())
self.tor_enabled.clicked.connect(self.tor_custom_port_cb.setEnabled)
self.tor_custom_port_cb.setChecked(bool(self.network.tor_controller.get_socks_port()))
self.tor_custom_port_cb.clicked.connect(self.on_custom_port_cb_click)
Expand All @@ -526,12 +519,13 @@ def hideEvent(slf, e):
self.tor_socks_port.setText(str(self.network.tor_controller.get_socks_port()))
self.tor_socks_port.setToolTip(custom_port_tooltip)
self.tor_socks_port.setValidator(UserPortValidator(self.tor_socks_port, accept_zero=True))
self.tor_socks_port.setEnabled(self.tor_custom_port_cb.isChecked())

# Start integrated Tor
self.update_tor_enabled()

# Start Tor
grid.addWidget(self.tor_enabled, 1, 0, 1, 2)
grid.addWidget(HelpButton(tor_enabled_help), 1, 4)
# Custom integrated Tor port
grid.addWidget(self.tor_enabled_help, 1, 4)
# Custom Tor port
hbox = QHBoxLayout()
hbox.addSpacing(20) # indentation
hbox.addWidget(self.tor_custom_port_cb, 0, Qt.AlignLeft|Qt.AlignVCenter)
Expand Down Expand Up @@ -597,6 +591,33 @@ def hideEvent(slf, e):
self.fill_in_proxy_settings()
self.update()

_tor_client_names = {
TorController.BinaryType.MISSING: _('Tor'),
TorController.BinaryType.SYSTEM: _('system Tor'),
TorController.BinaryType.INTEGRATED: _('integrated Tor')
}

def update_tor_enabled(self, *args):
tbt = self.network.tor_controller.tor_binary_type
tbname = self._tor_client_names[tbt]

self.tor_enabled.setText(_("Start {tor_binary_name} client").format(tor_binary_name=tbname))
avalable = tbt != TorController.BinaryType.MISSING
self.tor_enabled.setEnabled(avalable)
self.tor_custom_port_cb.setEnabled(avalable and self.tor_enabled.isChecked())
self.tor_socks_port.setEnabled(avalable and self.tor_custom_port_cb.isChecked())

tor_enabled_tooltip = [_("This will start a private instance of the Tor proxy controlled by Electron Cash.")]
if not avalable:
tor_enabled_tooltip.insert(0, _("This feature is unavailable because no Tor binary was found."))
tor_enabled_tooltip_text = ' '.join(tor_enabled_tooltip)
self.tor_enabled.setToolTip(tor_enabled_tooltip_text)
self.tor_enabled_help.help_text = (
tor_enabled_tooltip_text + "\n\n"
+ _("If unsure, it's safe to enable this feature, and leave 'Use Tor Proxy' disabled. "
"In that situation, only certain plugins (such as CashFusion) will use Tor, but your "
"regular SPV server connections will remain unaffected.") )

def jumpto(self, location : str):
if not isinstance(location, str):
return
Expand Down Expand Up @@ -862,14 +883,10 @@ def set_tor_enabled(self, enabled: bool):

@in_main_thread
def on_tor_status_changed(self, controller):
if controller.status in (TorController.Status.STARTED, TorController.Status.READY):
self.tor_enabled.setChecked(True)
elif controller.status == TorController.Status.ERRORED and self.tabs.isVisible():
QMessageBox.critical(None, _("Tor Client Error"),
_("The integrated Tor client experienced an error or could not be started."))
else:
self.tor_enabled.setChecked(False)
self.tor_custom_port_cb.setEnabled(self.tor_enabled.isChecked())
if controller.status == TorController.Status.ERRORED and self.tabs.isVisible():
tbname = self._tor_client_names[self.network.tor_controller.tor_binary_type]
msg = _("The {tor_binary_name} client experienced an error or could not be started.").format(tor_binary_name=tbname)
QMessageBox.critical(None, _("Tor Client Error"), msg)

def set_tor_socks_port(self):
socks_port = int(self.tor_socks_port.text())
Expand Down Expand Up @@ -983,7 +1000,7 @@ def run(self):
stopq = self.stopQ.get(timeout=10.0) # keep trying every 10 seconds
if stopq is None:
return # we must have gotten a stop signal if we get here, break out of function, ending thread
# We were kicked, which means the integrated tor port changed.
# We were kicked, which means the tor port changed.
# Run the detection after a slight delay which increases the reliability.
QThread.msleep(250)
continue
Expand Down
64 changes: 48 additions & 16 deletions lib/tor/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
import threading
import shutil
import socket
from enum import Enum
from enum import IntEnum, unique
from typing import Tuple, Optional

import stem.socket
import stem.process
Expand All @@ -54,30 +55,27 @@ def _make_socket_monkey_patch(self):
raise stem.SocketError(exc)
stem.socket.ControlPort._make_socket = _make_socket_monkey_patch

if sys.platform in ('windows', 'win32'):
_TOR_BINARY_NAME = os.path.join(
os.path.dirname(__file__), '..', '..', 'tor.exe')
else:
_TOR_BINARY_NAME = os.path.join(os.path.dirname(__file__), 'bin', 'tor')

if not os.path.isfile(_TOR_BINARY_NAME):
# Tor is not packaged / built, try to locate a system tor
_TOR_BINARY_NAME = shutil.which('tor')

_TOR_ENABLED_KEY = 'tor_enabled'
_TOR_ENABLED_DEFAULT = False

_TOR_SOCKS_PORT_KEY = 'tor_socks_port'
_TOR_SOCKS_PORT_DEFAULT = 0

class TorController(PrintError):
class Status(Enum):
@unique
class Status(IntEnum):
STOPPING = 0
STOPPED = 1
STARTED = 2
READY = 3
ERRORED = 4

@unique
class BinaryType(IntEnum):
MISSING = 0
INTEGRATED = 1
SYSTEM = 2

_config: SimpleConfig = None
_tor_process: subprocess.Popen = None
_tor_read_thread: threading.Thread = None
Expand All @@ -90,12 +88,19 @@ class Status(Enum):
active_control_port: int = None
active_port_changed = Event()

tor_binary: str
tor_binary_type: BinaryType = BinaryType.MISSING

def __init__(self, config: SimpleConfig):
if not config:
raise AssertionError('TorController: config must be set')

self._config = config

if not self.detect_tor() and self.is_enabled():
self.print_error("Tor enabled but no usable Tor binary found, disabling")
self.set_enabled(False)

socks_port = self._config.get(
_TOR_SOCKS_PORT_KEY, _TOR_SOCKS_PORT_DEFAULT)
if not socks_port or not self._check_port(int(socks_port)):
Expand Down Expand Up @@ -159,6 +164,33 @@ def _popen_monkey_patch(*args, **kwargs):
kwargs['creationflags'] = 0x08000000 # CREATE_NO_WINDOW, for < Python 3.7
return TorController._orig_subprocess_popen(*args, **kwargs)

@staticmethod
def _get_tor_binary() -> Tuple[Optional[str], BinaryType]:
# Try to locate a bundled tor binary
if sys.platform in ('windows', 'win32'):
res = os.path.join(os.path.dirname(
__file__), '..', '..', 'tor.exe')
else:
res = os.path.join(os.path.dirname(__file__), 'bin', 'tor')
if os.path.isfile(res):
return (res, TorController.BinaryType.INTEGRATED)

# Tor is not packaged / built, try to locate a system tor
res = shutil.which('tor')
if res and os.path.isfile(res):
return (res, TorController.BinaryType.SYSTEM)

return (None, TorController.BinaryType.MISSING)

def detect_tor(self) -> bool:
path, bintype = self._get_tor_binary()
self.tor_binary = path
self.tor_binary_type = bintype
return self.is_available()

def is_available(self) -> bool:
return self.tor_binary_type != TorController.BinaryType.MISSING

def start(self):
if self._tor_process:
# Tor is already running
Expand All @@ -168,7 +200,7 @@ def start(self):
# Don't start Tor if not enabled
return

if not _TOR_BINARY_NAME:
if self.tor_binary_type == TorController.BinaryType.MISSING:
self.print_error("No Tor binary found")
self.status = TorController.Status.ERRORED
self.status_changed(self)
Expand All @@ -182,7 +214,7 @@ def start(self):
try:
subprocess.Popen = TorController._popen_monkey_patch
self._tor_process = stem.process.launch_tor_with_config(
tor_cmd=_TOR_BINARY_NAME,
tor_cmd=self.tor_binary,
completion_percent=0, # We will monitor the bootstrap status
init_msg_handler=self._tor_msg_handler,
take_ownership=True,
Expand Down Expand Up @@ -216,7 +248,7 @@ def start(self):
port=self.active_control_port)
self._tor_controller.authenticate()
self._tor_controller.add_event_listener(
self._handle_network_liveliness_event, stem.control.EventType.NETWORK_LIVENESS)
self._handle_network_liveliness_event, stem.control.EventType.NETWORK_LIVENESS) # pylint: disable=no-member
except:
self.print_exception("Failed to connect to Tor control port")
self.stop()
Expand All @@ -239,7 +271,7 @@ def stop(self):

if self._tor_controller:
# tell tor to shut down
self._tor_controller.signal(stem.Signal.HALT)
self._tor_controller.signal(stem.Signal.HALT) # pylint: disable=no-member
self._tor_controller.close()
self._tor_controller = None

Expand Down
14 changes: 7 additions & 7 deletions plugins/fusion/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,12 @@ def chk_tor_ok():
# prompt user to enable automatic Tor if not enabled and no auto-detected Tor ports were found
window = self.gui.windows[-1]
network = self.gui.daemon.network
if network and not network.tor_controller.is_enabled():
if network and network.tor_controller.is_available() and not network.tor_controller.is_enabled():
self._integrated_tor_asked[0] = True
icon_pm = icon_fusion_logo.pixmap(32)
answer = window.question(
_('CashFusion requires Tor to operate anonymously. Would'
' you like to enable the integrated Tor client now?'),
' you like to enable the Tor client now?'),
icon = icon_pm,
title = _("Tor Required"),
parent = None,
Expand All @@ -379,9 +379,9 @@ def on_status(controller):
if controller.status == controller.Status.STARTED:
buttons = [ _('Settings...'), _('Ok') ]
index = window.show_message(
_('The integrated Tor client has been successfully started.'),
_('The Tor client has been successfully started.'),
detail_text = (
_("The integrated Tor client can be stopped at any time from the Network Settings -> Proxy Tab"
_("The Tor client can be stopped at any time from the Network Settings -> Proxy Tab"
", however CashFusion does require Tor in order to operate correctly.")
),
icon = icon_pm,
Expand All @@ -396,11 +396,11 @@ def on_status(controller):
self.gui.show_network_dialog(window, jumpto='tor')
else:
controller.set_enabled(False) # latch it back to False so we may prompt them again in the future
window.show_error(_('There was an error starting the integrated Tor client'))
window.show_error(_('There was an error starting the Tor client'))
network.tor_controller.status_changed.append(on_status)
network.tor_controller.set_enabled(True)
self._integrated_tor_asked[1] = t = QTimer()
# if in 5 seconds no tor port, ask user if they want to enable the integrated Tor
# if in 5 seconds no tor port, ask user if they want to enable the Tor
t.timeout.connect(chk_tor_ok)
t.setSingleShot(True)
t.start(5000)
Expand Down Expand Up @@ -664,7 +664,7 @@ def onClick():
if plugin:
plugin.show_settings_dialog()
ShowPopupLabel(name = name,
text="<center><b>{}</b><br><small>{}</small></center>".format(_("Server Error"),_("Click here to resolve")),
text="<center><b>{}</b><br><small>{}</small></center>".format(_("Server Error"),_("Click this popup to resolve")),
target=self,
timeout=20000, onClick=onClick, onRightClick=onClick,
dark_mode = ColorScheme.dark_scheme)
Expand Down

0 comments on commit ef7d53a

Please sign in to comment.