diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index 1e31cc2c..bab3fd86 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -310,32 +310,15 @@ class Common: def get_tor_paths(self): if self.platform == "Linux": - # Look in resources first - base_path = self.get_resource_path("tor") - if os.path.exists(base_path): - self.log( - "Common", "get_tor_paths", f"using tor binaries in {base_path}" - ) - tor_path = os.path.join(base_path, "tor") - tor_geo_ip_file_path = os.path.join(base_path, "geoip") - tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6") - obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy") - snowflake_file_path = os.path.join(base_path, "snowflake-client") - meek_client_file_path = os.path.join(base_path, "meek-client") - else: - # Fallback to looking in the path - self.log( - "Common", "get_tor_paths", f"using tor binaries in system path" - ) - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - snowflake_file_path = shutil.which("snowflake-client") - meek_client_file_path = shutil.which("meek-client") - prefix = os.path.dirname(os.path.dirname(tor_path)) - tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") - tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-client") + meek_client_file_path = shutil.which("meek-client") + prefix = os.path.dirname(os.path.dirname(tor_path)) + tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") + tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") elif self.platform == "Windows": base_path = self.get_resource_path("tor") tor_path = os.path.join(base_path, "Tor", "tor.exe") @@ -345,26 +328,15 @@ class Common: tor_geo_ip_file_path = os.path.join(base_path, "Data", "Tor", "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6") elif self.platform == "Darwin": - # Look in resources first - base_path = self.get_resource_path("tor") - if os.path.exists(base_path): - tor_path = os.path.join(base_path, "tor") - tor_geo_ip_file_path = os.path.join(base_path, "geoip") - tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6") - obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy") - meek_client_file_path = os.path.join(base_path, "meek-client") - snowflake_file_path = os.path.join(base_path, "snowflake-client") - else: - # Fallback to looking in the path - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - snowflake_file_path = shutil.which("snowflake-client") - meek_client_file_path = shutil.which("meek-client") - prefix = os.path.dirname(os.path.dirname(tor_path)) - tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") - tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-client") + meek_client_file_path = shutil.which("meek-client") + prefix = os.path.dirname(os.path.dirname(tor_path)) + tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip") + tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6") elif self.platform == "BSD": tor_path = "/usr/local/bin/tor" tor_geo_ip_file_path = "/usr/local/share/tor/geoip" diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 6b31a584..c5df7b7f 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -85,6 +85,10 @@ class Meek(object): self.common.log("Meek", "start", "Starting meek client") if self.common.platform == "Windows": + env = os.environ.copy() + for key in self.meek_env: + env[key] = self.meek_env[key] + # In Windows, hide console window when opening meek-client.exe subprocess startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW @@ -100,7 +104,7 @@ class Meek(object): stderr=subprocess.PIPE, startupinfo=startupinfo, bufsize=1, - env=self.meek_env, + env=env, text=True, ) else: @@ -129,6 +133,7 @@ class Meek(object): # read stdout without blocking try: line = q.get_nowait() + self.common.log("Meek", "start", line.strip()) except Empty: # no stdout yet? pass @@ -136,10 +141,17 @@ class Meek(object): if "CMETHOD meek socks5" in line: self.meek_host = line.split(" ")[3].split(":")[0] self.meek_port = line.split(" ")[3].split(":")[1] - self.common.log("Meek", "start", f"Meek host is {self.meek_host}") - self.common.log("Meek", "start", f"Meek port is {self.meek_port}") + self.common.log( + "Meek", + "start", + f"Meek running on {self.meek_host}:{self.meek_port}", + ) break + if "CMETHOD-ERROR" in line: + self.cleanup() + raise MeekNotRunning() + if self.meek_port: self.meek_proxies = { "http": f"socks5h://{self.meek_host}:{self.meek_port}", @@ -147,6 +159,7 @@ class Meek(object): } else: self.common.log("Meek", "start", "Could not obtain the meek port") + self.cleanup() raise MeekNotRunning() def cleanup(self): diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 9c5b6faa..536b9ba0 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -200,8 +200,6 @@ class Onion(object): ) return - self.common.log("Onion", "connect") - # Either use settings that are passed in, or use them from common if custom_settings: self.settings = custom_settings @@ -212,6 +210,12 @@ class Onion(object): self.common.load_settings() self.settings = self.common.settings + self.common.log( + "Onion", + "connect", + f"connection_type={self.settings.get('connection_type')}", + ) + # The Tor controller self.c = None @@ -315,40 +319,39 @@ class Onion(object): f.write(torrc_template) # Bridge support - if self.settings.get("tor_bridges_use_obfs4"): - with open( - self.common.get_resource_path("torrc_template-obfs4") - ) as o: - for line in o: - f.write(line) - elif self.settings.get("tor_bridges_use_meek_lite_azure"): - with open( - self.common.get_resource_path("torrc_template-meek_lite_azure") - ) as o: - for line in o: - f.write(line) - elif self.settings.get("tor_bridges_use_snowflake"): - with open( - self.common.get_resource_path("torrc_template-snowflake") - ) as o: - for line in o: - f.write(line) + if self.settings.get("bridges_enabled"): + if self.settings.get("bridges_type") == "built-in": + if self.settings.get("bridges_builtin_pt") == "obfs4": + with open( + self.common.get_resource_path("torrc_template-obfs4") + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "meek-azure": + with open( + self.common.get_resource_path( + "torrc_template-meek_lite_azure" + ) + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "snowflake": + with open( + self.common.get_resource_path( + "torrc_template-snowflake" + ) + ) as o: + f.write(o.read()) - elif self.settings.get("tor_bridges_use_moat"): - for line in self.settings.get("tor_bridges_use_moat_bridges").split( - "\n" - ): - if line.strip() != "": - f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") + elif self.settings.get("bridges_type") == "moat": + for line in self.settings.get("bridges_moat").split("\n"): + if line.strip() != "": + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") - elif self.settings.get("tor_bridges_use_custom_bridges"): - for line in self.settings.get( - "tor_bridges_use_custom_bridges" - ).split("\n"): - if line.strip() != "": - f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") + elif self.settings.get("bridges_type") == "custom": + for line in self.settings.get("bridges_custom").split("\n"): + if line.strip() != "": + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") # Execute a tor subprocess start_ts = time.time() @@ -421,11 +424,7 @@ class Onion(object): time.sleep(0.2) # If using bridges, it might take a bit longer to connect to Tor - if ( - self.settings.get("tor_bridges_use_custom_bridges") - or self.settings.get("tor_bridges_use_obfs4") - or self.settings.get("tor_bridges_use_meek_lite_azure") - ): + if self.settings.get("bridges_enabled"): # Only override timeout if a custom timeout has not been passed in if connect_timeout == 120: connect_timeout = 150 diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index 29b59c80..c7d74a70 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -105,13 +105,11 @@ class Settings(object): "auth_password": "", "use_autoupdate": True, "autoupdate_timestamp": None, - "no_bridges": True, - "tor_bridges_use_obfs4": False, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_snowflake": False, - "tor_bridges_use_moat": False, - "tor_bridges_use_moat_bridges": "", - "tor_bridges_use_custom_bridges": "", + "bridges_enabled": False, + "bridges_type": "built-in", # "built-in", "moat", or "custom" + "bridges_builtin_pt": "obfs4", # "obfs4", "meek-azure", or "snowflake" + "bridges_moat": "", + "bridges_custom": "", "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() "theme": 0, diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index b44ddbec..9513b013 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -29,13 +29,11 @@ class TestSettings: "auth_password": "", "use_autoupdate": True, "autoupdate_timestamp": None, - "no_bridges": True, - "tor_bridges_use_obfs4": False, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_snowflake": False, - "tor_bridges_use_moat": False, - "tor_bridges_use_moat_bridges": "", - "tor_bridges_use_custom_bridges": "", + "bridges_enabled": False, + "bridges_type": "built-in", + "bridges_builtin_pt": "obfs4", + "bridges_moat": "", + "bridges_custom": "", "persistent_tabs": [], "theme": 0, } @@ -96,10 +94,11 @@ class TestSettings: assert settings_obj.get("use_autoupdate") is True assert settings_obj.get("autoupdate_timestamp") is None assert settings_obj.get("autoupdate_timestamp") is None - assert settings_obj.get("no_bridges") is True - assert settings_obj.get("tor_bridges_use_obfs4") is False - assert settings_obj.get("tor_bridges_use_meek_lite_azure") is False - assert settings_obj.get("tor_bridges_use_custom_bridges") == "" + assert settings_obj.get("bridges_enabled") is False + assert settings_obj.get("bridges_type") == "built-in" + assert settings_obj.get("bridges_builtin_pt") == "obfs4" + assert settings_obj.get("bridges_moat") == "" + assert settings_obj.get("bridges_custom") == "" def test_set_version(self, settings_obj): settings_obj.set("version", "CUSTOM_VERSION") @@ -142,10 +141,10 @@ class TestSettings: def test_set_custom_bridge(self, settings_obj): settings_obj.set( - "tor_bridges_use_custom_bridges", + "bridges_custom", "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E", ) assert ( - settings_obj._settings["tor_bridges_use_custom_bridges"] + settings_obj._settings["bridges_custom"] == "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E" ) diff --git a/desktop/README.md b/desktop/README.md index 408b6852..7f13ad70 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -65,7 +65,7 @@ python scripts\get-tor-windows.py ### Compile dependencies -Install Go. The simplest way to make sure everything works is to install Go by following [these instructions](https://golang.org/doc/install). +Install Go. The simplest way to make sure everything works is to install Go by following [these instructions](https://golang.org/doc/install). (In Windows, make sure to install the 32-bit version of Go, such as `go1.17.3.windows-386.msi`.) Download and compile `meek-client`: diff --git a/desktop/scripts/build-meek-client.py b/desktop/scripts/build-meek-client.py index d043754c..af58173a 100755 --- a/desktop/scripts/build-meek-client.py +++ b/desktop/scripts/build-meek-client.py @@ -28,6 +28,7 @@ import shutil import os import subprocess import inspect +import platform def main(): @@ -46,16 +47,21 @@ def main(): root_path = os.path.dirname( os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) ) - dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + if platform.system() == "Windows": + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor", "Tor") + bin_filename = "meek-client.exe" + else: + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + bin_filename = "meek-client" - bin_path = os.path.expanduser("~/go/bin/meek-client") + bin_path = os.path.join(os.path.expanduser("~"), "go", "bin", bin_filename) shutil.copyfile( os.path.join(bin_path), - os.path.join(dist_path, "meek-client"), + os.path.join(dist_path, bin_filename), ) - os.chmod(os.path.join(dist_path, "meek-client"), 0o755) + os.chmod(os.path.join(dist_path, bin_filename), 0o755) - print(f"Installed meek-client in {dist_path}") + print(f"Installed {bin_filename} in {dist_path}") if __name__ == "__main__": diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py index b8f83c92..51beb475 100755 --- a/desktop/scripts/get-tor-linux.py +++ b/desktop/scripts/get-tor-linux.py @@ -34,10 +34,10 @@ import requests def main(): - tarball_url = "https://dist.torproject.org/torbrowser/11.0a9/tor-browser-linux64-11.0a9_en-US.tar.xz" - tarball_filename = "tor-browser-linux64-11.0a9_en-US.tar.xz" + tarball_url = "https://dist.torproject.org/torbrowser/11.0a10/tor-browser-linux64-11.0a10_en-US.tar.xz" + tarball_filename = "tor-browser-linux64-11.0a10_en-US.tar.xz" expected_tarball_sha256 = ( - "cba4a2120b4f847d1ade637e41e69bd01b2e70b4a13e41fe8e69d0424fcf7ca7" + "5d3e2ebc4fb6a10f44624359bc2a5a151a57e8402cbd8563d15f9b2524374f1f" ) # Build paths diff --git a/desktop/scripts/get-tor-osx.py b/desktop/scripts/get-tor-osx.py index be5f7a56..80d7aee8 100755 --- a/desktop/scripts/get-tor-osx.py +++ b/desktop/scripts/get-tor-osx.py @@ -34,10 +34,10 @@ import requests def main(): - dmg_url = "https://dist.torproject.org/torbrowser/11.0a7/TorBrowser-11.0a7-osx64_en-US.dmg" - dmg_filename = "TorBrowser-11.0a7-osx64_en-US.dmg" + dmg_url = "https://dist.torproject.org/torbrowser/11.0a10/TorBrowser-11.0a10-osx64_en-US.dmg" + dmg_filename = "TorBrowser-11.0a10-osx64_en-US.dmg" expected_dmg_sha256 = ( - "46594cefa29493150d1c0e1933dd656aafcb6b51ef310d44ac059eed2fd1388e" + "c6823a28fd28205437564815f93011ff93b7972da2a8ce16919adfc65909e7b9" ) # Build paths @@ -101,6 +101,14 @@ def main(): os.path.join(dist_path, "obfs4proxy"), ) os.chmod(os.path.join(dist_path, "obfs4proxy"), 0o755) + # snowflake-client binary + shutil.copyfile( + os.path.join( + dmg_tor_path, "MacOS", "Tor", "PluggableTransports", "snowflake-client" + ), + os.path.join(dist_path, "snowflake-client"), + ) + os.chmod(os.path.join(dist_path, "snowflake-client"), 0o755) # Eject dmg subprocess.call(["diskutil", "eject", "/Volumes/Tor Browser"]) diff --git a/desktop/scripts/get-tor-windows.py b/desktop/scripts/get-tor-windows.py index 751faecc..8ca2e79f 100644 --- a/desktop/scripts/get-tor-windows.py +++ b/desktop/scripts/get-tor-windows.py @@ -33,10 +33,10 @@ import requests def main(): - exe_url = "https://dist.torproject.org/torbrowser/11.0a7/torbrowser-install-11.0a7_en-US.exe" - exe_filename = "torbrowser-install-11.0a7_en-US.exe" + exe_url = "https://dist.torproject.org/torbrowser/11.0a10/torbrowser-install-11.0a10_en-US.exe" + exe_filename = "torbrowser-install-11.0a10_en-US.exe" expected_exe_sha256 = ( - "8b2013669d88e3ae8fa9bc17a3495eaac9475f79a849354e826e5132811a860b" + "f567dd8368dea0a8d7bbf7c19ece7840f93d493e70662939b92f5058c8dc8d2d" ) # Build paths root_path = os.path.dirname( diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 019cf193..0db0f051 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -93,6 +93,7 @@ class GuiCommon: share_zip_progess_bar_chunk_color = "#4E064F" history_background_color = "#ffffff" history_label_color = "#000000" + settings_error_color = "#FF0000" if color_mode == "dark": header_color = "#F2F2F2" title_color = "#F2F2F2" @@ -103,6 +104,7 @@ class GuiCommon: share_zip_progess_bar_border_color = "#F2F2F2" history_background_color = "#191919" history_label_color = "#ffffff" + settings_error_color = "#FF9999" return { # OnionShareGui styles @@ -281,6 +283,11 @@ class GuiCommon: QLabel { color: #cc0000; }""", + "tor_not_connected_label": """ + QLabel { + font-size: 16px; + font-style: italic; + }""", # New tab "new_tab_button_image": """ QLabel { @@ -392,10 +399,12 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", - # Moat dialog - "moat_error": """ + # Tor Settings dialogs + "tor_settings_error": """ QLabel { - color: #990000; + color: """ + + settings_error_color + + """; } """, } diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index c125741c..546592a1 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -23,9 +23,7 @@ import time from PySide2 import QtCore, QtWidgets, QtGui from . import strings -from .tor_connection_dialog import TorConnectionDialog -from .tor_settings_dialog import TorSettingsDialog -from .settings_dialog import SettingsDialog +from .tor_connection import TorConnectionDialog from .widgets import Alert from .update_checker import UpdateThread from .tab_widget import TabWidget @@ -245,21 +243,17 @@ class MainWindow(QtWidgets.QMainWindow): def open_tor_settings(self): """ - Open the TorSettingsDialog. + Open the TorSettingsTab """ self.common.log("MainWindow", "open_tor_settings") - d = TorSettingsDialog(self.common) - d.settings_saved.connect(self.settings_have_changed) - d.exec_() + self.tabs.open_tor_settings_tab() def open_settings(self): """ - Open the SettingsDialog. + Open the SettingsTab """ self.common.log("MainWindow", "open_settings") - d = SettingsDialog(self.common) - d.settings_saved.connect(self.settings_have_changed) - d.exec_() + self.tabs.open_settings_tab() def settings_have_changed(self): self.common.log("OnionShareGui", "settings_have_changed") diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 2821bb1e..84a52390 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -26,7 +26,7 @@ import json from . import strings from .gui_common import GuiCommon -from onionshare_cli.meek import MeekNotFound +from onionshare_cli.meek import MeekNotFound, MeekNotRunning class MoatDialog(QtWidgets.QDialog): @@ -70,7 +70,7 @@ class MoatDialog(QtWidgets.QDialog): # Error label self.error_label = QtWidgets.QLabel() - self.error_label.setStyleSheet(self.common.gui.css["moat_error"]) + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) self.error_label.hide() # Buttons @@ -237,7 +237,13 @@ class MoatThread(QtCore.QThread): try: self.meek.start() except MeekNotFound: - self.common.log("MoatThread", "run", f"Could not find the Meek Client") + self.common.log("MoatThread", "run", f"Could not find meek-client") + self.bridgedb_error.emit() + return + except MeekNotRunning: + self.common.log( + "MoatThread", "run", f"Ran meek-client, but there was an error" + ) self.bridgedb_error.emit() return diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index a9fb562a..868a6fa9 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -71,7 +71,8 @@ "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_bridge_custom_placeholder": "type address:port (one per line)", "gui_settings_moat_bridges_invalid": "You have not requested a bridge from torproject.org yet.", - "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", + "gui_settings_tor_bridges_invalid": "None of the bridges you added work. Double-check them or add others.", + "gui_settings_stop_active_tabs_label": "There are services running in some of your tabs.\nYou must stop all services to change your Tor settings.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", @@ -228,5 +229,6 @@ "moat_captcha_reload": "Reload", "moat_bridgedb_error": "Error contacting BridgeDB.", "moat_captcha_error": "The solution is not correct. Please try again.", - "moat_solution_empty_error": "You must enter the characters from the image" + "moat_solution_empty_error": "You must enter the characters from the image", + "mode_tor_not_connected_label": "OnionShare is not connected to the Tor network" } \ No newline at end of file diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_tab.py similarity index 82% rename from desktop/src/onionshare/settings_dialog.py rename to desktop/src/onionshare/settings_tab.py index b1003386..cfa3261e 100644 --- a/desktop/src/onionshare/settings_dialog.py +++ b/desktop/src/onionshare/settings_tab.py @@ -19,57 +19,30 @@ along with this program. If not, see . """ from PySide2 import QtCore, QtWidgets, QtGui -from PySide2.QtCore import Slot, Qt -from PySide2.QtGui import QPalette, QColor -import sys import platform import datetime -import re -import os from onionshare_cli.settings import Settings -from onionshare_cli.onion import ( - Onion, - TorErrorInvalidSetting, - TorErrorAutomatic, - TorErrorSocketPort, - TorErrorSocketFile, - TorErrorMissingPassword, - TorErrorUnreadableCookieFile, - TorErrorAuthError, - TorErrorProtocolError, - BundledTorTimeout, - BundledTorBroken, - TorTooOldEphemeral, - TorTooOldStealth, - PortNotAvailable, -) from . import strings from .widgets import Alert from .update_checker import UpdateThread -from .tor_connection_dialog import TorConnectionDialog -from .gui_common import GuiCommon -class SettingsDialog(QtWidgets.QDialog): +class SettingsTab(QtWidgets.QWidget): """ Settings dialog. """ - settings_saved = QtCore.Signal() + close_this_tab = QtCore.Signal() - def __init__(self, common): - super(SettingsDialog, self).__init__() + def __init__(self, common, tab_id): + super(SettingsTab, self).__init__() self.common = common - - self.common.log("SettingsDialog", "__init__") - - self.setModal(True) - self.setWindowTitle(strings._("gui_settings_window_title")) - self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + self.common.log("SettingsTab", "__init__") self.system = platform.system() + self.tab_id = tab_id # Automatic updates options @@ -100,9 +73,16 @@ class SettingsDialog(QtWidgets.QDialog): ) autoupdate_group.setLayout(autoupdate_group_layout) + autoupdate_layout = QtWidgets.QHBoxLayout() + autoupdate_layout.addStretch() + autoupdate_layout.addWidget(autoupdate_group) + autoupdate_layout.addStretch() + autoupdate_widget = QtWidgets.QWidget() + autoupdate_widget.setLayout(autoupdate_layout) + # Autoupdate is only available for Windows and Mac (Linux updates using package manager) if self.system != "Windows" and self.system != "Darwin": - autoupdate_group.hide() + autoupdate_widget.hide() # Language settings language_label = QtWidgets.QLabel(strings._("gui_settings_language_label")) @@ -117,6 +97,7 @@ class SettingsDialog(QtWidgets.QDialog): locale = language_names_to_locales[language_name] self.language_combobox.addItem(language_name, locale) language_layout = QtWidgets.QHBoxLayout() + language_layout.addStretch() language_layout.addWidget(language_label) language_layout.addWidget(self.language_combobox) language_layout.addStretch() @@ -131,6 +112,7 @@ class SettingsDialog(QtWidgets.QDialog): ] self.theme_combobox.addItems(theme_choices) theme_layout = QtWidgets.QHBoxLayout() + theme_layout.addStretch() theme_layout.addWidget(theme_label) theme_layout.addWidget(self.theme_combobox) theme_layout.addStretch() @@ -139,41 +121,44 @@ class SettingsDialog(QtWidgets.QDialog): version_label = QtWidgets.QLabel( strings._("gui_settings_version_label").format(self.common.version) ) + version_label.setAlignment(QtCore.Qt.AlignHCenter) help_label = QtWidgets.QLabel(strings._("gui_settings_help_label")) + help_label.setAlignment(QtCore.Qt.AlignHCenter) help_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) help_label.setOpenExternalLinks(True) # Buttons self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) - self.cancel_button = QtWidgets.QPushButton( - strings._("gui_settings_button_cancel") - ) - self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) - buttons_layout.addWidget(self.cancel_button) + buttons_layout.addStretch() # Layout layout = QtWidgets.QVBoxLayout() - layout.addWidget(autoupdate_group) - if autoupdate_group.isVisible(): + layout.addStretch() + layout.addWidget(autoupdate_widget) + if autoupdate_widget.isVisible(): layout.addSpacing(20) layout.addLayout(language_layout) layout.addLayout(theme_layout) layout.addSpacing(20) - layout.addStretch() layout.addWidget(version_label) layout.addWidget(help_label) layout.addSpacing(20) layout.addLayout(buttons_layout) + layout.addStretch() self.setLayout(layout) - self.cancel_button.setFocus() self.reload_settings() + if self.common.gui.onion.connected_to_tor: + self.tor_is_connected() + else: + self.tor_is_disconnected() + def reload_settings(self): # Load settings, and fill them in self.old_settings = Settings(self.common) @@ -199,7 +184,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Check for Updates button clicked. Manually force an update check. """ - self.common.log("SettingsDialog", "check_for_updates") + self.common.log("SettingsTab", "check_for_updates") # Disable buttons self._disable_buttons() self.common.gui.qtapp.processEvents() @@ -261,7 +246,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Save button clicked. Save current settings to disk. """ - self.common.log("SettingsDialog", "save_clicked") + self.common.log("SettingsTab", "save_clicked") def changed(s1, s2, keys): """ @@ -298,33 +283,14 @@ class SettingsDialog(QtWidgets.QDialog): # Save the new settings settings.save() - self.settings_saved.emit() - self.close() - - def cancel_clicked(self): - """ - Cancel button clicked. - """ - self.common.log("SettingsDialog", "cancel_clicked") - if ( - not self.common.gui.local_only - and not self.common.gui.onion.is_authenticated() - ): - Alert( - self.common, - strings._("gui_tor_connection_canceled"), - QtWidgets.QMessageBox.Warning, - ) - sys.exit() - else: - self.close() + self.close_this_tab.emit() def help_clicked(self): """ Help button clicked. """ - self.common.log("SettingsDialog", "help_clicked") - SettingsDialog.open_help() + self.common.log("SettingsTab", "help_clicked") + SettingsTab.open_help() @staticmethod def open_help(): @@ -335,7 +301,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Return a Settings object that's full of values from the settings dialog. """ - self.common.log("SettingsDialog", "settings_from_fields") + self.common.log("SettingsTab", "settings_from_fields") settings = Settings(self.common) settings.load() # To get the last update timestamp @@ -350,8 +316,12 @@ class SettingsDialog(QtWidgets.QDialog): return settings + def settings_have_changed(self): + # Global settings have changed + self.common.log("SettingsTab", "settings_have_changed") + def _update_autoupdate_timestamp(self, autoupdate_timestamp): - self.common.log("SettingsDialog", "_update_autoupdate_timestamp") + self.common.log("SettingsTab", "_update_autoupdate_timestamp") if autoupdate_timestamp: dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) @@ -363,18 +333,22 @@ class SettingsDialog(QtWidgets.QDialog): ) def _disable_buttons(self): - self.common.log("SettingsDialog", "_disable_buttons") + self.common.log("SettingsTab", "_disable_buttons") self.check_for_updates_button.setEnabled(False) self.save_button.setEnabled(False) - self.cancel_button.setEnabled(False) def _enable_buttons(self): - self.common.log("SettingsDialog", "_enable_buttons") + self.common.log("SettingsTab", "_enable_buttons") # We can't check for updates if we're still not connected to Tor if not self.common.gui.onion.connected_to_tor: self.check_for_updates_button.setEnabled(False) else: self.check_for_updates_button.setEnabled(True) self.save_button.setEnabled(True) - self.cancel_button.setEnabled(True) + + def tor_is_connected(self): + self.check_for_updates_button.show() + + def tor_is_disconnected(self): + self.check_for_updates_button.hide() diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py index d4f2c23a..c9b5cad1 100644 --- a/desktop/src/onionshare/tab/mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/__init__.py @@ -28,7 +28,7 @@ from .mode_settings_widget import ModeSettingsWidget from ..server_status import ServerStatus from ... import strings from ...threads import OnionThread, AutoStartTimer -from ...widgets import Alert +from ...widgets import Alert, MinimumSizeWidget class Mode(QtWidgets.QWidget): @@ -101,6 +101,38 @@ class Mode(QtWidgets.QWidget): self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) + # It's up to the downstream Mode to add stuff to self.content_layout + # self.content_layout shows the actual content of the mode + # self.tor_not_connected_layout is displayed when Tor isn't connected + self.content_layout = QtWidgets.QVBoxLayout() + self.content_widget = QtWidgets.QWidget() + self.content_widget.setLayout(self.content_layout) + + tor_not_connected_label = QtWidgets.QLabel( + strings._("mode_tor_not_connected_label") + ) + tor_not_connected_label.setAlignment(QtCore.Qt.AlignHCenter) + tor_not_connected_label.setStyleSheet( + self.common.gui.css["tor_not_connected_label"] + ) + self.tor_not_connected_layout = QtWidgets.QVBoxLayout() + self.tor_not_connected_layout.addStretch() + self.tor_not_connected_layout.addWidget(tor_not_connected_label) + self.tor_not_connected_layout.addWidget(MinimumSizeWidget(700, 0)) + self.tor_not_connected_layout.addStretch() + self.tor_not_connected_widget = QtWidgets.QWidget() + self.tor_not_connected_widget.setLayout(self.tor_not_connected_layout) + + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addWidget(self.content_widget) + self.wrapper_layout.addWidget(self.tor_not_connected_widget) + self.setLayout(self.wrapper_layout) + + if self.common.gui.onion.connected_to_tor: + self.tor_connection_started() + else: + self.tor_connection_stopped() + def init(self): """ Add custom initialization here. @@ -524,3 +556,21 @@ class Mode(QtWidgets.QWidget): Used in both Share and Website modes, so implemented here. """ self.history.cancel(event["data"]["id"]) + + def tor_connection_started(self): + """ + This is called on every Mode when Tor is connected + """ + self.content_widget.show() + self.tor_not_connected_widget.hide() + + def tor_connection_stopped(self): + """ + This is called on every Mode when Tor is disconnected + """ + if self.common.gui.local_only: + self.tor_connection_started() + return + + self.content_widget.hide() + self.tor_not_connected_widget.show() diff --git a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py index e7a17ce7..1081fe9d 100644 --- a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py @@ -98,10 +98,8 @@ class ChatMode(Mode): self.column_layout.addWidget(self.image) self.column_layout.addLayout(self.main_layout) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) def get_type(self): """ diff --git a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py index d5036d1d..b2b2fc5a 100644 --- a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py @@ -198,10 +198,8 @@ class ReceiveMode(Mode): self.column_layout.addLayout(row_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) def get_type(self): """ diff --git a/desktop/src/onionshare/tab/mode/share_mode/__init__.py b/desktop/src/onionshare/tab/mode/share_mode/__init__.py index 5d3e3c35..7be93f1d 100644 --- a/desktop/src/onionshare/tab/mode/share_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/share_mode/__init__.py @@ -169,10 +169,8 @@ class ShareMode(Mode): self.column_layout.addLayout(self.main_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/desktop/src/onionshare/tab/mode/website_mode/__init__.py b/desktop/src/onionshare/tab/mode/website_mode/__init__.py index a50d15b9..73c4bad2 100644 --- a/desktop/src/onionshare/tab/mode/website_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py @@ -167,10 +167,8 @@ class WebsiteMode(Mode): self.column_layout.addLayout(self.main_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/desktop/src/onionshare/tab/tab.py b/desktop/src/onionshare/tab/tab.py index 5d9bb077..fb7f1836 100644 --- a/desktop/src/onionshare/tab/tab.py +++ b/desktop/src/onionshare/tab/tab.py @@ -96,7 +96,6 @@ class Tab(QtWidgets.QWidget): tab_id, system_tray, status_bar, - mode_settings=None, filenames=None, ): super(Tab, self).__init__() diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index a955ea53..7162fcc4 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -26,6 +26,8 @@ from . import strings from .tab import Tab from .threads import EventHandlerThread from .gui_common import GuiCommon +from .tor_settings_tab import TorSettingsTab +from .settings_tab import SettingsTab class TabWidget(QtWidgets.QTabWidget): @@ -43,9 +45,12 @@ class TabWidget(QtWidgets.QTabWidget): self.system_tray = system_tray self.status_bar = status_bar - # Keep track of tabs in a dictionary + # Keep track of tabs in a dictionary that maps tab_id to tab. + # Each tab has a unique, auto-incremented id (tab_id). This is different than the + # tab's index, which changes as tabs are re-arranged. self.tabs = {} self.current_tab_id = 0 # Each tab has a unique id + self.tor_settings_tab = None # Define the new tab button self.new_tab_button = QtWidgets.QPushButton("+", parent=self) @@ -89,9 +94,12 @@ class TabWidget(QtWidgets.QTabWidget): self.event_handler_t.wait(50) # Clean up each tab - for index in range(self.count()): - tab = self.widget(index) - tab.cleanup() + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + self.tabs[tab_id].cleanup() def move_new_tab_button(self): # Find the width of all tabs @@ -114,8 +122,28 @@ class TabWidget(QtWidgets.QTabWidget): def tab_changed(self): # Active tab was changed - tab_id = self.currentIndex() + tab = self.widget(self.currentIndex()) + if not tab: + self.common.log( + "TabWidget", + "tab_changed", + f"tab at index {self.currentIndex()} does not exist", + ) + return + + tab_id = tab.tab_id self.common.log("TabWidget", "tab_changed", f"Tab was changed to {tab_id}") + + # If it's Settings or Tor Settings, ignore + if ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + # Blank the server status indicator + self.status_bar.server_status_image_label.clear() + self.status_bar.server_status_label.clear() + return + try: mode = self.tabs[tab_id].get_mode() if mode: @@ -158,23 +186,6 @@ class TabWidget(QtWidgets.QTabWidget): index = self.addTab(tab, strings._("gui_new_tab")) self.setCurrentIndex(index) - # In macOS, manually create a close button because tabs don't seem to have them otherwise - if self.common.platform == "Darwin": - - def close_tab(): - self.tabBar().tabCloseRequested.emit(self.indexOf(tab)) - - tab.close_button = QtWidgets.QPushButton() - tab.close_button.setFlat(True) - tab.close_button.setFixedWidth(40) - tab.close_button.setIcon( - QtGui.QIcon(GuiCommon.get_resource_path("images/close_tab.png")) - ) - tab.close_button.clicked.connect(close_tab) - self.tabBar().setTabButton( - index, QtWidgets.QTabBar.RightSide, tab.close_button - ) - tab.init(mode_settings) # Make sure the title is set @@ -187,6 +198,44 @@ class TabWidget(QtWidgets.QTabWidget): # Bring the window to front, in case this is being added by an event self.bring_to_front.emit() + def open_settings_tab(self): + self.common.log("TabWidget", "open_settings_tab") + + # See if a settings tab is already open, and if so switch to it + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) + return + + settings_tab = SettingsTab(self.common, self.current_tab_id) + settings_tab.close_this_tab.connect(self.close_settings_tab) + self.tabs[self.current_tab_id] = settings_tab + self.current_tab_id += 1 + index = self.addTab(settings_tab, strings._("gui_settings_window_title")) + self.setCurrentIndex(index) + + def open_tor_settings_tab(self): + self.common.log("TabWidget", "open_tor_settings_tab") + + # See if a settings tab is already open, and if so switch to it + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is TorSettingsTab: + self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) + return + + self.tor_settings_tab = TorSettingsTab( + self.common, self.current_tab_id, self.are_tabs_active(), self.status_bar + ) + self.tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) + self.tor_settings_tab.tor_is_connected.connect(self.tor_is_connected) + self.tor_settings_tab.tor_is_disconnected.connect(self.tor_is_disconnected) + self.tabs[self.current_tab_id] = self.tor_settings_tab + self.current_tab_id += 1 + index = self.addTab( + self.tor_settings_tab, strings._("gui_tor_settings_window_title") + ) + self.setCurrentIndex(index) + def change_title(self, tab_id, title): shortened_title = title if len(shortened_title) > 11: @@ -200,6 +249,11 @@ class TabWidget(QtWidgets.QTabWidget): index = self.indexOf(self.tabs[tab_id]) self.setTabIcon(index, QtGui.QIcon(GuiCommon.get_resource_path(icon_path))) + # The icon changes when the server status changes, so if we have an open + # Tor Settings tab, tell it to update + if self.tor_settings_tab: + self.tor_settings_tab.active_tabs_changed(self.are_tabs_active()) + def change_persistent(self, tab_id, is_persistent): self.common.log( "TabWidget", @@ -223,10 +277,14 @@ class TabWidget(QtWidgets.QTabWidget): def save_persistent_tabs(self): # Figure out the order of persistent tabs to save in settings persistent_tabs = [] - for index in range(self.count()): - tab = self.widget(index) - if tab.settings.get("persistent", "enabled"): - persistent_tabs.append(tab.settings.id) + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + tab = self.widget(self.indexOf(self.tabs[tab_id])) + if tab.settings.get("persistent", "enabled"): + persistent_tabs.append(tab.settings.id) # Only save if tabs have actually moved if persistent_tabs != self.common.settings.get("persistent_tabs"): self.common.settings.set("persistent_tabs", persistent_tabs) @@ -235,10 +293,16 @@ class TabWidget(QtWidgets.QTabWidget): def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) - if tab.close_tab(): - # If the tab is persistent, delete the settings file from disk - if tab.settings.get("persistent", "enabled"): - tab.settings.delete() + tab_id = tab.tab_id + + if ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + self.common.log("TabWidget", "closing a settings tab") + + if type(self.tabs[tab_id]) is TorSettingsTab: + self.tor_settings_tab = None # Remove the tab self.removeTab(index) @@ -248,17 +312,56 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() - self.save_persistent_tabs() + else: + self.common.log("TabWidget", "closing a service tab") + if tab.close_tab(): + self.common.log("TabWidget", "user is okay with closing the tab") + + # If the tab is persistent, delete the settings file from disk + if tab.settings.get("persistent", "enabled"): + tab.settings.delete() + + self.save_persistent_tabs() + + # Remove the tab + self.removeTab(index) + del self.tabs[tab.tab_id] + + # If the last tab is closed, open a new one + if self.count() == 0: + self.new_tab_clicked() + else: + self.common.log("TabWidget", "user does not want to close the tab") + + def close_settings_tab(self): + self.common.log("TabWidget", "close_settings_tab") + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + index = self.indexOf(self.tabs[tab_id]) + self.close_tab(index) + return + + def close_tor_settings_tab(self): + self.common.log("TabWidget", "close_tor_settings_tab") + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is TorSettingsTab: + index = self.indexOf(self.tabs[tab_id]) + self.close_tab(index) + return def are_tabs_active(self): """ See if there are active servers in any open tabs """ for tab_id in self.tabs: - mode = self.tabs[tab_id].get_mode() - if mode: - if mode.server_status.status != mode.server_status.STATUS_STOPPED: - return True + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + mode = self.tabs[tab_id].get_mode() + if mode: + if mode.server_status.status != mode.server_status.STATUS_STOPPED: + return True return False def paintEvent(self, event): @@ -273,6 +376,26 @@ class TabWidget(QtWidgets.QTabWidget): super(TabWidget, self).resizeEvent(event) self.move_new_tab_button() + def tor_is_connected(self): + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + self.tabs[tab_id].tor_is_connected() + else: + if not type(self.tabs[tab_id]) is TorSettingsTab: + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_started() + + def tor_is_disconnected(self): + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + self.tabs[tab_id].tor_is_disconnected() + else: + if not type(self.tabs[tab_id]) is TorSettingsTab: + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_stopped() + class TabBar(QtWidgets.QTabBar): """ diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection.py similarity index 63% rename from desktop/src/onionshare/tor_connection_dialog.py rename to desktop/src/onionshare/tor_connection.py index daf49a32..2cc599c4 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection.py @@ -117,7 +117,6 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def _connected_to_tor(self): self.common.log("TorConnectionDialog", "_connected_to_tor") self.active = False - # Close the dialog after connecting self.setValue(self.maximum()) @@ -157,26 +156,136 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): QtCore.QTimer.singleShot(1, self.cancel) +class TorConnectionWidget(QtWidgets.QWidget): + """ + Connecting to Tor widget, with a progress bar + """ + + open_tor_settings = QtCore.Signal() + success = QtCore.Signal() + fail = QtCore.Signal(str) + + def __init__(self, common, status_bar): + super(TorConnectionWidget, self).__init__(None) + self.common = common + self.common.log("TorConnectionWidget", "__init__") + + self.status_bar = status_bar + self.label = QtWidgets.QLabel(strings._("connecting_to_tor")) + self.label.setAlignment(QtCore.Qt.AlignHCenter) + + self.progress = QtWidgets.QProgressBar() + self.progress.setRange(0, 100) + self.cancel_button = QtWidgets.QPushButton( + strings._("gui_settings_button_cancel") + ) + self.cancel_button.clicked.connect(self.cancel_clicked) + + progress_layout = QtWidgets.QHBoxLayout() + progress_layout.addWidget(self.progress) + progress_layout.addWidget(self.cancel_button) + + inner_layout = QtWidgets.QVBoxLayout() + inner_layout.addWidget(self.label) + inner_layout.addLayout(progress_layout) + + layout = QtWidgets.QHBoxLayout() + layout.addStretch() + layout.addLayout(inner_layout) + layout.addStretch() + self.setLayout(layout) + + # Start displaying the status at 0 + self._tor_status_update(0, "") + + def start(self, custom_settings=False, testing_settings=False, onion=None): + self.common.log("TorConnectionWidget", "start") + self.was_canceled = False + + self.testing_settings = testing_settings + + if custom_settings: + self.settings = custom_settings + else: + self.settings = self.common.settings + + if self.testing_settings: + self.onion = onion + else: + self.onion = self.common.gui.onion + + t = TorConnectionThread(self.common, self.settings, self) + t.tor_status_update.connect(self._tor_status_update) + t.connected_to_tor.connect(self._connected_to_tor) + t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor) + t.error_connecting_to_tor.connect(self._error_connecting_to_tor) + t.start() + + # The main thread needs to remain active, and checking for Qt events, + # until the thread is finished. Otherwise it won't be able to handle + # accepting signals. + self.active = True + while self.active: + time.sleep(0.1) + self.common.gui.qtapp.processEvents() + + def cancel_clicked(self): + self.was_canceled = True + self.fail.emit("") + + def wasCanceled(self): + return self.was_canceled + + def _tor_status_update(self, progress, summary): + self.progress.setValue(int(progress)) + self.label.setText( + f"{strings._('connecting_to_tor')}
{summary}" + ) + + def _connected_to_tor(self): + self.common.log("TorConnectionWidget", "_connected_to_tor") + self.active = False + self.status_bar.clearMessage() + + # Close the dialog after connecting + self.progress.setValue(self.progress.maximum()) + + self.success.emit() + + def _canceled_connecting_to_tor(self): + self.common.log("TorConnectionWidget", "_canceled_connecting_to_tor") + self.active = False + self.onion.cleanup() + + # Cancel connecting to Tor + QtCore.QTimer.singleShot(1, self.cancel_clicked) + + def _error_connecting_to_tor(self, msg): + self.common.log("TorConnectionWidget", "_error_connecting_to_tor") + self.active = False + self.fail.emit(msg) + + class TorConnectionThread(QtCore.QThread): tor_status_update = QtCore.Signal(str, str) connected_to_tor = QtCore.Signal() canceled_connecting_to_tor = QtCore.Signal() error_connecting_to_tor = QtCore.Signal(str) - def __init__(self, common, settings, dialog): + def __init__(self, common, settings, parent): super(TorConnectionThread, self).__init__() self.common = common self.common.log("TorConnectionThread", "__init__") self.settings = settings - self.dialog = dialog + self.parent = parent def run(self): self.common.log("TorConnectionThread", "run") # Connect to the Onion try: - self.dialog.onion.connect(self.settings, False, self._tor_status_update) - if self.dialog.onion.connected_to_tor: + self.parent.onion.connect(self.settings, False, self._tor_status_update) + if self.parent.onion.connected_to_tor: self.connected_to_tor.emit() else: self.canceled_connecting_to_tor.emit() @@ -212,4 +321,4 @@ class TorConnectionThread(QtCore.QThread): self.tor_status_update.emit(progress, summary) # Return False if the dialog was canceled - return not self.dialog.wasCanceled() + return not self.parent.wasCanceled() diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_tab.py similarity index 76% rename from desktop/src/onionshare/tor_settings_dialog.py rename to desktop/src/onionshare/tor_settings_tab.py index 6737ae4b..e28e5260 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -30,32 +30,30 @@ from onionshare_cli.onion import Onion from . import strings from .widgets import Alert -from .tor_connection_dialog import TorConnectionDialog +from .tor_connection import TorConnectionWidget from .moat_dialog import MoatDialog -from .gui_common import GuiCommon -class TorSettingsDialog(QtWidgets.QDialog): +class TorSettingsTab(QtWidgets.QWidget): """ Settings dialog. """ - settings_saved = QtCore.Signal() + close_this_tab = QtCore.Signal() + tor_is_connected = QtCore.Signal() + tor_is_disconnected = QtCore.Signal() - def __init__(self, common): - super(TorSettingsDialog, self).__init__() + def __init__(self, common, tab_id, are_tabs_active, status_bar): + super(TorSettingsTab, self).__init__() self.common = common + self.common.log("TorSettingsTab", "__init__") - self.common.log("TorSettingsDialog", "__init__") - + self.status_bar = status_bar self.meek = Meek(common, get_tor_paths=self.common.gui.get_tor_paths) - self.setModal(True) - self.setWindowTitle(strings._("gui_tor_settings_window_title")) - self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) - self.system = platform.system() + self.tab_id = tab_id # Connection type: either automatic, control port, or socket file @@ -299,6 +297,7 @@ class TorSettingsDialog(QtWidgets.QDialog): connection_type_radio_group_layout.addWidget( self.connection_type_socket_file_radio ) + connection_type_radio_group_layout.addStretch() connection_type_radio_group = QtWidgets.QGroupBox( strings._("gui_settings_connection_type_label") ) @@ -319,6 +318,28 @@ class TorSettingsDialog(QtWidgets.QDialog): connection_type_layout = QtWidgets.QVBoxLayout() connection_type_layout.addWidget(self.tor_settings_group) connection_type_layout.addWidget(self.connection_type_bridges_radio_group) + connection_type_layout.addStretch() + + # Settings are in columns + columns_layout = QtWidgets.QHBoxLayout() + columns_layout.addWidget(connection_type_radio_group) + columns_layout.addSpacing(20) + columns_layout.addLayout(connection_type_layout, stretch=1) + columns_wrapper = QtWidgets.QWidget() + columns_wrapper.setFixedHeight(400) + columns_wrapper.setLayout(columns_layout) + + # Tor connection widget + self.tor_con = TorConnectionWidget(self.common, self.status_bar) + self.tor_con.success.connect(self.tor_con_success) + self.tor_con.fail.connect(self.tor_con_fail) + self.tor_con.hide() + self.tor_con_type = None + + # Error label + self.error_label = QtWidgets.QLabel() + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) + self.error_label.setWordWrap(True) # Buttons self.test_tor_button = QtWidgets.QPushButton( @@ -327,26 +348,42 @@ class TorSettingsDialog(QtWidgets.QDialog): self.test_tor_button.clicked.connect(self.test_tor_clicked) self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) - self.cancel_button = QtWidgets.QPushButton( - strings._("gui_settings_button_cancel") - ) - self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addWidget(self.error_label, stretch=1) + buttons_layout.addSpacing(20) buttons_layout.addWidget(self.test_tor_button) - buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) - buttons_layout.addWidget(self.cancel_button) - # Layout + # Main layout + main_layout = QtWidgets.QVBoxLayout() + main_layout.addWidget(columns_wrapper) + main_layout.addStretch() + main_layout.addWidget(self.tor_con) + main_layout.addStretch() + main_layout.addLayout(buttons_layout) + self.main_widget = QtWidgets.QWidget() + self.main_widget.setLayout(main_layout) + + # Tabs are active label + active_tabs_label = QtWidgets.QLabel( + strings._("gui_settings_stop_active_tabs_label") + ) + active_tabs_label.setAlignment(QtCore.Qt.AlignHCenter) + + # Active tabs layout + active_tabs_layout = QtWidgets.QVBoxLayout() + active_tabs_layout.addStretch() + active_tabs_layout.addWidget(active_tabs_label) + active_tabs_layout.addStretch() + self.active_tabs_widget = QtWidgets.QWidget() + self.active_tabs_widget.setLayout(active_tabs_layout) + layout = QtWidgets.QVBoxLayout() - layout.addWidget(connection_type_radio_group) - layout.addLayout(connection_type_layout) - layout.addStretch() - layout.addLayout(buttons_layout) - + layout.addWidget(self.main_widget) + layout.addWidget(self.active_tabs_widget) self.setLayout(layout) - self.cancel_button.setFocus() + self.active_tabs_changed(are_tabs_active) self.reload_settings() def reload_settings(self): @@ -391,63 +428,68 @@ class TorSettingsDialog(QtWidgets.QDialog): self.old_settings.get("auth_password") ) - if self.old_settings.get("no_bridges"): - self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.bridge_settings.hide() - - else: + if self.old_settings.get("bridges_enabled"): self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) self.bridge_settings.show() - builtin_obfs4 = self.old_settings.get("tor_bridges_use_obfs4") - builtin_meek_azure = self.old_settings.get( - "tor_bridges_use_meek_lite_azure" - ) - builtin_snowflake = self.old_settings.get("tor_bridges_use_snowflake") - - if builtin_obfs4 or builtin_meek_azure or builtin_snowflake: + bridges_type = self.old_settings.get("bridges_type") + if bridges_type == "built-in": self.bridge_builtin_radio.setChecked(True) self.bridge_builtin_dropdown.show() - if builtin_obfs4: + self.bridge_moat_radio.setChecked(False) + self.bridge_moat_textbox_options.hide() + self.bridge_custom_radio.setChecked(False) + self.bridge_custom_textbox_options.hide() + + bridges_builtin_pt = self.old_settings.get("bridges_builtin_pt") + if bridges_builtin_pt == "obfs4": self.bridge_builtin_dropdown.setCurrentText("obfs4") - elif builtin_meek_azure: + elif bridges_builtin_pt == "meek-azure": self.bridge_builtin_dropdown.setCurrentText("meek-azure") - elif builtin_snowflake: + else: self.bridge_builtin_dropdown.setCurrentText("snowflake") self.bridge_moat_textbox_options.hide() self.bridge_custom_textbox_options.hide() + + elif bridges_type == "moat": + self.bridge_builtin_radio.setChecked(False) + self.bridge_builtin_dropdown.hide() + self.bridge_moat_radio.setChecked(True) + self.bridge_moat_textbox_options.show() + self.bridge_custom_radio.setChecked(False) + self.bridge_custom_textbox_options.hide() + else: self.bridge_builtin_radio.setChecked(False) self.bridge_builtin_dropdown.hide() + self.bridge_moat_radio.setChecked(False) + self.bridge_moat_textbox_options.hide() + self.bridge_custom_radio.setChecked(True) + self.bridge_custom_textbox_options.show() - use_moat = self.old_settings.get("tor_bridges_use_moat") - self.bridge_moat_radio.setChecked(use_moat) - if use_moat: - self.bridge_builtin_dropdown.hide() - self.bridge_custom_textbox_options.hide() + bridges_moat = self.old_settings.get("bridges_moat") + self.bridge_moat_textbox.document().setPlainText(bridges_moat) + bridges_custom = self.old_settings.get("bridges_custom") + self.bridge_custom_textbox.document().setPlainText(bridges_custom) - moat_bridges = self.old_settings.get("tor_bridges_use_moat_bridges") - self.bridge_moat_textbox.document().setPlainText(moat_bridges) - if len(moat_bridges.strip()) > 0: - self.bridge_moat_textbox_options.show() - else: - self.bridge_moat_textbox_options.hide() + else: + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.bridge_settings.hide() - custom_bridges = self.old_settings.get("tor_bridges_use_custom_bridges") - if len(custom_bridges.strip()) != 0: - self.bridge_custom_radio.setChecked(True) - self.bridge_custom_textbox.setPlainText(custom_bridges) - - self.bridge_builtin_dropdown.hide() - self.bridge_moat_textbox_options.hide() - self.bridge_custom_textbox_options.show() + def active_tabs_changed(self, are_tabs_active): + if are_tabs_active: + self.main_widget.hide() + self.active_tabs_widget.show() + else: + self.main_widget.show() + self.active_tabs_widget.hide() def connection_type_bundled_toggled(self, checked): """ Connection type bundled was toggled """ - self.common.log("TorSettingsDialog", "connection_type_bundled_toggled") + self.common.log("TorSettingsTab", "connection_type_bundled_toggled") if checked: self.tor_settings_group.hide() self.connection_type_socks.hide() @@ -479,7 +521,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ if selection == "meek-azure": # Alert the user about meek's costliness if it looks like they're turning it on - if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): + if not self.old_settings.get("bridges_builtin_pt") == "meek-azure": Alert( self.common, strings._("gui_settings_meek_lite_expensive_warning"), @@ -499,7 +541,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Request new bridge button clicked """ - self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") + self.common.log("TorSettingsTab", "bridge_moat_button_clicked") moat_dialog = MoatDialog(self.common, self.meek) moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges) @@ -509,7 +551,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Got new bridges from moat """ - self.common.log("TorSettingsDialog", "bridge_moat_got_bridges") + self.common.log("TorSettingsTab", "bridge_moat_got_bridges") self.bridge_moat_textbox.document().setPlainText(bridges) self.bridge_moat_textbox.show() @@ -526,7 +568,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Connection type automatic was toggled. If checked, hide authentication fields. """ - self.common.log("TorSettingsDialog", "connection_type_automatic_toggled") + self.common.log("TorSettingsTab", "connection_type_automatic_toggled") if checked: self.tor_settings_group.hide() self.connection_type_socks.hide() @@ -537,7 +579,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type control port was toggled. If checked, show extra fields for Tor control address and port. If unchecked, hide those extra fields. """ - self.common.log("TorSettingsDialog", "connection_type_control_port_toggled") + self.common.log("TorSettingsTab", "connection_type_control_port_toggled") if checked: self.tor_settings_group.show() self.connection_type_control_port_extras.show() @@ -551,7 +593,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type socket file was toggled. If checked, show extra fields for socket file. If unchecked, hide those extra fields. """ - self.common.log("TorSettingsDialog", "connection_type_socket_file_toggled") + self.common.log("TorSettingsTab", "connection_type_socket_file_toggled") if checked: self.tor_settings_group.show() self.connection_type_socket_file_extras.show() @@ -564,7 +606,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Authentication option no authentication was toggled. """ - self.common.log("TorSettingsDialog", "authenticate_no_auth_toggled") + self.common.log("TorSettingsTab", "authenticate_no_auth_toggled") if checked: self.authenticate_password_extras.hide() else: @@ -575,39 +617,34 @@ class TorSettingsDialog(QtWidgets.QDialog): Test Tor Settings button clicked. With the given settings, see if we can successfully connect and authenticate to Tor. """ - self.common.log("TorSettingsDialog", "test_tor_clicked") + self.common.log("TorSettingsTab", "test_tor_clicked") + + self.error_label.setText("") + settings = self.settings_from_fields() if not settings: return - onion = Onion( - self.common, use_tmp_dir=True, get_tor_paths=self.common.gui.get_tor_paths + self.test_tor_button.hide() + self.save_button.hide() + + self.test_onion = Onion( + self.common, + use_tmp_dir=True, + get_tor_paths=self.common.gui.get_tor_paths, ) - tor_con = TorConnectionDialog(self.common, settings, True, onion) - tor_con.start() - - # If Tor settings worked, show results - if onion.connected_to_tor: - Alert( - self.common, - strings._("settings_test_success").format( - onion.tor_version, - onion.supports_ephemeral, - onion.supports_stealth, - onion.supports_v3_onions, - ), - title=strings._("gui_settings_connection_type_test_button"), - ) - - # Clean up - onion.cleanup() + self.tor_con_type = "test" + self.tor_con.show() + self.tor_con.start(settings, True, self.test_onion) def save_clicked(self): """ Save button clicked. Save current settings to disk. """ - self.common.log("TorSettingsDialog", "save_clicked") + self.common.log("TorSettingsTab", "save_clicked") + + self.error_label.setText("") def changed(s1, s2, keys): """ @@ -630,7 +667,7 @@ class TorSettingsDialog(QtWidgets.QDialog): if not self.common.gui.local_only: if self.common.gui.onion.is_authenticated(): self.common.log( - "TorSettingsDialog", "save_clicked", "Connected to Tor" + "TorSettingsTab", "save_clicked", "Connected to Tor" ) if changed( @@ -645,10 +682,11 @@ class TorSettingsDialog(QtWidgets.QDialog): "socket_file_path", "auth_type", "auth_password", - "no_bridges", - "tor_bridges_use_obfs4", - "tor_bridges_use_meek_lite_azure", - "tor_bridges_use_custom_bridges", + "bridges_enabled", + "bridges_type", + "bridges_builtin_pt", + "bridges_moat", + "bridges_custom", ], ): @@ -656,65 +694,86 @@ class TorSettingsDialog(QtWidgets.QDialog): else: self.common.log( - "TorSettingsDialog", "save_clicked", "Not connected to Tor" + "TorSettingsTab", "save_clicked", "Not connected to Tor" ) # Tor isn't connected, so try connecting reboot_onion = True # Do we need to reinitialize Tor? if reboot_onion: + # Tell the tabs that Tor is disconnected + self.tor_is_disconnected.emit() + # Reinitialize the Onion object self.common.log( - "TorSettingsDialog", "save_clicked", "rebooting the Onion" + "TorSettingsTab", "save_clicked", "rebooting the Onion" ) self.common.gui.onion.cleanup() - tor_con = TorConnectionDialog(self.common, settings) - tor_con.start() - - self.common.log( - "TorSettingsDialog", - "save_clicked", - f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", - ) - - if ( - self.common.gui.onion.is_authenticated() - and not tor_con.wasCanceled() - ): - self.settings_saved.emit() - self.close() + self.test_tor_button.hide() + self.save_button.hide() + self.tor_con_type = "save" + self.tor_con.show() + self.tor_con.start(settings) else: - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() else: - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() - def cancel_clicked(self): + def tor_con_success(self): """ - Cancel button clicked. + Finished testing tor connection. """ - self.common.log("TorSettingsDialog", "cancel_clicked") - if ( - not self.common.gui.local_only - and not self.common.gui.onion.is_authenticated() - ): + self.tor_con.hide() + self.test_tor_button.show() + self.save_button.show() + + if self.tor_con_type == "test": Alert( self.common, - strings._("gui_tor_connection_canceled"), - QtWidgets.QMessageBox.Warning, + strings._("settings_test_success").format( + self.test_onion.tor_version, + self.test_onion.supports_ephemeral, + self.test_onion.supports_stealth, + self.test_onion.supports_v3_onions, + ), + title=strings._("gui_settings_connection_type_test_button"), ) - sys.exit() - else: - self.close() + self.test_onion.cleanup() + + elif self.tor_con_type == "save": + if ( + self.common.gui.onion.is_authenticated() + and not self.tor_con.wasCanceled() + ): + # Tell the tabs that Tor is connected + self.tor_is_connected.emit() + # Close the tab + self.close_this_tab.emit() + + self.tor_con_type = None + + def tor_con_fail(self, msg): + """ + Finished testing tor connection. + """ + self.tor_con.hide() + self.test_tor_button.show() + self.save_button.show() + + self.error_label.setText(msg) + + if self.tor_con_type == "test": + self.test_onion.cleanup() + + self.tor_con_type = None def settings_from_fields(self): """ Return a Settings object that's full of values from the settings dialog. """ - self.common.log("TorSettingsDialog", "settings_from_fields") + self.common.log("TorSettingsTab", "settings_from_fields") settings = Settings(self.common) settings.load() # To get the last update timestamp @@ -751,47 +810,30 @@ class TorSettingsDialog(QtWidgets.QDialog): # Whether we use bridges if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked: - settings.set("no_bridges", False) + settings.set("bridges_enabled", True) if self.bridge_builtin_radio.isChecked(): - selection = self.bridge_builtin_dropdown.currentText() - if selection == "obfs4": - settings.set("tor_bridges_use_obfs4", True) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - elif selection == "meek-azure": - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", True) - settings.set("tor_bridges_use_snowflake", False) - elif selection == "snowflake": - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", True) + settings.set("bridges_type", "built-in") - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") + selection = self.bridge_builtin_dropdown.currentText() + settings.set("bridges_builtin_pt", selection) if self.bridge_moat_radio.isChecked(): - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - - settings.set("tor_bridges_use_moat", True) - + settings.set("bridges_type", "moat") moat_bridges = self.bridge_moat_textbox.toPlainText() - if moat_bridges.strip() == "": - Alert(self.common, strings._("gui_settings_moat_bridges_invalid")) + if ( + self.connection_type_bundled_radio.isChecked() + and moat_bridges.strip() == "" + ): + self.error_label.setText( + strings._("gui_settings_moat_bridges_invalid") + ) return False - settings.set("tor_bridges_use_moat_bridges", moat_bridges) - - settings.set("tor_bridges_use_custom_bridges", "") + settings.set("bridges_moat", moat_bridges) if self.bridge_custom_radio.isChecked(): - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) + settings.set("bridges_type", "custom") new_bridges = [] bridges = self.bridge_custom_textbox.toPlainText().split("\n") @@ -822,26 +864,32 @@ class TorSettingsDialog(QtWidgets.QDialog): if bridges_valid: new_bridges = "\n".join(new_bridges) + "\n" - settings.set("tor_bridges_use_custom_bridges", new_bridges) + settings.set("bridges_custom", new_bridges) else: - Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) + self.error_label.setText( + strings._("gui_settings_tor_bridges_invalid") + ) return False else: - settings.set("no_bridges", True) + settings.set("bridges_enabled", False) return settings def closeEvent(self, e): - self.common.log("TorSettingsDialog", "closeEvent") + self.common.log("TorSettingsTab", "closeEvent") # On close, if Tor isn't connected, then quit OnionShare altogether if not self.common.gui.local_only: if not self.common.gui.onion.is_authenticated(): self.common.log( - "TorSettingsDialog", + "TorSettingsTab", "closeEvent", "Closing while not connected to Tor", ) # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) + + def settings_have_changed(self): + # Global settings have changed + self.common.log("TorSettingsTab", "settings_have_changed")