From b727a9651f065379a9725a01398b247aa4a2ef0e Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 11 Nov 2021 17:33:19 +1100 Subject: [PATCH 1/4] Initial work on supporting the option to automatically attempt to fetch bridges based on the user's location if Tor fails to connect (censorship circumvention) --- cli/onionshare_cli/settings.py | 1 + cli/tests/test_cli_settings.py | 1 + .../src/onionshare/resources/locale/en.json | 3 +- desktop/src/onionshare/tor_connection.py | 31 +++++++++++++++++-- desktop/src/onionshare/tor_settings_tab.py | 23 +++++++++++++- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index c7d74a70..020c2776 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -113,6 +113,7 @@ class Settings(object): "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() "theme": 0, + "censorship_circumvention": False, } self._settings = {} self.fill_in_defaults() diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index 9513b013..9486e8d1 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -36,6 +36,7 @@ class TestSettings: "bridges_custom": "", "persistent_tabs": [], "theme": 0, + "censorship_circumvention": False, } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 868a6fa9..a69a7101 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -63,6 +63,7 @@ "gui_settings_tor_bridges": "Connect using a Tor bridge?", "gui_settings_tor_bridges_label": "Bridges help you access the Tor Network in places where Tor is blocked. Depending on where you are, one bridge may work better than another.", "gui_settings_bridge_use_checkbox": "Use a bridge", + "gui_settings_censorship_circumvention_checkbox": "Attempt to automatically find a bridge based on your country if Tor fails to connect", "gui_settings_bridge_radio_builtin": "Select a built-in bridge", "gui_settings_bridge_none_radio_option": "Don't use a bridge", "gui_settings_meek_lite_expensive_warning": "Warning: The meek-azure bridges are very costly for the Tor Project to run.

Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.", @@ -231,4 +232,4 @@ "moat_captcha_error": "The solution is not correct. Please try again.", "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/tor_connection.py b/desktop/src/onionshare/tor_connection.py index 2cc599c4..0f3b7b2b 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -41,7 +41,7 @@ from onionshare_cli.onion import ( from . import strings from .gui_common import GuiCommon from .widgets import Alert - +from onionshare_cli.censorship import CensorshipCircumvention class TorConnectionDialog(QtWidgets.QProgressDialog): """ @@ -165,7 +165,7 @@ class TorConnectionWidget(QtWidgets.QWidget): success = QtCore.Signal() fail = QtCore.Signal(str) - def __init__(self, common, status_bar): + def __init__(self, common, status_bar, meek): super(TorConnectionWidget, self).__init__(None) self.common = common self.common.log("TorConnectionWidget", "__init__") @@ -181,6 +181,8 @@ class TorConnectionWidget(QtWidgets.QWidget): ) self.cancel_button.clicked.connect(self.cancel_clicked) + self.meek = meek + progress_layout = QtWidgets.QHBoxLayout() progress_layout.addWidget(self.progress) progress_layout.addWidget(self.cancel_button) @@ -263,7 +265,30 @@ class TorConnectionWidget(QtWidgets.QWidget): def _error_connecting_to_tor(self, msg): self.common.log("TorConnectionWidget", "_error_connecting_to_tor") self.active = False - self.fail.emit(msg) + # If we are allowed to try automatically resolving connection issues + # (e.g possible censorship) by obtaining bridges for the user, do so + if self.settings.get("censorship_circumvention"): + # Automatically try to obtain bridges from the Censorship Circumvention API + self.common.log("TorConnectionWidget", "_error_connecting_to_tor", "Trying to automatically obtain bridges") + self.meek.start() + self.censorship_circumvention = CensorshipCircumvention(self.common, self.meek) + request_bridges = self.censorship_circumvention.request_settings(country="cn") + if request_bridges: + # @TODO there might be several bridges + bridges = request_bridges["settings"][0]["bridges"]["bridge_strings"][0] + self.common.log("TorConnectionWidget", "_error_connecting_to_tor", f"Obtained bridges: {bridges}") + self.settings.set("bridges_enabled", True) + self.settings.set("bridges_type", "custom") + # @TODO there might be several bridges + self.settings.set("bridges_custom", bridges) + self.common.log("TorConnectionWidget", "_error_connecting_to_tor", "Starting Tor again") + self.settings.save() + # Now try and connect again + self.start() + else: + self.fail.emit() + else: + self.fail.emit() class TorConnectionThread(QtCore.QThread): diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index e28e5260..382b34fd 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -91,6 +91,12 @@ class TorSettingsTab(QtWidgets.QWidget): self.bridge_use_checkbox.stateChanged.connect( self.bridge_use_checkbox_state_changed ) + self.censorship_circumvention_checkbox = QtWidgets.QCheckBox( + strings._("gui_settings_censorship_circumvention_checkbox") + ) + self.censorship_circumvention_checkbox.stateChanged.connect( + self.censorship_circumvention_checkbox_state_changed + ) # Built-in bridge self.bridge_builtin_radio = QtWidgets.QRadioButton( @@ -164,6 +170,7 @@ class TorSettingsTab(QtWidgets.QWidget): bridges_layout = QtWidgets.QVBoxLayout() bridges_layout.addWidget(bridges_label) bridges_layout.addWidget(self.bridge_use_checkbox) + bridges_layout.addWidget(self.censorship_circumvention_checkbox) bridges_layout.addWidget(self.bridge_settings) self.bridges = QtWidgets.QWidget() @@ -330,7 +337,7 @@ class TorSettingsTab(QtWidgets.QWidget): columns_wrapper.setLayout(columns_layout) # Tor connection widget - self.tor_con = TorConnectionWidget(self.common, self.status_bar) + self.tor_con = TorConnectionWidget(self.common, self.status_bar, self.meek) self.tor_con.success.connect(self.tor_con_success) self.tor_con.fail.connect(self.tor_con_fail) self.tor_con.hide() @@ -430,6 +437,7 @@ class TorSettingsTab(QtWidgets.QWidget): if self.old_settings.get("bridges_enabled"): self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) + self.censorship_circumvention_checkbox.setCheckState(QtCore.Qt.Checked) self.bridge_settings.show() bridges_type = self.old_settings.get("bridges_type") @@ -506,6 +514,16 @@ class TorSettingsTab(QtWidgets.QWidget): else: self.bridge_settings.hide() + def censorship_circumvention_checkbox_state_changed(self, state): + """ + 'Allow censorship circumvention (automatic bridges)' checkbox changed + """ + # Turning on censorship circumvention through the act of + # automatic bridge selection, implicitly means enabling + # bridges. + if state == QtCore.Qt.Checked: + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) + def bridge_builtin_radio_toggled(self, checked): """ 'Select a built-in bridge' radio button toggled @@ -812,6 +830,9 @@ class TorSettingsTab(QtWidgets.QWidget): if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked: settings.set("bridges_enabled", True) + if self.censorship_circumvention_checkbox.checkState() == QtCore.Qt.Checked: + settings.set("censorship_circumvention", True) + if self.bridge_builtin_radio.isChecked(): settings.set("bridges_type", "built-in") From e2603ed7ad1e3ad6cdc9b77a64c8b18e7aa6fb54 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 11 Nov 2021 18:46:22 +1100 Subject: [PATCH 2/4] Better detection of different types of bridges, different sources, and multiple bridges of the same type --- desktop/src/onionshare/tor_connection.py | 64 ++++++++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py index 0f3b7b2b..51100650 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -43,6 +43,7 @@ from .gui_common import GuiCommon from .widgets import Alert from onionshare_cli.censorship import CensorshipCircumvention + class TorConnectionDialog(QtWidgets.QProgressDialog): """ Connecting to Tor dialog. @@ -269,19 +270,60 @@ class TorConnectionWidget(QtWidgets.QWidget): # (e.g possible censorship) by obtaining bridges for the user, do so if self.settings.get("censorship_circumvention"): # Automatically try to obtain bridges from the Censorship Circumvention API - self.common.log("TorConnectionWidget", "_error_connecting_to_tor", "Trying to automatically obtain bridges") + self.common.log( + "TorConnectionWidget", + "_error_connecting_to_tor", + "Trying to automatically obtain bridges", + ) self.meek.start() - self.censorship_circumvention = CensorshipCircumvention(self.common, self.meek) - request_bridges = self.censorship_circumvention.request_settings(country="cn") + self.censorship_circumvention = CensorshipCircumvention( + self.common, self.meek + ) + request_bridges = self.censorship_circumvention.request_settings( + country="tm" + ) if request_bridges: - # @TODO there might be several bridges - bridges = request_bridges["settings"][0]["bridges"]["bridge_strings"][0] - self.common.log("TorConnectionWidget", "_error_connecting_to_tor", f"Obtained bridges: {bridges}") - self.settings.set("bridges_enabled", True) - self.settings.set("bridges_type", "custom") - # @TODO there might be several bridges - self.settings.set("bridges_custom", bridges) - self.common.log("TorConnectionWidget", "_error_connecting_to_tor", "Starting Tor again") + # @TODO there might be several bridge types recommended. + # Should we attempt to iterate over each type if one of them fails to connect? + # But if so, how to stop it starting 3 separate Tor connection threads? + # for bridges in request_bridges["settings"]: + bridges = request_bridges["settings"][0]["bridges"] + self.common.log( + "TorConnectionWidget", + "_error_connecting_to_tor", + f"Obtained bridges: {bridges}", + ) + bridge_strings = bridges["bridge_strings"] + bridge_type = bridges["type"] + bridge_source = bridges["source"] + + # If the recommended bridge source is to use the built-in + # bridges, set that in our settings, as if the user had + # selected the built-in bridges for a specific PT themselves. + # + # @TODO should we fetch the built-in bridges from + # censorship_circumvention.request_builtin_bridges()? + # + # In fact, the bridge_string returned for a bridge type 'builtin' + # is in fact the same bridges we'd get from that other method anyway. + if bridge_source == "builtin": + self.settings.set("bridges_type", "built-in") + if bridge_type == "obfs4": + self.settings.set("bridges_builtin_pt", "obfs4") + if bridge_type == "snowflake": + self.settings.set("bridges_builtin_pt", "snowflake") + if bridge_type == "meek": + self.settings.set("bridges_builtin_pt", "meek-azure") + else: + self.settings.set("bridges_type", "custom") + # @TODO do we want to to a sanity check on the bridges like custom ones? + self.settings.set("bridges_custom", "\n".join(bridge_strings)) + + self.common.log( + "TorConnectionWidget", + "_error_connecting_to_tor", + "Starting Tor again", + ) self.settings.save() # Now try and connect again self.start() From b151eeb3c3a0eb6e2ff999377edf5fbc40742e09 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Nov 2021 10:01:14 +1100 Subject: [PATCH 3/4] Make sanity checking of bridges a reusable component in cli.Common, so we can reuse it for automatic bridge fetching in censorship circumvention --- cli/onionshare_cli/common.py | 35 ++++++++++++++++++++++ desktop/src/onionshare/tor_connection.py | 11 +++---- desktop/src/onionshare/tor_settings_tab.py | 30 ++----------------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index bab3fd86..7a8bc857 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -28,6 +28,7 @@ import sys import threading import time import shutil +import re from pkg_resources import resource_filename import colorama @@ -432,6 +433,40 @@ class Common: r = random.SystemRandom() return "-".join(r.choice(wordlist) for _ in range(word_count)) + def check_bridges_valid(self, bridges): + """ + Does a regex check against a supplied list of bridges, to make sure they + are valid strings depending on the bridge type. + """ + valid_bridges = [] + self.log("Common", "check_bridges_valid", "Checking bridge syntax") + for bridge in bridges: + if bridge != "": + # Check the syntax of the custom bridge to make sure it looks legitimate + ipv4_pattern = re.compile( + "(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$" + ) + ipv6_pattern = re.compile( + "(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$" + ) + meek_lite_pattern = re.compile( + "(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)" + ) + snowflake_pattern = re.compile( + "(snowflake)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)" + ) + if ( + ipv4_pattern.match(bridge) + or ipv6_pattern.match(bridge) + or meek_lite_pattern.match(bridge) + or snowflake_pattern.match(bridge) + ): + valid_bridges.append(bridge) + if valid_bridges: + return valid_bridges + else: + return False + @staticmethod def random_string(num_bytes, output_len=None): """ diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py index 51100650..a0025623 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -301,11 +301,6 @@ class TorConnectionWidget(QtWidgets.QWidget): # bridges, set that in our settings, as if the user had # selected the built-in bridges for a specific PT themselves. # - # @TODO should we fetch the built-in bridges from - # censorship_circumvention.request_builtin_bridges()? - # - # In fact, the bridge_string returned for a bridge type 'builtin' - # is in fact the same bridges we'd get from that other method anyway. if bridge_source == "builtin": self.settings.set("bridges_type", "built-in") if bridge_type == "obfs4": @@ -316,8 +311,10 @@ class TorConnectionWidget(QtWidgets.QWidget): self.settings.set("bridges_builtin_pt", "meek-azure") else: self.settings.set("bridges_type", "custom") - # @TODO do we want to to a sanity check on the bridges like custom ones? - self.settings.set("bridges_custom", "\n".join(bridge_strings)) + # Sanity check the bridges provided from the Tor API before saving + bridges_checked = self.common.check_bridges_valid(bridge_strings) + if bridges_checked: + self.settings.set("bridges_custom", "\n".join(bridges_checked)) self.common.log( "TorConnectionWidget", diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 382b34fd..9d9ec2ef 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -21,7 +21,6 @@ along with this program. If not, see . from PySide2 import QtCore, QtWidgets, QtGui import sys import platform -import re import os from onionshare_cli.meek import Meek @@ -856,35 +855,10 @@ class TorSettingsTab(QtWidgets.QWidget): if self.bridge_custom_radio.isChecked(): settings.set("bridges_type", "custom") - new_bridges = [] bridges = self.bridge_custom_textbox.toPlainText().split("\n") - bridges_valid = False - for bridge in bridges: - if bridge != "": - # Check the syntax of the custom bridge to make sure it looks legitimate - ipv4_pattern = re.compile( - "(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$" - ) - ipv6_pattern = re.compile( - "(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$" - ) - meek_lite_pattern = re.compile( - "(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)" - ) - snowflake_pattern = re.compile( - "(snowflake)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)" - ) - if ( - ipv4_pattern.match(bridge) - or ipv6_pattern.match(bridge) - or meek_lite_pattern.match(bridge) - or snowflake_pattern.match(bridge) - ): - new_bridges.append(bridge) - bridges_valid = True - + bridges_valid = self.common.check_bridges_valid(bridges) if bridges_valid: - new_bridges = "\n".join(new_bridges) + "\n" + new_bridges = "\n".join(bridges_valid) + "\n" settings.set("bridges_custom", new_bridges) else: self.error_label.setText( From 3273832da717aed487955be82bff71407991842a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Nov 2021 10:50:47 +1100 Subject: [PATCH 4/4] Make the saving of the automatically-obtained bridges reusable (move it to CensorshipCircumvention class). Add the same functionality used in TorConnectionWidget to TorConnectionDialog. --- cli/onionshare_cli/censorship.py | 62 ++++++++++++++++++++ desktop/src/onionshare/main_window.py | 6 +- desktop/src/onionshare/tor_connection.py | 74 ++++++++++-------------- 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py index f84b1058..89513faa 100644 --- a/cli/onionshare_cli/censorship.py +++ b/cli/onionshare_cli/censorship.py @@ -167,3 +167,65 @@ class CensorshipCircumvention(object): return False return result + + def save_settings(self, settings, bridge_settings): + """ + Checks the bridges and saves them in settings. + """ + bridges_ok = False + self.settings = settings + + # @TODO there might be several bridge types recommended. + # Should we attempt to iterate over each type if one of them fails to connect? + # But if so, how to stop it starting 3 separate Tor connection threads? + # for bridges in request_bridges["settings"]: + bridges = bridge_settings["settings"][0]["bridges"] + self.common.log( + "CensorshipCircumvention", + "save_settings", + f"Obtained bridges: {bridges}", + ) + bridge_strings = bridges["bridge_strings"] + bridge_type = bridges["type"] + bridge_source = bridges["source"] + + # If the recommended bridge source is to use the built-in + # bridges, set that in our settings, as if the user had + # selected the built-in bridges for a specific PT themselves. + # + if bridge_source == "builtin": + self.settings.set("bridges_type", "built-in") + if bridge_type == "obfs4": + self.settings.set("bridges_builtin_pt", "obfs4") + if bridge_type == "snowflake": + self.settings.set("bridges_builtin_pt", "snowflake") + if bridge_type == "meek": + self.settings.set("bridges_builtin_pt", "meek-azure") + bridges_ok = True + else: + # Any other type of bridge we can treat as custom. + self.settings.set("bridges_type", "custom") + + # Sanity check the bridges provided from the Tor API before saving + bridges_checked = self.common.check_bridges_valid(bridge_strings) + + if bridges_checked: + self.settings.set("bridges_custom", "\n".join(bridges_checked)) + bridges_ok = True + + # If we got any good bridges, save them to settings and return. + if bridges_ok: + self.common.log( + "CensorshipCircumvention", + "save_settings", + "Saving settings with automatically-obtained bridges", + ) + self.settings.save() + return True + else: + self.common.log( + "CensorshipCircumvention", + "save_settings", + "Could not use any of the obtained bridges.", + ) + return False diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index 546592a1..79738d38 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -30,6 +30,7 @@ from .tab_widget import TabWidget from .gui_common import GuiCommon from .threads import OnionCleanupThread +from onionshare_cli.meek import Meek class MainWindow(QtWidgets.QMainWindow): """ @@ -160,8 +161,11 @@ class MainWindow(QtWidgets.QMainWindow): self.setCentralWidget(central_widget) self.show() + # Instantiate Meek, which the TorConnectionDialog may use to resolve + # connection issues by automatically obtaining bridges. + self.meek = Meek(self.common, get_tor_paths=self.common.gui.get_tor_paths) # Start the "Connecting to Tor" dialog, which calls onion.connect() - tor_con = TorConnectionDialog(self.common) + tor_con = TorConnectionDialog(self.common, self.meek) tor_con.canceled.connect(self.tor_connection_canceled) tor_con.open_tor_settings.connect(self.tor_connection_open_tor_settings) if not self.common.gui.local_only: diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py index a0025623..5c427d3d 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -53,13 +53,15 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): success = QtCore.Signal() def __init__( - self, common, custom_settings=False, testing_settings=False, onion=None + self, common, meek, custom_settings=False, testing_settings=False, onion=None ): super(TorConnectionDialog, self).__init__(None) self.common = common self.testing_settings = testing_settings + self.meek = meek + if custom_settings: self.settings = custom_settings else: @@ -138,6 +140,30 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def alert(): Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title) + # If we are allowed to try automatically resolving connection issues + # (e.g possible censorship) by obtaining bridges for the user, do so + elif self.settings.get("censorship_circumvention"): + def alert(): + return + + # Automatically try to obtain bridges from the Censorship Circumvention API + self.common.log( + "TorConnectionDialog", + "_error_connecting_to_tor", + "Trying to automatically obtain bridges", + ) + self.meek.start() + self.censorship_circumvention = CensorshipCircumvention( + self.common, self.meek + ) + bridge_settings = self.censorship_circumvention.request_settings( + country="tm" + ) + self.meek.cleanup() + + if bridge_settings and self.censorship_circumvention.save_settings(self.settings, bridge_settings): + # Try and connect again + self.start() else: # If not testing, open settings after displaying the error def alert(): @@ -266,6 +292,7 @@ class TorConnectionWidget(QtWidgets.QWidget): def _error_connecting_to_tor(self, msg): self.common.log("TorConnectionWidget", "_error_connecting_to_tor") self.active = False + # If we are allowed to try automatically resolving connection issues # (e.g possible censorship) by obtaining bridges for the user, do so if self.settings.get("censorship_circumvention"): @@ -279,50 +306,13 @@ class TorConnectionWidget(QtWidgets.QWidget): self.censorship_circumvention = CensorshipCircumvention( self.common, self.meek ) - request_bridges = self.censorship_circumvention.request_settings( + bridge_settings = self.censorship_circumvention.request_settings( country="tm" ) - if request_bridges: - # @TODO there might be several bridge types recommended. - # Should we attempt to iterate over each type if one of them fails to connect? - # But if so, how to stop it starting 3 separate Tor connection threads? - # for bridges in request_bridges["settings"]: - bridges = request_bridges["settings"][0]["bridges"] - self.common.log( - "TorConnectionWidget", - "_error_connecting_to_tor", - f"Obtained bridges: {bridges}", - ) - bridge_strings = bridges["bridge_strings"] - bridge_type = bridges["type"] - bridge_source = bridges["source"] + self.meek.cleanup() - # If the recommended bridge source is to use the built-in - # bridges, set that in our settings, as if the user had - # selected the built-in bridges for a specific PT themselves. - # - if bridge_source == "builtin": - self.settings.set("bridges_type", "built-in") - if bridge_type == "obfs4": - self.settings.set("bridges_builtin_pt", "obfs4") - if bridge_type == "snowflake": - self.settings.set("bridges_builtin_pt", "snowflake") - if bridge_type == "meek": - self.settings.set("bridges_builtin_pt", "meek-azure") - else: - self.settings.set("bridges_type", "custom") - # Sanity check the bridges provided from the Tor API before saving - bridges_checked = self.common.check_bridges_valid(bridge_strings) - if bridges_checked: - self.settings.set("bridges_custom", "\n".join(bridges_checked)) - - self.common.log( - "TorConnectionWidget", - "_error_connecting_to_tor", - "Starting Tor again", - ) - self.settings.save() - # Now try and connect again + if bridge_settings and self.censorship_circumvention.save_settings(self.settings, bridge_settings): + # Try and connect again self.start() else: self.fail.emit()