mirror of
https://github.com/onionshare/onionshare.git
synced 2024-10-01 01:35:40 -04:00
Merge branch 'develop' into advisory-fix-1
This commit is contained in:
commit
dc92a1a4b7
@ -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("")
|
||||
|
169
cli/onionshare_cli/censorship.py
Normal file
169
cli/onionshare_cli/censorship.py
Normal 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
|
@ -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
210
cli/onionshare_cli/meek.py
Normal 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.
|
||||
"""
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -1,2 +0,0 @@
|
||||
Bridge meek_lite 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com
|
||||
UseBridges 1
|
@ -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
|
@ -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
|
||||
|
3
cli/onionshare_cli/resources/torrc_template-snowflake
Normal file
3
cli/onionshare_cli/resources/torrc_template-snowflake
Normal file
@ -0,0 +1,3 @@
|
||||
# Enable built-in snowflake bridge
|
||||
Bridge snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72
|
||||
UseBridges 1
|
@ -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
536
cli/poetry.lock
generated
@ -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"},
|
||||
]
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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/).
|
||||
|
68
desktop/scripts/build-meek-client.py
Executable file
68
desktop/scripts/build-meek-client.py
Executable 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
131
desktop/scripts/get-tor-linux.py
Executable 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()
|
@ -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"])
|
||||
|
@ -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(
|
||||
|
@ -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__":
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
372
desktop/src/onionshare/moat_dialog.py
Normal file
372
desktop/src/onionshare/moat_dialog.py
Normal 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}")
|
BIN
desktop/src/onionshare/resources/images/dark_tor_settings.png
Normal file
BIN
desktop/src/onionshare/resources/images/dark_tor_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
desktop/src/onionshare/resources/images/light_tor_settings.png
Normal file
BIN
desktop/src/onionshare/resources/images/light_tor_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -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
354
desktop/src/onionshare/settings_tab.py
Normal file
354
desktop/src/onionshare/settings_tab.py
Normal 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()
|
@ -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()
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -96,7 +96,6 @@ class Tab(QtWidgets.QWidget):
|
||||
tab_id,
|
||||
system_tray,
|
||||
status_bar,
|
||||
mode_settings=None,
|
||||
filenames=None,
|
||||
):
|
||||
super(Tab, self).__init__()
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
895
desktop/src/onionshare/tor_settings_tab.py
Normal file
895
desktop/src/onionshare/tor_settings_tab.py
Normal 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")
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user