From 8a19d8088ec62f1ea93743d6139b93a15e408629 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 18 Oct 2021 17:17:47 +1100 Subject: [PATCH] Move Censorship stuff into its own class. Early attempt at subprocessing out to meek (unfinished) --- cli/onionshare_cli/__init__.py | 17 +-- cli/onionshare_cli/censorship.py | 216 +++++++++++++++++++++++++++++++ cli/onionshare_cli/common.py | 73 +---------- cli/onionshare_cli/onion.py | 1 + 4 files changed, 226 insertions(+), 81 deletions(-) create mode 100644 cli/onionshare_cli/censorship.py diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index 4bc00929..ddba332e 100644 --- a/cli/onionshare_cli/__init__.py +++ b/cli/onionshare_cli/__init__.py @@ -27,13 +27,9 @@ from datetime import datetime from datetime import timedelta from .common import Common, CannotFindTor +from .censorship import CensorshipCircumvention from .web import Web -from .onion import ( - TorErrorProtocolError, - TorTooOldEphemeral, - TorTooOldStealth, - Onion, -) +from .onion import TorErrorProtocolError, TorTooOldEphemeral, TorTooOldStealth, Onion from .onionshare import OnionShare from .mode_settings import ModeSettings @@ -94,12 +90,7 @@ def main(cwd=None): help="Filename of persistent session", ) # General args - parser.add_argument( - "--title", - metavar="TITLE", - default=None, - help="Set a title", - ) + parser.add_argument("--title", metavar="TITLE", default=None, help="Set a title") parser.add_argument( "--public", action="store_true", @@ -409,7 +400,7 @@ def main(cwd=None): sys.exit(1) # Warn about sending large files over Tor - if web.share_mode.download_filesize >= 157286400: # 150mb + if web.share_mode.download_filesize >= 157_286_400: # 150mb print("") print("Warning: Sending a large share could take hours") print("") diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py new file mode 100644 index 00000000..176f95e6 --- /dev/null +++ b/cli/onionshare_cli/censorship.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2021 Micah Lee, et al. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import requests +import subprocess + + +class CensorshipCircumvention: + """ + The CensorShipCircumvention object contains methods to detect + and offer solutions to censorship when connecting to Tor. + """ + + def __init__(self, common): + + self.common = common + self.common.log("CensorshipCircumvention", "__init__") + + get_tor_paths = self.common.get_tor_paths + ( + self.tor_path, + self.tor_geo_ip_file_path, + self.tor_geo_ipv6_file_path, + self.obfs4proxy_file_path, + self.meek_client_file_path, + ) = get_tor_paths() + + meek_url = "https://moat.torproject.org.global.prod.fastly.net/" + meek_front = "cdn.sstatic.net" + meek_env = { + "TOR_PT_MANAGED_TRANSPORT_VER": "1", + "TOR_PT_CLIENT_TRANSPORTS": "meek", + } + + # @TODO detect the port from the subprocess output + meek_address = "127.0.0.1" + meek_port = "43533" # hardcoded for testing + self.meek_proxies = { + "http": f"socks5h://{meek_address}:{meek_port}", + "https": f"socks5h://{meek_address}:{meek_port}", + } + + # Start the Meek Client as a subprocess. + # This will be used to do domain fronting to the Tor + # Moat API endpoints for censorship circumvention as + # well as BridgeDB lookups. + + if self.common.platform == "Windows": + # In Windows, hide console window when opening tor.exe subprocess + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + self.meek_proc = subprocess.Popen( + [self.meek_client_file_path, "--url", meek_url, "--front", meek_front], + stdout=subprocess.PIPE, + startupinfo=startupinfo, + bufsize=1, + env=meek_env, + text=True, + ) + else: + self.meek_proc = subprocess.Popen( + [self.meek_client_file_path, "--url", meek_url, "--front", meek_front], + stdout=subprocess.PIPE, + bufsize=1, + env=meek_env, + text=True, + ) + + # 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("CensorshipCircumvention", "__init__", f"Meek host is {self.meek_host}") + # self.common.log("CensorshipCircumvention", "__init__", f"Meek port is {self.meek_port}") + + def censorship_obtain_map(self, country=False): + """ + Retrieves the Circumvention map from Tor Project and store it + locally for further look-ups if required. + + Optionally pass a country code in order to get recommended settings + just for that country. + + Note that this API endpoint doesn't return actual bridges, + it just returns the recommended bridge type countries. + """ + endpoint = "https://bridges.torproject.org/moat/circumvention/map" + data = {} + if country: + data = {"country": country} + + r = requests.post( + endpoint, + json=data, + headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek_proxies, + ) + if r.status_code != 200: + self.common.log( + "CensorshipCircumvention", + "censorship_obtain_map", + f"status_code={r.status_code}", + ) + return False + + result = r.json() + + if "errors" in result: + self.common.log( + "CensorshipCircumvention", + "censorship_obtain_map", + f"errors={result['errors']}", + ) + return False + + return result + + def censorship_obtain_settings(self, country=False, transports=False): + """ + Retrieves the Circumvention Settings from Tor Project, which + will return recommended settings based on the country code of + the requesting IP. + + Optionally, a country code can be specified in order to override + the IP detection. + + Optionally, a list of transports can be specified in order to + return recommended settings for just that transport type. + """ + 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.meek_proxies, + ) + if r.status_code != 200: + self.common.log( + "CensorshipCircumvention", + "censorship_obtain_settings", + f"status_code={r.status_code}", + ) + return False + + result = r.json() + + 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 + + return result + + def censorship_obtain_builtin_bridges(self): + """ + Retrieves the list of built-in bridges from the Tor Project. + """ + endpoint = "https://bridges.torproject.org/moat/circumvention/builtin" + r = requests.post( + endpoint, + headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek_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 diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index 195de2fe..549b1c21 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -314,6 +314,7 @@ class Common: if not tor_path: raise CannotFindTor() obfs4proxy_file_path = shutil.which("obfs4proxy") + 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") @@ -321,6 +322,7 @@ class Common: base_path = self.get_resource_path("tor") tor_path = os.path.join(base_path, "Tor", "tor.exe") obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe") + meek_client_file_path = os.path.join(base_path, "Tor", "meek-client.exe") 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": @@ -328,6 +330,7 @@ class Common: if not tor_path: raise CannotFindTor() obfs4proxy_file_path = shutil.which("obfs4proxy") + 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") @@ -336,12 +339,14 @@ class Common: tor_geo_ip_file_path = "/usr/local/share/tor/geoip" tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6" obfs4proxy_file_path = "/usr/local/bin/obfs4proxy" + meek_client_file_path = "/usr/local/bin/meek-client" return ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + meek_client_file_path, ) def build_data_dir(self): @@ -505,74 +510,6 @@ class Common: total_size += os.path.getsize(fp) return total_size - def censorship_obtain_map(self): - """ - Retrieves the Circumvention map from Tor Project and store it - locally for further look-ups if required. - """ - endpoint = "https://bridges.torproject.org/moat/circumvention/map" - # @TODO this needs to be using domain fronting to defeat censorship - # of the lookup itself. - response = requests.get(endpoint) - self.censorship_map = response.json() - self.log("Common", "censorship_obtain_map", self.censorship_map) - - def censorship_obtain_settings_from_api(self): - """ - Retrieves the Circumvention Settings from Tor Project, which - will return recommended settings based on the country code of - the requesting IP. - """ - endpoint = "https://bridges.torproject.org/moat/circumvention/settings" - # @TODO this needs to be using domain fronting to defeat censorship - # of the lookup itself. - response = requests.get(endpoint) - self.censorship_settings = response.json() - self.log( - "Common", "censorship_obtain_settings_from_api", self.censorship_settings - ) - - def censorship_obtain_settings_from_map(self, country): - """ - Retrieves the Circumvention Settings for this country from the - circumvention map we have stored locally, rather than from the - API endpoint. - - This is for when the user has specified the country themselves - rather than requesting auto-detection. - """ - try: - # Fetch the map. - self.censorship_obtain_map() - self.censorship_settings = self.censorship_map[country] - self.log( - "Common", - "censorship_obtain_settings_from_map", - f"Settings are {self.censorship_settings}", - ) - except KeyError: - self.log( - "Common", - "censorship_obtain_settings_from_map", - "No censorship settings found for this country", - ) - return False - - def censorship_obtain_builtin_bridges(self): - """ - Retrieves the list of built-in bridges from the Tor Project. - """ - endpoint = "https://bridges.torproject.org/moat/circumvention/builtin" - # @TODO this needs to be using domain fronting to defeat censorship - # of the lookup itself. - response = requests.get(endpoint) - self.censorship_builtin_bridges = response.json() - self.log( - "Common", - "censorship_obtain_builtin_bridges", - self.censorship_builtin_bridges, - ) - class AutoStopTimer(threading.Thread): """ diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 7f6faa17..aa5e276b 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -153,6 +153,7 @@ class Onion(object): self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path, + self.meek_client_file_path, ) = get_tor_paths() # The tor process