diff options
author | Miguel Jacq <mig@mig5.net> | 2021-11-12 10:56:15 +1100 |
---|---|---|
committer | Miguel Jacq <mig@mig5.net> | 2021-11-12 10:56:15 +1100 |
commit | dbae142a873c0bb326d0b6fa9ab3a4872280fe9b (patch) | |
tree | a2563db6480eb0d39d32ce6518147b57a5aafaa1 | |
parent | 627c185fcb3c369f291b285910421d9cdcbf2f86 (diff) | |
parent | da94b92d1807fd2126f748836c79bcbf38a4c0a4 (diff) | |
download | onionshare-dbae142a873c0bb326d0b6fa9ab3a4872280fe9b.tar.gz onionshare-dbae142a873c0bb326d0b6fa9ab3a4872280fe9b.zip |
Resolve conflicts in locale
38 files changed, 3101 insertions, 1590 deletions
diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index 4041c5ea..4155a32a 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", @@ -308,6 +300,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) @@ -424,7 +430,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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +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("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") + if self.settings.get("bridges_enabled"): + if self.settings.get("bridges_type") == "built-in": + if self.settings.get("bridges_builtin_pt") == "obfs4": + with open( + self.common.get_resource_path("torrc_template-obfs4") + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "meek-azure": + with open( + self.common.get_resource_path( + "torrc_template-meek_lite_azure" + ) + ) as o: + f.write(o.read()) + elif self.settings.get("bridges_builtin_pt") == "snowflake": + with open( + self.common.get_resource_path( + "torrc_template-snowflake" + ) + ) as o: + f.write(o.read()) + + elif self.settings.get("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" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] -category = "main" -description = "Composable command line interface toolkit" name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" 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" +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.*" -version = "0.4.4" [[package]] -category = "main" -description = "DNS toolkit" name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.16.0" +python-versions = ">=3.6" [package.extras] -DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] -IDNA = ["idna (>=2.1)"] +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]] -category = "main" -description = "Highly concurrent networking library" 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" +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.*" -version = "1.10.0" [[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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" + +""" +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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" + +""" +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": """ - 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": """ + # Tor Settings dialogs + "tor_settings_error": """ 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 <http://www.gnu.org/licenses/>. """ +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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" + +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 Binary files differnew file mode 100644 index 00000000..0b44bd95 --- /dev/null +++ b/desktop/src/onionshare/resources/images/dark_tor_settings.png diff --git a/desktop/src/onionshare/resources/images/light_tor_settings.png b/desktop/src/onionshare/resources/images/light_tor_settings.png Binary files differnew file mode 100644 index 00000000..e8db08eb --- /dev/null +++ b/desktop/src/onionshare/resources/images/light_tor_settings.png diff --git a/desktop/src/onionshare/resources/locale/en.json b/desktop/src/onionshare/resources/locale/en.json index 437a3eda..db5fbfe5 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.<br><br>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 <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>", - "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.<br><br>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 <a href='https://docs.onionshare.org'>docs.onionshare.org</a>", "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. <a href='{}'>Click here</a> to get it.<br><br>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.<br><br><b>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.</b>", "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", @@ -197,7 +203,7 @@ "mode_settings_receive_disable_files_checkbox": "Disable uploading files", "mode_settings_receive_webhook_url_checkbox": "Use notification webhook", "mode_settings_website_disable_csp_checkbox": "Don't send default Content Security Policy header (allows your website to use third-party resources)", - "mode_settings_website_custom_csp_checkbox": "Send a Custom Content Security Policy header", + "mode_settings_website_custom_csp_checkbox": "Send a custom Content Security Policy header", "gui_all_modes_transfer_finished_range": "Transferred {} - {}", "gui_all_modes_transfer_finished": "Transferred {}", "gui_all_modes_transfer_canceled_range": "Canceled {} - {}", @@ -216,5 +222,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" } 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. <micah@micahflee.com> - -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 <http://www.gnu.org/licenses/>. -""" - -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"<strong>{strings._('connecting_to_tor')}</strong><br>{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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" + +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 6d1df88c..0acbc1a2 100644 --- a/desktop/src/onionshare/tab/mode/website_mode/__init__.py +++ b/desktop/src/onionshare/tab/mode/website_mode/__init__.py @@ -188,10 +188,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 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, + ) + + # Open settings + self.open_tor_settings.emit() - QtCore.QTimer.singleShot(1, alert_and_open_settings) + 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"<strong>{strings._('connecting_to_tor')}</strong><br>{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. <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" + +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) |