From 88c7b9fdecc43c3cd52234aa75be5cc7bb87cd6f 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 fae1f349eead7b4de4769bf5087826ef456120c6 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 c58272c11713767952f2050397b600f3c540ee6a 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 03d19532462328c6444490c1ecaa07269db3012c 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 84c1fc022509383609def17fbfbf56767f7b23a2 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 d69f4b51d563994bbb01562742b4c64421faf440 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 cd95fcc61fea0493e0d075c61a006b165f6523b9 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 dd8163f0e3e5fed181b0e07f006b35bfebcf1ac2 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 cf3a69504e4d6e527df8310e2122fd21e1fb8c9b 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 5d99cba913ebfa0cc4d81759e85a6bea56b2e01d 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 1244f56d26798c841bb41a5fff25020951a37c2f 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 3ea92d6bad61d8543b8b332a9b710532fac08306 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 3e9b9b2f930a514986c27b52e9c52f1a6c4d2705 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 451107e9fbac6dbf2f189af31526fd469141ff87 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 879dd0452e828351760f2002b797481d0d3fef58 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 dc424983a42624ac7ca1a08c6026c31863d131d5 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 12b9ff62807e23d9f5c9d1ee6f844385c1b38fa5 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 4c42ef9de33ee32081b4c5d4000ad2a8cd5ba280 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 4508e55bd2cb5f61ae87482c8b4907b960578a90 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 6faa1349baf91a73732cd86c5d71c282e5ae9359 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 ee5895bbda4d8ace27f41de02f6bfa457f2011ee 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 7faa1cde26dbcbc0a812c86bef151171b54a883e 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 cc375082926221368d2fd6cd0070d8b8f4dfd765 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 70dce471cde0ca0783c343fe0c1fe17b97622d7f 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 edeccc965c39b543bbb3c51c723deac1e1cfdde3 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 8d135defddefb3a68f45bc6aeb511d4fecebaac8 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 494bdc0c147da27270626a6be3617be842d49a1a 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 8a19d8088ec62f1ea93743d6139b93a15e408629 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 a0c386123f0ae49daddd8147cff242a8b7496873 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 cc3729c5b827cdd15d1d511ec49340e73432859e 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 0113c9f317e3e4df91efa75a99f60cff020496c0 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 cbd95759e8c005b93decb4e3674d74164513333e 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 2a821678524dfbf169fb5e9031849dd0a2244e1f 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 084455deb4f4194ead44696c9948d3a8bfe51a69 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 476e4bd441547a9bdda9dce7c9252f0c877b66a6 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 cfa7597555fcec2bba9efe1214927ea55ea2ae07 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 0f9cb2a7324a77f6d3e44d0f9cd152b41d110071 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 c15d9ff3e65cd2fe247137cc16ce8417f2580e71 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 75451f0eb7e4f4759f8e00ee8e95d7ee2380cc11 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 6bd1a4c527f62617f285742d85750765eb2c8fed 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 969cd2bb621dbd4ff00575c43dd1c8b4363ceea8 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 fa0f707a22340b6cf794f07aa46bc00ff5101f3e 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 93ea5eb06817f3ceaf0c53c0f48c26e3fcee0d1f 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 315833c67834972615ce90a81bb9dcd0cab53e2a 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 a5ff00c1f57172ec09f27897d9afdafc2ab8dacc 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 cee540c9ca77db3108f0f239eb40fd82fa168907 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 51997f870ba234f96389f0f27922fc82a831320e 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 876b96b6351e437d736c7a0fd9792b086b58ef66 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 6ff9fa5e9a9d6f909ab67c13797e4c1ee7d094b1 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 38d3a26a4edf8ff6678cfcea09839e5ab100237c 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 fe538905835c11a6889028893c1e90964a40acac 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 3ffff26f02bdb2db2bdea1479ae61ebe716d768a 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 01c079b8b7a915755187577ffc09d50596078c7a 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 bed9596ce37512a8869f1741a4718ea1e67e6d36 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 0220c0049bb30bec04eefc842c076c6947846f46 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 4b56595ac322e812554a5060ebfabf061986c472 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 7752f5fa9be3228b62c319b1e925ea1dd7a79535 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 ffee426e6d9c2837c773a1da9653158019c9d8f9 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 f4eeab03dbe25b64cac4649feaee383c586d2faf 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 1d4d841239e5a061c3028a9a05690509137e9146 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 8919e2924b564edd098ac28bd4b0f3704dc2b669 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 985e0fdf6beecdb4ac0670a80b0ecdef299e5ed3 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 42507b11382de7ca41e1ecf89169ff5f24cb1eeb 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 22fc1354ce266fdfe1df80e71e96e60c976350d1 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 9f40d2d1d323573b11f35f67c972ee171ae10508 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 71b6d77e975cc675ad702d11e0df0fb2bef6ca36 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 1eb2476d3c1f9c05fa72ab8fb3f85ecd0488227d 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 08ae2e616be8055c9903b0ccbde19527262e0e6f 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 e6a0b97283451f02def7c021cb36158507119119 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 ba6947ff4e2f9bfc2b4dbcb7b77b328252726188 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):