aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
authorMicah Lee <micah@micahflee.com>2022-03-28 18:18:57 -0700
committerMicah Lee <micah@micahflee.com>2022-03-28 18:18:57 -0700
commit4ff23fa9bd927f7300b2ded3f44b3af17fb6780e (patch)
treead07df49cb97980cee3952324fef600ce99a8fa1 /cli
parentf08e6e98306ecf55a6caee5dd28dd643044f7241 (diff)
parent52cb5cf71aef4ad05d2345757f8e2a66abc86753 (diff)
downloadonionshare-4ff23fa9bd927f7300b2ded3f44b3af17fb6780e.tar.gz
onionshare-4ff23fa9bd927f7300b2ded3f44b3af17fb6780e.zip
Merge branch 'develop' into upgrade-flask
Diffstat (limited to 'cli')
-rw-r--r--cli/onionshare_cli/censorship.py229
-rw-r--r--cli/onionshare_cli/common.py37
-rw-r--r--cli/onionshare_cli/onion.py14
-rw-r--r--cli/onionshare_cli/settings.py1
-rw-r--r--cli/onionshare_cli/web/send_base_mode.py18
-rw-r--r--cli/onionshare_cli/web/web.py6
-rw-r--r--cli/tests/test_cli_settings.py1
-rw-r--r--cli/tests/test_cli_web.py5
8 files changed, 207 insertions, 104 deletions
diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py
index c1845f6a..4ab5c366 100644
--- a/cli/onionshare_cli/censorship.py
+++ b/cli/onionshare_cli/censorship.py
@@ -22,6 +22,12 @@ import requests
from .meek import MeekNotRunning
+class CensorshipCircumventionError(Exception):
+ """
+ There was a problem connecting to the Tor CensorshipCircumvention API.
+ """
+
+
class CensorshipCircumvention(object):
"""
Connect to the Tor Moat APIs to retrieve censorship
@@ -47,7 +53,7 @@ class CensorshipCircumvention(object):
self.common.log(
"CensorshipCircumvention",
"__init__",
- "Using Meek with CensorShipCircumvention API",
+ "Using Meek with CensorshipCircumvention API",
)
self.api_proxies = self.meek.meek_proxies
if onion:
@@ -58,7 +64,7 @@ class CensorshipCircumvention(object):
self.common.log(
"CensorshipCircumvention",
"__init__",
- "Using Tor with CensorShipCircumvention API",
+ "Using Tor with CensorshipCircumvention API",
)
(socks_address, socks_port) = self.onion.get_tor_socks_port()
self.api_proxies = {
@@ -84,31 +90,34 @@ class CensorshipCircumvention(object):
if country:
data = {"country": country}
- r = requests.post(
- endpoint,
- json=data,
- headers={"Content-Type": "application/vnd.api+json"},
- proxies=self.api_proxies,
- )
- if r.status_code != 200:
- self.common.log(
- "CensorshipCircumvention",
- "censorship_obtain_map",
- f"status_code={r.status_code}",
+ try:
+ r = requests.post(
+ endpoint,
+ json=data,
+ headers={"Content-Type": "application/vnd.api+json"},
+ proxies=self.api_proxies,
)
- return False
+ if r.status_code != 200:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_map",
+ f"status_code={r.status_code}",
+ )
+ return False
- result = r.json()
+ result = r.json()
- if "errors" in result:
- self.common.log(
- "CensorshipCircumvention",
- "censorship_obtain_map",
- f"errors={result['errors']}",
- )
- return False
+ if "errors" in result:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_map",
+ f"errors={result['errors']}",
+ )
+ return False
- return result
+ return result
+ except requests.exceptions.RequestException as e:
+ raise CensorshipCircumventionError(e)
def request_settings(self, country=False, transports=False):
"""
@@ -127,45 +136,53 @@ class CensorshipCircumvention(object):
endpoint = "https://bridges.torproject.org/moat/circumvention/settings"
data = {}
if country:
- data = {"country": country}
- if transports:
- data.append({"transports": transports})
- r = requests.post(
- endpoint,
- json=data,
- headers={"Content-Type": "application/vnd.api+json"},
- proxies=self.api_proxies,
- )
- if r.status_code != 200:
self.common.log(
"CensorshipCircumvention",
"censorship_obtain_settings",
- f"status_code={r.status_code}",
+ f"Trying to obtain bridges for country={country}",
)
- return False
+ data = {"country": country}
+ if transports:
+ data.append({"transports": transports})
+ try:
+ r = requests.post(
+ endpoint,
+ json=data,
+ headers={"Content-Type": "application/vnd.api+json"},
+ proxies=self.api_proxies,
+ )
+ if r.status_code != 200:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_settings",
+ f"status_code={r.status_code}",
+ )
+ return False
- result = r.json()
+ result = r.json()
- if "errors" in result:
- self.common.log(
- "CensorshipCircumvention",
- "censorship_obtain_settings",
- f"errors={result['errors']}",
- )
- return False
+ if "errors" in result:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_settings",
+ f"errors={result['errors']}",
+ )
+ return False
- # There are no settings - perhaps this country doesn't require censorship circumvention?
- # This is not really an error, so we can just check if False and assume direct Tor
- # connection will work.
- if not "settings" in result:
- self.common.log(
- "CensorshipCircumvention",
- "censorship_obtain_settings",
- "No settings found for this country",
- )
- return False
+ # There are no settings - perhaps this country doesn't require censorship circumvention?
+ # This is not really an error, so we can just check if False and assume direct Tor
+ # connection will work.
+ if not "settings" in result:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_settings",
+ "No settings found for this country",
+ )
+ return False
- return result
+ return result
+ except requests.exceptions.RequestException as e:
+ raise CensorshipCircumventionError(e)
def request_builtin_bridges(self):
"""
@@ -174,27 +191,103 @@ class CensorshipCircumvention(object):
if not self.api_proxies:
return False
endpoint = "https://bridges.torproject.org/moat/circumvention/builtin"
- r = requests.post(
- endpoint,
- headers={"Content-Type": "application/vnd.api+json"},
- proxies=self.api_proxies,
+ try:
+ r = requests.post(
+ endpoint,
+ headers={"Content-Type": "application/vnd.api+json"},
+ proxies=self.api_proxies,
+ )
+ if r.status_code != 200:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_builtin_bridges",
+ f"status_code={r.status_code}",
+ )
+ return False
+
+ result = r.json()
+
+ if "errors" in result:
+ self.common.log(
+ "CensorshipCircumvention",
+ "censorship_obtain_builtin_bridges",
+ f"errors={result['errors']}",
+ )
+ return False
+
+ return result
+ except requests.exceptions.RequestException as e:
+ raise CensorshipCircumventionError(e)
+
+ 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}",
)
- if r.status_code != 200:
+ 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.common.log(
"CensorshipCircumvention",
- "censorship_obtain_builtin_bridges",
- f"status_code={r.status_code}",
+ "save_settings",
+ "Will be using built-in bridges",
)
- return False
+ 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:
+ self.common.log(
+ "CensorshipCircumvention",
+ "save_settings",
+ "Will be using custom bridges",
+ )
+ # 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)
- result = r.json()
+ if bridges_checked:
+ self.settings.set("bridges_custom", "\n".join(bridges_checked))
+ bridges_ok = True
- if "errors" in result:
+ # If we got any good bridges, save them to settings and return.
+ if bridges_ok:
self.common.log(
"CensorshipCircumvention",
- "censorship_obtain_builtin_bridges",
- f"errors={result['errors']}",
+ "save_settings",
+ "Saving settings with automatically-obtained bridges",
+ )
+ self.settings.set("bridges_enabled", True)
+ self.settings.save()
+ return True
+ else:
+ self.common.log(
+ "CensorshipCircumvention",
+ "save_settings",
+ "Could not use any of the obtained bridges.",
)
return False
-
- return result
diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py
index 82ac9883..ceec654d 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
@@ -312,7 +313,6 @@ class Common:
"""
Returns the absolute path of a resource
"""
- self.log("Common", "get_resource_path", f"filename={filename}")
path = resource_filename("onionshare_cli", os.path.join("resources", filename))
self.log("Common", "get_resource_path", f"filename={filename}, path={path}")
return path
@@ -467,6 +467,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
@@ -479,6 +513,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/onion.py b/cli/onionshare_cli/onion.py
index 6ef4af2c..6e1dad74 100644
--- a/cli/onionshare_cli/onion.py
+++ b/cli/onionshare_cli/onion.py
@@ -954,20 +954,6 @@ class Onion(object):
"update_builtin_bridges",
f"Obtained bridges: {builtin_bridges}",
)
- if builtin_bridges["meek"]:
- # Meek bridge needs to be defined as "meek_lite", not "meek",
- # for it to work with obfs4proxy.
- # We also refer to this bridge type as 'meek-azure' in our settings.
- # So first, rename the key in the dict
- builtin_bridges["meek-azure"] = builtin_bridges.pop("meek")
- new_meek_bridges = []
- # Now replace the values. They also need the url/front params appended
- for item in builtin_bridges["meek-azure"]:
- newline = item.replace("meek", "meek_lite")
- new_meek_bridges.append(
- f"{newline} url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"
- )
- builtin_bridges["meek-azure"] = new_meek_bridges
# Save the new settings
self.settings.set("bridges_builtin", builtin_bridges)
self.settings.save()
diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py
index 52e9c972..cab64681 100644
--- a/cli/onionshare_cli/settings.py
+++ b/cli/onionshare_cli/settings.py
@@ -103,6 +103,7 @@ class Settings(object):
"socket_file_path": "/var/run/tor/control",
"auth_type": "no_auth",
"auth_password": "",
+ "auto_connect": False,
"use_autoupdate": True,
"autoupdate_timestamp": None,
"bridges_enabled": False,
diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py
index e608298b..d690c98d 100644
--- a/cli/onionshare_cli/web/send_base_mode.py
+++ b/cli/onionshare_cli/web/send_base_mode.py
@@ -44,8 +44,9 @@ class SendBaseModeWeb:
self.download_filesize = None
self.zip_writer = None
- # Store the tempfile objects here, so when they're garbage collected the files are deleted
- self.gzip_files = []
+ # Create a temporary dir to store gzip files in
+ self.gzip_tmp_dir = tempfile.TemporaryDirectory(dir=self.common.build_tmp_dir())
+ self.gzip_counter = 0
# If autostop_sharing, only allow one download at a time
self.download_in_progress = False
@@ -193,15 +194,12 @@ class SendBaseModeWeb:
# gzip compress the individual file, if it hasn't already been compressed
if use_gzip:
if filesystem_path not in self.gzip_individual_files:
- self.gzip_files.append(
- tempfile.NamedTemporaryFile("wb+", dir=self.common.build_tmp_dir())
+ gzip_filename = os.path.join(
+ self.gzip_tmp_dir.name, str(self.gzip_counter)
)
- gzip_file = self.gzip_files[-1]
- self._gzip_compress(filesystem_path, gzip_file.name, 6, None)
- self.gzip_individual_files[filesystem_path] = gzip_file.name
-
- # Cleanup this temp file
- self.web.cleanup_tempfiles.append(gzip_file)
+ self.gzip_counter += 1
+ self._gzip_compress(filesystem_path, gzip_filename, 6, None)
+ self.gzip_individual_files[filesystem_path] = gzip_filename
file_to_download = self.gzip_individual_files[filesystem_path]
filesize = os.path.getsize(self.gzip_individual_files[filesystem_path])
diff --git a/cli/onionshare_cli/web/web.py b/cli/onionshare_cli/web/web.py
index 64844b5c..fdbed567 100644
--- a/cli/onionshare_cli/web/web.py
+++ b/cli/onionshare_cli/web/web.py
@@ -171,7 +171,6 @@ class Web:
self.socketio.init_app(self.app)
self.chat_mode = ChatModeWeb(self.common, self)
- self.cleanup_tempfiles = []
self.cleanup_tempdirs = []
def get_mode(self):
@@ -405,13 +404,8 @@ class Web:
"""
self.common.log("Web", "cleanup")
- # Close all of the tempfile.NamedTemporaryFile
- for file in self.cleanup_tempfiles:
- file.close()
-
# Clean up the tempfile.NamedTemporaryDirectory objects
for dir in self.cleanup_tempdirs:
dir.cleanup()
- self.cleanup_tempfiles = []
self.cleanup_tempdirs = []
diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py
index f370a674..ead4630b 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,
+ "auto_connect": False,
}
for key in settings_obj._settings:
# Skip locale, it will not always default to the same thing
diff --git a/cli/tests/test_cli_web.py b/cli/tests/test_cli_web.py
index f6076ef9..335c3a1a 100644
--- a/cli/tests/test_cli_web.py
+++ b/cli/tests/test_cli_web.py
@@ -50,7 +50,6 @@ def web_obj(temp_dir, common_obj, mode, num_files=0):
web = Web(common_obj, False, mode_settings, mode)
web.running = True
- web.cleanup_tempfiles == []
web.cleanup_tempdirs == []
web.app.testing = True
@@ -308,17 +307,13 @@ class TestWeb:
def test_cleanup(self, common_obj, temp_dir_1024):
web = web_obj(temp_dir_1024, common_obj, "share", 3)
- temp_file = tempfile.NamedTemporaryFile()
temp_dir = tempfile.TemporaryDirectory()
- web.cleanup_tempfiles = [temp_file]
web.cleanup_tempdirs = [temp_dir]
web.cleanup()
- assert os.path.exists(temp_file.name) is False
assert os.path.exists(temp_dir.name) is False
- assert web.cleanup_tempfiles == []
assert web.cleanup_tempdirs == []