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", "")