Merge branch 'develop' into advisory-fix-1

This commit is contained in:
Micah Lee 2021-11-11 15:32:54 -08:00
commit dc92a1a4b7
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
38 changed files with 3113 additions and 1602 deletions

View File

@ -27,13 +27,10 @@ from datetime import datetime
from datetime import timedelta
from .common import Common, CannotFindTor
from .censorship import CensorshipCircumvention
from .meek import Meek, MeekNotRunning
from .web import Web
from .onion import (
TorErrorProtocolError,
TorTooOldEphemeral,
TorTooOldStealth,
Onion,
)
from .onion import TorErrorProtocolError, TorTooOldEphemeral, TorTooOldStealth, Onion
from .onionshare import OnionShare
from .mode_settings import ModeSettings
@ -94,12 +91,7 @@ def main(cwd=None):
help="Filename of persistent session",
)
# General args
parser.add_argument(
"--title",
metavar="TITLE",
default=None,
help="Set a title",
)
parser.add_argument("--title", metavar="TITLE", default=None, help="Set a title")
parser.add_argument(
"--public",
action="store_true",
@ -293,6 +285,20 @@ def main(cwd=None):
# Create the Web object
web = Web(common, False, mode_settings, mode)
# Create the Meek object and start the meek client
# meek = Meek(common)
# meek.start()
# Create the CensorshipCircumvention object to make
# API calls to Tor over Meek
censorship = CensorshipCircumvention(common, meek)
# Example: request recommended bridges, pretending to be from China, using
# domain fronting.
# censorship_recommended_settings = censorship.request_settings(country="cn")
# print(censorship_recommended_settings)
# Clean up the meek subprocess once we're done working with the censorship circumvention API
# meek.cleanup()
# Start the Onion object
try:
onion = Onion(common, use_tmp_dir=True)
@ -409,7 +415,7 @@ def main(cwd=None):
sys.exit(1)
# Warn about sending large files over Tor
if web.share_mode.download_filesize >= 157286400: # 150mb
if web.share_mode.download_filesize >= 157_286_400: # 150mb
print("")
print("Warning: Sending a large share could take hours")
print("")

View File

@ -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

View File

@ -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):

210
cli/onionshare_cli/meek.py Normal file
View File

@ -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.
"""

View File

@ -153,6 +153,8 @@ class Onion(object):
self.tor_geo_ip_file_path,
self.tor_geo_ipv6_file_path,
self.obfs4proxy_file_path,
self.snowflake_file_path,
self.meek_client_file_path,
) = get_tor_paths()
# The tor process
@ -178,10 +180,10 @@ class Onion(object):
key_bytes = bytes(key)
key_b32 = base64.b32encode(key_bytes)
# strip trailing ====
assert key_b32[-4:] == b'===='
assert key_b32[-4:] == b"===="
key_b32 = key_b32[:-4]
# change from b'ASDF' to ASDF
s = key_b32.decode('utf-8')
s = key_b32.decode("utf-8")
return s
def connect(
@ -198,8 +200,6 @@ class Onion(object):
)
return
self.common.log("Onion", "connect")
# Either use settings that are passed in, or use them from common
if custom_settings:
self.settings = custom_settings
@ -210,6 +210,12 @@ class Onion(object):
self.common.load_settings()
self.settings = self.common.settings
self.common.log(
"Onion",
"connect",
f"connection_type={self.settings.get('connection_type')}",
)
# The Tor controller
self.c = None
@ -302,43 +308,50 @@ class Onion(object):
torrc_template = torrc_template.replace(
"{{socks_port}}", str(self.tor_socks_port)
)
torrc_template = torrc_template.replace(
"{{obfs4proxy_path}}", str(self.obfs4proxy_file_path)
)
torrc_template = torrc_template.replace(
"{{snowflake_path}}", str(self.snowflake_file_path)
)
with open(self.tor_torrc, "w") as f:
f.write(torrc_template)
# Bridge support
if self.settings.get("tor_bridges_use_obfs4"):
f.write(
f"ClientTransportPlugin obfs4 exec {self.obfs4proxy_file_path}\n"
)
with open(
self.common.get_resource_path("torrc_template-obfs4")
) as o:
for line in o:
f.write(line)
elif self.settings.get("tor_bridges_use_meek_lite_azure"):
f.write(
f"ClientTransportPlugin meek_lite exec {self.obfs4proxy_file_path}\n"
)
with open(
self.common.get_resource_path("torrc_template-meek_lite_azure")
) as o:
for line in o:
f.write(line)
if self.settings.get("bridges_enabled"):
if self.settings.get("bridges_type") == "built-in":
if self.settings.get("bridges_builtin_pt") == "obfs4":
with open(
self.common.get_resource_path("torrc_template-obfs4")
) as o:
f.write(o.read())
elif self.settings.get("bridges_builtin_pt") == "meek-azure":
with open(
self.common.get_resource_path(
"torrc_template-meek_lite_azure"
)
) as o:
f.write(o.read())
elif self.settings.get("bridges_builtin_pt") == "snowflake":
with open(
self.common.get_resource_path(
"torrc_template-snowflake"
)
) as o:
f.write(o.read())
if self.settings.get("tor_bridges_use_custom_bridges"):
if "obfs4" in self.settings.get("tor_bridges_use_custom_bridges"):
f.write(
f"ClientTransportPlugin obfs4 exec {self.obfs4proxy_file_path}\n"
)
elif "meek_lite" in self.settings.get(
"tor_bridges_use_custom_bridges"
):
f.write(
f"ClientTransportPlugin meek_lite exec {self.obfs4proxy_file_path}\n"
)
f.write(self.settings.get("tor_bridges_use_custom_bridges"))
f.write("\nUseBridges 1")
elif self.settings.get("bridges_type") == "moat":
for line in self.settings.get("bridges_moat").split("\n"):
if line.strip() != "":
f.write(f"Bridge {line}\n")
f.write("\nUseBridges 1\n")
elif self.settings.get("bridges_type") == "custom":
for line in self.settings.get("bridges_custom").split("\n"):
if line.strip() != "":
f.write(f"Bridge {line}\n")
f.write("\nUseBridges 1\n")
# Execute a tor subprocess
start_ts = time.time()
@ -357,6 +370,7 @@ class Onion(object):
[self.tor_path, "-f", self.tor_torrc],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={"LD_LIBRARY_PATH": os.path.dirname(self.tor_path)},
)
# Wait for the tor controller to start
@ -410,11 +424,7 @@ class Onion(object):
time.sleep(0.2)
# If using bridges, it might take a bit longer to connect to Tor
if (
self.settings.get("tor_bridges_use_custom_bridges")
or self.settings.get("tor_bridges_use_obfs4")
or self.settings.get("tor_bridges_use_meek_lite_azure")
):
if self.settings.get("bridges_enabled"):
# Only override timeout if a custom timeout has not been passed in
if connect_timeout == 120:
connect_timeout = 150
@ -650,16 +660,24 @@ class Onion(object):
)
raise TorTooOldStealth()
else:
if key_type == "NEW" or not mode_settings.get("onion", "client_auth_priv_key"):
if key_type == "NEW" or not mode_settings.get(
"onion", "client_auth_priv_key"
):
# Generate a new key pair for Client Auth on new onions, or if
# it's a persistent onion but for some reason we don't them
client_auth_priv_key_raw = nacl.public.PrivateKey.generate()
client_auth_priv_key = self.key_str(client_auth_priv_key_raw)
client_auth_pub_key = self.key_str(client_auth_priv_key_raw.public_key)
client_auth_pub_key = self.key_str(
client_auth_priv_key_raw.public_key
)
else:
# These should have been saved in settings from the previous run of a persistent onion
client_auth_priv_key = mode_settings.get("onion", "client_auth_priv_key")
client_auth_pub_key = mode_settings.get("onion", "client_auth_pub_key")
client_auth_priv_key = mode_settings.get(
"onion", "client_auth_priv_key"
)
client_auth_pub_key = mode_settings.get(
"onion", "client_auth_pub_key"
)
try:
if not self.supports_stealth:

View File

@ -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

View File

@ -1,2 +0,0 @@
Bridge meek_lite 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com
UseBridges 1

View File

@ -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
UseBridges 1

View File

@ -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

View File

@ -0,0 +1,3 @@
# Enable built-in snowflake bridge
Bridge snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72
UseBridges 1

View File

@ -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,

536
cli/poetry.lock generated
View File

@ -1,122 +1,120 @@
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
version = "21.2.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "21.2.0"
[package.extras]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
category = "main"
description = "The bidirectional mapping library for Python."
name = "bidict"
version = "0.21.3"
description = "The bidirectional mapping library for Python."
category = "main"
optional = false
python-versions = ">=3.6"
version = "0.21.2"
[package.extras]
coverage = ["coverage (<6)", "pytest-cov (<3)"]
dev = ["setuptools-scm", "hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)", "coverage (<6)", "pytest-cov (<3)", "pre-commit (<3)", "tox (<4)"]
docs = ["Sphinx (<4)", "sphinx-autodoc-typehints (<2)"]
precommit = ["pre-commit (<3)"]
test = ["hypothesis (<6)", "py (<2)", "pytest (<7)", "pytest-benchmark (>=3.2.0,<4)", "sortedcollections (<2)", "sortedcontainers (<3)", "Sphinx (<4)", "sphinx-autodoc-typehints (<2)"]
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
version = "2021.5.30"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
version = "1.14.6"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
version = "1.14.6"
[package.dependencies]
pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "4.0.0"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.2"
[[package]]
category = "main"
description = "Cross-platform colored terminal text."
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.4"
[[package]]
category = "main"
description = "DNS toolkit"
name = "dnspython"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.16.0"
python-versions = ">=3.5.0"
[package.extras]
DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"]
IDNA = ["idna (>=2.1)"]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "main"
description = "Highly concurrent networking library"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dnspython"
version = "2.1.0"
description = "DNS toolkit"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
dnssec = ["cryptography (>=2.6)"]
doh = ["requests", "requests-toolbelt"]
idna = ["idna (>=2.1)"]
curio = ["curio (>=1.2)", "sniffio (>=1.1)"]
trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"]
[[package]]
name = "eventlet"
version = "0.32.0"
description = "Highly concurrent networking library"
category = "main"
optional = false
python-versions = "*"
version = "0.31.0"
[package.dependencies]
dnspython = ">=1.15.0,<2.0.0"
dnspython = ">=1.15.0"
greenlet = ">=0.3"
six = ">=1.10.0"
[[package]]
category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
version = "1.1.4"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.4"
[package.dependencies]
Jinja2 = ">=2.10.1,<3.0"
Werkzeug = ">=0.15,<2.0"
click = ">=5.1,<8.0"
itsdangerous = ">=0.24,<2.0"
Jinja2 = ">=2.10.1,<3.0"
Werkzeug = ">=0.15,<2.0"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
@ -124,79 +122,76 @@ docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-
dotenv = ["python-dotenv"]
[[package]]
category = "main"
description = "Socket.IO integration for Flask applications"
name = "flask-socketio"
version = "5.0.1"
description = "Socket.IO integration for Flask applications"
category = "main"
optional = false
python-versions = "*"
version = "5.0.1"
[package.dependencies]
Flask = ">=0.9"
python-socketio = ">=5.0.2"
[[package]]
category = "main"
description = "Lightweight in-process concurrent programming"
name = "greenlet"
version = "1.1.2"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
version = "1.1.0"
[package.extras]
docs = ["sphinx"]
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.10"
python-versions = ">=3.5"
[[package]]
category = "dev"
description = "Read metadata from Python packages"
marker = "python_version < \"3.8\""
name = "importlib-metadata"
version = "4.8.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
version = "4.4.0"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.dependencies.typing-extensions]
python = "<3.8"
version = ">=3.6.4"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
perf = ["ipython"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
category = "dev"
description = "iniconfig: brain-dead simple config-ini parsing"
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
version = "1.1.1"
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
version = "1.1.0"
description = "Various helpers to pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
version = "2.11.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.3"
[package.dependencies]
MarkupSafe = ">=0.23"
@ -205,74 +200,73 @@ MarkupSafe = ">=0.23"
i18n = ["Babel (>=0.8)"]
[[package]]
category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
version = "2.0.1"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
version = "21.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.9"
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
python-versions = ">=3.6"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
category = "main"
description = "Cross-platform lib for process and system monitoring in Python."
name = "psutil"
version = "5.8.0"
description = "Cross-platform lib for process and system monitoring in Python."
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.8.0"
[package.extras]
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
category = "main"
description = "C parser in Python"
name = "pycparser"
version = "2.20"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]]
category = "main"
description = "Python binding to the Networking and Cryptography (NaCl) library"
name = "pynacl"
version = "1.4.0"
description = "Python binding to the Networking and Cryptography (NaCl) library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[package.dependencies]
cffi = ">=1.4.1"
@ -280,186 +274,181 @@ six = "*"
[package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)", "hypothesis (>=3.27.0)"]
tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"]
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "main"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
name = "pysocks"
version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.7.1"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
version = "6.2.4"
[package.dependencies]
atomicwrites = ">=1.0"
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = "*"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "main"
description = "Engine.IO server"
name = "python-engineio"
version = "4.2.1"
description = "Engine.IO server and client for Python"
category = "main"
optional = false
python-versions = "*"
version = "4.2.0"
python-versions = ">=3.6"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
category = "main"
description = "Socket.IO server"
name = "python-socketio"
version = "5.4.0"
description = "Socket.IO server and client for Python"
category = "main"
optional = false
python-versions = "*"
version = "5.3.0"
python-versions = ">=3.6"
[package.dependencies]
bidict = ">=0.21.0"
python-engineio = ">=4.1.0"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"]
asyncio_client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
version = "2.26.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.25.1"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""}
urllib3 = ">=1.21.1,<1.27"
[package.dependencies.PySocks]
optional = true
version = ">=1.5.6,<1.5.7 || >1.5.7"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.16.0"
[[package]]
category = "main"
description = ""
name = "stem"
version = "1.8.1"
description = "Stem is a Python controller library that allows applications to interact with Tor (https://www.torproject.org/)."
category = "main"
optional = false
python-versions = "*"
version = "1.8.1"
develop = false
[package.source]
reference = "de3d03a03c7ee57c74c80e9c63cb88072d833717"
type = "git"
url = "https://github.com/onionshare/stem.git"
reference = "1.8.1"
resolved_reference = "de3d03a03c7ee57c74c80e9c63cb88072d833717"
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.10.2"
[[package]]
category = "dev"
description = "Backported and Experimental Type Hints for Python 3.5+"
marker = "python_version < \"3.8\""
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
version = "3.10.0.0"
[[package]]
category = "main"
description = "ASCII transliterations of Unicode text"
name = "unidecode"
version = "1.3.2"
description = "ASCII transliterations of Unicode text"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.2.0"
python-versions = ">=3.5"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.26.5"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
version = "1.0.1"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.1"
[package.extras]
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"]
[[package]]
category = "dev"
description = "Backport of pathlib-compatible object wrapper for zip files"
marker = "python_version < \"3.8\""
name = "zipp"
version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.6"
version = "3.4.1"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata]
content-hash = "181891640e59dac730905019444d42ef8e99da0c34c96fb8a616781661bae537"
lock-version = "1.1"
python-versions = "^3.6"
content-hash = "181891640e59dac730905019444d42ef8e99da0c34c96fb8a616781661bae537"
[metadata.files]
atomicwrites = [
@ -471,12 +460,12 @@ attrs = [
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
bidict = [
{file = "bidict-0.21.2-py2.py3-none-any.whl", hash = "sha256:929d056e8d0d9b17ceda20ba5b24ac388e2a4d39802b87f9f4d3f45ecba070bf"},
{file = "bidict-0.21.2.tar.gz", hash = "sha256:4fa46f7ff96dc244abfc437383d987404ae861df797e2fd5b190e233c302be09"},
{file = "bidict-0.21.3-py3-none-any.whl", hash = "sha256:2cce0d01eb3db9b3fa85db501c00aaa3389ee4cab7ef82178604552dfa943a1b"},
{file = "bidict-0.21.3.tar.gz", hash = "sha256:d50bd81fae75e34198ffc94979a0eb0939ff9adb3ef32bcc93a913d8b3e3ed1d"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
cffi = [
{file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
@ -525,9 +514,9 @@ cffi = [
{file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
{file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
charset-normalizer = [
{file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
{file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@ -538,12 +527,12 @@ colorama = [
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dnspython = [
{file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"},
{file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"},
{file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"},
{file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"},
]
eventlet = [
{file = "eventlet-0.31.0-py2.py3-none-any.whl", hash = "sha256:27ae41fad9deed9bbf4166f3e3b65acc15d524d42210a518e5877da85a6b8c5d"},
{file = "eventlet-0.31.0.tar.gz", hash = "sha256:b36ec2ecc003de87fc87b93197d77fea528aa0f9204a34fdf3b2f8d0f01e017b"},
{file = "eventlet-0.32.0-py2.py3-none-any.whl", hash = "sha256:a3a67b02f336e97a1894b277bc33b695831525758781eb024f4da00e75ce5e25"},
{file = "eventlet-0.32.0.tar.gz", hash = "sha256:2f0bb8ed0dc0ab21d683975d5d8ab3c054d588ce61def9faf7a465ee363e839b"},
]
flask = [
{file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"},
@ -554,63 +543,64 @@ flask-socketio = [
{file = "Flask_SocketIO-5.0.1-py2.py3-none-any.whl", hash = "sha256:5d9a4438bafd806c5a3b832e74b69758781a8ee26fb6c9b1dbdda9b4fced432e"},
]
greenlet = [
{file = "greenlet-1.1.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3"},
{file = "greenlet-1.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922"},
{file = "greenlet-1.1.0-cp27-cp27m-win32.whl", hash = "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821"},
{file = "greenlet-1.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f"},
{file = "greenlet-1.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56"},
{file = "greenlet-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831"},
{file = "greenlet-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22"},
{file = "greenlet-1.1.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47"},
{file = "greenlet-1.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08"},
{file = "greenlet-1.1.0-cp35-cp35m-win32.whl", hash = "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131"},
{file = "greenlet-1.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5"},
{file = "greenlet-1.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919"},
{file = "greenlet-1.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e"},
{file = "greenlet-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8"},
{file = "greenlet-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb"},
{file = "greenlet-1.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319"},
{file = "greenlet-1.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05"},
{file = "greenlet-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f"},
{file = "greenlet-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a"},
{file = "greenlet-1.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99"},
{file = "greenlet-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da"},
{file = "greenlet-1.1.0-cp38-cp38-win32.whl", hash = "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad"},
{file = "greenlet-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8"},
{file = "greenlet-1.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"},
{file = "greenlet-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832"},
{file = "greenlet-1.1.0-cp39-cp39-win32.whl", hash = "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11"},
{file = "greenlet-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535"},
{file = "greenlet-1.1.0.tar.gz", hash = "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee"},
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
{file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"},
{file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"},
{file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"},
{file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"},
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"},
{file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"},
{file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"},
{file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"},
{file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"},
{file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"},
{file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"},
{file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"},
{file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"},
{file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"},
{file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"},
{file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"},
{file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"},
{file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"},
{file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"},
{file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"},
{file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"},
{file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"},
{file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"},
{file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"},
{file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"},
{file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"},
{file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"},
{file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"},
{file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"},
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
importlib-metadata = [
{file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"},
{file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"},
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -681,12 +671,12 @@ markupsafe = [
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
psutil = [
{file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
@ -756,20 +746,20 @@ pysocks = [
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
]
pytest = [
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
python-engineio = [
{file = "python-engineio-4.2.0.tar.gz", hash = "sha256:4e97c1189c23923858f5bb6dc47cfcd915005383c3c039ff01c89f2c00d62077"},
{file = "python_engineio-4.2.0-py2.py3-none-any.whl", hash = "sha256:c6c119c2039fcb6f64d260211ca92c0c61b2b888a28678732a961f2aaebcc848"},
{file = "python-engineio-4.2.1.tar.gz", hash = "sha256:d510329b6d8ed5662547862f58bc73659ae62defa66b66d745ba021de112fa62"},
{file = "python_engineio-4.2.1-py3-none-any.whl", hash = "sha256:f3ef9a2c048d08990f294c5f8991f6f162c3b12ecbd368baa0d90441de907d1c"},
]
python-socketio = [
{file = "python-socketio-5.3.0.tar.gz", hash = "sha256:3dcc9785aaeef3a9eeb36c3818095662342744bdcdabd050fe697cdb826a1c2b"},
{file = "python_socketio-5.3.0-py2.py3-none-any.whl", hash = "sha256:d74314fd4241342c8a55c4f66d5cfea8f1a8fffd157af216c67e1c3a649a2444"},
{file = "python-socketio-5.4.0.tar.gz", hash = "sha256:ca807c9e1f168e96dea412d64dd834fb47c470d27fd83da0504aa4b248ba2544"},
{file = "python_socketio-5.4.0-py3-none-any.whl", hash = "sha256:7ed57f6c024abdfeb9b25c74c0c00ffc18da47d903e8d72deecb87584370d1fc"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@ -781,23 +771,23 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
unidecode = [
{file = "Unidecode-1.2.0-py2.py3-none-any.whl", hash = "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00"},
{file = "Unidecode-1.2.0.tar.gz", hash = "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"},
{file = "Unidecode-1.3.2-py3-none-any.whl", hash = "sha256:215fe33c9d1c889fa823ccb66df91b02524eb8cc8c9c80f9c5b8129754d27829"},
{file = "Unidecode-1.3.2.tar.gz", hash = "sha256:669898c1528912bcf07f9819dc60df18d057f7528271e31f8ec28cc88ef27504"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
]
zipp = [
{file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
{file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
]

View File

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

View File

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

View File

@ -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/).

View File

@ -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()

131
desktop/scripts/get-tor-linux.py Executable file
View File

@ -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()

View File

@ -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"])

View File

@ -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(

View File

@ -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__":

View File

@ -93,6 +93,7 @@ class GuiCommon:
share_zip_progess_bar_chunk_color = "#4E064F"
history_background_color = "#ffffff"
history_label_color = "#000000"
settings_error_color = "#FF0000"
if color_mode == "dark":
header_color = "#F2F2F2"
title_color = "#F2F2F2"
@ -103,6 +104,7 @@ class GuiCommon:
share_zip_progess_bar_border_color = "#F2F2F2"
history_background_color = "#191919"
history_label_color = "#ffffff"
settings_error_color = "#FF9999"
return {
# OnionShareGui styles
@ -205,14 +207,14 @@ class GuiCommon:
"downloads_uploads_not_empty": """
QWidget{
background-color: """
+ history_background_color
+""";
+ history_background_color
+ """;
}""",
"downloads_uploads_empty": """
QWidget {
background-color: """
+ history_background_color
+""";
+ history_background_color
+ """;
border: 1px solid #999999;
}
QWidget QLabel {
@ -263,7 +265,7 @@ class GuiCommon:
+ """;
width: 10px;
}""",
"history_default_label" : """
"history_default_label": """
QLabel {
color: """
+ history_label_color
@ -281,6 +283,11 @@ class GuiCommon:
QLabel {
color: #cc0000;
}""",
"tor_not_connected_label": """
QLabel {
font-size: 16px;
font-style: italic;
}""",
# New tab
"new_tab_button_image": """
QLabel {
@ -392,44 +399,50 @@ class GuiCommon:
QPushButton {
padding: 5px 10px;
}""",
# Settings dialog
"settings_version": """
# Tor Settings dialogs
"tor_settings_error": """
QLabel {
color: #666666;
}""",
"settings_tor_status": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
}""",
"settings_whats_this": """
QLabel {
font-size: 12px;
}""",
"settings_connect_to_tor": """
QLabel {
font-style: italic;
}""",
color: """
+ settings_error_color
+ """;
}
""",
}
def get_tor_paths(self):
if self.common.platform == "Linux":
tor_path = shutil.which("tor")
obfs4proxy_file_path = shutil.which("obfs4proxy")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
elif self.common.platform == "Windows":
base_path = self.get_resource_path("tor")
if os.path.exists(base_path):
tor_path = os.path.join(base_path, "tor")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
else:
# Fallback to looking in the path
tor_path = shutil.which("tor")
obfs4proxy_file_path = shutil.which("obfs4proxy")
snowflake_file_path = shutil.which("snowflake-client")
meek_client_file_path = shutil.which("meek-client")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
if self.common.platform == "Windows":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "Tor", "tor.exe")
obfs4proxy_file_path = os.path.join(base_path, "Tor", "obfs4proxy.exe")
snowflake_file_path = os.path.join(base_path, "Tor", "snowflake-client.exe")
meek_client_file_path = os.path.join(base_path, "Tor", "meek-client.exe")
tor_geo_ip_file_path = os.path.join(base_path, "Data", "Tor", "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "Data", "Tor", "geoip6")
elif self.common.platform == "Darwin":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "tor")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
elif self.common.platform == "BSD":
@ -437,12 +450,16 @@ class GuiCommon:
tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
meek_client_file_path = "/usr/local/bin/meek-client"
snowflake_file_path = "/usr/local/bin/snowflake-client"
return (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
snowflake_file_path,
meek_client_file_path,
)
@staticmethod

View File

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

View File

@ -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}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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",
@ -215,5 +221,14 @@
"gui_rendezvous_cleanup_quit_early": "Quit Early",
"error_port_not_available": "OnionShare port not available",
"history_receive_read_message_button": "Read Message",
"error_tor_protocol_error": "There was an error with Tor: {}"
"error_tor_protocol_error": "There was an error with Tor: {}",
"moat_contact_label": "Contacting BridgeDB...",
"moat_captcha_label": "Solve the CAPTCHA to request a bridge.",
"moat_captcha_placeholder": "Enter the characters from the image",
"moat_captcha_submit": "Submit",
"moat_captcha_reload": "Reload",
"moat_bridgedb_error": "Error contacting BridgeDB.",
"moat_captcha_error": "The solution is not correct. Please try again.",
"moat_solution_empty_error": "You must enter the characters from the image",
"mode_tor_not_connected_label": "OnionShare is not connected to the Tor network"
}

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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()

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -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()

View File

@ -167,10 +167,8 @@ class WebsiteMode(Mode):
self.column_layout.addLayout(self.main_layout)
self.column_layout.addWidget(self.history, stretch=1)
# Wrapper layout
self.wrapper_layout = QtWidgets.QVBoxLayout()
self.wrapper_layout.addLayout(self.column_layout)
self.setLayout(self.wrapper_layout)
# Content layout
self.content_layout.addLayout(self.column_layout)
# Always start with focus on file selection
self.file_selection.setFocus()

View File

@ -96,7 +96,6 @@ class Tab(QtWidgets.QWidget):
tab_id,
system_tray,
status_bar,
mode_settings=None,
filenames=None,
):
super(Tab, self).__init__()

View File

@ -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):
"""

View File

@ -48,12 +48,16 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
Connecting to Tor dialog.
"""
open_settings = QtCore.Signal()
open_tor_settings = QtCore.Signal()
success = QtCore.Signal()
def __init__(self, common, custom_settings=False):
def __init__(
self, common, custom_settings=False, testing_settings=False, onion=None
):
super(TorConnectionDialog, self).__init__(None)
self.common = common
self.testing_settings = testing_settings
if custom_settings:
self.settings = custom_settings
@ -62,7 +66,15 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.common.log("TorConnectionDialog", "__init__")
self.setWindowTitle("OnionShare")
if self.testing_settings:
self.title = strings._("gui_settings_connection_type_test_button")
self.onion = onion
else:
self.title = "OnionShare"
self.onion = self.common.gui.onion
self.setWindowTitle(self.title)
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
self.setModal(True)
self.setFixedSize(400, 150)
@ -105,14 +117,13 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
def _connected_to_tor(self):
self.common.log("TorConnectionDialog", "_connected_to_tor")
self.active = False
# Close the dialog after connecting
self.setValue(self.maximum())
def _canceled_connecting_to_tor(self):
self.common.log("TorConnectionDialog", "_canceled_connecting_to_tor")
self.active = False
self.common.gui.onion.cleanup()
self.onion.cleanup()
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
@ -121,47 +132,160 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.common.log("TorConnectionDialog", "_error_connecting_to_tor")
self.active = False
def alert_and_open_settings():
# Display the exception in an alert box
Alert(
self.common,
f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}",
QtWidgets.QMessageBox.Warning,
)
if self.testing_settings:
# If testing, just display the error but don't open settings
def alert():
Alert(self.common, msg, QtWidgets.QMessageBox.Warning, title=self.title)
# Open settings
self.open_settings.emit()
else:
# If not testing, open settings after displaying the error
def alert():
Alert(
self.common,
f"{msg}\n\n{strings._('gui_tor_connection_error_settings')}",
QtWidgets.QMessageBox.Warning,
title=self.title,
)
QtCore.QTimer.singleShot(1, alert_and_open_settings)
# Open settings
self.open_tor_settings.emit()
QtCore.QTimer.singleShot(1, alert)
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
class TorConnectionWidget(QtWidgets.QWidget):
"""
Connecting to Tor widget, with a progress bar
"""
open_tor_settings = QtCore.Signal()
success = QtCore.Signal()
fail = QtCore.Signal(str)
def __init__(self, common, status_bar):
super(TorConnectionWidget, self).__init__(None)
self.common = common
self.common.log("TorConnectionWidget", "__init__")
self.status_bar = status_bar
self.label = QtWidgets.QLabel(strings._("connecting_to_tor"))
self.label.setAlignment(QtCore.Qt.AlignHCenter)
self.progress = QtWidgets.QProgressBar()
self.progress.setRange(0, 100)
self.cancel_button = QtWidgets.QPushButton(
strings._("gui_settings_button_cancel")
)
self.cancel_button.clicked.connect(self.cancel_clicked)
progress_layout = QtWidgets.QHBoxLayout()
progress_layout.addWidget(self.progress)
progress_layout.addWidget(self.cancel_button)
inner_layout = QtWidgets.QVBoxLayout()
inner_layout.addWidget(self.label)
inner_layout.addLayout(progress_layout)
layout = QtWidgets.QHBoxLayout()
layout.addStretch()
layout.addLayout(inner_layout)
layout.addStretch()
self.setLayout(layout)
# Start displaying the status at 0
self._tor_status_update(0, "")
def start(self, custom_settings=False, testing_settings=False, onion=None):
self.common.log("TorConnectionWidget", "start")
self.was_canceled = False
self.testing_settings = testing_settings
if custom_settings:
self.settings = custom_settings
else:
self.settings = self.common.settings
if self.testing_settings:
self.onion = onion
else:
self.onion = self.common.gui.onion
t = TorConnectionThread(self.common, self.settings, self)
t.tor_status_update.connect(self._tor_status_update)
t.connected_to_tor.connect(self._connected_to_tor)
t.canceled_connecting_to_tor.connect(self._canceled_connecting_to_tor)
t.error_connecting_to_tor.connect(self._error_connecting_to_tor)
t.start()
# The main thread needs to remain active, and checking for Qt events,
# until the thread is finished. Otherwise it won't be able to handle
# accepting signals.
self.active = True
while self.active:
time.sleep(0.1)
self.common.gui.qtapp.processEvents()
def cancel_clicked(self):
self.was_canceled = True
self.fail.emit("")
def wasCanceled(self):
return self.was_canceled
def _tor_status_update(self, progress, summary):
self.progress.setValue(int(progress))
self.label.setText(
f"<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()

View File

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

View File

@ -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)