diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py
index 9f41d61c..d9b02616 100644
--- a/cli/onionshare_cli/censorship.py
+++ b/cli/onionshare_cli/censorship.py
@@ -198,3 +198,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/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py
index a8e32411..4b483cf5 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
@@ -441,6 +442,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
+
def is_flatpak(self):
"""
Returns True if OnionShare is running in a Flatpak sandbox
@@ -453,6 +488,7 @@ class Common:
"""
return os.environ.get("SNAP_INSTANCE_NAME") == "onionshare"
+
@staticmethod
def random_string(num_bytes, output_len=None):
"""
diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py
index 8a4a9939..241ed083 100644
--- a/cli/onionshare_cli/settings.py
+++ b/cli/onionshare_cli/settings.py
@@ -114,6 +114,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 f370a674..9f255e8e 100644
--- a/cli/tests/test_cli_settings.py
+++ b/cli/tests/test_cli_settings.py
@@ -37,6 +37,7 @@ class TestSettings:
"bridges_builtin": {},
"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/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/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json
index f8c4cd2b..85b7e483 100644
--- a/desktop/src/onionshare/resources/locale/en.json
+++ b/desktop/src/onionshare/resources/locale/en.json
@@ -64,6 +64,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.",
diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py
index 2cc599c4..5c427d3d 100644
--- a/desktop/src/onionshare/tor_connection.py
+++ b/desktop/src/onionshare/tor_connection.py
@@ -41,6 +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):
@@ -52,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:
@@ -137,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():
@@ -165,7 +192,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 +208,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 +292,32 @@ 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
+ )
+ 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:
+ 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..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
@@ -91,6 +90,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 +169,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 +336,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 +436,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 +513,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 +829,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")
@@ -835,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(