diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index 4bc00929..4e34a508 100644 --- a/cli/onionshare_cli/__init__.py +++ b/cli/onionshare_cli/__init__.py @@ -27,13 +27,10 @@ from datetime import datetime 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 .onion import TorErrorProtocolError, TorTooOldEphemeral, TorTooOldStealth, Onion from .onionshare import OnionShare from .mode_settings import ModeSettings @@ -94,12 +91,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", @@ -293,6 +285,20 @@ 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) + # Clean up the meek subprocess once we're done working with the censorship circumvention API + # meek.cleanup() + # Start the Onion object try: onion = Onion(common, use_tmp_dir=True) @@ -409,7 +415,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..f84b1058 --- /dev/null +++ b/cli/onionshare_cli/censorship.py @@ -0,0 +1,169 @@ +# -*- 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 + +from .meek import MeekNotRunning + + +class CensorshipCircumvention(object): + """ + Connect to the Tor Moat APIs to retrieve censorship + circumvention recommendations, over the Meek client. + """ + + 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__") + + # Bail out if we requested domain fronting but we can't use meek + if domain_fronting and not self.meek.meek_proxies: + raise MeekNotRunning() + + def request_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.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 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 + 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.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 request_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.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 dd92eb0b..bab3fd86 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 @@ -313,6 +314,8 @@ class Common: if not tor_path: raise CannotFindTor() 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") @@ -320,6 +323,8 @@ 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") + 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.platform == "Darwin": @@ -327,6 +332,8 @@ class Common: if not tor_path: raise CannotFindTor() 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") @@ -335,12 +342,16 @@ 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" + snowflake_file_path = "/usr/local/bin/snowflake-client" + 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, + snowflake_file_path, + meek_client_file_path, ) def build_data_dir(self): diff --git a/cli/onionshare_cli/meek.py b/cli/onionshare_cli/meek.py new file mode 100644 index 00000000..c5df7b7f --- /dev/null +++ b/cli/onionshare_cli/meek.py @@ -0,0 +1,210 @@ +# -*- 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 os +import subprocess +import time +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, get_tor_paths=None): + """ + Set up the Meek object + """ + + self.common = common + self.common.log("Meek", "__init__") + + # 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, + 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() + + # 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 + ): + raise MeekNotFound() + + # Start the Meek Client as a subprocess. + 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 + self.meek_proc = subprocess.Popen( + [ + self.meek_client_file_path, + "--url", + self.meek_url, + "--front", + self.meek_front, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo, + bufsize=1, + env=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, + stderr=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() + self.common.log("Meek", "start", line.strip()) + 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 running on {self.meek_host}:{self.meek_port}", + ) + 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}", + "https": f"socks5h://{self.meek_host}:{self.meek_port}", + } + else: + self.common.log("Meek", "start", "Could not obtain the meek port") + self.cleanup() + 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): + """ + 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/cli/onionshare_cli/onion.py b/cli/onionshare_cli/onion.py index 7f6faa17..536b9ba0 100644 --- a/cli/onionshare_cli/onion.py +++ b/cli/onionshare_cli/onion.py @@ -153,6 +153,8 @@ class Onion(object): 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() # The tor process @@ -178,10 +180,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( @@ -198,8 +200,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 @@ -210,6 +210,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 @@ -302,43 +308,50 @@ 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) + 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()) - 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("\nUseBridges 1") + 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("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() @@ -357,6 +370,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 @@ -410,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 @@ -650,16 +660,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/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_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/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 diff --git a/cli/onionshare_cli/settings.py b/cli/onionshare_cli/settings.py index 4755d5b3..c7d74a70 100644 --- a/cli/onionshare_cli/settings.py +++ b/cli/onionshare_cli/settings.py @@ -105,10 +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_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/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"}, ] diff --git a/cli/tests/test_cli_common.py b/cli/tests/test_cli_common.py index 9f113a84..9a64d762 100644 --- a/cli/tests/test_cli_common.py +++ b/cli/tests/test_cli_common.py @@ -162,11 +162,19 @@ 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" + ) assert common_obj.get_tor_paths() == ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, + meek_client_file_path, ) @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") @@ -176,6 +184,8 @@ class TestGetTorPaths: tor_geo_ip_file_path, 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" @@ -199,6 +209,12 @@ 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" + ) + 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" ) @@ -210,6 +226,8 @@ class TestGetTorPaths: tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, + meek_client_file_path, ) diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index ed8d5bb9..9513b013 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -29,12 +29,13 @@ 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_custom_bridges": "", + "bridges_enabled": False, + "bridges_type": "built-in", + "bridges_builtin_pt": "obfs4", + "bridges_moat": "", + "bridges_custom": "", "persistent_tabs": [], - "theme":0 + "theme": 0, } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing @@ -93,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") @@ -139,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" ) diff --git a/desktop/README.md b/desktop/README.md index 4a59fe03..7f13ad70 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 @@ -53,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). (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`: + +``` +./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..af58173a --- /dev/null +++ b/desktop/scripts/build-meek-client.py @@ -0,0 +1,68 @@ +#!/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 +import platform + + +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()))) + ) + 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.join(os.path.expanduser("~"), "go", "bin", bin_filename) + shutil.copyfile( + os.path.join(bin_path), + os.path.join(dist_path, bin_filename), + ) + os.chmod(os.path.join(dist_path, bin_filename), 0o755) + + print(f"Installed {bin_filename} in {dist_path}") + + +if __name__ == "__main__": + main() diff --git a/desktop/scripts/get-tor-linux.py b/desktop/scripts/get-tor-linux.py new file mode 100755 index 00000000..51beb475 --- /dev/null +++ b/desktop/scripts/get-tor-linux.py @@ -0,0 +1,131 @@ +#!/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.0a10/tor-browser-linux64-11.0a10_en-US.tar.xz" + tarball_filename = "tor-browser-linux64-11.0a10_en-US.tar.xz" + expected_tarball_sha256 = ( + "5d3e2ebc4fb6a10f44624359bc2a5a151a57e8402cbd8563d15f9b2524374f1f" + ) + + # 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) + 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) + + 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() diff --git a/desktop/scripts/get-tor-osx.py b/desktop/scripts/get-tor-osx.py index be5f7a56..80d7aee8 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 @@ -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"]) 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( 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..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 @@ -205,14 +207,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 +265,7 @@ class GuiCommon: + """; width: 10px; }""", - "history_default_label" : """ + "history_default_label": """ QLabel { color: """ + history_label_color @@ -281,6 +283,11 @@ class GuiCommon: QLabel { color: #cc0000; }""", + "tor_not_connected_label": """ + QLabel { + font-size: 16px; + font-style: italic; + }""", # New tab "new_tab_button_image": """ QLabel { @@ -392,44 +399,50 @@ class GuiCommon: QPushButton { padding: 5px 10px; }""", - # Settings dialog - "settings_version": """ + # Tor Settings dialogs + "tor_settings_error": """ 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; - }""", + color: """ + + settings_error_color + + """; + } + """, } 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": + 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") + 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") + + 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") + 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": 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") + 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,12 +450,16 @@ 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 ( tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path, + snowflake_file_path, + meek_client_file_path, ) @staticmethod diff --git a/desktop/src/onionshare/main_window.py b/desktop/src/onionshare/main_window.py index d87092b6..546592a1 100644 --- a/desktop/src/onionshare/main_window.py +++ b/desktop/src/onionshare/main_window.py @@ -18,12 +18,12 @@ 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 .settings_dialog import SettingsDialog +from .tor_connection import TorConnectionDialog from .widgets import Alert from .update_checker import UpdateThread from .tab_widget import TabWidget @@ -106,6 +106,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) @@ -145,7 +163,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() @@ -200,7 +218,7 @@ class MainWindow(QtWidgets.QMainWindow): "_tor_connection_canceled", "Settings button clicked", ) - self.open_settings() + self.open_tor_settings() if a.clickedButton() == quit_button: # Quit @@ -214,23 +232,28 @@ 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): + """ + Open the TorSettingsTab + """ + self.common.log("MainWindow", "open_tor_settings") + self.tabs.open_tor_settings_tab() 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() def settings_have_changed(self): self.common.log("OnionShareGui", "settings_have_changed") diff --git a/desktop/src/onionshare/moat_dialog.py b/desktop/src/onionshare/moat_dialog.py new file mode 100644 index 00000000..84a52390 --- /dev/null +++ b/desktop/src/onionshare/moat_dialog.py @@ -0,0 +1,372 @@ +# -*- 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 +import os +import base64 +import json + +from . import strings +from .gui_common import GuiCommon +from onionshare_cli.meek import MeekNotFound, MeekNotRunning + + +class MoatDialog(QtWidgets.QDialog): + """ + Moat dialog: Request a bridge from torproject.org + """ + + got_bridges = QtCore.Signal(str) + + 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"))) + + # Label + self.label = QtWidgets.QLabel() + + # 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.setPlaceholderText(strings._("moat_captcha_placeholder")) + 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() + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) + self.error_label.hide() + + # Buttons + 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.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.addStretch() + layout.addWidget(self.error_label) + layout.addLayout(buttons_layout) + + self.setLayout(layout) + self.cancel_button.setFocus() + + self.reload_clicked() + + def reload_clicked(self): + """ + Reload button clicked. + """ + self.common.log("MoatDialog", "reload_clicked") + + 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, 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() + + def submit_clicked(self): + """ + Submit button clicked. + """ + self.error_label.hide() + self.solution_lineedit.setEnabled(False) + + 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, + self.meek, + "check", + { + "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) + 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") + self.close() + + def bridgedb_error(self): + self.common.log("MoatDialog", "bridgedb_error") + 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 == "": + self.error_label.setText(strings._("moat_captcha_error")) + else: + self.error_label.setText(msg) + self.error_label.show() + + self.solution_lineedit.setEnabled(True) + + 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 + 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.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) + self.close() + + +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 + + """ + + bridgedb_error = QtCore.Signal() + captcha_error = QtCore.Signal(str) + captcha_ready = QtCore.Signal(str, str, str) + bridges_ready = QtCore.Signal(str) + + 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): + + # Start Meek so that we can do domain fronting + try: + self.meek.start() + except MeekNotFound: + 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 + + # 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.bridgedb_error.emit() + return + + if self.action == "fetch": + self.common.log("MoatThread", "run", f"starting fetch") + + # Request a bridge + r = requests.post( + "https://bridges.torproject.org/moat/fetch", + headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek.meek_proxies, + json={ + "data": [ + { + "version": "0.1.0", + "type": "client-transports", + "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() + return + + 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 + + transport = moat_res["data"][0]["transport"] + image = moat_res["data"][0]["image"] + challenge = moat_res["data"][0]["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() + 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"}, + proxies=self.meek.meek_proxies, + json={ + "data": [ + { + "id": "2", + "type": "moat-solution", + "version": "0.1.0", + "transport": self.data["transport"], + "challenge": self.data["challenge"], + "solution": self.data["solution"], + "qrcode": "false", + } + ] + }, + ) + + self.meek.cleanup() + + 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() + 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']}") + 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/images/dark_tor_settings.png b/desktop/src/onionshare/resources/images/dark_tor_settings.png new file mode 100644 index 00000000..0b44bd95 Binary files /dev/null and b/desktop/src/onionshare/resources/images/dark_tor_settings.png differ diff --git a/desktop/src/onionshare/resources/images/light_tor_settings.png b/desktop/src/onionshare/resources/images/light_tor_settings.png new file mode 100644 index 00000000..e8db08eb Binary files /dev/null and b/desktop/src/onionshare/resources/images/light_tor_settings.png differ diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 03694947..868a6fa9 100644 --- a/desktop/src/onionshare/resources/locale/en.json +++ b/desktop/src/onionshare/resources/locale/en.json @@ -40,6 +40,7 @@ "gui_please_wait_no_button": "Starting…", "gui_please_wait": "Starting… Click to cancel.", "zip_progress_bar_format": "Compressing: %p%", + "gui_tor_settings_window_title": "Tor Settings", "gui_settings_window_title": "Settings", "gui_settings_autoupdate_label": "Check for new version", "gui_settings_autoupdate_option": "Notify me when a new version is available", @@ -49,29 +50,34 @@ "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_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_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.", + "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_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_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", + "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 {}.", @@ -125,7 +131,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", @@ -215,5 +221,14 @@ "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.", + "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/settings_dialog.py b/desktop/src/onionshare/settings_dialog.py deleted file mode 100644 index e8d2752c..00000000 --- a/desktop/src/onionshare/settings_dialog.py +++ /dev/null @@ -1,1114 +0,0 @@ -# -*- 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 SettingsDialog(QtWidgets.QDialog): - """ - Settings dialog. - """ - - settings_saved = QtCore.Signal() - - def __init__(self, common): - super(SettingsDialog, 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.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 - ) - - # 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.common.gui.get_tor_paths() - 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 - ( - 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 - ): - 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("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. - """ - self.common.log("SettingsDialog", "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("SettingsDialog", "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( - "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() - - 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() - - @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("SettingsDialog", "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("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") - - 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("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) - - def _enable_buttons(self): - self.common.log("SettingsDialog", "_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) diff --git a/desktop/src/onionshare/settings_tab.py b/desktop/src/onionshare/settings_tab.py new file mode 100644 index 00000000..cfa3261e --- /dev/null +++ b/desktop/src/onionshare/settings_tab.py @@ -0,0 +1,354 @@ +# -*- 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 platform +import datetime +from onionshare_cli.settings import Settings + +from . import strings +from .widgets import Alert +from .update_checker import UpdateThread + + +class SettingsTab(QtWidgets.QWidget): + """ + Settings dialog. + """ + + close_this_tab = QtCore.Signal() + + def __init__(self, common, tab_id): + super(SettingsTab, self).__init__() + + self.common = common + self.common.log("SettingsTab", "__init__") + + self.system = platform.system() + self.tab_id = tab_id + + # 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_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_widget.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.addStretch() + 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.addStretch() + theme_layout.addWidget(theme_label) + 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) + ) + 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) + + # Buttons + 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.save_button) + buttons_layout.addStretch() + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addStretch() + layout.addWidget(autoupdate_widget) + if autoupdate_widget.isVisible(): + layout.addSpacing(20) + layout.addLayout(language_layout) + layout.addLayout(theme_layout) + layout.addSpacing(20) + layout.addWidget(version_label) + layout.addWidget(help_label) + layout.addSpacing(20) + layout.addLayout(buttons_layout) + layout.addStretch() + + self.setLayout(layout) + + 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) + 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) + + def check_for_updates(self): + """ + Check for Updates button clicked. Manually force an update check. + """ + self.common.log("SettingsTab", "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("SettingsTab", "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() + self.close_this_tab.emit() + + def help_clicked(self): + """ + Help button clicked. + """ + self.common.log("SettingsTab", "help_clicked") + SettingsTab.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("SettingsTab", "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) + + 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") + + 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 _disable_buttons(self): + self.common.log("SettingsTab", "_disable_buttons") + + self.check_for_updates_button.setEnabled(False) + self.save_button.setEnabled(False) + + def _enable_buttons(self): + 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) + + 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/mode/__init__.py b/desktop/src/onionshare/tab/mode/__init__.py index d4f2c23a..c9b5cad1 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,38 @@ 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) + + if self.common.gui.onion.connected_to_tor: + self.tor_connection_started() + else: + self.tor_connection_stopped() + def init(self): """ Add custom initialization here. @@ -524,3 +556,21 @@ class Mode(QtWidgets.QWidget): Used in both Share and Website modes, so implemented here. """ self.history.cancel(event["data"]["id"]) + + 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 + """ + if self.common.gui.local_only: + self.tor_connection_started() + return + + 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 7276b8da..2e592771 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 a955ea53..7162fcc4 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): @@ -43,9 +45,12 @@ 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 + self.tor_settings_tab = None # Define the new tab button self.new_tab_button = QtWidgets.QPushButton("+", parent=self) @@ -89,9 +94,12 @@ class TabWidget(QtWidgets.QTabWidget): self.event_handler_t.wait(50) # Clean up each tab - for index in range(self.count()): - 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 @@ -114,8 +122,28 @@ 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 ( + 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: mode = self.tabs[tab_id].get_mode() if mode: @@ -158,23 +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": - - 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 - ) - tab.init(mode_settings) # Make sure the title is set @@ -187,6 +198,44 @@ 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 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")) + self.setCurrentIndex(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 tab_id in self.tabs: + if type(self.tabs[tab_id]) is TorSettingsTab: + self.setCurrentIndex(self.indexOf(self.tabs[tab_id])) + return + + self.tor_settings_tab = TorSettingsTab( + 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( + self.tor_settings_tab, strings._("gui_tor_settings_window_title") + ) + self.setCurrentIndex(index) + def change_title(self, tab_id, title): shortened_title = title if len(shortened_title) > 11: @@ -200,6 +249,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", @@ -223,10 +277,14 @@ 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()): - tab = self.widget(index) - if tab.settings.get("persistent", "enabled"): - persistent_tabs.append(tab.settings.id) + 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 if persistent_tabs != self.common.settings.get("persistent_tabs"): self.common.settings.set("persistent_tabs", persistent_tabs) @@ -235,10 +293,16 @@ 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() + 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 type(self.tabs[tab_id]) is TorSettingsTab: + self.tor_settings_tab = None # Remove the tab self.removeTab(index) @@ -248,17 +312,56 @@ class TabWidget(QtWidgets.QTabWidget): if self.count() == 0: self.new_tab_clicked() - self.save_persistent_tabs() + 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() + + 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() + 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") + 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 """ 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 ( + 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: + return True return False def paintEvent(self, event): @@ -273,6 +376,26 @@ class TabWidget(QtWidgets.QTabWidget): super(TabWidget, self).resizeEvent(event) self.move_new_tab_button() + def tor_is_connected(self): + for tab_id in self.tabs: + 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 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): """ diff --git a/desktop/src/onionshare/tor_connection_dialog.py b/desktop/src/onionshare/tor_connection.py similarity index 51% rename from desktop/src/onionshare/tor_connection_dialog.py rename to desktop/src/onionshare/tor_connection.py index b5c2f61c..2cc599c4 100644 --- a/desktop/src/onionshare/tor_connection_dialog.py +++ b/desktop/src/onionshare/tor_connection.py @@ -48,12 +48,16 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): Connecting to Tor dialog. """ - open_settings = QtCore.Signal() + open_tor_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) @@ -105,14 +117,13 @@ 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()) 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,47 +132,160 @@ 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_tor_settings.emit() + + QtCore.QTimer.singleShot(1, alert) # Cancel connecting to Tor 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(str) + + 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) + + 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 + self.status_bar.clearMessage() + + # 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 + self.fail.emit(msg) + + class TorConnectionThread(QtCore.QThread): tor_status_update = QtCore.Signal(str, str) connected_to_tor = QtCore.Signal() 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.common.gui.onion.connect(self.settings, False, self._tor_status_update) - if self.common.gui.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() @@ -197,4 +321,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 new file mode 100644 index 00000000..e28e5260 --- /dev/null +++ b/desktop/src/onionshare/tor_settings_tab.py @@ -0,0 +1,895 @@ +# -*- 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 sys +import platform +import re +import os + +from onionshare_cli.meek import Meek +from onionshare_cli.settings import Settings +from onionshare_cli.onion import Onion + +from . import strings +from .widgets import Alert +from .tor_connection import TorConnectionWidget +from .moat_dialog import MoatDialog + + +class TorSettingsTab(QtWidgets.QWidget): + """ + Settings dialog. + """ + + 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__() + + 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() + self.tab_id = tab_id + + # 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 + + ( + 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, + ) = self.common.gui.get_tor_paths() + + bridges_label = QtWidgets.QLabel(strings._("gui_settings_tor_bridges_label")) + bridges_label.setWordWrap(True) + + self.bridge_use_checkbox = QtWidgets.QCheckBox( + strings._("gui_settings_bridge_use_checkbox") + ) + self.bridge_use_checkbox.stateChanged.connect( + self.bridge_use_checkbox_state_changed + ) + + # Built-in bridge + self.bridge_builtin_radio = QtWidgets.QRadioButton( + strings._("gui_settings_bridge_radio_builtin") + ) + 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( + 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.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) + 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) + self.bridge_moat_textbox_options.hide() + + # Custom bridges radio and textbox + 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.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) + + self.bridge_custom_textbox_options = QtWidgets.QWidget() + self.bridge_custom_textbox_options.setLayout( + bridge_custom_textbox_options_layout + ) + 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_use_checkbox) + bridges_layout.addWidget(self.bridge_settings) + + 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 + self.authenticate_no_auth_checkbox = QtWidgets.QCheckBox( + strings._("gui_settings_authenticate_no_auth_option") + ) + self.authenticate_no_auth_checkbox.toggled.connect( + self.authenticate_no_auth_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() + + # 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.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() + 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_layout.addStretch() + 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() + + # 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.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) + columns_wrapper = QtWidgets.QWidget() + columns_wrapper.setFixedHeight(400) + columns_wrapper.setLayout(columns_layout) + + # Tor connection widget + 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() + 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( + 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) + buttons_layout = QtWidgets.QHBoxLayout() + 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) + + # 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): + # Load settings, and fill them in + self.old_settings = Settings(self.common) + self.old_settings.load() + + 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_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") + ) + + if self.old_settings.get("bridges_enabled"): + self.bridge_use_checkbox.setCheckState(QtCore.Qt.Checked) + self.bridge_settings.show() + + bridges_type = self.old_settings.get("bridges_type") + if bridges_type == "built-in": + self.bridge_builtin_radio.setChecked(True) + self.bridge_builtin_dropdown.show() + 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 bridges_builtin_pt == "meek-azure": + self.bridge_builtin_dropdown.setCurrentText("meek-azure") + 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() + + 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) + + else: + 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 + """ + self.common.log("TorSettingsTab", "connection_type_bundled_toggled") + if checked: + self.tor_settings_group.hide() + self.connection_type_socks.hide() + self.connection_type_bridges_radio_group.show() + + def bridge_use_checkbox_state_changed(self, state): + """ + '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_builtin_dropdown_changed(self, selection): + """ + Build-in bridge selection changed + """ + 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("bridges_builtin_pt") == "meek-azure": + Alert( + self.common, + strings._("gui_settings_meek_lite_expensive_warning"), + QtWidgets.QMessageBox.Warning, + ) + + 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() + + def bridge_moat_button_clicked(self): + """ + Request new bridge button clicked + """ + self.common.log("TorSettingsTab", "bridge_moat_button_clicked") + + moat_dialog = MoatDialog(self.common, self.meek) + 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("TorSettingsTab", "bridge_moat_got_bridges") + self.bridge_moat_textbox.document().setPlainText(bridges) + self.bridge_moat_textbox.show() + + def bridge_custom_radio_toggled(self, checked): + """ + Custom bridges option was toggled. If checked, show custom bridge options. + """ + if checked: + self.bridge_builtin_dropdown.hide() + self.bridge_moat_textbox_options.hide() + self.bridge_custom_textbox_options.show() + + def connection_type_automatic_toggled(self, checked): + """ + Connection type automatic was toggled. If checked, hide authentication fields. + """ + self.common.log("TorSettingsTab", "connection_type_automatic_toggled") + if checked: + self.tor_settings_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("TorSettingsTab", "connection_type_control_port_toggled") + if checked: + self.tor_settings_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("TorSettingsTab", "connection_type_socket_file_toggled") + if checked: + self.tor_settings_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("TorSettingsTab", "authenticate_no_auth_toggled") + if checked: + self.authenticate_password_extras.hide() + else: + self.authenticate_password_extras.show() + + 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("TorSettingsTab", "test_tor_clicked") + + self.error_label.setText("") + + settings = self.settings_from_fields() + if not settings: + return + + self.test_tor_button.hide() + self.save_button.hide() + + 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.start(settings, True, self.test_onion) + + def save_clicked(self): + """ + Save button clicked. Save current settings to disk. + """ + 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 + 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: + # 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( + "TorSettingsTab", "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", + "bridges_enabled", + "bridges_type", + "bridges_builtin_pt", + "bridges_moat", + "bridges_custom", + ], + ): + + reboot_onion = True + + else: + self.common.log( + "TorSettingsTab", "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: + # 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" + ) + self.common.gui.onion.cleanup() + + 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() + ): + # 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 + + 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 settings_from_fields(self): + """ + Return a Settings object that's full of values from the settings dialog. + """ + self.common.log("TorSettingsTab", "settings_from_fields") + settings = Settings(self.common) + settings.load() # To get the last update timestamp + + # 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") + + 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_checkbox.checkState() == QtCore.Qt.Checked: + settings.set("auth_type", "no_auth") + else: + settings.set("auth_type", "password") + + settings.set("auth_password", self.authenticate_password_extras_password.text()) + + # Whether we use bridges + if self.bridge_use_checkbox.checkState() == QtCore.Qt.Checked: + settings.set("bridges_enabled", True) + + if self.bridge_builtin_radio.isChecked(): + settings.set("bridges_type", "built-in") + + selection = self.bridge_builtin_dropdown.currentText() + settings.set("bridges_builtin_pt", selection) + + if self.bridge_moat_radio.isChecked(): + settings.set("bridges_type", "moat") + moat_bridges = self.bridge_moat_textbox.toPlainText() + if ( + self.connection_type_bundled_radio.isChecked() + and moat_bridges.strip() == "" + ): + self.error_label.setText( + strings._("gui_settings_moat_bridges_invalid") + ) + return False + + settings.set("bridges_moat", moat_bridges) + + if self.bridge_custom_radio.isChecked(): + settings.set("bridges_type", "custom") + + 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=(.+)" + ) + 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 + + if bridges_valid: + new_bridges = "\n".join(new_bridges) + "\n" + settings.set("bridges_custom", new_bridges) + else: + self.error_label.setText( + strings._("gui_settings_tor_bridges_invalid") + ) + return False + else: + settings.set("bridges_enabled", False) + + return settings + + def closeEvent(self, e): + 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( + "TorSettingsTab", + "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 settings_have_changed(self): + # Global settings have changed + self.common.log("TorSettingsTab", "settings_have_changed") 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)