From 5b4d77c3634c0c13ae8ab1f27be540260027d0a8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 19 Oct 2021 11:36:03 +1100 Subject: [PATCH] Refactor to CensorshipCircumvention and Meek classes. Use Meek domain fronting when requesting bridges in frontend --- cli/onionshare_cli/__init__.py | 13 ++ cli/onionshare_cli/censorship.py | 87 +++-------- cli/onionshare_cli/meek.py | 144 ++++++++++++++++++ desktop/src/onionshare/gui_common.py | 6 + desktop/src/onionshare/moat_dialog.py | 16 +- desktop/src/onionshare/tor_settings_dialog.py | 15 +- 6 files changed, 202 insertions(+), 79 deletions(-) create mode 100644 cli/onionshare_cli/meek.py diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index ddba332e..99992b25 100644 --- a/cli/onionshare_cli/__init__.py +++ b/cli/onionshare_cli/__init__.py @@ -28,6 +28,7 @@ from datetime import timedelta from .common import Common, CannotFindTor from .censorship import CensorshipCircumvention +from .meek import Meek, MeekNotRunning from .web import Web from .onion import TorErrorProtocolError, TorTooOldEphemeral, TorTooOldStealth, Onion from .onionshare import OnionShare @@ -284,6 +285,18 @@ def main(cwd=None): # Create the Web object web = Web(common, False, mode_settings, mode) + # Create the Meek object and start the meek client + meek = Meek(common) + meek.start() + + # Create the CensorshipCircumvention object to make + # API calls to Tor over Meek + censorship = CensorshipCircumvention(common, meek) + # Example: request recommended bridges, pretending to be from China, using + # domain fronting. + # censorship_recommended_settings = censorship.request_settings(country="cn") + # print(censorship_recommended_settings) + # Start the Onion object try: onion = Onion(common, use_tmp_dir=True) diff --git a/cli/onionshare_cli/censorship.py b/cli/onionshare_cli/censorship.py index 176f95e6..f84b1058 100644 --- a/cli/onionshare_cli/censorship.py +++ b/cli/onionshare_cli/censorship.py @@ -18,77 +18,30 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import requests -import subprocess + +from .meek import MeekNotRunning -class CensorshipCircumvention: +class CensorshipCircumvention(object): """ - The CensorShipCircumvention object contains methods to detect - and offer solutions to censorship when connecting to Tor. + Connect to the Tor Moat APIs to retrieve censorship + circumvention recommendations, over the Meek client. """ - def __init__(self, common): - + def __init__(self, common, meek, domain_fronting=True): + """ + Set up the CensorshipCircumvention object to hold + common and meek objects. + """ self.common = common + self.meek = meek 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() + # Bail out if we requested domain fronting but we can't use meek + if domain_fronting and not self.meek.meek_proxies: + raise MeekNotRunning() - 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): + def request_map(self, country=False): """ Retrieves the Circumvention map from Tor Project and store it locally for further look-ups if required. @@ -108,7 +61,7 @@ class CensorshipCircumvention: endpoint, json=data, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek_proxies, + proxies=self.meek.meek_proxies, ) if r.status_code != 200: self.common.log( @@ -130,7 +83,7 @@ class CensorshipCircumvention: return result - def censorship_obtain_settings(self, country=False, transports=False): + def request_settings(self, country=False, transports=False): """ Retrieves the Circumvention Settings from Tor Project, which will return recommended settings based on the country code of @@ -152,7 +105,7 @@ class CensorshipCircumvention: endpoint, json=data, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek_proxies, + proxies=self.meek.meek_proxies, ) if r.status_code != 200: self.common.log( @@ -185,7 +138,7 @@ class CensorshipCircumvention: return result - def censorship_obtain_builtin_bridges(self): + def request_builtin_bridges(self): """ Retrieves the list of built-in bridges from the Tor Project. """ @@ -193,7 +146,7 @@ class CensorshipCircumvention: r = requests.post( endpoint, headers={"Content-Type": "application/vnd.api+json"}, - proxies=self.meek_proxies, + proxies=self.meek.meek_proxies, ) if r.status_code != 200: self.common.log( diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py new file mode 100644 index 00000000..4fc42756 --- /dev/null +++ b/cli/onionshare_cli/meek.py @@ -0,0 +1,144 @@ +# -*- 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 subprocess +from queue import Queue, Empty +from threading import Thread + + +class Meek(object): + """ + The Meek object starts the meek-client as a subprocess. + This process is used to do domain-fronting to connect to + the Tor APIs for censorship circumvention and retrieving + bridges, before connecting to Tor. + """ + + def __init__(self, common): + """ + Set up the Meek object + """ + + self.common = common + self.common.log("Meek", "__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.snowflake_file_path, + self.meek_client_file_path, + ) = get_tor_paths() + + self.meek_proxies = {} + self.meek_url = "https://moat.torproject.org.global.prod.fastly.net/" + self.meek_front = "cdn.sstatic.net" + self.meek_env = { + "TOR_PT_MANAGED_TRANSPORT_VER": "1", + "TOR_PT_CLIENT_TRANSPORTS": "meek", + } + self.meek_host = "127.0.0.1" + self.meek_port = None + + def start(self): + """ + Start the Meek Client and populate the SOCKS proxies dict + for use with requests to the Tor Moat API. + """ + # Small method to read stdout from the subprocess. + # We use this to obtain the random port that Meek + # started on + def enqueue_output(out, queue): + for line in iter(out.readline, b""): + queue.put(line) + out.close() + + # Start the Meek Client as a subprocess. + + 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", + self.meek_url, + "--front", + self.meek_front, + ], + stdout=subprocess.PIPE, + startupinfo=startupinfo, + bufsize=1, + env=self.meek_env, + text=True, + ) + else: + self.meek_proc = subprocess.Popen( + [ + self.meek_client_file_path, + "--url", + self.meek_url, + "--front", + self.meek_front, + ], + stdout=subprocess.PIPE, + bufsize=1, + env=self.meek_env, + text=True, + ) + + # Queue up the stdout from the subprocess for polling later + q = Queue() + t = Thread(target=enqueue_output, args=(self.meek_proc.stdout, q)) + t.daemon = True # thread dies with the program + t.start() + + while True: + # read stdout without blocking + try: + line = q.get_nowait() + except Empty: + # no stdout yet? + pass + else: # we got stdout + 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}") + break + + if self.meek_port: + self.meek_proxies = { + "http": f"socks5h://{self.meek_host}:{self.meek_port}", + "https": f"socks5h://{self.meek_host}:{self.meek_port}", + } + else: + self.common.log("Meek", "start", "Could not obtain the meek port") + raise MeekNotRunning() + + +class MeekNotRunning(Exception): + """ + We were unable to start Meek or obtain the port + number it started on, in order to do domain fronting. + """ diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 0f1dd46e..019cf193 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -409,11 +409,13 @@ class GuiCommon: 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 tor_path = shutil.which("tor") 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") @@ -423,6 +425,7 @@ class GuiCommon: tor_path = os.path.join(base_path, "Tor", "tor.exe") obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe") snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-client.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.common.platform == "Darwin": @@ -430,6 +433,7 @@ class GuiCommon: tor_path = os.path.join(base_path, "tor") 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") tor_geo_ip_file_path = os.path.join(base_path, "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6") elif self.common.platform == "BSD": @@ -437,6 +441,7 @@ class GuiCommon: 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" snowflake_file_path = "/usr/local/bin/snowflake-client" return ( @@ -445,6 +450,7 @@ class GuiCommon: tor_geo_ipv6_file_path, obfs4proxy_file_path, snowflake_file_path, + meek_client_file_path, ) @staticmethod diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 2651736e..78a05482 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -34,13 +34,15 @@ class MoatDialog(QtWidgets.QDialog): got_bridges = QtCore.Signal(str) - def __init__(self, common): + def __init__(self, common, meek): super(MoatDialog, self).__init__() self.common = common self.common.log("MoatDialog", "__init__") + self.meek = meek + self.setModal(True) self.setWindowTitle(strings._("gui_settings_bridge_moat_button")) self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) @@ -108,7 +110,7 @@ class MoatDialog(QtWidgets.QDialog): self.submit_button.hide() # BridgeDB fetch - self.t_fetch = MoatThread(self.common, "fetch") + self.t_fetch = MoatThread(self.common, self.meek, "fetch") self.t_fetch.bridgedb_error.connect(self.bridgedb_error) self.t_fetch.captcha_ready.connect(self.captcha_ready) self.t_fetch.start() @@ -130,6 +132,7 @@ class MoatDialog(QtWidgets.QDialog): # BridgeDB check self.t_check = MoatThread( self.common, + self.meek, "check", {"challenge": self.challenge, "solution": self.solution_lineedit.text()}, ) @@ -209,17 +212,20 @@ class MoatThread(QtCore.QThread): captcha_ready = QtCore.Signal(str, str) bridges_ready = QtCore.Signal(str) - def __init__(self, common, action, data={}): + def __init__(self, common, meek, action, data={}): super(MoatThread, self).__init__() self.common = common self.common.log("MoatThread", "__init__", f"action={action}") + self.meek = meek self.transport = "obfs4" self.action = action self.data = data def run(self): - # TODO: Do all of this using domain fronting + + # Start Meek so that we can do domain fronting + self.meek.start() if self.action == "fetch": self.common.log("MoatThread", "run", f"starting fetch") @@ -228,6 +234,7 @@ class MoatThread(QtCore.QThread): r = requests.post( "https://bridges.torproject.org/moat/fetch", headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek.meek_proxies, json={ "data": [ { @@ -280,6 +287,7 @@ class MoatThread(QtCore.QThread): r = requests.post( "https://bridges.torproject.org/moat/check", headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek.meek_proxies, json={ "data": [ { diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index adad6931..e92be2aa 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -24,6 +24,7 @@ import platform import re import os +from onionshare_cli.meek import Meek from onionshare_cli.settings import Settings from onionshare_cli.onion import Onion @@ -48,6 +49,8 @@ class TorSettingsDialog(QtWidgets.QDialog): self.common.log("TorSettingsDialog", "__init__") + self.meek = Meek(common) + self.setModal(True) self.setWindowTitle(strings._("gui_tor_settings_window_title")) self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) @@ -78,6 +81,7 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path, self.snowflake_file_path, + self.meek_client_file_path, ) = self.common.gui.get_tor_paths() bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label")) @@ -497,7 +501,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") - moat_dialog = MoatDialog(self.common) + moat_dialog = MoatDialog(self.common, self.meek) moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges) moat_dialog.exec_() @@ -577,9 +581,7 @@ class TorSettingsDialog(QtWidgets.QDialog): return onion = Onion( - self.common, - use_tmp_dir=True, - get_tor_paths=self.common.gui.get_tor_paths, + self.common, use_tmp_dir=True, get_tor_paths=self.common.gui.get_tor_paths ) tor_con = TorConnectionDialog(self.common, settings, True, onion) @@ -781,10 +783,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Alert(self.common, strings._("gui_settings_moat_bridges_invalid")) return False - settings.set( - "tor_bridges_use_moat_bridges", - moat_bridges, - ) + settings.set("tor_bridges_use_moat_bridges", moat_bridges) settings.set("tor_bridges_use_custom_bridges", "")