From 3e61e8dd37d768cba35d7b7f4bc97f872d24aaef Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 11 Oct 2021 20:17:26 -0700 Subject: [PATCH 01/70] Write Linux script to download Tor Browser and extract binaries --- desktop/README.md | 14 +++- desktop/scripts/get-tor-linux.py | 128 +++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100755 desktop/scripts/get-tor-linux.py diff --git a/desktop/README.md b/desktop/README.md index 4a59fe03..c8c51519 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -13,9 +13,19 @@ cd onionshare/desktop #### Linux -If you're using Linux, install `tor` and `obfs4proxy` from either the [official Debian repository](https://support.torproject.org/apt/tor-deb-repo/), or from your package manager. +In Ubuntu 20.04 you need the `libxcb-xinerama0` package installed. -In Ubuntu 20.04 you also need the `libxcb-xinerama0` package installed. +Install python dependencies: + +```sh +pip3 install --user poetry requests +``` + +Download Tor Browser and extract the binaries: + +```sh +./scripts/get-tor-linux.py +``` #### macOS diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py new file mode 100755 index 00000000..4bd9ff13 --- /dev/null +++ b/desktop/scripts/get-tor-linux.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- 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 . +""" + +""" +This script downloads a pre-built tor binary to bundle with OnionShare. +In order to avoid a Mac gnupg dependency, I manually verify the signature +and hard-code the sha256 hash. +""" +import inspect +import os +import sys +import hashlib +import shutil +import subprocess +import requests + + +def main(): + tarball_url = "https://dist.torproject.org/torbrowser/11.0a7/tor-browser-linux64-11.0a7_en-US.tar.xz" + tarball_filename = "tor-browser-linux64-11.0a7_en-US.tar.xz" + expected_tarball_sha256 = ( + "bc9861c692f899fe0344c960dc615ff0e275cf74c61066c8735c88e3ddc2b623" + ) + + # Build paths + root_path = os.path.dirname( + os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + ) + working_path = os.path.join(root_path, "build", "tor") + tarball_path = os.path.join(working_path, tarball_filename) + + # Make sure the dist path exists + dist_path = os.path.join(working_path, "dist") + if not os.path.exists(dist_path): + os.makedirs(dist_path, exist_ok=True) + + # Make sure the tarball is downloaded + if not os.path.exists(tarball_path): + print("Downloading {}".format(tarball_url)) + r = requests.get(tarball_url) + open(tarball_path, "wb").write(r.content) + tarball_sha256 = hashlib.sha256(r.content).hexdigest() + else: + tarball_data = open(tarball_path, "rb").read() + tarball_sha256 = hashlib.sha256(tarball_data).hexdigest() + + # Compare the hash + if tarball_sha256 != expected_tarball_sha256: + print("ERROR! The sha256 doesn't match:") + print("expected: {}".format(expected_tarball_sha256)) + print(" actual: {}".format(tarball_sha256)) + sys.exit(-1) + + # Delete extracted tarball, if it's there + shutil.rmtree(os.path.join(working_path, "tor-browser_en-US"), ignore_errors=True) + + # Extract the tarball + subprocess.call(["tar", "-xvf", tarball_path], cwd=working_path) + tarball_tor_path = os.path.join( + working_path, "tor-browser_en-US", "Browser", "TorBrowser" + ) + + # Copy into dist + shutil.copyfile( + os.path.join(tarball_tor_path, "Data", "Tor", "geoip"), + os.path.join(dist_path, "geoip"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Data", "Tor", "geoip6"), + os.path.join(dist_path, "geoip6"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "tor"), + os.path.join(dist_path, "tor"), + ) + os.chmod(os.path.join(dist_path, "tor"), 0o755) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "libcrypto.so.1.1"), + os.path.join(dist_path, "libcrypto.so.1.1"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "libevent-2.1.so.7"), + os.path.join(dist_path, "libevent-2.1.so.7"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "libssl.so.1.1"), + os.path.join(dist_path, "libssl.so.1.1"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "libstdc++", "libstdc++.so.6"), + os.path.join(dist_path, "libstdc++.so.6"), + ) + shutil.copyfile( + os.path.join(tarball_tor_path, "Tor", "PluggableTransports", "obfs4proxy"), + os.path.join(dist_path, "obfs4proxy"), + ) + os.chmod(os.path.join(dist_path, "obfs4proxy"), 0o755) + shutil.copyfile( + os.path.join( + tarball_tor_path, "Tor", "PluggableTransports", "snowflake-client" + ), + os.path.join(dist_path, "snowflake-client"), + ) + os.chmod(os.path.join(dist_path, "snowflake-client"), 0o755) + + print(f"Tor binaries extracted to: {dist_path}") + + +if __name__ == "__main__": + main() From 343a8cccc13319a8014ad7c12e49a5db52014b54 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 11 Oct 2021 20:44:42 -0700 Subject: [PATCH 02/70] Update dependencies --- cli/poetry.lock | 536 ++++++++++++++++++++++++------------------------ 1 file changed, 263 insertions(+), 273 deletions(-) diff --git a/cli/poetry.lock b/cli/poetry.lock index c51e1d62..9314b096 100644 --- a/cli/poetry.lock +++ b/cli/poetry.lock @@ -1,122 +1,120 @@ [[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" [[package]] -category = "dev" -description = "Classes Without Boilerplate" name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "21.2.0" [package.extras] -dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] -category = "main" -description = "The bidirectional mapping library for Python." name = "bidict" +version = "0.21.3" +description = "The bidirectional mapping library for Python." +category = "main" optional = false python-versions = ">=3.6" -version = "0.21.2" - -[package.extras] -coverage = ["coverage (<6)", "pytest-cov (<3)"] -dev = ["setuptools-scm", "hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)", "coverage (<6)", "pytest-cov (<3)", "pre-commit (<3)", "tox (<4)"] -docs = ["Sphinx (<4)", "sphinx-autodoc-typehints (<2)"] -precommit = ["pre-commit (<3)"] -test = ["hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)"] [[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle." name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = "*" -version = "2021.5.30" [[package]] -category = "main" -description = "Foreign Function Interface for Python calling C code." name = "cffi" +version = "1.14.6" +description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" -version = "1.14.6" [package.dependencies] pycparser = "*" [[package]] +name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" -description = "Universal encoding detector for Python 2 and 3" -name = "chardet" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.0.0" - -[[package]] -category = "main" -description = "Composable command line interface toolkit" -name = "click" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" - -[[package]] -category = "main" -description = "Cross-platform colored terminal text." -name = "colorama" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.4" - -[[package]] -category = "main" -description = "DNS toolkit" -name = "dnspython" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.16.0" +python-versions = ">=3.5.0" [package.extras] -DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] -IDNA = ["idna (>=2.1)"] +unicode_backport = ["unicodedata2"] [[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" category = "main" -description = "Highly concurrent networking library" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + +[[package]] name = "eventlet" +version = "0.32.0" +description = "Highly concurrent networking library" +category = "main" optional = false python-versions = "*" -version = "0.31.0" [package.dependencies] -dnspython = ">=1.15.0,<2.0.0" +dnspython = ">=1.15.0" greenlet = ">=0.3" six = ">=1.10.0" [[package]] -category = "main" -description = "A simple framework for building complex web applications." name = "flask" +version = "1.1.4" +description = "A simple framework for building complex web applications." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.1.4" [package.dependencies] -Jinja2 = ">=2.10.1,<3.0" -Werkzeug = ">=0.15,<2.0" click = ">=5.1,<8.0" itsdangerous = ">=0.24,<2.0" +Jinja2 = ">=2.10.1,<3.0" +Werkzeug = ">=0.15,<2.0" [package.extras] dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] @@ -124,79 +122,76 @@ docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx- dotenv = ["python-dotenv"] [[package]] -category = "main" -description = "Socket.IO integration for Flask applications" name = "flask-socketio" +version = "5.0.1" +description = "Socket.IO integration for Flask applications" +category = "main" optional = false python-versions = "*" -version = "5.0.1" [package.dependencies] Flask = ">=0.9" python-socketio = ">=5.0.2" [[package]] -category = "main" -description = "Lightweight in-process concurrent programming" name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "1.1.0" [package.extras] docs = ["sphinx"] [[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" +python-versions = ">=3.5" [[package]] -category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = ">=3.6" -version = "4.4.0" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" -[package.dependencies.typing-extensions] -python = "<3.8" -version = ">=3.6.4" - [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] -category = "dev" -description = "iniconfig: brain-dead simple config-ini parsing" name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = "*" -version = "1.1.1" [[package]] -category = "main" -description = "Various helpers to pass data to untrusted environments and back." name = "itsdangerous" +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.0" [[package]] -category = "main" -description = "A very fast and expressive template engine." name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.3" [package.dependencies] MarkupSafe = ">=0.23" @@ -205,74 +200,73 @@ MarkupSafe = ">=0.23" i18n = ["Babel (>=0.8)"] [[package]] -category = "main" -description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.6" -version = "2.0.1" [[package]] -category = "dev" -description = "Core utilities for Python packages" name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.9" +python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" +python-versions = ">=3.6" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] -category = "main" -description = "Cross-platform lib for process and system monitoring in Python." name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.8.0" [package.extras] test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -category = "main" -description = "C parser in Python" name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.20" [[package]] -category = "main" -description = "Python binding to the Networking and Cryptography (NaCl) library" name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" [package.dependencies] cffi = ">=1.4.1" @@ -280,186 +274,181 @@ six = "*" [package.extras] docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] -category = "dev" -description = "Python parsing module" name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" [[package]] -category = "main" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7.1" [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.6" -version = "6.2.4" [package.dependencies] -atomicwrites = ">=1.0" +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" -colorama = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] -category = "main" -description = "Engine.IO server" name = "python-engineio" +version = "4.2.1" +description = "Engine.IO server and client for Python" +category = "main" optional = false -python-versions = "*" -version = "4.2.0" +python-versions = ">=3.6" [package.extras] asyncio_client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] -category = "main" -description = "Socket.IO server" name = "python-socketio" +version = "5.4.0" +description = "Socket.IO server and client for Python" +category = "main" optional = false -python-versions = "*" -version = "5.3.0" +python-versions = ">=3.6" [package.dependencies] bidict = ">=0.21.0" python-engineio = ">=4.1.0" [package.extras] -asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"] +asyncio_client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] -category = "main" -description = "Python HTTP for Humans." name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.25.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<1.27" -[package.dependencies.PySocks] -optional = true -version = ">=1.5.6,<1.5.7 || >1.5.7" - [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.16.0" [[package]] -category = "main" -description = "" name = "stem" +version = "1.8.1" +description = "Stem is a Python controller library that allows applications to interact with Tor (https://www.torproject.org/)." +category = "main" optional = false python-versions = "*" -version = "1.8.1" +develop = false [package.source] -reference = "de3d03a03c7ee57c74c80e9c63cb88072d833717" type = "git" url = "https://github.com/onionshare/stem.git" +reference = "1.8.1" +resolved_reference = "de3d03a03c7ee57c74c80e9c63cb88072d833717" [[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.10.2" [[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" -marker = "python_version < \"3.8\"" name = "typing-extensions" +version = "3.10.0.2" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" optional = false python-versions = "*" -version = "3.10.0.0" [[package]] -category = "main" -description = "ASCII transliterations of Unicode text" name = "unidecode" +version = "1.3.2" +description = "ASCII transliterations of Unicode text" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.2.0" +python-versions = ">=3.5" [[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.26.5" [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] -category = "main" -description = "The comprehensive WSGI web application library." name = "werkzeug" +version = "1.0.1" +description = "The comprehensive WSGI web application library." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.0.1" [package.extras] dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] [[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.4.1" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] -content-hash = "181891640e59dac730905019444d42ef8e99da0c34c96fb8a616781661bae537" +lock-version = "1.1" python-versions = "^3.6" +content-hash = "181891640e59dac730905019444d42ef8e99da0c34c96fb8a616781661bae537" [metadata.files] atomicwrites = [ @@ -471,12 +460,12 @@ attrs = [ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] bidict = [ - {file = "bidict-0.21.2-py2.py3-none-any.whl", hash = "sha256:929d056e8d0d9b17ceda20ba5b24ac388e2a4d39802b87f9f4d3f45ecba070bf"}, - {file = "bidict-0.21.2.tar.gz", hash = "sha256:4fa46f7ff96dc244abfc437383d987404ae861df797e2fd5b190e233c302be09"}, + {file = "bidict-0.21.3-py3-none-any.whl", hash = "sha256:2cce0d01eb3db9b3fa85db501c00aaa3389ee4cab7ef82178604552dfa943a1b"}, + {file = "bidict-0.21.3.tar.gz", hash = "sha256:d50bd81fae75e34198ffc94979a0eb0939ff9adb3ef32bcc93a913d8b3e3ed1d"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, @@ -525,9 +514,9 @@ cffi = [ {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -538,12 +527,12 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] dnspython = [ - {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, - {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, + {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] eventlet = [ - {file = "eventlet-0.31.0-py2.py3-none-any.whl", hash = "sha256:27ae41fad9deed9bbf4166f3e3b65acc15d524d42210a518e5877da85a6b8c5d"}, - {file = "eventlet-0.31.0.tar.gz", hash = "sha256:b36ec2ecc003de87fc87b93197d77fea528aa0f9204a34fdf3b2f8d0f01e017b"}, + {file = "eventlet-0.32.0-py2.py3-none-any.whl", hash = "sha256:a3a67b02f336e97a1894b277bc33b695831525758781eb024f4da00e75ce5e25"}, + {file = "eventlet-0.32.0.tar.gz", hash = "sha256:2f0bb8ed0dc0ab21d683975d5d8ab3c054d588ce61def9faf7a465ee363e839b"}, ] flask = [ {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, @@ -554,63 +543,64 @@ flask-socketio = [ {file = "Flask_SocketIO-5.0.1-py2.py3-none-any.whl", hash = "sha256:5d9a4438bafd806c5a3b832e74b69758781a8ee26fb6c9b1dbdda9b4fced432e"}, ] greenlet = [ - {file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"}, - {file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"}, - {file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"}, - {file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"}, - {file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"}, - {file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"}, - {file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"}, - {file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"}, - {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"}, - {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"}, - {file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"}, - {file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"}, - {file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"}, - {file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"}, - {file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"}, - {file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"}, - {file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"}, - {file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"}, - {file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"}, - {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"}, - {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"}, - {file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"}, - {file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"}, - {file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"}, - {file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"}, - {file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"}, - {file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"}, - {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"}, - {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"}, - {file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"}, - {file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"}, - {file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"}, - {file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"}, - {file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"}, - {file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"}, - {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"}, - {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"}, - {file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"}, - {file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"}, - {file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"}, - {file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"}, - {file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"}, - {file = "greenlet-1.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e"}, - {file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959"}, - {file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"}, - {file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832"}, - {file = "greenlet-1.1.0-cp39-cp39-win32.whl", hash = "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11"}, - {file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535"}, - {file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee"}, + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, - {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"}, + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -681,12 +671,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -756,20 +746,20 @@ pysocks = [ {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] python-engineio = [ - {file = "python-engineio-4.2.0.tar.gz", hash = "sha256:4e97c1189c23923858f5bb6dc47cfcd915005383c3c039ff01c89f2c00d62077"}, - {file = "python_engineio-4.2.0-py2.py3-none-any.whl", hash = "sha256:c6c119c2039fcb6f64d260211ca92c0c61b2b888a28678732a961f2aaebcc848"}, + {file = "python-engineio-4.2.1.tar.gz", hash = "sha256:d510329b6d8ed5662547862f58bc73659ae62defa66b66d745ba021de112fa62"}, + {file = "python_engineio-4.2.1-py3-none-any.whl", hash = "sha256:f3ef9a2c048d08990f294c5f8991f6f162c3b12ecbd368baa0d90441de907d1c"}, ] python-socketio = [ - {file = "python-socketio-5.3.0.tar.gz", hash = "sha256:3dcc9785aaeef3a9eeb36c3818095662342744bdcdabd050fe697cdb826a1c2b"}, - {file = "python_socketio-5.3.0-py2.py3-none-any.whl", hash = "sha256:d74314fd4241342c8a55c4f66d5cfea8f1a8fffd157af216c67e1c3a649a2444"}, + {file = "python-socketio-5.4.0.tar.gz", hash = "sha256:ca807c9e1f168e96dea412d64dd834fb47c470d27fd83da0504aa4b248ba2544"}, + {file = "python_socketio-5.4.0-py3-none-any.whl", hash = "sha256:7ed57f6c024abdfeb9b25c74c0c00ffc18da47d903e8d72deecb87584370d1fc"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -781,23 +771,23 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] unidecode = [ - {file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"}, - {file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"}, + {file = "Unidecode-1.3.2-py3-none-any.whl", hash = "sha256:215fe33c9d1c889fa823ccb66df91b02524eb8cc8c9c80f9c5b8129754d27829"}, + {file = "Unidecode-1.3.2.tar.gz", hash = "sha256:669898c1528912bcf07f9819dc60df18d057f7528271e31f8ec28cc88ef27504"}, ] urllib3 = [ - {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, - {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] From 229da0aaab94ee84e8a0daf6d23251279a639f32 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 11 Oct 2021 20:45:28 -0700 Subject: [PATCH 03/70] Make get_tor_paths work properly now that in linux the tor binaries are bundled too --- cli/onionshare_cli/common.py | 53 ++++++++++++++++++++-------- cli/onionshare_cli/onion.py | 21 +++++++---- desktop/scripts/get-tor-linux.py | 7 ++-- desktop/scripts/rebuild-cli.py | 1 + desktop/src/onionshare/gui_common.py | 23 ++++++------ 5 files changed, 72 insertions(+), 33 deletions(-) diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index dd92eb0b..78da8882 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -309,38 +309,63 @@ class Common: def get_tor_paths(self): if self.platform == "Linux": - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - 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") + # Look in resources first + base_path = self.get_resource_path("tor") + if os.path.exists(base_path): + tor_path = os.path.join(base_path, "tor") + tor_geo_ip_file_path = os.path.join(base_path, "geoip") + 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") + else: + # Fallback to looking in the path + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-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") elif self.platform == "Windows": 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") + snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-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": - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - 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") + # Look in resources first + base_path = self.get_resource_path("tor") + if os.path.exists(base_path): + tor_path = os.path.join(base_path, "tor") + tor_geo_ip_file_path = os.path.join(base_path, "geoip") + 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") + else: + # Fallback to looking in the path + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-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") elif self.platform == "BSD": tor_path = "/usr/local/bin/tor" 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" + snowflake_file_path = "/usr/local/bin/snowflake-client" return ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, ) def build_data_dir(self): diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 7f6faa17..a0f967b9 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.snowflake_file_path, ) = get_tor_paths() # The tor process @@ -178,10 +179,10 @@ class Onion(object): key_bytes = bytes(key) key_b32 = base64.b32encode(key_bytes) # strip trailing ==== - assert key_b32[-4:] == b'====' + assert key_b32[-4:] == b"====" key_b32 = key_b32[:-4] # change from b'ASDF' to ASDF - s = key_b32.decode('utf-8') + s = key_b32.decode("utf-8") return s def connect( @@ -650,16 +651,24 @@ class Onion(object): ) raise TorTooOldStealth() else: - if key_type == "NEW" or not mode_settings.get("onion", "client_auth_priv_key"): + if key_type == "NEW" or not mode_settings.get( + "onion", "client_auth_priv_key" + ): # Generate a new key pair for Client Auth on new onions, or if # it's a persistent onion but for some reason we don't them client_auth_priv_key_raw = nacl.public.PrivateKey.generate() client_auth_priv_key = self.key_str(client_auth_priv_key_raw) - client_auth_pub_key = self.key_str(client_auth_priv_key_raw.public_key) + client_auth_pub_key = self.key_str( + client_auth_priv_key_raw.public_key + ) else: # These should have been saved in settings from the previous run of a persistent onion - client_auth_priv_key = mode_settings.get("onion", "client_auth_priv_key") - client_auth_pub_key = mode_settings.get("onion", "client_auth_pub_key") + client_auth_priv_key = mode_settings.get( + "onion", "client_auth_priv_key" + ) + client_auth_pub_key = mode_settings.get( + "onion", "client_auth_pub_key" + ) try: if not self.supports_stealth: diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py index 4bd9ff13..e47ae03d 100755 --- a/desktop/scripts/get-tor-linux.py +++ b/desktop/scripts/get-tor-linux.py @@ -46,9 +46,12 @@ def main(): ) working_path = os.path.join(root_path, "build", "tor") tarball_path = os.path.join(working_path, tarball_filename) + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + + # Make sure dirs exist + if not os.path.exists(working_path): + os.makedirs(working_path, exist_ok=True) - # Make sure the dist path exists - dist_path = os.path.join(working_path, "dist") if not os.path.exists(dist_path): os.makedirs(dist_path, exist_ok=True) diff --git a/desktop/scripts/rebuild-cli.py b/desktop/scripts/rebuild-cli.py index 66582cf1..f9a43554 100755 --- a/desktop/scripts/rebuild-cli.py +++ b/desktop/scripts/rebuild-cli.py @@ -38,6 +38,7 @@ def main(): # Reinstall the new wheel subprocess.call(["pip", "uninstall", "onionshare-cli", "-y"]) subprocess.call(["pip", "install", os.path.join(desktop_path, wheel_basename)]) + subprocess.call(["pip", "install", "typing-extensions"]) if __name__ == "__main__": diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 182d63f2..1dffab26 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -205,14 +205,14 @@ class GuiCommon: "downloads_uploads_not_empty": """ QWidget{ background-color: """ - + history_background_color - +"""; + + history_background_color + + """; }""", "downloads_uploads_empty": """ QWidget { background-color: """ - + history_background_color - +"""; + + history_background_color + + """; border: 1px solid #999999; } QWidget QLabel { @@ -263,7 +263,7 @@ class GuiCommon: + """; width: 10px; }""", - "history_default_label" : """ + "history_default_label": """ QLabel { color: """ + history_label_color @@ -415,21 +415,20 @@ class GuiCommon: def get_tor_paths(self): if self.common.platform == "Linux": - tor_path = shutil.which("tor") - obfs4proxy_file_path = shutil.which("obfs4proxy") - 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") - elif self.common.platform == "Windows": + return self.common.get_tor_paths() + + if self.common.platform == "Windows": 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") + snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-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": base_path = self.get_resource_path("tor") 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") 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,12 +436,14 @@ 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" + snowflake_file_path = "/usr/local/bin/snowflake-client" return ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, ) @staticmethod From 76659bdc6e8c54a014287a1bdb9f0716fea6203f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 12 Oct 2021 21:09:58 -0700 Subject: [PATCH 04/70] Start splitting settings into normal settings and Tor settings --- desktop/src/onionshare/main_window.py | 29 + .../resources/images/dark_tor_settings.png | Bin 0 -> 3600 bytes .../resources/images/light_tor_settings.png | Bin 0 -> 1503 bytes .../src/onionshare/resources/locale/en.json | 1 + desktop/src/onionshare/settings_dialog.py | 28 +- desktop/src/onionshare/tor_settings_dialog.py | 1112 +++++++++++++++++ 6 files changed, 1154 insertions(+), 16 deletions(-) create mode 100644 desktop/src/onionshare/resources/images/dark_tor_settings.png create mode 100644 desktop/src/onionshare/resources/images/light_tor_settings.png create mode 100644 desktop/src/onionshare/tor_settings_dialog.py diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index d87092b6..0e7e3f88 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -18,11 +18,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import time from PySide2 import QtCore, QtWidgets, QtGui from . import strings from .tor_connection_dialog import TorConnectionDialog +from .tor_settings_dialog import TorSettingsDialog from .settings_dialog import SettingsDialog from .widgets import Alert from .update_checker import UpdateThread @@ -106,6 +108,24 @@ class MainWindow(QtWidgets.QMainWindow): ) self.status_bar.addPermanentWidget(self.status_bar.server_status_indicator) + # Tor settings button + self.tor_settings_button = QtWidgets.QPushButton() + self.tor_settings_button.setDefault(False) + self.tor_settings_button.setFixedSize(40, 50) + self.tor_settings_button.setIcon( + QtGui.QIcon( + GuiCommon.get_resource_path( + "images/{}_tor_settings.png".format(self.common.gui.color_mode) + ) + ) + ) + self.tor_settings_button.clicked.connect(self.open_tor_settings) + self.tor_settings_button.setStyleSheet(self.common.gui.css["settings_button"]) + self.status_bar.addPermanentWidget(self.tor_settings_button) + + if os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1": + self.tor_settings_button.hide() + # Settings button self.settings_button = QtWidgets.QPushButton() self.settings_button.setDefault(False) @@ -223,6 +243,15 @@ class MainWindow(QtWidgets.QMainWindow): # Wait 1ms for the event loop to finish closing the TorConnectionDialog QtCore.QTimer.singleShot(1, self.open_settings) + def open_tor_settings(self): + """ + Open the TorSettingsDialog. + """ + self.common.log("MainWindow", "open_tor_settings") + d = TorSettingsDialog(self.common) + d.settings_saved.connect(self.settings_have_changed) + d.exec_() + def open_settings(self): """ Open the SettingsDialog. diff --git a/desktop/src/onionshare/resources/images/dark_tor_settings.png b/desktop/src/onionshare/resources/images/dark_tor_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..0b44bd95e6ddd5b7075c585787c028efff9f3742 GIT binary patch literal 3600 zcmV+r4)5`aP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O?T!mK(bbh5vIESpsK}%i(xVc97-g151*n9{gl? z(z3)-RjCNx3tRxSS^xR>Hvi%;l$cFSQgY4N@)v8YzHw0O^;gfQv+;ahU*heWdp&O+ z7d)o|W4OLbyWQV7pML$|K8NeC=S{g?@#Xd)_jvI63p%qNuV*7U@2~TBLrlHhkZX}= zQ~T{f<8E>J^^Om-u0g+>mlKi)culbqa`G7HSMa=#1wr2~G~V`ndhT$caGZiO3_jc~ z0g#vF-A8Mm0eS)Xeq=tP|2X;pzHhhl;RBZWh7ku}UU2Ed`S6&=KTjMU7Wvl;W83*@ zIk)%Tv+TXD)wP)MyL|Jg1Gc+Ah8sJOhx4<{OL!-a<-97dVw)XMKJBob*KpN&(BC+u zo36R-))|Kzm>B)?!sz{gC_Pt#`07Vco}#|o_7ZBCu;#PxWdfS zajSPc48p&EncrRflb6|hC`8Vd2UghS)vqx_nbS}1f)KZFyrl#1{dJ??KKYM2bL2LK zxv;=y`}K&P+%Mk>hv&e3iSY^{XJprIeGTVp2^l zha7XtIZIZNYZ4?%k}M)cs801+2JSQBNFxs!Wz^B8pJB$CCPFdGtg|g=7AmpgN-Hl}W!2R-zP7`TJMFw> zmtA*zuy$kh)9Vjdb2rxfVM_0l2Wy-)BKJo)m6N2L!I+N@#zh%W1yjy!aWQ&jPC2vH z6BNl~WKwR-bjlbM#^rq4?!nzJ=HBAXr1BPT{-2mLO5Oj0IRkZ{dHaI3sd;W3#;zz- zOl=_gxV}m=zBEm0?XSn);cSn%aWXH0P?S!!A|N`u&`-tY$_kp?AD!#_Y-nOt1jQ3r2^sIRhs? zkskIuFm(MalR`u%bTMfek({|7{B->IAn8^X2=DEa-Td3#rdZXAh7LC6NG(c)lig}< z0;5r<=|wNBP*W$h^<4y9xQS&4Ze9hCkXij|u@oF!@6M&>IGRIRl>r}GD`t;Ys8Y(s z1@k_}KAiH5j6{%@5@isM2tsqK^__{K8sd&9WLU#&$B_h2u~bKI znb{5nn@XY7or&gx%Aj)Mm(6LmdNBUL?AheIB*n^#_Nn8B# zhDxx3lhjEx)C ziw@kz2RsDLGeGBeuy%gfml{AkY@I%FxgW+Y)0dYp|7Hp1$Kc#BFzK>EAvgdW8$Ltu zjKG)cH=h9b;2)QJysrL&Kg|n%Iq;je0C;0G-(BdPY#Ej#Oh+G4&@)FkQfvj8&8v#d zl?K|~N(z$AB*)PC(-_kZl@$}hcP|XSt_z~2)$Zpo{;$h79op70k4qqCur>lM}>TCWrzSwAk| z-HQt}vMCPYzlxk|G~ZqLZVanl`6a`ORUfjX+_B9*4t5EsYg9?PXc_LZ#8EAA3LEk0 zkz1#dI$?2Le>@FgyFE~Sl#g?n=aFj^sggm4{usD=oOTVb)FEo>-7a;{~WMbykb3TQm6-w7hWlm2+!b}bq7zgslnpJ-uYqq!I!hBfx z7cjnuqWJ`4`Mbb1`rCGJS3@Xtr8WxcSyGNyotvB%gh|EfH~G(eOn`qaBO26K)vb|^ zIhRsn5OZS=5ZufG?&%KfYEnFysZsm@lT5lw_&=~<4V9@?nDZXp1h$F9cvhRbquN=7 zq@Qb4YWU&#NlizktjgNHx&!JlWFj0VMyW386_XXdvb1nHw{^;Wf;Nm9VU@_H2S9zw zp|v}0Vz37_z~P%vF;L{rx+e}fb!TdfQE9aX6|UC_yy{rN1g{?d6V3ir#=JvD4rUjY z;U_9sdZ%Or7bHC(T@yk)ZEs?kr%==`6by=XTziQMp&fYHX)DOIgPxqu;kXvM2PAP# zwZo0O1m}w)2{!?b@RTnn*7_K?q&!xVR5hb7)L8il1x8lw@xpp5U2C>B|BH03qj_Is zzaMo{^OJo4obJs#0K8MPnO6w@p=SHdXx;|l+oJD6!Th}FyD%`97kujkgxf-dO7&M> z3UqCv)eP;0hUt`{`v4kibQ=FX(7k8a&k2OZfV^{b_Ul`D1^F3Ijm)EL-U16 zqn(*FZ&&_|#5=CFitBBWpmr&TO%n6zu5>%V(~TlOtkpE_;4(EA&uEiYG6N>Izl9TdMi0{DH2jr68p9KCdfqxAVEhA9f zei?IY+Zc200bovlnA1VMdqVjFUo-m~#{EWWucS;_ zB{;BtX-*x<$98d6)LF*q+e@K?Brj<#81f*c#W0ho-Xyo0ts!`sb%D12pFy<_M@lEM zDHHl|>qi|V1r^nTCSSF1+8|U3njrgjLDS)EsK?@Xq)Hw>8wrhW-*0%2Gvp%*UOMTz zCl;1Uw-VEElf{_8W5etdAc_hu!@oDq%X*Az&F1mHw9QELV zz=dmbR5yla%e4osXkq&73`goCRf2>el1tWugQ_-bh4R(NrS-zYc=kZx;auZ2$lPk7+|gP)S2WAaHVTW@&6?004NL zeUZCM!%!5)zotq{X>rg&q(g@4AQp;(2)bCsB2);qf>j6e=tD@-kfgXc3a$kQ(Z$!G ze}L%f;3^1$B8a&75BOT7#B-BCi?klN++V(&^KtKY0q$ItHLEKHXu56YGHHIfu)>Gl z5J3z9q%bMUmN6{|S#+%L2kNA{PcpC0eQhhI6fGGD@WgYnVcNtM;_*$}V0=W}Cs#y; z_=0%EqyrMaGF|fclX2N)F3i}>=VplG#C*Ahr52VerbawPoKQ5K{JC|H6~^0)HMJ)D z=VUjG7WJh?+J|Yyk%5X0XfR-*2p$q@^sT~1l6IXG3pwgaQ?~7W23#M3<|NlkrkPn( z$y0P4z~16-##U);)N_iGbN7qmd<+7iU7%5SoS$RIX`BGTXW&|I&Q~eNi7(RYO)YT< z^w2#oZ_)v)Gc~P4INELZp?1pt z$E4f^`gLrVxhGS{R4nhk`7G3CjO@j7K7&G19}H(!CTknVw|ZKXa%_s};mo11S)mM_ zBH?=l;kxXOZxJ{A3Q2jOZUcqG&@Nu8n*z}$PnHxF9JmICKo4jDufP&`Op@g1zqw!J W6R9P4kz4@)0000 zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=KFlH4E+hTl0wj({YD#Bnf7Rc?^u=f~T0&rG&T zo>FZJgM}=~`efW4#<#CCe8E8_b4Y5QOU@BTDyeYA#N%<4{Y)|K=i?%sYxFD+_X0yC z80EOMdg@om_3ehs2W`*t;O7eaX^3va`ytEonvcgIA?HJW1ro}skc-=6sM~3%T@m|R zPj^_yu&%?Cz;Z*rxLe9C?q`MuiDRxp0fWf(tVqby<6UEnj|qB8 z@-++ZvwzRtC79tDWPhR{sZ*s%V;>tegqV1+VPtLq zwPY^Fg_|r+X*HT`Qlx_#OXCs1ED7JS(C)YF{u((vcY-Pti~;zMTlm@HpR_P%TPdRG zcdif@uewGUi`+Ux3qWX|+;mUy)lU5Jp?+2|C#bD7J07sc?J%(^z15cNoCSJ{@py$O zSU(LQMewZ%h6Ds+CQ-^1joFBhM+cya$XOC^AV8|z5hN!O>_^7V%Qx;Z+I3ku^Aclk z0tl5X1~x@1V5Ov}A4`rJsw$dPHLIxyEn2hWlr`sUd2Mpl#FD9HGjl6eT|BvZc60aQ zwQv#afm(90;-!>YIaD~RuvMX7A=!A7Ep58x%{Jf4RvXHvrKT-6Yu-w$oxAkZwPT~` z-b=58!oV&u($JBI4IgFHiCUX7)6|)#O`m1fo7zeBL;C}2bW-D;)Y7w?8l=JOGC^xQ z(ZvkJI1z~3BCvL_#Vk6d#EaZw77MS9j1i!h3A8@b=&7Sw->8~=)2 zSm^!*xd3z@xqU*dukT#ji5+j@%4rn5eOMn}1AAQ`wfeW?%h1cv%h1cv%h1cv%g}#d zXyV5M{=|mw1}&YvuEX>4Tx0C=2zk-JO7P!z_$rb&^`R0y?#RR{CvLrBt)q_{W=t_26t#n+&JfavPrDhPrih`9I< z_*$gIbCW=ev>v$JU%s64aqo8l?p&2Mt1AU)x^3n%X@0q|!iU}vK@0(;Fe%EGF)avL zbgb_O>ZH0)GOx~kZ7Ze}Eg1;##B;J?+Qb#&@lD%cd_>$QS44&Qf_TKF0}{V7UGn&o zaoJ@q%-GE5W{Bg&e7S|C7M3ffMm$BFP&A$Vxpj{f#@mcFwI=)LWH*c!^`%AHhiS!; zfr<@iFkqnw9ujKwt-?lu~R|pWi$dApG0o zC-@6aAavOrz0eW>000JJOGiWi000000Qp0^e*gdg32;bRa{vGf6951U69E94oEQKA z00(qQO+^Rg3Jn1>B>OE3I{*Lx-AP12R5;76(lJT{K@`u(d?teZ)XzA. """ from PySide2 import QtCore, QtWidgets, QtGui -from PySide2.QtCore import Slot,Qt +from PySide2.QtCore import Slot, Qt from PySide2.QtGui import QPalette, QColor import sys import platform @@ -46,8 +46,7 @@ from onionshare_cli.onion import ( from . import strings from .widgets import Alert -from .update_checker import ( - UpdateThread) +from .update_checker import UpdateThread from .tor_connection_dialog import TorConnectionDialog from .gui_common import GuiCommon @@ -125,13 +124,13 @@ class SettingsDialog(QtWidgets.QDialog): language_layout.addWidget(self.language_combobox) language_layout.addStretch() - #Theme Settings + # Theme Settings theme_label = QtWidgets.QLabel(strings._("gui_settings_theme_label")) self.theme_combobox = QtWidgets.QComboBox() theme_choices = [ strings._("gui_settings_theme_auto"), strings._("gui_settings_theme_light"), - strings._("gui_settings_theme_dark") + strings._("gui_settings_theme_dark"), ] self.theme_combobox.addItems(theme_choices) theme_layout = QtWidgets.QHBoxLayout() @@ -165,14 +164,16 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_bridges_no_bridges_radio_toggled ) - # obfs4 option radio - # if the obfs4proxy binary is missing, we can't use obfs4 transports ( self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path, + self.snowflake_file_path, ) = self.common.gui.get_tor_paths() + + # obfs4 option radio + # if the obfs4proxy binary is missing, we can't use obfs4 transports if not self.obfs4proxy_file_path or not os.path.isfile( self.obfs4proxy_file_path ): @@ -190,12 +191,6 @@ class SettingsDialog(QtWidgets.QDialog): # meek_lite-azure option radio # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports - ( - self.tor_path, - self.tor_geo_ip_file_path, - self.tor_geo_ipv6_file_path, - self.obfs4proxy_file_path, - ) = self.common.gui.get_tor_paths() if not self.obfs4proxy_file_path or not os.path.isfile( self.obfs4proxy_file_path ): @@ -802,7 +797,9 @@ class SettingsDialog(QtWidgets.QDialog): ) close_forced_update_thread() - forced_update_thread = UpdateThread(self.common, self.common.gui.onion, force=True) + forced_update_thread = UpdateThread( + self.common, self.common.gui.onion, force=True + ) forced_update_thread.update_available.connect(update_available) forced_update_thread.update_not_available.connect(update_not_available) forced_update_thread.update_error.connect(update_error) @@ -843,7 +840,6 @@ class SettingsDialog(QtWidgets.QDialog): notice = strings._("gui_settings_language_changed_notice") Alert(self.common, notice, QtWidgets.QMessageBox.Information) - # If color mode changed, inform user they need to restart OnionShare if changed(settings, self.old_settings, ["theme"]): notice = strings._("gui_color_mode_changed_notice") @@ -960,7 +956,7 @@ class SettingsDialog(QtWidgets.QDialog): # Theme theme_index = self.theme_combobox.currentIndex() - settings.set("theme",theme_index) + settings.set("theme", theme_index) # Language locale_index = self.language_combobox.currentIndex() diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py new file mode 100644 index 00000000..1ff46b3e --- /dev/null +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -0,0 +1,1112 @@ +# -*- 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 . +""" + +from PySide2 import QtCore, QtWidgets, QtGui +from PySide2.QtCore import Slot, Qt +from PySide2.QtGui import QPalette, QColor +import sys +import platform +import datetime +import re +import os +from onionshare_cli.settings import Settings +from onionshare_cli.onion import ( + Onion, + TorErrorInvalidSetting, + TorErrorAutomatic, + TorErrorSocketPort, + TorErrorSocketFile, + TorErrorMissingPassword, + TorErrorUnreadableCookieFile, + TorErrorAuthError, + TorErrorProtocolError, + BundledTorTimeout, + BundledTorBroken, + TorTooOldEphemeral, + TorTooOldStealth, + PortNotAvailable, +) + +from . import strings +from .widgets import Alert +from .update_checker import UpdateThread +from .tor_connection_dialog import TorConnectionDialog +from .gui_common import GuiCommon + + +class TorSettingsDialog(QtWidgets.QDialog): + """ + Settings dialog. + """ + + settings_saved = QtCore.Signal() + + def __init__(self, common): + super(TorSettingsDialog, self).__init__() + + self.common = common + + self.common.log("TorSettingsDialog", "__init__") + + self.setModal(True) + self.setWindowTitle(strings._("gui_tor_settings_window_title")) + self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + + self.system = platform.system() + + # If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog + self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1" + + # Automatic updates options + + # Autoupdate + self.autoupdate_checkbox = QtWidgets.QCheckBox() + self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option")) + + # Last update time + self.autoupdate_timestamp = QtWidgets.QLabel() + + # Check for updates button + self.check_for_updates_button = QtWidgets.QPushButton( + strings._("gui_settings_autoupdate_check_button") + ) + self.check_for_updates_button.clicked.connect(self.check_for_updates) + # We can't check for updates if not connected to Tor + if not self.common.gui.onion.connected_to_tor: + self.check_for_updates_button.setEnabled(False) + + # Autoupdate options layout + autoupdate_group_layout = QtWidgets.QVBoxLayout() + autoupdate_group_layout.addWidget(self.autoupdate_checkbox) + autoupdate_group_layout.addWidget(self.autoupdate_timestamp) + autoupdate_group_layout.addWidget(self.check_for_updates_button) + autoupdate_group = QtWidgets.QGroupBox( + strings._("gui_settings_autoupdate_label") + ) + autoupdate_group.setLayout(autoupdate_group_layout) + + # Autoupdate is only available for Windows and Mac (Linux updates using package manager) + if self.system != "Windows" and self.system != "Darwin": + autoupdate_group.hide() + + # Language settings + language_label = QtWidgets.QLabel(strings._("gui_settings_language_label")) + self.language_combobox = QtWidgets.QComboBox() + # Populate the dropdown with all of OnionShare's available languages + language_names_to_locales = { + v: k for k, v in self.common.settings.available_locales.items() + } + language_names = list(language_names_to_locales) + language_names.sort() + for language_name in language_names: + locale = language_names_to_locales[language_name] + self.language_combobox.addItem(language_name, locale) + language_layout = QtWidgets.QHBoxLayout() + language_layout.addWidget(language_label) + language_layout.addWidget(self.language_combobox) + language_layout.addStretch() + + # Theme Settings + theme_label = QtWidgets.QLabel(strings._("gui_settings_theme_label")) + self.theme_combobox = QtWidgets.QComboBox() + theme_choices = [ + strings._("gui_settings_theme_auto"), + strings._("gui_settings_theme_light"), + strings._("gui_settings_theme_dark"), + ] + self.theme_combobox.addItems(theme_choices) + theme_layout = QtWidgets.QHBoxLayout() + theme_layout.addWidget(theme_label) + theme_layout.addWidget(self.theme_combobox) + theme_layout.addStretch() + + # Connection type: either automatic, control port, or socket file + + # Bundled Tor + self.connection_type_bundled_radio = QtWidgets.QRadioButton( + strings._("gui_settings_connection_type_bundled_option") + ) + self.connection_type_bundled_radio.toggled.connect( + self.connection_type_bundled_toggled + ) + + # Bundled Tor doesn't work on dev mode in Windows or Mac + if (self.system == "Windows" or self.system == "Darwin") and getattr( + sys, "onionshare_dev_mode", False + ): + self.connection_type_bundled_radio.setEnabled(False) + + # Bridge options for bundled tor + + # No bridges option radio + self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_no_bridges_radio_option") + ) + self.tor_bridges_no_bridges_radio.toggled.connect( + self.tor_bridges_no_bridges_radio_toggled + ) + + ( + self.tor_path, + self.tor_geo_ip_file_path, + self.tor_geo_ipv6_file_path, + self.obfs4proxy_file_path, + self.snowflake_file_path, + ) = self.common.gui.get_tor_paths() + + # obfs4 option radio + # if the obfs4proxy binary is missing, we can't use obfs4 transports + if not self.obfs4proxy_file_path or not os.path.isfile( + self.obfs4proxy_file_path + ): + self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy") + ) + self.tor_bridges_use_obfs4_radio.setEnabled(False) + else: + self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_obfs4_radio_option") + ) + self.tor_bridges_use_obfs4_radio.toggled.connect( + self.tor_bridges_use_obfs4_radio_toggled + ) + + # meek_lite-azure option radio + # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports + if not self.obfs4proxy_file_path or not os.path.isfile( + self.obfs4proxy_file_path + ): + self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( + strings._( + "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy" + ) + ) + self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False) + else: + self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option") + ) + self.tor_bridges_use_meek_lite_azure_radio.toggled.connect( + self.tor_bridges_use_meek_lite_azure_radio_toggled + ) + + # Custom bridges radio and textbox + self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_custom_radio_option") + ) + self.tor_bridges_use_custom_radio.toggled.connect( + self.tor_bridges_use_custom_radio_toggled + ) + + self.tor_bridges_use_custom_label = QtWidgets.QLabel( + strings._("gui_settings_tor_bridges_custom_label") + ) + self.tor_bridges_use_custom_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + self.tor_bridges_use_custom_label.setOpenExternalLinks(True) + self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() + self.tor_bridges_use_custom_textbox.setMaximumHeight(200) + self.tor_bridges_use_custom_textbox.setPlaceholderText( + "[address:port] [identifier]" + ) + + tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() + tor_bridges_use_custom_textbox_options_layout.addWidget( + self.tor_bridges_use_custom_label + ) + tor_bridges_use_custom_textbox_options_layout.addWidget( + self.tor_bridges_use_custom_textbox + ) + + self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget() + self.tor_bridges_use_custom_textbox_options.setLayout( + tor_bridges_use_custom_textbox_options_layout + ) + self.tor_bridges_use_custom_textbox_options.hide() + + # Bridges layout/widget + bridges_layout = QtWidgets.QVBoxLayout() + bridges_layout.addWidget(self.tor_bridges_no_bridges_radio) + bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio) + bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio) + bridges_layout.addWidget(self.tor_bridges_use_custom_radio) + bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options) + + self.bridges = QtWidgets.QWidget() + self.bridges.setLayout(bridges_layout) + + # Automatic + self.connection_type_automatic_radio = QtWidgets.QRadioButton( + strings._("gui_settings_connection_type_automatic_option") + ) + self.connection_type_automatic_radio.toggled.connect( + self.connection_type_automatic_toggled + ) + + # Control port + self.connection_type_control_port_radio = QtWidgets.QRadioButton( + strings._("gui_settings_connection_type_control_port_option") + ) + self.connection_type_control_port_radio.toggled.connect( + self.connection_type_control_port_toggled + ) + + connection_type_control_port_extras_label = QtWidgets.QLabel( + strings._("gui_settings_control_port_label") + ) + self.connection_type_control_port_extras_address = QtWidgets.QLineEdit() + self.connection_type_control_port_extras_port = QtWidgets.QLineEdit() + connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout() + connection_type_control_port_extras_layout.addWidget( + connection_type_control_port_extras_label + ) + connection_type_control_port_extras_layout.addWidget( + self.connection_type_control_port_extras_address + ) + connection_type_control_port_extras_layout.addWidget( + self.connection_type_control_port_extras_port + ) + + self.connection_type_control_port_extras = QtWidgets.QWidget() + self.connection_type_control_port_extras.setLayout( + connection_type_control_port_extras_layout + ) + self.connection_type_control_port_extras.hide() + + # Socket file + self.connection_type_socket_file_radio = QtWidgets.QRadioButton( + strings._("gui_settings_connection_type_socket_file_option") + ) + self.connection_type_socket_file_radio.toggled.connect( + self.connection_type_socket_file_toggled + ) + + connection_type_socket_file_extras_label = QtWidgets.QLabel( + strings._("gui_settings_socket_file_label") + ) + self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit() + connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout() + connection_type_socket_file_extras_layout.addWidget( + connection_type_socket_file_extras_label + ) + connection_type_socket_file_extras_layout.addWidget( + self.connection_type_socket_file_extras_path + ) + + self.connection_type_socket_file_extras = QtWidgets.QWidget() + self.connection_type_socket_file_extras.setLayout( + connection_type_socket_file_extras_layout + ) + self.connection_type_socket_file_extras.hide() + + # Tor SOCKS address and port + gui_settings_socks_label = QtWidgets.QLabel( + strings._("gui_settings_socks_label") + ) + self.connection_type_socks_address = QtWidgets.QLineEdit() + self.connection_type_socks_port = QtWidgets.QLineEdit() + connection_type_socks_layout = QtWidgets.QHBoxLayout() + connection_type_socks_layout.addWidget(gui_settings_socks_label) + connection_type_socks_layout.addWidget(self.connection_type_socks_address) + connection_type_socks_layout.addWidget(self.connection_type_socks_port) + + self.connection_type_socks = QtWidgets.QWidget() + self.connection_type_socks.setLayout(connection_type_socks_layout) + self.connection_type_socks.hide() + + # Authentication options + + # No authentication + self.authenticate_no_auth_radio = QtWidgets.QRadioButton( + strings._("gui_settings_authenticate_no_auth_option") + ) + self.authenticate_no_auth_radio.toggled.connect( + self.authenticate_no_auth_toggled + ) + + # Password + self.authenticate_password_radio = QtWidgets.QRadioButton( + strings._("gui_settings_authenticate_password_option") + ) + self.authenticate_password_radio.toggled.connect( + self.authenticate_password_toggled + ) + + authenticate_password_extras_label = QtWidgets.QLabel( + strings._("gui_settings_password_label") + ) + self.authenticate_password_extras_password = QtWidgets.QLineEdit("") + authenticate_password_extras_layout = QtWidgets.QHBoxLayout() + authenticate_password_extras_layout.addWidget( + authenticate_password_extras_label + ) + authenticate_password_extras_layout.addWidget( + self.authenticate_password_extras_password + ) + + self.authenticate_password_extras = QtWidgets.QWidget() + self.authenticate_password_extras.setLayout(authenticate_password_extras_layout) + self.authenticate_password_extras.hide() + + # Authentication options layout + authenticate_group_layout = QtWidgets.QVBoxLayout() + authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) + authenticate_group_layout.addWidget(self.authenticate_password_radio) + authenticate_group_layout.addWidget(self.authenticate_password_extras) + self.authenticate_group = QtWidgets.QGroupBox( + strings._("gui_settings_authenticate_label") + ) + self.authenticate_group.setLayout(authenticate_group_layout) + + # Put the radios into their own group so they are exclusive + connection_type_radio_group_layout = QtWidgets.QVBoxLayout() + connection_type_radio_group_layout.addWidget(self.connection_type_bundled_radio) + connection_type_radio_group_layout.addWidget( + self.connection_type_automatic_radio + ) + connection_type_radio_group_layout.addWidget( + self.connection_type_control_port_radio + ) + connection_type_radio_group_layout.addWidget( + self.connection_type_socket_file_radio + ) + connection_type_radio_group = QtWidgets.QGroupBox( + strings._("gui_settings_connection_type_label") + ) + connection_type_radio_group.setLayout(connection_type_radio_group_layout) + + # The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges) + connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout() + connection_type_bridges_radio_group_layout.addWidget(self.bridges) + self.connection_type_bridges_radio_group = QtWidgets.QGroupBox( + strings._("gui_settings_tor_bridges") + ) + self.connection_type_bridges_radio_group.setLayout( + connection_type_bridges_radio_group_layout + ) + self.connection_type_bridges_radio_group.hide() + + # Test tor settings button + self.connection_type_test_button = QtWidgets.QPushButton( + strings._("gui_settings_connection_type_test_button") + ) + self.connection_type_test_button.clicked.connect(self.test_tor_clicked) + connection_type_test_button_layout = QtWidgets.QHBoxLayout() + connection_type_test_button_layout.addWidget(self.connection_type_test_button) + connection_type_test_button_layout.addStretch() + + # Connection type layout + connection_type_layout = QtWidgets.QVBoxLayout() + connection_type_layout.addWidget(self.connection_type_control_port_extras) + connection_type_layout.addWidget(self.connection_type_socket_file_extras) + connection_type_layout.addWidget(self.connection_type_socks) + connection_type_layout.addWidget(self.authenticate_group) + connection_type_layout.addWidget(self.connection_type_bridges_radio_group) + connection_type_layout.addLayout(connection_type_test_button_layout) + + # Buttons + self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) + self.save_button.clicked.connect(self.save_clicked) + self.cancel_button = QtWidgets.QPushButton( + strings._("gui_settings_button_cancel") + ) + self.cancel_button.clicked.connect(self.cancel_clicked) + version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") + version_label.setStyleSheet(self.common.gui.css["settings_version"]) + self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help")) + self.help_button.clicked.connect(self.help_clicked) + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addWidget(version_label) + buttons_layout.addWidget(self.help_button) + buttons_layout.addStretch() + buttons_layout.addWidget(self.save_button) + buttons_layout.addWidget(self.cancel_button) + + # Tor network connection status + self.tor_status = QtWidgets.QLabel() + self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"]) + self.tor_status.hide() + + # Layout + tor_layout = QtWidgets.QVBoxLayout() + tor_layout.addWidget(connection_type_radio_group) + tor_layout.addLayout(connection_type_layout) + tor_layout.addWidget(self.tor_status) + tor_layout.addStretch() + + layout = QtWidgets.QVBoxLayout() + if not self.hide_tor_settings: + layout.addLayout(tor_layout) + layout.addSpacing(20) + layout.addWidget(autoupdate_group) + if autoupdate_group.isVisible(): + layout.addSpacing(20) + layout.addLayout(language_layout) + layout.addSpacing(20) + layout.addLayout(theme_layout) + layout.addSpacing(20) + layout.addStretch() + layout.addLayout(buttons_layout) + + self.setLayout(layout) + self.cancel_button.setFocus() + + self.reload_settings() + + def reload_settings(self): + # Load settings, and fill them in + self.old_settings = Settings(self.common) + self.old_settings.load() + + use_autoupdate = self.old_settings.get("use_autoupdate") + if use_autoupdate: + self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked) + + autoupdate_timestamp = self.old_settings.get("autoupdate_timestamp") + self._update_autoupdate_timestamp(autoupdate_timestamp) + + locale = self.old_settings.get("locale") + locale_index = self.language_combobox.findData(locale) + self.language_combobox.setCurrentIndex(locale_index) + + theme_choice = self.old_settings.get("theme") + self.theme_combobox.setCurrentIndex(theme_choice) + + connection_type = self.old_settings.get("connection_type") + if connection_type == "bundled": + if self.connection_type_bundled_radio.isEnabled(): + self.connection_type_bundled_radio.setChecked(True) + else: + # If bundled tor is disabled, fallback to automatic + self.connection_type_automatic_radio.setChecked(True) + elif connection_type == "automatic": + self.connection_type_automatic_radio.setChecked(True) + elif connection_type == "control_port": + self.connection_type_control_port_radio.setChecked(True) + elif connection_type == "socket_file": + self.connection_type_socket_file_radio.setChecked(True) + self.connection_type_control_port_extras_address.setText( + self.old_settings.get("control_port_address") + ) + self.connection_type_control_port_extras_port.setText( + str(self.old_settings.get("control_port_port")) + ) + self.connection_type_socket_file_extras_path.setText( + self.old_settings.get("socket_file_path") + ) + self.connection_type_socks_address.setText( + self.old_settings.get("socks_address") + ) + self.connection_type_socks_port.setText( + str(self.old_settings.get("socks_port")) + ) + auth_type = self.old_settings.get("auth_type") + if auth_type == "no_auth": + self.authenticate_no_auth_radio.setChecked(True) + elif auth_type == "password": + self.authenticate_password_radio.setChecked(True) + self.authenticate_password_extras_password.setText( + self.old_settings.get("auth_password") + ) + + if self.old_settings.get("no_bridges"): + self.tor_bridges_no_bridges_radio.setChecked(True) + self.tor_bridges_use_obfs4_radio.setChecked(False) + self.tor_bridges_use_meek_lite_azure_radio.setChecked(False) + self.tor_bridges_use_custom_radio.setChecked(False) + else: + self.tor_bridges_no_bridges_radio.setChecked(False) + self.tor_bridges_use_obfs4_radio.setChecked( + self.old_settings.get("tor_bridges_use_obfs4") + ) + self.tor_bridges_use_meek_lite_azure_radio.setChecked( + self.old_settings.get("tor_bridges_use_meek_lite_azure") + ) + + if self.old_settings.get("tor_bridges_use_custom_bridges"): + self.tor_bridges_use_custom_radio.setChecked(True) + # Remove the 'Bridge' lines at the start of each bridge. + # They are added automatically to provide compatibility with + # copying/pasting bridges provided from https://bridges.torproject.org + new_bridges = [] + bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split( + "Bridge " + ) + for bridge in bridges: + new_bridges.append(bridge) + new_bridges = "".join(new_bridges) + self.tor_bridges_use_custom_textbox.setPlainText(new_bridges) + + def connection_type_bundled_toggled(self, checked): + """ + Connection type bundled was toggled. If checked, hide authentication fields. + """ + self.common.log("TorSettingsDialog", "connection_type_bundled_toggled") + if self.hide_tor_settings: + return + if checked: + self.authenticate_group.hide() + self.connection_type_socks.hide() + self.connection_type_bridges_radio_group.show() + + def tor_bridges_no_bridges_radio_toggled(self, checked): + """ + 'No bridges' option was toggled. If checked, enable other bridge options. + """ + if self.hide_tor_settings: + return + if checked: + self.tor_bridges_use_custom_textbox_options.hide() + + def tor_bridges_use_obfs4_radio_toggled(self, checked): + """ + obfs4 bridges option was toggled. If checked, disable custom bridge options. + """ + if self.hide_tor_settings: + return + if checked: + self.tor_bridges_use_custom_textbox_options.hide() + + def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked): + """ + meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. + """ + if self.hide_tor_settings: + return + if checked: + self.tor_bridges_use_custom_textbox_options.hide() + # Alert the user about meek's costliness if it looks like they're turning it on + if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): + Alert( + self.common, + strings._("gui_settings_meek_lite_expensive_warning"), + QtWidgets.QMessageBox.Warning, + ) + + def tor_bridges_use_custom_radio_toggled(self, checked): + """ + Custom bridges option was toggled. If checked, show custom bridge options. + """ + if self.hide_tor_settings: + return + if checked: + self.tor_bridges_use_custom_textbox_options.show() + + def connection_type_automatic_toggled(self, checked): + """ + Connection type automatic was toggled. If checked, hide authentication fields. + """ + self.common.log("TorSettingsDialog", "connection_type_automatic_toggled") + if self.hide_tor_settings: + return + if checked: + self.authenticate_group.hide() + self.connection_type_socks.hide() + self.connection_type_bridges_radio_group.hide() + + def connection_type_control_port_toggled(self, checked): + """ + Connection type control port was toggled. If checked, show extra fields + for Tor control address and port. If unchecked, hide those extra fields. + """ + self.common.log("TorSettingsDialog", "connection_type_control_port_toggled") + if self.hide_tor_settings: + return + if checked: + self.authenticate_group.show() + self.connection_type_control_port_extras.show() + self.connection_type_socks.show() + self.connection_type_bridges_radio_group.hide() + else: + self.connection_type_control_port_extras.hide() + + def connection_type_socket_file_toggled(self, checked): + """ + Connection type socket file was toggled. If checked, show extra fields + for socket file. If unchecked, hide those extra fields. + """ + self.common.log("TorSettingsDialog", "connection_type_socket_file_toggled") + if self.hide_tor_settings: + return + if checked: + self.authenticate_group.show() + self.connection_type_socket_file_extras.show() + self.connection_type_socks.show() + self.connection_type_bridges_radio_group.hide() + else: + self.connection_type_socket_file_extras.hide() + + def authenticate_no_auth_toggled(self, checked): + """ + Authentication option no authentication was toggled. + """ + self.common.log("TorSettingsDialog", "authenticate_no_auth_toggled") + + def authenticate_password_toggled(self, checked): + """ + Authentication option password was toggled. If checked, show extra fields + for password auth. If unchecked, hide those extra fields. + """ + self.common.log("TorSettingsDialog", "authenticate_password_toggled") + if checked: + self.authenticate_password_extras.show() + else: + self.authenticate_password_extras.hide() + + def test_tor_clicked(self): + """ + Test Tor Settings button clicked. With the given settings, see if we can + successfully connect and authenticate to Tor. + """ + self.common.log("TorSettingsDialog", "test_tor_clicked") + settings = self.settings_from_fields() + + try: + # Show Tor connection status if connection type is bundled tor + if settings.get("connection_type") == "bundled": + self.tor_status.show() + self._disable_buttons() + + def tor_status_update_func(progress, summary): + self._tor_status_update(progress, summary) + return True + + else: + tor_status_update_func = None + + onion = Onion( + self.common, + use_tmp_dir=True, + get_tor_paths=self.common.gui.get_tor_paths, + ) + onion.connect( + custom_settings=settings, + tor_status_update_func=tor_status_update_func, + ) + + # If an exception hasn't been raised yet, the Tor settings work + Alert( + self.common, + strings._("settings_test_success").format( + onion.tor_version, + onion.supports_ephemeral, + onion.supports_stealth, + onion.supports_v3_onions, + ), + ) + + # Clean up + onion.cleanup() + + except ( + TorErrorInvalidSetting, + TorErrorAutomatic, + TorErrorSocketPort, + TorErrorSocketFile, + TorErrorMissingPassword, + TorErrorUnreadableCookieFile, + TorErrorAuthError, + TorErrorProtocolError, + BundledTorTimeout, + BundledTorBroken, + TorTooOldEphemeral, + TorTooOldStealth, + PortNotAvailable, + ) as e: + message = self.common.gui.get_translated_tor_error(e) + Alert( + self.common, + message, + QtWidgets.QMessageBox.Warning, + ) + if settings.get("connection_type") == "bundled": + self.tor_status.hide() + self._enable_buttons() + + def check_for_updates(self): + """ + Check for Updates button clicked. Manually force an update check. + """ + self.common.log("TorSettingsDialog", "check_for_updates") + # Disable buttons + self._disable_buttons() + self.common.gui.qtapp.processEvents() + + def update_timestamp(): + # Update the last checked label + settings = Settings(self.common) + settings.load() + autoupdate_timestamp = settings.get("autoupdate_timestamp") + self._update_autoupdate_timestamp(autoupdate_timestamp) + + def close_forced_update_thread(): + forced_update_thread.quit() + # Enable buttons + self._enable_buttons() + # Update timestamp + update_timestamp() + + # Check for updates + def update_available(update_url, installed_version, latest_version): + Alert( + self.common, + strings._("update_available").format( + update_url, installed_version, latest_version + ), + ) + close_forced_update_thread() + + def update_not_available(): + Alert(self.common, strings._("update_not_available")) + close_forced_update_thread() + + def update_error(): + Alert( + self.common, + strings._("update_error_check_error"), + QtWidgets.QMessageBox.Warning, + ) + close_forced_update_thread() + + def update_invalid_version(latest_version): + Alert( + self.common, + strings._("update_error_invalid_latest_version").format(latest_version), + QtWidgets.QMessageBox.Warning, + ) + close_forced_update_thread() + + forced_update_thread = UpdateThread( + self.common, self.common.gui.onion, force=True + ) + forced_update_thread.update_available.connect(update_available) + forced_update_thread.update_not_available.connect(update_not_available) + forced_update_thread.update_error.connect(update_error) + forced_update_thread.update_invalid_version.connect(update_invalid_version) + forced_update_thread.start() + + def save_clicked(self): + """ + Save button clicked. Save current settings to disk. + """ + self.common.log("TorSettingsDialog", "save_clicked") + + def changed(s1, s2, keys): + """ + Compare the Settings objects s1 and s2 and return true if any values + have changed for the given keys. + """ + for key in keys: + if s1.get(key) != s2.get(key): + return True + return False + + settings = self.settings_from_fields() + if settings: + # If language changed, inform user they need to restart OnionShare + if changed(settings, self.old_settings, ["locale"]): + # Look up error message in different locale + new_locale = settings.get("locale") + if ( + new_locale in strings.translations + and "gui_settings_language_changed_notice" + in strings.translations[new_locale] + ): + notice = strings.translations[new_locale][ + "gui_settings_language_changed_notice" + ] + else: + notice = strings._("gui_settings_language_changed_notice") + Alert(self.common, notice, QtWidgets.QMessageBox.Information) + + # If color mode changed, inform user they need to restart OnionShare + if changed(settings, self.old_settings, ["theme"]): + notice = strings._("gui_color_mode_changed_notice") + Alert(self.common, notice, QtWidgets.QMessageBox.Information) + + # Save the new settings + settings.save() + + # If Tor isn't connected, or if Tor settings have changed, Reinitialize + # the Onion object + reboot_onion = False + if not self.common.gui.local_only: + if self.common.gui.onion.is_authenticated(): + self.common.log( + "TorSettingsDialog", "save_clicked", "Connected to Tor" + ) + + if changed( + settings, + self.old_settings, + [ + "connection_type", + "control_port_address", + "control_port_port", + "socks_address", + "socks_port", + "socket_file_path", + "auth_type", + "auth_password", + "no_bridges", + "tor_bridges_use_obfs4", + "tor_bridges_use_meek_lite_azure", + "tor_bridges_use_custom_bridges", + ], + ): + + reboot_onion = True + + else: + self.common.log( + "TorSettingsDialog", "save_clicked", "Not connected to Tor" + ) + # Tor isn't connected, so try connecting + reboot_onion = True + + # Do we need to reinitialize Tor? + if reboot_onion: + # Reinitialize the Onion object + self.common.log( + "TorSettingsDialog", "save_clicked", "rebooting the Onion" + ) + self.common.gui.onion.cleanup() + + tor_con = TorConnectionDialog(self.common, settings) + tor_con.start() + + self.common.log( + "TorSettingsDialog", + "save_clicked", + f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", + ) + + if ( + self.common.gui.onion.is_authenticated() + and not tor_con.wasCanceled() + ): + self.settings_saved.emit() + self.close() + + else: + self.settings_saved.emit() + self.close() + else: + self.settings_saved.emit() + self.close() + + def cancel_clicked(self): + """ + Cancel button clicked. + """ + self.common.log("TorSettingsDialog", "cancel_clicked") + if ( + not self.common.gui.local_only + and not self.common.gui.onion.is_authenticated() + ): + Alert( + self.common, + strings._("gui_tor_connection_canceled"), + QtWidgets.QMessageBox.Warning, + ) + sys.exit() + else: + self.close() + + def help_clicked(self): + """ + Help button clicked. + """ + self.common.log("TorSettingsDialog", "help_clicked") + TorSettingsDialog.open_help() + + @staticmethod + def open_help(): + help_url = "https://docs.onionshare.org/" + QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url)) + + def settings_from_fields(self): + """ + Return a Settings object that's full of values from the settings dialog. + """ + self.common.log("TorSettingsDialog", "settings_from_fields") + settings = Settings(self.common) + settings.load() # To get the last update timestamp + + # Theme + theme_index = self.theme_combobox.currentIndex() + settings.set("theme", theme_index) + + # Language + locale_index = self.language_combobox.currentIndex() + locale = self.language_combobox.itemData(locale_index) + settings.set("locale", locale) + + # Tor connection + if self.connection_type_bundled_radio.isChecked(): + settings.set("connection_type", "bundled") + if self.connection_type_automatic_radio.isChecked(): + settings.set("connection_type", "automatic") + if self.connection_type_control_port_radio.isChecked(): + settings.set("connection_type", "control_port") + if self.connection_type_socket_file_radio.isChecked(): + settings.set("connection_type", "socket_file") + + if self.autoupdate_checkbox.isChecked(): + settings.set("use_autoupdate", True) + else: + settings.set("use_autoupdate", False) + + settings.set( + "control_port_address", + self.connection_type_control_port_extras_address.text(), + ) + settings.set( + "control_port_port", self.connection_type_control_port_extras_port.text() + ) + settings.set( + "socket_file_path", self.connection_type_socket_file_extras_path.text() + ) + + settings.set("socks_address", self.connection_type_socks_address.text()) + settings.set("socks_port", self.connection_type_socks_port.text()) + + if self.authenticate_no_auth_radio.isChecked(): + settings.set("auth_type", "no_auth") + if self.authenticate_password_radio.isChecked(): + settings.set("auth_type", "password") + + settings.set("auth_password", self.authenticate_password_extras_password.text()) + + # Whether we use bridges + if self.tor_bridges_no_bridges_radio.isChecked(): + settings.set("no_bridges", True) + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_custom_bridges", "") + if self.tor_bridges_use_obfs4_radio.isChecked(): + settings.set("no_bridges", False) + settings.set("tor_bridges_use_obfs4", True) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_custom_bridges", "") + if self.tor_bridges_use_meek_lite_azure_radio.isChecked(): + settings.set("no_bridges", False) + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", True) + settings.set("tor_bridges_use_custom_bridges", "") + if self.tor_bridges_use_custom_radio.isChecked(): + settings.set("no_bridges", False) + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + + # Insert a 'Bridge' line at the start of each bridge. + # This makes it easier to copy/paste a set of bridges + # provided from https://bridges.torproject.org + new_bridges = [] + bridges = self.tor_bridges_use_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=(.+)" + ) + if ( + ipv4_pattern.match(bridge) + or ipv6_pattern.match(bridge) + or meek_lite_pattern.match(bridge) + ): + new_bridges.append("".join(["Bridge ", bridge, "\n"])) + bridges_valid = True + + if bridges_valid: + new_bridges = "".join(new_bridges) + settings.set("tor_bridges_use_custom_bridges", new_bridges) + else: + Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) + settings.set("no_bridges", True) + return False + + return settings + + def closeEvent(self, e): + self.common.log("TorSettingsDialog", "closeEvent") + + # On close, if Tor isn't connected, then quit OnionShare altogether + if not self.common.gui.local_only: + if not self.common.gui.onion.is_authenticated(): + self.common.log( + "TorSettingsDialog", + "closeEvent", + "Closing while not connected to Tor", + ) + + # Wait 1ms for the event loop to finish, then quit + QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) + + def _update_autoupdate_timestamp(self, autoupdate_timestamp): + self.common.log("TorSettingsDialog", "_update_autoupdate_timestamp") + + if autoupdate_timestamp: + dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) + last_checked = dt.strftime("%B %d, %Y %H:%M") + else: + last_checked = strings._("gui_settings_autoupdate_timestamp_never") + self.autoupdate_timestamp.setText( + strings._("gui_settings_autoupdate_timestamp").format(last_checked) + ) + + def _tor_status_update(self, progress, summary): + self.tor_status.setText( + f"{strings._('connecting_to_tor')}
{progress}% {summary}" + ) + self.common.gui.qtapp.processEvents() + if "Done" in summary: + self.tor_status.hide() + self._enable_buttons() + + def _disable_buttons(self): + self.common.log("TorSettingsDialog", "_disable_buttons") + + self.check_for_updates_button.setEnabled(False) + self.connection_type_test_button.setEnabled(False) + self.save_button.setEnabled(False) + self.cancel_button.setEnabled(False) + + def _enable_buttons(self): + self.common.log("TorSettingsDialog", "_enable_buttons") + # We can't check for updates if we're still not connected to Tor + if not self.common.gui.onion.connected_to_tor: + self.check_for_updates_button.setEnabled(False) + else: + self.check_for_updates_button.setEnabled(True) + self.connection_type_test_button.setEnabled(True) + self.save_button.setEnabled(True) + self.cancel_button.setEnabled(True) From 4b976e538f8ab9447dc98de425e9f914c85158b2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 12 Oct 2021 21:19:48 -0700 Subject: [PATCH 05/70] Rip out non-Tor settings from TorSettingsDialog --- desktop/src/onionshare/tor_settings_dialog.py | 251 +----------------- 1 file changed, 3 insertions(+), 248 deletions(-) diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 1ff46b3e..fbf93044 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -71,73 +71,6 @@ class TorSettingsDialog(QtWidgets.QDialog): self.system = platform.system() - # If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog - self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1" - - # Automatic updates options - - # Autoupdate - self.autoupdate_checkbox = QtWidgets.QCheckBox() - self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option")) - - # Last update time - self.autoupdate_timestamp = QtWidgets.QLabel() - - # Check for updates button - self.check_for_updates_button = QtWidgets.QPushButton( - strings._("gui_settings_autoupdate_check_button") - ) - self.check_for_updates_button.clicked.connect(self.check_for_updates) - # We can't check for updates if not connected to Tor - if not self.common.gui.onion.connected_to_tor: - self.check_for_updates_button.setEnabled(False) - - # Autoupdate options layout - autoupdate_group_layout = QtWidgets.QVBoxLayout() - autoupdate_group_layout.addWidget(self.autoupdate_checkbox) - autoupdate_group_layout.addWidget(self.autoupdate_timestamp) - autoupdate_group_layout.addWidget(self.check_for_updates_button) - autoupdate_group = QtWidgets.QGroupBox( - strings._("gui_settings_autoupdate_label") - ) - autoupdate_group.setLayout(autoupdate_group_layout) - - # Autoupdate is only available for Windows and Mac (Linux updates using package manager) - if self.system != "Windows" and self.system != "Darwin": - autoupdate_group.hide() - - # Language settings - language_label = QtWidgets.QLabel(strings._("gui_settings_language_label")) - self.language_combobox = QtWidgets.QComboBox() - # Populate the dropdown with all of OnionShare's available languages - language_names_to_locales = { - v: k for k, v in self.common.settings.available_locales.items() - } - language_names = list(language_names_to_locales) - language_names.sort() - for language_name in language_names: - locale = language_names_to_locales[language_name] - self.language_combobox.addItem(language_name, locale) - language_layout = QtWidgets.QHBoxLayout() - language_layout.addWidget(language_label) - language_layout.addWidget(self.language_combobox) - language_layout.addStretch() - - # Theme Settings - theme_label = QtWidgets.QLabel(strings._("gui_settings_theme_label")) - self.theme_combobox = QtWidgets.QComboBox() - theme_choices = [ - strings._("gui_settings_theme_auto"), - strings._("gui_settings_theme_light"), - strings._("gui_settings_theme_dark"), - ] - self.theme_combobox.addItems(theme_choices) - theme_layout = QtWidgets.QHBoxLayout() - theme_layout.addWidget(theme_label) - theme_layout.addWidget(self.theme_combobox) - theme_layout.addStretch() - # Connection type: either automatic, control port, or socket file # Bundled Tor @@ -430,13 +363,7 @@ class TorSettingsDialog(QtWidgets.QDialog): strings._("gui_settings_button_cancel") ) self.cancel_button.clicked.connect(self.cancel_clicked) - version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") - version_label.setStyleSheet(self.common.gui.css["settings_version"]) - self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help")) - self.help_button.clicked.connect(self.help_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addWidget(version_label) - buttons_layout.addWidget(self.help_button) buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) buttons_layout.addWidget(self.cancel_button) @@ -447,23 +374,10 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_status.hide() # Layout - tor_layout = QtWidgets.QVBoxLayout() - tor_layout.addWidget(connection_type_radio_group) - tor_layout.addLayout(connection_type_layout) - tor_layout.addWidget(self.tor_status) - tor_layout.addStretch() - layout = QtWidgets.QVBoxLayout() - if not self.hide_tor_settings: - layout.addLayout(tor_layout) - layout.addSpacing(20) - layout.addWidget(autoupdate_group) - if autoupdate_group.isVisible(): - layout.addSpacing(20) - layout.addLayout(language_layout) - layout.addSpacing(20) - layout.addLayout(theme_layout) - layout.addSpacing(20) + layout.addWidget(connection_type_radio_group) + layout.addLayout(connection_type_layout) + layout.addWidget(self.tor_status) layout.addStretch() layout.addLayout(buttons_layout) @@ -477,22 +391,6 @@ class TorSettingsDialog(QtWidgets.QDialog): self.old_settings = Settings(self.common) self.old_settings.load() - use_autoupdate = self.old_settings.get("use_autoupdate") - if use_autoupdate: - self.autoupdate_checkbox.setCheckState(QtCore.Qt.Checked) - else: - self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked) - - autoupdate_timestamp = self.old_settings.get("autoupdate_timestamp") - self._update_autoupdate_timestamp(autoupdate_timestamp) - - locale = self.old_settings.get("locale") - locale_index = self.language_combobox.findData(locale) - self.language_combobox.setCurrentIndex(locale_index) - - theme_choice = self.old_settings.get("theme") - self.theme_combobox.setCurrentIndex(theme_choice) - connection_type = self.old_settings.get("connection_type") if connection_type == "bundled": if self.connection_type_bundled_radio.isEnabled(): @@ -563,8 +461,6 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type bundled was toggled. If checked, hide authentication fields. """ self.common.log("TorSettingsDialog", "connection_type_bundled_toggled") - if self.hide_tor_settings: - return if checked: self.authenticate_group.hide() self.connection_type_socks.hide() @@ -574,8 +470,6 @@ class TorSettingsDialog(QtWidgets.QDialog): """ 'No bridges' option was toggled. If checked, enable other bridge options. """ - if self.hide_tor_settings: - return if checked: self.tor_bridges_use_custom_textbox_options.hide() @@ -583,8 +477,6 @@ class TorSettingsDialog(QtWidgets.QDialog): """ obfs4 bridges option was toggled. If checked, disable custom bridge options. """ - if self.hide_tor_settings: - return if checked: self.tor_bridges_use_custom_textbox_options.hide() @@ -592,8 +484,6 @@ class TorSettingsDialog(QtWidgets.QDialog): """ meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. """ - if self.hide_tor_settings: - return if checked: self.tor_bridges_use_custom_textbox_options.hide() # Alert the user about meek's costliness if it looks like they're turning it on @@ -608,8 +498,6 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Custom bridges option was toggled. If checked, show custom bridge options. """ - if self.hide_tor_settings: - return if checked: self.tor_bridges_use_custom_textbox_options.show() @@ -618,8 +506,6 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type automatic was toggled. If checked, hide authentication fields. """ self.common.log("TorSettingsDialog", "connection_type_automatic_toggled") - if self.hide_tor_settings: - return if checked: self.authenticate_group.hide() self.connection_type_socks.hide() @@ -631,8 +517,6 @@ class TorSettingsDialog(QtWidgets.QDialog): for Tor control address and port. If unchecked, hide those extra fields. """ self.common.log("TorSettingsDialog", "connection_type_control_port_toggled") - if self.hide_tor_settings: - return if checked: self.authenticate_group.show() self.connection_type_control_port_extras.show() @@ -647,8 +531,6 @@ class TorSettingsDialog(QtWidgets.QDialog): for socket file. If unchecked, hide those extra fields. """ self.common.log("TorSettingsDialog", "connection_type_socket_file_toggled") - if self.hide_tor_settings: - return if checked: self.authenticate_group.show() self.connection_type_socket_file_extras.show() @@ -744,68 +626,6 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_status.hide() self._enable_buttons() - def check_for_updates(self): - """ - Check for Updates button clicked. Manually force an update check. - """ - self.common.log("TorSettingsDialog", "check_for_updates") - # Disable buttons - self._disable_buttons() - self.common.gui.qtapp.processEvents() - - def update_timestamp(): - # Update the last checked label - settings = Settings(self.common) - settings.load() - autoupdate_timestamp = settings.get("autoupdate_timestamp") - self._update_autoupdate_timestamp(autoupdate_timestamp) - - def close_forced_update_thread(): - forced_update_thread.quit() - # Enable buttons - self._enable_buttons() - # Update timestamp - update_timestamp() - - # Check for updates - def update_available(update_url, installed_version, latest_version): - Alert( - self.common, - strings._("update_available").format( - update_url, installed_version, latest_version - ), - ) - close_forced_update_thread() - - def update_not_available(): - Alert(self.common, strings._("update_not_available")) - close_forced_update_thread() - - def update_error(): - Alert( - self.common, - strings._("update_error_check_error"), - QtWidgets.QMessageBox.Warning, - ) - close_forced_update_thread() - - def update_invalid_version(latest_version): - Alert( - self.common, - strings._("update_error_invalid_latest_version").format(latest_version), - QtWidgets.QMessageBox.Warning, - ) - close_forced_update_thread() - - forced_update_thread = UpdateThread( - self.common, self.common.gui.onion, force=True - ) - forced_update_thread.update_available.connect(update_available) - forced_update_thread.update_not_available.connect(update_not_available) - forced_update_thread.update_error.connect(update_error) - forced_update_thread.update_invalid_version.connect(update_invalid_version) - forced_update_thread.start() - def save_clicked(self): """ Save button clicked. Save current settings to disk. @@ -824,27 +644,6 @@ class TorSettingsDialog(QtWidgets.QDialog): settings = self.settings_from_fields() if settings: - # If language changed, inform user they need to restart OnionShare - if changed(settings, self.old_settings, ["locale"]): - # Look up error message in different locale - new_locale = settings.get("locale") - if ( - new_locale in strings.translations - and "gui_settings_language_changed_notice" - in strings.translations[new_locale] - ): - notice = strings.translations[new_locale][ - "gui_settings_language_changed_notice" - ] - else: - notice = strings._("gui_settings_language_changed_notice") - Alert(self.common, notice, QtWidgets.QMessageBox.Information) - - # If color mode changed, inform user they need to restart OnionShare - if changed(settings, self.old_settings, ["theme"]): - notice = strings._("gui_color_mode_changed_notice") - Alert(self.common, notice, QtWidgets.QMessageBox.Information) - # Save the new settings settings.save() @@ -934,18 +733,6 @@ class TorSettingsDialog(QtWidgets.QDialog): else: self.close() - def help_clicked(self): - """ - Help button clicked. - """ - self.common.log("TorSettingsDialog", "help_clicked") - TorSettingsDialog.open_help() - - @staticmethod - def open_help(): - help_url = "https://docs.onionshare.org/" - QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url)) - def settings_from_fields(self): """ Return a Settings object that's full of values from the settings dialog. @@ -954,15 +741,6 @@ class TorSettingsDialog(QtWidgets.QDialog): settings = Settings(self.common) settings.load() # To get the last update timestamp - # Theme - theme_index = self.theme_combobox.currentIndex() - settings.set("theme", theme_index) - - # Language - locale_index = self.language_combobox.currentIndex() - locale = self.language_combobox.itemData(locale_index) - settings.set("locale", locale) - # Tor connection if self.connection_type_bundled_radio.isChecked(): settings.set("connection_type", "bundled") @@ -973,11 +751,6 @@ class TorSettingsDialog(QtWidgets.QDialog): if self.connection_type_socket_file_radio.isChecked(): settings.set("connection_type", "socket_file") - if self.autoupdate_checkbox.isChecked(): - settings.set("use_autoupdate", True) - else: - settings.set("use_autoupdate", False) - settings.set( "control_port_address", self.connection_type_control_port_extras_address.text(), @@ -1071,18 +844,6 @@ class TorSettingsDialog(QtWidgets.QDialog): # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) - def _update_autoupdate_timestamp(self, autoupdate_timestamp): - self.common.log("TorSettingsDialog", "_update_autoupdate_timestamp") - - if autoupdate_timestamp: - dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) - last_checked = dt.strftime("%B %d, %Y %H:%M") - else: - last_checked = strings._("gui_settings_autoupdate_timestamp_never") - self.autoupdate_timestamp.setText( - strings._("gui_settings_autoupdate_timestamp").format(last_checked) - ) - def _tor_status_update(self, progress, summary): self.tor_status.setText( f"{strings._('connecting_to_tor')}
{progress}% {summary}" @@ -1095,18 +856,12 @@ class TorSettingsDialog(QtWidgets.QDialog): def _disable_buttons(self): self.common.log("TorSettingsDialog", "_disable_buttons") - self.check_for_updates_button.setEnabled(False) self.connection_type_test_button.setEnabled(False) self.save_button.setEnabled(False) self.cancel_button.setEnabled(False) def _enable_buttons(self): self.common.log("TorSettingsDialog", "_enable_buttons") - # We can't check for updates if we're still not connected to Tor - if not self.common.gui.onion.connected_to_tor: - self.check_for_updates_button.setEnabled(False) - else: - self.check_for_updates_button.setEnabled(True) self.connection_type_test_button.setEnabled(True) self.save_button.setEnabled(True) self.cancel_button.setEnabled(True) From fda4f5ff2694759c051f73e0c8937a6dea264888 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 12 Oct 2021 21:24:18 -0700 Subject: [PATCH 06/70] Rip out Tor settings from SettingsDialog --- desktop/src/onionshare/settings_dialog.py | 738 +--------------------- 1 file changed, 2 insertions(+), 736 deletions(-) diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_dialog.py index 8ef13399..de5c6992 100644 --- a/desktop/src/onionshare/settings_dialog.py +++ b/desktop/src/onionshare/settings_dialog.py @@ -71,9 +71,6 @@ class SettingsDialog(QtWidgets.QDialog): self.system = platform.system() - # If ONIONSHARE_HIDE_TOR_SETTINGS=1, hide Tor settings in the dialog - self.hide_tor_settings = os.environ.get("ONIONSHARE_HIDE_TOR_SETTINGS") == "1" - # Automatic updates options # Autoupdate @@ -138,291 +135,6 @@ class SettingsDialog(QtWidgets.QDialog): theme_layout.addWidget(self.theme_combobox) theme_layout.addStretch() - # Connection type: either automatic, control port, or socket file - - # Bundled Tor - self.connection_type_bundled_radio = QtWidgets.QRadioButton( - strings._("gui_settings_connection_type_bundled_option") - ) - self.connection_type_bundled_radio.toggled.connect( - self.connection_type_bundled_toggled - ) - - # Bundled Tor doesn't work on dev mode in Windows or Mac - if (self.system == "Windows" or self.system == "Darwin") and getattr( - sys, "onionshare_dev_mode", False - ): - self.connection_type_bundled_radio.setEnabled(False) - - # Bridge options for bundled tor - - # No bridges option radio - self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_no_bridges_radio_option") - ) - self.tor_bridges_no_bridges_radio.toggled.connect( - self.tor_bridges_no_bridges_radio_toggled - ) - - ( - self.tor_path, - self.tor_geo_ip_file_path, - self.tor_geo_ipv6_file_path, - self.obfs4proxy_file_path, - self.snowflake_file_path, - ) = self.common.gui.get_tor_paths() - - # obfs4 option radio - # if the obfs4proxy binary is missing, we can't use obfs4 transports - if not self.obfs4proxy_file_path or not os.path.isfile( - self.obfs4proxy_file_path - ): - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy") - ) - self.tor_bridges_use_obfs4_radio.setEnabled(False) - else: - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_obfs4_radio_option") - ) - self.tor_bridges_use_obfs4_radio.toggled.connect( - self.tor_bridges_use_obfs4_radio_toggled - ) - - # meek_lite-azure option radio - # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports - if not self.obfs4proxy_file_path or not os.path.isfile( - self.obfs4proxy_file_path - ): - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( - strings._( - "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy" - ) - ) - self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False) - else: - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option") - ) - self.tor_bridges_use_meek_lite_azure_radio.toggled.connect( - self.tor_bridges_use_meek_lite_azure_radio_toggled - ) - - # Custom bridges radio and textbox - self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_custom_radio_option") - ) - self.tor_bridges_use_custom_radio.toggled.connect( - self.tor_bridges_use_custom_radio_toggled - ) - - self.tor_bridges_use_custom_label = QtWidgets.QLabel( - strings._("gui_settings_tor_bridges_custom_label") - ) - self.tor_bridges_use_custom_label.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - self.tor_bridges_use_custom_label.setOpenExternalLinks(True) - self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() - self.tor_bridges_use_custom_textbox.setMaximumHeight(200) - self.tor_bridges_use_custom_textbox.setPlaceholderText( - "[address:port] [identifier]" - ) - - tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() - tor_bridges_use_custom_textbox_options_layout.addWidget( - self.tor_bridges_use_custom_label - ) - tor_bridges_use_custom_textbox_options_layout.addWidget( - self.tor_bridges_use_custom_textbox - ) - - self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget() - self.tor_bridges_use_custom_textbox_options.setLayout( - tor_bridges_use_custom_textbox_options_layout - ) - self.tor_bridges_use_custom_textbox_options.hide() - - # Bridges layout/widget - bridges_layout = QtWidgets.QVBoxLayout() - bridges_layout.addWidget(self.tor_bridges_no_bridges_radio) - bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio) - bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio) - bridges_layout.addWidget(self.tor_bridges_use_custom_radio) - bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options) - - self.bridges = QtWidgets.QWidget() - self.bridges.setLayout(bridges_layout) - - # Automatic - self.connection_type_automatic_radio = QtWidgets.QRadioButton( - strings._("gui_settings_connection_type_automatic_option") - ) - self.connection_type_automatic_radio.toggled.connect( - self.connection_type_automatic_toggled - ) - - # Control port - self.connection_type_control_port_radio = QtWidgets.QRadioButton( - strings._("gui_settings_connection_type_control_port_option") - ) - self.connection_type_control_port_radio.toggled.connect( - self.connection_type_control_port_toggled - ) - - connection_type_control_port_extras_label = QtWidgets.QLabel( - strings._("gui_settings_control_port_label") - ) - self.connection_type_control_port_extras_address = QtWidgets.QLineEdit() - self.connection_type_control_port_extras_port = QtWidgets.QLineEdit() - connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout() - connection_type_control_port_extras_layout.addWidget( - connection_type_control_port_extras_label - ) - connection_type_control_port_extras_layout.addWidget( - self.connection_type_control_port_extras_address - ) - connection_type_control_port_extras_layout.addWidget( - self.connection_type_control_port_extras_port - ) - - self.connection_type_control_port_extras = QtWidgets.QWidget() - self.connection_type_control_port_extras.setLayout( - connection_type_control_port_extras_layout - ) - self.connection_type_control_port_extras.hide() - - # Socket file - self.connection_type_socket_file_radio = QtWidgets.QRadioButton( - strings._("gui_settings_connection_type_socket_file_option") - ) - self.connection_type_socket_file_radio.toggled.connect( - self.connection_type_socket_file_toggled - ) - - connection_type_socket_file_extras_label = QtWidgets.QLabel( - strings._("gui_settings_socket_file_label") - ) - self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit() - connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout() - connection_type_socket_file_extras_layout.addWidget( - connection_type_socket_file_extras_label - ) - connection_type_socket_file_extras_layout.addWidget( - self.connection_type_socket_file_extras_path - ) - - self.connection_type_socket_file_extras = QtWidgets.QWidget() - self.connection_type_socket_file_extras.setLayout( - connection_type_socket_file_extras_layout - ) - self.connection_type_socket_file_extras.hide() - - # Tor SOCKS address and port - gui_settings_socks_label = QtWidgets.QLabel( - strings._("gui_settings_socks_label") - ) - self.connection_type_socks_address = QtWidgets.QLineEdit() - self.connection_type_socks_port = QtWidgets.QLineEdit() - connection_type_socks_layout = QtWidgets.QHBoxLayout() - connection_type_socks_layout.addWidget(gui_settings_socks_label) - connection_type_socks_layout.addWidget(self.connection_type_socks_address) - connection_type_socks_layout.addWidget(self.connection_type_socks_port) - - self.connection_type_socks = QtWidgets.QWidget() - self.connection_type_socks.setLayout(connection_type_socks_layout) - self.connection_type_socks.hide() - - # Authentication options - - # No authentication - self.authenticate_no_auth_radio = QtWidgets.QRadioButton( - strings._("gui_settings_authenticate_no_auth_option") - ) - self.authenticate_no_auth_radio.toggled.connect( - self.authenticate_no_auth_toggled - ) - - # Password - self.authenticate_password_radio = QtWidgets.QRadioButton( - strings._("gui_settings_authenticate_password_option") - ) - self.authenticate_password_radio.toggled.connect( - self.authenticate_password_toggled - ) - - authenticate_password_extras_label = QtWidgets.QLabel( - strings._("gui_settings_password_label") - ) - self.authenticate_password_extras_password = QtWidgets.QLineEdit("") - authenticate_password_extras_layout = QtWidgets.QHBoxLayout() - authenticate_password_extras_layout.addWidget( - authenticate_password_extras_label - ) - authenticate_password_extras_layout.addWidget( - self.authenticate_password_extras_password - ) - - self.authenticate_password_extras = QtWidgets.QWidget() - self.authenticate_password_extras.setLayout(authenticate_password_extras_layout) - self.authenticate_password_extras.hide() - - # Authentication options layout - authenticate_group_layout = QtWidgets.QVBoxLayout() - authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) - authenticate_group_layout.addWidget(self.authenticate_password_radio) - authenticate_group_layout.addWidget(self.authenticate_password_extras) - self.authenticate_group = QtWidgets.QGroupBox( - strings._("gui_settings_authenticate_label") - ) - self.authenticate_group.setLayout(authenticate_group_layout) - - # Put the radios into their own group so they are exclusive - connection_type_radio_group_layout = QtWidgets.QVBoxLayout() - connection_type_radio_group_layout.addWidget(self.connection_type_bundled_radio) - connection_type_radio_group_layout.addWidget( - self.connection_type_automatic_radio - ) - connection_type_radio_group_layout.addWidget( - self.connection_type_control_port_radio - ) - connection_type_radio_group_layout.addWidget( - self.connection_type_socket_file_radio - ) - connection_type_radio_group = QtWidgets.QGroupBox( - strings._("gui_settings_connection_type_label") - ) - connection_type_radio_group.setLayout(connection_type_radio_group_layout) - - # The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges) - connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout() - connection_type_bridges_radio_group_layout.addWidget(self.bridges) - self.connection_type_bridges_radio_group = QtWidgets.QGroupBox( - strings._("gui_settings_tor_bridges") - ) - self.connection_type_bridges_radio_group.setLayout( - connection_type_bridges_radio_group_layout - ) - self.connection_type_bridges_radio_group.hide() - - # Test tor settings button - self.connection_type_test_button = QtWidgets.QPushButton( - strings._("gui_settings_connection_type_test_button") - ) - self.connection_type_test_button.clicked.connect(self.test_tor_clicked) - connection_type_test_button_layout = QtWidgets.QHBoxLayout() - connection_type_test_button_layout.addWidget(self.connection_type_test_button) - connection_type_test_button_layout.addStretch() - - # Connection type layout - connection_type_layout = QtWidgets.QVBoxLayout() - connection_type_layout.addWidget(self.connection_type_control_port_extras) - connection_type_layout.addWidget(self.connection_type_socket_file_extras) - connection_type_layout.addWidget(self.connection_type_socks) - connection_type_layout.addWidget(self.authenticate_group) - connection_type_layout.addWidget(self.connection_type_bridges_radio_group) - connection_type_layout.addLayout(connection_type_test_button_layout) - # Buttons self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) @@ -441,22 +153,8 @@ class SettingsDialog(QtWidgets.QDialog): buttons_layout.addWidget(self.save_button) buttons_layout.addWidget(self.cancel_button) - # Tor network connection status - self.tor_status = QtWidgets.QLabel() - self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"]) - self.tor_status.hide() - # Layout - tor_layout = QtWidgets.QVBoxLayout() - tor_layout.addWidget(connection_type_radio_group) - tor_layout.addLayout(connection_type_layout) - tor_layout.addWidget(self.tor_status) - tor_layout.addStretch() - layout = QtWidgets.QVBoxLayout() - if not self.hide_tor_settings: - layout.addLayout(tor_layout) - layout.addSpacing(20) layout.addWidget(autoupdate_group) if autoupdate_group.isVisible(): layout.addSpacing(20) @@ -493,257 +191,6 @@ class SettingsDialog(QtWidgets.QDialog): theme_choice = self.old_settings.get("theme") self.theme_combobox.setCurrentIndex(theme_choice) - connection_type = self.old_settings.get("connection_type") - if connection_type == "bundled": - if self.connection_type_bundled_radio.isEnabled(): - self.connection_type_bundled_radio.setChecked(True) - else: - # If bundled tor is disabled, fallback to automatic - self.connection_type_automatic_radio.setChecked(True) - elif connection_type == "automatic": - self.connection_type_automatic_radio.setChecked(True) - elif connection_type == "control_port": - self.connection_type_control_port_radio.setChecked(True) - elif connection_type == "socket_file": - self.connection_type_socket_file_radio.setChecked(True) - self.connection_type_control_port_extras_address.setText( - self.old_settings.get("control_port_address") - ) - self.connection_type_control_port_extras_port.setText( - str(self.old_settings.get("control_port_port")) - ) - self.connection_type_socket_file_extras_path.setText( - self.old_settings.get("socket_file_path") - ) - self.connection_type_socks_address.setText( - self.old_settings.get("socks_address") - ) - self.connection_type_socks_port.setText( - str(self.old_settings.get("socks_port")) - ) - auth_type = self.old_settings.get("auth_type") - if auth_type == "no_auth": - self.authenticate_no_auth_radio.setChecked(True) - elif auth_type == "password": - self.authenticate_password_radio.setChecked(True) - self.authenticate_password_extras_password.setText( - self.old_settings.get("auth_password") - ) - - if self.old_settings.get("no_bridges"): - self.tor_bridges_no_bridges_radio.setChecked(True) - self.tor_bridges_use_obfs4_radio.setChecked(False) - self.tor_bridges_use_meek_lite_azure_radio.setChecked(False) - self.tor_bridges_use_custom_radio.setChecked(False) - else: - self.tor_bridges_no_bridges_radio.setChecked(False) - self.tor_bridges_use_obfs4_radio.setChecked( - self.old_settings.get("tor_bridges_use_obfs4") - ) - self.tor_bridges_use_meek_lite_azure_radio.setChecked( - self.old_settings.get("tor_bridges_use_meek_lite_azure") - ) - - if self.old_settings.get("tor_bridges_use_custom_bridges"): - self.tor_bridges_use_custom_radio.setChecked(True) - # Remove the 'Bridge' lines at the start of each bridge. - # They are added automatically to provide compatibility with - # copying/pasting bridges provided from https://bridges.torproject.org - new_bridges = [] - bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split( - "Bridge " - ) - for bridge in bridges: - new_bridges.append(bridge) - new_bridges = "".join(new_bridges) - self.tor_bridges_use_custom_textbox.setPlainText(new_bridges) - - def connection_type_bundled_toggled(self, checked): - """ - Connection type bundled was toggled. If checked, hide authentication fields. - """ - self.common.log("SettingsDialog", "connection_type_bundled_toggled") - if self.hide_tor_settings: - return - if checked: - self.authenticate_group.hide() - self.connection_type_socks.hide() - self.connection_type_bridges_radio_group.show() - - def tor_bridges_no_bridges_radio_toggled(self, checked): - """ - 'No bridges' option was toggled. If checked, enable other bridge options. - """ - if self.hide_tor_settings: - return - if checked: - self.tor_bridges_use_custom_textbox_options.hide() - - def tor_bridges_use_obfs4_radio_toggled(self, checked): - """ - obfs4 bridges option was toggled. If checked, disable custom bridge options. - """ - if self.hide_tor_settings: - return - if checked: - self.tor_bridges_use_custom_textbox_options.hide() - - def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked): - """ - meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. - """ - if self.hide_tor_settings: - return - if checked: - self.tor_bridges_use_custom_textbox_options.hide() - # Alert the user about meek's costliness if it looks like they're turning it on - if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): - Alert( - self.common, - strings._("gui_settings_meek_lite_expensive_warning"), - QtWidgets.QMessageBox.Warning, - ) - - def tor_bridges_use_custom_radio_toggled(self, checked): - """ - Custom bridges option was toggled. If checked, show custom bridge options. - """ - if self.hide_tor_settings: - return - if checked: - self.tor_bridges_use_custom_textbox_options.show() - - def connection_type_automatic_toggled(self, checked): - """ - Connection type automatic was toggled. If checked, hide authentication fields. - """ - self.common.log("SettingsDialog", "connection_type_automatic_toggled") - if self.hide_tor_settings: - return - if checked: - self.authenticate_group.hide() - self.connection_type_socks.hide() - self.connection_type_bridges_radio_group.hide() - - def connection_type_control_port_toggled(self, checked): - """ - Connection type control port was toggled. If checked, show extra fields - for Tor control address and port. If unchecked, hide those extra fields. - """ - self.common.log("SettingsDialog", "connection_type_control_port_toggled") - if self.hide_tor_settings: - return - if checked: - self.authenticate_group.show() - self.connection_type_control_port_extras.show() - self.connection_type_socks.show() - self.connection_type_bridges_radio_group.hide() - else: - self.connection_type_control_port_extras.hide() - - def connection_type_socket_file_toggled(self, checked): - """ - Connection type socket file was toggled. If checked, show extra fields - for socket file. If unchecked, hide those extra fields. - """ - self.common.log("SettingsDialog", "connection_type_socket_file_toggled") - if self.hide_tor_settings: - return - if checked: - self.authenticate_group.show() - self.connection_type_socket_file_extras.show() - self.connection_type_socks.show() - self.connection_type_bridges_radio_group.hide() - else: - self.connection_type_socket_file_extras.hide() - - def authenticate_no_auth_toggled(self, checked): - """ - Authentication option no authentication was toggled. - """ - self.common.log("SettingsDialog", "authenticate_no_auth_toggled") - - def authenticate_password_toggled(self, checked): - """ - Authentication option password was toggled. If checked, show extra fields - for password auth. If unchecked, hide those extra fields. - """ - self.common.log("SettingsDialog", "authenticate_password_toggled") - if checked: - self.authenticate_password_extras.show() - else: - self.authenticate_password_extras.hide() - - def test_tor_clicked(self): - """ - Test Tor Settings button clicked. With the given settings, see if we can - successfully connect and authenticate to Tor. - """ - self.common.log("SettingsDialog", "test_tor_clicked") - settings = self.settings_from_fields() - - try: - # Show Tor connection status if connection type is bundled tor - if settings.get("connection_type") == "bundled": - self.tor_status.show() - self._disable_buttons() - - def tor_status_update_func(progress, summary): - self._tor_status_update(progress, summary) - return True - - else: - tor_status_update_func = None - - onion = Onion( - self.common, - use_tmp_dir=True, - get_tor_paths=self.common.gui.get_tor_paths, - ) - onion.connect( - custom_settings=settings, - tor_status_update_func=tor_status_update_func, - ) - - # If an exception hasn't been raised yet, the Tor settings work - Alert( - self.common, - strings._("settings_test_success").format( - onion.tor_version, - onion.supports_ephemeral, - onion.supports_stealth, - onion.supports_v3_onions, - ), - ) - - # Clean up - onion.cleanup() - - except ( - TorErrorInvalidSetting, - TorErrorAutomatic, - TorErrorSocketPort, - TorErrorSocketFile, - TorErrorMissingPassword, - TorErrorUnreadableCookieFile, - TorErrorAuthError, - TorErrorProtocolError, - BundledTorTimeout, - BundledTorBroken, - TorTooOldEphemeral, - TorTooOldStealth, - PortNotAvailable, - ) as e: - message = self.common.gui.get_translated_tor_error(e) - Alert( - self.common, - message, - QtWidgets.QMessageBox.Warning, - ) - if settings.get("connection_type") == "bundled": - self.tor_status.hide() - self._enable_buttons() - def check_for_updates(self): """ Check for Updates button clicked. Manually force an update check. @@ -847,74 +294,8 @@ class SettingsDialog(QtWidgets.QDialog): # Save the new settings settings.save() - - # If Tor isn't connected, or if Tor settings have changed, Reinitialize - # the Onion object - reboot_onion = False - if not self.common.gui.local_only: - if self.common.gui.onion.is_authenticated(): - self.common.log( - "SettingsDialog", "save_clicked", "Connected to Tor" - ) - - if changed( - settings, - self.old_settings, - [ - "connection_type", - "control_port_address", - "control_port_port", - "socks_address", - "socks_port", - "socket_file_path", - "auth_type", - "auth_password", - "no_bridges", - "tor_bridges_use_obfs4", - "tor_bridges_use_meek_lite_azure", - "tor_bridges_use_custom_bridges", - ], - ): - - reboot_onion = True - - else: - self.common.log( - "SettingsDialog", "save_clicked", "Not connected to Tor" - ) - # Tor isn't connected, so try connecting - reboot_onion = True - - # Do we need to reinitialize Tor? - if reboot_onion: - # Reinitialize the Onion object - self.common.log( - "SettingsDialog", "save_clicked", "rebooting the Onion" - ) - self.common.gui.onion.cleanup() - - tor_con = TorConnectionDialog(self.common, settings) - tor_con.start() - - self.common.log( - "SettingsDialog", - "save_clicked", - f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", - ) - - if ( - self.common.gui.onion.is_authenticated() - and not tor_con.wasCanceled() - ): - self.settings_saved.emit() - self.close() - - else: - self.settings_saved.emit() - self.close() - else: - self.settings_saved.emit() - self.close() + self.settings_saved.emit() + self.close() def cancel_clicked(self): """ @@ -963,112 +344,8 @@ class SettingsDialog(QtWidgets.QDialog): locale = self.language_combobox.itemData(locale_index) settings.set("locale", locale) - # Tor connection - if self.connection_type_bundled_radio.isChecked(): - settings.set("connection_type", "bundled") - if self.connection_type_automatic_radio.isChecked(): - settings.set("connection_type", "automatic") - if self.connection_type_control_port_radio.isChecked(): - settings.set("connection_type", "control_port") - if self.connection_type_socket_file_radio.isChecked(): - settings.set("connection_type", "socket_file") - - if self.autoupdate_checkbox.isChecked(): - settings.set("use_autoupdate", True) - else: - settings.set("use_autoupdate", False) - - settings.set( - "control_port_address", - self.connection_type_control_port_extras_address.text(), - ) - settings.set( - "control_port_port", self.connection_type_control_port_extras_port.text() - ) - settings.set( - "socket_file_path", self.connection_type_socket_file_extras_path.text() - ) - - settings.set("socks_address", self.connection_type_socks_address.text()) - settings.set("socks_port", self.connection_type_socks_port.text()) - - if self.authenticate_no_auth_radio.isChecked(): - settings.set("auth_type", "no_auth") - if self.authenticate_password_radio.isChecked(): - settings.set("auth_type", "password") - - settings.set("auth_password", self.authenticate_password_extras_password.text()) - - # Whether we use bridges - if self.tor_bridges_no_bridges_radio.isChecked(): - settings.set("no_bridges", True) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_obfs4_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", True) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_meek_lite_azure_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", True) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_custom_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - - # Insert a 'Bridge' line at the start of each bridge. - # This makes it easier to copy/paste a set of bridges - # provided from https://bridges.torproject.org - new_bridges = [] - bridges = self.tor_bridges_use_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=(.+)" - ) - if ( - ipv4_pattern.match(bridge) - or ipv6_pattern.match(bridge) - or meek_lite_pattern.match(bridge) - ): - new_bridges.append("".join(["Bridge ", bridge, "\n"])) - bridges_valid = True - - if bridges_valid: - new_bridges = "".join(new_bridges) - settings.set("tor_bridges_use_custom_bridges", new_bridges) - else: - Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) - settings.set("no_bridges", True) - return False - return settings - def closeEvent(self, e): - self.common.log("SettingsDialog", "closeEvent") - - # On close, if Tor isn't connected, then quit OnionShare altogether - if not self.common.gui.local_only: - if not self.common.gui.onion.is_authenticated(): - self.common.log( - "SettingsDialog", "closeEvent", "Closing while not connected to Tor" - ) - - # Wait 1ms for the event loop to finish, then quit - QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) - def _update_autoupdate_timestamp(self, autoupdate_timestamp): self.common.log("SettingsDialog", "_update_autoupdate_timestamp") @@ -1081,20 +358,10 @@ class SettingsDialog(QtWidgets.QDialog): strings._("gui_settings_autoupdate_timestamp").format(last_checked) ) - def _tor_status_update(self, progress, summary): - self.tor_status.setText( - f"{strings._('connecting_to_tor')}
{progress}% {summary}" - ) - self.common.gui.qtapp.processEvents() - if "Done" in summary: - self.tor_status.hide() - self._enable_buttons() - def _disable_buttons(self): self.common.log("SettingsDialog", "_disable_buttons") self.check_for_updates_button.setEnabled(False) - self.connection_type_test_button.setEnabled(False) self.save_button.setEnabled(False) self.cancel_button.setEnabled(False) @@ -1105,6 +372,5 @@ class SettingsDialog(QtWidgets.QDialog): self.check_for_updates_button.setEnabled(False) else: self.check_for_updates_button.setEnabled(True) - self.connection_type_test_button.setEnabled(True) self.save_button.setEnabled(True) self.cancel_button.setEnabled(True) From 17beca1692105681f7b1fa6b08eeffc6fd73d997 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 19:19:53 -0700 Subject: [PATCH 07/70] Open Tor settings when canceling connecting to tor --- desktop/src/onionshare/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index 0e7e3f88..b3f8b6c9 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -220,7 +220,7 @@ class MainWindow(QtWidgets.QMainWindow): "_tor_connection_canceled", "Settings button clicked", ) - self.open_settings() + self.open_tor_settings() if a.clickedButton() == quit_button: # Quit From c4a038720b5d893a612db79ff1a9c90fc81483a6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 19:49:51 -0700 Subject: [PATCH 08/70] Change some Tor settings language, and combine various settings into a single "Tor settings" group box --- .../src/onionshare/resources/locale/en.json | 6 +- desktop/src/onionshare/tor_settings_dialog.py | 65 +++++++------------ 2 files changed, 27 insertions(+), 44 deletions(-) diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index ca3f8a8f..f0dd7a6a 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -50,18 +50,18 @@ "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", + "gui_settings_controller_extras_label": "Tor settings", "gui_settings_connection_type_control_port_option": "Connect using control port", "gui_settings_connection_type_socket_file_option": "Connect using socket file", "gui_settings_connection_type_test_button": "Test Connection to Tor", "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor authentication settings", "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", - "gui_settings_tor_bridges": "Tor bridge support", - "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges", + "gui_settings_tor_bridges": "Would you like to Use a Tor bridge?", + "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use a bridge", "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports", "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)", "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index fbf93044..817fc599 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -267,23 +267,13 @@ class TorSettingsDialog(QtWidgets.QDialog): self.connection_type_socks.hide() # Authentication options - - # No authentication - self.authenticate_no_auth_radio = QtWidgets.QRadioButton( + self.authenticate_no_auth_checkbox = QtWidgets.QCheckBox( strings._("gui_settings_authenticate_no_auth_option") ) - self.authenticate_no_auth_radio.toggled.connect( + self.authenticate_no_auth_checkbox.toggled.connect( self.authenticate_no_auth_toggled ) - # Password - self.authenticate_password_radio = QtWidgets.QRadioButton( - strings._("gui_settings_authenticate_password_option") - ) - self.authenticate_password_radio.toggled.connect( - self.authenticate_password_toggled - ) - authenticate_password_extras_label = QtWidgets.QLabel( strings._("gui_settings_password_label") ) @@ -300,15 +290,18 @@ class TorSettingsDialog(QtWidgets.QDialog): self.authenticate_password_extras.setLayout(authenticate_password_extras_layout) self.authenticate_password_extras.hide() - # Authentication options layout - authenticate_group_layout = QtWidgets.QVBoxLayout() - authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) - authenticate_group_layout.addWidget(self.authenticate_password_radio) - authenticate_group_layout.addWidget(self.authenticate_password_extras) - self.authenticate_group = QtWidgets.QGroupBox( - strings._("gui_settings_authenticate_label") + # Group for Tor settings + tor_settings_layout = QtWidgets.QVBoxLayout() + tor_settings_layout.addWidget(self.connection_type_control_port_extras) + tor_settings_layout.addWidget(self.connection_type_socket_file_extras) + tor_settings_layout.addWidget(self.connection_type_socks) + tor_settings_layout.addWidget(self.authenticate_no_auth_checkbox) + tor_settings_layout.addWidget(self.authenticate_password_extras) + self.tor_settings_group = QtWidgets.QGroupBox( + strings._("gui_settings_controller_extras_label") ) - self.authenticate_group.setLayout(authenticate_group_layout) + self.tor_settings_group.setLayout(tor_settings_layout) + self.tor_settings_group.hide() # Put the radios into their own group so they are exclusive connection_type_radio_group_layout = QtWidgets.QVBoxLayout() @@ -349,10 +342,7 @@ class TorSettingsDialog(QtWidgets.QDialog): # Connection type layout connection_type_layout = QtWidgets.QVBoxLayout() - connection_type_layout.addWidget(self.connection_type_control_port_extras) - connection_type_layout.addWidget(self.connection_type_socket_file_extras) - connection_type_layout.addWidget(self.connection_type_socks) - connection_type_layout.addWidget(self.authenticate_group) + connection_type_layout.addWidget(self.tor_settings_group) connection_type_layout.addWidget(self.connection_type_bridges_radio_group) connection_type_layout.addLayout(connection_type_test_button_layout) @@ -421,9 +411,9 @@ class TorSettingsDialog(QtWidgets.QDialog): ) auth_type = self.old_settings.get("auth_type") if auth_type == "no_auth": - self.authenticate_no_auth_radio.setChecked(True) - elif auth_type == "password": - self.authenticate_password_radio.setChecked(True) + self.authenticate_no_auth_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.authenticate_no_auth_checkbox.setChecked(QtCore.Qt.Unchecked) self.authenticate_password_extras_password.setText( self.old_settings.get("auth_password") ) @@ -458,11 +448,11 @@ class TorSettingsDialog(QtWidgets.QDialog): def connection_type_bundled_toggled(self, checked): """ - Connection type bundled was toggled. If checked, hide authentication fields. + Connection type bundled was toggled """ self.common.log("TorSettingsDialog", "connection_type_bundled_toggled") if checked: - self.authenticate_group.hide() + self.tor_settings_group.hide() self.connection_type_socks.hide() self.connection_type_bridges_radio_group.show() @@ -507,7 +497,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "connection_type_automatic_toggled") if checked: - self.authenticate_group.hide() + self.tor_settings_group.hide() self.connection_type_socks.hide() self.connection_type_bridges_radio_group.hide() @@ -518,7 +508,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "connection_type_control_port_toggled") if checked: - self.authenticate_group.show() + self.tor_settings_group.show() self.connection_type_control_port_extras.show() self.connection_type_socks.show() self.connection_type_bridges_radio_group.hide() @@ -532,7 +522,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "connection_type_socket_file_toggled") if checked: - self.authenticate_group.show() + self.tor_settings_group.show() self.connection_type_socket_file_extras.show() self.connection_type_socks.show() self.connection_type_bridges_radio_group.hide() @@ -544,17 +534,10 @@ class TorSettingsDialog(QtWidgets.QDialog): Authentication option no authentication was toggled. """ self.common.log("TorSettingsDialog", "authenticate_no_auth_toggled") - - def authenticate_password_toggled(self, checked): - """ - Authentication option password was toggled. If checked, show extra fields - for password auth. If unchecked, hide those extra fields. - """ - self.common.log("TorSettingsDialog", "authenticate_password_toggled") if checked: - self.authenticate_password_extras.show() - else: self.authenticate_password_extras.hide() + else: + self.authenticate_password_extras.show() def test_tor_clicked(self): """ From 496adf01d2d097218e23c37a5b664ba587c96e91 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 20:14:38 -0700 Subject: [PATCH 09/70] When you click "Test Connecting to Tor" in Tor settings, it now uses the TorConnectionDialog --- .../src/onionshare/tor_connection_dialog.py | 53 +++++++---- desktop/src/onionshare/tor_settings_dialog.py | 93 +++---------------- desktop/src/onionshare/widgets.py | 3 +- 3 files changed, 50 insertions(+), 99 deletions(-) diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection_dialog.py index b5c2f61c..dd2721bd 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection_dialog.py @@ -49,11 +49,15 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): """ open_settings = QtCore.Signal() + success = QtCore.Signal() - def __init__(self, common, custom_settings=False): + def __init__( + self, common, custom_settings=False, testing_settings=False, onion=None + ): super(TorConnectionDialog, self).__init__(None) self.common = common + self.testing_settings = testing_settings if custom_settings: self.settings = custom_settings @@ -62,7 +66,15 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.common.log("TorConnectionDialog", "__init__") - self.setWindowTitle("OnionShare") + if self.testing_settings: + self.title = strings._("gui_settings_connection_type_test_button") + self.onion = onion + else: + self.title = "OnionShare" + self.onion = self.common.gui.onion + + self.setWindowTitle(self.title) + self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) self.setModal(True) self.setFixedSize(400, 150) @@ -112,7 +124,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def _canceled_connecting_to_tor(self): self.common.log("TorConnectionDialog", "_canceled_connecting_to_tor") self.active = False - self.common.gui.onion.cleanup() + self.onion.cleanup() # Cancel connecting to Tor QtCore.QTimer.singleShot(1, self.cancel) @@ -121,18 +133,25 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.common.log("TorConnectionDialog", "_error_connecting_to_tor") self.active = False - def alert_and_open_settings(): - # Display the exception in an alert box - Alert( - self.common, - f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}", - QtWidgets.QMessageBox.Warning, - ) + if self.testing_settings: + # If testing, just display the error but don't open settings + def alert(): + Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title) - # Open settings - self.open_settings.emit() + else: + # If not testing, open settings after displaying the error + def alert(): + Alert( + self.common, + f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}", + QtWidgets.QMessageBox.Warning, + title=self.title, + ) - QtCore.QTimer.singleShot(1, alert_and_open_settings) + # Open settings + self.open_settings.emit() + + QtCore.QTimer.singleShot(1, alert) # Cancel connecting to Tor QtCore.QTimer.singleShot(1, self.cancel) @@ -146,13 +165,9 @@ class TorConnectionThread(QtCore.QThread): def __init__(self, common, settings, dialog): super(TorConnectionThread, self).__init__() - self.common = common - self.common.log("TorConnectionThread", "__init__") - self.settings = settings - self.dialog = dialog def run(self): @@ -160,8 +175,8 @@ class TorConnectionThread(QtCore.QThread): # Connect to the Onion try: - self.common.gui.onion.connect(self.settings, False, self._tor_status_update) - if self.common.gui.onion.connected_to_tor: + self.dialog.onion.connect(self.settings, False, self._tor_status_update) + if self.dialog.onion.connected_to_tor: self.connected_to_tor.emit() else: self.canceled_connecting_to_tor.emit() diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 817fc599..60b006be 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -358,16 +358,10 @@ class TorSettingsDialog(QtWidgets.QDialog): buttons_layout.addWidget(self.save_button) buttons_layout.addWidget(self.cancel_button) - # Tor network connection status - self.tor_status = QtWidgets.QLabel() - self.tor_status.setStyleSheet(self.common.gui.css["settings_tor_status"]) - self.tor_status.hide() - # Layout layout = QtWidgets.QVBoxLayout() layout.addWidget(connection_type_radio_group) layout.addLayout(connection_type_layout) - layout.addWidget(self.tor_status) layout.addStretch() layout.addLayout(buttons_layout) @@ -547,30 +541,17 @@ class TorSettingsDialog(QtWidgets.QDialog): self.common.log("TorSettingsDialog", "test_tor_clicked") settings = self.settings_from_fields() - try: - # Show Tor connection status if connection type is bundled tor - if settings.get("connection_type") == "bundled": - self.tor_status.show() - self._disable_buttons() + onion = Onion( + self.common, + use_tmp_dir=True, + get_tor_paths=self.common.gui.get_tor_paths, + ) - def tor_status_update_func(progress, summary): - self._tor_status_update(progress, summary) - return True + tor_con = TorConnectionDialog(self.common, settings, True, onion) + tor_con.start() - else: - tor_status_update_func = None - - onion = Onion( - self.common, - use_tmp_dir=True, - get_tor_paths=self.common.gui.get_tor_paths, - ) - onion.connect( - custom_settings=settings, - tor_status_update_func=tor_status_update_func, - ) - - # If an exception hasn't been raised yet, the Tor settings work + # If Tor settings worked, show results + if onion.connected_to_tor: Alert( self.common, strings._("settings_test_success").format( @@ -579,35 +560,11 @@ class TorSettingsDialog(QtWidgets.QDialog): onion.supports_stealth, onion.supports_v3_onions, ), + title=strings._("gui_settings_connection_type_test_button"), ) - # Clean up - onion.cleanup() - - except ( - TorErrorInvalidSetting, - TorErrorAutomatic, - TorErrorSocketPort, - TorErrorSocketFile, - TorErrorMissingPassword, - TorErrorUnreadableCookieFile, - TorErrorAuthError, - TorErrorProtocolError, - BundledTorTimeout, - BundledTorBroken, - TorTooOldEphemeral, - TorTooOldStealth, - PortNotAvailable, - ) as e: - message = self.common.gui.get_translated_tor_error(e) - Alert( - self.common, - message, - QtWidgets.QMessageBox.Warning, - ) - if settings.get("connection_type") == "bundled": - self.tor_status.hide() - self._enable_buttons() + # Clean up + onion.cleanup() def save_clicked(self): """ @@ -748,9 +705,9 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("socks_address", self.connection_type_socks_address.text()) settings.set("socks_port", self.connection_type_socks_port.text()) - if self.authenticate_no_auth_radio.isChecked(): + if self.authenticate_no_auth_checkbox.checkState() == QtCore.Qt.Checked: settings.set("auth_type", "no_auth") - if self.authenticate_password_radio.isChecked(): + else: settings.set("auth_type", "password") settings.set("auth_password", self.authenticate_password_extras_password.text()) @@ -826,25 +783,3 @@ class TorSettingsDialog(QtWidgets.QDialog): # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) - - def _tor_status_update(self, progress, summary): - self.tor_status.setText( - f"{strings._('connecting_to_tor')}
{progress}% {summary}" - ) - self.common.gui.qtapp.processEvents() - if "Done" in summary: - self.tor_status.hide() - self._enable_buttons() - - def _disable_buttons(self): - self.common.log("TorSettingsDialog", "_disable_buttons") - - self.connection_type_test_button.setEnabled(False) - self.save_button.setEnabled(False) - self.cancel_button.setEnabled(False) - - def _enable_buttons(self): - self.common.log("TorSettingsDialog", "_enable_buttons") - self.connection_type_test_button.setEnabled(True) - self.save_button.setEnabled(True) - self.cancel_button.setEnabled(True) diff --git a/desktop/src/onionshare/widgets.py b/desktop/src/onionshare/widgets.py index b396c43f..761df212 100644 --- a/desktop/src/onionshare/widgets.py +++ b/desktop/src/onionshare/widgets.py @@ -37,6 +37,7 @@ class Alert(QtWidgets.QMessageBox): icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True, + title="OnionShare", ): super(Alert, self).__init__(None) @@ -44,7 +45,7 @@ class Alert(QtWidgets.QMessageBox): self.common.log("Alert", "__init__") - self.setWindowTitle("OnionShare") + self.setWindowTitle(title) self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) self.setText(message) self.setIcon(icon) From 67d61d2c5374ffaf14b585f6d3ced16a99283a82 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 20:27:17 -0700 Subject: [PATCH 10/70] Move test tor button to the bottons layout at the bottom --- desktop/src/onionshare/gui_common.py | 12 +----------- desktop/src/onionshare/tor_settings_dialog.py | 15 +++++---------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 1dffab26..a9539714 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -392,25 +392,15 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", - # Settings dialog + # Settings dialogs "settings_version": """ QLabel { color: #666666; }""", - "settings_tor_status": """ - QLabel { - background-color: #ffffff; - color: #000000; - padding: 10px; - }""", "settings_whats_this": """ QLabel { font-size: 12px; }""", - "settings_connect_to_tor": """ - QLabel { - font-style: italic; - }""", } def get_tor_paths(self): diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 60b006be..a1b02313 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -331,22 +331,16 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.connection_type_bridges_radio_group.hide() - # Test tor settings button - self.connection_type_test_button = QtWidgets.QPushButton( - strings._("gui_settings_connection_type_test_button") - ) - self.connection_type_test_button.clicked.connect(self.test_tor_clicked) - connection_type_test_button_layout = QtWidgets.QHBoxLayout() - connection_type_test_button_layout.addWidget(self.connection_type_test_button) - connection_type_test_button_layout.addStretch() - # Connection type layout connection_type_layout = QtWidgets.QVBoxLayout() connection_type_layout.addWidget(self.tor_settings_group) connection_type_layout.addWidget(self.connection_type_bridges_radio_group) - connection_type_layout.addLayout(connection_type_test_button_layout) # Buttons + self.test_tor_button = QtWidgets.QPushButton( + strings._("gui_settings_connection_type_test_button") + ) + self.test_tor_button.clicked.connect(self.test_tor_clicked) self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) self.cancel_button = QtWidgets.QPushButton( @@ -354,6 +348,7 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addWidget(self.test_tor_button) buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) buttons_layout.addWidget(self.cancel_button) From f2dbc972857a7963c82801ebae676b4a70f41c81 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 20:34:11 -0700 Subject: [PATCH 11/70] Improve layout of SettingsDialog --- desktop/src/onionshare/gui_common.py | 2 +- .../src/onionshare/resources/locale/en.json | 2 +- desktop/src/onionshare/settings_dialog.py | 20 ++++++++++++------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index a9539714..cb10eb3b 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -395,7 +395,7 @@ class GuiCommon: # Settings dialogs "settings_version": """ QLabel { - color: #666666; + font-size: 16px; }""", "settings_whats_this": """ QLabel { diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index f0dd7a6a..38da16fb 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -126,7 +126,7 @@ "error_cannot_create_data_dir": "Could not create OnionShare data folder: {}", "gui_receive_mode_warning": "Receive mode lets people upload files to your computer.

Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.", "gui_open_folder_error": "Failed to open folder with xdg-open. The file is here: {}", - "gui_settings_language_label": "Preferred language", + "gui_settings_language_label": "Language", "gui_settings_theme_label": "Theme", "gui_settings_theme_auto": "Auto", "gui_settings_theme_light": "Light", diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_dialog.py index de5c6992..5bb7be1c 100644 --- a/desktop/src/onionshare/settings_dialog.py +++ b/desktop/src/onionshare/settings_dialog.py @@ -71,6 +71,17 @@ class SettingsDialog(QtWidgets.QDialog): self.system = platform.system() + # Header + version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") + version_label.setStyleSheet(self.common.gui.css["settings_version"]) + self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help")) + self.help_button.clicked.connect(self.help_clicked) + header_layout = QtWidgets.QHBoxLayout() + header_layout.addStretch() + header_layout.addWidget(version_label) + header_layout.addWidget(self.help_button) + header_layout.addStretch() + # Automatic updates options # Autoupdate @@ -142,24 +153,19 @@ class SettingsDialog(QtWidgets.QDialog): strings._("gui_settings_button_cancel") ) self.cancel_button.clicked.connect(self.cancel_clicked) - version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") - version_label.setStyleSheet(self.common.gui.css["settings_version"]) - self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help")) - self.help_button.clicked.connect(self.help_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addWidget(version_label) - buttons_layout.addWidget(self.help_button) buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) buttons_layout.addWidget(self.cancel_button) # Layout layout = QtWidgets.QVBoxLayout() + layout.addLayout(header_layout) + layout.addSpacing(20) layout.addWidget(autoupdate_group) if autoupdate_group.isVisible(): layout.addSpacing(20) layout.addLayout(language_layout) - layout.addSpacing(20) layout.addLayout(theme_layout) layout.addSpacing(20) layout.addStretch() From 39d624e923d61dbbaf8d4216a4364c5b443d8802 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 13 Oct 2021 21:11:56 -0700 Subject: [PATCH 12/70] Allow selecting a snowflake bridge, and make it try to use the snowflake bridge --- cli/onionshare_cli/common.py | 6 ++ cli/onionshare_cli/onion.py | 5 ++ .../resources/torrc_template-meek_lite_amazon | 2 - cli/onionshare_cli/settings.py | 1 + desktop/src/onionshare/gui_common.py | 16 +++- .../src/onionshare/resources/locale/en.json | 12 +-- desktop/src/onionshare/tor_settings_dialog.py | 88 ++++++++++++++----- 7 files changed, 98 insertions(+), 32 deletions(-) delete mode 100644 cli/onionshare_cli/resources/torrc_template-meek_lite_amazon diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index 78da8882..945a75bb 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -312,6 +312,9 @@ class Common: # Look in resources first base_path = self.get_resource_path("tor") if os.path.exists(base_path): + self.log( + "Common", "get_tor_paths", f"using tor binaries in {base_path}" + ) tor_path = os.path.join(base_path, "tor") tor_geo_ip_file_path = os.path.join(base_path, "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6") @@ -319,6 +322,9 @@ class Common: snowflake_file_path = os.path.join(base_path, "snowflake-client") else: # Fallback to looking in the path + self.log( + "Common", "get_tor_paths", f"using tor binaries in system path" + ) tor_path = shutil.which("tor") if not tor_path: raise CannotFindTor() diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index a0f967b9..a4453651 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -326,6 +326,11 @@ class Onion(object): ) as o: for line in o: f.write(line) + elif self.settings.get("tor_bridges_use_snowflake"): + # Taken from: tor-browser_en-US/Browser/TorBrowser/Data/Tor/torrc-defaults + f.write( + f"ClientTransportPlugin snowflake exec {self.snowflake_file_path} -url https://snowflake-broker.torproject.net.global.prod.fastly.net/ -front cdn.sstatic.net -ice stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478\n" + ) if self.settings.get("tor_bridges_use_custom_bridges"): if "obfs4" in self.settings.get("tor_bridges_use_custom_bridges"): diff --git a/cli/onionshare_cli/resources/torrc_template-meek_lite_amazon b/cli/onionshare_cli/resources/torrc_template-meek_lite_amazon deleted file mode 100644 index 606ae889..00000000 --- a/cli/onionshare_cli/resources/torrc_template-meek_lite_amazon +++ /dev/null @@ -1,2 +0,0 @@ -Bridge meek_lite 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com -UseBridges 1 \ No newline at end of file diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index 4755d5b3..37c00bb6 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -108,6 +108,7 @@ class Settings(object): "no_bridges": True, "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_snowflake": False, "tor_bridges_use_custom_bridges": "", "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index cb10eb3b..5634d5f6 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -405,7 +405,21 @@ class GuiCommon: def get_tor_paths(self): if self.common.platform == "Linux": - return self.common.get_tor_paths() + base_path = self.get_resource_path("tor") + if os.path.exists(base_path): + tor_path = os.path.join(base_path, "tor") + tor_geo_ip_file_path = os.path.join(base_path, "geoip") + 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") + 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") + 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") if self.common.platform == "Windows": base_path = self.get_resource_path("tor") diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 38da16fb..3d6c8539 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -60,13 +60,13 @@ "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", - "gui_settings_tor_bridges": "Would you like to Use a Tor bridge?", + "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_tor_bridges_no_bridges_radio_option": "Don't use a bridge", - "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports", - "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)", - "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports", - "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)", - "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite 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.", + "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 bridge", + "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek-azure bridge", + "gui_settings_tor_bridges_snowflake_radio_option": "Use built-in snowflake 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.", "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges", "gui_settings_tor_bridges_custom_label": "You can get bridges from https://bridges.torproject.org", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index a1b02313..00d221a7 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -89,6 +89,9 @@ class TorSettingsDialog(QtWidgets.QDialog): # Bridge options for bundled tor + bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label")) + bridges_label.setWordWrap(True) + # No bridges option radio self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton( strings._("gui_settings_tor_bridges_no_bridges_radio_option") @@ -107,39 +110,55 @@ class TorSettingsDialog(QtWidgets.QDialog): # obfs4 option radio # if the obfs4proxy binary is missing, we can't use obfs4 transports - if not self.obfs4proxy_file_path or not os.path.isfile( - self.obfs4proxy_file_path - ): - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy") - ) - self.tor_bridges_use_obfs4_radio.setEnabled(False) - else: - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_obfs4_radio_option") - ) + self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_obfs4_radio_option") + ) self.tor_bridges_use_obfs4_radio.toggled.connect( self.tor_bridges_use_obfs4_radio_toggled ) - - # meek_lite-azure option radio - # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports if not self.obfs4proxy_file_path or not os.path.isfile( self.obfs4proxy_file_path ): - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( - strings._( - "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy" - ) - ) - self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False) - else: - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option") + self.common.log( + "TorSettingsDialog", + "__init__", + f"missing binary {self.obfs4proxy_file_path}, hiding obfs4 bridge", ) + self.tor_bridges_use_obfs4_radio.hide() + + # meek-azure option radio + # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports + self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option") + ) self.tor_bridges_use_meek_lite_azure_radio.toggled.connect( self.tor_bridges_use_meek_lite_azure_radio_toggled ) + if not self.obfs4proxy_file_path or not os.path.isfile( + self.obfs4proxy_file_path + ): + self.common.log( + "TorSettingsDialog", + "__init__", + f"missing binary {self.obfs4proxy_file_path}, hiding meek-azure bridge", + ) + self.tor_bridges_use_meek_lite_azure_radio.hide() + + # snowflake option radio + # if the snowflake-client binary is missing, we can't use snowflake transports + self.tor_bridges_use_snowflake_radio = QtWidgets.QRadioButton( + strings._("gui_settings_tor_bridges_snowflake_radio_option") + ) + self.tor_bridges_use_snowflake_radio.toggled.connect( + self.tor_bridges_use_snowflake_radio_toggled + ) + if not self.snowflake_file_path or not os.path.isfile(self.snowflake_file_path): + self.common.log( + "TorSettingsDialog", + "__init__", + f"missing binary {self.snowflake_file_path}, hiding snowflake bridge", + ) + self.tor_bridges_use_snowflake_radio.hide() # Custom bridges radio and textbox self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton( @@ -178,9 +197,11 @@ class TorSettingsDialog(QtWidgets.QDialog): # Bridges layout/widget bridges_layout = QtWidgets.QVBoxLayout() + bridges_layout.addWidget(bridges_label) bridges_layout.addWidget(self.tor_bridges_no_bridges_radio) bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio) bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio) + bridges_layout.addWidget(self.tor_bridges_use_snowflake_radio) bridges_layout.addWidget(self.tor_bridges_use_custom_radio) bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options) @@ -411,6 +432,7 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_bridges_no_bridges_radio.setChecked(True) self.tor_bridges_use_obfs4_radio.setChecked(False) self.tor_bridges_use_meek_lite_azure_radio.setChecked(False) + self.tor_bridges_use_snowflake_radio.setChecked(False) self.tor_bridges_use_custom_radio.setChecked(False) else: self.tor_bridges_no_bridges_radio.setChecked(False) @@ -420,6 +442,9 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_bridges_use_meek_lite_azure_radio.setChecked( self.old_settings.get("tor_bridges_use_meek_lite_azure") ) + self.tor_bridges_use_snowflake_radio.setChecked( + self.old_settings.get("tor_bridges_use_snowflake") + ) if self.old_settings.get("tor_bridges_use_custom_bridges"): self.tor_bridges_use_custom_radio.setChecked(True) @@ -473,6 +498,13 @@ class TorSettingsDialog(QtWidgets.QDialog): QtWidgets.QMessageBox.Warning, ) + def tor_bridges_use_snowflake_radio_toggled(self, checked): + """ + snowflake bridges option was toggled. If checked, disable custom bridge options. + """ + if checked: + self.tor_bridges_use_custom_textbox_options.hide() + def tor_bridges_use_custom_radio_toggled(self, checked): """ Custom bridges option was toggled. If checked, show custom bridge options. @@ -712,21 +744,31 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("no_bridges", True) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) settings.set("tor_bridges_use_custom_bridges", "") if self.tor_bridges_use_obfs4_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", True) settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) settings.set("tor_bridges_use_custom_bridges", "") if self.tor_bridges_use_meek_lite_azure_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", True) + settings.set("tor_bridges_use_snowflake", False) + settings.set("tor_bridges_use_custom_bridges", "") + if self.tor_bridges_use_snowflake_radio.isChecked(): + settings.set("no_bridges", False) + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", True) settings.set("tor_bridges_use_custom_bridges", "") if self.tor_bridges_use_custom_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) # Insert a 'Bridge' line at the start of each bridge. # This makes it easier to copy/paste a set of bridges From c9fa2308a7c9fb99b40d1c7b8c112c5b9f510d75 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 15 Oct 2021 14:24:27 +1100 Subject: [PATCH 13/70] Add early (non-domain-fronted!) methods for interacting with the planned Tor censorship circumvention moat endpoints. This is based on loose specs from https://gitlab.torproject.org/tpo/anti-censorship/bridgedb/-/issues/40025 --- cli/onionshare_cli/common.py | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index dd92eb0b..195de2fe 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -22,6 +22,7 @@ import hashlib import os import platform import random +import requests import socket import sys import threading @@ -504,6 +505,74 @@ 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): """ From 2ffd15ae82055585d963a6f59ae110b11f1b9f9a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 09:17:03 -0700 Subject: [PATCH 14/70] Move ClientTransportPlugin into normal torrc file, and fix snowflake support --- cli/onionshare_cli/onion.py | 33 +++++++------------ cli/onionshare_cli/resources/torrc_template | 4 +++ .../resources/torrc_template-meek_lite_azure | 3 +- .../resources/torrc_template-obfs4 | 1 + .../resources/torrc_template-snowflake | 3 ++ 5 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 cli/onionshare_cli/resources/torrc_template-snowflake diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index a4453651..f8fcf68e 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -303,47 +303,38 @@ class Onion(object): torrc_template = torrc_template.replace( "{{socks_port}}", str(self.tor_socks_port) ) + torrc_template = torrc_template.replace( + "{{obfs4proxy_path}}", str(self.obfs4proxy_file_path) + ) + torrc_template = torrc_template.replace( + "{{snowflake_path}}", str(self.snowflake_file_path) + ) with open(self.tor_torrc, "w") as f: f.write(torrc_template) # Bridge support if self.settings.get("tor_bridges_use_obfs4"): - f.write( - f"ClientTransportPlugin obfs4 exec {self.obfs4proxy_file_path}\n" - ) with open( self.common.get_resource_path("torrc_template-obfs4") ) as o: for line in o: f.write(line) elif self.settings.get("tor_bridges_use_meek_lite_azure"): - f.write( - f"ClientTransportPlugin meek_lite exec {self.obfs4proxy_file_path}\n" - ) with open( self.common.get_resource_path("torrc_template-meek_lite_azure") ) as o: for line in o: f.write(line) elif self.settings.get("tor_bridges_use_snowflake"): - # Taken from: tor-browser_en-US/Browser/TorBrowser/Data/Tor/torrc-defaults - f.write( - f"ClientTransportPlugin snowflake exec {self.snowflake_file_path} -url https://snowflake-broker.torproject.net.global.prod.fastly.net/ -front cdn.sstatic.net -ice stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478\n" - ) + with open( + self.common.get_resource_path("torrc_template-snowflake") + ) as o: + for line in o: + f.write(line) if self.settings.get("tor_bridges_use_custom_bridges"): - if "obfs4" in self.settings.get("tor_bridges_use_custom_bridges"): - f.write( - f"ClientTransportPlugin obfs4 exec {self.obfs4proxy_file_path}\n" - ) - elif "meek_lite" in self.settings.get( - "tor_bridges_use_custom_bridges" - ): - f.write( - f"ClientTransportPlugin meek_lite exec {self.obfs4proxy_file_path}\n" - ) - f.write(self.settings.get("tor_bridges_use_custom_bridges")) + f.write(self.settings.get("tor_bridges_use_custom_bridges") + "\n") f.write("\nUseBridges 1") # Execute a tor subprocess diff --git a/cli/onionshare_cli/resources/torrc_template b/cli/onionshare_cli/resources/torrc_template index 8ac9e1ef..70e1cb35 100644 --- a/cli/onionshare_cli/resources/torrc_template +++ b/cli/onionshare_cli/resources/torrc_template @@ -6,3 +6,7 @@ AvoidDiskWrites 1 Log notice stdout GeoIPFile {{geo_ip_file}} GeoIPv6File {{geo_ipv6_file}} + +# Bridge configurations +ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit exec {{obfs4proxy_path}} +ClientTransportPlugin snowflake exec {{snowflake_path}} -url https://snowflake-broker.torproject.net.global.prod.fastly.net/ -front cdn.sstatic.net -ice stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 diff --git a/cli/onionshare_cli/resources/torrc_template-meek_lite_azure b/cli/onionshare_cli/resources/torrc_template-meek_lite_azure index a9b374ba..6f601681 100644 --- a/cli/onionshare_cli/resources/torrc_template-meek_lite_azure +++ b/cli/onionshare_cli/resources/torrc_template-meek_lite_azure @@ -1,2 +1,3 @@ +# Enable built-in meek-azure bridge Bridge meek_lite 0.0.2.0:3 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com -UseBridges 1 \ No newline at end of file +UseBridges 1 diff --git a/cli/onionshare_cli/resources/torrc_template-obfs4 b/cli/onionshare_cli/resources/torrc_template-obfs4 index 8c52a011..720cc28c 100644 --- a/cli/onionshare_cli/resources/torrc_template-obfs4 +++ b/cli/onionshare_cli/resources/torrc_template-obfs4 @@ -1,3 +1,4 @@ +# Enable built-in obfs4-bridge Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1 Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1 Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1 diff --git a/cli/onionshare_cli/resources/torrc_template-snowflake b/cli/onionshare_cli/resources/torrc_template-snowflake new file mode 100644 index 00000000..4100d3be --- /dev/null +++ b/cli/onionshare_cli/resources/torrc_template-snowflake @@ -0,0 +1,3 @@ +# Enable built-in snowflake bridge +Bridge snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72 +UseBridges 1 From 64973a00ec05e9201c4e2b7883f4c688eb92d0ab Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 09:21:58 -0700 Subject: [PATCH 15/70] Fix CLI tests --- cli/tests/test_cli_common.py | 9 +++++++++ cli/tests/test_cli_settings.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cli/tests/test_cli_common.py b/cli/tests/test_cli_common.py index 9f113a84..a4798d1b 100644 --- a/cli/tests/test_cli_common.py +++ b/cli/tests/test_cli_common.py @@ -162,11 +162,15 @@ class TestGetTorPaths: tor_geo_ip_file_path = os.path.join(base_path, "Resources", "Tor", "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "Resources", "Tor", "geoip6") obfs4proxy_file_path = os.path.join(base_path, "Resources", "Tor", "obfs4proxy") + snowflake_file_path = os.path.join( + base_path, "Resources", "Tor", "snowflake-client" + ) assert common_obj.get_tor_paths() == ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, ) @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") @@ -176,6 +180,7 @@ class TestGetTorPaths: tor_geo_ip_file_path, tor_geo_ipv6_file_path, _, # obfs4proxy is optional + _, # snowflake-client is optional ) = common_obj.get_tor_paths() assert os.path.basename(tor_path) == "tor" @@ -199,6 +204,9 @@ class TestGetTorPaths: obfs4proxy_file_path = os.path.join( os.path.join(base_path, "Tor"), "obfs4proxy.exe" ) + snowflake_file_path = os.path.join( + os.path.join(base_path, "Tor"), "snowflake-client.exe" + ) tor_geo_ip_file_path = os.path.join( os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip" ) @@ -210,6 +218,7 @@ class TestGetTorPaths: tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, ) diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index ed8d5bb9..1e00f22d 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -32,9 +32,10 @@ class TestSettings: "no_bridges": True, "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_snowflake": False, "tor_bridges_use_custom_bridges": "", "persistent_tabs": [], - "theme":0 + "theme": 0, } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing From 66a744c9da3339934430828078e4f24372f67fc7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 14:14:12 -0700 Subject: [PATCH 16/70] Improve the look of the Settings dialog, displaying the version and help link --- desktop/src/onionshare/gui_common.py | 9 ---- .../src/onionshare/resources/locale/en.json | 6 ++- desktop/src/onionshare/settings_dialog.py | 24 +++++------ desktop/src/onionshare/tor_settings_dialog.py | 43 ++++++++++++++----- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 5634d5f6..3559eb92 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -392,15 +392,6 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", - # Settings dialogs - "settings_version": """ - QLabel { - font-size: 16px; - }""", - "settings_whats_this": """ - QLabel { - font-size: 12px; - }""", } def get_tor_paths(self): diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 3d6c8539..13326c4c 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -67,12 +67,14 @@ "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek-azure bridge", "gui_settings_tor_bridges_snowflake_radio_option": "Use built-in snowflake 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.", - "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges", - "gui_settings_tor_bridges_custom_label": "You can get bridges from https://bridges.torproject.org", + "gui_settings_tor_bridges_moat_radio_option": "Request a bridge from torproject.org", + "gui_settings_tor_bridges_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", + "gui_settings_version_label": "You are using OnionShare {}", + "gui_settings_help_label": "Need help? See docs.onionshare.org", "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.", "connecting_to_tor": "Connecting to the Tor network", "update_available": "New OnionShare out. Click here to get it.

You are using {} and the latest is {}.", diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_dialog.py index 5bb7be1c..b1003386 100644 --- a/desktop/src/onionshare/settings_dialog.py +++ b/desktop/src/onionshare/settings_dialog.py @@ -71,17 +71,6 @@ class SettingsDialog(QtWidgets.QDialog): self.system = platform.system() - # Header - version_label = QtWidgets.QLabel(f"OnionShare {self.common.version}") - version_label.setStyleSheet(self.common.gui.css["settings_version"]) - self.help_button = QtWidgets.QPushButton(strings._("gui_settings_button_help")) - self.help_button.clicked.connect(self.help_clicked) - header_layout = QtWidgets.QHBoxLayout() - header_layout.addStretch() - header_layout.addWidget(version_label) - header_layout.addWidget(self.help_button) - header_layout.addStretch() - # Automatic updates options # Autoupdate @@ -146,6 +135,14 @@ class SettingsDialog(QtWidgets.QDialog): theme_layout.addWidget(self.theme_combobox) theme_layout.addStretch() + # Version and help + version_label = QtWidgets.QLabel( + strings._("gui_settings_version_label").format(self.common.version) + ) + help_label = QtWidgets.QLabel(strings._("gui_settings_help_label")) + help_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + help_label.setOpenExternalLinks(True) + # Buttons self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) @@ -160,8 +157,6 @@ class SettingsDialog(QtWidgets.QDialog): # Layout layout = QtWidgets.QVBoxLayout() - layout.addLayout(header_layout) - layout.addSpacing(20) layout.addWidget(autoupdate_group) if autoupdate_group.isVisible(): layout.addSpacing(20) @@ -169,6 +164,9 @@ class SettingsDialog(QtWidgets.QDialog): layout.addLayout(theme_layout) layout.addSpacing(20) layout.addStretch() + layout.addWidget(version_label) + layout.addWidget(help_label) + layout.addSpacing(20) layout.addLayout(buttons_layout) self.setLayout(layout) diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 00d221a7..dc16e822 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -160,6 +160,38 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.tor_bridges_use_snowflake_radio.hide() + # Request a bridge from torproject.org (moat) + # self.tor_bridges_use_moat_radio = QtWidgets.QRadioButton( + # strings._("gui_settings_tor_bridges_moat_radio_option") + # ) + # self.tor_bridges_use_moat_radio.toggled.connect( + # self.tor_bridges_use_moat_radio_toggled + # ) + + # self.tor_bridges_use_moat_label = QtWidgets.QLabel( + # strings._("gui_settings_tor_bridges_moat_label") + # ) + # self.tor_bridges_use_custom_label.setOpenExternalLinks(True) + # self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() + # self.tor_bridges_use_custom_textbox.setMaximumHeight(200) + # self.tor_bridges_use_custom_textbox.setPlaceholderText( + # "[address:port] [identifier]" + # ) + + # tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() + # tor_bridges_use_custom_textbox_options_layout.addWidget( + # self.tor_bridges_use_custom_label + # ) + # tor_bridges_use_custom_textbox_options_layout.addWidget( + # self.tor_bridges_use_custom_textbox + # ) + + # self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget() + # self.tor_bridges_use_custom_textbox_options.setLayout( + # tor_bridges_use_custom_textbox_options_layout + # ) + # self.tor_bridges_use_custom_textbox_options.hide() + # Custom bridges radio and textbox self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton( strings._("gui_settings_tor_bridges_custom_radio_option") @@ -167,14 +199,6 @@ class TorSettingsDialog(QtWidgets.QDialog): self.tor_bridges_use_custom_radio.toggled.connect( self.tor_bridges_use_custom_radio_toggled ) - - self.tor_bridges_use_custom_label = QtWidgets.QLabel( - strings._("gui_settings_tor_bridges_custom_label") - ) - self.tor_bridges_use_custom_label.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - self.tor_bridges_use_custom_label.setOpenExternalLinks(True) self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() self.tor_bridges_use_custom_textbox.setMaximumHeight(200) self.tor_bridges_use_custom_textbox.setPlaceholderText( @@ -182,9 +206,6 @@ class TorSettingsDialog(QtWidgets.QDialog): ) tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() - tor_bridges_use_custom_textbox_options_layout.addWidget( - self.tor_bridges_use_custom_label - ) tor_bridges_use_custom_textbox_options_layout.addWidget( self.tor_bridges_use_custom_textbox ) From d1ae4e454ff11dff16c4598ae04a785b75c17541 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 14:25:18 -0700 Subject: [PATCH 17/70] Simplify variable names in TorSettingsDialog, and start adding UI for moat --- .../src/onionshare/resources/locale/en.json | 12 +- desktop/src/onionshare/tor_settings_dialog.py | 208 ++++++++---------- 2 files changed, 103 insertions(+), 117 deletions(-) diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 13326c4c..51a3d53a 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -62,13 +62,13 @@ "gui_settings_password_label": "Password", "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_tor_bridges_no_bridges_radio_option": "Don't use a bridge", - "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 bridge", - "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek-azure bridge", - "gui_settings_tor_bridges_snowflake_radio_option": "Use built-in snowflake bridge", + "gui_settings_bridge_none_radio_option": "Don't use a bridge", + "gui_settings_bridge_obfs4_radio_option": "Use built-in obfs4 bridge", + "gui_settings_bridge_meek_azure_radio_option": "Use built-in meek-azure bridge", + "gui_settings_bridge_snowflake_radio_option": "Use built-in snowflake 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.", - "gui_settings_tor_bridges_moat_radio_option": "Request a bridge from torproject.org", - "gui_settings_tor_bridges_custom_radio_option": "Provide a bridge you learned about from a trusted source", + "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org", + "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index dc16e822..abecf949 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -93,12 +93,10 @@ class TorSettingsDialog(QtWidgets.QDialog): bridges_label.setWordWrap(True) # No bridges option radio - self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_no_bridges_radio_option") - ) - self.tor_bridges_no_bridges_radio.toggled.connect( - self.tor_bridges_no_bridges_radio_toggled + self.bridge_none_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_none_radio_option") ) + self.bridge_none_radio.toggled.connect(self.bridge_none_radio_toggled) ( self.tor_path, @@ -110,12 +108,10 @@ class TorSettingsDialog(QtWidgets.QDialog): # obfs4 option radio # if the obfs4proxy binary is missing, we can't use obfs4 transports - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_obfs4_radio_option") - ) - self.tor_bridges_use_obfs4_radio.toggled.connect( - self.tor_bridges_use_obfs4_radio_toggled + self.bridge_obfs4_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_obfs4_radio_option") ) + self.bridge_obfs4_radio.toggled.connect(self.bridge_obfs4_radio_toggled) if not self.obfs4proxy_file_path or not os.path.isfile( self.obfs4proxy_file_path ): @@ -124,15 +120,15 @@ class TorSettingsDialog(QtWidgets.QDialog): "__init__", f"missing binary {self.obfs4proxy_file_path}, hiding obfs4 bridge", ) - self.tor_bridges_use_obfs4_radio.hide() + self.bridge_obfs4_radio.hide() # meek-azure option radio # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_meek_lite_azure_radio_option") + self.bridge_meek_azure_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_meek_azure_radio_option") ) - self.tor_bridges_use_meek_lite_azure_radio.toggled.connect( - self.tor_bridges_use_meek_lite_azure_radio_toggled + self.bridge_meek_azure_radio.toggled.connect( + self.bridge_meek_azure_radio_toggled ) if not self.obfs4proxy_file_path or not os.path.isfile( self.obfs4proxy_file_path @@ -142,89 +138,65 @@ class TorSettingsDialog(QtWidgets.QDialog): "__init__", f"missing binary {self.obfs4proxy_file_path}, hiding meek-azure bridge", ) - self.tor_bridges_use_meek_lite_azure_radio.hide() + self.bridge_meek_azure_radio.hide() # snowflake option radio # if the snowflake-client binary is missing, we can't use snowflake transports - self.tor_bridges_use_snowflake_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_snowflake_radio_option") - ) - self.tor_bridges_use_snowflake_radio.toggled.connect( - self.tor_bridges_use_snowflake_radio_toggled + self.bridge_snowflake_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_snowflake_radio_option") ) + self.bridge_snowflake_radio.toggled.connect(self.bridge_snowflake_radio_toggled) if not self.snowflake_file_path or not os.path.isfile(self.snowflake_file_path): self.common.log( "TorSettingsDialog", "__init__", f"missing binary {self.snowflake_file_path}, hiding snowflake bridge", ) - self.tor_bridges_use_snowflake_radio.hide() + self.bridge_snowflake_radio.hide() # Request a bridge from torproject.org (moat) - # self.tor_bridges_use_moat_radio = QtWidgets.QRadioButton( - # strings._("gui_settings_tor_bridges_moat_radio_option") - # ) - # self.tor_bridges_use_moat_radio.toggled.connect( - # self.tor_bridges_use_moat_radio_toggled - # ) - - # self.tor_bridges_use_moat_label = QtWidgets.QLabel( - # strings._("gui_settings_tor_bridges_moat_label") - # ) - # self.tor_bridges_use_custom_label.setOpenExternalLinks(True) - # self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() - # self.tor_bridges_use_custom_textbox.setMaximumHeight(200) - # self.tor_bridges_use_custom_textbox.setPlaceholderText( - # "[address:port] [identifier]" - # ) - - # tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() - # tor_bridges_use_custom_textbox_options_layout.addWidget( - # self.tor_bridges_use_custom_label - # ) - # tor_bridges_use_custom_textbox_options_layout.addWidget( - # self.tor_bridges_use_custom_textbox - # ) - - # self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget() - # self.tor_bridges_use_custom_textbox_options.setLayout( - # tor_bridges_use_custom_textbox_options_layout - # ) - # self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_moat_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_moat_radio_option") + ) + self.bridge_moat_radio.toggled.connect(self.bridge_moat_radio_toggled) + self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() + self.bridge_moat_textbox.setMaximumHeight(200) + self.bridge_moat_textbox.setEnabled(False) + bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() + bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox) + self.bridge_moat_textbox_options = QtWidgets.QWidget() + self.bridge_moat_textbox_options.setLayout(bridge_moat_textbox_options_layout) + self.bridge_moat_textbox_options.hide() # Custom bridges radio and textbox - self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton( - strings._("gui_settings_tor_bridges_custom_radio_option") - ) - self.tor_bridges_use_custom_radio.toggled.connect( - self.tor_bridges_use_custom_radio_toggled - ) - self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() - self.tor_bridges_use_custom_textbox.setMaximumHeight(200) - self.tor_bridges_use_custom_textbox.setPlaceholderText( - "[address:port] [identifier]" + self.bridge_custom_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_custom_radio_option") ) + self.bridge_custom_radio.toggled.connect(self.bridge_custom_radio_toggled) + self.bridge_custom_textbox = QtWidgets.QPlainTextEdit() + self.bridge_custom_textbox.setMaximumHeight(200) + self.bridge_custom_textbox.setPlaceholderText("[address:port] [identifier]") - tor_bridges_use_custom_textbox_options_layout = QtWidgets.QVBoxLayout() - tor_bridges_use_custom_textbox_options_layout.addWidget( - self.tor_bridges_use_custom_textbox - ) + bridge_custom_textbox_options_layout = QtWidgets.QVBoxLayout() + bridge_custom_textbox_options_layout.addWidget(self.bridge_custom_textbox) - self.tor_bridges_use_custom_textbox_options = QtWidgets.QWidget() - self.tor_bridges_use_custom_textbox_options.setLayout( - tor_bridges_use_custom_textbox_options_layout + self.bridge_custom_textbox_options = QtWidgets.QWidget() + self.bridge_custom_textbox_options.setLayout( + bridge_custom_textbox_options_layout ) - self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_custom_textbox_options.hide() # Bridges layout/widget bridges_layout = QtWidgets.QVBoxLayout() bridges_layout.addWidget(bridges_label) - bridges_layout.addWidget(self.tor_bridges_no_bridges_radio) - bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio) - bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio) - bridges_layout.addWidget(self.tor_bridges_use_snowflake_radio) - bridges_layout.addWidget(self.tor_bridges_use_custom_radio) - bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options) + bridges_layout.addWidget(self.bridge_none_radio) + bridges_layout.addWidget(self.bridge_obfs4_radio) + bridges_layout.addWidget(self.bridge_meek_azure_radio) + bridges_layout.addWidget(self.bridge_snowflake_radio) + bridges_layout.addWidget(self.bridge_moat_radio) + bridges_layout.addWidget(self.bridge_moat_textbox_options) + bridges_layout.addWidget(self.bridge_custom_radio) + bridges_layout.addWidget(self.bridge_custom_textbox_options) self.bridges = QtWidgets.QWidget() self.bridges.setLayout(bridges_layout) @@ -450,36 +422,36 @@ class TorSettingsDialog(QtWidgets.QDialog): ) if self.old_settings.get("no_bridges"): - self.tor_bridges_no_bridges_radio.setChecked(True) - self.tor_bridges_use_obfs4_radio.setChecked(False) - self.tor_bridges_use_meek_lite_azure_radio.setChecked(False) - self.tor_bridges_use_snowflake_radio.setChecked(False) - self.tor_bridges_use_custom_radio.setChecked(False) + self.bridge_none_radio.setChecked(True) + self.bridge_obfs4_radio.setChecked(False) + self.bridge_meek_azure_radio.setChecked(False) + self.bridge_snowflake_radio.setChecked(False) + self.bridge_custom_radio.setChecked(False) else: - self.tor_bridges_no_bridges_radio.setChecked(False) - self.tor_bridges_use_obfs4_radio.setChecked( + self.bridge_none_radio.setChecked(False) + self.bridge_obfs4_radio.setChecked( self.old_settings.get("tor_bridges_use_obfs4") ) - self.tor_bridges_use_meek_lite_azure_radio.setChecked( + self.bridge_meek_azure_radio.setChecked( self.old_settings.get("tor_bridges_use_meek_lite_azure") ) - self.tor_bridges_use_snowflake_radio.setChecked( + self.bridge_snowflake_radio.setChecked( self.old_settings.get("tor_bridges_use_snowflake") ) - if self.old_settings.get("tor_bridges_use_custom_bridges"): - self.tor_bridges_use_custom_radio.setChecked(True) + if self.old_settings.get("bridge_custom_bridges"): + self.bridge_custom_radio.setChecked(True) # Remove the 'Bridge' lines at the start of each bridge. # They are added automatically to provide compatibility with # copying/pasting bridges provided from https://bridges.torproject.org new_bridges = [] - bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split( + bridges = self.old_settings.get("bridge_custom_bridges").split( "Bridge " ) for bridge in bridges: new_bridges.append(bridge) new_bridges = "".join(new_bridges) - self.tor_bridges_use_custom_textbox.setPlainText(new_bridges) + self.bridge_custom_textbox.setPlainText(new_bridges) def connection_type_bundled_toggled(self, checked): """ @@ -491,26 +463,30 @@ class TorSettingsDialog(QtWidgets.QDialog): self.connection_type_socks.hide() self.connection_type_bridges_radio_group.show() - def tor_bridges_no_bridges_radio_toggled(self, checked): + def bridge_none_radio_toggled(self, checked): """ 'No bridges' option was toggled. If checked, enable other bridge options. """ if checked: - self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_custom_textbox_options.hide() + self.bridge_moat_textbox_options.hide() - def tor_bridges_use_obfs4_radio_toggled(self, checked): + def bridge_obfs4_radio_toggled(self, checked): """ obfs4 bridges option was toggled. If checked, disable custom bridge options. """ if checked: - self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_custom_textbox_options.hide() + self.bridge_moat_textbox_options.hide() - def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked): + def bridge_meek_azure_radio_toggled(self, checked): """ meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. """ if checked: - self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_custom_textbox_options.hide() + self.bridge_moat_textbox_options.hide() + # Alert the user about meek's costliness if it looks like they're turning it on if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): Alert( @@ -519,19 +495,29 @@ class TorSettingsDialog(QtWidgets.QDialog): QtWidgets.QMessageBox.Warning, ) - def tor_bridges_use_snowflake_radio_toggled(self, checked): + def bridge_snowflake_radio_toggled(self, checked): """ snowflake bridges option was toggled. If checked, disable custom bridge options. """ if checked: - self.tor_bridges_use_custom_textbox_options.hide() + self.bridge_custom_textbox_options.hide() + self.bridge_moat_textbox_options.hide() - def tor_bridges_use_custom_radio_toggled(self, checked): + def bridge_moat_radio_toggled(self, checked): + """ + Moat (request bridge) bridges option was toggled. If checked, show moat bridge options. + """ + if checked: + self.bridge_custom_textbox_options.hide() + self.bridge_moat_textbox_options.show() + + def bridge_custom_radio_toggled(self, checked): """ Custom bridges option was toggled. If checked, show custom bridge options. """ if checked: - self.tor_bridges_use_custom_textbox_options.show() + self.bridge_custom_textbox_options.show() + self.bridge_moat_textbox_options.hide() def connection_type_automatic_toggled(self, checked): """ @@ -659,7 +645,7 @@ class TorSettingsDialog(QtWidgets.QDialog): "no_bridges", "tor_bridges_use_obfs4", "tor_bridges_use_meek_lite_azure", - "tor_bridges_use_custom_bridges", + "bridge_custom_bridges", ], ): @@ -761,31 +747,31 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("auth_password", self.authenticate_password_extras_password.text()) # Whether we use bridges - if self.tor_bridges_no_bridges_radio.isChecked(): + if self.bridge_none_radio.isChecked(): settings.set("no_bridges", True) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_obfs4_radio.isChecked(): + settings.set("bridge_custom_bridges", "") + if self.bridge_obfs4_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", True) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_meek_lite_azure_radio.isChecked(): + settings.set("bridge_custom_bridges", "") + if self.bridge_meek_azure_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", True) settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_snowflake_radio.isChecked(): + settings.set("bridge_custom_bridges", "") + if self.bridge_snowflake_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", True) - settings.set("tor_bridges_use_custom_bridges", "") - if self.tor_bridges_use_custom_radio.isChecked(): + settings.set("bridge_custom_bridges", "") + if self.bridge_custom_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) @@ -795,7 +781,7 @@ class TorSettingsDialog(QtWidgets.QDialog): # This makes it easier to copy/paste a set of bridges # provided from https://bridges.torproject.org new_bridges = [] - bridges = self.tor_bridges_use_custom_textbox.toPlainText().split("\n") + bridges = self.bridge_custom_textbox.toPlainText().split("\n") bridges_valid = False for bridge in bridges: if bridge != "": @@ -819,7 +805,7 @@ class TorSettingsDialog(QtWidgets.QDialog): if bridges_valid: new_bridges = "".join(new_bridges) - settings.set("tor_bridges_use_custom_bridges", new_bridges) + settings.set("bridge_custom_bridges", new_bridges) else: Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) settings.set("no_bridges", True) From 168e3057ae56926518dd01931da4180a788afc7e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 14:44:09 -0700 Subject: [PATCH 18/70] Start implementing moat --- .../src/onionshare/resources/locale/en.json | 2 + desktop/src/onionshare/tor_settings_dialog.py | 75 ++++++++++++++----- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 51a3d53a..97ce5585 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -68,6 +68,8 @@ "gui_settings_bridge_snowflake_radio_option": "Use built-in snowflake 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.", "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org", + "gui_settings_bridge_moat_button": "Request a New Bridge...", + "gui_settings_bridge_moat_error": "Error requesting a bridge from torproject.org.", "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index abecf949..9f08d767 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -19,30 +19,14 @@ along with this program. If not, see . """ from PySide2 import QtCore, QtWidgets, QtGui -from PySide2.QtCore import Slot, Qt -from PySide2.QtGui import QPalette, QColor import sys import platform -import datetime import re import os +import requests + from onionshare_cli.settings import Settings -from onionshare_cli.onion import ( - Onion, - TorErrorInvalidSetting, - TorErrorAutomatic, - TorErrorSocketPort, - TorErrorSocketFile, - TorErrorMissingPassword, - TorErrorUnreadableCookieFile, - TorErrorAuthError, - TorErrorProtocolError, - BundledTorTimeout, - BundledTorBroken, - TorTooOldEphemeral, - TorTooOldStealth, - PortNotAvailable, -) +from onionshare_cli.onion import Onion from . import strings from .widgets import Alert @@ -159,10 +143,17 @@ class TorSettingsDialog(QtWidgets.QDialog): strings._("gui_settings_bridge_moat_radio_option") ) self.bridge_moat_radio.toggled.connect(self.bridge_moat_radio_toggled) + self.bridge_moat_button = QtWidgets.QPushButton( + strings._("gui_settings_bridge_moat_button") + ) + self.bridge_moat_button.setMinimumHeight(20) + self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked) self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() self.bridge_moat_textbox.setMaximumHeight(200) self.bridge_moat_textbox.setEnabled(False) + self.bridge_moat_textbox.hide() bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() + bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox) self.bridge_moat_textbox_options = QtWidgets.QWidget() self.bridge_moat_textbox_options.setLayout(bridge_moat_textbox_options_layout) @@ -511,6 +502,52 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_custom_textbox_options.hide() self.bridge_moat_textbox_options.show() + def bridge_moat_button_clicked(self): + """ + Request new bridge button clicked + """ + self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") + + def moat_error(): + Alert( + self.common, + strings._("gui_settings_bridge_moat_error"), + title=strings._("gui_settings_bridge_moat_button"), + ) + + # TODO: Do all of this using domain fronting + + # Request a bridge + r = requests.post( + "https://bridges.torproject.org/moat/fetch", + headers={"Content-Type": "application/vnd.api+json"}, + json={ + "data": [ + { + "version": "0.1.0", + "type": "client-transports", + "supported": ["obfs4"], + } + ] + }, + ) + if r.status_code != 200: + return moat_error() + + try: + moat_res = r.json() + if "errors" in moat_res or "data" not in moat_res: + return moat_error() + if moat_res["type"] != "moat-challenge": + return moat_error() + + moat_type = moat_res["type"] + moat_transport = moat_res["transport"] + moat_image = moat_res["image"] + moat_challenge = moat_res["challenge"] + except: + return moat_error() + def bridge_custom_radio_toggled(self, checked): """ Custom bridges option was toggled. If checked, show custom bridge options. From 67126a34976c4d036873323b1bf5a23c1d46340e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 15 Oct 2021 16:53:40 -0700 Subject: [PATCH 19/70] Start making MoatDialog --- desktop/src/onionshare/moat_dialog.py | 174 ++++++++++++++++++ .../src/onionshare/resources/locale/en.json | 10 +- desktop/src/onionshare/tor_settings_dialog.py | 47 +---- 3 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 desktop/src/onionshare/moat_dialog.py diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py new file mode 100644 index 00000000..ba4223eb --- /dev/null +++ b/desktop/src/onionshare/moat_dialog.py @@ -0,0 +1,174 @@ +# -*- 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 . +""" + +from PySide2 import QtCore, QtWidgets, QtGui +import requests + +from . import strings +from .gui_common import GuiCommon + + +class MoatDialog(QtWidgets.QDialog): + """ + Moat dialog: Request a bridge from torproject.org + """ + + def __init__(self, common): + super(MoatDialog, self).__init__() + + self.common = common + + self.common.log("MoatDialog", "__init__") + + self.setModal(True) + self.setWindowTitle(strings._("gui_settings_bridge_moat_button")) + self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + + # Label + self.label = QtWidgets.QLabel(strings._("moat_contact_label")) + + # CAPTCHA image + self.captcha = QtWidgets.QLabel() + self.captcha.setFixedSize(400, 125) # this is the size of the CAPTCHA image + + # Solution input + self.solution_lineedit = QtWidgets.QLineEdit() + self.solution_lineedit.editingFinished.connect(self.solution_editing_finished) + self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) + self.reload_button.clicked.connect(self.reload_clicked) + solution_layout = QtWidgets.QHBoxLayout() + solution_layout.addWidget(self.solution_lineedit) + solution_layout.addWidget(self.reload_button) + + # Error label + self.error_label = QtWidgets.QLabel() + self.error_label.hide() + + # Buttons + self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) + self.submit_button.clicked.connect(self.submit_clicked) + self.submit_button.setEnabled(False) + self.cancel_button = QtWidgets.QPushButton( + strings._("gui_settings_button_cancel") + ) + self.cancel_button.clicked.connect(self.cancel_clicked) + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addStretch() + buttons_layout.addWidget(self.submit_button) + buttons_layout.addWidget(self.cancel_button) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.captcha) + layout.addLayout(solution_layout) + layout.addWidget(self.error_label) + layout.addLayout(buttons_layout) + + self.setLayout(layout) + self.cancel_button.setFocus() + + self.reload_clicked() + self.exec_() + + def solution_editing_finished(self): + """ + Finished typing something in the solution field. + """ + self.common.log("MoatDialog", "solution_editing_finished") + pass + + def reload_clicked(self): + """ + Reload button clicked. + """ + self.common.log("MoatDialog", "reload_clicked") + pass + + def submit_clicked(self): + """ + Submit button clicked. + """ + self.common.log("MoatDialog", "submit_clicked") + pass + + def cancel_clicked(self): + """ + Cancel button clicked. + """ + self.common.log("MoatDialog", "cancel_clicked") + pass + + +class MoatThread(QtCore.QThread): + """ + This does all of the communicating with BridgeDB in a separate thread. + + Valid actions are: + - "fetch": requests a new CAPTCHA + - "check": sends a CAPTCHA solution + + """ + + tor_status_update = QtCore.Signal(str, str) + + def __init__(self, common, action, data): + super(MoatThread, self).__init__() + self.common = common + self.common.log("MoatThread", "__init__", f"action={action}") + + self.action = action + self.data = data + + def run(self): + self.common.log("MoatThread", "run") + + # TODO: Do all of this using domain fronting + + # Request a bridge + r = requests.post( + "https://bridges.torproject.org/moat/fetch", + headers={"Content-Type": "application/vnd.api+json"}, + json={ + "data": [ + { + "version": "0.1.0", + "type": "client-transports", + "supported": ["obfs4"], + } + ] + }, + ) + if r.status_code != 200: + return moat_error() + + try: + moat_res = r.json() + if "errors" in moat_res or "data" not in moat_res: + return moat_error() + if moat_res["type"] != "moat-challenge": + return moat_error() + + moat_type = moat_res["type"] + moat_transport = moat_res["transport"] + moat_image = moat_res["image"] + moat_challenge = moat_res["challenge"] + except: + return moat_error() diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 97ce5585..6bc424e0 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -69,7 +69,6 @@ "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.", "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org", "gui_settings_bridge_moat_button": "Request a New Bridge...", - "gui_settings_bridge_moat_error": "Error requesting a bridge from torproject.org.", "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", @@ -220,5 +219,12 @@ "gui_rendezvous_cleanup_quit_early": "Quit Early", "error_port_not_available": "OnionShare port not available", "history_receive_read_message_button": "Read Message", - "error_tor_protocol_error": "There was an error with Tor: {}" + "error_tor_protocol_error": "There was an error with Tor: {}", + "moat_contact_label": "Contacting BridgeDB...", + "moat_captcha_label": "Solve the CAPTCHA to request a bridge.", + "moat_captcha_placeholder": "Enter the characters from the image", + "moat_captcha_submit": "Submit", + "moat_captcha_reload": "Reload", + "moat_bridgedb_error": "Error contacting BridgeDB.", + "moat_captcha_error": "The solution is not correct. Please try again." } \ No newline at end of file diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 9f08d767..f5f0a302 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -23,15 +23,14 @@ import sys import platform import re import os -import requests from onionshare_cli.settings import Settings from onionshare_cli.onion import Onion from . import strings from .widgets import Alert -from .update_checker import UpdateThread from .tor_connection_dialog import TorConnectionDialog +from .moat_dialog import MoatDialog from .gui_common import GuiCommon @@ -146,16 +145,16 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_moat_button = QtWidgets.QPushButton( strings._("gui_settings_bridge_moat_button") ) - self.bridge_moat_button.setMinimumHeight(20) self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked) self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() - self.bridge_moat_textbox.setMaximumHeight(200) + self.bridge_moat_textbox.setMaximumHeight(100) self.bridge_moat_textbox.setEnabled(False) self.bridge_moat_textbox.hide() bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox) self.bridge_moat_textbox_options = QtWidgets.QWidget() + self.bridge_moat_textbox_options.setMinimumHeight(50) self.bridge_moat_textbox_options.setLayout(bridge_moat_textbox_options_layout) self.bridge_moat_textbox_options.hide() @@ -508,45 +507,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") - def moat_error(): - Alert( - self.common, - strings._("gui_settings_bridge_moat_error"), - title=strings._("gui_settings_bridge_moat_button"), - ) - - # TODO: Do all of this using domain fronting - - # Request a bridge - r = requests.post( - "https://bridges.torproject.org/moat/fetch", - headers={"Content-Type": "application/vnd.api+json"}, - json={ - "data": [ - { - "version": "0.1.0", - "type": "client-transports", - "supported": ["obfs4"], - } - ] - }, - ) - if r.status_code != 200: - return moat_error() - - try: - moat_res = r.json() - if "errors" in moat_res or "data" not in moat_res: - return moat_error() - if moat_res["type"] != "moat-challenge": - return moat_error() - - moat_type = moat_res["type"] - moat_transport = moat_res["transport"] - moat_image = moat_res["image"] - moat_challenge = moat_res["challenge"] - except: - return moat_error() + moat_dialog = MoatDialog(self.common) def bridge_custom_radio_toggled(self, checked): """ From 9f9328fd1fa0df867eb235cb23a886873cd39624 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 12:15:01 -0700 Subject: [PATCH 20/70] Update linux Tor Browser URL and hash --- desktop/scripts/get-tor-linux.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py index e47ae03d..b8f83c92 100755 --- a/desktop/scripts/get-tor-linux.py +++ b/desktop/scripts/get-tor-linux.py @@ -34,10 +34,10 @@ import requests def main(): - tarball_url = "https://dist.torproject.org/torbrowser/11.0a7/tor-browser-linux64-11.0a7_en-US.tar.xz" - tarball_filename = "tor-browser-linux64-11.0a7_en-US.tar.xz" + tarball_url = "https://dist.torproject.org/torbrowser/11.0a9/tor-browser-linux64-11.0a9_en-US.tar.xz" + tarball_filename = "tor-browser-linux64-11.0a9_en-US.tar.xz" expected_tarball_sha256 = ( - "bc9861c692f899fe0344c960dc615ff0e275cf74c61066c8735c88e3ddc2b623" + "cba4a2120b4f847d1ade637e41e69bd01b2e70b4a13e41fe8e69d0424fcf7ca7" ) # Build paths From 2161c58a4afaa78b46fbb2412dbd3cee99fb61d2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 12:15:25 -0700 Subject: [PATCH 21/70] If connecting to Tor fails, open the correct TorSettings dialog --- desktop/src/onionshare/main_window.py | 10 +++++----- desktop/src/onionshare/tor_connection_dialog.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index b3f8b6c9..c125741c 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -165,7 +165,7 @@ class MainWindow(QtWidgets.QMainWindow): # Start the "Connecting to Tor" dialog, which calls onion.connect() tor_con = TorConnectionDialog(self.common) tor_con.canceled.connect(self.tor_connection_canceled) - tor_con.open_settings.connect(self.tor_connection_open_settings) + tor_con.open_tor_settings.connect(self.tor_connection_open_tor_settings) if not self.common.gui.local_only: tor_con.start() self.settings_have_changed() @@ -234,14 +234,14 @@ class MainWindow(QtWidgets.QMainWindow): # Wait 100ms before asking QtCore.QTimer.singleShot(100, ask) - def tor_connection_open_settings(self): + def tor_connection_open_tor_settings(self): """ - The TorConnectionDialog wants to open the Settings dialog + The TorConnectionDialog wants to open the Tor Settings dialog """ - self.common.log("MainWindow", "tor_connection_open_settings") + self.common.log("MainWindow", "tor_connection_open_tor_settings") # Wait 1ms for the event loop to finish closing the TorConnectionDialog - QtCore.QTimer.singleShot(1, self.open_settings) + QtCore.QTimer.singleShot(1, self.open_tor_settings) def open_tor_settings(self): """ diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection_dialog.py index dd2721bd..daf49a32 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection_dialog.py @@ -48,7 +48,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): Connecting to Tor dialog. """ - open_settings = QtCore.Signal() + open_tor_settings = QtCore.Signal() success = QtCore.Signal() def __init__( @@ -149,7 +149,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): ) # Open settings - self.open_settings.emit() + self.open_tor_settings.emit() QtCore.QTimer.singleShot(1, alert) From 6bf839f8269fff99fd33827404ca12e2764f6bc6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 12:16:03 -0700 Subject: [PATCH 22/70] In some distros, LD_LIBRARY_PATH must be explicitly set for tor to work --- cli/onionshare_cli/onion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index f8fcf68e..c3ee4fac 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -354,6 +354,7 @@ class Onion(object): [self.tor_path, "-f", self.tor_torrc], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env={"LD_LIBRARY_PATH": os.path.dirname(self.tor_path)}, ) # Wait for the tor controller to start From 221258962511e58d95c73bc4d4cca7a229d791f3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 14:02:11 -0700 Subject: [PATCH 23/70] Actually get bridges from moat --- desktop/src/onionshare/gui_common.py | 6 + desktop/src/onionshare/moat_dialog.py | 237 ++++++++++++++---- .../src/onionshare/resources/locale/en.json | 3 +- desktop/src/onionshare/tor_settings_dialog.py | 13 +- 4 files changed, 210 insertions(+), 49 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 3559eb92..0f1dd46e 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -392,6 +392,12 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", + # Moat dialog + "moat_error": """ + QLabel { + color: #990000; + } + """, } def get_tor_paths(self): diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index ba4223eb..3cb6519b 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -20,6 +20,8 @@ along with this program. If not, see . from PySide2 import QtCore, QtWidgets, QtGui import requests +import os +import base64 from . import strings from .gui_common import GuiCommon @@ -30,6 +32,8 @@ class MoatDialog(QtWidgets.QDialog): Moat dialog: Request a bridge from torproject.org """ + got_bridges = QtCore.Signal(str) + def __init__(self, common): super(MoatDialog, self).__init__() @@ -42,7 +46,7 @@ class MoatDialog(QtWidgets.QDialog): self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) # Label - self.label = QtWidgets.QLabel(strings._("moat_contact_label")) + self.label = QtWidgets.QLabel() # CAPTCHA image self.captcha = QtWidgets.QLabel() @@ -50,7 +54,7 @@ class MoatDialog(QtWidgets.QDialog): # Solution input self.solution_lineedit = QtWidgets.QLineEdit() - self.solution_lineedit.editingFinished.connect(self.solution_editing_finished) + self.solution_lineedit.setPlaceholderText(strings._("moat_captcha_placeholder")) self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) self.reload_button.clicked.connect(self.reload_clicked) solution_layout = QtWidgets.QHBoxLayout() @@ -59,12 +63,12 @@ class MoatDialog(QtWidgets.QDialog): # Error label self.error_label = QtWidgets.QLabel() + self.error_label.setStyleSheet(self.common.gui.css["moat_error"]) self.error_label.hide() # Buttons self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) self.submit_button.clicked.connect(self.submit_clicked) - self.submit_button.setEnabled(False) self.cancel_button = QtWidgets.QPushButton( strings._("gui_settings_button_cancel") ) @@ -79,6 +83,7 @@ class MoatDialog(QtWidgets.QDialog): layout.addWidget(self.label) layout.addWidget(self.captcha) layout.addLayout(solution_layout) + layout.addStretch() layout.addWidget(self.error_label) layout.addLayout(buttons_layout) @@ -86,35 +91,95 @@ class MoatDialog(QtWidgets.QDialog): self.cancel_button.setFocus() self.reload_clicked() - self.exec_() - - def solution_editing_finished(self): - """ - Finished typing something in the solution field. - """ - self.common.log("MoatDialog", "solution_editing_finished") - pass def reload_clicked(self): """ Reload button clicked. """ self.common.log("MoatDialog", "reload_clicked") - pass + + self.label.setText(strings._("moat_contact_label")) + self.error_label.hide() + + self.captcha.hide() + self.solution_lineedit.hide() + self.reload_button.hide() + self.submit_button.hide() + + # BridgeDB fetch + self.t_fetch = MoatThread(self.common, "fetch") + self.t_fetch.bridgedb_error.connect(self.bridgedb_error) + self.t_fetch.captcha_ready.connect(self.captcha_ready) + self.t_fetch.start() def submit_clicked(self): """ Submit button clicked. """ - self.common.log("MoatDialog", "submit_clicked") - pass + self.error_label.hide() + + solution = self.solution_lineedit.text().strip() + if len(solution) == 0: + self.common.log("MoatDialog", "submit_clicked", "solution is blank") + self.error_label.setText(strings._("moat_solution_empty_error")) + self.error_label.show() + return + + # BridgeDB check + self.t_check = MoatThread( + self.common, + "check", + {"challenge": self.challenge, "solution": self.solution_lineedit.text()}, + ) + self.t_check.bridgedb_error.connect(self.bridgedb_error) + self.t_check.captcha_error.connect(self.captcha_error) + self.t_check.bridges_ready.connect(self.bridges_ready) + self.t_check.start() def cancel_clicked(self): """ Cancel button clicked. """ self.common.log("MoatDialog", "cancel_clicked") - pass + self.close() + + def bridgedb_error(self): + self.common.log("MoatDialog", "bridgedb_error") + self.error_label.setText(strings._("moat_bridgedb_error")) + self.error_label.show() + + def captcha_error(self, msg): + self.common.log("MoatDialog", "captcha_error") + if msg == "": + self.error_label.setText(strings._("moat_captcha_error")) + else: + self.error_label.setText(msg) + self.error_label.show() + + def captcha_ready(self, image, challenge): + self.common.log("MoatDialog", "captcha_ready") + + self.challenge = challenge + + # Save captcha image to disk, so we can load it + captcha_data = base64.b64decode(image) + captcha_filename = os.path.join(self.common.build_tmp_dir(), "captcha.jpg") + with open(captcha_filename, "wb") as f: + f.write(captcha_data) + + self.captcha.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(captcha_filename))) + os.remove(captcha_filename) + + self.label.setText(strings._("moat_captcha_label")) + self.captcha.show() + self.solution_lineedit.show() + self.reload_button.show() + self.submit_button.show() + + def bridges_ready(self, bridges): + self.common.log("MoatDialog", "bridges_ready", bridges) + self.got_bridges.emit(bridges) + self.close() class MoatThread(QtCore.QThread): @@ -127,48 +192,126 @@ class MoatThread(QtCore.QThread): """ - tor_status_update = QtCore.Signal(str, str) + bridgedb_error = QtCore.Signal() + captcha_error = QtCore.Signal(str) + captcha_ready = QtCore.Signal(str, str) + bridges_ready = QtCore.Signal(str) - def __init__(self, common, action, data): + def __init__(self, common, action, data={}): super(MoatThread, self).__init__() self.common = common self.common.log("MoatThread", "__init__", f"action={action}") + self.transport = "obfs4" self.action = action self.data = data def run(self): - self.common.log("MoatThread", "run") - # TODO: Do all of this using domain fronting - # Request a bridge - r = requests.post( - "https://bridges.torproject.org/moat/fetch", - headers={"Content-Type": "application/vnd.api+json"}, - json={ - "data": [ - { - "version": "0.1.0", - "type": "client-transports", - "supported": ["obfs4"], - } - ] - }, - ) - if r.status_code != 200: - return moat_error() + if self.action == "fetch": + self.common.log("MoatThread", "run", f"starting fetch") - try: - moat_res = r.json() - if "errors" in moat_res or "data" not in moat_res: - return moat_error() - if moat_res["type"] != "moat-challenge": - return moat_error() + # Request a bridge + r = requests.post( + "https://bridges.torproject.org/moat/fetch", + headers={"Content-Type": "application/vnd.api+json"}, + json={ + "data": [ + { + "version": "0.1.0", + "type": "client-transports", + "supported": [self.transport], + } + ] + }, + ) + if r.status_code != 200: + self.common.log("MoatThread", "run", f"status_code={r.status_code}") + self.bridgedb_error.emit() + return - moat_type = moat_res["type"] - moat_transport = moat_res["transport"] - moat_image = moat_res["image"] - moat_challenge = moat_res["challenge"] - except: - return moat_error() + try: + moat_res = r.json() + if "errors" in moat_res: + self.common.log("MoatThread", "run", f"errors={moat_res['errors']}") + self.bridgedb_error.emit() + return + if "data" not in moat_res: + self.common.log("MoatThread", "run", f"no data") + self.bridgedb_error.emit() + return + if moat_res["data"][0]["type"] != "moat-challenge": + self.common.log("MoatThread", "run", f"type != moat-challange") + self.bridgedb_error.emit() + return + if moat_res["data"][0]["transport"] != self.transport: + self.common.log( + "MoatThread", "run", f"transport != {self.transport}" + ) + self.bridgedb_error.emit() + return + + image = moat_res["data"][0]["image"] + challenge = moat_res["data"][0]["challenge"] + + self.captcha_ready.emit(image, challenge) + except Exception as e: + self.common.log("MoatThread", "run", f"hit exception: {e}") + self.bridgedb_error.emit() + return + + elif self.action == "check": + self.common.log("MoatThread", "run", f"starting check") + + # Check the CAPTCHA + r = requests.post( + "https://bridges.torproject.org/moat/check", + headers={"Content-Type": "application/vnd.api+json"}, + json={ + "data": [ + { + "id": "2", + "type": "moat-solution", + "version": "0.1.0", + "transport": self.transport, + "challenge": self.data["challenge"], + "solution": self.data["solution"], + "qrcode": "false", + } + ] + }, + ) + if r.status_code != 200: + self.common.log("MoatThread", "run", f"status_code={r.status_code}") + self.bridgedb_error.emit() + return + + try: + moat_res = r.json() + + if "errors" in moat_res: + self.common.log("MoatThread", "run", f"errors={moat_res['errors']}") + if moat_res["errors"][0]["code"] == 419: + self.captcha_error.emit("") + return + else: + errors = " ".join([e["detail"] for e in moat_res["errors"]]) + self.captcha_error.emit(errors) + return + + if moat_res["data"][0]["type"] != "moat-bridges": + self.common.log("MoatThread", "run", f"type != moat-bridges") + self.bridgedb_error.emit() + return + + bridges = moat_res["data"][0]["bridges"] + self.bridges_ready.emit("\n".join(bridges)) + + except Exception as e: + self.common.log("MoatThread", "run", f"hit exception: {e}") + self.bridgedb_error.emit() + return + + else: + self.common.log("MoatThread", "run", f"invalid action: {self.action}") diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 6bc424e0..8a14f8bf 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -226,5 +226,6 @@ "moat_captcha_submit": "Submit", "moat_captcha_reload": "Reload", "moat_bridgedb_error": "Error contacting BridgeDB.", - "moat_captcha_error": "The solution is not correct. Please try again." + "moat_captcha_error": "The solution is not correct. Please try again.", + "moat_solution_empty_error": "You must enter the characters from the image" } \ No newline at end of file diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index f5f0a302..b6495830 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -148,7 +148,8 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked) self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() self.bridge_moat_textbox.setMaximumHeight(100) - self.bridge_moat_textbox.setEnabled(False) + self.bridge_moat_textbox.setReadOnly(True) + self.bridge_moat_textbox.setWordWrapMode(QtGui.QTextOption.NoWrap) self.bridge_moat_textbox.hide() bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) @@ -508,6 +509,16 @@ class TorSettingsDialog(QtWidgets.QDialog): self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") moat_dialog = MoatDialog(self.common) + moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges) + moat_dialog.exec_() + + def bridge_moat_got_bridges(self, bridges): + """ + Got new bridges from moat + """ + self.common.log("TorSettingsDialog", "bridge_moat_got_bridges") + self.bridge_moat_textbox.document().setPlainText(bridges) + self.bridge_moat_textbox.show() def bridge_custom_radio_toggled(self, checked): """ From 01b51e94bf8916fa5b2120045068067990ab8e07 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 14:26:56 -0700 Subject: [PATCH 24/70] Save/load moat bridges to/from settings --- cli/onionshare_cli/settings.py | 2 + cli/tests/test_cli_settings.py | 2 + desktop/src/onionshare/tor_settings_dialog.py | 43 +++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index 37c00bb6..29b59c80 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -109,6 +109,8 @@ class Settings(object): "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_snowflake": False, + "tor_bridges_use_moat": False, + "tor_bridges_use_moat_bridges": "", "tor_bridges_use_custom_bridges": "", "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index 1e00f22d..b44ddbec 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -33,6 +33,8 @@ class TestSettings: "tor_bridges_use_obfs4": False, "tor_bridges_use_meek_lite_azure": False, "tor_bridges_use_snowflake": False, + "tor_bridges_use_moat": False, + "tor_bridges_use_moat_bridges": "", "tor_bridges_use_custom_bridges": "", "persistent_tabs": [], "theme": 0, diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index b6495830..477758c2 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -150,7 +150,6 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_moat_textbox.setMaximumHeight(100) self.bridge_moat_textbox.setReadOnly(True) self.bridge_moat_textbox.setWordWrapMode(QtGui.QTextOption.NoWrap) - self.bridge_moat_textbox.hide() bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox) @@ -417,6 +416,7 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_obfs4_radio.setChecked(False) self.bridge_meek_azure_radio.setChecked(False) self.bridge_snowflake_radio.setChecked(False) + self.bridge_moat_radio.setChecked(False) self.bridge_custom_radio.setChecked(False) else: self.bridge_none_radio.setChecked(False) @@ -429,14 +429,23 @@ class TorSettingsDialog(QtWidgets.QDialog): self.bridge_snowflake_radio.setChecked( self.old_settings.get("tor_bridges_use_snowflake") ) + self.bridge_moat_radio.setChecked( + self.old_settings.get("tor_bridges_use_moat") + ) + moat_bridges = self.old_settings.get("tor_bridges_use_moat_bridges") + self.bridge_moat_textbox.document().setPlainText(moat_bridges) + if len(moat_bridges.strip()) > 0: + self.bridge_moat_textbox.show() + else: + self.bridge_moat_textbox.hide() - if self.old_settings.get("bridge_custom_bridges"): + if self.old_settings.get("tor_bridges_use_custom_bridges"): self.bridge_custom_radio.setChecked(True) # Remove the 'Bridge' lines at the start of each bridge. # They are added automatically to provide compatibility with # copying/pasting bridges provided from https://bridges.torproject.org new_bridges = [] - bridges = self.old_settings.get("bridge_custom_bridges").split( + bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split( "Bridge " ) for bridge in bridges: @@ -654,7 +663,7 @@ class TorSettingsDialog(QtWidgets.QDialog): "no_bridges", "tor_bridges_use_obfs4", "tor_bridges_use_meek_lite_azure", - "bridge_custom_bridges", + "tor_bridges_use_custom_bridges", ], ): @@ -761,30 +770,46 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", False) - settings.set("bridge_custom_bridges", "") + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_custom_bridges", "") if self.bridge_obfs4_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", True) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", False) - settings.set("bridge_custom_bridges", "") + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_custom_bridges", "") if self.bridge_meek_azure_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", True) settings.set("tor_bridges_use_snowflake", False) - settings.set("bridge_custom_bridges", "") + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_custom_bridges", "") if self.bridge_snowflake_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", True) - settings.set("bridge_custom_bridges", "") + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_custom_bridges", "") + if self.bridge_moat_radio.isChecked(): + settings.set("no_bridges", False) + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) + settings.set("tor_bridges_use_moat", True) + settings.set( + "tor_bridges_use_moat_bridges", self.bridge_moat_textbox.toPlainText() + ) + settings.set("tor_bridges_use_custom_bridges", "") if self.bridge_custom_radio.isChecked(): settings.set("no_bridges", False) settings.set("tor_bridges_use_obfs4", False) settings.set("tor_bridges_use_meek_lite_azure", False) settings.set("tor_bridges_use_snowflake", False) + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_moat_bridges", "") # Insert a 'Bridge' line at the start of each bridge. # This makes it easier to copy/paste a set of bridges @@ -814,7 +839,7 @@ class TorSettingsDialog(QtWidgets.QDialog): if bridges_valid: new_bridges = "".join(new_bridges) - settings.set("bridge_custom_bridges", new_bridges) + settings.set("tor_bridges_use_custom_bridges", new_bridges) else: Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) settings.set("no_bridges", True) From 40cb55894a53dfc5a8cb477fa606563d37d274b6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 15:34:42 -0700 Subject: [PATCH 25/70] Totally change the Tor Settings dialog to even more closely resemble Tor Browser --- .../src/onionshare/resources/locale/en.json | 6 +- desktop/src/onionshare/tor_settings_dialog.py | 383 ++++++++---------- 2 files changed, 182 insertions(+), 207 deletions(-) diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 8a14f8bf..63bfd48c 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -62,14 +62,14 @@ "gui_settings_password_label": "Password", "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_bridge_radio_builtin": "Select a built-in bridge", "gui_settings_bridge_none_radio_option": "Don't use a bridge", - "gui_settings_bridge_obfs4_radio_option": "Use built-in obfs4 bridge", - "gui_settings_bridge_meek_azure_radio_option": "Use built-in meek-azure bridge", - "gui_settings_bridge_snowflake_radio_option": "Use built-in snowflake 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.", "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org", "gui_settings_bridge_moat_button": "Request a New Bridge...", "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", + "gui_settings_bridge_custom_placeholder": "type address:port (one per line)", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 477758c2..6fa4dda1 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -72,15 +72,6 @@ class TorSettingsDialog(QtWidgets.QDialog): # Bridge options for bundled tor - bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label")) - bridges_label.setWordWrap(True) - - # No bridges option radio - self.bridge_none_radio = QtWidgets.QRadioButton( - strings._("gui_settings_bridge_none_radio_option") - ) - self.bridge_none_radio.toggled.connect(self.bridge_none_radio_toggled) - ( self.tor_path, self.tor_geo_ip_file_path, @@ -89,53 +80,30 @@ class TorSettingsDialog(QtWidgets.QDialog): self.snowflake_file_path, ) = self.common.gui.get_tor_paths() - # obfs4 option radio - # if the obfs4proxy binary is missing, we can't use obfs4 transports - self.bridge_obfs4_radio = QtWidgets.QRadioButton( - strings._("gui_settings_bridge_obfs4_radio_option") - ) - self.bridge_obfs4_radio.toggled.connect(self.bridge_obfs4_radio_toggled) - if not self.obfs4proxy_file_path or not os.path.isfile( - self.obfs4proxy_file_path - ): - self.common.log( - "TorSettingsDialog", - "__init__", - f"missing binary {self.obfs4proxy_file_path}, hiding obfs4 bridge", - ) - self.bridge_obfs4_radio.hide() + bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label")) + bridges_label.setWordWrap(True) - # meek-azure option radio - # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports - self.bridge_meek_azure_radio = QtWidgets.QRadioButton( - strings._("gui_settings_bridge_meek_azure_radio_option") + self.bridge_use_checkbox = QtWidgets.QCheckBox( + strings._("gui_settings_bridge_use_checkbox") ) - self.bridge_meek_azure_radio.toggled.connect( - self.bridge_meek_azure_radio_toggled + self.bridge_use_checkbox.stateChanged.connect( + self.bridge_use_checkbox_state_changed ) - if not self.obfs4proxy_file_path or not os.path.isfile( - self.obfs4proxy_file_path - ): - self.common.log( - "TorSettingsDialog", - "__init__", - f"missing binary {self.obfs4proxy_file_path}, hiding meek-azure bridge", - ) - self.bridge_meek_azure_radio.hide() - # snowflake option radio - # if the snowflake-client binary is missing, we can't use snowflake transports - self.bridge_snowflake_radio = QtWidgets.QRadioButton( - strings._("gui_settings_bridge_snowflake_radio_option") + # Built-in bridge + self.bridge_builtin_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_radio_builtin") ) - self.bridge_snowflake_radio.toggled.connect(self.bridge_snowflake_radio_toggled) - if not self.snowflake_file_path or not os.path.isfile(self.snowflake_file_path): - self.common.log( - "TorSettingsDialog", - "__init__", - f"missing binary {self.snowflake_file_path}, hiding snowflake bridge", - ) - self.bridge_snowflake_radio.hide() + self.bridge_builtin_radio.toggled.connect(self.bridge_builtin_radio_toggled) + self.bridge_builtin_dropdown = QtWidgets.QComboBox() + self.bridge_builtin_dropdown.currentTextChanged.connect( + self.bridge_builtin_dropdown_changed + ) + if self.obfs4proxy_file_path and os.path.isfile(self.obfs4proxy_file_path): + self.bridge_builtin_dropdown.addItem("obfs4") + self.bridge_builtin_dropdown.addItem("meek-azure") + if self.snowflake_file_path and os.path.isfile(self.snowflake_file_path): + self.bridge_builtin_dropdown.addItem("snowflake") # Request a bridge from torproject.org (moat) self.bridge_moat_radio = QtWidgets.QRadioButton( @@ -147,6 +115,7 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked) self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() + self.bridge_moat_textbox.setMinimumHeight(100) self.bridge_moat_textbox.setMaximumHeight(100) self.bridge_moat_textbox.setReadOnly(True) self.bridge_moat_textbox.setWordWrapMode(QtGui.QTextOption.NoWrap) @@ -154,7 +123,6 @@ class TorSettingsDialog(QtWidgets.QDialog): bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_textbox) self.bridge_moat_textbox_options = QtWidgets.QWidget() - self.bridge_moat_textbox_options.setMinimumHeight(50) self.bridge_moat_textbox_options.setLayout(bridge_moat_textbox_options_layout) self.bridge_moat_textbox_options.hide() @@ -164,8 +132,11 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.bridge_custom_radio.toggled.connect(self.bridge_custom_radio_toggled) self.bridge_custom_textbox = QtWidgets.QPlainTextEdit() - self.bridge_custom_textbox.setMaximumHeight(200) - self.bridge_custom_textbox.setPlaceholderText("[address:port] [identifier]") + self.bridge_custom_textbox.setMinimumHeight(100) + self.bridge_custom_textbox.setMaximumHeight(100) + self.bridge_custom_textbox.setPlaceholderText( + strings._("gui_settings_bridge_custom_placeholder") + ) bridge_custom_textbox_options_layout = QtWidgets.QVBoxLayout() bridge_custom_textbox_options_layout.addWidget(self.bridge_custom_textbox) @@ -176,17 +147,22 @@ class TorSettingsDialog(QtWidgets.QDialog): ) self.bridge_custom_textbox_options.hide() + # Bridge settings layout + bridge_settings_layout = QtWidgets.QVBoxLayout() + bridge_settings_layout.addWidget(self.bridge_builtin_radio) + bridge_settings_layout.addWidget(self.bridge_builtin_dropdown) + bridge_settings_layout.addWidget(self.bridge_moat_radio) + bridge_settings_layout.addWidget(self.bridge_moat_textbox_options) + bridge_settings_layout.addWidget(self.bridge_custom_radio) + bridge_settings_layout.addWidget(self.bridge_custom_textbox_options) + self.bridge_settings = QtWidgets.QWidget() + self.bridge_settings.setLayout(bridge_settings_layout) + # Bridges layout/widget bridges_layout = QtWidgets.QVBoxLayout() bridges_layout.addWidget(bridges_label) - bridges_layout.addWidget(self.bridge_none_radio) - bridges_layout.addWidget(self.bridge_obfs4_radio) - bridges_layout.addWidget(self.bridge_meek_azure_radio) - bridges_layout.addWidget(self.bridge_snowflake_radio) - bridges_layout.addWidget(self.bridge_moat_radio) - bridges_layout.addWidget(self.bridge_moat_textbox_options) - bridges_layout.addWidget(self.bridge_custom_radio) - bridges_layout.addWidget(self.bridge_custom_textbox_options) + bridges_layout.addWidget(self.bridge_use_checkbox) + bridges_layout.addWidget(self.bridge_settings) self.bridges = QtWidgets.QWidget() self.bridges.setLayout(bridges_layout) @@ -412,46 +388,56 @@ class TorSettingsDialog(QtWidgets.QDialog): ) if self.old_settings.get("no_bridges"): - self.bridge_none_radio.setChecked(True) - self.bridge_obfs4_radio.setChecked(False) - self.bridge_meek_azure_radio.setChecked(False) - self.bridge_snowflake_radio.setChecked(False) - self.bridge_moat_radio.setChecked(False) - self.bridge_custom_radio.setChecked(False) - else: - self.bridge_none_radio.setChecked(False) - self.bridge_obfs4_radio.setChecked( - self.old_settings.get("tor_bridges_use_obfs4") - ) - self.bridge_meek_azure_radio.setChecked( - self.old_settings.get("tor_bridges_use_meek_lite_azure") - ) - self.bridge_snowflake_radio.setChecked( - self.old_settings.get("tor_bridges_use_snowflake") - ) - self.bridge_moat_radio.setChecked( - self.old_settings.get("tor_bridges_use_moat") - ) - moat_bridges = self.old_settings.get("tor_bridges_use_moat_bridges") - self.bridge_moat_textbox.document().setPlainText(moat_bridges) - if len(moat_bridges.strip()) > 0: - self.bridge_moat_textbox.show() - else: - self.bridge_moat_textbox.hide() + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.bridge_settings.hide() - if self.old_settings.get("tor_bridges_use_custom_bridges"): - self.bridge_custom_radio.setChecked(True) - # Remove the 'Bridge' lines at the start of each bridge. - # They are added automatically to provide compatibility with - # copying/pasting bridges provided from https://bridges.torproject.org - new_bridges = [] - bridges = self.old_settings.get("tor_bridges_use_custom_bridges").split( - "Bridge " - ) - for bridge in bridges: - new_bridges.append(bridge) - new_bridges = "".join(new_bridges) - self.bridge_custom_textbox.setPlainText(new_bridges) + else: + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) + self.bridge_settings.show() + + builtin_obfs4 = self.old_settings.get("tor_bridges_use_obfs4") + builtin_meek_azure = self.old_settings.get( + "tor_bridges_use_meek_lite_azure" + ) + builtin_snowflake = self.old_settings.get("tor_bridges_use_snowflake") + + if builtin_obfs4 or builtin_meek_azure or builtin_snowflake: + self.bridge_builtin_radio.setChecked(True) + self.bridge_builtin_dropdown.show() + if builtin_obfs4: + self.bridge_builtin_dropdown.setCurrentText("obfs4") + elif builtin_meek_azure: + self.bridge_builtin_dropdown.setCurrentText("meek-azure") + elif builtin_snowflake: + self.bridge_builtin_dropdown.setCurrentText("snowflake") + + self.bridge_moat_textbox_options.hide() + self.bridge_custom_textbox_options.hide() + else: + self.bridge_builtin_radio.setChecked(False) + self.bridge_builtin_dropdown.hide() + + use_moat = self.old_settings.get("tor_bridges_use_moat") + self.bridge_moat_radio.setChecked(use_moat) + if use_moat: + self.bridge_builtin_dropdown.hide() + self.bridge_custom_textbox_options.hide() + + moat_bridges = self.old_settings.get("tor_bridges_use_moat_bridges") + self.bridge_moat_textbox.document().setPlainText(moat_bridges) + if len(moat_bridges.strip()) > 0: + self.bridge_moat_textbox_options.show() + else: + self.bridge_moat_textbox_options.hide() + + custom_bridges = self.old_settings.get("tor_bridges_use_custom_bridges") + if len(custom_bridges.strip()) != 0: + self.bridge_custom_radio.setChecked(True) + self.bridge_custom_textbox.setPlainText(custom_bridges) + + self.bridge_builtin_dropdown.hide() + self.bridge_moat_textbox_options.hide() + self.bridge_custom_textbox_options.show() def connection_type_bundled_toggled(self, checked): """ @@ -463,30 +449,31 @@ class TorSettingsDialog(QtWidgets.QDialog): self.connection_type_socks.hide() self.connection_type_bridges_radio_group.show() - def bridge_none_radio_toggled(self, checked): + def bridge_use_checkbox_state_changed(self, state): """ - 'No bridges' option was toggled. If checked, enable other bridge options. + 'Use a bridge' checkbox changed + """ + if state == QtCore.Qt.Checked: + self.bridge_settings.show() + self.bridge_builtin_radio.click() + self.bridge_builtin_dropdown.setCurrentText("obfs4") + else: + self.bridge_settings.hide() + + def bridge_builtin_radio_toggled(self, checked): + """ + 'Select a built-in bridge' radio button toggled """ if checked: + self.bridge_builtin_dropdown.show() self.bridge_custom_textbox_options.hide() self.bridge_moat_textbox_options.hide() - def bridge_obfs4_radio_toggled(self, checked): + def bridge_builtin_dropdown_changed(self, selection): """ - obfs4 bridges option was toggled. If checked, disable custom bridge options. + Build-in bridge selection changed """ - if checked: - self.bridge_custom_textbox_options.hide() - self.bridge_moat_textbox_options.hide() - - def bridge_meek_azure_radio_toggled(self, checked): - """ - meek_lite_azure bridges option was toggled. If checked, disable custom bridge options. - """ - if checked: - self.bridge_custom_textbox_options.hide() - self.bridge_moat_textbox_options.hide() - + if selection == "meek-azure": # Alert the user about meek's costliness if it looks like they're turning it on if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): Alert( @@ -495,19 +482,12 @@ class TorSettingsDialog(QtWidgets.QDialog): QtWidgets.QMessageBox.Warning, ) - def bridge_snowflake_radio_toggled(self, checked): - """ - snowflake bridges option was toggled. If checked, disable custom bridge options. - """ - if checked: - self.bridge_custom_textbox_options.hide() - self.bridge_moat_textbox_options.hide() - def bridge_moat_radio_toggled(self, checked): """ Moat (request bridge) bridges option was toggled. If checked, show moat bridge options. """ if checked: + self.bridge_builtin_dropdown.hide() self.bridge_custom_textbox_options.hide() self.bridge_moat_textbox_options.show() @@ -534,8 +514,9 @@ class TorSettingsDialog(QtWidgets.QDialog): Custom bridges option was toggled. If checked, show custom bridge options. """ if checked: - self.bridge_custom_textbox_options.show() + self.bridge_builtin_dropdown.hide() self.bridge_moat_textbox_options.hide() + self.bridge_custom_textbox_options.show() def connection_type_automatic_toggled(self, checked): """ @@ -592,6 +573,8 @@ class TorSettingsDialog(QtWidgets.QDialog): """ self.common.log("TorSettingsDialog", "test_tor_clicked") settings = self.settings_from_fields() + if not settings: + return onion = Onion( self.common, @@ -765,85 +748,77 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("auth_password", self.authenticate_password_extras_password.text()) # Whether we use bridges - if self.bridge_none_radio.isChecked(): + if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked: + settings.set("no_bridges", False) + + if self.bridge_builtin_radio.isChecked(): + selection = self.bridge_builtin_dropdown.currentText() + if selection == "obfs4": + settings.set("tor_bridges_use_obfs4", True) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) + elif selection == "meek-azure": + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", True) + settings.set("tor_bridges_use_snowflake", False) + elif selection == "snowflake": + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", True) + + settings.set("tor_bridges_use_moat", False) + settings.set("tor_bridges_use_custom_bridges", "") + + if self.bridge_moat_radio.isChecked(): + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) + + settings.set("tor_bridges_use_moat", True) + settings.set( + "tor_bridges_use_moat_bridges", + self.bridge_moat_textbox.toPlainText(), + ) + + settings.set("tor_bridges_use_custom_bridges", "") + + if self.bridge_custom_radio.isChecked(): + settings.set("tor_bridges_use_obfs4", False) + settings.set("tor_bridges_use_meek_lite_azure", False) + settings.set("tor_bridges_use_snowflake", False) + settings.set("tor_bridges_use_moat", False) + + 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=(.+)" + ) + if ( + ipv4_pattern.match(bridge) + or ipv6_pattern.match(bridge) + or meek_lite_pattern.match(bridge) + ): + new_bridges.append(bridge) + bridges_valid = True + + if bridges_valid: + new_bridges = "\n".join(new_bridges) + "\n" + settings.set("tor_bridges_use_custom_bridges", new_bridges) + else: + Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) + return False + else: settings.set("no_bridges", True) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.bridge_obfs4_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", True) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.bridge_meek_azure_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", True) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.bridge_snowflake_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", True) - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") - if self.bridge_moat_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", True) - settings.set( - "tor_bridges_use_moat_bridges", self.bridge_moat_textbox.toPlainText() - ) - settings.set("tor_bridges_use_custom_bridges", "") - if self.bridge_custom_radio.isChecked(): - settings.set("no_bridges", False) - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_moat_bridges", "") - - # Insert a 'Bridge' line at the start of each bridge. - # This makes it easier to copy/paste a set of bridges - # provided from https://bridges.torproject.org - 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=(.+)" - ) - if ( - ipv4_pattern.match(bridge) - or ipv6_pattern.match(bridge) - or meek_lite_pattern.match(bridge) - ): - new_bridges.append("".join(["Bridge ", bridge, "\n"])) - bridges_valid = True - - if bridges_valid: - new_bridges = "".join(new_bridges) - settings.set("tor_bridges_use_custom_bridges", new_bridges) - else: - Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) - settings.set("no_bridges", True) - return False return settings From a89412e79d3aab8a64e5a5348bf6386544363612 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 15:47:11 -0700 Subject: [PATCH 26/70] Make it so when selecting moat tor actually uses those bridges, and improve tor settings dialog --- cli/onionshare_cli/onion.py | 16 +++++++++++++--- desktop/src/onionshare/moat_dialog.py | 1 + desktop/src/onionshare/resources/locale/en.json | 3 ++- desktop/src/onionshare/tor_settings_dialog.py | 8 +++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index c3ee4fac..d52af9f3 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -333,9 +333,19 @@ class Onion(object): for line in o: f.write(line) - if self.settings.get("tor_bridges_use_custom_bridges"): - f.write(self.settings.get("tor_bridges_use_custom_bridges") + "\n") - f.write("\nUseBridges 1") + elif self.settings.get("tor_bridges_use_moat"): + for line in self.settings.get("tor_bridges_use_moat_bridges").split( + "\n" + ): + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") + + elif self.settings.get("tor_bridges_use_custom_bridges"): + for line in self.settings.get( + "tor_bridges_use_custom_bridges" + ).split("\n"): + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") # Execute a tor subprocess start_ts = time.time() diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 3cb6519b..ea58898b 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -172,6 +172,7 @@ class MoatDialog(QtWidgets.QDialog): self.label.setText(strings._("moat_captcha_label")) self.captcha.show() + self.solution_lineedit.setText("") self.solution_lineedit.show() self.reload_button.show() self.submit_button.show() diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 63bfd48c..a9fb562a 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -67,9 +67,10 @@ "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.", "gui_settings_bridge_moat_radio_option": "Request a bridge from torproject.org", - "gui_settings_bridge_moat_button": "Request a New Bridge...", + "gui_settings_bridge_moat_button": "Request a New Bridge", "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_bridge_custom_placeholder": "type address:port (one per line)", + "gui_settings_moat_bridges_invalid": "You have not requested a bridge from torproject.org yet.", "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 6fa4dda1..adad6931 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -775,9 +775,15 @@ class TorSettingsDialog(QtWidgets.QDialog): settings.set("tor_bridges_use_snowflake", False) settings.set("tor_bridges_use_moat", True) + + moat_bridges = self.bridge_moat_textbox.toPlainText() + if moat_bridges.strip() == "": + Alert(self.common, strings._("gui_settings_moat_bridges_invalid")) + return False + settings.set( "tor_bridges_use_moat_bridges", - self.bridge_moat_textbox.toPlainText(), + moat_bridges, ) settings.set("tor_bridges_use_custom_bridges", "") From 3b6b74f649002f59c8fba07815b30c7340911687 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 17 Oct 2021 15:59:07 -0700 Subject: [PATCH 27/70] Rearrange moat dialog so pressing enter submits --- desktop/src/onionshare/moat_dialog.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index ea58898b..2651736e 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -55,11 +55,9 @@ class MoatDialog(QtWidgets.QDialog): # Solution input self.solution_lineedit = QtWidgets.QLineEdit() self.solution_lineedit.setPlaceholderText(strings._("moat_captcha_placeholder")) - self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) - self.reload_button.clicked.connect(self.reload_clicked) - solution_layout = QtWidgets.QHBoxLayout() - solution_layout.addWidget(self.solution_lineedit) - solution_layout.addWidget(self.reload_button) + self.solution_lineedit.editingFinished.connect( + self.solution_lineedit_editing_finished + ) # Error label self.error_label = QtWidgets.QLabel() @@ -69,20 +67,23 @@ class MoatDialog(QtWidgets.QDialog): # Buttons self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) self.submit_button.clicked.connect(self.submit_clicked) + self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) + self.reload_button.clicked.connect(self.reload_clicked) self.cancel_button = QtWidgets.QPushButton( strings._("gui_settings_button_cancel") ) self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addStretch() buttons_layout.addWidget(self.submit_button) + buttons_layout.addStretch() + buttons_layout.addWidget(self.reload_button) buttons_layout.addWidget(self.cancel_button) # Layout layout = QtWidgets.QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.captcha) - layout.addLayout(solution_layout) + layout.addWidget(self.solution_lineedit) layout.addStretch() layout.addWidget(self.error_label) layout.addLayout(buttons_layout) @@ -117,6 +118,7 @@ class MoatDialog(QtWidgets.QDialog): Submit button clicked. """ self.error_label.hide() + self.solution_lineedit.setEnabled(False) solution = self.solution_lineedit.text().strip() if len(solution) == 0: @@ -148,6 +150,8 @@ class MoatDialog(QtWidgets.QDialog): self.error_label.setText(strings._("moat_bridgedb_error")) self.error_label.show() + self.solution_lineedit.setEnabled(True) + def captcha_error(self, msg): self.common.log("MoatDialog", "captcha_error") if msg == "": @@ -156,6 +160,8 @@ class MoatDialog(QtWidgets.QDialog): self.error_label.setText(msg) self.error_label.show() + self.solution_lineedit.setEnabled(True) + def captcha_ready(self, image, challenge): self.common.log("MoatDialog", "captcha_ready") @@ -172,11 +178,16 @@ class MoatDialog(QtWidgets.QDialog): self.label.setText(strings._("moat_captcha_label")) self.captcha.show() + self.solution_lineedit.setEnabled(True) self.solution_lineedit.setText("") self.solution_lineedit.show() + self.solution_lineedit.setFocus() self.reload_button.show() self.submit_button.show() + def solution_lineedit_editing_finished(self): + self.common.log("MoatDialog", "solution_lineedit_editing_finished") + def bridges_ready(self, bridges): self.common.log("MoatDialog", "bridges_ready", bridges) self.got_bridges.emit(bridges) From 0989f2b133a46f293a65c9e11a01e8a097e479a1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 18 Oct 2021 17:17:47 +1100 Subject: [PATCH 28/70] 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 From 5b4d77c3634c0c13ae8ab1f27be540260027d0a8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 19 Oct 2021 11:36:03 +1100 Subject: [PATCH 29/70] 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", "") From bd6390042f73ec64be6cbe3eed4cbc67875f9f6d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 19 Oct 2021 11:46:21 +1100 Subject: [PATCH 30/70] Try to bail if we are not in local-only mode and couldn't start the Meek client --- desktop/src/onionshare/moat_dialog.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 78a05482..cedc52d8 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -25,6 +25,7 @@ import base64 from . import strings from .gui_common import GuiCommon +from onionshare_cli.meek import MeekNotRunning class MoatDialog(QtWidgets.QDialog): @@ -227,6 +228,11 @@ class MoatThread(QtCore.QThread): # Start Meek so that we can do domain fronting self.meek.start() + # We should only fetch bridges if we can domain front, + # but we can override this in local-only mode. + if not self.meek.meek_proxies and not self.common.gui.local_only: + raise MeekNotRunning() + if self.action == "fetch": self.common.log("MoatThread", "run", f"starting fetch") From 072f90df4f40d74c24769c5b1c7802c23bd77c3c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 18 Oct 2021 18:18:04 -0700 Subject: [PATCH 31/70] Move Submit button next to the input field in MoatDialog --- desktop/src/onionshare/moat_dialog.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 2651736e..fbaac788 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -58,6 +58,11 @@ class MoatDialog(QtWidgets.QDialog): self.solution_lineedit.editingFinished.connect( self.solution_lineedit_editing_finished ) + self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) + self.submit_button.clicked.connect(self.submit_clicked) + solution_layout = QtWidgets.QHBoxLayout() + solution_layout.addWidget(self.solution_lineedit) + solution_layout.addWidget(self.submit_button) # Error label self.error_label = QtWidgets.QLabel() @@ -65,8 +70,6 @@ class MoatDialog(QtWidgets.QDialog): self.error_label.hide() # Buttons - self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) - self.submit_button.clicked.connect(self.submit_clicked) self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) self.reload_button.clicked.connect(self.reload_clicked) self.cancel_button = QtWidgets.QPushButton( @@ -74,7 +77,6 @@ class MoatDialog(QtWidgets.QDialog): ) self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addWidget(self.submit_button) buttons_layout.addStretch() buttons_layout.addWidget(self.reload_button) buttons_layout.addWidget(self.cancel_button) @@ -83,7 +85,7 @@ class MoatDialog(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.captcha) - layout.addWidget(self.solution_lineedit) + layout.addLayout(solution_layout) layout.addStretch() layout.addWidget(self.error_label) layout.addLayout(buttons_layout) From 1f5dcd1689760fb568926cd70388734e538a23fc Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 19 Oct 2021 08:41:40 -0700 Subject: [PATCH 32/70] Ask BridgeDB for obfs4 and snowflake bridges, because that is what OnionShare supports --- desktop/src/onionshare/moat_dialog.py | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index fbaac788..28193c25 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -22,6 +22,7 @@ from PySide2 import QtCore, QtWidgets, QtGui import requests import os import base64 +import json from . import strings from .gui_common import GuiCommon @@ -133,7 +134,11 @@ class MoatDialog(QtWidgets.QDialog): self.t_check = MoatThread( self.common, "check", - {"challenge": self.challenge, "solution": self.solution_lineedit.text()}, + { + "transport": self.transport, + "challenge": self.challenge, + "solution": self.solution_lineedit.text(), + }, ) self.t_check.bridgedb_error.connect(self.bridgedb_error) self.t_check.captcha_error.connect(self.captcha_error) @@ -164,9 +169,10 @@ class MoatDialog(QtWidgets.QDialog): self.solution_lineedit.setEnabled(True) - def captcha_ready(self, image, challenge): + def captcha_ready(self, transport, image, challenge): self.common.log("MoatDialog", "captcha_ready") + self.transport = transport self.challenge = challenge # Save captcha image to disk, so we can load it @@ -208,7 +214,7 @@ class MoatThread(QtCore.QThread): bridgedb_error = QtCore.Signal() captcha_error = QtCore.Signal(str) - captcha_ready = QtCore.Signal(str, str) + captcha_ready = QtCore.Signal(str, str, str) bridges_ready = QtCore.Signal(str) def __init__(self, common, action, data={}): @@ -216,7 +222,6 @@ class MoatThread(QtCore.QThread): self.common = common self.common.log("MoatThread", "__init__", f"action={action}") - self.transport = "obfs4" self.action = action self.data = data @@ -235,7 +240,10 @@ class MoatThread(QtCore.QThread): { "version": "0.1.0", "type": "client-transports", - "supported": [self.transport], + "supported": [ + "obfs4", + "snowflake", + ], } ] }, @@ -259,17 +267,12 @@ class MoatThread(QtCore.QThread): self.common.log("MoatThread", "run", f"type != moat-challange") self.bridgedb_error.emit() return - if moat_res["data"][0]["transport"] != self.transport: - self.common.log( - "MoatThread", "run", f"transport != {self.transport}" - ) - self.bridgedb_error.emit() - return + transport = moat_res["data"][0]["transport"] image = moat_res["data"][0]["image"] challenge = moat_res["data"][0]["challenge"] - self.captcha_ready.emit(image, challenge) + self.captcha_ready.emit(transport, image, challenge) except Exception as e: self.common.log("MoatThread", "run", f"hit exception: {e}") self.bridgedb_error.emit() @@ -288,7 +291,7 @@ class MoatThread(QtCore.QThread): "id": "2", "type": "moat-solution", "version": "0.1.0", - "transport": self.transport, + "transport": self.data["transport"], "challenge": self.data["challenge"], "solution": self.data["solution"], "qrcode": "false", @@ -303,6 +306,11 @@ class MoatThread(QtCore.QThread): try: moat_res = r.json() + self.common.log( + "MoatThread", + "run", + f"got bridges:\n{json.dumps(moat_res,indent=2)}", + ) if "errors" in moat_res: self.common.log("MoatThread", "run", f"errors={moat_res['errors']}") From 3aa790269592d9d7e25b98c5b7e639c191f01f7d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 19 Oct 2021 08:50:33 -0700 Subject: [PATCH 33/70] Don't print Bridge lines in torrc for blank lines --- cli/onionshare_cli/onion.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index d52af9f3..0d205b97 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -337,14 +337,16 @@ class Onion(object): for line in self.settings.get("tor_bridges_use_moat_bridges").split( "\n" ): - f.write(f"Bridge {line}\n") + if line.strip() != "": + f.write(f"Bridge {line}\n") f.write("\nUseBridges 1\n") elif self.settings.get("tor_bridges_use_custom_bridges"): for line in self.settings.get( "tor_bridges_use_custom_bridges" ).split("\n"): - f.write(f"Bridge {line}\n") + if line.strip() != "": + f.write(f"Bridge {line}\n") f.write("\nUseBridges 1\n") # Execute a tor subprocess From 596c819957d6e74db397e55a2f0c4dcb2b1cc65d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 19 Oct 2021 08:53:52 -0700 Subject: [PATCH 34/70] Allow custom snowflake bridges --- desktop/src/onionshare/tor_settings_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index adad6931..38ff512a 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -809,10 +809,14 @@ class TorSettingsDialog(QtWidgets.QDialog): 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 From 1fa82818c3d8467238f35691061a8dceaa7cbcd4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 20 Oct 2021 15:55:24 +1100 Subject: [PATCH 35/70] Add meek_client stuff to CLI tests --- cli/tests/test_cli_common.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cli/tests/test_cli_common.py b/cli/tests/test_cli_common.py index a4798d1b..9a64d762 100644 --- a/cli/tests/test_cli_common.py +++ b/cli/tests/test_cli_common.py @@ -162,6 +162,9 @@ class TestGetTorPaths: tor_geo_ip_file_path = os.path.join(base_path, "Resources", "Tor", "geoip") tor_geo_ipv6_file_path = os.path.join(base_path, "Resources", "Tor", "geoip6") obfs4proxy_file_path = os.path.join(base_path, "Resources", "Tor", "obfs4proxy") + meek_client_file_path = os.path.join( + base_path, "Resources", "Tor", "meek-client" + ) snowflake_file_path = os.path.join( base_path, "Resources", "Tor", "snowflake-client" ) @@ -171,6 +174,7 @@ class TestGetTorPaths: tor_geo_ipv6_file_path, obfs4proxy_file_path, snowflake_file_path, + meek_client_file_path, ) @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") @@ -181,6 +185,7 @@ class TestGetTorPaths: tor_geo_ipv6_file_path, _, # obfs4proxy is optional _, # snowflake-client is optional + _, # meek-client is optional ) = common_obj.get_tor_paths() assert os.path.basename(tor_path) == "tor" @@ -207,6 +212,9 @@ class TestGetTorPaths: snowflake_file_path = os.path.join( os.path.join(base_path, "Tor"), "snowflake-client.exe" ) + meek_client_file_path = os.path.join( + os.path.join(base_path, "Tor"), "meek-client.exe" + ) tor_geo_ip_file_path = os.path.join( os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip" ) @@ -219,6 +227,7 @@ class TestGetTorPaths: tor_geo_ipv6_file_path, obfs4proxy_file_path, snowflake_file_path, + meek_client_file_path, ) From 55d6ac4e3d02e02d718569ee0c718ad8dcbe3eab Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 20 Oct 2021 18:56:37 -0700 Subject: [PATCH 36/70] Refactor SettingsDialog into SettingsTab --- desktop/src/onionshare/main_window.py | 19 ++-- .../{settings_dialog.py => settings_tab.py} | 78 +++------------- desktop/src/onionshare/tab_widget.py | 91 ++++++++++++++----- ...settings_dialog.py => tor_settings_tab.py} | 48 +++++----- 4 files changed, 115 insertions(+), 121 deletions(-) rename desktop/src/onionshare/{settings_dialog.py => settings_tab.py} (84%) rename desktop/src/onionshare/{tor_settings_dialog.py => tor_settings_tab.py} (95%) diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index c125741c..0f11cf8e 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -24,8 +24,6 @@ from PySide2 import QtCore, QtWidgets, QtGui from . import strings from .tor_connection_dialog import TorConnectionDialog -from .tor_settings_dialog import TorSettingsDialog -from .settings_dialog import SettingsDialog from .widgets import Alert from .update_checker import UpdateThread from .tab_widget import TabWidget @@ -245,21 +243,22 @@ class MainWindow(QtWidgets.QMainWindow): def open_tor_settings(self): """ - Open the TorSettingsDialog. + Open the TorSettingsTab """ self.common.log("MainWindow", "open_tor_settings") - d = TorSettingsDialog(self.common) - d.settings_saved.connect(self.settings_have_changed) - d.exec_() + # d = TorSettingsDialog(self.common) + # d.settings_saved.connect(self.settings_have_changed) + # d.exec_() def open_settings(self): """ - Open the SettingsDialog. + Open the SettingsTab """ self.common.log("MainWindow", "open_settings") - d = SettingsDialog(self.common) - d.settings_saved.connect(self.settings_have_changed) - d.exec_() + self.tabs.open_settings_tab() + # d = SettingsDialog(self.common) + # d.settings_saved.connect(self.settings_have_changed) + # d.exec_() def settings_have_changed(self): self.common.log("OnionShareGui", "settings_have_changed") diff --git a/desktop/src/onionshare/settings_dialog.py b/desktop/src/onionshare/settings_tab.py similarity index 84% rename from desktop/src/onionshare/settings_dialog.py rename to desktop/src/onionshare/settings_tab.py index b1003386..251783aa 100644 --- a/desktop/src/onionshare/settings_dialog.py +++ b/desktop/src/onionshare/settings_tab.py @@ -19,57 +19,34 @@ along with this program. If not, see . """ from PySide2 import QtCore, QtWidgets, QtGui -from PySide2.QtCore import Slot, Qt -from PySide2.QtGui import QPalette, QColor import sys import platform import datetime import re import os from onionshare_cli.settings import Settings -from onionshare_cli.onion import ( - Onion, - TorErrorInvalidSetting, - TorErrorAutomatic, - TorErrorSocketPort, - TorErrorSocketFile, - TorErrorMissingPassword, - TorErrorUnreadableCookieFile, - TorErrorAuthError, - TorErrorProtocolError, - BundledTorTimeout, - BundledTorBroken, - TorTooOldEphemeral, - TorTooOldStealth, - PortNotAvailable, -) from . import strings from .widgets import Alert from .update_checker import UpdateThread -from .tor_connection_dialog import TorConnectionDialog from .gui_common import GuiCommon -class SettingsDialog(QtWidgets.QDialog): +class SettingsTab(QtWidgets.QWidget): """ Settings dialog. """ settings_saved = QtCore.Signal() - def __init__(self, common): - super(SettingsDialog, self).__init__() + def __init__(self, common, tab_id): + super(SettingsTab, self).__init__() self.common = common - - self.common.log("SettingsDialog", "__init__") - - self.setModal(True) - self.setWindowTitle(strings._("gui_settings_window_title")) - self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + self.common.log("SettingsTab", "__init__") self.system = platform.system() + self.tab_id = tab_id # Automatic updates options @@ -146,31 +123,26 @@ class SettingsDialog(QtWidgets.QDialog): # Buttons self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) - self.cancel_button = QtWidgets.QPushButton( - strings._("gui_settings_button_cancel") - ) - self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) - buttons_layout.addWidget(self.cancel_button) # Layout layout = QtWidgets.QVBoxLayout() + layout.addStretch() layout.addWidget(autoupdate_group) if autoupdate_group.isVisible(): layout.addSpacing(20) layout.addLayout(language_layout) layout.addLayout(theme_layout) layout.addSpacing(20) - layout.addStretch() layout.addWidget(version_label) layout.addWidget(help_label) layout.addSpacing(20) layout.addLayout(buttons_layout) + layout.addStretch() self.setLayout(layout) - self.cancel_button.setFocus() self.reload_settings() @@ -199,7 +171,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Check for Updates button clicked. Manually force an update check. """ - self.common.log("SettingsDialog", "check_for_updates") + self.common.log("SettingsTab", "check_for_updates") # Disable buttons self._disable_buttons() self.common.gui.qtapp.processEvents() @@ -261,7 +233,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Save button clicked. Save current settings to disk. """ - self.common.log("SettingsDialog", "save_clicked") + self.common.log("SettingsTab", "save_clicked") def changed(s1, s2, keys): """ @@ -301,30 +273,12 @@ class SettingsDialog(QtWidgets.QDialog): self.settings_saved.emit() self.close() - def cancel_clicked(self): - """ - Cancel button clicked. - """ - self.common.log("SettingsDialog", "cancel_clicked") - if ( - not self.common.gui.local_only - and not self.common.gui.onion.is_authenticated() - ): - Alert( - self.common, - strings._("gui_tor_connection_canceled"), - QtWidgets.QMessageBox.Warning, - ) - sys.exit() - else: - self.close() - def help_clicked(self): """ Help button clicked. """ - self.common.log("SettingsDialog", "help_clicked") - SettingsDialog.open_help() + self.common.log("SettingsTab", "help_clicked") + SettingsTab.open_help() @staticmethod def open_help(): @@ -335,7 +289,7 @@ class SettingsDialog(QtWidgets.QDialog): """ Return a Settings object that's full of values from the settings dialog. """ - self.common.log("SettingsDialog", "settings_from_fields") + self.common.log("SettingsTab", "settings_from_fields") settings = Settings(self.common) settings.load() # To get the last update timestamp @@ -351,7 +305,7 @@ class SettingsDialog(QtWidgets.QDialog): return settings def _update_autoupdate_timestamp(self, autoupdate_timestamp): - self.common.log("SettingsDialog", "_update_autoupdate_timestamp") + self.common.log("SettingsTab", "_update_autoupdate_timestamp") if autoupdate_timestamp: dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) @@ -363,18 +317,16 @@ class SettingsDialog(QtWidgets.QDialog): ) def _disable_buttons(self): - self.common.log("SettingsDialog", "_disable_buttons") + self.common.log("SettingsTab", "_disable_buttons") self.check_for_updates_button.setEnabled(False) self.save_button.setEnabled(False) - self.cancel_button.setEnabled(False) def _enable_buttons(self): - self.common.log("SettingsDialog", "_enable_buttons") + self.common.log("SettingsTab", "_enable_buttons") # We can't check for updates if we're still not connected to Tor if not self.common.gui.onion.connected_to_tor: self.check_for_updates_button.setEnabled(False) else: self.check_for_updates_button.setEnabled(True) self.save_button.setEnabled(True) - self.cancel_button.setEnabled(True) diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index a955ea53..daf878d7 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -26,6 +26,8 @@ from . import strings from .tab import Tab from .threads import EventHandlerThread from .gui_common import GuiCommon +from .tor_settings_tab import TorSettingsTab +from .settings_tab import SettingsTab class TabWidget(QtWidgets.QTabWidget): @@ -116,6 +118,11 @@ class TabWidget(QtWidgets.QTabWidget): # Active tab was changed tab_id = self.currentIndex() self.common.log("TabWidget", "tab_changed", f"Tab was changed to {tab_id}") + + # If it's Settings or Tor Settings, ignore + if self.is_settings_tab(tab_id): + return + try: mode = self.tabs[tab_id].get_mode() if mode: @@ -160,20 +167,7 @@ class TabWidget(QtWidgets.QTabWidget): # In macOS, manually create a close button because tabs don't seem to have them otherwise if self.common.platform == "Darwin": - - def close_tab(): - self.tabBar().tabCloseRequested.emit(self.indexOf(tab)) - - tab.close_button = QtWidgets.QPushButton() - tab.close_button.setFlat(True) - tab.close_button.setFixedWidth(40) - tab.close_button.setIcon( - QtGui.QIcon(GuiCommon.get_resource_path("images/close_tab.png")) - ) - tab.close_button.clicked.connect(close_tab) - self.tabBar().setTabButton( - index, QtWidgets.QTabBar.RightSide, tab.close_button - ) + self.macos_create_close_button(tab, index) tab.init(mode_settings) @@ -187,6 +181,25 @@ class TabWidget(QtWidgets.QTabWidget): # Bring the window to front, in case this is being added by an event self.bring_to_front.emit() + def open_settings_tab(self): + self.common.log("TabWidget", "open_settings_tab") + + # See if a settings tab is already open, and if so switch to it + for index in range(self.count()): + if self.is_settings_tab(index): + self.setCurrentIndex(index) + return + + settings_tab = SettingsTab(self.common, self.current_tab_id) + self.tabs[self.current_tab_id] = settings_tab + self.current_tab_id += 1 + index = self.addTab(settings_tab, strings._("gui_settings_window_title")) + self.setCurrentIndex(index) + + # In macOS, manually create a close button because tabs don't seem to have them otherwise + if self.common.platform == "Darwin": + self.macos_create_close_button(settings_tab, index) + def change_title(self, tab_id, title): shortened_title = title if len(shortened_title) > 11: @@ -224,9 +237,10 @@ class TabWidget(QtWidgets.QTabWidget): # Figure out the order of persistent tabs to save in settings persistent_tabs = [] for index in range(self.count()): - tab = self.widget(index) - if tab.settings.get("persistent", "enabled"): - persistent_tabs.append(tab.settings.id) + if not self.is_settings_tab(index): + tab = self.widget(index) + if tab.settings.get("persistent", "enabled"): + persistent_tabs.append(tab.settings.id) # Only save if tabs have actually moved if persistent_tabs != self.common.settings.get("persistent_tabs"): self.common.settings.set("persistent_tabs", persistent_tabs) @@ -235,11 +249,8 @@ class TabWidget(QtWidgets.QTabWidget): def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) - if tab.close_tab(): - # If the tab is persistent, delete the settings file from disk - if tab.settings.get("persistent", "enabled"): - tab.settings.delete() + if self.is_settings_tab(index): # Remove the tab self.removeTab(index) del self.tabs[tab.tab_id] @@ -248,7 +259,21 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() - self.save_persistent_tabs() + else: + if tab.close_tab(): + # If the tab is persistent, delete the settings file from disk + if tab.settings.get("persistent", "enabled"): + tab.settings.delete() + + self.save_persistent_tabs() + + # Remove the tab + self.removeTab(index) + del self.tabs[tab.tab_id] + + # If the last tab is closed, open a new one + if self.count() == 0: + self.new_tab_clicked() def are_tabs_active(self): """ @@ -273,6 +298,28 @@ class TabWidget(QtWidgets.QTabWidget): super(TabWidget, self).resizeEvent(event) self.move_new_tab_button() + def macos_create_close_button(self, tab, index): + def close_tab(): + self.tabBar().tabCloseRequested.emit(self.indexOf(tab)) + + close_button = QtWidgets.QPushButton() + close_button.setFlat(True) + close_button.setFixedWidth(40) + close_button.setIcon( + QtGui.QIcon(GuiCommon.get_resource_path("images/close_tab.png")) + ) + close_button.clicked.connect(close_tab) + self.tabBar().setTabButton(index, QtWidgets.QTabBar.RightSide, tab.close_button) + + def is_settings_tab(self, tab_id): + if tab_id not in self.tabs: + return True + + return ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ) + class TabBar(QtWidgets.QTabBar): """ diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_tab.py similarity index 95% rename from desktop/src/onionshare/tor_settings_dialog.py rename to desktop/src/onionshare/tor_settings_tab.py index 38ff512a..279469df 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -34,25 +34,21 @@ from .moat_dialog import MoatDialog from .gui_common import GuiCommon -class TorSettingsDialog(QtWidgets.QDialog): +class TorSettingsTab(QtWidgets.QWidget): """ Settings dialog. """ settings_saved = QtCore.Signal() - def __init__(self, common): - super(TorSettingsDialog, self).__init__() + def __init__(self, common, tab_id): + super(TorSettingsTab, self).__init__() self.common = common - - self.common.log("TorSettingsDialog", "__init__") - - self.setModal(True) - self.setWindowTitle(strings._("gui_tor_settings_window_title")) - self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + self.common.log("TorSettingsTab", "__init__") self.system = platform.system() + self.tab_id = tab_id # Connection type: either automatic, control port, or socket file @@ -443,7 +439,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Connection type bundled was toggled """ - self.common.log("TorSettingsDialog", "connection_type_bundled_toggled") + self.common.log("TorSettingsTab", "connection_type_bundled_toggled") if checked: self.tor_settings_group.hide() self.connection_type_socks.hide() @@ -495,7 +491,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Request new bridge button clicked """ - self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") + self.common.log("TorSettingsTab", "bridge_moat_button_clicked") moat_dialog = MoatDialog(self.common) moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges) @@ -505,7 +501,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Got new bridges from moat """ - self.common.log("TorSettingsDialog", "bridge_moat_got_bridges") + self.common.log("TorSettingsTab", "bridge_moat_got_bridges") self.bridge_moat_textbox.document().setPlainText(bridges) self.bridge_moat_textbox.show() @@ -522,7 +518,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Connection type automatic was toggled. If checked, hide authentication fields. """ - self.common.log("TorSettingsDialog", "connection_type_automatic_toggled") + self.common.log("TorSettingsTab", "connection_type_automatic_toggled") if checked: self.tor_settings_group.hide() self.connection_type_socks.hide() @@ -533,7 +529,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type control port was toggled. If checked, show extra fields for Tor control address and port. If unchecked, hide those extra fields. """ - self.common.log("TorSettingsDialog", "connection_type_control_port_toggled") + self.common.log("TorSettingsTab", "connection_type_control_port_toggled") if checked: self.tor_settings_group.show() self.connection_type_control_port_extras.show() @@ -547,7 +543,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Connection type socket file was toggled. If checked, show extra fields for socket file. If unchecked, hide those extra fields. """ - self.common.log("TorSettingsDialog", "connection_type_socket_file_toggled") + self.common.log("TorSettingsTab", "connection_type_socket_file_toggled") if checked: self.tor_settings_group.show() self.connection_type_socket_file_extras.show() @@ -560,7 +556,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Authentication option no authentication was toggled. """ - self.common.log("TorSettingsDialog", "authenticate_no_auth_toggled") + self.common.log("TorSettingsTab", "authenticate_no_auth_toggled") if checked: self.authenticate_password_extras.hide() else: @@ -571,7 +567,7 @@ class TorSettingsDialog(QtWidgets.QDialog): Test Tor Settings button clicked. With the given settings, see if we can successfully connect and authenticate to Tor. """ - self.common.log("TorSettingsDialog", "test_tor_clicked") + self.common.log("TorSettingsTab", "test_tor_clicked") settings = self.settings_from_fields() if not settings: return @@ -605,7 +601,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Save button clicked. Save current settings to disk. """ - self.common.log("TorSettingsDialog", "save_clicked") + self.common.log("TorSettingsTab", "save_clicked") def changed(s1, s2, keys): """ @@ -628,7 +624,7 @@ class TorSettingsDialog(QtWidgets.QDialog): if not self.common.gui.local_only: if self.common.gui.onion.is_authenticated(): self.common.log( - "TorSettingsDialog", "save_clicked", "Connected to Tor" + "TorSettingsTab", "save_clicked", "Connected to Tor" ) if changed( @@ -654,7 +650,7 @@ class TorSettingsDialog(QtWidgets.QDialog): else: self.common.log( - "TorSettingsDialog", "save_clicked", "Not connected to Tor" + "TorSettingsTab", "save_clicked", "Not connected to Tor" ) # Tor isn't connected, so try connecting reboot_onion = True @@ -663,7 +659,7 @@ class TorSettingsDialog(QtWidgets.QDialog): if reboot_onion: # Reinitialize the Onion object self.common.log( - "TorSettingsDialog", "save_clicked", "rebooting the Onion" + "TorSettingsTab", "save_clicked", "rebooting the Onion" ) self.common.gui.onion.cleanup() @@ -671,7 +667,7 @@ class TorSettingsDialog(QtWidgets.QDialog): tor_con.start() self.common.log( - "TorSettingsDialog", + "TorSettingsTab", "save_clicked", f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", ) @@ -694,7 +690,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Cancel button clicked. """ - self.common.log("TorSettingsDialog", "cancel_clicked") + self.common.log("TorSettingsTab", "cancel_clicked") if ( not self.common.gui.local_only and not self.common.gui.onion.is_authenticated() @@ -712,7 +708,7 @@ class TorSettingsDialog(QtWidgets.QDialog): """ Return a Settings object that's full of values from the settings dialog. """ - self.common.log("TorSettingsDialog", "settings_from_fields") + self.common.log("TorSettingsTab", "settings_from_fields") settings = Settings(self.common) settings.load() # To get the last update timestamp @@ -833,13 +829,13 @@ class TorSettingsDialog(QtWidgets.QDialog): return settings def closeEvent(self, e): - self.common.log("TorSettingsDialog", "closeEvent") + self.common.log("TorSettingsTab", "closeEvent") # On close, if Tor isn't connected, then quit OnionShare altogether if not self.common.gui.local_only: if not self.common.gui.onion.is_authenticated(): self.common.log( - "TorSettingsDialog", + "TorSettingsTab", "closeEvent", "Closing while not connected to Tor", ) From 0fb7d7d761397d6240b76faaaafaa8bf6661280c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 20 Oct 2021 19:03:24 -0700 Subject: [PATCH 37/70] Refactor TorSettingsDialog into TorSettingsTab --- desktop/src/onionshare/main_window.py | 7 +--- desktop/src/onionshare/tab_widget.py | 37 +++++++++++++++++---- desktop/src/onionshare/tor_settings_tab.py | 38 ++++++++++------------ 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index 0f11cf8e..4a9d0c7e 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -246,9 +246,7 @@ class MainWindow(QtWidgets.QMainWindow): Open the TorSettingsTab """ self.common.log("MainWindow", "open_tor_settings") - # d = TorSettingsDialog(self.common) - # d.settings_saved.connect(self.settings_have_changed) - # d.exec_() + self.tabs.open_tor_settings_tab() def open_settings(self): """ @@ -256,9 +254,6 @@ class MainWindow(QtWidgets.QMainWindow): """ self.common.log("MainWindow", "open_settings") self.tabs.open_settings_tab() - # d = SettingsDialog(self.common) - # d.settings_saved.connect(self.settings_have_changed) - # d.exec_() def settings_have_changed(self): self.common.log("OnionShareGui", "settings_have_changed") diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index daf878d7..fe6d08dc 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -92,8 +92,9 @@ class TabWidget(QtWidgets.QTabWidget): # Clean up each tab for index in range(self.count()): - tab = self.widget(index) - tab.cleanup() + if not self.is_settings_tab(index): + tab = self.widget(index) + tab.cleanup() def move_new_tab_button(self): # Find the width of all tabs @@ -186,7 +187,7 @@ class TabWidget(QtWidgets.QTabWidget): # See if a settings tab is already open, and if so switch to it for index in range(self.count()): - if self.is_settings_tab(index): + if type(self.tabs[index]) is SettingsTab: self.setCurrentIndex(index) return @@ -200,6 +201,27 @@ class TabWidget(QtWidgets.QTabWidget): if self.common.platform == "Darwin": self.macos_create_close_button(settings_tab, index) + def open_tor_settings_tab(self): + self.common.log("TabWidget", "open_tor_settings_tab") + + # See if a settings tab is already open, and if so switch to it + for index in range(self.count()): + if type(self.tabs[index]) is TorSettingsTab: + self.setCurrentIndex(index) + return + + tor_settings_tab = TorSettingsTab(self.common, self.current_tab_id) + self.tabs[self.current_tab_id] = tor_settings_tab + self.current_tab_id += 1 + index = self.addTab( + tor_settings_tab, strings._("gui_tor_settings_window_title") + ) + self.setCurrentIndex(index) + + # In macOS, manually create a close button because tabs don't seem to have them otherwise + if self.common.platform == "Darwin": + self.macos_create_close_button(tor_settings_tab, index) + def change_title(self, tab_id, title): shortened_title = title if len(shortened_title) > 11: @@ -280,10 +302,11 @@ class TabWidget(QtWidgets.QTabWidget): See if there are active servers in any open tabs """ for tab_id in self.tabs: - mode = self.tabs[tab_id].get_mode() - if mode: - if mode.server_status.status != mode.server_status.STATUS_STOPPED: - return True + if not self.is_settings_tab(tab_id): + mode = self.tabs[tab_id].get_mode() + if mode: + if mode.server_status.status != mode.server_status.STATUS_STOPPED: + return True return False def paintEvent(self, event): diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 279469df..e46fa729 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -319,15 +319,10 @@ class TorSettingsTab(QtWidgets.QWidget): self.test_tor_button.clicked.connect(self.test_tor_clicked) self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) - self.cancel_button = QtWidgets.QPushButton( - strings._("gui_settings_button_cancel") - ) - self.cancel_button.clicked.connect(self.cancel_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addWidget(self.test_tor_button) buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) - buttons_layout.addWidget(self.cancel_button) # Layout layout = QtWidgets.QVBoxLayout() @@ -337,7 +332,6 @@ class TorSettingsTab(QtWidgets.QWidget): layout.addLayout(buttons_layout) self.setLayout(layout) - self.cancel_button.setFocus() self.reload_settings() @@ -686,23 +680,27 @@ class TorSettingsTab(QtWidgets.QWidget): self.settings_saved.emit() self.close() - def cancel_clicked(self): + def close_tab(self): """ - Cancel button clicked. + Tab is closed """ self.common.log("TorSettingsTab", "cancel_clicked") - if ( - not self.common.gui.local_only - and not self.common.gui.onion.is_authenticated() - ): - Alert( - self.common, - strings._("gui_tor_connection_canceled"), - QtWidgets.QMessageBox.Warning, - ) - sys.exit() - else: - self.close() + return True + + # TODO: Figure out flow for first connecting, when closing settings when not connected + + # if ( + # not self.common.gui.local_only + # and not self.common.gui.onion.is_authenticated() + # ): + # Alert( + # self.common, + # strings._("gui_tor_connection_canceled"), + # QtWidgets.QMessageBox.Warning, + # ) + # sys.exit() + # else: + # self.close() def settings_from_fields(self): """ From 3b9cc80160bf0658e6b7f908cac325f7c99b9041 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 20 Oct 2021 20:33:16 -0700 Subject: [PATCH 38/70] Create a TorConnectionWidget, and use that when testing settings --- .../src/onionshare/tor_connection_dialog.py | 128 ++++++++++++++++++ desktop/src/onionshare/tor_settings_tab.py | 38 +++++- 2 files changed, 160 insertions(+), 6 deletions(-) diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection_dialog.py index daf49a32..7ba7c800 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection_dialog.py @@ -157,6 +157,134 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): QtCore.QTimer.singleShot(1, self.cancel) +class TorConnectionWidget(QtWidgets.QWidget): + """ + Connecting to Tor widget, with a progress bar + """ + + open_tor_settings = QtCore.Signal() + success = QtCore.Signal() + fail = QtCore.Signal() + + def __init__(self, common): + super(TorConnectionWidget, self).__init__(None) + self.common = common + self.common.log("TorConnectionWidget", "__init__") + + self.label = QtWidgets.QLabel(strings._("connecting_to_tor")) + self.label.setAlignment(QtCore.Qt.AlignHCenter) + + self.progress = QtWidgets.QProgressBar() + self.progress.setRange(0, 100) + self.cancel_button = QtWidgets.QPushButton( + strings._("gui_settings_button_cancel") + ) + self.cancel_button.clicked.connect(self.cancel_clicked) + + progress_layout = QtWidgets.QHBoxLayout() + progress_layout.addWidget(self.progress) + progress_layout.addWidget(self.cancel_button) + + inner_layout = QtWidgets.QVBoxLayout() + inner_layout.addWidget(self.label) + inner_layout.addLayout(progress_layout) + + layout = QtWidgets.QHBoxLayout() + layout.addStretch() + layout.addLayout(inner_layout) + layout.addStretch() + self.setLayout(layout) + + # Start displaying the status at 0 + self._tor_status_update(0, "") + + def start(self, custom_settings=False, testing_settings=False, onion=None): + self.common.log("TorConnectionWidget", "start") + self.was_canceled = False + + self.testing_settings = testing_settings + + if custom_settings: + self.settings = custom_settings + else: + self.settings = self.common.settings + + if self.testing_settings: + self.onion = onion + else: + self.onion = self.common.gui.onion + + t = TorConnectionThread(self.common, self.settings, self) + t.tor_status_update.connect(self._tor_status_update) + t.connected_to_tor.connect(self._connected_to_tor) + t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor) + t.error_connecting_to_tor.connect(self._error_connecting_to_tor) + t.start() + + # The main thread needs to remain active, and checking for Qt events, + # until the thread is finished. Otherwise it won't be able to handle + # accepting signals. + self.active = True + while self.active: + time.sleep(0.1) + self.common.gui.qtapp.processEvents() + + def cancel_clicked(self): + self.was_canceled = True + self.fail.emit() + + def wasCanceled(self): + return self.was_canceled + + def _tor_status_update(self, progress, summary): + self.progress.setValue(int(progress)) + self.label.setText( + f"{strings._('connecting_to_tor')}
{summary}" + ) + + def _connected_to_tor(self): + self.common.log("TorConnectionWidget", "_connected_to_tor") + self.active = False + + # Close the dialog after connecting + self.progress.setValue(self.progress.maximum()) + + self.success.emit() + + def _canceled_connecting_to_tor(self): + self.common.log("TorConnectionWidget", "_canceled_connecting_to_tor") + self.active = False + self.onion.cleanup() + + # Cancel connecting to Tor + QtCore.QTimer.singleShot(1, self.cancel_clicked) + + def _error_connecting_to_tor(self, msg): + self.common.log("TorConnectionWidget", "_error_connecting_to_tor") + self.active = False + + if self.testing_settings: + # If testing, just display the error but don't open settings + def alert(): + Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title) + + else: + # If not testing, open settings after displaying the error + def alert(): + Alert( + self.common, + f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}", + QtWidgets.QMessageBox.Warning, + title=self.title, + ) + + # Open settings + self.open_tor_settings.emit() + + QtCore.QTimer.singleShot(1, alert) + self.fail.emit() + + class TorConnectionThread(QtCore.QThread): tor_status_update = QtCore.Signal(str, str) connected_to_tor = QtCore.Signal() diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index e46fa729..4b84e923 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -29,7 +29,7 @@ from onionshare_cli.onion import Onion from . import strings from .widgets import Alert -from .tor_connection_dialog import TorConnectionDialog +from .tor_connection_dialog import TorConnectionDialog, TorConnectionWidget from .moat_dialog import MoatDialog from .gui_common import GuiCommon @@ -291,6 +291,7 @@ class TorSettingsTab(QtWidgets.QWidget): connection_type_radio_group_layout.addWidget( self.connection_type_socket_file_radio ) + connection_type_radio_group_layout.addStretch() connection_type_radio_group = QtWidgets.QGroupBox( strings._("gui_settings_connection_type_label") ) @@ -311,6 +312,17 @@ class TorSettingsTab(QtWidgets.QWidget): connection_type_layout = QtWidgets.QVBoxLayout() connection_type_layout.addWidget(self.tor_settings_group) connection_type_layout.addWidget(self.connection_type_bridges_radio_group) + connection_type_layout.addStretch() + + # Settings are in columns + columns_layout = QtWidgets.QHBoxLayout() + columns_layout.addWidget(connection_type_radio_group) + columns_layout.addSpacing(20) + columns_layout.addLayout(connection_type_layout, stretch=1) + + # Tor connection widget + self.tor_con = TorConnectionWidget(self.common) + self.tor_con.hide() # Buttons self.test_tor_button = QtWidgets.QPushButton( @@ -320,16 +332,19 @@ class TorSettingsTab(QtWidgets.QWidget): self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addWidget(self.test_tor_button) buttons_layout.addStretch() + buttons_layout.addWidget(self.test_tor_button) buttons_layout.addWidget(self.save_button) + buttons_layout.addStretch() # Layout layout = QtWidgets.QVBoxLayout() - layout.addWidget(connection_type_radio_group) - layout.addLayout(connection_type_layout) + layout.addStretch() + layout.addLayout(columns_layout) + layout.addWidget(self.tor_con) layout.addStretch() layout.addLayout(buttons_layout) + layout.addStretch() self.setLayout(layout) @@ -566,14 +581,18 @@ class TorSettingsTab(QtWidgets.QWidget): if not settings: return + self.test_tor_button.hide() + onion = Onion( self.common, use_tmp_dir=True, get_tor_paths=self.common.gui.get_tor_paths, ) - tor_con = TorConnectionDialog(self.common, settings, True, onion) - tor_con.start() + self.tor_con.show() + self.tor_con.success.connect(self.test_tor_button_finished) + self.tor_con.fail.connect(self.test_tor_button_finished) + self.tor_con.start(settings, True, onion) # If Tor settings worked, show results if onion.connected_to_tor: @@ -591,6 +610,13 @@ class TorSettingsTab(QtWidgets.QWidget): # Clean up onion.cleanup() + def test_tor_button_finished(self): + """ + Finished testing tor connection. + """ + self.tor_con.hide() + self.test_tor_button.show() + def save_clicked(self): """ Save button clicked. Save current settings to disk. From 556aedf08d296d7fcb83c4f1933689f250561bd2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 20 Oct 2021 21:06:38 -0700 Subject: [PATCH 39/70] Fix mixup with tab_ids and their indicies, so tabs open and close smoothly --- desktop/src/onionshare/settings_tab.py | 10 +++++--- desktop/src/onionshare/tab_widget.py | 30 +++++++++++++++++----- desktop/src/onionshare/tor_settings_tab.py | 14 +++++----- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py index 251783aa..c01d2662 100644 --- a/desktop/src/onionshare/settings_tab.py +++ b/desktop/src/onionshare/settings_tab.py @@ -37,7 +37,7 @@ class SettingsTab(QtWidgets.QWidget): Settings dialog. """ - settings_saved = QtCore.Signal() + close_this_tab = QtCore.Signal() def __init__(self, common, tab_id): super(SettingsTab, self).__init__() @@ -94,6 +94,7 @@ class SettingsTab(QtWidgets.QWidget): locale = language_names_to_locales[language_name] self.language_combobox.addItem(language_name, locale) language_layout = QtWidgets.QHBoxLayout() + language_layout.addStretch() language_layout.addWidget(language_label) language_layout.addWidget(self.language_combobox) language_layout.addStretch() @@ -108,6 +109,7 @@ class SettingsTab(QtWidgets.QWidget): ] self.theme_combobox.addItems(theme_choices) theme_layout = QtWidgets.QHBoxLayout() + theme_layout.addStretch() theme_layout.addWidget(theme_label) theme_layout.addWidget(self.theme_combobox) theme_layout.addStretch() @@ -116,7 +118,9 @@ class SettingsTab(QtWidgets.QWidget): version_label = QtWidgets.QLabel( strings._("gui_settings_version_label").format(self.common.version) ) + version_label.setAlignment(QtCore.Qt.AlignHCenter) help_label = QtWidgets.QLabel(strings._("gui_settings_help_label")) + help_label.setAlignment(QtCore.Qt.AlignHCenter) help_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) help_label.setOpenExternalLinks(True) @@ -126,6 +130,7 @@ class SettingsTab(QtWidgets.QWidget): buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addStretch() buttons_layout.addWidget(self.save_button) + buttons_layout.addStretch() # Layout layout = QtWidgets.QVBoxLayout() @@ -270,8 +275,7 @@ class SettingsTab(QtWidgets.QWidget): # Save the new settings settings.save() - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() def help_clicked(self): """ diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index fe6d08dc..36a6c22f 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -186,12 +186,13 @@ class TabWidget(QtWidgets.QTabWidget): self.common.log("TabWidget", "open_settings_tab") # See if a settings tab is already open, and if so switch to it - for index in range(self.count()): - if type(self.tabs[index]) is SettingsTab: - self.setCurrentIndex(index) + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) return settings_tab = SettingsTab(self.common, self.current_tab_id) + settings_tab.close_this_tab.connect(self.close_settings_tab) self.tabs[self.current_tab_id] = settings_tab self.current_tab_id += 1 index = self.addTab(settings_tab, strings._("gui_settings_window_title")) @@ -205,12 +206,13 @@ class TabWidget(QtWidgets.QTabWidget): self.common.log("TabWidget", "open_tor_settings_tab") # See if a settings tab is already open, and if so switch to it - for index in range(self.count()): - if type(self.tabs[index]) is TorSettingsTab: - self.setCurrentIndex(index) + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is TorSettingsTab: + self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) return tor_settings_tab = TorSettingsTab(self.common, self.current_tab_id) + tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) self.tabs[self.current_tab_id] = tor_settings_tab self.current_tab_id += 1 index = self.addTab( @@ -297,6 +299,22 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() + def close_settings_tab(self): + self.common.log("TabWidget", "close_settings_tab") + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is SettingsTab: + index = self.indexOf(self.tabs[tab_id]) + self.close_tab(index) + return + + def close_tor_settings_tab(self): + self.common.log("TabWidget", "close_tor_settings_tab") + for tab_id in self.tabs: + if type(self.tabs[tab_id]) is TorSettingsTab: + index = self.indexOf(self.tabs[tab_id]) + self.close_tab(index) + return + def are_tabs_active(self): """ See if there are active servers in any open tabs diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 4b84e923..e2fcead4 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -39,7 +39,7 @@ class TorSettingsTab(QtWidgets.QWidget): Settings dialog. """ - settings_saved = QtCore.Signal() + close_this_tab = QtCore.Signal() def __init__(self, common, tab_id): super(TorSettingsTab, self).__init__() @@ -339,7 +339,6 @@ class TorSettingsTab(QtWidgets.QWidget): # Layout layout = QtWidgets.QVBoxLayout() - layout.addStretch() layout.addLayout(columns_layout) layout.addWidget(self.tor_con) layout.addStretch() @@ -582,6 +581,7 @@ class TorSettingsTab(QtWidgets.QWidget): return self.test_tor_button.hide() + self.save_button.hide() onion = Onion( self.common, @@ -616,6 +616,7 @@ class TorSettingsTab(QtWidgets.QWidget): """ self.tor_con.hide() self.test_tor_button.show() + self.save_button.show() def save_clicked(self): """ @@ -696,15 +697,12 @@ class TorSettingsTab(QtWidgets.QWidget): self.common.gui.onion.is_authenticated() and not tor_con.wasCanceled() ): - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() else: - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() else: - self.settings_saved.emit() - self.close() + self.close_this_tab.emit() def close_tab(self): """ From dbeb0f21945b07e949ac751d2b5f6311445bb24d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 11:48:18 -0700 Subject: [PATCH 40/70] Add script to compile meek-client and copy into resources --- desktop/README.md | 10 +++++ desktop/scripts/build-meek-client.py | 62 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 desktop/scripts/build-meek-client.py diff --git a/desktop/README.md b/desktop/README.md index c8c51519..408b6852 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -63,6 +63,16 @@ Download Tor Browser and extract the binaries: python scripts\get-tor-windows.py ``` +### Compile dependencies + +Install Go. The simplest way to make sure everything works is to install Go by following [these instructions](https://golang.org/doc/install). + +Download and compile `meek-client`: + +``` +./scripts/build-meek-client.py +``` + ### Prepare the virtual environment OnionShare uses [Briefcase](https://briefcase.readthedocs.io/en/latest/). diff --git a/desktop/scripts/build-meek-client.py b/desktop/scripts/build-meek-client.py new file mode 100755 index 00000000..d043754c --- /dev/null +++ b/desktop/scripts/build-meek-client.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- 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 . +""" + +""" +This script downloads a pre-built tor binary to bundle with OnionShare. +In order to avoid a Mac gnupg dependency, I manually verify the signature +and hard-code the sha256 hash. +""" +import shutil +import os +import subprocess +import inspect + + +def main(): + if shutil.which("go") is None: + print("Install go: https://golang.org/doc/install") + return + + subprocess.run( + [ + "go", + "install", + "git.torproject.org/pluggable-transports/meek.git/meek-client@v0.37.0", + ] + ) + + root_path = os.path.dirname( + os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + ) + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + + bin_path = os.path.expanduser("~/go/bin/meek-client") + shutil.copyfile( + os.path.join(bin_path), + os.path.join(dist_path, "meek-client"), + ) + os.chmod(os.path.join(dist_path, "meek-client"), 0o755) + + print(f"Installed meek-client in {dist_path}") + + +if __name__ == "__main__": + main() From c81862130ba0969228fa61a3aaf9612ff8424971 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 25 Oct 2021 10:28:06 +1100 Subject: [PATCH 41/70] Fix comment about meek-client.exe subprocess --- cli/onionshare_cli/meek.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 4fc42756..482deedd 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -74,7 +74,7 @@ class Meek(object): # Start the Meek Client as a subprocess. if self.common.platform == "Windows": - # In Windows, hide console window when opening tor.exe subprocess + # In Windows, hide console window when opening meek-client.exe subprocess startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW self.meek_proc = subprocess.Popen( From 3a715346af241707c952ff446734f8c7bfccd21f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 25 Oct 2021 10:44:38 +1100 Subject: [PATCH 42/70] Add cleanup method for the Meek class to kill any meek-client subprocesses once done. Hide stderr from the CLI printed output --- cli/onionshare_cli/__init__.py | 6 +++-- cli/onionshare_cli/meek.py | 37 +++++++++++++++++++++++++++ desktop/src/onionshare/moat_dialog.py | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index 99992b25..4e34a508 100644 --- a/cli/onionshare_cli/__init__.py +++ b/cli/onionshare_cli/__init__.py @@ -286,8 +286,8 @@ def main(cwd=None): web = Web(common, False, mode_settings, mode) # Create the Meek object and start the meek client - meek = Meek(common) - meek.start() + # meek = Meek(common) + # meek.start() # Create the CensorshipCircumvention object to make # API calls to Tor over Meek @@ -296,6 +296,8 @@ def main(cwd=None): # domain fronting. # censorship_recommended_settings = censorship.request_settings(country="cn") # print(censorship_recommended_settings) + # Clean up the meek subprocess once we're done working with the censorship circumvention API + # meek.cleanup() # Start the Onion object try: diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 482deedd..675402d3 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import subprocess +import time from queue import Queue, Empty from threading import Thread @@ -86,6 +87,7 @@ class Meek(object): self.meek_front, ], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, startupinfo=startupinfo, bufsize=1, env=self.meek_env, @@ -101,6 +103,7 @@ class Meek(object): self.meek_front, ], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, bufsize=1, env=self.meek_env, text=True, @@ -136,6 +139,40 @@ class Meek(object): self.common.log("Meek", "start", "Could not obtain the meek port") raise MeekNotRunning() + def cleanup(self): + """ + Kill any meek subprocesses. + """ + self.common.log("Meek", "cleanup") + + if self.meek_proc: + self.meek_proc.terminate() + time.sleep(0.2) + if self.meek_proc.poll() is None: + self.common.log( + "Meek", + "cleanup", + "Tried to terminate meek-client process but it's still running", + ) + try: + self.meek_proc.kill() + time.sleep(0.2) + if self.meek_proc.poll() is None: + self.common.log( + "Meek", + "cleanup", + "Tried to kill meek-client process but it's still running", + ) + except Exception: + self.common.log( + "Meek", "cleanup", "Exception while killing meek-client process" + ) + self.meek_proc = None + + # Reset other Meek settings + self.meek_proxies = {} + self.meek_port = None + class MeekNotRunning(Exception): """ diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index af7b70b6..9046c989 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -264,6 +264,7 @@ class MoatThread(QtCore.QThread): ] }, ) + self.meek.cleanup() if r.status_code != 200: self.common.log("MoatThread", "run", f"status_code={r.status_code}") self.bridgedb_error.emit() @@ -316,6 +317,7 @@ class MoatThread(QtCore.QThread): ] }, ) + self.meek.cleanup() if r.status_code != 200: self.common.log("MoatThread", "run", f"status_code={r.status_code}") self.bridgedb_error.emit() From 6f0674afd8c6b39818dd6ddda417db69d458f68f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 25 Oct 2021 11:12:38 +1100 Subject: [PATCH 43/70] React to Meek client binary not found --- cli/onionshare_cli/meek.py | 11 +++++++++++ desktop/src/onionshare/moat_dialog.py | 22 +++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 675402d3..ff44cb13 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -72,6 +72,12 @@ class Meek(object): queue.put(line) out.close() + # Abort early if we can't find the Meek client + # common.get_tor_paths() has already checked it's a file + # so just abort if it's a NoneType object + if self.meek_client_file_path is None: + raise MeekNotFound() + # Start the Meek Client as a subprocess. if self.common.platform == "Windows": @@ -179,3 +185,8 @@ class MeekNotRunning(Exception): We were unable to start Meek or obtain the port number it started on, in order to do domain fronting. """ + +class MeekNotFound(Exception): + """ + We were unable to find the Meek Client binary. + """ diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 9046c989..2821bb1e 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -26,7 +26,7 @@ import json from . import strings from .gui_common import GuiCommon -from onionshare_cli.meek import MeekNotRunning +from onionshare_cli.meek import MeekNotFound class MoatDialog(QtWidgets.QDialog): @@ -234,12 +234,19 @@ class MoatThread(QtCore.QThread): def run(self): # Start Meek so that we can do domain fronting - self.meek.start() + try: + self.meek.start() + except MeekNotFound: + self.common.log("MoatThread", "run", f"Could not find the Meek Client") + self.bridgedb_error.emit() + return # We should only fetch bridges if we can domain front, # but we can override this in local-only mode. if not self.meek.meek_proxies and not self.common.gui.local_only: - self.common.log("MoatThread", "run", f"Could not identify meek proxies to make request") + self.common.log( + "MoatThread", "run", f"Could not identify meek proxies to make request" + ) self.bridgedb_error.emit() return @@ -256,15 +263,14 @@ class MoatThread(QtCore.QThread): { "version": "0.1.0", "type": "client-transports", - "supported": [ - "obfs4", - "snowflake", - ], + "supported": ["obfs4", "snowflake"], } ] }, ) + self.meek.cleanup() + if r.status_code != 200: self.common.log("MoatThread", "run", f"status_code={r.status_code}") self.bridgedb_error.emit() @@ -317,7 +323,9 @@ class MoatThread(QtCore.QThread): ] }, ) + self.meek.cleanup() + if r.status_code != 200: self.common.log("MoatThread", "run", f"status_code={r.status_code}") self.bridgedb_error.emit() From c3eeaefb9f1f67afc81c75068e6e7216d9fe8bfb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 17:35:24 -0700 Subject: [PATCH 44/70] In CLI get_tor_path, stop trying to look in resources first --- cli/onionshare_cli/common.py | 58 ++++++++++-------------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index 945a75bb..07e0aa0a 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -309,30 +309,14 @@ class Common: def get_tor_paths(self): if self.platform == "Linux": - # Look in resources first - base_path = self.get_resource_path("tor") - if os.path.exists(base_path): - self.log( - "Common", "get_tor_paths", f"using tor binaries in {base_path}" - ) - tor_path = os.path.join(base_path, "tor") - tor_geo_ip_file_path = os.path.join(base_path, "geoip") - 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") - else: - # Fallback to looking in the path - self.log( - "Common", "get_tor_paths", f"using tor binaries in system path" - ) - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - snowflake_file_path = shutil.which("snowflake-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") + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-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") elif self.platform == "Windows": base_path = self.get_resource_path("tor") tor_path = os.path.join(base_path, "Tor", "tor.exe") @@ -341,24 +325,14 @@ class Common: 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": - # Look in resources first - base_path = self.get_resource_path("tor") - if os.path.exists(base_path): - tor_path = os.path.join(base_path, "tor") - tor_geo_ip_file_path = os.path.join(base_path, "geoip") - 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") - else: - # Fallback to looking in the path - tor_path = shutil.which("tor") - if not tor_path: - raise CannotFindTor() - obfs4proxy_file_path = shutil.which("obfs4proxy") - snowflake_file_path = shutil.which("snowflake-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") + tor_path = shutil.which("tor") + if not tor_path: + raise CannotFindTor() + obfs4proxy_file_path = shutil.which("obfs4proxy") + snowflake_file_path = shutil.which("snowflake-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") elif self.platform == "BSD": tor_path = "/usr/local/bin/tor" tor_geo_ip_file_path = "/usr/local/share/tor/geoip" From 8543d215dcdc92fb621a55299a04d8fcef738223 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 25 Oct 2021 11:45:50 +1100 Subject: [PATCH 45/70] Fix-ups for detecting if the meek binary doesn't exist. Pass the GUI's get_tor_paths down to the CLI when instantiating Meek object --- cli/onionshare_cli/meek.py | 16 +++++++++++----- desktop/src/onionshare/tor_settings_dialog.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index ff44cb13..762a454c 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -17,6 +17,7 @@ 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 os import subprocess import time from queue import Queue, Empty @@ -31,7 +32,7 @@ class Meek(object): bridges, before connecting to Tor. """ - def __init__(self, common): + def __init__(self, common, get_tor_paths=None): """ Set up the Meek object """ @@ -39,7 +40,9 @@ class Meek(object): self.common = common self.common.log("Meek", "__init__") - get_tor_paths = self.common.get_tor_paths + # Set the path of the meek binary + if not get_tor_paths: + get_tor_paths = self.common.get_tor_paths ( self.tor_path, self.tor_geo_ip_file_path, @@ -72,10 +75,12 @@ class Meek(object): queue.put(line) out.close() + self.common.log("Meek", "start", self.meek_client_file_path) + # Abort early if we can't find the Meek client - # common.get_tor_paths() has already checked it's a file - # so just abort if it's a NoneType object - if self.meek_client_file_path is None: + if self.meek_client_file_path is None or not os.path.exists( + self.meek_client_file_path + ): raise MeekNotFound() # Start the Meek Client as a subprocess. @@ -186,6 +191,7 @@ class MeekNotRunning(Exception): number it started on, in order to do domain fronting. """ + class MeekNotFound(Exception): """ We were unable to find the Meek Client binary. diff --git a/desktop/src/onionshare/tor_settings_dialog.py b/desktop/src/onionshare/tor_settings_dialog.py index 13edd112..6737ae4b 100644 --- a/desktop/src/onionshare/tor_settings_dialog.py +++ b/desktop/src/onionshare/tor_settings_dialog.py @@ -49,7 +49,7 @@ class TorSettingsDialog(QtWidgets.QDialog): self.common.log("TorSettingsDialog", "__init__") - self.meek = Meek(common) + self.meek = Meek(common, get_tor_paths=self.common.gui.get_tor_paths) self.setModal(True) self.setWindowTitle(strings._("gui_tor_settings_window_title")) From 54bfca5f4bfe50587e356f26f48a44cb203fc730 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 25 Oct 2021 11:56:33 +1100 Subject: [PATCH 46/70] Move debug log call in meek.start() --- cli/onionshare_cli/meek.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 762a454c..6b31a584 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -75,8 +75,6 @@ class Meek(object): queue.put(line) out.close() - self.common.log("Meek", "start", self.meek_client_file_path) - # Abort early if we can't find the Meek client if self.meek_client_file_path is None or not os.path.exists( self.meek_client_file_path @@ -84,6 +82,7 @@ class Meek(object): raise MeekNotFound() # Start the Meek Client as a subprocess. + self.common.log("Meek", "start", "Starting meek client") if self.common.platform == "Windows": # In Windows, hide console window when opening meek-client.exe subprocess From 1420b28d2332cb1f16a7f0ca5ae11c167e4a1eb0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 18:55:25 -0700 Subject: [PATCH 47/70] Saving tor settings connects to tor in the widget, not the dialog. And erros are displayed in a label, not an alert --- desktop/src/onionshare/gui_common.py | 6 +- desktop/src/onionshare/moat_dialog.py | 2 +- .../src/onionshare/resources/locale/en.json | 2 +- desktop/src/onionshare/settings_tab.py | 4 - .../src/onionshare/tor_connection_dialog.py | 26 +--- desktop/src/onionshare/tor_settings_tab.py | 130 +++++++++++------- 6 files changed, 88 insertions(+), 82 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 0f1dd46e..f2fd6ef0 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -392,10 +392,10 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", - # Moat dialog - "moat_error": """ + # Tor Settings dialogs + "tor_settings_error": """ QLabel { - color: #990000; + color: #FF0000; } """, } diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 28193c25..56e872b5 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -67,7 +67,7 @@ class MoatDialog(QtWidgets.QDialog): # Error label self.error_label = QtWidgets.QLabel() - self.error_label.setStyleSheet(self.common.gui.css["moat_error"]) + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) self.error_label.hide() # Buttons diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index a9fb562a..3f380466 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -71,7 +71,7 @@ "gui_settings_bridge_custom_radio_option": "Provide a bridge you learned about from a trusted source", "gui_settings_bridge_custom_placeholder": "type address:port (one per line)", "gui_settings_moat_bridges_invalid": "You have not requested a bridge from torproject.org yet.", - "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", + "gui_settings_tor_bridges_invalid": "None of the bridges you added work. Double-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py index c01d2662..c792d94e 100644 --- a/desktop/src/onionshare/settings_tab.py +++ b/desktop/src/onionshare/settings_tab.py @@ -19,17 +19,13 @@ along with this program. If not, see . """ from PySide2 import QtCore, QtWidgets, QtGui -import sys import platform import datetime -import re -import os from onionshare_cli.settings import Settings from . import strings from .widgets import Alert from .update_checker import UpdateThread -from .gui_common import GuiCommon class SettingsTab(QtWidgets.QWidget): diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection_dialog.py index 7ba7c800..3eb38876 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection_dialog.py @@ -164,7 +164,7 @@ class TorConnectionWidget(QtWidgets.QWidget): open_tor_settings = QtCore.Signal() success = QtCore.Signal() - fail = QtCore.Signal() + fail = QtCore.Signal(str) def __init__(self, common): super(TorConnectionWidget, self).__init__(None) @@ -231,7 +231,7 @@ class TorConnectionWidget(QtWidgets.QWidget): def cancel_clicked(self): self.was_canceled = True - self.fail.emit() + self.fail.emit("") def wasCanceled(self): return self.was_canceled @@ -262,27 +262,7 @@ class TorConnectionWidget(QtWidgets.QWidget): def _error_connecting_to_tor(self, msg): self.common.log("TorConnectionWidget", "_error_connecting_to_tor") self.active = False - - if self.testing_settings: - # If testing, just display the error but don't open settings - def alert(): - Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title) - - else: - # If not testing, open settings after displaying the error - def alert(): - Alert( - self.common, - f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}", - QtWidgets.QMessageBox.Warning, - title=self.title, - ) - - # Open settings - self.open_tor_settings.emit() - - QtCore.QTimer.singleShot(1, alert) - self.fail.emit() + self.fail.emit(msg) class TorConnectionThread(QtCore.QThread): diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index e2fcead4..be9dac37 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -29,9 +29,8 @@ from onionshare_cli.onion import Onion from . import strings from .widgets import Alert -from .tor_connection_dialog import TorConnectionDialog, TorConnectionWidget +from .tor_connection_dialog import TorConnectionWidget from .moat_dialog import MoatDialog -from .gui_common import GuiCommon class TorSettingsTab(QtWidgets.QWidget): @@ -319,10 +318,21 @@ class TorSettingsTab(QtWidgets.QWidget): columns_layout.addWidget(connection_type_radio_group) columns_layout.addSpacing(20) columns_layout.addLayout(connection_type_layout, stretch=1) + columns_wrapper = QtWidgets.QWidget() + columns_wrapper.setFixedHeight(400) + columns_wrapper.setLayout(columns_layout) # Tor connection widget self.tor_con = TorConnectionWidget(self.common) + self.tor_con.success.connect(self.tor_con_success) + self.tor_con.fail.connect(self.tor_con_fail) self.tor_con.hide() + self.tor_con_type = None + + # Error label + self.error_label = QtWidgets.QLabel() + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) + self.error_label.setWordWrap(True) # Buttons self.test_tor_button = QtWidgets.QPushButton( @@ -332,18 +342,18 @@ class TorSettingsTab(QtWidgets.QWidget): self.save_button = QtWidgets.QPushButton(strings._("gui_settings_button_save")) self.save_button.clicked.connect(self.save_clicked) buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addStretch() + buttons_layout.addWidget(self.error_label, stretch=1) + buttons_layout.addSpacing(20) buttons_layout.addWidget(self.test_tor_button) buttons_layout.addWidget(self.save_button) - buttons_layout.addStretch() # Layout layout = QtWidgets.QVBoxLayout() - layout.addLayout(columns_layout) + layout.addWidget(columns_wrapper) + layout.addStretch() layout.addWidget(self.tor_con) layout.addStretch() layout.addLayout(buttons_layout) - layout.addStretch() self.setLayout(layout) @@ -576,6 +586,9 @@ class TorSettingsTab(QtWidgets.QWidget): successfully connect and authenticate to Tor. """ self.common.log("TorSettingsTab", "test_tor_clicked") + + self.error_label.setText("") + settings = self.settings_from_fields() if not settings: return @@ -583,40 +596,15 @@ class TorSettingsTab(QtWidgets.QWidget): self.test_tor_button.hide() self.save_button.hide() - onion = Onion( + self.test_onion = Onion( self.common, use_tmp_dir=True, get_tor_paths=self.common.gui.get_tor_paths, ) + self.tor_con_type = "test" self.tor_con.show() - self.tor_con.success.connect(self.test_tor_button_finished) - self.tor_con.fail.connect(self.test_tor_button_finished) - self.tor_con.start(settings, True, onion) - - # If Tor settings worked, show results - if onion.connected_to_tor: - Alert( - self.common, - strings._("settings_test_success").format( - onion.tor_version, - onion.supports_ephemeral, - onion.supports_stealth, - onion.supports_v3_onions, - ), - title=strings._("gui_settings_connection_type_test_button"), - ) - - # Clean up - onion.cleanup() - - def test_tor_button_finished(self): - """ - Finished testing tor connection. - """ - self.tor_con.hide() - self.test_tor_button.show() - self.save_button.show() + self.tor_con.start(settings, True, self.test_onion) def save_clicked(self): """ @@ -624,6 +612,8 @@ class TorSettingsTab(QtWidgets.QWidget): """ self.common.log("TorSettingsTab", "save_clicked") + self.error_label.setText("") + def changed(s1, s2, keys): """ Compare the Settings objects s1 and s2 and return true if any values @@ -684,26 +674,62 @@ class TorSettingsTab(QtWidgets.QWidget): ) self.common.gui.onion.cleanup() - tor_con = TorConnectionDialog(self.common, settings) - tor_con.start() - - self.common.log( - "TorSettingsTab", - "save_clicked", - f"Onion done rebooting, connected to Tor: {self.common.gui.onion.connected_to_tor}", - ) - - if ( - self.common.gui.onion.is_authenticated() - and not tor_con.wasCanceled() - ): - self.close_this_tab.emit() + self.test_tor_button.hide() + self.save_button.hide() + self.tor_con_type = "save" + self.tor_con.show() + self.tor_con.start(settings) else: self.close_this_tab.emit() else: self.close_this_tab.emit() + def tor_con_success(self): + """ + Finished testing tor connection. + """ + self.tor_con.hide() + self.test_tor_button.show() + self.save_button.show() + + if self.tor_con_type == "test": + Alert( + self.common, + strings._("settings_test_success").format( + self.test_onion.tor_version, + self.test_onion.supports_ephemeral, + self.test_onion.supports_stealth, + self.test_onion.supports_v3_onions, + ), + title=strings._("gui_settings_connection_type_test_button"), + ) + self.test_onion.cleanup() + + elif self.tor_con_type == "save": + if ( + self.common.gui.onion.is_authenticated() + and not self.tor_con.wasCanceled() + ): + self.close_this_tab.emit() + + self.tor_con_type = None + + def tor_con_fail(self, msg): + """ + Finished testing tor connection. + """ + self.tor_con.hide() + self.test_tor_button.show() + self.save_button.show() + + self.error_label.setText(msg) + + if self.tor_con_type == "test": + self.test_onion.cleanup() + + self.tor_con_type = None + def close_tab(self): """ Tab is closed @@ -796,7 +822,9 @@ class TorSettingsTab(QtWidgets.QWidget): moat_bridges = self.bridge_moat_textbox.toPlainText() if moat_bridges.strip() == "": - Alert(self.common, strings._("gui_settings_moat_bridges_invalid")) + self.error_label.setText( + strings._("gui_settings_moat_bridges_invalid") + ) return False settings.set( @@ -843,7 +871,9 @@ class TorSettingsTab(QtWidgets.QWidget): new_bridges = "\n".join(new_bridges) + "\n" settings.set("tor_bridges_use_custom_bridges", new_bridges) else: - Alert(self.common, strings._("gui_settings_tor_bridges_invalid")) + self.error_label.setText( + strings._("gui_settings_tor_bridges_invalid") + ) return False else: settings.set("no_bridges", True) From 4897015ad7f6b98bcf27a50d20ebe5de339b6924 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 18:57:14 -0700 Subject: [PATCH 48/70] Rename tor_connection_dialog.py to tor_connection.py --- desktop/src/onionshare/main_window.py | 2 +- .../onionshare/{tor_connection_dialog.py => tor_connection.py} | 0 desktop/src/onionshare/tor_settings_tab.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename desktop/src/onionshare/{tor_connection_dialog.py => tor_connection.py} (100%) diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index 4a9d0c7e..546592a1 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -23,7 +23,7 @@ import time from PySide2 import QtCore, QtWidgets, QtGui from . import strings -from .tor_connection_dialog import TorConnectionDialog +from .tor_connection import TorConnectionDialog from .widgets import Alert from .update_checker import UpdateThread from .tab_widget import TabWidget diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection.py similarity index 100% rename from desktop/src/onionshare/tor_connection_dialog.py rename to desktop/src/onionshare/tor_connection.py diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index be9dac37..5905b44d 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -29,7 +29,7 @@ from onionshare_cli.onion import Onion from . import strings from .widgets import Alert -from .tor_connection_dialog import TorConnectionWidget +from .tor_connection import TorConnectionWidget from .moat_dialog import MoatDialog From 20a0d7f25bb066a7a67f47b11de69bb98c50ab63 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 19:31:53 -0700 Subject: [PATCH 49/70] Fix TabWidget to stop confusing tab_id and index --- desktop/src/onionshare/tab_widget.py | 69 +++++++++++++++------- desktop/src/onionshare/tor_settings_tab.py | 22 ------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index 36a6c22f..0ab19279 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -45,7 +45,9 @@ class TabWidget(QtWidgets.QTabWidget): self.system_tray = system_tray self.status_bar = status_bar - # Keep track of tabs in a dictionary + # Keep track of tabs in a dictionary that maps tab_id to tab. + # Each tab has a unique, auto-incremented id (tab_id). This is different than the + # tab's index, which changes as tabs are re-arranged. self.tabs = {} self.current_tab_id = 0 # Each tab has a unique id @@ -91,10 +93,12 @@ class TabWidget(QtWidgets.QTabWidget): self.event_handler_t.wait(50) # Clean up each tab - for index in range(self.count()): - if not self.is_settings_tab(index): - tab = self.widget(index) - tab.cleanup() + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + self.tabs[tab_id].cleanup() def move_new_tab_button(self): # Find the width of all tabs @@ -117,11 +121,26 @@ class TabWidget(QtWidgets.QTabWidget): def tab_changed(self): # Active tab was changed - tab_id = self.currentIndex() + tab = self.widget(self.currentIndex()) + if not tab: + self.common.log( + "TabWidget", + "tab_changed", + f"tab at index {self.currentIndex()} does not exist", + ) + return + + tab_id = tab.tab_id self.common.log("TabWidget", "tab_changed", f"Tab was changed to {tab_id}") # If it's Settings or Tor Settings, ignore - if self.is_settings_tab(tab_id): + if ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + # Blank the server status indicator + self.status_bar.server_status_image_label.clear() + self.status_bar.server_status_label.clear() return try: @@ -260,9 +279,12 @@ class TabWidget(QtWidgets.QTabWidget): def save_persistent_tabs(self): # Figure out the order of persistent tabs to save in settings persistent_tabs = [] - for index in range(self.count()): - if not self.is_settings_tab(index): - tab = self.widget(index) + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + tab = self.widget(self.indexOf(self.tabs[tab_id])) if tab.settings.get("persistent", "enabled"): persistent_tabs.append(tab.settings.id) # Only save if tabs have actually moved @@ -273,8 +295,14 @@ class TabWidget(QtWidgets.QTabWidget): def close_tab(self, index): self.common.log("TabWidget", "close_tab", f"{index}") tab = self.widget(index) + tab_id = tab.tab_id + + if ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + self.common.log("TabWidget", "closing a settings tab") - if self.is_settings_tab(index): # Remove the tab self.removeTab(index) del self.tabs[tab.tab_id] @@ -284,7 +312,10 @@ class TabWidget(QtWidgets.QTabWidget): self.new_tab_clicked() else: + self.common.log("TabWidget", "closing a service tab") if tab.close_tab(): + self.common.log("TabWidget", "user is okay with closing the tab") + # If the tab is persistent, delete the settings file from disk if tab.settings.get("persistent", "enabled"): tab.settings.delete() @@ -298,6 +329,8 @@ class TabWidget(QtWidgets.QTabWidget): # If the last tab is closed, open a new one if self.count() == 0: self.new_tab_clicked() + else: + self.common.log("TabWidget", "user does not want to close the tab") def close_settings_tab(self): self.common.log("TabWidget", "close_settings_tab") @@ -320,7 +353,10 @@ class TabWidget(QtWidgets.QTabWidget): See if there are active servers in any open tabs """ for tab_id in self.tabs: - if not self.is_settings_tab(tab_id): + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): mode = self.tabs[tab_id].get_mode() if mode: if mode.server_status.status != mode.server_status.STATUS_STOPPED: @@ -352,15 +388,6 @@ class TabWidget(QtWidgets.QTabWidget): close_button.clicked.connect(close_tab) self.tabBar().setTabButton(index, QtWidgets.QTabBar.RightSide, tab.close_button) - def is_settings_tab(self, tab_id): - if tab_id not in self.tabs: - return True - - return ( - type(self.tabs[tab_id]) is SettingsTab - or type(self.tabs[tab_id]) is TorSettingsTab - ) - class TabBar(QtWidgets.QTabBar): """ diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 5905b44d..21941268 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -730,28 +730,6 @@ class TorSettingsTab(QtWidgets.QWidget): self.tor_con_type = None - def close_tab(self): - """ - Tab is closed - """ - self.common.log("TorSettingsTab", "cancel_clicked") - return True - - # TODO: Figure out flow for first connecting, when closing settings when not connected - - # if ( - # not self.common.gui.local_only - # and not self.common.gui.onion.is_authenticated() - # ): - # Alert( - # self.common, - # strings._("gui_tor_connection_canceled"), - # QtWidgets.QMessageBox.Warning, - # ) - # sys.exit() - # else: - # self.close() - def settings_from_fields(self): """ Return a Settings object that's full of values from the settings dialog. From f784870c76bb89a41529c5e5282cc54e47148d9e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 19:53:37 -0700 Subject: [PATCH 50/70] Implement blank settings_have_changed in SettingsTab and TorSettingsTab --- desktop/src/onionshare/settings_tab.py | 4 ++++ desktop/src/onionshare/tor_connection.py | 10 +++++----- desktop/src/onionshare/tor_settings_tab.py | 4 ++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py index c792d94e..8f41b2aa 100644 --- a/desktop/src/onionshare/settings_tab.py +++ b/desktop/src/onionshare/settings_tab.py @@ -304,6 +304,10 @@ class SettingsTab(QtWidgets.QWidget): return settings + def settings_have_changed(self): + # Global settings have changed + self.common.log("SettingsTab", "settings_have_changed") + def _update_autoupdate_timestamp(self, autoupdate_timestamp): self.common.log("SettingsTab", "_update_autoupdate_timestamp") diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py index 3eb38876..1cfed2a8 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -271,20 +271,20 @@ class TorConnectionThread(QtCore.QThread): canceled_connecting_to_tor = QtCore.Signal() error_connecting_to_tor = QtCore.Signal(str) - def __init__(self, common, settings, dialog): + def __init__(self, common, settings, parent): super(TorConnectionThread, self).__init__() self.common = common self.common.log("TorConnectionThread", "__init__") self.settings = settings - self.dialog = dialog + self.parent = parent def run(self): self.common.log("TorConnectionThread", "run") # Connect to the Onion try: - self.dialog.onion.connect(self.settings, False, self._tor_status_update) - if self.dialog.onion.connected_to_tor: + self.parent.onion.connect(self.settings, False, self._tor_status_update) + if self.parent.onion.connected_to_tor: self.connected_to_tor.emit() else: self.canceled_connecting_to_tor.emit() @@ -320,4 +320,4 @@ class TorConnectionThread(QtCore.QThread): self.tor_status_update.emit(progress, summary) # Return False if the dialog was canceled - return not self.dialog.wasCanceled() + return not self.parent.wasCanceled() diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 21941268..df7cf3be 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -872,3 +872,7 @@ class TorSettingsTab(QtWidgets.QWidget): # Wait 1ms for the event loop to finish, then quit QtCore.QTimer.singleShot(1, self.common.gui.qtapp.quit) + + def settings_have_changed(self): + # Global settings have changed + self.common.log("TorSettingsTab", "settings_have_changed") From e6c7cc989f78a8de531a3cc7420eb9193abd9a06 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 20:03:19 -0700 Subject: [PATCH 51/70] Only show bridge error if connection type is bundled --- cli/onionshare_cli/onion.py | 8 ++++++-- desktop/src/onionshare/tor_settings_tab.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 0d205b97..aa2344db 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -199,8 +199,6 @@ class Onion(object): ) return - self.common.log("Onion", "connect") - # Either use settings that are passed in, or use them from common if custom_settings: self.settings = custom_settings @@ -211,6 +209,12 @@ class Onion(object): self.common.load_settings() self.settings = self.common.settings + self.common.log( + "Onion", + "connect", + f"connection_type={self.settings.get('connection_type')}", + ) + # The Tor controller self.c = None diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index df7cf3be..a56d360b 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -799,7 +799,10 @@ class TorSettingsTab(QtWidgets.QWidget): settings.set("tor_bridges_use_moat", True) moat_bridges = self.bridge_moat_textbox.toPlainText() - if moat_bridges.strip() == "": + if ( + self.connection_type_bundled_radio.isChecked() + and moat_bridges.strip() == "" + ): self.error_label.setText( strings._("gui_settings_moat_bridges_invalid") ) From 44f4053603eef5ea7dedd000416256cbdf6350c9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 20:23:55 -0700 Subject: [PATCH 52/70] Make meek debug log show host:port on one line --- cli/onionshare_cli/meek.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index 6b31a584..b2e70dc1 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -136,8 +136,11 @@ class Meek(object): 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}") + self.common.log( + "Meek", + "start", + f"Meek running on {self.meek_host}:{self.meek_port}", + ) break if self.meek_port: From 54f4f2a53fe541f2a17dd453051d32dc0e43a75b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 24 Oct 2021 20:26:36 -0700 Subject: [PATCH 53/70] Oops, fix meek-client path --- cli/onionshare_cli/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/onionshare_cli/common.py b/cli/onionshare_cli/common.py index b76e72b2..bab3fd86 100644 --- a/cli/onionshare_cli/common.py +++ b/cli/onionshare_cli/common.py @@ -315,7 +315,7 @@ class Common: raise CannotFindTor() obfs4proxy_file_path = shutil.which("obfs4proxy") snowflake_file_path = shutil.which("snowflake-client") - meek_client_file_path = os.path.join(base_path, "meek-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") From 2b3b6d7635b935741646281c7a206a71993c0f9e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 21:06:05 -0700 Subject: [PATCH 54/70] Update bridge related settings in Settings, and use those new settings in Onion --- cli/onionshare_cli/onion.py | 63 +++++++++++++++++----------------- cli/onionshare_cli/settings.py | 12 +++---- cli/tests/test_cli_settings.py | 21 ++++++------ 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index e8fcc12a..5ce83261 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -319,40 +319,39 @@ class Onion(object): f.write(torrc_template) # Bridge support - if self.settings.get("tor_bridges_use_obfs4"): - with open( - self.common.get_resource_path("torrc_template-obfs4") - ) as o: - for line in o: - f.write(line) - elif self.settings.get("tor_bridges_use_meek_lite_azure"): - with open( - self.common.get_resource_path("torrc_template-meek_lite_azure") - ) as o: - for line in o: - f.write(line) - elif self.settings.get("tor_bridges_use_snowflake"): - with open( - self.common.get_resource_path("torrc_template-snowflake") - ) as o: - for line in o: - f.write(line) + if self.settings.get("bridges_enabled"): + if self.settings.get("bridges_type") == "built-in": + if self.settings.get("bridges_builtin_pt") == "obfs4": + with open( + self.common.get_resource_path("torrc_template-obfs4") + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "meek-azure": + with open( + self.common.get_resource_path( + "torrc_template-meek_lite_azure" + ) + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "snowflake": + with open( + self.common.get_resource_path( + "torrc_template-snowflake" + ) + ) as o: + f.write(o.read()) - elif self.settings.get("tor_bridges_use_moat"): - for line in self.settings.get("tor_bridges_use_moat_bridges").split( - "\n" - ): - if line.strip() != "": - f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") + elif self.settings.get("bridges_type") == "moat": + for line in self.settings.get("bridges_moat").split("\n"): + if line.strip() != "": + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") - elif self.settings.get("tor_bridges_use_custom_bridges"): - for line in self.settings.get( - "tor_bridges_use_custom_bridges" - ).split("\n"): - if line.strip() != "": - f.write(f"Bridge {line}\n") - f.write("\nUseBridges 1\n") + elif self.settings.get("bridges_type") == "custom": + for line in self.settings.get("bridges_custom").split("\n"): + if line.strip() != "": + f.write(f"Bridge {line}\n") + f.write("\nUseBridges 1\n") # Execute a tor subprocess start_ts = time.time() diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index 29b59c80..c7d74a70 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -105,13 +105,11 @@ class Settings(object): "auth_password": "", "use_autoupdate": True, "autoupdate_timestamp": None, - "no_bridges": True, - "tor_bridges_use_obfs4": False, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_snowflake": False, - "tor_bridges_use_moat": False, - "tor_bridges_use_moat_bridges": "", - "tor_bridges_use_custom_bridges": "", + "bridges_enabled": False, + "bridges_type": "built-in", # "built-in", "moat", or "custom" + "bridges_builtin_pt": "obfs4", # "obfs4", "meek-azure", or "snowflake" + "bridges_moat": "", + "bridges_custom": "", "persistent_tabs": [], "locale": None, # this gets defined in fill_in_defaults() "theme": 0, diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index b44ddbec..c7140e70 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -29,13 +29,11 @@ class TestSettings: "auth_password": "", "use_autoupdate": True, "autoupdate_timestamp": None, - "no_bridges": True, - "tor_bridges_use_obfs4": False, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_snowflake": False, - "tor_bridges_use_moat": False, - "tor_bridges_use_moat_bridges": "", - "tor_bridges_use_custom_bridges": "", + "bridges_enabled": False, + "bridges_type": "built-in", + "bridges_builtin_pt": "obfs4", + "bridges_moat": "", + "bridges_custom": "", "persistent_tabs": [], "theme": 0, } @@ -96,10 +94,11 @@ class TestSettings: assert settings_obj.get("use_autoupdate") is True assert settings_obj.get("autoupdate_timestamp") is None assert settings_obj.get("autoupdate_timestamp") is None - assert settings_obj.get("no_bridges") is True - assert settings_obj.get("tor_bridges_use_obfs4") is False - assert settings_obj.get("tor_bridges_use_meek_lite_azure") is False - assert settings_obj.get("tor_bridges_use_custom_bridges") == "" + assert settings_obj.get("bridges_enabled") is False + assert settings_obj.get("bridges_type") == "built-in" + assert settings_obj.get("bridges_builtin_pt") == "obfs4" + assert settings_obj.get("bridges_moat") == "" + assert settings_obj.get("bridges_custom") == "" def test_set_version(self, settings_obj): settings_obj.set("version", "CUSTOM_VERSION") From 9515fe6aaf387868c1406c972435591edb70f785 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 21:07:38 -0700 Subject: [PATCH 55/70] Remove all references to old settings --- cli/onionshare_cli/onion.py | 6 +----- cli/tests/test_cli_settings.py | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 5ce83261..536b9ba0 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -424,11 +424,7 @@ class Onion(object): time.sleep(0.2) # If using bridges, it might take a bit longer to connect to Tor - if ( - self.settings.get("tor_bridges_use_custom_bridges") - or self.settings.get("tor_bridges_use_obfs4") - or self.settings.get("tor_bridges_use_meek_lite_azure") - ): + if self.settings.get("bridges_enabled"): # Only override timeout if a custom timeout has not been passed in if connect_timeout == 120: connect_timeout = 150 diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index c7140e70..9513b013 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -141,10 +141,10 @@ class TestSettings: def test_set_custom_bridge(self, settings_obj): settings_obj.set( - "tor_bridges_use_custom_bridges", + "bridges_custom", "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E", ) assert ( - settings_obj._settings["tor_bridges_use_custom_bridges"] + settings_obj._settings["bridges_custom"] == "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E" ) From 53c192665bfcc4942d42cfdd3b356524943950a9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 21:33:58 -0700 Subject: [PATCH 56/70] Refactor Tor Settings tab to use the new settings --- desktop/src/onionshare/tor_settings_tab.py | 119 +++++++++------------ 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 4f73ca66..e3dc9bee 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -405,57 +405,54 @@ class TorSettingsTab(QtWidgets.QWidget): self.old_settings.get("auth_password") ) - if self.old_settings.get("no_bridges"): - self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.bridge_settings.hide() - - else: + if self.old_settings.get("bridges_enabled"): self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) self.bridge_settings.show() - builtin_obfs4 = self.old_settings.get("tor_bridges_use_obfs4") - builtin_meek_azure = self.old_settings.get( - "tor_bridges_use_meek_lite_azure" - ) - builtin_snowflake = self.old_settings.get("tor_bridges_use_snowflake") - - if builtin_obfs4 or builtin_meek_azure or builtin_snowflake: + bridges_type = self.old_settings.get("bridges_type") + if bridges_type == "built-in": self.bridge_builtin_radio.setChecked(True) self.bridge_builtin_dropdown.show() - if builtin_obfs4: + self.bridge_moat_radio.setChecked(False) + self.bridge_moat_textbox_options.hide() + self.bridge_custom_radio.setChecked(False) + self.bridge_custom_textbox_options.hide() + + bridges_builtin_pt = self.old_settings.get("bridges_builtin_pt") + if bridges_builtin_pt == "obfs4": self.bridge_builtin_dropdown.setCurrentText("obfs4") - elif builtin_meek_azure: + elif bridges_builtin_pt == "meek-azure": self.bridge_builtin_dropdown.setCurrentText("meek-azure") - elif builtin_snowflake: + else: self.bridge_builtin_dropdown.setCurrentText("snowflake") self.bridge_moat_textbox_options.hide() self.bridge_custom_textbox_options.hide() + + elif bridges_type == "moat": + self.bridge_builtin_radio.setChecked(False) + self.bridge_builtin_dropdown.hide() + self.bridge_moat_radio.setChecked(True) + self.bridge_moat_textbox_options.show() + self.bridge_custom_radio.setChecked(False) + self.bridge_custom_textbox_options.hide() + else: self.bridge_builtin_radio.setChecked(False) self.bridge_builtin_dropdown.hide() + self.bridge_moat_radio.setChecked(False) + self.bridge_moat_textbox_options.hide() + self.bridge_custom_radio.setChecked(True) + self.bridge_custom_textbox_options.show() - use_moat = self.old_settings.get("tor_bridges_use_moat") - self.bridge_moat_radio.setChecked(use_moat) - if use_moat: - self.bridge_builtin_dropdown.hide() - self.bridge_custom_textbox_options.hide() + bridges_moat = self.old_settings.get("bridges_moat") + self.bridge_moat_textbox.document().setPlainText(bridges_moat) + bridges_custom = self.old_settings.get("bridges_custom") + self.bridge_custom_textbox.document().setPlainText(bridges_custom) - moat_bridges = self.old_settings.get("tor_bridges_use_moat_bridges") - self.bridge_moat_textbox.document().setPlainText(moat_bridges) - if len(moat_bridges.strip()) > 0: - self.bridge_moat_textbox_options.show() - else: - self.bridge_moat_textbox_options.hide() - - custom_bridges = self.old_settings.get("tor_bridges_use_custom_bridges") - if len(custom_bridges.strip()) != 0: - self.bridge_custom_radio.setChecked(True) - self.bridge_custom_textbox.setPlainText(custom_bridges) - - self.bridge_builtin_dropdown.hide() - self.bridge_moat_textbox_options.hide() - self.bridge_custom_textbox_options.show() + else: + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.bridge_settings.hide() def connection_type_bundled_toggled(self, checked): """ @@ -493,7 +490,7 @@ class TorSettingsTab(QtWidgets.QWidget): """ if selection == "meek-azure": # Alert the user about meek's costliness if it looks like they're turning it on - if not self.old_settings.get("tor_bridges_use_meek_lite_azure"): + if not self.old_settings.get("bridges_builtin_pt") == "meek-azure": Alert( self.common, strings._("gui_settings_meek_lite_expensive_warning"), @@ -654,10 +651,11 @@ class TorSettingsTab(QtWidgets.QWidget): "socket_file_path", "auth_type", "auth_password", - "no_bridges", - "tor_bridges_use_obfs4", - "tor_bridges_use_meek_lite_azure", - "tor_bridges_use_custom_bridges", + "bridges_enabled", + "bridges_type", + "bridges_builtin_pt", + "bridges_moat", + "bridges_custom", ], ): @@ -775,33 +773,16 @@ class TorSettingsTab(QtWidgets.QWidget): # Whether we use bridges if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked: - settings.set("no_bridges", False) + settings.set("bridges_enabled", True) if self.bridge_builtin_radio.isChecked(): - selection = self.bridge_builtin_dropdown.currentText() - if selection == "obfs4": - settings.set("tor_bridges_use_obfs4", True) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - elif selection == "meek-azure": - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", True) - settings.set("tor_bridges_use_snowflake", False) - elif selection == "snowflake": - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", True) + settings.set("bridges_type", "built-in") - settings.set("tor_bridges_use_moat", False) - settings.set("tor_bridges_use_custom_bridges", "") + selection = self.bridge_builtin_dropdown.currentText() + settings.set("bridges_builtin_pt", selection) if self.bridge_moat_radio.isChecked(): - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - - settings.set("tor_bridges_use_moat", True) - + settings.set("bridges_type", "moat") moat_bridges = self.bridge_moat_textbox.toPlainText() if ( self.connection_type_bundled_radio.isChecked() @@ -812,15 +793,11 @@ class TorSettingsTab(QtWidgets.QWidget): ) return False - settings.set("tor_bridges_use_moat_bridges", moat_bridges) - - settings.set("tor_bridges_use_custom_bridges", "") + settings.set("bridges_moat", moat_bridges) + settings.set("bridges_custom", "") if self.bridge_custom_radio.isChecked(): - settings.set("tor_bridges_use_obfs4", False) - settings.set("tor_bridges_use_meek_lite_azure", False) - settings.set("tor_bridges_use_snowflake", False) - settings.set("tor_bridges_use_moat", False) + settings.set("bridges_type", "custom") new_bridges = [] bridges = self.bridge_custom_textbox.toPlainText().split("\n") @@ -851,14 +828,14 @@ class TorSettingsTab(QtWidgets.QWidget): if bridges_valid: new_bridges = "\n".join(new_bridges) + "\n" - settings.set("tor_bridges_use_custom_bridges", new_bridges) + settings.set("bridges_custom", new_bridges) else: self.error_label.setText( strings._("gui_settings_tor_bridges_invalid") ) return False else: - settings.set("no_bridges", True) + settings.set("bridges_enabled", False) return settings From 706a04242ff7a70b2fc50b2dbda78f435db9d9e6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 22:00:39 -0700 Subject: [PATCH 57/70] Show message in Tor Settings tab if any tabs have active services, to prevent the user from changing settings without stopping them --- .../src/onionshare/resources/locale/en.json | 1 + desktop/src/onionshare/tab_widget.py | 21 ++++++--- desktop/src/onionshare/tor_settings_tab.py | 44 +++++++++++++++---- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 3f380466..398782af 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -72,6 +72,7 @@ "gui_settings_bridge_custom_placeholder": "type address:port (one per line)", "gui_settings_moat_bridges_invalid": "You have not requested a bridge from torproject.org yet.", "gui_settings_tor_bridges_invalid": "None of the bridges you added work. Double-check them or add others.", + "gui_settings_stop_active_tabs_label": "There are services running in some of your tabs.\nYou must stop all services to change your Tor settings.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index 0ab19279..ead4d960 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -50,6 +50,7 @@ class TabWidget(QtWidgets.QTabWidget): # tab's index, which changes as tabs are re-arranged. self.tabs = {} self.current_tab_id = 0 # Each tab has a unique id + self.tor_settings_tab = None # Define the new tab button self.new_tab_button = QtWidgets.QPushButton("+", parent=self) @@ -230,18 +231,20 @@ class TabWidget(QtWidgets.QTabWidget): self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) return - tor_settings_tab = TorSettingsTab(self.common, self.current_tab_id) - tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) - self.tabs[self.current_tab_id] = tor_settings_tab + self.tor_settings_tab = TorSettingsTab( + self.common, self.current_tab_id, self.are_tabs_active() + ) + self.tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) + self.tabs[self.current_tab_id] = self.tor_settings_tab self.current_tab_id += 1 index = self.addTab( - tor_settings_tab, strings._("gui_tor_settings_window_title") + self.tor_settings_tab, strings._("gui_tor_settings_window_title") ) self.setCurrentIndex(index) # In macOS, manually create a close button because tabs don't seem to have them otherwise if self.common.platform == "Darwin": - self.macos_create_close_button(tor_settings_tab, index) + self.macos_create_close_button(self.tor_settings_tab, index) def change_title(self, tab_id, title): shortened_title = title @@ -256,6 +259,11 @@ class TabWidget(QtWidgets.QTabWidget): index = self.indexOf(self.tabs[tab_id]) self.setTabIcon(index, QtGui.QIcon(GuiCommon.get_resource_path(icon_path))) + # The icon changes when the server status changes, so if we have an open + # Tor Settings tab, tell it to update + if self.tor_settings_tab: + self.tor_settings_tab.active_tabs_changed(self.are_tabs_active()) + def change_persistent(self, tab_id, is_persistent): self.common.log( "TabWidget", @@ -307,6 +315,9 @@ class TabWidget(QtWidgets.QTabWidget): self.removeTab(index) del self.tabs[tab.tab_id] + if type(self.tabs[tab_id]) is TorSettingsTab: + self.tor_settings_tab = None + # If the last tab is closed, open a new one if self.count() == 0: self.new_tab_clicked() diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index e3dc9bee..c8990901 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -41,7 +41,7 @@ class TorSettingsTab(QtWidgets.QWidget): close_this_tab = QtCore.Signal() - def __init__(self, common, tab_id): + def __init__(self, common, tab_id, are_tabs_active): super(TorSettingsTab, self).__init__() self.common = common @@ -351,16 +351,36 @@ class TorSettingsTab(QtWidgets.QWidget): buttons_layout.addWidget(self.test_tor_button) buttons_layout.addWidget(self.save_button) - # Layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(columns_wrapper) - layout.addStretch() - layout.addWidget(self.tor_con) - layout.addStretch() - layout.addLayout(buttons_layout) + # Main layout + main_layout = QtWidgets.QVBoxLayout() + main_layout.addWidget(columns_wrapper) + main_layout.addStretch() + main_layout.addWidget(self.tor_con) + main_layout.addStretch() + main_layout.addLayout(buttons_layout) + self.main_widget = QtWidgets.QWidget() + self.main_widget.setLayout(main_layout) + # Tabs are active label + active_tabs_label = QtWidgets.QLabel( + strings._("gui_settings_stop_active_tabs_label") + ) + active_tabs_label.setAlignment(QtCore.Qt.AlignHCenter) + + # Active tabs layout + active_tabs_layout = QtWidgets.QVBoxLayout() + active_tabs_layout.addStretch() + active_tabs_layout.addWidget(active_tabs_label) + active_tabs_layout.addStretch() + self.active_tabs_widget = QtWidgets.QWidget() + self.active_tabs_widget.setLayout(active_tabs_layout) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.main_widget) + layout.addWidget(self.active_tabs_widget) self.setLayout(layout) + self.active_tabs_changed(are_tabs_active) self.reload_settings() def reload_settings(self): @@ -454,6 +474,14 @@ class TorSettingsTab(QtWidgets.QWidget): self.bridge_use_checkbox.setCheckState(QtCore.Qt.Unchecked) self.bridge_settings.hide() + def active_tabs_changed(self, are_tabs_active): + if are_tabs_active: + self.main_widget.hide() + self.active_tabs_widget.show() + else: + self.main_widget.show() + self.active_tabs_widget.hide() + def connection_type_bundled_toggled(self, checked): """ Connection type bundled was toggled From bde94d37fc1cdf16767437a624bbdfe2150bf598 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 22:09:24 -0700 Subject: [PATCH 58/70] Don't delete any custom bridges that are set --- desktop/src/onionshare/tor_settings_tab.py | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index c8990901..7ef8d850 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -822,7 +822,6 @@ class TorSettingsTab(QtWidgets.QWidget): return False settings.set("bridges_moat", moat_bridges) - settings.set("bridges_custom", "") if self.bridge_custom_radio.isChecked(): settings.set("bridges_type", "custom") From de3c95cc507968b406871f46620b84991df2f056 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 26 Oct 2021 22:12:22 -0700 Subject: [PATCH 59/70] Set self.torr_settings_tab to None _before_ deleting the tab --- desktop/src/onionshare/tab_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index ead4d960..da7d50bf 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -311,13 +311,13 @@ class TabWidget(QtWidgets.QTabWidget): ): self.common.log("TabWidget", "closing a settings tab") + if type(self.tabs[tab_id]) is TorSettingsTab: + self.tor_settings_tab = None + # Remove the tab self.removeTab(index) del self.tabs[tab.tab_id] - if type(self.tabs[tab_id]) is TorSettingsTab: - self.tor_settings_tab = None - # If the last tab is closed, open a new one if self.count() == 0: self.new_tab_clicked() From 7da1ac187b7991a4bdd0f8b0e2828bc31f0adcb7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 19:20:36 -0700 Subject: [PATCH 60/70] Remove sticky "Disconnected from Tor" message (patch thanks to @mig5) --- desktop/src/onionshare/tab_widget.py | 2 +- desktop/src/onionshare/tor_connection.py | 5 +++-- desktop/src/onionshare/tor_settings_tab.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index da7d50bf..3579d21b 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -232,7 +232,7 @@ class TabWidget(QtWidgets.QTabWidget): return self.tor_settings_tab = TorSettingsTab( - self.common, self.current_tab_id, self.are_tabs_active() + self.common, self.current_tab_id, self.are_tabs_active(), self.status_bar ) self.tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) self.tabs[self.current_tab_id] = self.tor_settings_tab diff --git a/desktop/src/onionshare/tor_connection.py b/desktop/src/onionshare/tor_connection.py index 1cfed2a8..2cc599c4 100644 --- a/desktop/src/onionshare/tor_connection.py +++ b/desktop/src/onionshare/tor_connection.py @@ -117,7 +117,6 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def _connected_to_tor(self): self.common.log("TorConnectionDialog", "_connected_to_tor") self.active = False - # Close the dialog after connecting self.setValue(self.maximum()) @@ -166,11 +165,12 @@ class TorConnectionWidget(QtWidgets.QWidget): success = QtCore.Signal() fail = QtCore.Signal(str) - def __init__(self, common): + def __init__(self, common, status_bar): super(TorConnectionWidget, self).__init__(None) self.common = common self.common.log("TorConnectionWidget", "__init__") + self.status_bar = status_bar self.label = QtWidgets.QLabel(strings._("connecting_to_tor")) self.label.setAlignment(QtCore.Qt.AlignHCenter) @@ -245,6 +245,7 @@ class TorConnectionWidget(QtWidgets.QWidget): def _connected_to_tor(self): self.common.log("TorConnectionWidget", "_connected_to_tor") self.active = False + self.status_bar.clearMessage() # Close the dialog after connecting self.progress.setValue(self.progress.maximum()) diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 7ef8d850..85645ca0 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -41,12 +41,13 @@ class TorSettingsTab(QtWidgets.QWidget): close_this_tab = QtCore.Signal() - def __init__(self, common, tab_id, are_tabs_active): + def __init__(self, common, tab_id, are_tabs_active, status_bar): super(TorSettingsTab, self).__init__() self.common = common self.common.log("TorSettingsTab", "__init__") + self.status_bar = status_bar self.meek = Meek(common, get_tor_paths=self.common.gui.get_tor_paths) self.system = platform.system() @@ -327,7 +328,7 @@ class TorSettingsTab(QtWidgets.QWidget): columns_wrapper.setLayout(columns_layout) # Tor connection widget - self.tor_con = TorConnectionWidget(self.common) + self.tor_con = TorConnectionWidget(self.common, self.status_bar) self.tor_con.success.connect(self.tor_con_success) self.tor_con.fail.connect(self.tor_con_fail) self.tor_con.hide() From 7b162b4bc7475ab124a91f95dc8e58986f3ab476 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:05:20 -0700 Subject: [PATCH 61/70] In all modes, if Tor isn't connected display a message instead of showing the mode content --- desktop/src/onionshare/gui_common.py | 5 ++ .../src/onionshare/resources/locale/en.json | 3 +- desktop/src/onionshare/tab/mode/__init__.py | 54 ++++++++++++++++++- .../onionshare/tab/mode/chat_mode/__init__.py | 6 +-- .../tab/mode/receive_mode/__init__.py | 6 +-- .../tab/mode/share_mode/__init__.py | 6 +-- .../tab/mode/website_mode/__init__.py | 6 +-- desktop/src/onionshare/tab/tab.py | 1 - desktop/src/onionshare/tab_widget.py | 22 ++++++++ desktop/src/onionshare/tor_settings_tab.py | 8 +++ 10 files changed, 98 insertions(+), 19 deletions(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index b081774e..39f6d46f 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -281,6 +281,11 @@ class GuiCommon: QLabel { color: #cc0000; }""", + "tor_not_connected_label": """ + QLabel { + font-size: 16px; + font-style: italic; + }""", # New tab "new_tab_button_image": """ QLabel { diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 398782af..868a6fa9 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -229,5 +229,6 @@ "moat_captcha_reload": "Reload", "moat_bridgedb_error": "Error contacting BridgeDB.", "moat_captcha_error": "The solution is not correct. Please try again.", - "moat_solution_empty_error": "You must enter the characters from the image" + "moat_solution_empty_error": "You must enter the characters from the image", + "mode_tor_not_connected_label": "OnionShare is not connected to the Tor network" } \ No newline at end of file diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py index d4f2c23a..936c91cb 100644 --- a/desktop/src/onionshare/tab/mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/__init__.py @@ -28,7 +28,7 @@ from .mode_settings_widget import ModeSettingsWidget from ..server_status import ServerStatus from ... import strings from ...threads import OnionThread, AutoStartTimer -from ...widgets import Alert +from ...widgets import Alert, MinimumSizeWidget class Mode(QtWidgets.QWidget): @@ -101,6 +101,35 @@ class Mode(QtWidgets.QWidget): self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) + # It's up to the downstream Mode to add stuff to self.content_layout + # self.content_layout shows the actual content of the mode + # self.tor_not_connected_layout is displayed when Tor isn't connected + self.content_layout = QtWidgets.QVBoxLayout() + self.content_widget = QtWidgets.QWidget() + self.content_widget.setLayout(self.content_layout) + + tor_not_connected_label = QtWidgets.QLabel( + strings._("mode_tor_not_connected_label") + ) + tor_not_connected_label.setAlignment(QtCore.Qt.AlignHCenter) + tor_not_connected_label.setStyleSheet( + self.common.gui.css["tor_not_connected_label"] + ) + self.tor_not_connected_layout = QtWidgets.QVBoxLayout() + self.tor_not_connected_layout.addStretch() + self.tor_not_connected_layout.addWidget(tor_not_connected_label) + self.tor_not_connected_layout.addWidget(MinimumSizeWidget(700, 0)) + self.tor_not_connected_layout.addStretch() + self.tor_not_connected_widget = QtWidgets.QWidget() + self.tor_not_connected_widget.setLayout(self.tor_not_connected_layout) + + self.wrapper_layout = QtWidgets.QVBoxLayout() + self.wrapper_layout.addWidget(self.content_widget) + self.wrapper_layout.addWidget(self.tor_not_connected_widget) + self.setLayout(self.wrapper_layout) + + self.tor_connection_init() + def init(self): """ Add custom initialization here. @@ -524,3 +553,26 @@ class Mode(QtWidgets.QWidget): Used in both Share and Website modes, so implemented here. """ self.history.cancel(event["data"]["id"]) + + def tor_connection_init(self): + """ + Figure out if Tor is connected and display the right widget + """ + if self.common.gui.onion.is_authenticated(): + self.tor_connection_started() + else: + self.tor_connection_stopped() + + def tor_connection_started(self): + """ + This is called on every Mode when Tor is connected + """ + self.content_widget.show() + self.tor_not_connected_widget.hide() + + def tor_connection_stopped(self): + """ + This is called on every Mode when Tor is disconnected + """ + self.content_widget.hide() + self.tor_not_connected_widget.show() diff --git a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py index e7a17ce7..1081fe9d 100644 --- a/desktop/src/onionshare/tab/mode/chat_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/chat_mode/__init__.py @@ -98,10 +98,8 @@ class ChatMode(Mode): self.column_layout.addWidget(self.image) self.column_layout.addLayout(self.main_layout) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) def get_type(self): """ diff --git a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py index d5036d1d..b2b2fc5a 100644 --- a/desktop/src/onionshare/tab/mode/receive_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/receive_mode/__init__.py @@ -198,10 +198,8 @@ class ReceiveMode(Mode): self.column_layout.addLayout(row_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) def get_type(self): """ diff --git a/desktop/src/onionshare/tab/mode/share_mode/__init__.py b/desktop/src/onionshare/tab/mode/share_mode/__init__.py index 5d3e3c35..7be93f1d 100644 --- a/desktop/src/onionshare/tab/mode/share_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/share_mode/__init__.py @@ -169,10 +169,8 @@ class ShareMode(Mode): self.column_layout.addLayout(self.main_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/desktop/src/onionshare/tab/mode/website_mode/__init__.py b/desktop/src/onionshare/tab/mode/website_mode/__init__.py index a50d15b9..73c4bad2 100644 --- a/desktop/src/onionshare/tab/mode/website_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py @@ -167,10 +167,8 @@ class WebsiteMode(Mode): self.column_layout.addLayout(self.main_layout) self.column_layout.addWidget(self.history, stretch=1) - # Wrapper layout - self.wrapper_layout = QtWidgets.QVBoxLayout() - self.wrapper_layout.addLayout(self.column_layout) - self.setLayout(self.wrapper_layout) + # Content layout + self.content_layout.addLayout(self.column_layout) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/desktop/src/onionshare/tab/tab.py b/desktop/src/onionshare/tab/tab.py index 5d9bb077..fb7f1836 100644 --- a/desktop/src/onionshare/tab/tab.py +++ b/desktop/src/onionshare/tab/tab.py @@ -96,7 +96,6 @@ class Tab(QtWidgets.QWidget): tab_id, system_tray, status_bar, - mode_settings=None, filenames=None, ): super(Tab, self).__init__() diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index 3579d21b..c7a3552a 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -235,6 +235,8 @@ class TabWidget(QtWidgets.QTabWidget): self.common, self.current_tab_id, self.are_tabs_active(), self.status_bar ) self.tor_settings_tab.close_this_tab.connect(self.close_tor_settings_tab) + self.tor_settings_tab.tor_is_connected.connect(self.tor_is_connected) + self.tor_settings_tab.tor_is_disconnected.connect(self.tor_is_disconnected) self.tabs[self.current_tab_id] = self.tor_settings_tab self.current_tab_id += 1 index = self.addTab( @@ -399,6 +401,26 @@ class TabWidget(QtWidgets.QTabWidget): close_button.clicked.connect(close_tab) self.tabBar().setTabButton(index, QtWidgets.QTabBar.RightSide, tab.close_button) + def tor_is_connected(self): + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_started() + + def tor_is_disconnected(self): + for tab_id in self.tabs: + if not ( + type(self.tabs[tab_id]) is SettingsTab + or type(self.tabs[tab_id]) is TorSettingsTab + ): + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_stopped() + class TabBar(QtWidgets.QTabBar): """ diff --git a/desktop/src/onionshare/tor_settings_tab.py b/desktop/src/onionshare/tor_settings_tab.py index 85645ca0..e28e5260 100644 --- a/desktop/src/onionshare/tor_settings_tab.py +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -40,6 +40,8 @@ class TorSettingsTab(QtWidgets.QWidget): """ close_this_tab = QtCore.Signal() + tor_is_connected = QtCore.Signal() + tor_is_disconnected = QtCore.Signal() def __init__(self, common, tab_id, are_tabs_active, status_bar): super(TorSettingsTab, self).__init__() @@ -699,6 +701,9 @@ class TorSettingsTab(QtWidgets.QWidget): # Do we need to reinitialize Tor? if reboot_onion: + # Tell the tabs that Tor is disconnected + self.tor_is_disconnected.emit() + # Reinitialize the Onion object self.common.log( "TorSettingsTab", "save_clicked", "rebooting the Onion" @@ -742,6 +747,9 @@ class TorSettingsTab(QtWidgets.QWidget): self.common.gui.onion.is_authenticated() and not self.tor_con.wasCanceled() ): + # Tell the tabs that Tor is connected + self.tor_is_connected.emit() + # Close the tab self.close_this_tab.emit() self.tor_con_type = None From 7448f76f2e54899e932ecc2a116c04cf25ed061d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:17:02 -0700 Subject: [PATCH 62/70] Respect --local-only --- desktop/src/onionshare/tab/mode/__init__.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py index 936c91cb..6be97995 100644 --- a/desktop/src/onionshare/tab/mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/__init__.py @@ -128,7 +128,10 @@ class Mode(QtWidgets.QWidget): self.wrapper_layout.addWidget(self.tor_not_connected_widget) self.setLayout(self.wrapper_layout) - self.tor_connection_init() + if self.common.gui.onion.is_authenticated(): + self.tor_connection_started() + else: + self.tor_connection_stopped() def init(self): """ @@ -554,15 +557,6 @@ class Mode(QtWidgets.QWidget): """ self.history.cancel(event["data"]["id"]) - def tor_connection_init(self): - """ - Figure out if Tor is connected and display the right widget - """ - if self.common.gui.onion.is_authenticated(): - self.tor_connection_started() - else: - self.tor_connection_stopped() - def tor_connection_started(self): """ This is called on every Mode when Tor is connected @@ -574,5 +568,9 @@ class Mode(QtWidgets.QWidget): """ This is called on every Mode when Tor is disconnected """ + if self.common.gui.local_only: + self.tor_connection_started() + return + self.content_widget.hide() self.tor_not_connected_widget.show() From 4e62b8831a867c47cfa880c7d0841ba1483da5c2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:42:51 -0700 Subject: [PATCH 63/70] Get tor from Tor Browser 11.0a10 on all platforms --- desktop/scripts/get-tor-linux.py | 6 +++--- desktop/scripts/get-tor-osx.py | 6 +++--- desktop/scripts/get-tor-windows.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py index b8f83c92..51beb475 100755 --- a/desktop/scripts/get-tor-linux.py +++ b/desktop/scripts/get-tor-linux.py @@ -34,10 +34,10 @@ import requests def main(): - tarball_url = "https://dist.torproject.org/torbrowser/11.0a9/tor-browser-linux64-11.0a9_en-US.tar.xz" - tarball_filename = "tor-browser-linux64-11.0a9_en-US.tar.xz" + tarball_url = "https://dist.torproject.org/torbrowser/11.0a10/tor-browser-linux64-11.0a10_en-US.tar.xz" + tarball_filename = "tor-browser-linux64-11.0a10_en-US.tar.xz" expected_tarball_sha256 = ( - "cba4a2120b4f847d1ade637e41e69bd01b2e70b4a13e41fe8e69d0424fcf7ca7" + "5d3e2ebc4fb6a10f44624359bc2a5a151a57e8402cbd8563d15f9b2524374f1f" ) # Build paths diff --git a/desktop/scripts/get-tor-osx.py b/desktop/scripts/get-tor-osx.py index be5f7a56..410a8157 100755 --- a/desktop/scripts/get-tor-osx.py +++ b/desktop/scripts/get-tor-osx.py @@ -34,10 +34,10 @@ import requests def main(): - dmg_url = "https://dist.torproject.org/torbrowser/11.0a7/TorBrowser-11.0a7-osx64_en-US.dmg" - dmg_filename = "TorBrowser-11.0a7-osx64_en-US.dmg" + dmg_url = "https://dist.torproject.org/torbrowser/11.0a10/TorBrowser-11.0a10-osx64_en-US.dmg" + dmg_filename = "TorBrowser-11.0a10-osx64_en-US.dmg" expected_dmg_sha256 = ( - "46594cefa29493150d1c0e1933dd656aafcb6b51ef310d44ac059eed2fd1388e" + "c6823a28fd28205437564815f93011ff93b7972da2a8ce16919adfc65909e7b9" ) # Build paths diff --git a/desktop/scripts/get-tor-windows.py b/desktop/scripts/get-tor-windows.py index 751faecc..8ca2e79f 100644 --- a/desktop/scripts/get-tor-windows.py +++ b/desktop/scripts/get-tor-windows.py @@ -33,10 +33,10 @@ import requests def main(): - exe_url = "https://dist.torproject.org/torbrowser/11.0a7/torbrowser-install-11.0a7_en-US.exe" - exe_filename = "torbrowser-install-11.0a7_en-US.exe" + exe_url = "https://dist.torproject.org/torbrowser/11.0a10/torbrowser-install-11.0a10_en-US.exe" + exe_filename = "torbrowser-install-11.0a10_en-US.exe" expected_exe_sha256 = ( - "8b2013669d88e3ae8fa9bc17a3495eaac9475f79a849354e826e5132811a860b" + "f567dd8368dea0a8d7bbf7c19ece7840f93d493e70662939b92f5058c8dc8d2d" ) # Build paths root_path = os.path.dirname( From fa70368a8d4891461ae503ba40594e456e1b4dae Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:46:52 -0700 Subject: [PATCH 64/70] Copy snowflake-client from macOS Tor Browser --- desktop/scripts/get-tor-osx.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/desktop/scripts/get-tor-osx.py b/desktop/scripts/get-tor-osx.py index 410a8157..80d7aee8 100755 --- a/desktop/scripts/get-tor-osx.py +++ b/desktop/scripts/get-tor-osx.py @@ -101,6 +101,14 @@ def main(): os.path.join(dist_path, "obfs4proxy"), ) os.chmod(os.path.join(dist_path, "obfs4proxy"), 0o755) + # snowflake-client binary + shutil.copyfile( + os.path.join( + dmg_tor_path, "MacOS", "Tor", "PluggableTransports", "snowflake-client" + ), + os.path.join(dist_path, "snowflake-client"), + ) + os.chmod(os.path.join(dist_path, "snowflake-client"), 0o755) # Eject dmg subprocess.call(["diskutil", "eject", "/Volumes/Tor Browser"]) From 0e2d2a1e46f87df6fa76bf180f6bda2df1efa5e9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:52:05 -0700 Subject: [PATCH 65/70] macOS seems to have close buttons that work on their own now --- desktop/src/onionshare/tab_widget.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index c7a3552a..7f42632d 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -186,10 +186,6 @@ class TabWidget(QtWidgets.QTabWidget): index = self.addTab(tab, strings._("gui_new_tab")) self.setCurrentIndex(index) - # In macOS, manually create a close button because tabs don't seem to have them otherwise - if self.common.platform == "Darwin": - self.macos_create_close_button(tab, index) - tab.init(mode_settings) # Make sure the title is set @@ -218,10 +214,6 @@ class TabWidget(QtWidgets.QTabWidget): index = self.addTab(settings_tab, strings._("gui_settings_window_title")) self.setCurrentIndex(index) - # In macOS, manually create a close button because tabs don't seem to have them otherwise - if self.common.platform == "Darwin": - self.macos_create_close_button(settings_tab, index) - def open_tor_settings_tab(self): self.common.log("TabWidget", "open_tor_settings_tab") @@ -244,10 +236,6 @@ class TabWidget(QtWidgets.QTabWidget): ) self.setCurrentIndex(index) - # In macOS, manually create a close button because tabs don't seem to have them otherwise - if self.common.platform == "Darwin": - self.macos_create_close_button(self.tor_settings_tab, index) - def change_title(self, tab_id, title): shortened_title = title if len(shortened_title) > 11: @@ -388,19 +376,6 @@ class TabWidget(QtWidgets.QTabWidget): super(TabWidget, self).resizeEvent(event) self.move_new_tab_button() - def macos_create_close_button(self, tab, index): - def close_tab(): - self.tabBar().tabCloseRequested.emit(self.indexOf(tab)) - - close_button = QtWidgets.QPushButton() - close_button.setFlat(True) - close_button.setFixedWidth(40) - close_button.setIcon( - QtGui.QIcon(GuiCommon.get_resource_path("images/close_tab.png")) - ) - close_button.clicked.connect(close_tab) - self.tabBar().setTabButton(index, QtWidgets.QTabBar.RightSide, tab.close_button) - def tor_is_connected(self): for tab_id in self.tabs: if not ( From f915f911c6fc7dd4dea500538a993b7382fee85b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 20:55:50 -0700 Subject: [PATCH 66/70] Make autoupdate group in Settings Tab centered --- desktop/src/onionshare/settings_tab.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py index 8f41b2aa..75b7a326 100644 --- a/desktop/src/onionshare/settings_tab.py +++ b/desktop/src/onionshare/settings_tab.py @@ -73,9 +73,16 @@ class SettingsTab(QtWidgets.QWidget): ) autoupdate_group.setLayout(autoupdate_group_layout) + autoupdate_layout = QtWidgets.QHBoxLayout() + autoupdate_layout.addStretch() + autoupdate_layout.addWidget(autoupdate_group) + autoupdate_layout.addStretch() + autoupdate_widget = QtWidgets.QWidget() + autoupdate_widget.setLayout(autoupdate_layout) + # Autoupdate is only available for Windows and Mac (Linux updates using package manager) if self.system != "Windows" and self.system != "Darwin": - autoupdate_group.hide() + autoupdate_widget.hide() # Language settings language_label = QtWidgets.QLabel(strings._("gui_settings_language_label")) @@ -131,8 +138,8 @@ class SettingsTab(QtWidgets.QWidget): # Layout layout = QtWidgets.QVBoxLayout() layout.addStretch() - layout.addWidget(autoupdate_group) - if autoupdate_group.isVisible(): + layout.addWidget(autoupdate_widget) + if autoupdate_widget.isVisible(): layout.addSpacing(20) layout.addLayout(language_layout) layout.addLayout(theme_layout) From 472e383b7d0f4040e4963ffff95da1e86fe85cca Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 6 Nov 2021 21:02:24 -0700 Subject: [PATCH 67/70] Fix settings error color in dark mode --- desktop/src/onionshare/gui_common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/src/onionshare/gui_common.py b/desktop/src/onionshare/gui_common.py index 39f6d46f..0db0f051 100644 --- a/desktop/src/onionshare/gui_common.py +++ b/desktop/src/onionshare/gui_common.py @@ -93,6 +93,7 @@ class GuiCommon: share_zip_progess_bar_chunk_color = "#4E064F" history_background_color = "#ffffff" history_label_color = "#000000" + settings_error_color = "#FF0000" if color_mode == "dark": header_color = "#F2F2F2" title_color = "#F2F2F2" @@ -103,6 +104,7 @@ class GuiCommon: share_zip_progess_bar_border_color = "#F2F2F2" history_background_color = "#191919" history_label_color = "#ffffff" + settings_error_color = "#FF9999" return { # OnionShareGui styles @@ -400,7 +402,9 @@ class GuiCommon: # Tor Settings dialogs "tor_settings_error": """ QLabel { - color: #FF0000; + color: """ + + settings_error_color + + """; } """, } From 9430439b5f02829e3677ac187e1db7a52076ddad Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 7 Nov 2021 12:12:12 -0800 Subject: [PATCH 68/70] Fix meek-client in Windows --- cli/onionshare_cli/meek.py | 12 +++++++++++- desktop/README.md | 2 +- desktop/scripts/build-meek-client.py | 16 +++++++++++----- desktop/src/onionshare/moat_dialog.py | 10 ++++++++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py index b2e70dc1..c5df7b7f 100644 --- a/cli/onionshare_cli/meek.py +++ b/cli/onionshare_cli/meek.py @@ -85,6 +85,10 @@ class Meek(object): self.common.log("Meek", "start", "Starting meek client") if self.common.platform == "Windows": + env = os.environ.copy() + for key in self.meek_env: + env[key] = self.meek_env[key] + # In Windows, hide console window when opening meek-client.exe subprocess startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW @@ -100,7 +104,7 @@ class Meek(object): stderr=subprocess.PIPE, startupinfo=startupinfo, bufsize=1, - env=self.meek_env, + env=env, text=True, ) else: @@ -129,6 +133,7 @@ class Meek(object): # read stdout without blocking try: line = q.get_nowait() + self.common.log("Meek", "start", line.strip()) except Empty: # no stdout yet? pass @@ -143,6 +148,10 @@ class Meek(object): ) break + if "CMETHOD-ERROR" in line: + self.cleanup() + raise MeekNotRunning() + if self.meek_port: self.meek_proxies = { "http": f"socks5h://{self.meek_host}:{self.meek_port}", @@ -150,6 +159,7 @@ class Meek(object): } else: self.common.log("Meek", "start", "Could not obtain the meek port") + self.cleanup() raise MeekNotRunning() def cleanup(self): diff --git a/desktop/README.md b/desktop/README.md index 408b6852..7f13ad70 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -65,7 +65,7 @@ python scripts\get-tor-windows.py ### Compile dependencies -Install Go. The simplest way to make sure everything works is to install Go by following [these instructions](https://golang.org/doc/install). +Install Go. The simplest way to make sure everything works is to install Go by following [these instructions](https://golang.org/doc/install). (In Windows, make sure to install the 32-bit version of Go, such as `go1.17.3.windows-386.msi`.) Download and compile `meek-client`: diff --git a/desktop/scripts/build-meek-client.py b/desktop/scripts/build-meek-client.py index d043754c..af58173a 100755 --- a/desktop/scripts/build-meek-client.py +++ b/desktop/scripts/build-meek-client.py @@ -28,6 +28,7 @@ import shutil import os import subprocess import inspect +import platform def main(): @@ -46,16 +47,21 @@ def main(): root_path = os.path.dirname( os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) ) - dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + if platform.system() == "Windows": + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor", "Tor") + bin_filename = "meek-client.exe" + else: + dist_path = os.path.join(root_path, "src", "onionshare", "resources", "tor") + bin_filename = "meek-client" - bin_path = os.path.expanduser("~/go/bin/meek-client") + bin_path = os.path.join(os.path.expanduser("~"), "go", "bin", bin_filename) shutil.copyfile( os.path.join(bin_path), - os.path.join(dist_path, "meek-client"), + os.path.join(dist_path, bin_filename), ) - os.chmod(os.path.join(dist_path, "meek-client"), 0o755) + os.chmod(os.path.join(dist_path, bin_filename), 0o755) - print(f"Installed meek-client in {dist_path}") + print(f"Installed {bin_filename} in {dist_path}") if __name__ == "__main__": diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py index 85b5e888..84a52390 100644 --- a/desktop/src/onionshare/moat_dialog.py +++ b/desktop/src/onionshare/moat_dialog.py @@ -26,7 +26,7 @@ import json from . import strings from .gui_common import GuiCommon -from onionshare_cli.meek import MeekNotFound +from onionshare_cli.meek import MeekNotFound, MeekNotRunning class MoatDialog(QtWidgets.QDialog): @@ -237,7 +237,13 @@ class MoatThread(QtCore.QThread): try: self.meek.start() except MeekNotFound: - self.common.log("MoatThread", "run", f"Could not find the Meek Client") + self.common.log("MoatThread", "run", f"Could not find meek-client") + self.bridgedb_error.emit() + return + except MeekNotRunning: + self.common.log( + "MoatThread", "run", f"Ran meek-client, but there was an error" + ) self.bridgedb_error.emit() return From 73b570f4b44da8790d7e7d22b6c160b047fc86c7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Nov 2021 18:49:23 -0800 Subject: [PATCH 69/70] Check if Tor is connected instead of if the Tor controller is authenticated --- desktop/src/onionshare/tab/mode/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/onionshare/tab/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py index 6be97995..c9b5cad1 100644 --- a/desktop/src/onionshare/tab/mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/__init__.py @@ -128,7 +128,7 @@ class Mode(QtWidgets.QWidget): self.wrapper_layout.addWidget(self.tor_not_connected_widget) self.setLayout(self.wrapper_layout) - if self.common.gui.onion.is_authenticated(): + if self.common.gui.onion.connected_to_tor: self.tor_connection_started() else: self.tor_connection_stopped() @@ -571,6 +571,6 @@ class Mode(QtWidgets.QWidget): if self.common.gui.local_only: self.tor_connection_started() return - + self.content_widget.hide() self.tor_not_connected_widget.show() From d88005d550ab65024e298f05e0c269eccf98f9e3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 9 Nov 2021 18:57:07 -0800 Subject: [PATCH 70/70] When Tor is disconnected, hide the Check for Updates button in the Settings tab --- desktop/src/onionshare/settings_tab.py | 11 ++++++++++ desktop/src/onionshare/tab_widget.py | 28 +++++++++++++------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py index 75b7a326..cfa3261e 100644 --- a/desktop/src/onionshare/settings_tab.py +++ b/desktop/src/onionshare/settings_tab.py @@ -154,6 +154,11 @@ class SettingsTab(QtWidgets.QWidget): self.reload_settings() + if self.common.gui.onion.connected_to_tor: + self.tor_is_connected() + else: + self.tor_is_disconnected() + def reload_settings(self): # Load settings, and fill them in self.old_settings = Settings(self.common) @@ -341,3 +346,9 @@ class SettingsTab(QtWidgets.QWidget): else: self.check_for_updates_button.setEnabled(True) self.save_button.setEnabled(True) + + def tor_is_connected(self): + self.check_for_updates_button.show() + + def tor_is_disconnected(self): + self.check_for_updates_button.hide() diff --git a/desktop/src/onionshare/tab_widget.py b/desktop/src/onionshare/tab_widget.py index 7f42632d..7162fcc4 100644 --- a/desktop/src/onionshare/tab_widget.py +++ b/desktop/src/onionshare/tab_widget.py @@ -378,23 +378,23 @@ class TabWidget(QtWidgets.QTabWidget): def tor_is_connected(self): for tab_id in self.tabs: - if not ( - type(self.tabs[tab_id]) is SettingsTab - or type(self.tabs[tab_id]) is TorSettingsTab - ): - mode = self.tabs[tab_id].get_mode() - if mode: - mode.tor_connection_started() + if type(self.tabs[tab_id]) is SettingsTab: + self.tabs[tab_id].tor_is_connected() + else: + if not type(self.tabs[tab_id]) is TorSettingsTab: + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_started() def tor_is_disconnected(self): for tab_id in self.tabs: - if not ( - type(self.tabs[tab_id]) is SettingsTab - or type(self.tabs[tab_id]) is TorSettingsTab - ): - mode = self.tabs[tab_id].get_mode() - if mode: - mode.tor_connection_stopped() + if type(self.tabs[tab_id]) is SettingsTab: + self.tabs[tab_id].tor_is_disconnected() + else: + if not type(self.tabs[tab_id]) is TorSettingsTab: + mode = self.tabs[tab_id].get_mode() + if mode: + mode.tor_connection_stopped() class TabBar(QtWidgets.QTabBar):