Merge branch 'black_formatting' into develop

This commit is contained in:
Micah Lee 2019-10-12 21:07:34 -07:00
commit cc2cead50b
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
87 changed files with 4293 additions and 2374 deletions

View File

@ -33,12 +33,25 @@ import fileinput, argparse, re, os, codecs, json, sys
def arg_parser():
desc = __doc__.strip().splitlines()[0]
p = argparse.ArgumentParser(description=desc)
p.add_argument('-d', default='.', help='onionshare directory',
metavar='ONIONSHARE_DIR', dest='onionshare_dir')
p.add_argument('--show-all-keys', action='store_true',
help='show translation key in source and exit'),
p.add_argument('-l', default='all', help='language code (default: all)',
metavar='LANG_CODE', dest='lang_code')
p.add_argument(
"-d",
default=".",
help="onionshare directory",
metavar="ONIONSHARE_DIR",
dest="onionshare_dir",
)
p.add_argument(
"--show-all-keys",
action="store_true",
help="show translation key in source and exit",
),
p.add_argument(
"-l",
default="all",
help="language code (default: all)",
metavar="LANG_CODE",
dest="lang_code",
)
return p
@ -54,27 +67,29 @@ def main():
dir = args.onionshare_dir
src = files_in(dir, 'onionshare') + \
files_in(dir, 'onionshare_gui') + \
files_in(dir, 'onionshare_gui/mode') + \
files_in(dir, 'onionshare_gui/mode/share_mode') + \
files_in(dir, 'onionshare_gui/mode/receive_mode') + \
files_in(dir, 'onionshare_gui/mode/website_mode') + \
files_in(dir, 'install/scripts') + \
files_in(dir, 'tests')
pysrc = [p for p in src if p.endswith('.py')]
src = (
files_in(dir, "onionshare")
+ files_in(dir, "onionshare_gui")
+ files_in(dir, "onionshare_gui/mode")
+ files_in(dir, "onionshare_gui/mode/share_mode")
+ files_in(dir, "onionshare_gui/mode/receive_mode")
+ files_in(dir, "onionshare_gui/mode/website_mode")
+ files_in(dir, "install/scripts")
+ files_in(dir, "tests")
)
pysrc = [p for p in src if p.endswith(".py")]
lang_code = args.lang_code
translate_keys = set()
# load translate key from python source
for line in fileinput.input(pysrc, openhook=fileinput.hook_encoded('utf-8')):
for line in fileinput.input(pysrc, openhook=fileinput.hook_encoded("utf-8")):
# search `strings._('translate_key')`
# `strings._('translate_key', True)`
m = re.findall(r'strings\._\((.*?)\)', line)
m = re.findall(r"strings\._\((.*?)\)", line)
if m:
for match in m:
key = match.split(',')[0].strip('''"' ''')
key = match.split(",")[0].strip(""""' """)
translate_keys.add(key)
if args.show_all_keys:
@ -82,12 +97,16 @@ def main():
print(k)
sys.exit()
if lang_code == 'all':
locale_files = [f for f in files_in(dir, 'share/locale') if f.endswith('.json')]
if lang_code == "all":
locale_files = [f for f in files_in(dir, "share/locale") if f.endswith(".json")]
else:
locale_files = [f for f in files_in(dir, 'share/locale') if f.endswith('%s.json' % lang_code)]
locale_files = [
f
for f in files_in(dir, "share/locale")
if f.endswith("%s.json" % lang_code)
]
for locale_file in locale_files:
with codecs.open(locale_file, 'r', encoding='utf-8') as f:
with codecs.open(locale_file, "r", encoding="utf-8") as f:
trans = json.load(f)
# trans -> {"key1": "translate-text1", "key2": "translate-text2", ...}
locale_keys = set(trans.keys())
@ -97,11 +116,11 @@ def main():
locale, ext = os.path.splitext(os.path.basename(locale_file))
for k in sorted(disused):
print(locale, 'disused', k)
print(locale, "disused", k)
for k in sorted(lacked):
print(locale, 'lacked', k)
print(locale, "lacked", k)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -34,17 +34,24 @@ import shutil
import subprocess
import requests
def main():
dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.5.5/TorBrowser-8.5.5-osx64_en-US.dmg'
dmg_filename = 'TorBrowser-8.5.5-osx64_en-US.dmg'
expected_dmg_sha256 = '9c1b7840bd251a4c52f0c919991e57cafb9178c55e11fa49f83ffacce3c20511'
dmg_url = "https://archive.torproject.org/tor-package-archive/torbrowser/8.5.5/TorBrowser-8.5.5-osx64_en-US.dmg"
dmg_filename = "TorBrowser-8.5.5-osx64_en-US.dmg"
expected_dmg_sha256 = (
"9c1b7840bd251a4c52f0c919991e57cafb9178c55e11fa49f83ffacce3c20511"
)
# 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')
dmg_tor_path = os.path.join('/Volumes', 'Tor Browser', 'Tor Browser.app', 'Contents')
root_path = os.path.dirname(
os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
working_path = os.path.join(root_path, "build", "tor")
dmg_tor_path = os.path.join(
"/Volumes", "Tor Browser", "Tor Browser.app", "Contents"
)
dmg_path = os.path.join(working_path, dmg_filename)
dist_path = os.path.join(root_path, 'dist', 'OnionShare.app', 'Contents')
dist_path = os.path.join(root_path, "dist", "OnionShare.app", "Contents")
# Make sure the working folder exists
if not os.path.exists(working_path):
@ -54,10 +61,10 @@ def main():
if not os.path.exists(dmg_path):
print("Downloading {}".format(dmg_url))
r = requests.get(dmg_url)
open(dmg_path, 'wb').write(r.content)
open(dmg_path, "wb").write(r.content)
dmg_sha256 = hashlib.sha256(r.content).hexdigest()
else:
dmg_data = open(dmg_path, 'rb').read()
dmg_data = open(dmg_path, "rb").read()
dmg_sha256 = hashlib.sha256(dmg_data).hexdigest()
# Compare the hash
@ -68,34 +75,52 @@ def main():
sys.exit(-1)
# Mount the dmg, copy data to the working path
subprocess.call(['hdiutil', 'attach', dmg_path])
subprocess.call(["hdiutil", "attach", dmg_path])
# Make sure Resources/tor exists before copying files
if os.path.exists(os.path.join(dist_path, 'Resources', 'Tor')):
shutil.rmtree(os.path.join(dist_path, 'Resources', 'Tor'))
os.makedirs(os.path.join(dist_path, 'Resources', 'Tor'))
if os.path.exists(os.path.join(dist_path, 'MacOS', 'Tor')):
shutil.rmtree(os.path.join(dist_path, 'MacOS', 'Tor'))
os.makedirs(os.path.join(dist_path, 'MacOS', 'Tor'))
if os.path.exists(os.path.join(dist_path, "Resources", "Tor")):
shutil.rmtree(os.path.join(dist_path, "Resources", "Tor"))
os.makedirs(os.path.join(dist_path, "Resources", "Tor"))
if os.path.exists(os.path.join(dist_path, "MacOS", "Tor")):
shutil.rmtree(os.path.join(dist_path, "MacOS", "Tor"))
os.makedirs(os.path.join(dist_path, "MacOS", "Tor"))
# Modify the tor script to adjust the path
tor_script = open(os.path.join(dmg_tor_path, 'Resources', 'TorBrowser', 'Tor', 'tor'), 'r').read()
tor_script = tor_script.replace('../../../MacOS/Tor', '../../MacOS/Tor')
open(os.path.join(dist_path, 'Resources', 'Tor', 'tor'), 'w').write(tor_script)
tor_script = open(
os.path.join(dmg_tor_path, "Resources", "TorBrowser", "Tor", "tor"), "r"
).read()
tor_script = tor_script.replace("../../../MacOS/Tor", "../../MacOS/Tor")
open(os.path.join(dist_path, "Resources", "Tor", "tor"), "w").write(tor_script)
# Copy into dist
shutil.copyfile(os.path.join(dmg_tor_path, 'Resources', 'TorBrowser', 'Tor', 'geoip'), os.path.join(dist_path, 'Resources', 'Tor', 'geoip'))
shutil.copyfile(os.path.join(dmg_tor_path, 'Resources', 'TorBrowser', 'Tor', 'geoip6'), os.path.join(dist_path, 'Resources', 'Tor', 'geoip6'))
os.chmod(os.path.join(dist_path, 'Resources', 'Tor', 'tor'), 0o755)
shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'tor.real'), os.path.join(dist_path, 'MacOS', 'Tor', 'tor.real'))
shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'libevent-2.1.6.dylib'), os.path.join(dist_path, 'MacOS', 'Tor', 'libevent-2.1.6.dylib'))
os.chmod(os.path.join(dist_path, 'MacOS', 'Tor', 'tor.real'), 0o755)
shutil.copyfile(
os.path.join(dmg_tor_path, "Resources", "TorBrowser", "Tor", "geoip"),
os.path.join(dist_path, "Resources", "Tor", "geoip"),
)
shutil.copyfile(
os.path.join(dmg_tor_path, "Resources", "TorBrowser", "Tor", "geoip6"),
os.path.join(dist_path, "Resources", "Tor", "geoip6"),
)
os.chmod(os.path.join(dist_path, "Resources", "Tor", "tor"), 0o755)
shutil.copyfile(
os.path.join(dmg_tor_path, "MacOS", "Tor", "tor.real"),
os.path.join(dist_path, "MacOS", "Tor", "tor.real"),
)
shutil.copyfile(
os.path.join(dmg_tor_path, "MacOS", "Tor", "libevent-2.1.6.dylib"),
os.path.join(dist_path, "MacOS", "Tor", "libevent-2.1.6.dylib"),
)
os.chmod(os.path.join(dist_path, "MacOS", "Tor", "tor.real"), 0o755)
# obfs4proxy binary
shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'PluggableTransports', 'obfs4proxy'), os.path.join(dist_path, 'Resources', 'Tor', 'obfs4proxy'))
os.chmod(os.path.join(dist_path, 'Resources', 'Tor', 'obfs4proxy'), 0o755)
shutil.copyfile(
os.path.join(dmg_tor_path, "MacOS", "Tor", "PluggableTransports", "obfs4proxy"),
os.path.join(dist_path, "Resources", "Tor", "obfs4proxy"),
)
os.chmod(os.path.join(dist_path, "Resources", "Tor", "obfs4proxy"), 0o755)
# Eject dmg
subprocess.call(['diskutil', 'eject', '/Volumes/Tor Browser'])
subprocess.call(["diskutil", "eject", "/Volumes/Tor Browser"])
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -32,15 +32,22 @@ import shutil
import subprocess
import requests
def main():
exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.5.5/torbrowser-install-8.5.5_en-US.exe'
exe_filename = 'torbrowser-install-8.5.5_en-US.exe'
expected_exe_sha256 = 'a3aa7e626d1d2365dcecc6f17055f467f31c4ff9558a769e51d4b90640e48bb0'
exe_url = "https://archive.torproject.org/tor-package-archive/torbrowser/8.5.5/torbrowser-install-8.5.5_en-US.exe"
exe_filename = "torbrowser-install-8.5.5_en-US.exe"
expected_exe_sha256 = (
"a3aa7e626d1d2365dcecc6f17055f467f31c4ff9558a769e51d4b90640e48bb0"
)
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
working_path = os.path.join(os.path.join(root_path, 'build'), 'tor')
root_path = os.path.dirname(
os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
working_path = os.path.join(os.path.join(root_path, "build"), "tor")
exe_path = os.path.join(working_path, exe_filename)
dist_path = os.path.join(os.path.join(os.path.join(root_path, 'dist'), 'onionshare'), 'tor')
dist_path = os.path.join(
os.path.join(os.path.join(root_path, "dist"), "onionshare"), "tor"
)
# Make sure the working folder exists
if not os.path.exists(working_path):
@ -50,10 +57,10 @@ def main():
if not os.path.exists(exe_path):
print("Downloading {}".format(exe_url))
r = requests.get(exe_url)
open(exe_path, 'wb').write(r.content)
open(exe_path, "wb").write(r.content)
exe_sha256 = hashlib.sha256(r.content).hexdigest()
else:
exe_data = open(exe_path, 'rb').read()
exe_data = open(exe_path, "rb").read()
exe_sha256 = hashlib.sha256(exe_data).hexdigest()
# Compare the hash
@ -64,8 +71,22 @@ def main():
sys.exit(-1)
# Extract the bits we need from the exe
cmd = ['7z', 'e', '-y', exe_path, 'Browser\TorBrowser\Tor', '-o%s' % os.path.join(working_path, 'Tor')]
cmd2 = ['7z', 'e', '-y', exe_path, 'Browser\TorBrowser\Data\Tor\geoip*', '-o%s' % os.path.join(working_path, 'Data')]
cmd = [
"7z",
"e",
"-y",
exe_path,
"Browser\TorBrowser\Tor",
"-o%s" % os.path.join(working_path, "Tor"),
]
cmd2 = [
"7z",
"e",
"-y",
exe_path,
"Browser\TorBrowser\Data\Tor\geoip*",
"-o%s" % os.path.join(working_path, "Data"),
]
subprocess.Popen(cmd).wait()
subprocess.Popen(cmd2).wait()
@ -73,8 +94,11 @@ def main():
if os.path.exists(dist_path):
shutil.rmtree(dist_path)
os.makedirs(dist_path)
shutil.copytree( os.path.join(working_path, 'Tor'), os.path.join(dist_path, 'Tor') )
shutil.copytree( os.path.join(working_path, 'Data'), os.path.join(dist_path, 'Data', 'Tor') )
shutil.copytree(os.path.join(working_path, "Tor"), os.path.join(dist_path, "Tor"))
shutil.copytree(
os.path.join(working_path, "Data"), os.path.join(dist_path, "Data", "Tor")
)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -5,7 +5,8 @@ import locale
import subprocess
import urllib
import gi
gi.require_version('Nautilus', '3.0')
gi.require_version("Nautilus", "3.0")
from gi.repository import Nautilus
from gi.repository import GObject
@ -15,12 +16,12 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
def __init__(self):
# Get the localized string for "Share via OnionShare" label
self.label = None
default_label = 'Share via OnionShare'
default_label = "Share via OnionShare"
try:
# Re-implement localization in python2
default_locale = 'en'
locale_dir = os.path.join(sys.prefix, 'share/onionshare/locale')
default_locale = "en"
locale_dir = os.path.join(sys.prefix, "share/onionshare/locale")
if os.path.exists(locale_dir):
# Load all translations
strings = {}
@ -28,7 +29,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
for filename in os.listdir(locale_dir):
abs_filename = os.path.join(locale_dir, filename)
lang, ext = os.path.splitext(filename)
if ext == '.json':
if ext == ".json":
with open(abs_filename) as f:
translations[lang] = json.load(f)
@ -42,7 +43,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
if key in translations[lang]:
strings[key] = translations[lang][key]
self.label = strings['share_via_onionshare']
self.label = strings["share_via_onionshare"]
except:
self.label = default_label
@ -74,19 +75,18 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
# (nautilus:3090): Gtk-ERROR **: GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same process is not supported
# sys.argv = ["", "--filenames"] + filenames
# sys.exit(onionshare_gui.main())
path = os.path.join(os.sep, 'usr', 'bin', 'onionshare-gui')
path = os.path.join(os.sep, "usr", "bin", "onionshare-gui")
cmd = [path, "--filenames"] + filenames
subprocess.Popen(cmd)
def get_file_items(self, window, files):
menuitem = Nautilus.MenuItem(name='OnionShare::Nautilus',
label=self.label,
tip='',
icon='')
menuitem = Nautilus.MenuItem(
name="OnionShare::Nautilus", label=self.label, tip="", icon=""
)
menu = Nautilus.Menu()
menu.append_item(menuitem)
menuitem.connect("activate", self.menu_activate_cb, files)
return menuitem,
return (menuitem,)
def menu_activate_cb(self, menu, files):
file_list = []

View File

@ -30,10 +30,10 @@ from .onionshare import OnionShare
def build_url(common, app, web):
# Build the URL
if common.settings.get('public_mode'):
return 'http://{0:s}'.format(app.onion_host)
if common.settings.get("public_mode"):
return "http://{0:s}".format(app.onion_host)
else:
return 'http://onionshare:{0:s}@{1:s}'.format(web.password, app.onion_host)
return "http://onionshare:{0:s}@{1:s}".format(web.password, app.onion_host)
def main(cwd=None):
@ -47,23 +47,84 @@ def main(cwd=None):
print("OnionShare {0:s} | https://onionshare.org/".format(common.version))
# OnionShare CLI in OSX needs to change current working directory (#132)
if common.platform == 'Darwin':
if common.platform == "Darwin":
if cwd:
os.chdir(cwd)
# Parse arguments
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=28))
parser.add_argument('--local-only', action='store_true', dest='local_only', help="Don't use Tor (only for development)")
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help="Continue sharing after files have been sent")
parser.add_argument('--auto-start-timer', metavar='<int>', dest='autostart_timer', default=0, help="Schedule this share to start N seconds from now")
parser.add_argument('--auto-stop-timer', metavar='<int>', dest='autostop_timer', default=0, help="Stop sharing after a given amount of seconds")
parser.add_argument('--connect-timeout', metavar='<int>', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)")
parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)")
parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them")
parser.add_argument('--website', action='store_true', dest='website', help="Publish a static website")
parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)")
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk")
parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share")
parser = argparse.ArgumentParser(
formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=28)
)
parser.add_argument(
"--local-only",
action="store_true",
dest="local_only",
help="Don't use Tor (only for development)",
)
parser.add_argument(
"--stay-open",
action="store_true",
dest="stay_open",
help="Continue sharing after files have been sent",
)
parser.add_argument(
"--auto-start-timer",
metavar="<int>",
dest="autostart_timer",
default=0,
help="Schedule this share to start N seconds from now",
)
parser.add_argument(
"--auto-stop-timer",
metavar="<int>",
dest="autostop_timer",
default=0,
help="Stop sharing after a given amount of seconds",
)
parser.add_argument(
"--connect-timeout",
metavar="<int>",
dest="connect_timeout",
default=120,
help="Give up connecting to Tor after a given amount of seconds (default: 120)",
)
parser.add_argument(
"--stealth",
action="store_true",
dest="stealth",
help="Use client authorization (advanced)",
)
parser.add_argument(
"--receive",
action="store_true",
dest="receive",
help="Receive shares instead of sending them",
)
parser.add_argument(
"--website",
action="store_true",
dest="website",
help="Publish a static website",
)
parser.add_argument(
"--config",
metavar="config",
default=False,
help="Custom JSON config file location (optional)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
dest="verbose",
help="Log OnionShare errors to stdout, and web errors to disk",
)
parser.add_argument(
"filename",
metavar="filename",
nargs="*",
help="List of files or folders to share",
)
args = parser.parse_args()
filenames = args.filename
@ -82,14 +143,14 @@ def main(cwd=None):
config = args.config
if receive:
mode = 'receive'
mode = "receive"
elif website:
mode = 'website'
mode = "website"
else:
mode = 'share'
mode = "share"
# In share an website mode, you must supply a list of filenames
if mode == 'share' or mode == 'website':
if mode == "share" or mode == "website":
# Make sure filenames given if not using receiver mode
if len(filenames) == 0:
parser.print_help()
@ -122,7 +183,9 @@ def main(cwd=None):
# Start the Onion object
onion = Onion(common)
try:
onion.connect(custom_settings=False, config=config, connect_timeout=connect_timeout)
onion.connect(
custom_settings=False, config=config, connect_timeout=connect_timeout
)
except KeyboardInterrupt:
print("")
sys.exit()
@ -132,8 +195,8 @@ def main(cwd=None):
# Start the onionshare app
try:
common.settings.load()
if not common.settings.get('public_mode'):
web.generate_password(common.settings.get('password'))
if not common.settings.get("public_mode"):
web.generate_password(common.settings.get("password"))
else:
web.password = None
app = OnionShare(common, onion, local_only, autostop_timer)
@ -144,30 +207,54 @@ def main(cwd=None):
if autostart_timer > 0:
# Can't set a schedule that is later than the auto-stop timer
if app.autostop_timer > 0 and app.autostop_timer < autostart_timer:
print("The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing.")
print(
"The auto-stop time can't be the same or earlier than the auto-start time. Please update it to start sharing."
)
sys.exit()
app.start_onion_service(False, True)
url = build_url(common, app, web)
schedule = datetime.now() + timedelta(seconds=autostart_timer)
if mode == 'receive':
print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir')))
print('')
print("Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.")
print('')
if mode == "receive":
print(
"Files sent to you appear in this folder: {}".format(
common.settings.get("data_dir")
)
)
print("")
print(
"Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
)
print("")
if stealth:
print("Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
print(
"Give this address and HidServAuth lineto your sender, and tell them it won't be accessible until: {}".format(
schedule.strftime("%I:%M:%S%p, %b %d, %y")
)
)
print(app.auth_string)
else:
print("Give this address to your sender, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
print(
"Give this address to your sender, and tell them it won't be accessible until: {}".format(
schedule.strftime("%I:%M:%S%p, %b %d, %y")
)
)
else:
if stealth:
print("Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
print(
"Give this address and HidServAuth line to your recipient, and tell them it won't be accessible until: {}".format(
schedule.strftime("%I:%M:%S%p, %b %d, %y")
)
)
print(app.auth_string)
else:
print("Give this address to your recipient, and tell them it won't be accessible until: {}".format(schedule.strftime("%I:%M:%S%p, %b %d, %y")))
print(
"Give this address to your recipient, and tell them it won't be accessible until: {}".format(
schedule.strftime("%I:%M:%S%p, %b %d, %y")
)
)
print(url)
print('')
print("")
print("Waiting for the scheduled time before starting...")
app.onion.cleanup(False)
time.sleep(autostart_timer)
@ -182,7 +269,7 @@ def main(cwd=None):
print(e.args[0])
sys.exit()
if mode == 'website':
if mode == "website":
# Prepare files to share
try:
web.website_mode.set_file_info(filenames)
@ -190,7 +277,7 @@ def main(cwd=None):
print(e.strerror)
sys.exit(1)
if mode == 'share':
if mode == "share":
# Prepare files to share
print("Compressing files.")
try:
@ -202,12 +289,15 @@ def main(cwd=None):
# Warn about sending large files over Tor
if web.share_mode.download_filesize >= 157286400: # 150mb
print('')
print("")
print("Warning: Sending a large share could take hours")
print('')
print("")
# Start OnionShare http service in new thread
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.password))
t = threading.Thread(
target=web.start,
args=(app.port, stay_open, common.settings.get("public_mode"), web.password),
)
t.daemon = True
t.start()
@ -220,23 +310,29 @@ def main(cwd=None):
app.autostop_timer_thread.start()
# Save the web password if we are using a persistent private key
if common.settings.get('save_private_key'):
if not common.settings.get('password'):
common.settings.set('password', web.password)
if common.settings.get("save_private_key"):
if not common.settings.get("password"):
common.settings.set("password", web.password)
common.settings.save()
# Build the URL
url = build_url(common, app, web)
print('')
print("")
if autostart_timer > 0:
print("Server started")
else:
if mode == 'receive':
print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir')))
print('')
print("Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.")
print('')
if mode == "receive":
print(
"Files sent to you appear in this folder: {}".format(
common.settings.get("data_dir")
)
)
print("")
print(
"Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing."
)
print("")
if stealth:
print("Give this address and HidServAuth to the sender:")
@ -253,7 +349,7 @@ def main(cwd=None):
else:
print("Give this address to the recipient:")
print(url)
print('')
print("")
print("Press Ctrl+C to stop the server")
# Wait for app to close
@ -261,14 +357,17 @@ def main(cwd=None):
if app.autostop_timer > 0:
# if the auto-stop timer was set and has run out, stop the server
if not app.autostop_timer_thread.is_alive():
if mode == 'share' or (mode == 'website'):
if mode == "share" or (mode == "website"):
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.share_mode.cur_history_id == 0 or web.done:
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
if mode == 'receive':
if web.receive_mode.cur_history_id == 0 or not web.receive_mode.uploads_in_progress:
if mode == "receive":
if (
web.receive_mode.cur_history_id == 0
or not web.receive_mode.uploads_in_progress
):
print("Stopped because auto-stop timer ran out")
web.stop(app.port)
break
@ -284,5 +383,6 @@ def main(cwd=None):
app.cleanup()
onion.cleanup()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -36,16 +36,17 @@ class Common(object):
"""
The Common object is shared amongst all parts of OnionShare.
"""
def __init__(self, verbose=False):
self.verbose = verbose
# The platform OnionShare is running on
self.platform = platform.system()
if self.platform.endswith('BSD') or self.platform == 'DragonFly':
self.platform = 'BSD'
if self.platform.endswith("BSD") or self.platform == "DragonFly":
self.platform = "BSD"
# The current version of OnionShare
with open(self.get_resource_path('version.txt')) as f:
with open(self.get_resource_path("version.txt")) as f:
self.version = f.read().strip()
def load_settings(self, config=None):
@ -64,7 +65,7 @@ class Common(object):
final_msg = "[{}] {}.{}".format(timestamp, module, func)
if msg:
final_msg = '{}: {}'.format(final_msg, msg)
final_msg = "{}: {}".format(final_msg, msg)
print(final_msg)
def get_resource_path(self, filename):
@ -73,72 +74,105 @@ class Common(object):
systemwide, and whether regardless of platform
"""
# On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
if self.platform == 'Windows':
filename = filename.replace('/', '\\')
if self.platform == "Windows":
filename = filename.replace("/", "\\")
if getattr(sys, 'onionshare_dev_mode', False):
if getattr(sys, "onionshare_dev_mode", False):
# Look for resources directory relative to python file
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))), 'share')
prefix = os.path.join(
os.path.dirname(
os.path.dirname(
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
),
"share",
)
if not os.path.exists(prefix):
# While running tests during stdeb bdist_deb, look 3 directories up for the share folder
prefix = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(prefix)))), 'share')
prefix = os.path.join(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(prefix)))
),
"share",
)
elif self.platform == 'BSD' or self.platform == 'Linux':
elif self.platform == "BSD" or self.platform == "Linux":
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
prefix = os.path.join(sys.prefix, 'share/onionshare')
prefix = os.path.join(sys.prefix, "share/onionshare")
elif getattr(sys, 'frozen', False):
elif getattr(sys, "frozen", False):
# Check if app is "frozen"
# https://pythonhosted.org/PyInstaller/#run-time-information
if self.platform == 'Darwin':
prefix = os.path.join(sys._MEIPASS, 'share')
elif self.platform == 'Windows':
prefix = os.path.join(os.path.dirname(sys.executable), 'share')
if self.platform == "Darwin":
prefix = os.path.join(sys._MEIPASS, "share")
elif self.platform == "Windows":
prefix = os.path.join(os.path.dirname(sys.executable), "share")
return os.path.join(prefix, filename)
def get_tor_paths(self):
if self.platform == 'Linux':
tor_path = '/usr/bin/tor'
tor_geo_ip_file_path = '/usr/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/share/tor/geoip6'
obfs4proxy_file_path = '/usr/bin/obfs4proxy'
elif self.platform == 'Windows':
base_path = os.path.join(os.path.dirname(os.path.dirname(self.get_resource_path(''))), 'tor')
tor_path = os.path.join(os.path.join(base_path, 'Tor'), 'tor.exe')
obfs4proxy_file_path = os.path.join(os.path.join(base_path, 'Tor'), 'obfs4proxy.exe')
tor_geo_ip_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip')
tor_geo_ipv6_file_path = os.path.join(os.path.join(os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
elif self.platform == 'Darwin':
base_path = os.path.dirname(os.path.dirname(os.path.dirname(self.get_resource_path(''))))
tor_path = os.path.join(base_path, 'Resources', 'Tor', 'tor')
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')
elif self.platform == 'BSD':
tor_path = '/usr/local/bin/tor'
tor_geo_ip_file_path = '/usr/local/share/tor/geoip'
tor_geo_ipv6_file_path = '/usr/local/share/tor/geoip6'
obfs4proxy_file_path = '/usr/local/bin/obfs4proxy'
if self.platform == "Linux":
tor_path = "/usr/bin/tor"
tor_geo_ip_file_path = "/usr/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/share/tor/geoip6"
obfs4proxy_file_path = "/usr/bin/obfs4proxy"
elif self.platform == "Windows":
base_path = os.path.join(
os.path.dirname(os.path.dirname(self.get_resource_path(""))), "tor"
)
tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe")
obfs4proxy_file_path = os.path.join(
os.path.join(base_path, "Tor"), "obfs4proxy.exe"
)
tor_geo_ip_file_path = os.path.join(
os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip"
)
tor_geo_ipv6_file_path = os.path.join(
os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip6"
)
elif self.platform == "Darwin":
base_path = os.path.dirname(
os.path.dirname(os.path.dirname(self.get_resource_path("")))
)
tor_path = os.path.join(base_path, "Resources", "Tor", "tor")
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"
)
elif self.platform == "BSD":
tor_path = "/usr/local/bin/tor"
tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
return (tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path)
return (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
)
def build_data_dir(self):
"""
Returns the path of the OnionShare data directory.
"""
if self.platform == 'Windows':
if self.platform == "Windows":
try:
appdata = os.environ['APPDATA']
onionshare_data_dir = '{}\\OnionShare'.format(appdata)
appdata = os.environ["APPDATA"]
onionshare_data_dir = "{}\\OnionShare".format(appdata)
except:
# If for some reason we don't have the 'APPDATA' environment variable
# (like running tests in Linux while pretending to be in Windows)
onionshare_data_dir = os.path.expanduser('~/.config/onionshare')
elif self.platform == 'Darwin':
onionshare_data_dir = os.path.expanduser('~/Library/Application Support/OnionShare')
onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
elif self.platform == "Darwin":
onionshare_data_dir = os.path.expanduser(
"~/Library/Application Support/OnionShare"
)
else:
onionshare_data_dir = os.path.expanduser('~/.config/onionshare')
onionshare_data_dir = os.path.expanduser("~/.config/onionshare")
os.makedirs(onionshare_data_dir, 0o700, True)
return onionshare_data_dir
@ -147,11 +181,11 @@ class Common(object):
"""
Returns a random string made from two words from the wordlist, such as "deter-trig".
"""
with open(self.get_resource_path('wordlist.txt')) as f:
with open(self.get_resource_path("wordlist.txt")) as f:
wordlist = f.read().split()
r = random.SystemRandom()
return '-'.join(r.choice(wordlist) for _ in range(2))
return "-".join(r.choice(wordlist) for _ in range(2))
def define_css(self):
"""
@ -160,7 +194,7 @@ class Common(object):
"""
self.css = {
# OnionShareGui styles
'mode_switcher_selected_style': """
"mode_switcher_selected_style": """
QPushButton {
color: #ffffff;
background-color: #4e064f;
@ -169,8 +203,7 @@ class Common(object):
font-weight: bold;
border-radius: 0;
}""",
'mode_switcher_unselected_style': """
"mode_switcher_unselected_style": """
QPushButton {
color: #ffffff;
background-color: #601f61;
@ -178,23 +211,20 @@ class Common(object):
font-weight: normal;
border-radius: 0;
}""",
'settings_button': """
"settings_button": """
QPushButton {
background-color: #601f61;
border: 0;
border-left: 1px solid #69266b;
border-radius: 0;
}""",
'server_status_indicator_label': """
"server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
'status_bar': """
"status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
@ -202,16 +232,14 @@ class Common(object):
QStatusBar::item {
border: 0px;
}""",
# Common styles between modes and their child widgets
'mode_info_label': """
"mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
'server_status_url': """
"server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
@ -220,14 +248,12 @@ class Common(object):
font-size: 12px;
}
""",
'server_status_url_buttons': """
"server_status_url_buttons": """
QPushButton {
color: #3f7fcf;
}
""",
'server_status_button_stopped': """
"server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
@ -235,8 +261,7 @@ class Common(object):
border: 0;
border-radius: 5px;
}""",
'server_status_button_working': """
"server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
@ -245,8 +270,7 @@ class Common(object):
border-radius: 5px;
font-style: italic;
}""",
'server_status_button_started': """
"server_status_button_started": """
QPushButton {
background-color: #d0011b;
color: #ffffff;
@ -254,8 +278,7 @@ class Common(object):
border: 0;
border-radius: 5px;
}""",
'downloads_uploads_empty': """
"downloads_uploads_empty": """
QWidget {
background-color: #ffffff;
border: 1px solid #999999;
@ -265,13 +288,11 @@ class Common(object):
border: 0px;
}
""",
'downloads_uploads_empty_text': """
"downloads_uploads_empty_text": """
QLabel {
color: #999999;
}""",
'downloads_uploads_label': """
"downloads_uploads_label": """
QLabel {
font-weight: bold;
font-size 14px;
@ -279,14 +300,12 @@ class Common(object):
background-color: none;
border: none;
}""",
'downloads_uploads_clear': """
"downloads_uploads_clear": """
QPushButton {
color: #3f7fcf;
}
""",
'download_uploads_indicator': """
"download_uploads_indicator": """
QLabel {
color: #ffffff;
background-color: #f44449;
@ -296,8 +315,7 @@ class Common(object):
border-radius: 7px;
text-align: center;
}""",
'downloads_uploads_progress_bar': """
"downloads_uploads_progress_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
@ -309,24 +327,20 @@ class Common(object):
background-color: #4e064f;
width: 10px;
}""",
'history_individual_file_timestamp_label': """
"history_individual_file_timestamp_label": """
QLabel {
color: #666666;
}""",
'history_individual_file_status_code_label_2xx': """
"history_individual_file_status_code_label_2xx": """
QLabel {
color: #008800;
}""",
'history_individual_file_status_code_label_4xx': """
"history_individual_file_status_code_label_4xx": """
QLabel {
color: #cc0000;
}""",
# Share mode and child widget styles
'share_zip_progess_bar': """
"share_zip_progess_bar": """
QProgressBar {
border: 1px solid #4e064f;
background-color: #ffffff !important;
@ -338,21 +352,18 @@ class Common(object):
background-color: #4e064f;
width: 10px;
}""",
'share_filesize_warning': """
"share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: #333333;
}
""",
'share_file_selection_drop_here_label': """
"share_file_selection_drop_here_label": """
QLabel {
color: #999999;
}""",
'share_file_selection_drop_count_label': """
"share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
@ -360,60 +371,51 @@ class Common(object):
padding: 5px 10px;
border-radius: 10px;
}""",
'share_file_list_drag_enter': """
"share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
'share_file_list_drag_leave': """
"share_file_list_drag_leave": """
FileList {
border: none;
}
""",
'share_file_list_item_size': """
"share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Receive mode and child widget styles
'receive_file': """
"receive_file": """
QWidget {
background-color: #ffffff;
}
""",
'receive_file_size': """
"receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Settings dialog
'settings_version': """
"settings_version": """
QLabel {
color: #666666;
}""",
'settings_tor_status': """
"settings_tor_status": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
}""",
'settings_whats_this': """
"settings_whats_this": """
QLabel {
font-size: 12px;
}""",
'settings_connect_to_tor': """
"settings_connect_to_tor": """
QLabel {
font-style: italic;
}"""
}""",
}
@staticmethod
@ -423,7 +425,7 @@ class Common(object):
"""
b = os.urandom(num_bytes)
h = hashlib.sha256(b).digest()[:16]
s = base64.b32encode(h).lower().replace(b'=', b'').decode('utf-8')
s = base64.b32encode(h).lower().replace(b"=", b"").decode("utf-8")
if not output_len:
return s
return s[:output_len]
@ -435,14 +437,14 @@ class Common(object):
"""
thresh = 1024.0
if b < thresh:
return '{:.1f} B'.format(b)
units = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')
return "{:.1f} B".format(b)
units = ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
u = 0
b /= thresh
while b >= thresh:
b /= thresh
u += 1
return '{:.1f} {}'.format(b, units[u])
return "{:.1f} {}".format(b, units[u])
@staticmethod
def format_seconds(seconds):
@ -460,7 +462,7 @@ class Common(object):
human_readable.append("{:.0f}m".format(minutes))
if seconds or not human_readable:
human_readable.append("{:.0f}s".format(seconds))
return ''.join(human_readable)
return "".join(human_readable)
@staticmethod
def estimated_time_remaining(bytes_downloaded, total_bytes, started):
@ -504,6 +506,7 @@ class AutoStopTimer(threading.Thread):
"""
Background thread sleeps t hours and returns.
"""
def __init__(self, common, time):
threading.Thread.__init__(self)
@ -513,6 +516,8 @@ class AutoStopTimer(threading.Thread):
self.time = time
def run(self):
self.common.log('AutoStopTimer', 'Server will shut down after {} seconds'.format(self.time))
self.common.log(
"AutoStopTimer", "Server will shut down after {} seconds".format(self.time)
)
time.sleep(self.time)
return 1

View File

@ -28,90 +28,113 @@ from distutils.version import LooseVersion as Version
from . import common, strings
from .settings import Settings
class TorErrorAutomatic(Exception):
"""
OnionShare is failing to connect and authenticate to the Tor controller,
using automatic settings that should work with Tor Browser.
"""
pass
class TorErrorInvalidSetting(Exception):
"""
This exception is raised if the settings just don't make sense.
"""
pass
class TorErrorSocketPort(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied address and port.
"""
pass
class TorErrorSocketFile(Exception):
"""
OnionShare can't connect to the Tor controller using the supplied socket file.
"""
pass
class TorErrorMissingPassword(Exception):
"""
OnionShare connected to the Tor controller, but it requires a password.
"""
pass
class TorErrorUnreadableCookieFile(Exception):
"""
OnionShare connected to the Tor controller, but your user does not have permission
to access the cookie file.
"""
pass
class TorErrorAuthError(Exception):
"""
OnionShare connected to the address and port, but can't authenticate. It's possible
that a Tor controller isn't listening on this port.
"""
pass
class TorErrorProtocolError(Exception):
"""
This exception is raised if onionshare connects to the Tor controller, but it
isn't acting like a Tor controller (such as in Whonix).
"""
pass
class TorTooOld(Exception):
"""
This exception is raised if onionshare needs to use a feature of Tor or stem
(like stealth ephemeral onion services) but the version you have installed
is too old.
"""
pass
class BundledTorNotSupported(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but it's not supported on that platform, or in dev mode.
"""
class BundledTorTimeout(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but Tor doesn't finish connecting promptly.
"""
class BundledTorCanceled(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
and the user cancels connecting to Tor
"""
class BundledTorBroken(Exception):
"""
This exception is raised if onionshare is set to use the bundled Tor binary,
but the process seems to fail to run.
"""
class Onion(object):
"""
Onion is an abstraction layer for connecting to the Tor control port and
@ -126,10 +149,11 @@ class Onion(object):
call this function and pass in a status string while connecting to tor. This
is necessary for status updates to reach the GUI.
"""
def __init__(self, common):
self.common = common
self.common.log('Onion', '__init__')
self.common.log("Onion", "__init__")
self.stealth = False
self.service_id = None
@ -137,13 +161,20 @@ class Onion(object):
self.scheduled_auth_cookie = None
# Is bundled tor supported?
if (self.common.platform == 'Windows' or self.common.platform == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
if (
self.common.platform == "Windows" or self.common.platform == "Darwin"
) and getattr(sys, "onionshare_dev_mode", False):
self.bundle_tor_supported = False
else:
self.bundle_tor_supported = True
# Set the path of the tor binary, for bundled tor
(self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = 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.common.get_tor_paths()
# The tor process
self.tor_proc = None
@ -154,8 +185,14 @@ class Onion(object):
# Start out not connected to Tor
self.connected_to_tor = False
def connect(self, custom_settings=False, config=False, tor_status_update_func=None, connect_timeout=120):
self.common.log('Onion', 'connect')
def connect(
self,
custom_settings=False,
config=False,
tor_status_update_func=None,
connect_timeout=120,
):
self.common.log("Onion", "connect")
# Either use settings that are passed in, or use them from common
if custom_settings:
@ -171,95 +208,157 @@ class Onion(object):
# The Tor controller
self.c = None
if self.settings.get('connection_type') == 'bundled':
if self.settings.get("connection_type") == "bundled":
if not self.bundle_tor_supported:
raise BundledTorNotSupported(strings._('settings_error_bundled_tor_not_supported'))
raise BundledTorNotSupported(
strings._("settings_error_bundled_tor_not_supported")
)
# Create a torrc for this session
self.tor_data_directory = tempfile.TemporaryDirectory(dir=self.common.build_data_dir())
self.common.log('Onion', 'connect', 'tor_data_directory={}'.format(self.tor_data_directory.name))
self.tor_data_directory = tempfile.TemporaryDirectory(
dir=self.common.build_data_dir()
)
self.common.log(
"Onion",
"connect",
"tor_data_directory={}".format(self.tor_data_directory.name),
)
# Create the torrc
with open(self.common.get_resource_path('torrc_template')) as f:
with open(self.common.get_resource_path("torrc_template")) as f:
torrc_template = f.read()
self.tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
self.tor_cookie_auth_file = os.path.join(
self.tor_data_directory.name, "cookie"
)
try:
self.tor_socks_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
raise OSError(strings._("no_available_port"))
self.tor_torrc = os.path.join(self.tor_data_directory.name, "torrc")
if self.common.platform == 'Windows' or self.common.platform == "Darwin":
if self.common.platform == "Windows" or self.common.platform == "Darwin":
# Windows doesn't support unix sockets, so it must use a network port.
# macOS can't use unix sockets either because socket filenames are limited to
# 100 chars, and the macOS sandbox forces us to put the socket file in a place
# with a really long path.
torrc_template += 'ControlPort {{control_port}}\n'
torrc_template += "ControlPort {{control_port}}\n"
try:
self.tor_control_port = self.common.get_available_port(1000, 65535)
except:
raise OSError(strings._('no_available_port'))
raise OSError(strings._("no_available_port"))
self.tor_control_socket = None
else:
# Linux and BSD can use unix sockets
torrc_template += 'ControlSocket {{control_socket}}\n'
torrc_template += "ControlSocket {{control_socket}}\n"
self.tor_control_port = None
self.tor_control_socket = os.path.join(self.tor_data_directory.name, 'control_socket')
self.tor_control_socket = os.path.join(
self.tor_data_directory.name, "control_socket"
)
torrc_template = torrc_template.replace('{{data_directory}}', self.tor_data_directory.name)
torrc_template = torrc_template.replace('{{control_port}}', str(self.tor_control_port))
torrc_template = torrc_template.replace('{{control_socket}}', str(self.tor_control_socket))
torrc_template = torrc_template.replace('{{cookie_auth_file}}', self.tor_cookie_auth_file)
torrc_template = torrc_template.replace('{{geo_ip_file}}', self.tor_geo_ip_file_path)
torrc_template = torrc_template.replace('{{geo_ipv6_file}}', self.tor_geo_ipv6_file_path)
torrc_template = torrc_template.replace('{{socks_port}}', str(self.tor_socks_port))
torrc_template = torrc_template.replace(
"{{data_directory}}", self.tor_data_directory.name
)
torrc_template = torrc_template.replace(
"{{control_port}}", str(self.tor_control_port)
)
torrc_template = torrc_template.replace(
"{{control_socket}}", str(self.tor_control_socket)
)
torrc_template = torrc_template.replace(
"{{cookie_auth_file}}", self.tor_cookie_auth_file
)
torrc_template = torrc_template.replace(
"{{geo_ip_file}}", self.tor_geo_ip_file_path
)
torrc_template = torrc_template.replace(
"{{geo_ipv6_file}}", self.tor_geo_ipv6_file_path
)
torrc_template = torrc_template.replace(
"{{socks_port}}", str(self.tor_socks_port)
)
with open(self.tor_torrc, 'w') as f:
with open(self.tor_torrc, "w") as f:
f.write(torrc_template)
# Bridge support
if self.settings.get('tor_bridges_use_obfs4'):
f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
with open(self.common.get_resource_path('torrc_template-obfs4')) as o:
if self.settings.get("tor_bridges_use_obfs4"):
f.write(
"ClientTransportPlugin obfs4 exec {}\n".format(
self.obfs4proxy_file_path
)
)
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('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
with open(self.common.get_resource_path('torrc_template-meek_lite_azure')) as o:
elif self.settings.get("tor_bridges_use_meek_lite_azure"):
f.write(
"ClientTransportPlugin meek_lite exec {}\n".format(
self.obfs4proxy_file_path
)
)
with open(
self.common.get_resource_path("torrc_template-meek_lite_azure")
) as o:
for line in o:
f.write(line)
if self.settings.get('tor_bridges_use_custom_bridges'):
if 'obfs4' in self.settings.get('tor_bridges_use_custom_bridges'):
f.write('ClientTransportPlugin obfs4 exec {}\n'.format(self.obfs4proxy_file_path))
elif 'meek_lite' in self.settings.get('tor_bridges_use_custom_bridges'):
f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
f.write(self.settings.get('tor_bridges_use_custom_bridges'))
f.write('\nUseBridges 1')
if self.settings.get("tor_bridges_use_custom_bridges"):
if "obfs4" in self.settings.get("tor_bridges_use_custom_bridges"):
f.write(
"ClientTransportPlugin obfs4 exec {}\n".format(
self.obfs4proxy_file_path
)
)
elif "meek_lite" in self.settings.get(
"tor_bridges_use_custom_bridges"
):
f.write(
"ClientTransportPlugin meek_lite exec {}\n".format(
self.obfs4proxy_file_path
)
)
f.write(self.settings.get("tor_bridges_use_custom_bridges"))
f.write("\nUseBridges 1")
# Execute a tor subprocess
start_ts = time.time()
if self.common.platform == 'Windows':
if self.common.platform == "Windows":
# In Windows, hide console window when opening tor.exe subprocess
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.tor_proc = subprocess.Popen([self.tor_path, '-f', self.tor_torrc], stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
self.tor_proc = subprocess.Popen(
[self.tor_path, "-f", self.tor_torrc],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
)
else:
self.tor_proc = subprocess.Popen([self.tor_path, '-f', self.tor_torrc], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.tor_proc = subprocess.Popen(
[self.tor_path, "-f", self.tor_torrc],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# Wait for the tor controller to start
time.sleep(2)
# Connect to the controller
try:
if self.common.platform == 'Windows' or self.common.platform == "Darwin":
if (
self.common.platform == "Windows"
or self.common.platform == "Darwin"
):
self.c = Controller.from_port(port=self.tor_control_port)
self.c.authenticate()
else:
self.c = Controller.from_socket_file(path=self.tor_control_socket)
self.c.authenticate()
except Exception as e:
raise BundledTorBroken(strings._('settings_error_bundled_tor_broken').format(e.args[0]))
raise BundledTorBroken(
strings._("settings_error_bundled_tor_broken").format(e.args[0])
)
while True:
try:
@ -268,28 +367,39 @@ class Onion(object):
raise BundledTorCanceled()
res_parts = shlex.split(res)
progress = res_parts[2].split('=')[1]
summary = res_parts[4].split('=')[1]
progress = res_parts[2].split("=")[1]
summary = res_parts[4].split("=")[1]
# "\033[K" clears the rest of the line
print("Connecting to the Tor network: {}% - {}{}".format(progress, summary, "\033[K"), end="\r")
print(
"Connecting to the Tor network: {}% - {}{}".format(
progress, summary, "\033[K"
),
end="\r",
)
if callable(tor_status_update_func):
if not tor_status_update_func(progress, summary):
# If the dialog was canceled, stop connecting to Tor
self.common.log('Onion', 'connect', 'tor_status_update_func returned false, canceling connecting to Tor')
self.common.log(
"Onion",
"connect",
"tor_status_update_func returned false, canceling connecting to Tor",
)
print()
return False
if summary == 'Done':
if summary == "Done":
print("")
break
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("tor_bridges_use_custom_bridges")
or self.settings.get("tor_bridges_use_obfs4")
or self.settings.get("tor_bridges_use_meek_lite_azure")
):
# Only override timeout if a custom timeout has not been passed in
if connect_timeout == 120:
connect_timeout = 150
@ -297,18 +407,20 @@ class Onion(object):
print("")
try:
self.tor_proc.terminate()
raise BundledTorTimeout(strings._('settings_error_bundled_tor_timeout'))
raise BundledTorTimeout(
strings._("settings_error_bundled_tor_timeout")
)
except FileNotFoundError:
pass
elif self.settings.get('connection_type') == 'automatic':
elif self.settings.get("connection_type") == "automatic":
# Automatically try to guess the right way to connect to Tor Browser
# Try connecting to control port
found_tor = False
# If the TOR_CONTROL_PORT environment variable is set, use that
env_port = os.environ.get('TOR_CONTROL_PORT')
env_port = os.environ.get("TOR_CONTROL_PORT")
if env_port:
try:
self.c = Controller.from_port(port=int(env_port))
@ -327,11 +439,13 @@ class Onion(object):
pass
# If this still didn't work, try guessing the default socket file path
socket_file_path = ''
socket_file_path = ""
if not found_tor:
try:
if self.common.platform == 'Darwin':
socket_file_path = os.path.expanduser('~/Library/Application Support/TorBrowser-Data/Tor/control.socket')
if self.common.platform == "Darwin":
socket_file_path = os.path.expanduser(
"~/Library/Application Support/TorBrowser-Data/Tor/control.socket"
)
self.c = Controller.from_socket_file(path=socket_file_path)
found_tor = True
@ -342,74 +456,108 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.common.platform == 'Darwin':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.common.platform == 'Windows':
if self.common.platform == "Linux" or self.common.platform == "BSD":
socket_file_path = "/run/user/{}/Tor/control.socket".format(
os.geteuid()
)
elif self.common.platform == "Darwin":
socket_file_path = "/run/user/{}/Tor/control.socket".format(
os.geteuid()
)
elif self.common.platform == "Windows":
# Windows doesn't support unix sockets
raise TorErrorAutomatic(strings._('settings_error_automatic'))
raise TorErrorAutomatic(strings._("settings_error_automatic"))
self.c = Controller.from_socket_file(path=socket_file_path)
except:
raise TorErrorAutomatic(strings._('settings_error_automatic'))
raise TorErrorAutomatic(strings._("settings_error_automatic"))
# Try authenticating
try:
self.c.authenticate()
except:
raise TorErrorAutomatic(strings._('settings_error_automatic'))
raise TorErrorAutomatic(strings._("settings_error_automatic"))
else:
# Use specific settings to connect to tor
# Try connecting
try:
if self.settings.get('connection_type') == 'control_port':
self.c = Controller.from_port(address=self.settings.get('control_port_address'), port=self.settings.get('control_port_port'))
elif self.settings.get('connection_type') == 'socket_file':
self.c = Controller.from_socket_file(path=self.settings.get('socket_file_path'))
if self.settings.get("connection_type") == "control_port":
self.c = Controller.from_port(
address=self.settings.get("control_port_address"),
port=self.settings.get("control_port_port"),
)
elif self.settings.get("connection_type") == "socket_file":
self.c = Controller.from_socket_file(
path=self.settings.get("socket_file_path")
)
else:
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
except:
if self.settings.get('connection_type') == 'control_port':
raise TorErrorSocketPort(strings._("settings_error_socket_port").format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
if self.settings.get("connection_type") == "control_port":
raise TorErrorSocketPort(
strings._("settings_error_socket_port").format(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
)
else:
raise TorErrorSocketFile(strings._("settings_error_socket_file").format(self.settings.get('socket_file_path')))
raise TorErrorSocketFile(
strings._("settings_error_socket_file").format(
self.settings.get("socket_file_path")
)
)
# Try authenticating
try:
if self.settings.get('auth_type') == 'no_auth':
if self.settings.get("auth_type") == "no_auth":
self.c.authenticate()
elif self.settings.get('auth_type') == 'password':
self.c.authenticate(self.settings.get('auth_password'))
elif self.settings.get("auth_type") == "password":
self.c.authenticate(self.settings.get("auth_password"))
else:
raise TorErrorInvalidSetting(strings._("settings_error_unknown"))
except MissingPassword:
raise TorErrorMissingPassword(strings._('settings_error_missing_password'))
raise TorErrorMissingPassword(
strings._("settings_error_missing_password")
)
except UnreadableCookieFile:
raise TorErrorUnreadableCookieFile(strings._('settings_error_unreadable_cookie_file'))
raise TorErrorUnreadableCookieFile(
strings._("settings_error_unreadable_cookie_file")
)
except AuthenticationFailure:
raise TorErrorAuthError(strings._('settings_error_auth').format(self.settings.get('control_port_address'), self.settings.get('control_port_port')))
raise TorErrorAuthError(
strings._("settings_error_auth").format(
self.settings.get("control_port_address"),
self.settings.get("control_port_port"),
)
)
# If we made it this far, we should be connected to Tor
self.connected_to_tor = True
# Get the tor version
self.tor_version = self.c.get_version().version_str
self.common.log('Onion', 'connect', 'Connected to tor {}'.format(self.tor_version))
self.common.log(
"Onion", "connect", "Connected to tor {}".format(self.tor_version)
)
# Do the versions of stem and tor that I'm using support ephemeral onion services?
list_ephemeral_hidden_services = getattr(self.c, "list_ephemeral_hidden_services", None)
self.supports_ephemeral = callable(list_ephemeral_hidden_services) and self.tor_version >= '0.2.7.1'
list_ephemeral_hidden_services = getattr(
self.c, "list_ephemeral_hidden_services", None
)
self.supports_ephemeral = (
callable(list_ephemeral_hidden_services) and self.tor_version >= "0.2.7.1"
)
# Do the versions of stem and tor that I'm using support stealth onion services?
try:
res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False)
res = self.c.create_ephemeral_hidden_service(
{1: 1}, basic_auth={"onionshare": None}, await_publication=False
)
tmp_service_id = res.service_id
self.c.remove_ephemeral_hidden_service(tmp_service_id)
self.supports_stealth = True
@ -420,7 +568,7 @@ class Onion(object):
# Does this version of Tor support next-gen ('v3') onions?
# Note, this is the version of Tor where this bug was fixed:
# https://trac.torproject.org/projects/tor/ticket/28619
self.supports_v3_onions = self.tor_version >= Version('0.3.5.7')
self.supports_v3_onions = self.tor_version >= Version("0.3.5.7")
def is_authenticated(self):
"""
@ -431,13 +579,12 @@ class Onion(object):
else:
return False
def start_onion_service(self, port, await_publication, save_scheduled_key=False):
"""
Start a onion service on port 80, pointing to the given port, and
return the onion hostname.
"""
self.common.log('Onion', 'start_onion_service')
self.common.log("Onion", "start_onion_service")
# Settings may have changed in the frontend but not updated in our settings object,
# such as persistence. Reload the settings now just to be sure.
self.settings.load()
@ -445,27 +592,27 @@ class Onion(object):
self.auth_string = None
if not self.supports_ephemeral:
raise TorTooOld(strings._('error_ephemeral_not_supported'))
raise TorTooOld(strings._("error_ephemeral_not_supported"))
if self.stealth and not self.supports_stealth:
raise TorTooOld(strings._('error_stealth_not_supported'))
raise TorTooOld(strings._("error_stealth_not_supported"))
if not save_scheduled_key:
print("Setting up onion service on port {0:d}.".format(int(port)))
if self.stealth:
if self.settings.get('hidservauth_string'):
hidservauth_string = self.settings.get('hidservauth_string').split()[2]
basic_auth = {'onionshare':hidservauth_string}
if self.settings.get("hidservauth_string"):
hidservauth_string = self.settings.get("hidservauth_string").split()[2]
basic_auth = {"onionshare": hidservauth_string}
else:
if self.scheduled_auth_cookie:
basic_auth = {'onionshare':self.scheduled_auth_cookie}
basic_auth = {"onionshare": self.scheduled_auth_cookie}
else:
basic_auth = {'onionshare':None}
basic_auth = {"onionshare": None}
else:
basic_auth = None
if self.settings.get('private_key'):
key_content = self.settings.get('private_key')
if self.settings.get("private_key"):
key_content = self.settings.get("private_key")
if self.is_v2_key(key_content):
key_type = "RSA1024"
else:
@ -483,7 +630,9 @@ class Onion(object):
else:
key_type = "NEW"
# Work out if we can support v3 onion services, which are preferred
if self.supports_v3_onions and not self.settings.get('use_legacy_v2_onions'):
if self.supports_v3_onions and not self.settings.get(
"use_legacy_v2_onions"
):
key_content = "ED25519-V3"
else:
# fall back to v2 onion services
@ -491,31 +640,48 @@ class Onion(object):
# v3 onions don't yet support basic auth. Our ticket:
# https://github.com/micahflee/onionshare/issues/697
if key_type == "NEW" and key_content == "ED25519-V3" and not self.settings.get('use_legacy_v2_onions'):
if (
key_type == "NEW"
and key_content == "ED25519-V3"
and not self.settings.get("use_legacy_v2_onions")
):
basic_auth = None
self.stealth = False
debug_message = 'key_type={}'.format(key_type)
debug_message = "key_type={}".format(key_type)
if key_type == "NEW":
debug_message += ', key_content={}'.format(key_content)
self.common.log('Onion', 'start_onion_service', '{}'.format(debug_message))
debug_message += ", key_content={}".format(key_content)
self.common.log("Onion", "start_onion_service", "{}".format(debug_message))
try:
if basic_auth != None:
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=await_publication, basic_auth=basic_auth, key_type=key_type, key_content=key_content)
res = self.c.create_ephemeral_hidden_service(
{80: port},
await_publication=await_publication,
basic_auth=basic_auth,
key_type=key_type,
key_content=key_content,
)
else:
# if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=await_publication, key_type=key_type, key_content=key_content)
res = self.c.create_ephemeral_hidden_service(
{80: port},
await_publication=await_publication,
key_type=key_type,
key_content=key_content,
)
except ProtocolError as e:
raise TorErrorProtocolError(strings._('error_tor_protocol_error').format(e.args[0]))
raise TorErrorProtocolError(
strings._("error_tor_protocol_error").format(e.args[0])
)
self.service_id = res.service_id
onion_host = self.service_id + '.onion'
onion_host = self.service_id + ".onion"
# A new private key was generated and is in the Control port response.
if self.settings.get('save_private_key'):
if not self.settings.get('private_key'):
self.settings.set('private_key', res.private_key)
if self.settings.get("save_private_key"):
if not self.settings.get("private_key"):
self.settings.set("private_key", res.private_key)
# If we were scheduling a future share, register the private key for later re-use
if save_scheduled_key:
@ -529,24 +695,30 @@ class Onion(object):
# in the first place.
# If we sent the basic_auth (due to a saved hidservauth_string in the settings),
# there is no response here, so use the saved value from settings.
if self.settings.get('save_private_key'):
if self.settings.get('hidservauth_string'):
self.auth_string = self.settings.get('hidservauth_string')
if self.settings.get("save_private_key"):
if self.settings.get("hidservauth_string"):
self.auth_string = self.settings.get("hidservauth_string")
else:
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
self.settings.set('hidservauth_string', self.auth_string)
self.auth_string = "HidServAuth {} {}".format(
onion_host, auth_cookie
)
self.settings.set("hidservauth_string", self.auth_string)
else:
if not self.scheduled_auth_cookie:
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
self.auth_string = "HidServAuth {} {}".format(
onion_host, auth_cookie
)
if save_scheduled_key:
# Register the HidServAuth for the scheduled share
self.scheduled_auth_cookie = auth_cookie
else:
self.scheduled_auth_cookie = None
else:
self.auth_string = 'HidServAuth {} {}'.format(onion_host, self.scheduled_auth_cookie)
self.auth_string = "HidServAuth {} {}".format(
onion_host, self.scheduled_auth_cookie
)
if not save_scheduled_key:
# We've used the scheduled share's HidServAuth. Reset it to None for future shares
self.scheduled_auth_cookie = None
@ -555,23 +727,29 @@ class Onion(object):
self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._('error_tor_protocol_error_unknown'))
raise TorErrorProtocolError(strings._("error_tor_protocol_error_unknown"))
def cleanup(self, stop_tor=True):
"""
Stop onion services that were created earlier. If there's a tor subprocess running, kill it.
"""
self.common.log('Onion', 'cleanup')
self.common.log("Onion", "cleanup")
# Cleanup the ephemeral onion services, if we have any
try:
onions = self.c.list_ephemeral_hidden_services()
for onion in onions:
try:
self.common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
self.common.log(
"Onion", "cleanup", "trying to remove onion {}".format(onion)
)
self.c.remove_ephemeral_hidden_service(onion)
except:
self.common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
self.common.log(
"Onion",
"cleanup",
"could not remove onion {}.. moving on anyway".format(onion),
)
pass
except:
pass
@ -608,14 +786,14 @@ class Onion(object):
"""
Returns a (address, port) tuple for the Tor SOCKS port
"""
self.common.log('Onion', 'get_tor_socks_port')
self.common.log("Onion", "get_tor_socks_port")
if self.settings.get('connection_type') == 'bundled':
return ('127.0.0.1', self.tor_socks_port)
elif self.settings.get('connection_type') == 'automatic':
return ('127.0.0.1', 9150)
if self.settings.get("connection_type") == "bundled":
return ("127.0.0.1", self.tor_socks_port)
elif self.settings.get("connection_type") == "automatic":
return ("127.0.0.1", 9150)
else:
return (self.settings.get('socks_address'), self.settings.get('socks_port'))
return (self.settings.get("socks_address"), self.settings.get("socks_port"))
def is_v2_key(self, key):
"""

View File

@ -24,15 +24,17 @@ from . import common, strings
from .onion import TorTooOld, TorErrorProtocolError
from .common import AutoStopTimer
class OnionShare(object):
"""
OnionShare is the main application class. Pass in options and run
start_onion_service and it will do the magic.
"""
def __init__(self, common, onion, local_only=False, autostop_timer=0):
self.common = common
self.common.log('OnionShare', '__init__')
self.common.log("OnionShare", "__init__")
# The Onion object
self.onion = onion
@ -54,7 +56,7 @@ class OnionShare(object):
self.autostop_timer_thread = None
def set_stealth(self, stealth):
self.common.log('OnionShare', 'set_stealth', 'stealth={}'.format(stealth))
self.common.log("OnionShare", "set_stealth", "stealth={}".format(stealth))
self.stealth = stealth
self.onion.stealth = stealth
@ -66,13 +68,13 @@ class OnionShare(object):
try:
self.port = self.common.get_available_port(17600, 17650)
except:
raise OSError(strings._('no_available_port'))
raise OSError(strings._("no_available_port"))
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
"""
Start the onionshare onion service.
"""
self.common.log('OnionShare', 'start_onion_service')
self.common.log("OnionShare", "start_onion_service")
if not self.port:
self.choose_port()
@ -81,10 +83,12 @@ class OnionShare(object):
self.autostop_timer_thread = AutoStopTimer(self.common, self.autostop_timer)
if self.local_only:
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
self.onion_host = "127.0.0.1:{0:d}".format(self.port)
return
self.onion_host = self.onion.start_onion_service(self.port, await_publication, save_scheduled_key)
self.onion_host = self.onion.start_onion_service(
self.port, await_publication, save_scheduled_key
)
if self.stealth:
self.auth_string = self.onion.auth_string
@ -93,7 +97,7 @@ class OnionShare(object):
"""
Shut everything down and clean up temporary files, etc.
"""
self.common.log('OnionShare', 'cleanup')
self.common.log("OnionShare", "cleanup")
# Cleanup files
try:

View File

@ -39,17 +39,22 @@ class Settings(object):
which is to attempt to connect automatically using default Tor Browser
settings.
"""
def __init__(self, common, config=False):
self.common = common
self.common.log('Settings', '__init__')
self.common.log("Settings", "__init__")
# If a readable config file was provided, use that instead
if config:
if os.path.isfile(config):
self.filename = config
else:
self.common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location')
self.common.log(
"Settings",
"__init__",
"Supplied config does not exist or is unreadable. Falling back to default location",
)
self.filename = self.build_filename()
else:
@ -60,62 +65,62 @@ class Settings(object):
# mapped to the language name, in that language
self.available_locales = {
#'bn': 'বাংলা', # Bengali (commented out because not at 90% translation)
'ca': 'Català', # Catalan
'zh_Hant': '正體中文 (繁體)', # Traditional Chinese
'zh_Hans': '中文 (简体)', # Simplified Chinese
'da': 'Dansk', # Danish
'en': 'English', # English
'fi': 'Suomi', # Finnish
'fr': 'Français', # French
'de': 'Deutsch', # German
'el': 'Ελληνικά', # Greek
'is': 'Íslenska', # Icelandic
'ga': 'Gaeilge', # Irish
'it': 'Italiano', # Italian
'ja': '日本語', # Japanese
'nb': 'Norsk Bokmål', # Norwegian Bokmål
"ca": "Català", # Catalan
"zh_Hant": "正體中文 (繁體)", # Traditional Chinese
"zh_Hans": "中文 (简体)", # Simplified Chinese
"da": "Dansk", # Danish
"en": "English", # English
"fi": "Suomi", # Finnish
"fr": "Français", # French
"de": "Deutsch", # German
"el": "Ελληνικά", # Greek
"is": "Íslenska", # Icelandic
"ga": "Gaeilge", # Irish
"it": "Italiano", # Italian
"ja": "日本語", # Japanese
"nb": "Norsk Bokmål", # Norwegian Bokmål
#'fa': 'فارسی', # Persian (commented out because not at 90% translation)
'pl': 'Polski', # Polish
'pt_BR': 'Português (Brasil)', # Portuguese Brazil
'pt_PT': 'Português (Portugal)', # Portuguese Portugal
'ru': 'Русский', # Russian
'es': 'Español', # Spanish
'sv': 'Svenska', # Swedish
'te': 'తెలుగు', # Telugu
'tr': 'Türkçe', # Turkish
'uk': 'Українська', # Ukrainian
"pl": "Polski", # Polish
"pt_BR": "Português (Brasil)", # Portuguese Brazil
"pt_PT": "Português (Portugal)", # Portuguese Portugal
"ru": "Русский", # Russian
"es": "Español", # Spanish
"sv": "Svenska", # Swedish
"te": "తెలుగు", # Telugu
"tr": "Türkçe", # Turkish
"uk": "Українська", # Ukrainian
}
# These are the default settings. They will get overwritten when loading from disk
self.default_settings = {
'version': self.common.version,
'connection_type': 'bundled',
'control_port_address': '127.0.0.1',
'control_port_port': 9051,
'socks_address': '127.0.0.1',
'socks_port': 9050,
'socket_file_path': '/var/run/tor/control',
'auth_type': 'no_auth',
'auth_password': '',
'close_after_first_download': True,
'autostop_timer': False,
'autostart_timer': False,
'use_stealth': False,
'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': '',
'use_legacy_v2_onions': False,
'save_private_key': False,
'private_key': '',
'public_mode': False,
'password': '',
'hidservauth_string': '',
'data_dir': self.build_default_data_dir(),
'csp_header_disabled': False,
'locale': None # this gets defined in fill_in_defaults()
"version": self.common.version,
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"auth_type": "no_auth",
"auth_password": "",
"close_after_first_download": True,
"autostop_timer": False,
"autostart_timer": False,
"use_stealth": False,
"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": "",
"use_legacy_v2_onions": False,
"save_private_key": False,
"private_key": "",
"public_mode": False,
"password": "",
"hidservauth_string": "",
"data_dir": self.build_default_data_dir(),
"csp_header_disabled": False,
"locale": None, # this gets defined in fill_in_defaults()
}
self._settings = {}
self.fill_in_defaults()
@ -130,14 +135,14 @@ class Settings(object):
self._settings[key] = self.default_settings[key]
# Choose the default locale based on the OS preference, and fall-back to English
if self._settings['locale'] is None:
if self._settings["locale"] is None:
language_code, encoding = locale.getdefaultlocale()
# Default to English
if not language_code:
language_code = 'en_US'
language_code = "en_US"
if language_code == 'pt_PT' and language_code == 'pt_BR':
if language_code == "pt_PT" and language_code == "pt_BR":
# Portuguese locales include country code
default_locale = language_code
else:
@ -145,14 +150,14 @@ class Settings(object):
default_locale = language_code[:2]
if default_locale not in self.available_locales:
default_locale = 'en'
self._settings['locale'] = default_locale
default_locale = "en"
self._settings["locale"] = default_locale
def build_filename(self):
"""
Returns the path of the settings file.
"""
return os.path.join(self.common.build_data_dir(), 'onionshare.json')
return os.path.join(self.common.build_data_dir(), "onionshare.json")
def build_default_data_dir(self):
"""
@ -163,26 +168,28 @@ class Settings(object):
# We can't use os.path.expanduser() in macOS because in the sandbox it
# returns the path to the sandboxed homedir
real_homedir = pwd.getpwuid(os.getuid()).pw_dir
return os.path.join(real_homedir, 'OnionShare')
return os.path.join(real_homedir, "OnionShare")
elif self.common.platform == "Windows":
# On Windows, os.path.expanduser() needs to use backslash, or else it
# retains the forward slash, which breaks opening the folder in explorer.
return os.path.expanduser('~\OnionShare')
return os.path.expanduser("~\OnionShare")
else:
# All other OSes
return os.path.expanduser('~/OnionShare')
return os.path.expanduser("~/OnionShare")
def load(self):
"""
Load the settings from file.
"""
self.common.log('Settings', 'load')
self.common.log("Settings", "load")
# If the settings file exists, load it
if os.path.exists(self.filename):
try:
self.common.log('Settings', 'load', 'Trying to load {}'.format(self.filename))
with open(self.filename, 'r') as f:
self.common.log(
"Settings", "load", "Trying to load {}".format(self.filename)
)
with open(self.filename, "r") as f:
self._settings = json.load(f)
self.fill_in_defaults()
except:
@ -190,7 +197,7 @@ class Settings(object):
# Make sure data_dir exists
try:
os.makedirs(self.get('data_dir'), exist_ok=True)
os.makedirs(self.get("data_dir"), exist_ok=True)
except:
pass
@ -198,22 +205,24 @@ class Settings(object):
"""
Save settings to file.
"""
self.common.log('Settings', 'save')
open(self.filename, 'w').write(json.dumps(self._settings, indent=2))
self.common.log('Settings', 'save', 'Settings saved in {}'.format(self.filename))
self.common.log("Settings", "save")
open(self.filename, "w").write(json.dumps(self._settings, indent=2))
self.common.log(
"Settings", "save", "Settings saved in {}".format(self.filename)
)
def get(self, key):
return self._settings[key]
def set(self, key, val):
# If typecasting int values fails, fallback to default values
if key == 'control_port_port' or key == 'socks_port':
if key == "control_port_port" or key == "socks_port":
try:
val = int(val)
except:
if key == 'control_port_port':
val = self.default_settings['control_port_port']
elif key == 'socks_port':
val = self.default_settings['socks_port']
if key == "control_port_port":
val = self.default_settings["control_port_port"]
elif key == "socks_port":
val = self.default_settings["socks_port"]
self._settings[key] = val

View File

@ -35,14 +35,14 @@ def load_strings(common):
# Load all translations
translations = {}
for locale in common.settings.available_locales:
locale_dir = common.get_resource_path('locale')
locale_dir = common.get_resource_path("locale")
filename = os.path.join(locale_dir, "{}.json".format(locale))
with open(filename, encoding='utf-8') as f:
with open(filename, encoding="utf-8") as f:
translations[locale] = json.load(f)
# Build strings
default_locale = 'en'
current_locale = common.settings.get('locale')
default_locale = "en"
current_locale = common.settings.get("locale")
strings = {}
for s in translations[default_locale]:
if s in translations[current_locale] and translations[current_locale][s] != "":
@ -57,4 +57,5 @@ def translated(k):
"""
return strings[k]
_ = translated

View File

@ -12,9 +12,10 @@ class ReceiveModeWeb:
"""
All of the web logic for receive mode
"""
def __init__(self, common, web):
self.common = common
self.common.log('ReceiveModeWeb', '__init__')
self.common.log("ReceiveModeWeb", "__init__")
self.web = web
@ -30,59 +31,84 @@ class ReceiveModeWeb:
"""
The web app routes for receiving files
"""
@self.web.app.route("/")
def index():
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
'id': history_id,
'status_code': 200
})
self.web.add_request(
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
"{}".format(request.path),
{"id": history_id, "status_code": 200},
)
self.web.add_request(self.web.REQUEST_LOAD, request.path)
r = make_response(render_template('receive.html',
static_url_path=self.web.static_url_path))
r = make_response(
render_template(
"receive.html", static_url_path=self.web.static_url_path
)
)
return self.web.add_security_headers(r)
@self.web.app.route("/upload", methods=['POST'])
@self.web.app.route("/upload", methods=["POST"])
def upload(ajax=False):
"""
Handle the upload files POST request, though at this point, the files have
already been uploaded and saved to their correct locations.
"""
files = request.files.getlist('file[]')
files = request.files.getlist("file[]")
filenames = []
for f in files:
if f.filename != '':
if f.filename != "":
filename = secure_filename(f.filename)
filenames.append(filename)
local_path = os.path.join(request.receive_mode_dir, filename)
basename = os.path.basename(local_path)
# Tell the GUI the receive mode directory for this file
self.web.add_request(self.web.REQUEST_UPLOAD_SET_DIR, request.path, {
'id': request.history_id,
'filename': basename,
'dir': request.receive_mode_dir
})
self.web.add_request(
self.web.REQUEST_UPLOAD_SET_DIR,
request.path,
{
"id": request.history_id,
"filename": basename,
"dir": request.receive_mode_dir,
},
)
self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path))
print('\n' + "Received: {}".format(local_path))
self.common.log(
"ReceiveModeWeb",
"define_routes",
"/upload, uploaded {}, saving to {}".format(
f.filename, local_path
),
)
print("\n" + "Received: {}".format(local_path))
if request.upload_error:
self.common.log('ReceiveModeWeb', 'define_routes', '/upload, there was an upload error')
self.common.log(
"ReceiveModeWeb",
"define_routes",
"/upload, there was an upload error",
)
self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, {
"receive_mode_dir": request.receive_mode_dir
})
print("Could not create OnionShare data folder: {}".format(request.receive_mode_dir))
self.web.add_request(
self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
request.path,
{"receive_mode_dir": request.receive_mode_dir},
)
print(
"Could not create OnionShare data folder: {}".format(
request.receive_mode_dir
)
)
msg = 'Error uploading, please inform the OnionShare user'
msg = "Error uploading, please inform the OnionShare user"
if ajax:
return json.dumps({"error_flashes": [msg]})
else:
flash(msg, 'error')
return redirect('/')
flash(msg, "error")
return redirect("/")
# Note that flash strings are in English, and not translated, on purpose,
# to avoid leaking the locale of the OnionShare user
@ -90,37 +116,45 @@ class ReceiveModeWeb:
info_flashes = []
if len(filenames) == 0:
msg = 'No files uploaded'
msg = "No files uploaded"
if ajax:
info_flashes.append(msg)
else:
flash(msg, 'info')
flash(msg, "info")
else:
msg = 'Sent '
msg = "Sent "
for filename in filenames:
msg += '{}, '.format(filename)
msg = msg.rstrip(', ')
msg += "{}, ".format(filename)
msg = msg.rstrip(", ")
if ajax:
info_flashes.append(msg)
else:
flash(msg, 'info')
flash(msg, "info")
if self.can_upload:
if ajax:
return json.dumps({"info_flashes": info_flashes})
else:
return redirect('/')
return redirect("/")
else:
if ajax:
return json.dumps({
"new_body": render_template('thankyou.html', static_url_path=self.web.static_url_path)
})
return json.dumps(
{
"new_body": render_template(
"thankyou.html",
static_url_path=self.web.static_url_path,
)
}
)
else:
# It was the last upload and the timer ran out
r = make_response(render_template('thankyou.html'), static_url_path=self.web.static_url_path)
r = make_response(
render_template("thankyou.html"),
static_url_path=self.web.static_url_path,
)
return self.web.add_security_headers(r)
@self.web.app.route("/upload-ajax", methods=['POST'])
@self.web.app.route("/upload-ajax", methods=["POST"])
def upload_ajax_public():
if not self.can_upload:
return self.web.error403()
@ -132,13 +166,14 @@ class ReceiveModeWSGIMiddleware(object):
Custom WSGI middleware in order to attach the Web object to environ, so
ReceiveModeRequest can access it.
"""
def __init__(self, app, web):
self.app = app
self.web = web
def __call__(self, environ, start_response):
environ['web'] = self.web
environ['stop_q'] = self.web.stop_q
environ["web"] = self.web
environ["stop_q"] = self.web.stop_q
return self.app(environ, start_response)
@ -148,6 +183,7 @@ class ReceiveModeFile(object):
written to it, in order to track the progress of uploads. It starts out with
a .part file extension, and when it's complete it removes that extension.
"""
def __init__(self, request, filename, write_func, close_func):
self.onionshare_request = request
self.onionshare_filename = filename
@ -155,24 +191,44 @@ class ReceiveModeFile(object):
self.onionshare_close_func = close_func
self.filename = os.path.join(self.onionshare_request.receive_mode_dir, filename)
self.filename_in_progress = '{}.part'.format(self.filename)
self.filename_in_progress = "{}.part".format(self.filename)
# Open the file
self.upload_error = False
try:
self.f = open(self.filename_in_progress, 'wb+')
self.f = open(self.filename_in_progress, "wb+")
except:
# This will only happen if someone is messing with the data dir while
# OnionShare is running, but if it does make sure to throw an error
self.upload_error = True
self.f = tempfile.TemporaryFile('wb+')
self.f = tempfile.TemporaryFile("wb+")
# Make all the file-like methods and attributes actually access the
# TemporaryFile, except for write
attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode',
'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto',
'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell',
'truncate', 'writable', 'writelines']
attrs = [
"closed",
"detach",
"fileno",
"flush",
"isatty",
"mode",
"name",
"peek",
"raw",
"read",
"read1",
"readable",
"readinto",
"readinto1",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"writable",
"writelines",
]
for attr in attrs:
setattr(self, attr, getattr(self.f, attr))
@ -214,20 +270,21 @@ class ReceiveModeRequest(Request):
A custom flask Request object that keeps track of how much data has been
uploaded for each file, for receive mode.
"""
def __init__(self, environ, populate_request=True, shallow=False):
super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow)
self.web = environ['web']
self.stop_q = environ['stop_q']
self.web = environ["web"]
self.stop_q = environ["stop_q"]
self.web.common.log('ReceiveModeRequest', '__init__')
self.web.common.log("ReceiveModeRequest", "__init__")
# Prevent running the close() method more than once
self.closed = False
# Is this a valid upload request?
self.upload_request = False
if self.method == 'POST':
if self.path == '/upload' or self.path == '/upload-ajax':
if self.method == "POST":
if self.path == "/upload" or self.path == "/upload-ajax":
self.upload_request = True
if self.upload_request:
@ -238,7 +295,9 @@ class ReceiveModeRequest(Request):
now = datetime.now()
date_dir = now.strftime("%Y-%m-%d")
time_dir = now.strftime("%H.%M.%S")
self.receive_mode_dir = os.path.join(self.web.common.settings.get('data_dir'), date_dir, time_dir)
self.receive_mode_dir = os.path.join(
self.web.common.settings.get("data_dir"), date_dir, time_dir
)
# Create that directory, which shouldn't exist yet
try:
@ -250,7 +309,7 @@ class ReceiveModeRequest(Request):
# Keep going until we find a directory name that's available
i = 1
while True:
new_receive_mode_dir = '{}-{}'.format(self.receive_mode_dir, i)
new_receive_mode_dir = "{}-{}".format(self.receive_mode_dir, i)
try:
os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False)
self.receive_mode_dir = new_receive_mode_dir
@ -260,15 +319,29 @@ class ReceiveModeRequest(Request):
i += 1
# Failsafe
if i == 100:
self.web.common.log('ReceiveModeRequest', '__init__', 'Error finding available receive mode directory')
self.web.common.log(
"ReceiveModeRequest",
"__init__",
"Error finding available receive mode directory",
)
self.upload_error = True
break
except PermissionError:
self.web.add_request(self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE, request.path, {
"receive_mode_dir": self.receive_mode_dir
})
print("Could not create OnionShare data folder: {}".format(self.receive_mode_dir))
self.web.common.log('ReceiveModeRequest', '__init__', 'Permission denied creating receive mode directory')
self.web.add_request(
self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
request.path,
{"receive_mode_dir": self.receive_mode_dir},
)
print(
"Could not create OnionShare data folder: {}".format(
self.receive_mode_dir
)
)
self.web.common.log(
"ReceiveModeRequest",
"__init__",
"Permission denied creating receive mode directory",
)
self.upload_error = True
# If there's an error so far, finish early
@ -287,21 +360,27 @@ class ReceiveModeRequest(Request):
# Figure out the content length
try:
self.content_length = int(self.headers['Content-Length'])
self.content_length = int(self.headers["Content-Length"])
except:
self.content_length = 0
print("{}: {}".format(
print(
"{}: {}".format(
datetime.now().strftime("%b %d, %I:%M%p"),
strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length))
))
strings._("receive_mode_upload_starting").format(
self.web.common.human_readable_filesize(self.content_length)
),
)
)
# Don't tell the GUI that a request has started until we start receiving files
self.told_gui_about_request = False
self.previous_file = None
def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None):
def _get_file_stream(
self, total_content_length, content_type, filename=None, content_length=None
):
"""
This gets called for each file that gets uploaded, and returns an file-like
writable stream.
@ -309,24 +388,26 @@ class ReceiveModeRequest(Request):
if self.upload_request:
if not self.told_gui_about_request:
# Tell the GUI about the request
self.web.add_request(self.web.REQUEST_STARTED, self.path, {
'id': self.history_id,
'content_length': self.content_length
})
self.web.add_request(
self.web.REQUEST_STARTED,
self.path,
{"id": self.history_id, "content_length": self.content_length},
)
self.web.receive_mode.uploads_in_progress.append(self.history_id)
self.told_gui_about_request = True
self.filename = secure_filename(filename)
self.progress[self.filename] = {
'uploaded_bytes': 0,
'complete': False
}
self.progress[self.filename] = {"uploaded_bytes": 0, "complete": False}
f = ReceiveModeFile(self, self.filename, self.file_write_func, self.file_close_func)
f = ReceiveModeFile(
self, self.filename, self.file_write_func, self.file_close_func
)
if f.upload_error:
self.web.common.log('ReceiveModeRequest', '_get_file_stream', 'Error creating file')
self.web.common.log(
"ReceiveModeRequest", "_get_file_stream", "Error creating file"
)
self.upload_error = True
return f
@ -341,22 +422,25 @@ class ReceiveModeRequest(Request):
return
self.closed = True
self.web.common.log('ReceiveModeRequest', 'close')
self.web.common.log("ReceiveModeRequest", "close")
try:
if self.told_gui_about_request:
history_id = self.history_id
if not self.web.stop_q.empty() or not self.progress[self.filename]['complete']:
if (
not self.web.stop_q.empty()
or not self.progress[self.filename]["complete"]
):
# Inform the GUI that the upload has canceled
self.web.add_request(self.web.REQUEST_UPLOAD_CANCELED, self.path, {
'id': history_id
})
self.web.add_request(
self.web.REQUEST_UPLOAD_CANCELED, self.path, {"id": history_id}
)
else:
# Inform the GUI that the upload has finished
self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, {
'id': history_id
})
self.web.add_request(
self.web.REQUEST_UPLOAD_FINISHED, self.path, {"id": history_id}
)
self.web.receive_mode.uploads_in_progress.remove(history_id)
except AttributeError:
@ -370,28 +454,34 @@ class ReceiveModeRequest(Request):
return
if self.upload_request:
self.progress[filename]['uploaded_bytes'] += length
self.progress[filename]["uploaded_bytes"] += length
if self.previous_file != filename:
self.previous_file = filename
print('\r=> {:15s} {}'.format(
self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']),
filename
), end='')
print(
"\r=> {:15s} {}".format(
self.web.common.human_readable_filesize(
self.progress[filename]["uploaded_bytes"]
),
filename,
),
end="",
)
# Update the GUI on the upload progress
if self.told_gui_about_request:
self.web.add_request(self.web.REQUEST_PROGRESS, self.path, {
'id': self.history_id,
'progress': self.progress
})
self.web.add_request(
self.web.REQUEST_PROGRESS,
self.path,
{"id": self.history_id, "progress": self.progress},
)
def file_close_func(self, filename, upload_error=False):
"""
This function gets called when a specific file is closed.
"""
self.progress[filename]['complete'] = True
self.progress[filename]["complete"] = True
# If the file tells us there was an upload error, let the request know as well
if upload_error:

View File

@ -12,6 +12,7 @@ class SendBaseModeWeb:
"""
All of the web logic shared between share and website mode (modes where the user sends files)
"""
def __init__(self, common, web):
super(SendBaseModeWeb, self).__init__()
self.common = common
@ -41,20 +42,24 @@ class SendBaseModeWeb:
"""
# If there's just one folder, replace filenames with a list of files inside that folder
if len(filenames) == 1 and os.path.isdir(filenames[0]):
filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])]
filenames = [
os.path.join(filenames[0], x) for x in os.listdir(filenames[0])
]
# Re-initialize
self.files = {} # Dictionary mapping file paths to filenames on disk
self.root_files = {} # This is only the root files and dirs, as opposed to all of them
self.root_files = (
{}
) # This is only the root files and dirs, as opposed to all of them
self.cleanup_filenames = []
self.cur_history_id = 0
self.file_info = {'files': [], 'dirs': []}
self.file_info = {"files": [], "dirs": []}
self.gzip_individual_files = {}
self.init()
# Build the file list
for filename in filenames:
basename = os.path.basename(filename.rstrip('/'))
basename = os.path.basename(filename.rstrip("/"))
# If it's a filename, add it
if os.path.isfile(filename):
@ -63,42 +68,50 @@ class SendBaseModeWeb:
# If it's a directory, add it recursively
elif os.path.isdir(filename):
self.root_files[basename + '/'] = filename
self.root_files[basename + "/"] = filename
for root, _, nested_filenames in os.walk(filename):
# Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
# and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
# The normalized_root should be "some_folder/foobar"
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
normalized_root = os.path.join(
basename, root[len(filename) :].lstrip("/")
).rstrip("/")
# Add the dir itself
self.files[normalized_root + '/'] = root
self.files[normalized_root + "/"] = root
# Add the files in this dir
for nested_filename in nested_filenames:
self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename)
self.files[
os.path.join(normalized_root, nested_filename)
] = os.path.join(root, nested_filename)
self.set_file_info_custom(filenames, processed_size_callback)
def directory_listing(self, filenames, path='', filesystem_path=None):
def directory_listing(self, filenames, path="", filesystem_path=None):
# Tell the GUI about the directory listing
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, '/{}'.format(path), {
'id': history_id,
'method': request.method,
'status_code': 200
})
self.web.add_request(
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
"/{}".format(path),
{"id": history_id, "method": request.method, "status_code": 200},
)
breadcrumbs = [('', '/')]
parts = path.split('/')[:-1]
breadcrumbs = [("", "/")]
parts = path.split("/")[:-1]
for i in range(len(parts)):
breadcrumbs.append(('{}'.format(parts[i]), '/{}/'.format('/'.join(parts[0:i+1]))))
breadcrumbs.append(
("{}".format(parts[i]), "/{}/".format("/".join(parts[0 : i + 1])))
)
breadcrumbs_leaf = breadcrumbs.pop()[0]
# If filesystem_path is None, this is the root directory listing
files, dirs = self.build_directory_listing(filenames, filesystem_path)
r = self.directory_listing_template(path, files, dirs, breadcrumbs, breadcrumbs_leaf)
r = self.directory_listing_template(
path, files, dirs, breadcrumbs, breadcrumbs_leaf
)
return self.web.add_security_headers(r)
def build_directory_listing(self, filenames, filesystem_path):
@ -114,16 +127,11 @@ class SendBaseModeWeb:
is_dir = os.path.isdir(this_filesystem_path)
if is_dir:
dirs.append({
'basename': filename
})
dirs.append({"basename": filename})
else:
size = os.path.getsize(this_filesystem_path)
size_human = self.common.human_readable_filesize(size)
files.append({
'basename': filename,
'size_human': size_human
})
files.append({"basename": filename, "size_human": size_human})
return files, dirs
def stream_individual_file(self, filesystem_path):
@ -136,7 +144,7 @@ class SendBaseModeWeb:
# gzip compress the individual file, if it hasn't already been compressed
if use_gzip:
if filesystem_path not in self.gzip_individual_files:
gzip_filename = tempfile.mkstemp('wb+')[1]
gzip_filename = tempfile.mkstemp("wb+")[1]
self._gzip_compress(filesystem_path, gzip_filename, 6, None)
self.gzip_individual_files[filesystem_path] = gzip_filename
@ -154,10 +162,11 @@ class SendBaseModeWeb:
# Tell GUI the individual file started
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_STARTED, path, {
'id': history_id,
'filesize': filesize
})
self.web.add_request(
self.web.REQUEST_INDIVIDUAL_FILE_STARTED,
path,
{"id": history_id, "filesize": filesize},
)
# Only GET requests are allowed, any other method should fail
if request.method != "GET":
@ -166,11 +175,11 @@ class SendBaseModeWeb:
def generate():
chunk_size = 102400 # 100kb
fp = open(file_to_download, 'rb')
fp = open(file_to_download, "rb")
done = False
while not done:
chunk = fp.read(chunk_size)
if chunk == b'':
if chunk == b"":
done = True
else:
try:
@ -179,59 +188,79 @@ class SendBaseModeWeb:
# Tell GUI the progress
downloaded_bytes = fp.tell()
percent = (1.0 * downloaded_bytes / filesize) * 100
if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
if (
not self.web.is_gui
or self.common.platform == "Linux"
or self.common.platform == "BSD"
):
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
"\r{0:s}, {1:.2f}% ".format(
self.common.human_readable_filesize(
downloaded_bytes
),
percent,
)
)
sys.stdout.flush()
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS, path, {
'id': history_id,
'bytes': downloaded_bytes,
'filesize': filesize
})
self.web.add_request(
self.web.REQUEST_INDIVIDUAL_FILE_PROGRESS,
path,
{
"id": history_id,
"bytes": downloaded_bytes,
"filesize": filesize,
},
)
done = False
except:
# Looks like the download was canceled
done = True
# Tell the GUI the individual file was canceled
self.web.add_request(self.web.REQUEST_INDIVIDUAL_FILE_CANCELED, path, {
'id': history_id
})
self.web.add_request(
self.web.REQUEST_INDIVIDUAL_FILE_CANCELED,
path,
{"id": history_id},
)
fp.close()
if self.common.platform != 'Darwin':
if self.common.platform != "Darwin":
sys.stdout.write("\n")
basename = os.path.basename(filesystem_path)
r = Response(generate())
if use_gzip:
r.headers.set('Content-Encoding', 'gzip')
r.headers.set('Content-Length', filesize)
r.headers.set('Content-Disposition', 'inline', filename=basename)
r.headers.set("Content-Encoding", "gzip")
r.headers.set("Content-Length", filesize)
r.headers.set("Content-Disposition", "inline", filename=basename)
r = self.web.add_security_headers(r)
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
r.headers.set("Content-Type", content_type)
return r
def should_use_gzip(self):
"""
Should we use gzip for this browser?
"""
return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower())
return (not self.is_zipped) and (
"gzip" in request.headers.get("Accept-Encoding", "").lower()
)
def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None):
def _gzip_compress(
self, input_filename, output_filename, level, processed_size_callback=None
):
"""
Compress a file with gzip, without loading the whole thing into memory
Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror
"""
bytes_processed = 0
blocksize = 1 << 16 # 64kB
with open(input_filename, 'rb') as input_file:
output_file = gzip.open(output_filename, 'wb', level)
with open(input_filename, "rb") as input_file:
output_file = gzip.open(output_filename, "wb", level)
while True:
if processed_size_callback is not None:
processed_size_callback(bytes_processed)
@ -269,7 +298,7 @@ class SendBaseModeWeb:
"""
pass
def render_logic(self, path=''):
def render_logic(self, path=""):
"""
Inherited class will implement this.
"""

View File

@ -13,18 +13,22 @@ class ShareModeWeb(SendBaseModeWeb):
"""
All of the web logic for share mode
"""
def init(self):
self.common.log('ShareModeWeb', 'init')
self.common.log("ShareModeWeb", "init")
# Allow downloading individual files if "Stop sharing after files have been sent" is unchecked
self.download_individual_files = not self.common.settings.get('close_after_first_download')
self.download_individual_files = not self.common.settings.get(
"close_after_first_download"
)
def define_routes(self):
"""
The web app routes for sharing files
"""
@self.web.app.route('/', defaults={'path': ''})
@self.web.app.route('/<path:path>')
@self.web.app.route("/", defaults={"path": ""})
@self.web.app.route("/<path:path>")
def index(path):
"""
Render the template for the onionshare landing page.
@ -35,8 +39,10 @@ class ShareModeWeb(SendBaseModeWeb):
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html'),
static_url_path=self.web.static_url_path)
r = make_response(
render_template("denied.html"),
static_url_path=self.web.static_url_path,
)
return self.web.add_security_headers(r)
# If download is allowed to continue, serve download page
@ -56,13 +62,16 @@ class ShareModeWeb(SendBaseModeWeb):
# currently a download
deny_download = not self.web.stay_open and self.download_in_progress
if deny_download:
r = make_response(render_template('denied.html',
static_url_path=self.web.static_url_path))
r = make_response(
render_template(
"denied.html", static_url_path=self.web.static_url_path
)
)
return self.web.add_security_headers(r)
# Prepare some variables to use inside generate() function below
# which is outside of the request context
shutdown_func = request.environ.get('werkzeug.server.shutdown')
shutdown_func = request.environ.get("werkzeug.server.shutdown")
path = request.path
# If this is a zipped file, then serve as-is. If it's not zipped, then,
@ -79,10 +88,9 @@ class ShareModeWeb(SendBaseModeWeb):
# Tell GUI the download started
history_id = self.cur_history_id
self.cur_history_id += 1
self.web.add_request(self.web.REQUEST_STARTED, path, {
'id': history_id,
'use_gzip': use_gzip
})
self.web.add_request(
self.web.REQUEST_STARTED, path, {"id": history_id, "use_gzip": use_gzip}
)
basename = os.path.basename(self.download_filename)
@ -93,19 +101,19 @@ class ShareModeWeb(SendBaseModeWeb):
chunk_size = 102400 # 100kb
fp = open(file_to_download, 'rb')
fp = open(file_to_download, "rb")
self.web.done = False
canceled = False
while not self.web.done:
# The user has canceled the download, so stop serving the file
if not self.web.stop_q.empty():
self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': history_id
})
self.web.add_request(
self.web.REQUEST_CANCELED, path, {"id": history_id}
)
break
chunk = fp.read(chunk_size)
if chunk == b'':
if chunk == b"":
self.web.done = True
else:
try:
@ -116,15 +124,26 @@ class ShareModeWeb(SendBaseModeWeb):
percent = (1.0 * downloaded_bytes / self.filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD':
if (
not self.web.is_gui
or self.common.platform == "Linux"
or self.common.platform == "BSD"
):
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent))
"\r{0:s}, {1:.2f}% ".format(
self.common.human_readable_filesize(
downloaded_bytes
),
percent,
)
)
sys.stdout.flush()
self.web.add_request(self.web.REQUEST_PROGRESS, path, {
'id': history_id,
'bytes': downloaded_bytes
})
self.web.add_request(
self.web.REQUEST_PROGRESS,
path,
{"id": history_id, "bytes": downloaded_bytes},
)
self.web.done = False
except:
# looks like the download was canceled
@ -132,13 +151,13 @@ class ShareModeWeb(SendBaseModeWeb):
canceled = True
# tell the GUI the download has canceled
self.web.add_request(self.web.REQUEST_CANCELED, path, {
'id': history_id
})
self.web.add_request(
self.web.REQUEST_CANCELED, path, {"id": history_id}
)
fp.close()
if self.common.platform != 'Darwin':
if self.common.platform != "Darwin":
sys.stdout.write("\n")
# Download is finished
@ -151,26 +170,29 @@ class ShareModeWeb(SendBaseModeWeb):
self.web.running = False
try:
if shutdown_func is None:
raise RuntimeError('Not running with the Werkzeug Server')
raise RuntimeError("Not running with the Werkzeug Server")
shutdown_func()
except:
pass
r = Response(generate())
if use_gzip:
r.headers.set('Content-Encoding', 'gzip')
r.headers.set('Content-Length', self.filesize)
r.headers.set('Content-Disposition', 'attachment', filename=basename)
r.headers.set("Content-Encoding", "gzip")
r.headers.set("Content-Length", self.filesize)
r.headers.set("Content-Disposition", "attachment", filename=basename)
r = self.web.add_security_headers(r)
# guess content type
(content_type, _) = mimetypes.guess_type(basename, strict=False)
if content_type is not None:
r.headers.set('Content-Type', content_type)
r.headers.set("Content-Type", content_type)
return r
def directory_listing_template(self, path, files, dirs, breadcrumbs, breadcrumbs_leaf):
return make_response(render_template(
'send.html',
def directory_listing_template(
self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
):
return make_response(
render_template(
"send.html",
file_info=self.file_info,
files=files,
dirs=dirs,
@ -178,17 +200,21 @@ class ShareModeWeb(SendBaseModeWeb):
breadcrumbs_leaf=breadcrumbs_leaf,
filename=os.path.basename(self.download_filename),
filesize=self.filesize,
filesize_human=self.common.human_readable_filesize(self.download_filesize),
filesize_human=self.common.human_readable_filesize(
self.download_filesize
),
is_zipped=self.is_zipped,
static_url_path=self.web.static_url_path,
download_individual_files=self.download_individual_files))
download_individual_files=self.download_individual_files,
)
)
def set_file_info_custom(self, filenames, processed_size_callback):
self.common.log("ShareModeWeb", "set_file_info_custom")
self.web.cancel_compression = False
self.build_zipfile_list(filenames, processed_size_callback)
def render_logic(self, path=''):
def render_logic(self, path=""):
if path in self.files:
filesystem_path = self.files[path]
@ -198,7 +224,7 @@ class ShareModeWeb(SendBaseModeWeb):
filenames = []
for filename in os.listdir(filesystem_path):
if os.path.isdir(os.path.join(filesystem_path, filename)):
filenames.append(filename + '/')
filenames.append(filename + "/")
else:
filenames.append(filename)
filenames.sort()
@ -221,7 +247,7 @@ class ShareModeWeb(SendBaseModeWeb):
else:
# Special case loading /
if path == '':
if path == "":
# Root directory listing
filenames = list(self.root_files)
filenames.sort()
@ -237,28 +263,34 @@ class ShareModeWeb(SendBaseModeWeb):
self.common.log("ShareModeWeb", "build_zipfile_list")
for filename in filenames:
info = {
'filename': filename,
'basename': os.path.basename(filename.rstrip('/'))
"filename": filename,
"basename": os.path.basename(filename.rstrip("/")),
}
if os.path.isfile(filename):
info['size'] = os.path.getsize(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['files'].append(info)
info["size"] = os.path.getsize(filename)
info["size_human"] = self.common.human_readable_filesize(info["size"])
self.file_info["files"].append(info)
if os.path.isdir(filename):
info['size'] = self.common.dir_size(filename)
info['size_human'] = self.common.human_readable_filesize(info['size'])
self.file_info['dirs'].append(info)
self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename'])
self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename'])
info["size"] = self.common.dir_size(filename)
info["size_human"] = self.common.human_readable_filesize(info["size"])
self.file_info["dirs"].append(info)
self.file_info["files"] = sorted(
self.file_info["files"], key=lambda k: k["basename"]
)
self.file_info["dirs"] = sorted(
self.file_info["dirs"], key=lambda k: k["basename"]
)
# Check if there's only 1 file and no folders
if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0:
self.download_filename = self.file_info['files'][0]['filename']
self.download_filesize = self.file_info['files'][0]['size']
if len(self.file_info["files"]) == 1 and len(self.file_info["dirs"]) == 0:
self.download_filename = self.file_info["files"][0]["filename"]
self.download_filesize = self.file_info["files"][0]["size"]
# Compress the file with gzip now, so we don't have to do it on each request
self.gzip_filename = tempfile.mkstemp('wb+')[1]
self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback)
self.gzip_filename = tempfile.mkstemp("wb+")[1]
self._gzip_compress(
self.download_filename, self.gzip_filename, 6, processed_size_callback
)
self.gzip_filesize = os.path.getsize(self.gzip_filename)
# Make sure the gzip file gets cleaned up when onionshare stops
@ -268,17 +300,19 @@ class ShareModeWeb(SendBaseModeWeb):
else:
# Zip up the files and folders
self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback)
self.zip_writer = ZipWriter(
self.common, processed_size_callback=processed_size_callback
)
self.download_filename = self.zip_writer.zip_filename
for info in self.file_info['files']:
self.zip_writer.add_file(info['filename'])
for info in self.file_info["files"]:
self.zip_writer.add_file(info["filename"])
# Canceling early?
if self.web.cancel_compression:
self.zip_writer.close()
return False
for info in self.file_info['dirs']:
if not self.zip_writer.add_dir(info['filename']):
for info in self.file_info["dirs"]:
if not self.zip_writer.add_dir(info["filename"]):
return False
self.zip_writer.close()
@ -298,6 +332,7 @@ class ZipWriter(object):
with. If a zip_filename is not passed in, it will use the default onionshare
filename.
"""
def __init__(self, common, zip_filename=None, processed_size_callback=None):
self.common = common
self.cancel_compression = False
@ -305,9 +340,11 @@ class ZipWriter(object):
if zip_filename:
self.zip_filename = zip_filename
else:
self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6))
self.zip_filename = "{0:s}/onionshare_{1:s}.zip".format(
tempfile.mkdtemp(), self.common.random_string(4, 6)
)
self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True)
self.z = zipfile.ZipFile(self.zip_filename, "w", allowZip64=True)
self.processed_size_callback = processed_size_callback
if self.processed_size_callback is None:
self.processed_size_callback = lambda _: None
@ -326,7 +363,7 @@ class ZipWriter(object):
"""
Add a directory, and all of its children, to the zip archive.
"""
dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/'
dir_to_strip = os.path.dirname(filename.rstrip("/")) + "/"
for dirpath, dirnames, filenames in os.walk(filename):
for f in filenames:
# Canceling early?

View File

@ -10,7 +10,15 @@ from distutils.version import LooseVersion as Version
from urllib.request import urlopen
import flask
from flask import Flask, request, render_template, abort, make_response, send_file, __version__ as flask_version
from flask import (
Flask,
request,
render_template,
abort,
make_response,
send_file,
__version__ as flask_version,
)
from flask_httpauth import HTTPBasicAuth
from .. import strings
@ -24,6 +32,7 @@ from .website_mode import WebsiteModeWeb
def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
pass
try:
flask.cli.show_server_banner = stubbed_show_server_banner
except:
@ -34,6 +43,7 @@ class Web:
"""
The Web object is the OnionShare web server, powered by flask
"""
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
@ -50,14 +60,16 @@ class Web:
REQUEST_OTHER = 13
REQUEST_INVALID_PASSWORD = 14
def __init__(self, common, is_gui, mode='share'):
def __init__(self, common, is_gui, mode="share"):
self.common = common
self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
self.common.log("Web", "__init__", "is_gui={}, mode={}".format(is_gui, mode))
# The flask app
self.app = Flask(__name__,
static_folder=self.common.get_resource_path('static'),
template_folder=self.common.get_resource_path('templates'))
self.app = Flask(
__name__,
static_folder=self.common.get_resource_path("static"),
template_folder=self.common.get_resource_path("templates"),
)
self.app.secret_key = self.common.random_string(8)
self.generate_static_url_path()
self.auth = HTTPBasicAuth()
@ -77,7 +89,7 @@ class Web:
# Are we using receive mode?
self.mode = mode
if self.mode == 'receive':
if self.mode == "receive":
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
@ -87,16 +99,16 @@ class Web:
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
if Version(flask_version) < Version('0.11'):
if Version(flask_version) < Version("0.11"):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
self.security_headers = [
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
('Referrer-Policy', 'no-referrer'),
('Server', 'OnionShare')
("X-Frame-Options", "DENY"),
("X-Xss-Protection", "1; mode=block"),
("X-Content-Type-Options", "nosniff"),
("Referrer-Policy", "no-referrer"),
("Server", "OnionShare"),
]
self.q = queue.Queue()
@ -119,19 +131,19 @@ class Web:
self.share_mode = None
self.receive_mode = None
self.website_mode = None
if self.mode == 'share':
if self.mode == "share":
self.share_mode = ShareModeWeb(self.common, self)
elif self.mode == 'receive':
elif self.mode == "receive":
self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == 'website':
elif self.mode == "website":
self.website_mode = WebsiteModeWeb(self.common, self)
def get_mode(self):
if self.mode == 'share':
if self.mode == "share":
return self.share_mode
elif self.mode == 'receive':
elif self.mode == "receive":
return self.receive_mode
elif self.mode == 'website':
elif self.mode == "website":
return self.website_mode
else:
return None
@ -139,14 +151,20 @@ class Web:
def generate_static_url_path(self):
# The static URL path has a 128-bit random number in it to avoid having name
# collisions with files that might be getting shared
self.static_url_path = '/static_{}'.format(self.common.random_string(16))
self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
self.static_url_path = "/static_{}".format(self.common.random_string(16))
self.common.log(
"Web",
"generate_static_url_path",
"new static_url_path is {}".format(self.static_url_path),
)
# Update the flask route to handle the new static URL path
self.app.static_url_path = self.static_url_path
self.app.add_url_rule(
self.static_url_path + '/<path:filename>',
endpoint='static', view_func=self.app.send_static_file)
self.static_url_path + "/<path:filename>",
endpoint="static",
view_func=self.app.send_static_file,
)
def define_common_routes(self):
"""
@ -155,7 +173,7 @@ class Web:
@self.auth.get_password
def get_pw(username):
if username == 'onionshare':
if username == "onionshare":
return self.password
else:
return None
@ -163,11 +181,12 @@ class Web:
@self.app.before_request
def conditional_auth_check():
# Allow static files without basic authentication
if(request.path.startswith(self.static_url_path + '/')):
if request.path.startswith(self.static_url_path + "/"):
return None
# If public mode is disabled, require authentication
if not self.common.settings.get('public_mode'):
if not self.common.settings.get("public_mode"):
@self.auth.login_required
def _check_login():
return None
@ -191,46 +210,63 @@ class Web:
return ""
abort(404)
if self.mode != 'website':
if self.mode != "website":
@self.app.route("/favicon.ico")
def favicon():
return send_file('{}/img/favicon.ico'.format(self.common.get_resource_path('static')))
return send_file(
"{}/img/favicon.ico".format(self.common.get_resource_path("static"))
)
def error401(self):
auth = request.authorization
if auth:
if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_passwords:
print('Invalid password guess: {}'.format(auth['password']))
self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth['password'])
if (
auth["username"] == "onionshare"
and auth["password"] not in self.invalid_passwords
):
print("Invalid password guess: {}".format(auth["password"]))
self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth["password"])
self.invalid_passwords.append(auth['password'])
self.invalid_passwords.append(auth["password"])
self.invalid_passwords_count += 1
if self.invalid_passwords_count == 20:
self.add_request(Web.REQUEST_RATE_LIMIT)
self.force_shutdown()
print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.")
print(
"Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share."
)
r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401)
r = make_response(
render_template("401.html", static_url_path=self.static_url_path), 401
)
return self.add_security_headers(r)
def error403(self):
self.add_request(Web.REQUEST_OTHER, request.path)
r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403)
r = make_response(
render_template("403.html", static_url_path=self.static_url_path), 403
)
return self.add_security_headers(r)
def error404(self, history_id):
self.add_request(self.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
'id': history_id,
'status_code': 404
})
self.add_request(
self.REQUEST_INDIVIDUAL_FILE_STARTED,
"{}".format(request.path),
{"id": history_id, "status_code": 404},
)
self.add_request(Web.REQUEST_OTHER, request.path)
r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
r = make_response(
render_template("404.html", static_url_path=self.static_url_path), 404
)
return self.add_security_headers(r)
def error405(self):
r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405)
r = make_response(
render_template("405.html", static_url_path=self.static_url_path), 405
)
return self.add_security_headers(r)
def add_security_headers(self, r):
@ -240,39 +276,53 @@ class Web:
for header, value in self.security_headers:
r.headers.set(header, value)
# Set a CSP header unless in website mode and the user has disabled it
if not self.common.settings.get('csp_header_disabled') or self.mode != 'website':
r.headers.set('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;')
if (
not self.common.settings.get("csp_header_disabled")
or self.mode != "website"
):
r.headers.set(
"Content-Security-Policy",
"default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;",
)
return r
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
return filename.endswith((".html", ".htm", ".xml", ".xhtml"))
def add_request(self, request_type, path=None, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
self.q.put({
'type': request_type,
'path': path,
'data': data
})
self.q.put({"type": request_type, "path": path, "data": data})
def generate_password(self, persistent_password=None):
self.common.log('Web', 'generate_password', 'persistent_password={}'.format(persistent_password))
if persistent_password != None and persistent_password != '':
self.common.log(
"Web",
"generate_password",
"persistent_password={}".format(persistent_password),
)
if persistent_password != None and persistent_password != "":
self.password = persistent_password
self.common.log('Web', 'generate_password', 'persistent_password sent, so password is: "{}"'.format(self.password))
self.common.log(
"Web",
"generate_password",
'persistent_password sent, so password is: "{}"'.format(self.password),
)
else:
self.password = self.common.build_password()
self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
self.common.log(
"Web",
"generate_password",
'built random password: "{}"'.format(self.password),
)
def verbose_mode(self):
"""
Turn on verbose mode, which will log flask errors to a file.
"""
flask_log_filename = os.path.join(self.common.build_data_dir(), 'flask.log')
flask_log_filename = os.path.join(self.common.build_data_dir(), "flask.log")
log_handler = logging.FileHandler(flask_log_filename)
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
@ -287,9 +337,9 @@ class Web:
"""
# Shutdown the flask service
try:
func = request.environ.get('werkzeug.server.shutdown')
func = request.environ.get("werkzeug.server.shutdown")
if func is None:
raise RuntimeError('Not running with the Werkzeug Server')
raise RuntimeError("Not running with the Werkzeug Server")
func()
except:
pass
@ -299,7 +349,13 @@ class Web:
"""
Start the flask web server.
"""
self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, password={}'.format(port, stay_open, public_mode, password))
self.common.log(
"Web",
"start",
"port={}, stay_open={}, public_mode={}, password={}".format(
port, stay_open, public_mode, password
),
)
self.stay_open = stay_open
@ -311,10 +367,10 @@ class Web:
pass
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
host = '0.0.0.0'
if os.path.exists("/usr/share/anon-ws-base-files/workstation"):
host = "0.0.0.0"
else:
host = '127.0.0.1'
host = "127.0.0.1"
self.running = True
self.app.run(host=host, port=port, threaded=True)
@ -323,7 +379,7 @@ class Web:
"""
Stop the flask web server by loading /shutdown.
"""
self.common.log('Web', 'stop', 'stopping server')
self.common.log("Web", "stop", "stopping server")
# Let the mode know that the user stopped the server
self.stop_q.put(True)
@ -331,8 +387,10 @@ class Web:
# To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown
# (We're putting the shutdown_password in the path as well to make routing simpler)
if self.running:
requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_password),
auth=requests.auth.HTTPBasicAuth('onionshare', self.password))
requests.get(
"http://127.0.0.1:{}/{}/shutdown".format(port, self.shutdown_password),
auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
)
# Reset any password that was in use
self.password = None

View File

@ -12,6 +12,7 @@ class WebsiteModeWeb(SendBaseModeWeb):
"""
All of the web logic for website mode
"""
def init(self):
pass
@ -19,38 +20,45 @@ class WebsiteModeWeb(SendBaseModeWeb):
"""
The web app routes for sharing a website
"""
@self.web.app.route('/', defaults={'path': ''})
@self.web.app.route('/<path:path>')
@self.web.app.route("/", defaults={"path": ""})
@self.web.app.route("/<path:path>")
def path_public(path):
return path_logic(path)
def path_logic(path=''):
def path_logic(path=""):
"""
Render the onionshare website.
"""
return self.render_logic(path)
def directory_listing_template(self, path, files, dirs, breadcrumbs, breadcrumbs_leaf):
return make_response(render_template('listing.html',
def directory_listing_template(
self, path, files, dirs, breadcrumbs, breadcrumbs_leaf
):
return make_response(
render_template(
"listing.html",
path=path,
files=files,
dirs=dirs,
breadcrumbs=breadcrumbs,
breadcrumbs_leaf=breadcrumbs_leaf,
static_url_path=self.web.static_url_path))
static_url_path=self.web.static_url_path,
)
)
def set_file_info_custom(self, filenames, processed_size_callback):
self.common.log("WebsiteModeWeb", "set_file_info_custom")
self.web.cancel_compression = True
def render_logic(self, path=''):
def render_logic(self, path=""):
if path in self.files:
filesystem_path = self.files[path]
# If it's a directory
if os.path.isdir(filesystem_path):
# Is there an index.html?
index_path = os.path.join(path, 'index.html')
index_path = os.path.join(path, "index.html")
if index_path in self.files:
# Render it
return self.stream_individual_file(self.files[index_path])
@ -60,7 +68,7 @@ class WebsiteModeWeb(SendBaseModeWeb):
filenames = []
for filename in os.listdir(filesystem_path):
if os.path.isdir(os.path.join(filesystem_path, filename)):
filenames.append(filename + '/')
filenames.append(filename + "/")
else:
filenames.append(filename)
filenames.sort()
@ -78,8 +86,8 @@ class WebsiteModeWeb(SendBaseModeWeb):
else:
# Special case loading /
if path == '':
index_path = 'index.html'
if path == "":
index_path = "index.html"
if index_path in self.files:
# Render it
return self.stream_individual_file(self.files[index_path])

View File

@ -32,21 +32,25 @@ from onionshare.onionshare import OnionShare
from .onionshare_gui import OnionShareGui
class Application(QtWidgets.QApplication):
"""
This is Qt's QApplication class. It has been overridden to support threads
and the quick keyboard shortcut.
"""
def __init__(self, common):
if common.platform == 'Linux' or common.platform == 'BSD':
if common.platform == "Linux" or common.platform == "BSD":
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
QtWidgets.QApplication.__init__(self, sys.argv)
self.installEventFilter(self)
def eventFilter(self, obj, event):
if (event.type() == QtCore.QEvent.KeyPress and
event.key() == QtCore.Qt.Key_Q and
event.modifiers() == QtCore.Qt.ControlModifier):
if (
event.type() == QtCore.QEvent.KeyPress
and event.key() == QtCore.Qt.Key_Q
and event.modifiers() == QtCore.Qt.ControlModifier
):
self.quit()
return False
@ -70,11 +74,34 @@ def main():
qtapp = Application(common)
# Parse arguments
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=48))
parser.add_argument('--local-only', action='store_true', dest='local_only', help="Don't use Tor (only for development)")
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk")
parser.add_argument('--filenames', metavar='filenames', nargs='+', help="List of files or folders to share")
parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)")
parser = argparse.ArgumentParser(
formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=48)
)
parser.add_argument(
"--local-only",
action="store_true",
dest="local_only",
help="Don't use Tor (only for development)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
dest="verbose",
help="Log OnionShare errors to stdout, and web errors to disk",
)
parser.add_argument(
"--filenames",
metavar="filenames",
nargs="+",
help="List of files or folders to share",
)
parser.add_argument(
"--config",
metavar="config",
default=False,
help="Custom JSON config file location (optional)",
)
args = parser.parse_args()
filenames = args.filenames
@ -118,10 +145,12 @@ def main():
def shutdown():
onion.cleanup()
app.cleanup()
qtapp.aboutToQuit.connect(shutdown)
# All done
sys.exit(qtapp.exec_())
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -29,10 +29,12 @@ from ..threads import OnionThread
from ..threads import AutoStartTimer
from ..widgets import Alert
class Mode(QtWidgets.QWidget):
"""
The class that all modes inherit from
"""
start_server_finished = QtCore.pyqtSignal()
stop_server_finished = QtCore.pyqtSignal()
starting_server_step2 = QtCore.pyqtSignal()
@ -41,7 +43,17 @@ class Mode(QtWidgets.QWidget):
starting_server_early = QtCore.pyqtSignal()
set_server_active = QtCore.pyqtSignal(bool)
def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False):
def __init__(
self,
common,
qtapp,
app,
status_bar,
server_status_label,
system_tray,
filenames=None,
local_only=False,
):
super(Mode, self).__init__()
self.common = common
self.qtapp = qtapp
@ -65,7 +77,9 @@ class Mode(QtWidgets.QWidget):
self.startup_thread = None
# Server status
self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only)
self.server_status = ServerStatus(
self.common, self.qtapp, self.app, None, self.local_only
)
self.server_status.server_started.connect(self.start_server)
self.server_status.server_stopped.connect(self.stop_server)
self.server_status.server_canceled.connect(self.cancel_server)
@ -103,11 +117,21 @@ class Mode(QtWidgets.QWidget):
minutes = (secs - days * 86400 - hours * 3600) // 60
seconds = secs - days * 86400 - hours * 3600 - minutes * 60
if not seconds:
seconds = '0'
result = ("{0}{1}, ".format(days, strings._('days_first_letter')) if days else "") + \
("{0}{1}, ".format(hours, strings._('hours_first_letter')) if hours else "") + \
("{0}{1}, ".format(minutes, strings._('minutes_first_letter')) if minutes else "") + \
"{0}{1}".format(seconds, strings._('seconds_first_letter'))
seconds = "0"
result = (
("{0}{1}, ".format(days, strings._("days_first_letter")) if days else "")
+ (
"{0}{1}, ".format(hours, strings._("hours_first_letter"))
if hours
else ""
)
+ (
"{0}{1}, ".format(minutes, strings._("minutes_first_letter"))
if minutes
else ""
)
+ "{0}{1}".format(seconds, strings._("seconds_first_letter"))
)
return result
@ -120,25 +144,45 @@ class Mode(QtWidgets.QWidget):
if self.server_status.autostart_timer_datetime:
now = QtCore.QDateTime.currentDateTime()
if self.server_status.local_only:
seconds_remaining = now.secsTo(self.server_status.autostart_timer_widget.dateTime())
seconds_remaining = now.secsTo(
self.server_status.autostart_timer_widget.dateTime()
)
else:
seconds_remaining = now.secsTo(self.server_status.autostart_timer_datetime.replace(second=0, microsecond=0))
seconds_remaining = now.secsTo(
self.server_status.autostart_timer_datetime.replace(
second=0, microsecond=0
)
)
# Update the server button
if seconds_remaining > 0:
self.server_status.server_button.setText(strings._('gui_waiting_to_start').format(self.human_friendly_time(seconds_remaining)))
self.server_status.server_button.setText(
strings._("gui_waiting_to_start").format(
self.human_friendly_time(seconds_remaining)
)
)
else:
self.server_status.server_button.setText(strings._('gui_please_wait'))
self.server_status.server_button.setText(
strings._("gui_please_wait")
)
# If the auto-stop timer has stopped, stop the server
if self.server_status.status == ServerStatus.STATUS_STARTED:
if self.app.autostop_timer_thread and self.common.settings.get('autostop_timer'):
if self.app.autostop_timer_thread and self.common.settings.get(
"autostop_timer"
):
if self.autostop_timer_datetime_delta > 0:
now = QtCore.QDateTime.currentDateTime()
seconds_remaining = now.secsTo(self.server_status.autostop_timer_datetime)
seconds_remaining = now.secsTo(
self.server_status.autostop_timer_datetime
)
# Update the server button
server_button_text = self.get_stop_server_autostop_timer_text()
self.server_status.server_button.setText(server_button_text.format(self.human_friendly_time(seconds_remaining)))
self.server_status.server_button.setText(
server_button_text.format(
self.human_friendly_time(seconds_remaining)
)
)
self.status_bar.clearMessage()
if not self.app.autostop_timer_thread.is_alive():
@ -168,16 +212,16 @@ class Mode(QtWidgets.QWidget):
Start the onionshare server. This uses multiple threads to start the Tor onion
server and the web app.
"""
self.common.log('Mode', 'start_server')
self.common.log("Mode", "start_server")
self.start_server_custom()
self.set_server_active.emit(True)
self.app.set_stealth(self.common.settings.get('use_stealth'))
self.app.set_stealth(self.common.settings.get("use_stealth"))
# Clear the status bar
self.status_bar.clearMessage()
self.server_status_label.setText('')
self.server_status_label.setText("")
# Ensure we always get a new random port each time we might launch an OnionThread
self.app.port = None
@ -192,7 +236,7 @@ class Mode(QtWidgets.QWidget):
# If scheduling a share, delay starting the real share
if self.server_status.autostart_timer_datetime:
self.common.log('Mode', 'start_server', 'Starting auto-start timer')
self.common.log("Mode", "start_server", "Starting auto-start timer")
self.startup_thread = AutoStartTimer(self)
# Once the timer has finished, start the real share, with a WebThread
self.startup_thread.success.connect(self.start_scheduled_service)
@ -201,7 +245,7 @@ class Mode(QtWidgets.QWidget):
self.startup_thread.start()
def start_onion_thread(self, obtain_onion_early=False):
self.common.log('Mode', 'start_server', 'Starting an onion thread')
self.common.log("Mode", "start_server", "Starting an onion thread")
self.obtain_onion_early = obtain_onion_early
self.onion_thread = OnionThread(self)
self.onion_thread.success.connect(self.starting_server_step2.emit)
@ -213,7 +257,7 @@ class Mode(QtWidgets.QWidget):
# We start a new OnionThread with the saved scheduled key from settings
self.common.settings.load()
self.obtain_onion_early = obtain_onion_early
self.common.log('Mode', 'start_server', 'Starting a scheduled onion thread')
self.common.log("Mode", "start_server", "Starting a scheduled onion thread")
self.onion_thread = OnionThread(self)
self.onion_thread.success.connect(self.starting_server_step2.emit)
self.onion_thread.error.connect(self.starting_server_error.emit)
@ -237,7 +281,7 @@ class Mode(QtWidgets.QWidget):
"""
Step 2 in starting the onionshare server.
"""
self.common.log('Mode', 'start_server_step2')
self.common.log("Mode", "start_server_step2")
self.start_server_step2_custom()
@ -257,22 +301,28 @@ class Mode(QtWidgets.QWidget):
"""
Step 3 in starting the onionshare server.
"""
self.common.log('Mode', 'start_server_step3')
self.common.log("Mode", "start_server_step3")
self.start_server_step3_custom()
if self.common.settings.get('autostop_timer'):
if self.common.settings.get("autostop_timer"):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.autostop_timer_datetime_delta = now.secsTo(self.server_status.autostop_timer_datetime)
self.autostop_timer_datetime_delta = now.secsTo(
self.server_status.autostop_timer_datetime
)
# Start the auto-stop timer
if self.autostop_timer_datetime_delta > 0:
self.app.autostop_timer_thread = AutoStopTimer(self.common, self.autostop_timer_datetime_delta)
self.app.autostop_timer_thread = AutoStopTimer(
self.common, self.autostop_timer_datetime_delta
)
self.app.autostop_timer_thread.start()
# The auto-stop timer has actually already passed since the user clicked Start. Probably the Onion service took too long to start.
else:
self.stop_server()
self.start_server_error(strings._('gui_server_started_after_autostop_timer'))
self.start_server_error(
strings._("gui_server_started_after_autostop_timer")
)
def start_server_step3_custom(self):
"""
@ -284,7 +334,7 @@ class Mode(QtWidgets.QWidget):
"""
If there's an error when trying to start the onion service
"""
self.common.log('Mode', 'start_server_error')
self.common.log("Mode", "start_server_error")
Alert(self.common, error, QtWidgets.QMessageBox.Warning)
self.set_server_active.emit(False)
@ -305,16 +355,16 @@ class Mode(QtWidgets.QWidget):
"""
self.cancel_server_custom()
if self.startup_thread:
self.common.log('Mode', 'cancel_server: quitting startup thread')
self.common.log("Mode", "cancel_server: quitting startup thread")
self.startup_thread.canceled = True
self.app.onion.scheduled_key = None
self.app.onion.scheduled_auth_cookie = None
self.startup_thread.quit()
if self.onion_thread:
self.common.log('Mode', 'cancel_server: quitting onion thread')
self.common.log("Mode", "cancel_server: quitting onion thread")
self.onion_thread.quit()
if self.web_thread:
self.common.log('Mode', 'cancel_server: quitting web thread')
self.common.log("Mode", "cancel_server: quitting web thread")
self.web_thread.quit()
self.stop_server()
@ -328,7 +378,7 @@ class Mode(QtWidgets.QWidget):
"""
Stop the onionshare server.
"""
self.common.log('Mode', 'stop_server')
self.common.log("Mode", "stop_server")
if self.server_status.status != ServerStatus.STATUS_STOPPED:
try:
@ -382,7 +432,9 @@ class Mode(QtWidgets.QWidget):
Handle REQUEST_RATE_LIMIT event.
"""
self.stop_server()
Alert(self.common, strings._('error_rate_limit'), QtWidgets.QMessageBox.Critical)
Alert(
self.common, strings._("error_rate_limit"), QtWidgets.QMessageBox.Critical
)
def handle_request_progress(self, event):
"""

View File

@ -24,11 +24,13 @@ from onionshare import strings
from ..widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel):
"""
When there are no files or folders in the FileList yet, display the
'drop files here' message and graphic.
"""
def __init__(self, common, parent, image=False):
self.parent = parent
super(DropHereLabel, self).__init__(parent=parent)
@ -39,10 +41,16 @@ class DropHereLabel(QtWidgets.QLabel):
self.setAlignment(QtCore.Qt.AlignCenter)
if image:
self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png'))))
self.setPixmap(
QtGui.QPixmap.fromImage(
QtGui.QImage(
self.common.get_resource_path("images/logo_transparent.png")
)
)
)
else:
self.setText(strings._('gui_drag_and_drop'))
self.setStyleSheet(self.common.css['share_file_selection_drop_here_label'])
self.setText(strings._("gui_drag_and_drop"))
self.setStyleSheet(self.common.css["share_file_selection_drop_here_label"])
self.hide()
@ -57,6 +65,7 @@ class DropCountLabel(QtWidgets.QLabel):
While dragging files over the FileList, this counter displays the
number of files you're dragging.
"""
def __init__(self, common, parent):
self.parent = parent
super(DropCountLabel, self).__init__(parent=parent)
@ -65,8 +74,8 @@ class DropCountLabel(QtWidgets.QLabel):
self.setAcceptDrops(True)
self.setAlignment(QtCore.Qt.AlignCenter)
self.setText(strings._('gui_drag_and_drop'))
self.setStyleSheet(self.common.css['share_file_selection_drop_count_label'])
self.setText(strings._("gui_drag_and_drop"))
self.setStyleSheet(self.common.css["share_file_selection_drop_count_label"])
self.hide()
def dragEnterEvent(self, event):
@ -78,6 +87,7 @@ class FileList(QtWidgets.QListWidget):
"""
The list of files and folders in the GUI.
"""
files_dropped = QtCore.pyqtSignal()
files_updated = QtCore.pyqtSignal()
@ -139,7 +149,7 @@ class FileList(QtWidgets.QListWidget):
if self.count() > 0:
# Add and delete an empty item, to force all items to get redrawn
# This is ugly, but the only way I could figure out how to proceed
item = QtWidgets.QListWidgetItem('fake item')
item = QtWidgets.QListWidgetItem("fake item")
self.addItem(item)
self.takeItem(self.row(item))
self.update()
@ -149,21 +159,27 @@ class FileList(QtWidgets.QListWidget):
# and extend based on the overall width minus that amount.
for index in range(self.count()):
metrics = QtGui.QFontMetrics(self.item(index).font())
elided = metrics.elidedText(self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200)
elided = metrics.elidedText(
self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200
)
self.item(index).setText(elided)
def dragEnterEvent(self, event):
"""
dragEnterEvent for dragging files and directories into the widget.
"""
if event.mimeData().hasUrls:
self.setStyleSheet(self.common.css['share_file_list_drag_enter'])
self.setStyleSheet(self.common.css["share_file_list_drag_enter"])
count = len(event.mimeData().urls())
self.drop_count.setText('+{}'.format(count))
self.drop_count.setText("+{}".format(count))
size_hint = self.drop_count.sizeHint()
self.drop_count.setGeometry(self.width() - size_hint.width() - 10, self.height() - size_hint.height() - 10, size_hint.width(), size_hint.height())
self.drop_count.setGeometry(
self.width() - size_hint.width() - 10,
self.height() - size_hint.height() - 10,
size_hint.width(),
size_hint.height(),
)
self.drop_count.show()
event.accept()
else:
@ -173,7 +189,7 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
self.setStyleSheet(self.common.css['share_file_list_drag_leave'])
self.setStyleSheet(self.common.css["share_file_list_drag_leave"])
self.drop_count.hide()
event.accept()
self.update()
@ -201,7 +217,7 @@ class FileList(QtWidgets.QListWidget):
else:
event.ignore()
self.setStyleSheet(self.common.css['share_file_list_drag_leave'])
self.setStyleSheet(self.common.css["share_file_list_drag_leave"])
self.drop_count.hide()
self.files_dropped.emit()
@ -238,12 +254,14 @@ class FileList(QtWidgets.QListWidget):
# Item's filename attribute and size labels
item.filename = filename
item_size = QtWidgets.QLabel(size_readable)
item_size.setStyleSheet(self.common.css['share_file_list_item_size'])
item_size.setStyleSheet(self.common.css["share_file_list_item_size"])
item.basename = os.path.basename(filename.rstrip('/'))
item.basename = os.path.basename(filename.rstrip("/"))
# Use the basename as the method with which to sort the list
metrics = QtGui.QFontMetrics(item.font())
elided = metrics.elidedText(item.basename, QtCore.Qt.ElideRight, self.sizeHint().width())
elided = metrics.elidedText(
item.basename, QtCore.Qt.ElideRight, self.sizeHint().width()
)
item.setData(QtCore.Qt.DisplayRole, elided)
# Item's delete button
@ -255,9 +273,13 @@ class FileList(QtWidgets.QListWidget):
item.item_button = QtWidgets.QPushButton()
item.item_button.setDefault(False)
item.item_button.setFlat(True)
item.item_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/file_delete.png')) )
item.item_button.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/file_delete.png"))
)
item.item_button.clicked.connect(delete_item)
item.item_button.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
item.item_button.setSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
)
# Item info widget, with a white background
item_info_layout = QtWidgets.QHBoxLayout()
@ -265,7 +287,7 @@ class FileList(QtWidgets.QListWidget):
item_info_layout.addWidget(item_size)
item_info_layout.addWidget(item.item_button)
item_info = QtWidgets.QWidget()
item_info.setObjectName('item-info')
item_info.setObjectName("item-info")
item_info.setLayout(item_info_layout)
# Create the item's widget and layouts
@ -288,6 +310,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
The list of files and folders in the GUI, as well as buttons to add and
delete the files and folders.
"""
def __init__(self, common, parent):
super(FileSelection, self).__init__()
@ -303,21 +326,21 @@ class FileSelection(QtWidgets.QVBoxLayout):
self.file_list.files_updated.connect(self.update)
# Buttons
if self.common.platform == 'Darwin':
if self.common.platform == "Darwin":
# The macOS sandbox makes it so the Mac version needs separate add files
# and folders buttons, in order to use native file selection dialogs
self.add_files_button = QtWidgets.QPushButton(strings._('gui_add_files'))
self.add_files_button = QtWidgets.QPushButton(strings._("gui_add_files"))
self.add_files_button.clicked.connect(self.add_files)
self.add_folder_button = QtWidgets.QPushButton(strings._('gui_add_folder'))
self.add_folder_button = QtWidgets.QPushButton(strings._("gui_add_folder"))
self.add_folder_button.clicked.connect(self.add_folder)
else:
self.add_button = QtWidgets.QPushButton(strings._('gui_add'))
self.add_button = QtWidgets.QPushButton(strings._("gui_add"))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete'))
self.delete_button = QtWidgets.QPushButton(strings._("gui_delete"))
self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch()
if self.common.platform == 'Darwin':
if self.common.platform == "Darwin":
button_layout.addWidget(self.add_files_button)
button_layout.addWidget(self.add_folder_button)
else:
@ -336,14 +359,14 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
# All buttons should be hidden if the server is on
if self.server_on:
if self.common.platform == 'Darwin':
if self.common.platform == "Darwin":
self.add_files_button.hide()
self.add_folder_button.hide()
else:
self.add_button.hide()
self.delete_button.hide()
else:
if self.common.platform == 'Darwin':
if self.common.platform == "Darwin":
self.add_files_button.show()
self.add_folder_button.show()
else:
@ -362,7 +385,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Add button clicked.
"""
file_dialog = AddFileDialog(self.common, caption=strings._('gui_choose_items'))
file_dialog = AddFileDialog(self.common, caption=strings._("gui_choose_items"))
if file_dialog.exec_() == QtWidgets.QDialog.Accepted:
for filename in file_dialog.selectedFiles():
self.file_list.add_file(filename)
@ -374,7 +397,9 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Add files button clicked.
"""
files = QtWidgets.QFileDialog.getOpenFileNames(self.parent, caption=strings._('gui_choose_items'))
files = QtWidgets.QFileDialog.getOpenFileNames(
self.parent, caption=strings._("gui_choose_items")
)
filenames = files[0]
for filename in filenames:
self.file_list.add_file(filename)
@ -383,9 +408,11 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Add folder button clicked.
"""
filename = QtWidgets.QFileDialog.getExistingDirectory(self.parent,
caption=strings._('gui_choose_items'),
options=QtWidgets.QFileDialog.ShowDirsOnly)
filename = QtWidgets.QFileDialog.getExistingDirectory(
self.parent,
caption=strings._("gui_choose_items"),
options=QtWidgets.QFileDialog.ShowDirsOnly,
)
self.file_list.add_file(filename)
def delete(self):

View File

@ -31,6 +31,7 @@ class HistoryItem(QtWidgets.QWidget):
"""
The base history item
"""
STATUS_STARTED = 0
STATUS_FINISHED = 1
STATUS_CANCELED = 2
@ -49,34 +50,42 @@ class HistoryItem(QtWidgets.QWidget):
When an item finishes, returns a string displaying the start/end datetime range.
started is a datetime object.
"""
return self._get_label_text('gui_all_modes_transfer_finished', 'gui_all_modes_transfer_finished_range', started)
return self._get_label_text(
"gui_all_modes_transfer_finished",
"gui_all_modes_transfer_finished_range",
started,
)
def get_canceled_label_text(self, started):
"""
When an item is canceled, returns a string displaying the start/end datetime range.
started is a datetime object.
"""
return self._get_label_text('gui_all_modes_transfer_canceled', 'gui_all_modes_transfer_canceled_range', started)
return self._get_label_text(
"gui_all_modes_transfer_canceled",
"gui_all_modes_transfer_canceled_range",
started,
)
def _get_label_text(self, string_name, string_range_name, started):
"""
Return a string that contains a date, or date range.
"""
ended = datetime.now()
if started.year == ended.year and started.month == ended.month and started.day == ended.day:
if (
started.year == ended.year
and started.month == ended.month
and started.day == ended.day
):
if started.hour == ended.hour and started.minute == ended.minute:
text = strings._(string_name).format(
started.strftime("%b %d, %I:%M%p")
text = strings._(string_name).format(started.strftime("%b %d, %I:%M%p"))
else:
text = strings._(string_range_name).format(
started.strftime("%b %d, %I:%M%p"), ended.strftime("%I:%M%p")
)
else:
text = strings._(string_range_name).format(
started.strftime("%b %d, %I:%M%p"),
ended.strftime("%I:%M%p")
)
else:
text = strings._(string_range_name).format(
started.strftime("%b %d, %I:%M%p"),
ended.strftime("%b %d, %I:%M%p")
started.strftime("%b %d, %I:%M%p"), ended.strftime("%b %d, %I:%M%p")
)
return text
@ -85,6 +94,7 @@ class ShareHistoryItem(HistoryItem):
"""
Download history item, for share mode
"""
def __init__(self, common, id, total_bytes):
super(ShareHistoryItem, self).__init__()
self.common = common
@ -97,7 +107,11 @@ class ShareHistoryItem(HistoryItem):
self.status = HistoryItem.STATUS_STARTED
# Label
self.label = QtWidgets.QLabel(strings._('gui_all_modes_transfer_started').format(self.started_dt.strftime("%b %d, %I:%M%p")))
self.label = QtWidgets.QLabel(
strings._("gui_all_modes_transfer_started").format(
self.started_dt.strftime("%b %d, %I:%M%p")
)
)
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
@ -107,7 +121,9 @@ class ShareHistoryItem(HistoryItem):
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(total_bytes)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
)
self.progress_bar.total_bytes = total_bytes
# Layout
@ -124,8 +140,9 @@ class ShareHistoryItem(HistoryItem):
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
pb_fmt = strings._('gui_all_modes_progress_complete').format(
self.common.format_seconds(time.time() - self.started))
pb_fmt = strings._("gui_all_modes_progress_complete").format(
self.common.format_seconds(time.time() - self.started)
)
# Change the label
self.label.setText(self.get_finished_label_text(self.started_dt))
@ -137,24 +154,26 @@ class ShareHistoryItem(HistoryItem):
# Wait a couple of seconds for the download rate to stabilize.
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_all_modes_progress_starting').format(
self.common.human_readable_filesize(downloaded_bytes))
pb_fmt = strings._("gui_all_modes_progress_starting").format(
self.common.human_readable_filesize(downloaded_bytes)
)
else:
pb_fmt = strings._('gui_all_modes_progress_eta').format(
pb_fmt = strings._("gui_all_modes_progress_eta").format(
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.estimated_time_remaining,
)
self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
self.progress_bar.setFormat(strings._("gui_canceled"))
self.status = HistoryItem.STATUS_CANCELED
@property
def estimated_time_remaining(self):
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
return self.common.estimated_time_remaining(
self.downloaded_bytes, self.total_bytes, self.started
)
class ReceiveHistoryItemFile(QtWidgets.QWidget):
@ -162,7 +181,9 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
super(ReceiveHistoryItemFile, self).__init__()
self.common = common
self.common.log('ReceiveHistoryItemFile', '__init__', 'filename: {}'.format(filename))
self.common.log(
"ReceiveHistoryItemFile", "__init__", "filename: {}".format(filename)
)
self.filename = filename
self.dir = None
@ -174,11 +195,13 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
# File size label
self.filesize_label = QtWidgets.QLabel()
self.filesize_label.setStyleSheet(self.common.css['receive_file_size'])
self.filesize_label.setStyleSheet(self.common.css["receive_file_size"])
self.filesize_label.hide()
# Folder button
folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png')))
folder_pixmap = QtGui.QPixmap.fromImage(
QtGui.QImage(self.common.get_resource_path("images/open_folder.png"))
)
folder_icon = QtGui.QIcon(folder_pixmap)
self.folder_button = QtWidgets.QPushButton()
self.folder_button.clicked.connect(self.open_folder)
@ -213,29 +236,36 @@ class ReceiveHistoryItemFile(QtWidgets.QWidget):
"""
Open the downloads folder, with the file selected, in a cross-platform manner
"""
self.common.log('ReceiveHistoryItemFile', 'open_folder')
self.common.log("ReceiveHistoryItemFile", "open_folder")
if not self.dir:
self.common.log('ReceiveHistoryItemFile', 'open_folder', "dir has not been set yet, can't open folder")
self.common.log(
"ReceiveHistoryItemFile",
"open_folder",
"dir has not been set yet, can't open folder",
)
return
abs_filename = os.path.join(self.dir, self.filename)
# Linux
if self.common.platform == 'Linux' or self.common.platform == 'BSD':
if self.common.platform == "Linux" or self.common.platform == "BSD":
try:
# If nautilus is available, open it
subprocess.Popen(['nautilus', abs_filename])
subprocess.Popen(["nautilus", abs_filename])
except:
Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename))
Alert(
self.common,
strings._("gui_open_folder_error_nautilus").format(abs_filename),
)
# macOS
elif self.common.platform == 'Darwin':
subprocess.call(['open', '-R', abs_filename])
elif self.common.platform == "Darwin":
subprocess.call(["open", "-R", abs_filename])
# Windows
elif self.common.platform == 'Windows':
subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)])
elif self.common.platform == "Windows":
subprocess.Popen(["explorer", "/select,{}".format(abs_filename)])
class ReceiveHistoryItem(HistoryItem):
@ -248,7 +278,11 @@ class ReceiveHistoryItem(HistoryItem):
self.status = HistoryItem.STATUS_STARTED
# Label
self.label = QtWidgets.QLabel(strings._('gui_all_modes_transfer_started').format(self.started.strftime("%b %d, %I:%M%p")))
self.label = QtWidgets.QLabel(
strings._("gui_all_modes_transfer_started").format(
self.started.strftime("%b %d, %I:%M%p")
)
)
# Progress bar
self.progress_bar = QtWidgets.QProgressBar()
@ -257,13 +291,15 @@ class ReceiveHistoryItem(HistoryItem):
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
)
# This layout contains file widgets
self.files_layout = QtWidgets.QVBoxLayout()
self.files_layout.setContentsMargins(0, 0, 0, 0)
files_widget = QtWidgets.QWidget()
files_widget.setStyleSheet(self.common.css['receive_file'])
files_widget.setStyleSheet(self.common.css["receive_file"])
files_widget.setLayout(self.files_layout)
# Layout
@ -282,10 +318,10 @@ class ReceiveHistoryItem(HistoryItem):
Using the progress from Web, update the progress bar and file size labels
for each file
"""
if data['action'] == 'progress':
if data["action"] == "progress":
total_uploaded_bytes = 0
for filename in data['progress']:
total_uploaded_bytes += data['progress'][filename]['uploaded_bytes']
for filename in data["progress"]:
total_uploaded_bytes += data["progress"][filename]["uploaded_bytes"]
# Update the progress bar
self.progress_bar.setMaximum(self.content_length)
@ -293,35 +329,39 @@ class ReceiveHistoryItem(HistoryItem):
elapsed = datetime.now() - self.started
if elapsed.seconds < 10:
pb_fmt = strings._('gui_all_modes_progress_starting').format(
self.common.human_readable_filesize(total_uploaded_bytes))
pb_fmt = strings._("gui_all_modes_progress_starting").format(
self.common.human_readable_filesize(total_uploaded_bytes)
)
else:
estimated_time_remaining = self.common.estimated_time_remaining(
total_uploaded_bytes,
self.content_length,
self.started.timestamp())
pb_fmt = strings._('gui_all_modes_progress_eta').format(
total_uploaded_bytes, self.content_length, self.started.timestamp()
)
pb_fmt = strings._("gui_all_modes_progress_eta").format(
self.common.human_readable_filesize(total_uploaded_bytes),
estimated_time_remaining)
estimated_time_remaining,
)
# Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration"
for filename in list(data['progress']):
for filename in list(data["progress"]):
# Add a new file if needed
if filename not in self.files:
self.files[filename] = ReceiveHistoryItemFile(self.common, filename)
self.files_layout.addWidget(self.files[filename])
# Update the file
self.files[filename].update(data['progress'][filename]['uploaded_bytes'], data['progress'][filename]['complete'])
self.files[filename].update(
data["progress"][filename]["uploaded_bytes"],
data["progress"][filename]["complete"],
)
elif data['action'] == 'rename':
self.files[data['old_filename']].rename(data['new_filename'])
self.files[data['new_filename']] = self.files.pop(data['old_filename'])
elif data["action"] == "rename":
self.files[data["old_filename"]].rename(data["new_filename"])
self.files[data["new_filename"]] = self.files.pop(data["old_filename"])
elif data['action'] == 'set_dir':
self.files[data['filename']].set_dir(data['dir'])
elif data["action"] == "set_dir":
self.files[data["filename"]].set_dir(data["dir"])
elif data['action'] == 'finished':
elif data["action"] == "finished":
# Change the status
self.status = HistoryItem.STATUS_FINISHED
@ -331,7 +371,7 @@ class ReceiveHistoryItem(HistoryItem):
# Change the label
self.label.setText(self.get_finished_label_text(self.started))
elif data['action'] == 'canceled':
elif data["action"] == "canceled":
# Change the status
self.status = HistoryItem.STATUS_CANCELED
@ -346,6 +386,7 @@ class IndividualFileHistoryItem(HistoryItem):
"""
Individual file history item, for share mode viewing of individual files
"""
def __init__(self, common, data, path):
super(IndividualFileHistoryItem, self).__init__()
self.status = HistoryItem.STATUS_STARTED
@ -359,11 +400,15 @@ class IndividualFileHistoryItem(HistoryItem):
self.started_dt = datetime.fromtimestamp(self.started)
self.status = HistoryItem.STATUS_STARTED
self.directory_listing = 'directory_listing' in data
self.directory_listing = "directory_listing" in data
# Labels
self.timestamp_label = QtWidgets.QLabel(self.started_dt.strftime("%b %d, %I:%M%p"))
self.timestamp_label.setStyleSheet(self.common.css['history_individual_file_timestamp_label'])
self.timestamp_label = QtWidgets.QLabel(
self.started_dt.strftime("%b %d, %I:%M%p")
)
self.timestamp_label.setStyleSheet(
self.common.css["history_individual_file_timestamp_label"]
)
self.path_label = QtWidgets.QLabel("{}".format(self.path))
self.status_code_label = QtWidgets.QLabel()
@ -373,7 +418,9 @@ class IndividualFileHistoryItem(HistoryItem):
self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar'])
self.progress_bar.setStyleSheet(
self.common.css["downloads_uploads_progress_bar"]
)
# Text layout
labels_layout = QtWidgets.QHBoxLayout()
@ -389,21 +436,25 @@ class IndividualFileHistoryItem(HistoryItem):
self.setLayout(layout)
# Is a status code already sent?
if 'status_code' in data:
self.status_code_label.setText("{}".format(data['status_code']))
if data['status_code'] >= 200 and data['status_code'] < 300:
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
if data['status_code'] >= 400 and data['status_code'] < 500:
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_4xx'])
if "status_code" in data:
self.status_code_label.setText("{}".format(data["status_code"]))
if data["status_code"] >= 200 and data["status_code"] < 300:
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_2xx"]
)
if data["status_code"] >= 400 and data["status_code"] < 500:
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_4xx"]
)
self.status = HistoryItem.STATUS_FINISHED
self.progress_bar.hide()
return
else:
self.total_bytes = data['filesize']
self.total_bytes = data["filesize"]
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(data['filesize'])
self.progress_bar.total_bytes = data['filesize']
self.progress_bar.setMaximum(data["filesize"])
self.progress_bar.total_bytes = data["filesize"]
# Start at 0
self.update(0)
@ -414,7 +465,9 @@ class IndividualFileHistoryItem(HistoryItem):
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
self.status_code_label.setText("200")
self.status_code_label.setStyleSheet(self.common.css['history_individual_file_status_code_label_2xx'])
self.status_code_label.setStyleSheet(
self.common.css["history_individual_file_status_code_label_2xx"]
)
self.progress_bar.hide()
self.status = HistoryItem.STATUS_FINISHED
@ -424,30 +477,33 @@ class IndividualFileHistoryItem(HistoryItem):
# Wait a couple of seconds for the download rate to stabilize.
# This prevents a "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = strings._('gui_all_modes_progress_starting').format(
self.common.human_readable_filesize(downloaded_bytes))
pb_fmt = strings._("gui_all_modes_progress_starting").format(
self.common.human_readable_filesize(downloaded_bytes)
)
else:
pb_fmt = strings._('gui_all_modes_progress_eta').format(
pb_fmt = strings._("gui_all_modes_progress_eta").format(
self.common.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.estimated_time_remaining,
)
self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
self.progress_bar.setFormat(strings._("gui_canceled"))
self.status = HistoryItem.STATUS_CANCELED
@property
def estimated_time_remaining(self):
return self.common.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
return self.common.estimated_time_remaining(
self.downloaded_bytes, self.total_bytes, self.started
)
class HistoryItemList(QtWidgets.QScrollArea):
"""
List of items
"""
def __init__(self, common):
super(HistoryItemList, self).__init__()
self.common = common
@ -511,12 +567,14 @@ class HistoryItemList(QtWidgets.QScrollArea):
item.close()
del self.items[key]
class History(QtWidgets.QWidget):
"""
A history of what's happened so far in this mode. This contains an internal
object full of a scrollable list of items.
"""
def __init__(self, common, empty_image, empty_text, header_text, mode=''):
def __init__(self, common, empty_image, empty_text, header_text, mode=""):
super(History, self).__init__()
self.common = common
self.mode = mode
@ -530,17 +588,19 @@ class History(QtWidgets.QWidget):
# In progress, completed, and requests labels
self.in_progress_label = QtWidgets.QLabel()
self.in_progress_label.setStyleSheet(self.common.css['mode_info_label'])
self.in_progress_label.setStyleSheet(self.common.css["mode_info_label"])
self.completed_label = QtWidgets.QLabel()
self.completed_label.setStyleSheet(self.common.css['mode_info_label'])
self.completed_label.setStyleSheet(self.common.css["mode_info_label"])
self.requests_label = QtWidgets.QLabel()
self.requests_label.setStyleSheet(self.common.css['mode_info_label'])
self.requests_label.setStyleSheet(self.common.css["mode_info_label"])
# Header
self.header_label = QtWidgets.QLabel(header_text)
self.header_label.setStyleSheet(self.common.css['downloads_uploads_label'])
self.clear_button = QtWidgets.QPushButton(strings._('gui_all_modes_clear_history'))
self.clear_button.setStyleSheet(self.common.css['downloads_uploads_clear'])
self.header_label.setStyleSheet(self.common.css["downloads_uploads_label"])
self.clear_button = QtWidgets.QPushButton(
strings._("gui_all_modes_clear_history")
)
self.clear_button.setStyleSheet(self.common.css["downloads_uploads_clear"])
self.clear_button.setFlat(True)
self.clear_button.clicked.connect(self.reset)
header_layout = QtWidgets.QHBoxLayout()
@ -557,14 +617,14 @@ class History(QtWidgets.QWidget):
self.empty_image.setPixmap(empty_image)
self.empty_text = QtWidgets.QLabel(empty_text)
self.empty_text.setAlignment(QtCore.Qt.AlignCenter)
self.empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text'])
self.empty_text.setStyleSheet(self.common.css["downloads_uploads_empty_text"])
empty_layout = QtWidgets.QVBoxLayout()
empty_layout.addStretch()
empty_layout.addWidget(self.empty_image)
empty_layout.addWidget(self.empty_text)
empty_layout.addStretch()
self.empty = QtWidgets.QWidget()
self.empty.setStyleSheet(self.common.css['downloads_uploads_empty'])
self.empty.setStyleSheet(self.common.css["downloads_uploads_empty"])
self.empty.setLayout(empty_layout)
# When there are items
@ -589,7 +649,7 @@ class History(QtWidgets.QWidget):
"""
Add a new item.
"""
self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item))
self.common.log("History", "add", "id: {}, item: {}".format(id, item))
# Hide empty, show not empty
self.empty.hide()
@ -636,35 +696,47 @@ class History(QtWidgets.QWidget):
Update the 'completed' widget.
"""
if self.completed_count == 0:
image = self.common.get_resource_path('images/history_completed_none.png')
image = self.common.get_resource_path("images/history_completed_none.png")
else:
image = self.common.get_resource_path('images/history_completed.png')
self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count))
self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count))
image = self.common.get_resource_path("images/history_completed.png")
self.completed_label.setText(
'<img src="{0:s}" /> {1:d}'.format(image, self.completed_count)
)
self.completed_label.setToolTip(
strings._("history_completed_tooltip").format(self.completed_count)
)
def update_in_progress(self):
"""
Update the 'in progress' widget.
"""
if self.in_progress_count == 0:
image = self.common.get_resource_path('images/history_in_progress_none.png')
image = self.common.get_resource_path("images/history_in_progress_none.png")
else:
image = self.common.get_resource_path('images/history_in_progress.png')
image = self.common.get_resource_path("images/history_in_progress.png")
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))
self.in_progress_label.setText(
'<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count)
)
self.in_progress_label.setToolTip(
strings._("history_in_progress_tooltip").format(self.in_progress_count)
)
def update_requests(self):
"""
Update the 'web requests' widget.
"""
if self.requests_count == 0:
image = self.common.get_resource_path('images/history_requests_none.png')
image = self.common.get_resource_path("images/history_requests_none.png")
else:
image = self.common.get_resource_path('images/history_requests.png')
image = self.common.get_resource_path("images/history_requests.png")
self.requests_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.requests_count))
self.requests_label.setToolTip(strings._('history_requests_tooltip').format(self.requests_count))
self.requests_label.setText(
'<img src="{0:s}" /> {1:d}'.format(image, self.requests_count)
)
self.requests_label.setToolTip(
strings._("history_requests_tooltip").format(self.requests_count)
)
class ToggleHistory(QtWidgets.QPushButton):
@ -672,6 +744,7 @@ class ToggleHistory(QtWidgets.QPushButton):
Widget for toggling showing or hiding the history, as well as keeping track
of the indicator counter if it's hidden
"""
def __init__(self, common, current_mode, history_widget, icon, selected_icon):
super(ToggleHistory, self).__init__()
self.common = common
@ -691,7 +764,9 @@ class ToggleHistory(QtWidgets.QPushButton):
# Keep track of indicator
self.indicator_count = 0
self.indicator_label = QtWidgets.QLabel(parent=self)
self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator'])
self.indicator_label.setStyleSheet(
self.common.css["download_uploads_indicator"]
)
self.update_indicator()
def update_indicator(self, increment=False):
@ -708,14 +783,16 @@ class ToggleHistory(QtWidgets.QPushButton):
self.indicator_label.hide()
else:
size = self.indicator_label.sizeHint()
self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height())
self.indicator_label.setGeometry(
35 - size.width(), 0, size.width(), size.height()
)
self.indicator_label.show()
def toggle_clicked(self):
"""
Toggle showing and hiding the history widget
"""
self.common.log('ToggleHistory', 'toggle_clicked')
self.common.log("ToggleHistory", "toggle_clicked")
if self.history_widget.isVisible():
self.history_widget.hide()

View File

@ -25,19 +25,21 @@ from onionshare.web import Web
from ..history import History, ToggleHistory, ReceiveHistoryItem
from .. import Mode
class ReceiveMode(Mode):
"""
Parts of the main window UI for receiving files.
"""
def init(self):
"""
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, 'receive')
self.web = Web(self.common, True, "receive")
# Server status
self.server_status.set_mode('receive')
self.server_status.set_mode("receive")
self.server_status.server_started_finished.connect(self.update_primary_action)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.update_primary_action)
@ -49,21 +51,31 @@ class ReceiveMode(Mode):
# Upload history
self.history = History(
self.common,
QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/receive_icon_transparent.png'))),
strings._('gui_receive_mode_no_files'),
strings._('gui_all_modes_history')
QtGui.QPixmap.fromImage(
QtGui.QImage(
self.common.get_resource_path("images/receive_icon_transparent.png")
)
),
strings._("gui_receive_mode_no_files"),
strings._("gui_all_modes_history"),
)
self.history.hide()
# Toggle history
self.toggle_history = ToggleHistory(
self.common, self, self.history,
QtGui.QIcon(self.common.get_resource_path('images/receive_icon_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/receive_icon_toggle_selected.png'))
self.common,
self,
self.history,
QtGui.QIcon(
self.common.get_resource_path("images/receive_icon_toggle.png")
),
QtGui.QIcon(
self.common.get_resource_path("images/receive_icon_toggle_selected.png")
),
)
# Receive mode warning
receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning'))
receive_warning = QtWidgets.QLabel(strings._("gui_receive_mode_warning"))
receive_warning.setMinimumHeight(80)
receive_warning.setWordWrap(True)
@ -90,20 +102,25 @@ class ReceiveMode(Mode):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._('gui_receive_stop_server_autostop_timer')
return strings._("gui_receive_stop_server_autostop_timer")
def autostop_timer_finished_should_stop_server(self):
"""
The auto-stop timer expired, should we stop the server? Returns a bool
"""
# If there were no attempts to upload files, or all uploads are done, we can stop
if self.web.receive_mode.cur_history_id == 0 or not self.web.receive_mode.uploads_in_progress:
if (
self.web.receive_mode.cur_history_id == 0
or not self.web.receive_mode.uploads_in_progress
):
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
self.server_status_label.setText(strings._("close_on_autostop_timer"))
return True
# An upload is probably still running - hold off on stopping the share, but block new shares.
else:
self.server_status_label.setText(strings._('gui_receive_mode_autostop_timer_waiting'))
self.server_status_label.setText(
strings._("gui_receive_mode_autostop_timer_waiting")
)
self.web.receive_mode.can_upload = False
return False
@ -136,56 +153,68 @@ class ReceiveMode(Mode):
"""
Handle REQUEST_LOAD event.
"""
self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_page_loaded_message'))
self.system_tray.showMessage(
strings._("systray_page_loaded_title"),
strings._("systray_page_loaded_message"),
)
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
item = ReceiveHistoryItem(self.common, event["data"]["id"], event["data"]["content_length"])
item = ReceiveHistoryItem(
self.common, event["data"]["id"], event["data"]["content_length"]
)
self.history.add(event["data"]["id"], item)
self.toggle_history.update_indicator(True)
self.history.in_progress_count += 1
self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_receive_started_title'), strings._('systray_receive_started_message'))
self.system_tray.showMessage(
strings._("systray_receive_started_title"),
strings._("systray_receive_started_message"),
)
def handle_request_progress(self, event):
"""
Handle REQUEST_PROGRESS event.
"""
self.history.update(event["data"]["id"], {
'action': 'progress',
'progress': event["data"]["progress"]
})
self.history.update(
event["data"]["id"],
{"action": "progress", "progress": event["data"]["progress"]},
)
def handle_request_upload_file_renamed(self, event):
"""
Handle REQUEST_UPLOAD_FILE_RENAMED event.
"""
self.history.update(event["data"]["id"], {
'action': 'rename',
'old_filename': event["data"]["old_filename"],
'new_filename': event["data"]["new_filename"]
})
self.history.update(
event["data"]["id"],
{
"action": "rename",
"old_filename": event["data"]["old_filename"],
"new_filename": event["data"]["new_filename"],
},
)
def handle_request_upload_set_dir(self, event):
"""
Handle REQUEST_UPLOAD_SET_DIR event.
"""
self.history.update(event["data"]["id"], {
'action': 'set_dir',
'filename': event["data"]["filename"],
'dir': event["data"]["dir"]
})
self.history.update(
event["data"]["id"],
{
"action": "set_dir",
"filename": event["data"]["filename"],
"dir": event["data"]["dir"],
},
)
def handle_request_upload_finished(self, event):
"""
Handle REQUEST_UPLOAD_FINISHED event.
"""
self.history.update(event["data"]["id"], {
'action': 'finished'
})
self.history.update(event["data"]["id"], {"action": "finished"})
self.history.completed_count += 1
self.history.in_progress_count -= 1
self.history.update_completed()
@ -195,9 +224,7 @@ class ReceiveMode(Mode):
"""
Handle REQUEST_UPLOAD_CANCELED event.
"""
self.history.update(event["data"]["id"], {
'action': 'canceled'
})
self.history.update(event["data"]["id"], {"action": "canceled"})
self.history.in_progress_count -= 1
self.history.update_in_progress()
@ -216,4 +243,4 @@ class ReceiveMode(Mode):
self.toggle_history.update_indicator()
def update_primary_action(self):
self.common.log('ReceiveMode', 'update_primary_action')
self.common.log("ReceiveMode", "update_primary_action")

View File

@ -36,6 +36,7 @@ class ShareMode(Mode):
"""
Parts of the main window UI for sharing files.
"""
def init(self):
"""
Custom initialization for ReceiveMode.
@ -44,7 +45,7 @@ class ShareMode(Mode):
self.compress_thread = None
# Create the Web object
self.web = Web(self.common, True, 'share')
self.web = Web(self.common, True, "share")
# File selection
self.file_selection = FileSelection(self.common, self)
@ -53,7 +54,7 @@ class ShareMode(Mode):
self.file_selection.file_list.add_file(filename)
# Server status
self.server_status.set_mode('share', self.file_selection)
self.server_status.set_mode("share", self.file_selection)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.update_primary_action)
@ -68,15 +69,19 @@ class ShareMode(Mode):
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"])
self.filesize_warning.hide()
# Download history
self.history = History(
self.common,
QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/share_icon_transparent.png'))),
strings._('gui_share_mode_no_files'),
strings._('gui_all_modes_history')
QtGui.QPixmap.fromImage(
QtGui.QImage(
self.common.get_resource_path("images/share_icon_transparent.png")
)
),
strings._("gui_share_mode_no_files"),
strings._("gui_all_modes_history"),
)
self.history.hide()
@ -86,9 +91,13 @@ class ShareMode(Mode):
# Toggle history
self.toggle_history = ToggleHistory(
self.common, self, self.history,
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle_selected.png'))
self.common,
self,
self.history,
QtGui.QIcon(self.common.get_resource_path("images/share_icon_toggle.png")),
QtGui.QIcon(
self.common.get_resource_path("images/share_icon_toggle_selected.png")
),
)
# Top bar
@ -125,7 +134,7 @@ class ShareMode(Mode):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._('gui_share_stop_server_autostop_timer')
return strings._("gui_share_stop_server_autostop_timer")
def autostop_timer_finished_should_stop_server(self):
"""
@ -134,11 +143,13 @@ class ShareMode(Mode):
# If there were no attempts to download the share, or all downloads are done, we can stop
if self.web.share_mode.cur_history_id == 0 or self.web.done:
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
self.server_status_label.setText(strings._("close_on_autostop_timer"))
return True
# A download is probably still running - hold off on stopping the share
else:
self.server_status_label.setText(strings._('gui_share_mode_autostop_timer_waiting'))
self.server_status_label.setText(
strings._("gui_share_mode_autostop_timer_waiting")
)
return False
def start_server_custom(self):
@ -162,7 +173,9 @@ class ShareMode(Mode):
for index in range(self.file_selection.file_list.count()):
self.filenames.append(self.file_selection.file_list.item(index).filename)
self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames)
self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(
self.filenames
)
self.status_bar.insertWidget(0, self._zip_progress_bar)
# prepare the files for sending in a new thread
@ -216,7 +229,7 @@ class ShareMode(Mode):
Stop the compression thread on cancel
"""
if self.compress_thread:
self.common.log('ShareMode', 'cancel_server: quitting compress thread')
self.common.log("ShareMode", "cancel_server: quitting compress thread")
self.compress_thread.quit()
def handle_tor_broke_custom(self):
@ -240,7 +253,10 @@ class ShareMode(Mode):
self.history.in_progress_count += 1
self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_share_started_title'), strings._('systray_share_started_message'))
self.system_tray.showMessage(
strings._("systray_share_started_title"),
strings._("systray_share_started_message"),
)
def handle_request_progress(self, event):
"""
@ -250,7 +266,10 @@ class ShareMode(Mode):
# Is the download complete?
if event["data"]["bytes"] == self.web.share_mode.filesize:
self.system_tray.showMessage(strings._('systray_share_completed_title'), strings._('systray_share_completed_message'))
self.system_tray.showMessage(
strings._("systray_share_completed_title"),
strings._("systray_share_completed_message"),
)
# Update completed and in progress labels
self.history.completed_count += 1
@ -259,10 +278,10 @@ class ShareMode(Mode):
self.history.update_in_progress()
# Close on finish?
if self.common.settings.get('close_after_first_download'):
if self.common.settings.get("close_after_first_download"):
self.server_status.stop_server()
self.status_bar.clearMessage()
self.server_status_label.setText(strings._('closing_automatically'))
self.server_status_label.setText(strings._("closing_automatically"))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.history.cancel(event["data"]["id"])
@ -278,7 +297,10 @@ class ShareMode(Mode):
# Update in progress count
self.history.in_progress_count -= 1
self.history.update_in_progress()
self.system_tray.showMessage(strings._('systray_share_canceled_title'), strings._('systray_share_canceled_message'))
self.system_tray.showMessage(
strings._("systray_share_canceled_title"),
strings._("systray_share_canceled_message"),
)
def on_reload_settings(self):
"""
@ -290,7 +312,7 @@ class ShareMode(Mode):
self.info_label.show()
def update_primary_action(self):
self.common.log('ShareMode', 'update_primary_action')
self.common.log("ShareMode", "update_primary_action")
# Show or hide primary action layout
file_count = self.file_selection.file_list.count()
@ -306,9 +328,15 @@ class ShareMode(Mode):
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
if file_count > 1:
self.info_label.setText(strings._('gui_file_info').format(file_count, total_size_readable))
self.info_label.setText(
strings._("gui_file_info").format(file_count, total_size_readable)
)
else:
self.info_label.setText(strings._('gui_file_info_single').format(file_count, total_size_readable))
self.info_label.setText(
strings._("gui_file_info_single").format(
file_count, total_size_readable
)
)
else:
self.primary_action.hide()
@ -343,8 +371,8 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setMaximumHeight(20)
self.setMinimumWidth(200)
self.setValue(0)
self.setFormat(strings._('zip_progress_bar_format'))
self.setStyleSheet(self.common.css['share_zip_progess_bar'])
self.setFormat(strings._("zip_progress_bar_format"))
self.setStyleSheet(self.common.css["share_zip_progess_bar"])
self._total_files_size = total_files_size
self._processed_size = 0

View File

@ -24,13 +24,14 @@ class CompressThread(QtCore.QThread):
"""
Compresses files to be shared
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
super(CompressThread, self).__init__()
self.mode = mode
self.mode.common.log('CompressThread', '__init__')
self.mode.common.log("CompressThread", "__init__")
# prepare files to share
def set_processed_size(self, x):
@ -38,17 +39,21 @@ class CompressThread(QtCore.QThread):
self.mode._zip_progress_bar.update_processed_size_signal.emit(x)
def run(self):
self.mode.common.log('CompressThread', 'run')
self.mode.common.log("CompressThread", "run")
try:
self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size)
self.mode.web.share_mode.set_file_info(
self.mode.filenames, processed_size_callback=self.set_processed_size
)
self.success.emit()
self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames
self.mode.app.cleanup_filenames += (
self.mode.web.share_mode.cleanup_filenames
)
except OSError as e:
self.error.emit(e.strerror)
def cancel(self):
self.mode.common.log('CompressThread', 'cancel')
self.mode.common.log("CompressThread", "cancel")
# Let the Web and ZipWriter objects know that we're canceling compression early
self.mode.web.cancel_compression = True

View File

@ -33,10 +33,12 @@ from .. import Mode
from ..history import History, ToggleHistory
from ...widgets import Alert
class WebsiteMode(Mode):
"""
Parts of the main window UI for sharing files.
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
@ -45,7 +47,7 @@ class WebsiteMode(Mode):
Custom initialization for ReceiveMode.
"""
# Create the Web object
self.web = Web(self.common, True, 'website')
self.web = Web(self.common, True, "website")
# File selection
self.file_selection = FileSelection(self.common, self)
@ -54,7 +56,7 @@ class WebsiteMode(Mode):
self.file_selection.file_list.add_file(filename)
# Server status
self.server_status.set_mode('website', self.file_selection)
self.server_status.set_mode("website", self.file_selection)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.update_primary_action)
@ -69,16 +71,20 @@ class WebsiteMode(Mode):
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning'])
self.filesize_warning.setStyleSheet(self.common.css["share_filesize_warning"])
self.filesize_warning.hide()
# Download history
self.history = History(
self.common,
QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/share_icon_transparent.png'))),
strings._('gui_website_mode_no_files'),
strings._('gui_all_modes_history'),
'website'
QtGui.QPixmap.fromImage(
QtGui.QImage(
self.common.get_resource_path("images/share_icon_transparent.png")
)
),
strings._("gui_website_mode_no_files"),
strings._("gui_all_modes_history"),
"website",
)
self.history.in_progress_label.hide()
self.history.completed_label.hide()
@ -90,9 +96,13 @@ class WebsiteMode(Mode):
# Toggle history
self.toggle_history = ToggleHistory(
self.common, self, self.history,
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle_selected.png'))
self.common,
self,
self.history,
QtGui.QIcon(self.common.get_resource_path("images/share_icon_toggle.png")),
QtGui.QIcon(
self.common.get_resource_path("images/share_icon_toggle_selected.png")
),
)
# Top bar
@ -126,7 +136,7 @@ class WebsiteMode(Mode):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._('gui_share_stop_server_autostop_timer')
return strings._("gui_share_stop_server_autostop_timer")
def autostop_timer_finished_should_stop_server(self):
"""
@ -134,10 +144,9 @@ class WebsiteMode(Mode):
"""
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
self.server_status_label.setText(strings._("close_on_autostop_timer"))
return True
def start_server_custom(self):
"""
Starting the server.
@ -161,7 +170,6 @@ class WebsiteMode(Mode):
self.starting_server_step3.emit()
self.start_server_finished.emit()
def start_server_step3_custom(self):
"""
Step 3 in starting the server. Display large filesize
@ -191,8 +199,7 @@ class WebsiteMode(Mode):
"""
Log that the server has been cancelled
"""
self.common.log('WebsiteMode', 'cancel_server')
self.common.log("WebsiteMode", "cancel_server")
def handle_tor_broke_custom(self):
"""
@ -210,7 +217,7 @@ class WebsiteMode(Mode):
self.info_label.show()
def update_primary_action(self):
self.common.log('WebsiteMode', 'update_primary_action')
self.common.log("WebsiteMode", "update_primary_action")
# Show or hide primary action layout
file_count = self.file_selection.file_list.count()
@ -226,9 +233,15 @@ class WebsiteMode(Mode):
total_size_readable = self.common.human_readable_filesize(total_size_bytes)
if file_count > 1:
self.info_label.setText(strings._('gui_file_info').format(file_count, total_size_readable))
self.info_label.setText(
strings._("gui_file_info").format(file_count, total_size_readable)
)
else:
self.info_label.setText(strings._('gui_file_info_single').format(file_count, total_size_readable))
self.info_label.setText(
strings._("gui_file_info_single").format(
file_count, total_size_readable
)
)
else:
self.primary_action.hide()

View File

@ -33,20 +33,24 @@ from .widgets import Alert
from .update_checker import UpdateThread
from .server_status import ServerStatus
class OnionShareGui(QtWidgets.QMainWindow):
"""
OnionShareGui is the main window for the GUI that contains all of the
GUI elements.
"""
MODE_SHARE = 'share'
MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False):
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
def __init__(
self, common, onion, qtapp, app, filenames, config=False, local_only=False
):
super(OnionShareGui, self).__init__()
self.common = common
self.common.log('OnionShareGui', '__init__')
self.common.log("OnionShareGui", "__init__")
self.setMinimumWidth(820)
self.setMinimumHeight(660)
@ -57,8 +61,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.mode = self.MODE_SHARE
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setWindowTitle("OnionShare")
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
# Load settings, if a custom config was passed in
self.config = config
@ -71,40 +77,52 @@ class OnionShareGui(QtWidgets.QMainWindow):
# System tray
menu = QtWidgets.QMenu()
self.settings_action = menu.addAction(strings._('gui_settings_window_title'))
self.settings_action = menu.addAction(strings._("gui_settings_window_title"))
self.settings_action.triggered.connect(self.open_settings)
self.help_action = menu.addAction(strings._('gui_settings_button_help'))
self.help_action = menu.addAction(strings._("gui_settings_button_help"))
self.help_action.triggered.connect(lambda: SettingsDialog.help_clicked(self))
exit_action = menu.addAction(strings._('systray_menu_exit'))
exit_action = menu.addAction(strings._("systray_menu_exit"))
exit_action.triggered.connect(self.close)
self.system_tray = QtWidgets.QSystemTrayIcon(self)
# The convention is Mac systray icons are always grayscale
if self.common.platform == 'Darwin':
self.system_tray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo_grayscale.png')))
if self.common.platform == "Darwin":
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo_grayscale.png"))
)
else:
self.system_tray.setIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.system_tray.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
self.system_tray.setContextMenu(menu)
self.system_tray.show()
# Mode switcher, to switch between share files and receive files
self.share_mode_button = QtWidgets.QPushButton(strings._('gui_mode_share_button'));
self.share_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_share_button")
)
self.share_mode_button.setFixedHeight(50)
self.share_mode_button.clicked.connect(self.share_mode_clicked)
self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button'));
self.receive_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_receive_button")
)
self.receive_mode_button.setFixedHeight(50)
self.receive_mode_button.clicked.connect(self.receive_mode_clicked)
self.website_mode_button = QtWidgets.QPushButton(strings._('gui_mode_website_button'));
self.website_mode_button = QtWidgets.QPushButton(
strings._("gui_mode_website_button")
)
self.website_mode_button.setFixedHeight(50)
self.website_mode_button.clicked.connect(self.website_mode_clicked)
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFixedWidth(40)
self.settings_button.setFixedHeight(50)
self.settings_button.setIcon( QtGui.QIcon(self.common.get_resource_path('images/settings.png')) )
self.settings_button.setIcon(
QtGui.QIcon(self.common.get_resource_path("images/settings.png"))
)
self.settings_button.clicked.connect(self.open_settings)
self.settings_button.setStyleSheet(self.common.css['settings_button'])
mode_switcher_layout = QtWidgets.QHBoxLayout();
self.settings_button.setStyleSheet(self.common.css["settings_button"])
mode_switcher_layout = QtWidgets.QHBoxLayout()
mode_switcher_layout.setSpacing(0)
mode_switcher_layout.addWidget(self.share_mode_button)
mode_switcher_layout.addWidget(self.receive_mode_button)
@ -112,13 +130,21 @@ class OnionShareGui(QtWidgets.QMainWindow):
mode_switcher_layout.addWidget(self.settings_button)
# Server status indicator on the status bar
self.server_status_image_stopped = QtGui.QImage(self.common.get_resource_path('images/server_stopped.png'))
self.server_status_image_working = QtGui.QImage(self.common.get_resource_path('images/server_working.png'))
self.server_status_image_started = QtGui.QImage(self.common.get_resource_path('images/server_started.png'))
self.server_status_image_stopped = QtGui.QImage(
self.common.get_resource_path("images/server_stopped.png")
)
self.server_status_image_working = QtGui.QImage(
self.common.get_resource_path("images/server_working.png")
)
self.server_status_image_started = QtGui.QImage(
self.common.get_resource_path("images/server_started.png")
)
self.server_status_image_label = QtWidgets.QLabel()
self.server_status_image_label.setFixedWidth(20)
self.server_status_label = QtWidgets.QLabel('')
self.server_status_label.setStyleSheet(self.common.css['server_status_indicator_label'])
self.server_status_label = QtWidgets.QLabel("")
self.server_status_label.setStyleSheet(
self.common.css["server_status_indicator_label"]
)
server_status_indicator_layout = QtWidgets.QHBoxLayout()
server_status_indicator_layout.addWidget(self.server_status_image_label)
server_status_indicator_layout.addWidget(self.server_status_label)
@ -128,17 +154,34 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Status bar
self.status_bar = QtWidgets.QStatusBar()
self.status_bar.setSizeGripEnabled(False)
self.status_bar.setStyleSheet(self.common.css['status_bar'])
self.status_bar.setStyleSheet(self.common.css["status_bar"])
self.status_bar.addPermanentWidget(self.server_status_indicator)
self.setStatusBar(self.status_bar)
# Share mode
self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only)
self.share_mode = ShareMode(
self.common,
qtapp,
app,
self.status_bar,
self.server_status_label,
self.system_tray,
filenames,
self.local_only,
)
self.share_mode.init()
self.share_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
self.share_mode.start_server_finished.connect(self.update_server_status_indicator)
self.share_mode.stop_server_finished.connect(self.update_server_status_indicator)
self.share_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.share_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.share_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.share_mode.stop_server_finished.connect(self.stop_server_finished)
self.share_mode.start_server_finished.connect(self.clear_message)
self.share_mode.server_status.button_clicked.connect(self.clear_message)
@ -147,31 +190,68 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.set_server_active.connect(self.set_server_active)
# Receive mode
self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only)
self.receive_mode = ReceiveMode(
self.common,
qtapp,
app,
self.status_bar,
self.server_status_label,
self.system_tray,
None,
self.local_only,
)
self.receive_mode.init()
self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
self.receive_mode.start_server_finished.connect(self.update_server_status_indicator)
self.receive_mode.stop_server_finished.connect(self.update_server_status_indicator)
self.receive_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.receive_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.receive_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.receive_mode.stop_server_finished.connect(self.stop_server_finished)
self.receive_mode.start_server_finished.connect(self.clear_message)
self.receive_mode.server_status.button_clicked.connect(self.clear_message)
self.receive_mode.server_status.url_copied.connect(self.copy_url)
self.receive_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.receive_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.receive_mode.set_server_active.connect(self.set_server_active)
# Website mode
self.website_mode = WebsiteMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames)
self.website_mode = WebsiteMode(
self.common,
qtapp,
app,
self.status_bar,
self.server_status_label,
self.system_tray,
filenames,
)
self.website_mode.init()
self.website_mode.server_status.server_started.connect(self.update_server_status_indicator)
self.website_mode.server_status.server_stopped.connect(self.update_server_status_indicator)
self.website_mode.start_server_finished.connect(self.update_server_status_indicator)
self.website_mode.stop_server_finished.connect(self.update_server_status_indicator)
self.website_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.website_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.website_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.website_mode.stop_server_finished.connect(self.stop_server_finished)
self.website_mode.start_server_finished.connect(self.clear_message)
self.website_mode.server_status.button_clicked.connect(self.clear_message)
self.website_mode.server_status.url_copied.connect(self.copy_url)
self.website_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.website_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.website_mode.set_server_active.connect(self.set_server_active)
self.update_mode_switcher()
@ -218,25 +298,43 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Based on the current mode, switch the mode switcher button styles,
# and show and hide widgets to switch modes
if self.mode == self.MODE_SHARE:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode.hide()
self.share_mode.show()
self.website_mode.hide()
elif self.mode == self.MODE_WEBSITE:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.receive_mode.hide()
self.share_mode.hide()
self.website_mode.show()
else:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style'])
self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style'])
self.share_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.receive_mode_button.setStyleSheet(
self.common.css["mode_switcher_selected_style"]
)
self.website_mode_button.setStyleSheet(
self.common.css["mode_switcher_unselected_style"]
)
self.share_mode.hide()
self.receive_mode.show()
@ -246,19 +344,19 @@ class OnionShareGui(QtWidgets.QMainWindow):
def share_mode_clicked(self):
if self.mode != self.MODE_SHARE:
self.common.log('OnionShareGui', 'share_mode_clicked')
self.common.log("OnionShareGui", "share_mode_clicked")
self.mode = self.MODE_SHARE
self.update_mode_switcher()
def receive_mode_clicked(self):
if self.mode != self.MODE_RECEIVE:
self.common.log('OnionShareGui', 'receive_mode_clicked')
self.common.log("OnionShareGui", "receive_mode_clicked")
self.mode = self.MODE_RECEIVE
self.update_mode_switcher()
def website_mode_clicked(self):
if self.mode != self.MODE_WEBSITE:
self.common.log('OnionShareGui', 'website_mode_clicked')
self.common.log("OnionShareGui", "website_mode_clicked")
self.mode = self.MODE_WEBSITE
self.update_mode_switcher()
@ -267,42 +365,82 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.mode == self.MODE_SHARE:
# Share mode
if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
self.server_status_label.setText(strings._('gui_status_indicator_share_stopped'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_stopped")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
if self.share_mode.server_status.autostart_timer_datetime:
self.server_status_label.setText(strings._('gui_status_indicator_share_scheduled'))
self.server_status_label.setText(
strings._("gui_status_indicator_share_scheduled")
)
else:
self.server_status_label.setText(strings._('gui_status_indicator_share_working'))
self.server_status_label.setText(
strings._("gui_status_indicator_share_working")
)
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_share_started'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_started")
)
elif self.mode == self.MODE_WEBSITE:
# Website mode
if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
self.server_status_label.setText(strings._('gui_status_indicator_share_stopped'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_stopped")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
self.server_status_label.setText(strings._('gui_status_indicator_share_working'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_working")
)
elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_share_started'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_share_started")
)
else:
# Receive mode
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_stopped)
)
self.server_status_label.setText(
strings._("gui_status_indicator_receive_stopped")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_working)
)
if self.receive_mode.server_status.autostart_timer_datetime:
self.server_status_label.setText(strings._('gui_status_indicator_receive_scheduled'))
self.server_status_label.setText(
strings._("gui_status_indicator_receive_scheduled")
)
else:
self.server_status_label.setText(strings._('gui_status_indicator_receive_working'))
self.server_status_label.setText(
strings._("gui_status_indicator_receive_working")
)
elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED:
self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_receive_started'))
self.server_status_image_label.setPixmap(
QtGui.QPixmap.fromImage(self.server_status_image_started)
)
self.server_status_label.setText(
strings._("gui_status_indicator_receive_started")
)
def stop_server_finished(self):
# When the server stopped, cleanup the ephemeral onion service
@ -313,12 +451,22 @@ class OnionShareGui(QtWidgets.QMainWindow):
If the user cancels before Tor finishes connecting, ask if they want to
quit, or open settings.
"""
self.common.log('OnionShareGui', '_tor_connection_canceled')
self.common.log("OnionShareGui", "_tor_connection_canceled")
def ask():
a = Alert(self.common, strings._('gui_tor_connection_ask'), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False)
settings_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_open_settings'))
quit_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_quit'))
a = Alert(
self.common,
strings._("gui_tor_connection_ask"),
QtWidgets.QMessageBox.Question,
buttons=QtWidgets.QMessageBox.NoButton,
autostart=False,
)
settings_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_open_settings")
)
quit_button = QtWidgets.QPushButton(
strings._("gui_tor_connection_ask_quit")
)
a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole)
a.addButton(quit_button, QtWidgets.QMessageBox.RejectRole)
a.setDefaultButton(settings_button)
@ -326,12 +474,18 @@ class OnionShareGui(QtWidgets.QMainWindow):
if a.clickedButton() == settings_button:
# Open settings
self.common.log('OnionShareGui', '_tor_connection_canceled', 'Settings button clicked')
self.common.log(
"OnionShareGui",
"_tor_connection_canceled",
"Settings button clicked",
)
self.open_settings()
if a.clickedButton() == quit_button:
# Quit
self.common.log('OnionShareGui', '_tor_connection_canceled', 'Quit button clicked')
self.common.log(
"OnionShareGui", "_tor_connection_canceled", "Quit button clicked"
)
# Wait 1ms for the event loop to finish, then quit
QtCore.QTimer.singleShot(1, self.qtapp.quit)
@ -343,7 +497,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
The TorConnectionDialog wants to open the Settings dialog
"""
self.common.log('OnionShareGui', '_tor_connection_open_settings')
self.common.log("OnionShareGui", "_tor_connection_open_settings")
# Wait 1ms for the event loop to finish closing the TorConnectionDialog
QtCore.QTimer.singleShot(1, self.open_settings)
@ -352,10 +506,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Open the SettingsDialog.
"""
self.common.log('OnionShareGui', 'open_settings')
self.common.log("OnionShareGui", "open_settings")
def reload_settings():
self.common.log('OnionShareGui', 'open_settings', 'settings have changed, reloading')
self.common.log(
"OnionShareGui", "open_settings", "settings have changed, reloading"
)
self.common.settings.load()
# We might've stopped the main requests timer if a Tor connection failed.
@ -371,12 +527,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.status_bar.clearMessage()
# If we switched off the auto-stop timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostop_timer'):
if not self.common.settings.get("autostop_timer"):
self.share_mode.server_status.autostop_timer_container.hide()
self.receive_mode.server_status.autostop_timer_container.hide()
self.website_mode.server_status.autostop_timer_container.hide()
# If we switched off the auto-start timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostart_timer'):
if not self.common.settings.get("autostart_timer"):
self.share_mode.server_status.autostart_timer_datetime = None
self.receive_mode.server_status.autostart_timer_datetime = None
self.website_mode.server_status.autostart_timer_datetime = None
@ -384,7 +540,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.receive_mode.server_status.autostart_timer_container.hide()
self.website_mode.server_status.autostart_timer_container.hide()
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d = SettingsDialog(
self.common, self.onion, self.qtapp, self.config, self.local_only
)
d.settings_saved.connect(reload_settings)
d.exec_()
@ -397,10 +555,16 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Check for updates in a new thread, if enabled.
"""
if self.common.platform == 'Windows' or self.common.platform == 'Darwin':
if self.common.settings.get('use_autoupdate'):
if self.common.platform == "Windows" or self.common.platform == "Darwin":
if self.common.settings.get("use_autoupdate"):
def update_available(update_url, installed_version, latest_version):
Alert(self.common, strings._("update_available").format(update_url, installed_version, latest_version))
Alert(
self.common,
strings._("update_available").format(
update_url, installed_version, latest_version
),
)
self.update_thread = UpdateThread(self.common, self.onion, self.config)
self.update_thread.update_available.connect(update_available)
@ -417,8 +581,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Have we lost connection to Tor somehow?
if not self.onion.is_authenticated():
self.timer.stop()
self.status_bar.showMessage(strings._('gui_tor_connection_lost'))
self.system_tray.showMessage(strings._('gui_tor_connection_lost'), strings._('gui_tor_connection_error_settings'))
self.status_bar.showMessage(strings._("gui_tor_connection_lost"))
self.system_tray.showMessage(
strings._("gui_tor_connection_lost"),
strings._("gui_tor_connection_error_settings"),
)
self.share_mode.handle_tor_broke()
self.receive_mode.handle_tor_broke()
@ -480,14 +647,31 @@ class OnionShareGui(QtWidgets.QMainWindow):
mode.handle_request_individual_file_canceled(event)
if event["type"] == Web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE:
Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"]))
Alert(
self.common,
strings._("error_cannot_create_data_dir").format(
event["data"]["receive_mode_dir"]
),
)
if event["type"] == Web.REQUEST_OTHER:
if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_password):
self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"]))
if event["path"] != "/favicon.ico" and event[
"path"
] != "/{}/shutdown".format(mode.web.shutdown_password):
self.status_bar.showMessage(
"{0:s}: {1:s}".format(
strings._("other_page_loaded"), event["path"]
)
)
if event["type"] == Web.REQUEST_INVALID_PASSWORD:
self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_passwords_count, strings._('incorrect_password'), event["data"]))
self.status_bar.showMessage(
"[#{0:d}] {1:s}: {2:s}".format(
mode.web.invalid_passwords_count,
strings._("incorrect_password"),
event["data"],
)
)
mode.timer_callback()
@ -495,15 +679,20 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
self.common.log('OnionShareGui', 'copy_url')
self.system_tray.showMessage(strings._('gui_copied_url_title'), strings._('gui_copied_url'))
self.common.log("OnionShareGui", "copy_url")
self.system_tray.showMessage(
strings._("gui_copied_url_title"), strings._("gui_copied_url")
)
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
self.common.log('OnionShareGui', 'copy_hidservauth')
self.system_tray.showMessage(strings._('gui_copied_hidservauth_title'), strings._('gui_copied_hidservauth'))
self.common.log("OnionShareGui", "copy_hidservauth")
self.system_tray.showMessage(
strings._("gui_copied_hidservauth_title"),
strings._("gui_copied_hidservauth"),
)
def clear_message(self):
"""
@ -539,7 +728,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.settings_action.setEnabled(not active)
def closeEvent(self, e):
self.common.log('OnionShareGui', 'closeEvent')
self.common.log("OnionShareGui", "closeEvent")
self.system_tray.hide()
try:
if self.mode == OnionShareGui.MODE_SHARE:
@ -549,16 +738,21 @@ class OnionShareGui(QtWidgets.QMainWindow):
else:
server_status = self.receive_mode.server_status
if server_status.status != server_status.STATUS_STOPPED:
self.common.log('OnionShareGui', 'closeEvent, opening warning dialog')
self.common.log("OnionShareGui", "closeEvent, opening warning dialog")
dialog = QtWidgets.QMessageBox()
dialog.setWindowTitle(strings._('gui_quit_title'))
dialog.setWindowTitle(strings._("gui_quit_title"))
if self.mode == OnionShareGui.MODE_SHARE:
dialog.setText(strings._('gui_share_quit_warning'))
dialog.setText(strings._("gui_share_quit_warning"))
else:
dialog.setText(strings._('gui_receive_quit_warning'))
dialog.setText(strings._("gui_receive_quit_warning"))
dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(strings._('gui_quit_warning_quit'), QtWidgets.QMessageBox.YesRole)
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit'), QtWidgets.QMessageBox.NoRole)
quit_button = dialog.addButton(
strings._("gui_quit_warning_quit"), QtWidgets.QMessageBox.YesRole
)
dont_quit_button = dialog.addButton(
strings._("gui_quit_warning_dont_quit"),
QtWidgets.QMessageBox.NoRole,
)
dialog.setDefaultButton(dont_quit_button)
reply = dialog.exec_()

View File

@ -25,10 +25,12 @@ from onionshare import strings
from .widgets import Alert
class ServerStatus(QtWidgets.QWidget):
"""
The server status chunk of the GUI.
"""
server_started = QtCore.pyqtSignal()
server_started_finished = QtCore.pyqtSignal()
server_stopped = QtCore.pyqtSignal()
@ -37,9 +39,9 @@ class ServerStatus(QtWidgets.QWidget):
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
MODE_SHARE = 'share'
MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
STATUS_STOPPED = 0
STATUS_WORKING = 1
@ -63,19 +65,31 @@ class ServerStatus(QtWidgets.QWidget):
self.resizeEvent(None)
# Auto-start timer layout
self.autostart_timer_label = QtWidgets.QLabel(strings._('gui_settings_autostart_timer'))
self.autostart_timer_label = QtWidgets.QLabel(
strings._("gui_settings_autostart_timer")
)
self.autostart_timer_widget = QtWidgets.QDateTimeEdit()
self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
if self.local_only:
# For testing
self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15))
self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime())
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
# Set proposed timer to be 5 minutes into the future
self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now
self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.autostart_timer_widget.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
self.autostart_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
autostart_timer_layout = QtWidgets.QHBoxLayout()
autostart_timer_layout.addWidget(self.autostart_timer_label)
autostart_timer_layout.addWidget(self.autostart_timer_widget)
@ -88,19 +102,31 @@ class ServerStatus(QtWidgets.QWidget):
self.autostart_timer_container.hide()
# Auto-stop timer layout
self.autostop_timer_label = QtWidgets.QLabel(strings._('gui_settings_autostop_timer'))
self.autostop_timer_label = QtWidgets.QLabel(
strings._("gui_settings_autostop_timer")
)
self.autostop_timer_widget = QtWidgets.QDateTimeEdit()
self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
if self.local_only:
# For testing
self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15))
self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime())
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(15)
)
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime()
)
else:
# Set proposed timer to be 5 minutes into the future
self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now
self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.autostop_timer_widget.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
self.autostop_timer_widget.setCurrentSection(
QtWidgets.QDateTimeEdit.MinuteSection
)
autostop_timer_layout = QtWidgets.QHBoxLayout()
autostop_timer_layout.addWidget(self.autostop_timer_label)
autostop_timer_layout.addWidget(self.autostop_timer_widget)
@ -125,16 +151,20 @@ class ServerStatus(QtWidgets.QWidget):
self.url.setFont(url_font)
self.url.setWordWrap(True)
self.url.setMinimumSize(self.url.sizeHint())
self.url.setStyleSheet(self.common.css['server_status_url'])
self.url.setStyleSheet(self.common.css["server_status_url"])
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url'))
self.copy_url_button = QtWidgets.QPushButton(strings._("gui_copy_url"))
self.copy_url_button.setFlat(True)
self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons'])
self.copy_url_button.setStyleSheet(self.common.css["server_status_url_buttons"])
self.copy_url_button.setMinimumHeight(65)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth'))
self.copy_hidservauth_button = QtWidgets.QPushButton(
strings._("gui_copy_hidservauth")
)
self.copy_hidservauth_button.setFlat(True)
self.copy_hidservauth_button.setStyleSheet(self.common.css['server_status_url_buttons'])
self.copy_hidservauth_button.setStyleSheet(
self.common.css["server_status_url_buttons"]
)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
url_buttons_layout = QtWidgets.QHBoxLayout()
url_buttons_layout.addWidget(self.copy_url_button)
@ -160,7 +190,9 @@ class ServerStatus(QtWidgets.QWidget):
"""
self.mode = share_mode
if (self.mode == ServerStatus.MODE_SHARE) or (self.mode == ServerStatus.MODE_WEBSITE):
if (self.mode == ServerStatus.MODE_SHARE) or (
self.mode == ServerStatus.MODE_WEBSITE
):
self.file_selection = file_selection
self.update()
@ -186,17 +218,25 @@ class ServerStatus(QtWidgets.QWidget):
"""
Reset the auto-start timer in the UI after stopping a share
"""
self.autostart_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.autostart_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
if not self.local_only:
self.autostart_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.autostart_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
def autostop_timer_reset(self):
"""
Reset the auto-stop timer in the UI after stopping a share
"""
self.autostop_timer_widget.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
self.autostop_timer_widget.setDateTime(
QtCore.QDateTime.currentDateTime().addSecs(300)
)
if not self.local_only:
self.autostop_timer_widget.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
self.autostop_timer_widget.setMinimumDateTime(
QtCore.QDateTime.currentDateTime().addSecs(60)
)
def show_url(self):
"""
@ -204,26 +244,38 @@ class ServerStatus(QtWidgets.QWidget):
"""
self.url_description.show()
info_image = self.common.get_resource_path('images/info.png')
info_image = self.common.get_resource_path("images/info.png")
if self.mode == ServerStatus.MODE_SHARE:
self.url_description.setText(strings._('gui_share_url_description').format(info_image))
self.url_description.setText(
strings._("gui_share_url_description").format(info_image)
)
elif self.mode == ServerStatus.MODE_WEBSITE:
self.url_description.setText(strings._('gui_website_url_description').format(info_image))
self.url_description.setText(
strings._("gui_website_url_description").format(info_image)
)
else:
self.url_description.setText(strings._('gui_receive_url_description').format(info_image))
self.url_description.setText(
strings._("gui_receive_url_description").format(info_image)
)
# Show a Tool Tip explaining the lifecycle of this URL
if self.common.settings.get('save_private_key'):
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent'))
if self.common.settings.get("save_private_key"):
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get(
"close_after_first_download"
):
self.url_description.setToolTip(
strings._("gui_url_label_onetime_and_persistent")
)
else:
self.url_description.setToolTip(strings._('gui_url_label_persistent'))
self.url_description.setToolTip(strings._("gui_url_label_persistent"))
else:
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'):
self.url_description.setToolTip(strings._('gui_url_label_onetime'))
if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get(
"close_after_first_download"
):
self.url_description.setToolTip(strings._("gui_url_label_onetime"))
else:
self.url_description.setToolTip(strings._('gui_url_label_stay_open'))
self.url_description.setToolTip(strings._("gui_url_label_stay_open"))
self.url.setText(self.get_url())
self.url.show()
@ -245,15 +297,15 @@ class ServerStatus(QtWidgets.QWidget):
self.common.settings.load()
self.show_url()
if self.common.settings.get('save_private_key'):
if not self.common.settings.get('password'):
self.common.settings.set('password', self.web.password)
if self.common.settings.get("save_private_key"):
if not self.common.settings.get("password"):
self.common.settings.set("password", self.web.password)
self.common.settings.save()
if self.common.settings.get('autostart_timer'):
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
if self.common.settings.get('autostop_timer'):
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
else:
self.url_description.hide()
@ -262,59 +314,91 @@ class ServerStatus(QtWidgets.QWidget):
self.copy_hidservauth_button.hide()
# Button
if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0:
if (
self.mode == ServerStatus.MODE_SHARE
and self.file_selection.get_num_files() == 0
):
self.server_button.hide()
elif self.mode == ServerStatus.MODE_WEBSITE and self.file_selection.get_num_files() == 0:
elif (
self.mode == ServerStatus.MODE_WEBSITE
and self.file_selection.get_num_files() == 0
):
self.server_button.hide()
else:
self.server_button.show()
if self.status == self.STATUS_STOPPED:
self.server_button.setStyleSheet(self.common.css['server_status_button_stopped'])
self.server_button.setStyleSheet(
self.common.css["server_status_button_stopped"]
)
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_start_server'))
self.server_button.setText(strings._("gui_share_start_server"))
elif self.mode == ServerStatus.MODE_WEBSITE:
self.server_button.setText(strings._('gui_share_start_server'))
self.server_button.setText(strings._("gui_share_start_server"))
else:
self.server_button.setText(strings._('gui_receive_start_server'))
self.server_button.setToolTip('')
if self.common.settings.get('autostart_timer'):
self.server_button.setText(strings._("gui_receive_start_server"))
self.server_button.setToolTip("")
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.show()
if self.common.settings.get('autostop_timer'):
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.show()
elif self.status == self.STATUS_STARTED:
self.server_button.setStyleSheet(self.common.css['server_status_button_started'])
self.server_button.setStyleSheet(
self.common.css["server_status_button_started"]
)
self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_stop_server'))
self.server_button.setText(strings._("gui_share_stop_server"))
elif self.mode == ServerStatus.MODE_WEBSITE:
self.server_button.setText(strings._('gui_share_stop_server'))
self.server_button.setText(strings._("gui_share_stop_server"))
else:
self.server_button.setText(strings._('gui_receive_stop_server'))
if self.common.settings.get('autostart_timer'):
self.server_button.setText(strings._("gui_receive_stop_server"))
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
if self.common.settings.get('autostop_timer'):
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
self.server_button.setToolTip(strings._('gui_stop_server_autostop_timer_tooltip').format(self.autostop_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy")))
self.server_button.setToolTip(
strings._("gui_stop_server_autostop_timer_tooltip").format(
self.autostop_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
elif self.status == self.STATUS_WORKING:
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
self.server_button.setStyleSheet(
self.common.css["server_status_button_working"]
)
self.server_button.setEnabled(True)
if self.autostart_timer_datetime:
self.autostart_timer_container.hide()
self.server_button.setToolTip(strings._('gui_start_server_autostart_timer_tooltip').format(self.autostart_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy")))
self.server_button.setToolTip(
strings._("gui_start_server_autostart_timer_tooltip").format(
self.autostart_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
else:
self.server_button.setText(strings._('gui_please_wait'))
if self.common.settings.get('autostop_timer'):
self.server_button.setText(strings._("gui_please_wait"))
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
else:
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
self.server_button.setStyleSheet(
self.common.css["server_status_button_working"]
)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
if self.common.settings.get('autostart_timer'):
self.server_button.setText(strings._("gui_please_wait"))
if self.common.settings.get("autostart_timer"):
self.autostart_timer_container.hide()
self.server_button.setToolTip(strings._('gui_start_server_autostart_timer_tooltip').format(self.autostart_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy")))
if self.common.settings.get('autostop_timer'):
self.server_button.setToolTip(
strings._("gui_start_server_autostart_timer_tooltip").format(
self.autostart_timer_widget.dateTime().toString(
"h:mm AP, MMMM dd, yyyy"
)
)
)
if self.common.settings.get("autostop_timer"):
self.autostop_timer_container.hide()
def server_button_clicked(self):
@ -323,28 +407,60 @@ class ServerStatus(QtWidgets.QWidget):
"""
if self.status == self.STATUS_STOPPED:
can_start = True
if self.common.settings.get('autostart_timer'):
if self.common.settings.get("autostart_timer"):
if self.local_only:
self.autostart_timer_datetime = self.autostart_timer_widget.dateTime().toPyDateTime()
self.autostart_timer_datetime = (
self.autostart_timer_widget.dateTime().toPyDateTime()
)
else:
self.autostart_timer_datetime = self.autostart_timer_widget.dateTime().toPyDateTime().replace(second=0, microsecond=0)
self.autostart_timer_datetime = (
self.autostart_timer_widget.dateTime()
.toPyDateTime()
.replace(second=0, microsecond=0)
)
# If the timer has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.autostart_timer_datetime:
if (
QtCore.QDateTime.currentDateTime().toPyDateTime()
> self.autostart_timer_datetime
):
can_start = False
Alert(self.common, strings._('gui_server_autostart_timer_expired'), QtWidgets.QMessageBox.Warning)
if self.common.settings.get('autostop_timer'):
Alert(
self.common,
strings._("gui_server_autostart_timer_expired"),
QtWidgets.QMessageBox.Warning,
)
if self.common.settings.get("autostop_timer"):
if self.local_only:
self.autostop_timer_datetime = self.autostop_timer_widget.dateTime().toPyDateTime()
self.autostop_timer_datetime = (
self.autostop_timer_widget.dateTime().toPyDateTime()
)
else:
# Get the timer chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
self.autostop_timer_datetime = self.autostop_timer_widget.dateTime().toPyDateTime().replace(second=0, microsecond=0)
self.autostop_timer_datetime = (
self.autostop_timer_widget.dateTime()
.toPyDateTime()
.replace(second=0, microsecond=0)
)
# If the timer has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.autostop_timer_datetime:
if (
QtCore.QDateTime.currentDateTime().toPyDateTime()
> self.autostop_timer_datetime
):
can_start = False
Alert(self.common, strings._('gui_server_autostop_timer_expired'), QtWidgets.QMessageBox.Warning)
if self.common.settings.get('autostart_timer'):
Alert(
self.common,
strings._("gui_server_autostop_timer_expired"),
QtWidgets.QMessageBox.Warning,
)
if self.common.settings.get("autostart_timer"):
if self.autostop_timer_datetime <= self.autostart_timer_datetime:
Alert(self.common, strings._('gui_autostop_timer_cant_be_earlier_than_autostart_timer'), QtWidgets.QMessageBox.Warning)
Alert(
self.common,
strings._(
"gui_autostop_timer_cant_be_earlier_than_autostart_timer"
),
QtWidgets.QMessageBox.Warning,
)
can_start = False
if can_start:
self.start_server()
@ -385,7 +501,9 @@ class ServerStatus(QtWidgets.QWidget):
"""
Cancel the server.
"""
self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
self.common.log(
"ServerStatus", "cancel_server", "Canceling the server mid-startup"
)
self.status = self.STATUS_WORKING
self.autostart_timer_reset()
self.autostop_timer_reset()
@ -421,8 +539,10 @@ class ServerStatus(QtWidgets.QWidget):
"""
Returns the OnionShare URL.
"""
if self.common.settings.get('public_mode'):
url = 'http://{0:s}'.format(self.app.onion_host)
if self.common.settings.get("public_mode"):
url = "http://{0:s}".format(self.app.onion_host)
else:
url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.password, self.app.onion_host)
url = "http://onionshare:{0:s}@{1:s}".format(
self.web.password, self.app.onion_host
)
return url

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ class OnionThread(QtCore.QThread):
"""
Starts the onion service, and waits for it to finish
"""
success = QtCore.pyqtSignal()
success_early = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
@ -34,28 +35,34 @@ class OnionThread(QtCore.QThread):
def __init__(self, mode):
super(OnionThread, self).__init__()
self.mode = mode
self.mode.common.log('OnionThread', '__init__')
self.mode.common.log("OnionThread", "__init__")
# allow this thread to be terminated
self.setTerminationEnabled()
def run(self):
self.mode.common.log('OnionThread', 'run')
self.mode.common.log("OnionThread", "run")
# Make a new static URL path for each new share
self.mode.web.generate_static_url_path()
# Choose port and password early, because we need them to exist in advance for scheduled shares
self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
self.mode.app.stay_open = not self.mode.common.settings.get(
"close_after_first_download"
)
if not self.mode.app.port:
self.mode.app.choose_port()
if not self.mode.common.settings.get('public_mode'):
if not self.mode.common.settings.get("public_mode"):
if not self.mode.web.password:
self.mode.web.generate_password(self.mode.common.settings.get('password'))
self.mode.web.generate_password(
self.mode.common.settings.get("password")
)
try:
if self.mode.obtain_onion_early:
self.mode.app.start_onion_service(await_publication=False, save_scheduled_key=True)
self.mode.app.start_onion_service(
await_publication=False, save_scheduled_key=True
)
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
self.success_early.emit()
@ -70,7 +77,19 @@ class OnionThread(QtCore.QThread):
self.mode.web_thread.start()
self.success.emit()
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e:
except (
TorTooOld,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
OSError,
) as e:
self.error.emit(e.args[0])
return
@ -79,17 +98,23 @@ class WebThread(QtCore.QThread):
"""
Starts the web service
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
super(WebThread, self).__init__()
self.mode = mode
self.mode.common.log('WebThread', '__init__')
self.mode.common.log("WebThread", "__init__")
def run(self):
self.mode.common.log('WebThread', 'run')
self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.password)
self.mode.common.log("WebThread", "run")
self.mode.web.start(
self.mode.app.port,
self.mode.app.stay_open,
self.mode.common.settings.get("public_mode"),
self.mode.web.password,
)
self.success.emit()
@ -97,30 +122,40 @@ class AutoStartTimer(QtCore.QThread):
"""
Waits for a prescribed time before allowing a share to start
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode, canceled=False):
super(AutoStartTimer, self).__init__()
self.mode = mode
self.canceled = canceled
self.mode.common.log('AutoStartTimer', '__init__')
self.mode.common.log("AutoStartTimer", "__init__")
# allow this thread to be terminated
self.setTerminationEnabled()
def run(self):
now = QtCore.QDateTime.currentDateTime()
autostart_timer_datetime_delta = now.secsTo(self.mode.server_status.autostart_timer_datetime)
autostart_timer_datetime_delta = now.secsTo(
self.mode.server_status.autostart_timer_datetime
)
try:
# Sleep until scheduled time
while autostart_timer_datetime_delta > 0 and self.canceled == False:
time.sleep(0.1)
now = QtCore.QDateTime.currentDateTime()
autostart_timer_datetime_delta = now.secsTo(self.mode.server_status.autostart_timer_datetime)
autostart_timer_datetime_delta = now.secsTo(
self.mode.server_status.autostart_timer_datetime
)
# Timer has now finished
if self.canceled == False:
self.mode.server_status.server_button.setText(strings._('gui_please_wait'))
self.mode.server_status_label.setText(strings._('gui_status_indicator_share_working'))
self.mode.server_status.server_button.setText(
strings._("gui_please_wait")
)
self.mode.server_status_label.setText(
strings._("gui_status_indicator_share_working")
)
self.success.emit()
except ValueError as e:
self.error.emit(e.args[0])

View File

@ -24,10 +24,12 @@ from onionshare.onion import *
from .widgets import Alert
class TorConnectionDialog(QtWidgets.QProgressDialog):
"""
Connecting to Tor dialog.
"""
open_settings = QtCore.pyqtSignal()
def __init__(self, common, qtapp, onion, custom_settings=False):
@ -40,18 +42,20 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
else:
self.settings = self.common.settings
self.common.log('TorConnectionDialog', '__init__')
self.common.log("TorConnectionDialog", "__init__")
self.qtapp = qtapp
self.onion = onion
self.setWindowTitle("OnionShare")
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
self.setModal(True)
self.setFixedSize(400, 150)
# Label
self.setLabelText(strings._('connecting_to_tor'))
self.setLabelText(strings._("connecting_to_tor"))
# Progress bar ticks from 0 to 100
self.setRange(0, 100)
@ -59,10 +63,10 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
self.setMinimumDuration(100)
# Start displaying the status at 0
self._tor_status_update(0, '')
self._tor_status_update(0, "")
def start(self):
self.common.log('TorConnectionDialog', 'start')
self.common.log("TorConnectionDialog", "start")
t = TorConnectionThread(self.common, self.settings, self, self.onion)
t.tor_status_update.connect(self._tor_status_update)
@ -81,17 +85,19 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
def _tor_status_update(self, progress, summary):
self.setValue(int(progress))
self.setLabelText("<strong>{}</strong><br>{}".format(strings._('connecting_to_tor'), summary))
self.setLabelText(
"<strong>{}</strong><br>{}".format(strings._("connecting_to_tor"), summary)
)
def _connected_to_tor(self):
self.common.log('TorConnectionDialog', '_connected_to_tor')
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.common.log("TorConnectionDialog", "_canceled_connecting_to_tor")
self.active = False
self.onion.cleanup()
@ -99,12 +105,16 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
QtCore.QTimer.singleShot(1, self.cancel)
def _error_connecting_to_tor(self, msg):
self.common.log('TorConnectionDialog', '_error_connecting_to_tor')
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, "{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings')), QtWidgets.QMessageBox.Warning)
Alert(
self.common,
"{}\n\n{}".format(msg, strings._("gui_tor_connection_error_settings")),
QtWidgets.QMessageBox.Warning,
)
# Open settings
self.open_settings.emit()
@ -114,6 +124,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog):
# Cancel connecting to Tor
QtCore.QTimer.singleShot(1, self.cancel)
class TorConnectionThread(QtCore.QThread):
tor_status_update = QtCore.pyqtSignal(str, str)
connected_to_tor = QtCore.pyqtSignal()
@ -125,7 +136,7 @@ class TorConnectionThread(QtCore.QThread):
self.common = common
self.common.log('TorConnectionThread', '__init__')
self.common.log("TorConnectionThread", "__init__")
self.settings = settings
@ -133,7 +144,7 @@ class TorConnectionThread(QtCore.QThread):
self.onion = onion
def run(self):
self.common.log('TorConnectionThread', 'run')
self.common.log("TorConnectionThread", "run")
# Connect to the Onion
try:
@ -144,11 +155,15 @@ class TorConnectionThread(QtCore.QThread):
self.canceled_connecting_to_tor.emit()
except BundledTorCanceled as e:
self.common.log('TorConnectionThread', 'run', 'caught exception: BundledTorCanceled')
self.common.log(
"TorConnectionThread", "run", "caught exception: BundledTorCanceled"
)
self.canceled_connecting_to_tor.emit()
except Exception as e:
self.common.log('TorConnectionThread', 'run', 'caught exception: {}'.format(e.args[0]))
self.common.log(
"TorConnectionThread", "run", "caught exception: {}".format(e.args[0])
)
self.error_connecting_to_tor.emit(str(e.args[0]))
def _tor_status_update(self, progress, summary):

View File

@ -27,21 +27,26 @@ from onionshare.onion import Onion
from onionshare import strings
class UpdateCheckerCheckError(Exception):
"""
Error checking for updates because of some Tor connection issue, or because
the OnionShare website is down.
"""
pass
class UpdateCheckerInvalidLatestVersion(Exception):
"""
Successfully downloaded the latest version, but it doesn't appear to be a
valid version string.
"""
def __init__(self, latest_version):
self.latest_version = latest_version
class UpdateChecker(QtCore.QObject):
"""
Load http://elx57ue5uyfplgva.onion/latest-version.txt to see what the latest
@ -50,6 +55,7 @@ class UpdateChecker(QtCore.QObject):
Only check at most once per day, unless force is True.
"""
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
update_error = QtCore.pyqtSignal()
@ -60,12 +66,12 @@ class UpdateChecker(QtCore.QObject):
self.common = common
self.common.log('UpdateChecker', '__init__')
self.common.log("UpdateChecker", "__init__")
self.onion = onion
self.config = config
def check(self, force=False, config=False):
self.common.log('UpdateChecker', 'check', 'force={}'.format(force))
self.common.log("UpdateChecker", "check", "force={}".format(force))
# Load the settings
settings = Settings(self.common, config)
settings.load()
@ -77,7 +83,7 @@ class UpdateChecker(QtCore.QObject):
check_for_updates = False
# See if it's been 1 day since the last check
autoupdate_timestamp = settings.get('autoupdate_timestamp')
autoupdate_timestamp = settings.get("autoupdate_timestamp")
if autoupdate_timestamp:
last_checked = datetime.datetime.fromtimestamp(autoupdate_timestamp)
now = datetime.datetime.now()
@ -90,24 +96,32 @@ class UpdateChecker(QtCore.QObject):
# Check for updates
if check_for_updates:
self.common.log('UpdateChecker', 'check', 'checking for updates')
self.common.log("UpdateChecker", "check", "checking for updates")
# Download the latest-version file over Tor
try:
# User agent string includes OnionShare version and platform
user_agent = 'OnionShare {}, {}'.format(self.common.version, self.common.platform)
user_agent = "OnionShare {}, {}".format(
self.common.version, self.common.platform
)
# If the update is forced, add '?force=1' to the URL, to more
# accurately measure daily users
path = '/latest-version.txt'
path = "/latest-version.txt"
if force:
path += '?force=1'
path += "?force=1"
if Version(self.onion.tor_version) >= Version('0.3.2.9'):
onion_domain = 'lldan5gahapx5k7iafb3s4ikijc4ni7gx5iywdflkba5y2ezyg6sjgyd.onion'
if Version(self.onion.tor_version) >= Version("0.3.2.9"):
onion_domain = (
"lldan5gahapx5k7iafb3s4ikijc4ni7gx5iywdflkba5y2ezyg6sjgyd.onion"
)
else:
onion_domain = 'elx57ue5uyfplgva.onion'
onion_domain = "elx57ue5uyfplgva.onion"
self.common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path))
self.common.log(
"UpdateChecker",
"check",
"loading http://{}{}".format(onion_domain, path),
)
(socks_address, socks_port) = self.onion.get_tor_socks_port()
socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port)
@ -116,19 +130,27 @@ class UpdateChecker(QtCore.QObject):
s.settimeout(15) # 15 second timeout
s.connect((onion_domain, 80))
http_request = 'GET {} HTTP/1.0\r\n'.format(path)
http_request += 'Host: {}\r\n'.format(onion_domain)
http_request += 'User-Agent: {}\r\n'.format(user_agent)
http_request += '\r\n'
s.sendall(http_request.encode('utf-8'))
http_request = "GET {} HTTP/1.0\r\n".format(path)
http_request += "Host: {}\r\n".format(onion_domain)
http_request += "User-Agent: {}\r\n".format(user_agent)
http_request += "\r\n"
s.sendall(http_request.encode("utf-8"))
http_response = s.recv(1024)
latest_version = http_response[http_response.find(b'\r\n\r\n'):].strip().decode('utf-8')
latest_version = (
http_response[http_response.find(b"\r\n\r\n") :]
.strip()
.decode("utf-8")
)
self.common.log('UpdateChecker', 'check', 'latest OnionShare version: {}'.format(latest_version))
self.common.log(
"UpdateChecker",
"check",
"latest OnionShare version: {}".format(latest_version),
)
except Exception as e:
self.common.log('UpdateChecker', 'check', '{}'.format(e))
self.common.log("UpdateChecker", "check", "{}".format(e))
self.update_error.emit()
raise UpdateCheckerCheckError
@ -140,22 +162,32 @@ class UpdateChecker(QtCore.QObject):
raise UpdateCheckerInvalidLatestVersion(latest_version)
# Update the last checked timestamp (dropping the seconds and milliseconds)
timestamp = datetime.datetime.now().replace(microsecond=0).replace(second=0).timestamp()
timestamp = (
datetime.datetime.now()
.replace(microsecond=0)
.replace(second=0)
.timestamp()
)
# Re-load the settings first before saving, just in case they've changed since we started our thread
settings.load()
settings.set('autoupdate_timestamp', timestamp)
settings.set("autoupdate_timestamp", timestamp)
settings.save()
# Do we need to update?
update_url = 'https://github.com/micahflee/onionshare/releases/tag/v{}'.format(latest_version)
update_url = "https://github.com/micahflee/onionshare/releases/tag/v{}".format(
latest_version
)
installed_version = self.common.version
if installed_version < latest_version:
self.update_available.emit(update_url, installed_version, latest_version)
self.update_available.emit(
update_url, installed_version, latest_version
)
return
# No updates are available
self.update_not_available.emit()
class UpdateThread(QtCore.QThread):
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
@ -167,13 +199,13 @@ class UpdateThread(QtCore.QThread):
self.common = common
self.common.log('UpdateThread', '__init__')
self.common.log("UpdateThread", "__init__")
self.onion = onion
self.config = config
self.force = force
def run(self):
self.common.log('UpdateThread', 'run')
self.common.log("UpdateThread", "run")
u = UpdateChecker(self.common, self.onion, self.config)
u.update_available.connect(self._update_available)
@ -185,25 +217,25 @@ class UpdateThread(QtCore.QThread):
u.check(config=self.config, force=self.force)
except Exception as e:
# If update check fails, silently ignore
self.common.log('UpdateThread', 'run', '{}'.format(e))
self.common.log("UpdateThread", "run", "{}".format(e))
pass
def _update_available(self, update_url, installed_version, latest_version):
self.common.log('UpdateThread', '_update_available')
self.common.log("UpdateThread", "_update_available")
self.active = False
self.update_available.emit(update_url, installed_version, latest_version)
def _update_not_available(self):
self.common.log('UpdateThread', '_update_not_available')
self.common.log("UpdateThread", "_update_not_available")
self.active = False
self.update_not_available.emit()
def _update_error(self):
self.common.log('UpdateThread', '_update_error')
self.common.log("UpdateThread", "_update_error")
self.active = False
self.update_error.emit()
def _update_invalid_version(self, latest_version):
self.common.log('UpdateThread', '_update_invalid_version')
self.common.log("UpdateThread", "_update_invalid_version")
self.active = False
self.update_invalid_version.emit(latest_version)

View File

@ -19,19 +19,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5 import QtCore, QtWidgets, QtGui
class Alert(QtWidgets.QMessageBox):
"""
An alert box dialog.
"""
def __init__(self, common, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True):
def __init__(
self,
common,
message,
icon=QtWidgets.QMessageBox.NoIcon,
buttons=QtWidgets.QMessageBox.Ok,
autostart=True,
):
super(Alert, self).__init__(None)
self.common = common
self.common.log('Alert', '__init__')
self.common.log("Alert", "__init__")
self.setWindowTitle("OnionShare")
self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png')))
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
self.setText(message)
self.setIcon(icon)
self.setStandardButtons(buttons)
@ -49,11 +60,12 @@ class AddFileDialog(QtWidgets.QFileDialog):
This is because the macOS sandbox requires native dialogs, and this is a Qt5
dialog.
"""
def __init__(self, common, *args, **kwargs):
QtWidgets.QFileDialog.__init__(self, *args, **kwargs)
self.common = common
self.common.log('AddFileDialog', '__init__')
self.common.log("AddFileDialog", "__init__")
self.setOption(self.DontUseNativeDialog, True)
self.setOption(self.ReadOnly, True)
@ -65,5 +77,5 @@ class AddFileDialog(QtWidgets.QFileDialog):
list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def accept(self):
self.common.log('AddFileDialog', 'accept')
self.common.log("AddFileDialog", "accept")
QtWidgets.QDialog.accept(self)

106
setup.py
View File

@ -22,6 +22,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, sys, platform, tempfile
from distutils.core import setup
def file_list(path):
files = []
for filename in os.listdir(path):
@ -29,7 +30,8 @@ def file_list(path):
files.append(os.path.join(path, filename))
return files
version = open('share/version.txt').read().strip()
version = open("share/version.txt").read().strip()
description = (
"""OnionShare lets you securely and anonymously send and receive files. It """
"""works by starting a web server, making it accessible as a Tor onion """
@ -37,7 +39,10 @@ description = (
"""files from you, or upload files to you. It does _not_ require setting up """
"""a separate server or using a third party file-sharing service."""
)
long_description = description + "\n\n" + (
long_description = (
description
+ "\n\n"
+ (
"""If you want to send files to someone, OnionShare hosts them on your own """
"""computer and uses a Tor onion service to make them temporarily accessible """
"""over the internet. The receiving user just needs to open the web address """
@ -47,11 +52,12 @@ long_description = description + "\n\n" + (
"""Other users can upload files to you from by loading the web address in """
"""Tor Browser."""
)
author = 'Micah Lee'
author_email = 'micah@micahflee.com'
url = 'https://github.com/micahflee/onionshare'
license = 'GPL v3'
keywords = 'onion, share, onionshare, tor, anonymous, web server'
)
author = "Micah Lee"
author_email = "micah@micahflee.com"
url = "https://github.com/micahflee/onionshare"
license = "GPL v3"
keywords = "onion, share, onionshare, tor, anonymous, web server"
classifiers = [
"Programming Language :: Python :: 3",
"Framework :: Flask",
@ -60,38 +66,72 @@ classifiers = [
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Intended Audience :: End Users/Desktop",
"Operating System :: OS Independent",
"Environment :: Web Environment"
"Environment :: Web Environment",
]
data_files = [
(os.path.join(sys.prefix, 'share/applications'), ['install/org.onionshare.OnionShare.desktop']),
(os.path.join(sys.prefix, 'share/icons/hicolor/scalable/apps'), ['install/org.onionshare.OnionShare.svg']),
(os.path.join(sys.prefix, 'share/metainfo'), ['install/org.onionshare.OnionShare.appdata.xml']),
(os.path.join(sys.prefix, 'share/onionshare'), file_list('share')),
(os.path.join(sys.prefix, 'share/onionshare/images'), file_list('share/images')),
(os.path.join(sys.prefix, 'share/onionshare/locale'), file_list('share/locale')),
(os.path.join(sys.prefix, 'share/onionshare/templates'), file_list('share/templates')),
(os.path.join(sys.prefix, 'share/onionshare/static/css'), file_list('share/static/css')),
(os.path.join(sys.prefix, 'share/onionshare/static/img'), file_list('share/static/img')),
(os.path.join(sys.prefix, 'share/onionshare/static/js'), file_list('share/static/js'))
(
os.path.join(sys.prefix, "share/applications"),
["install/org.onionshare.OnionShare.desktop"],
),
(
os.path.join(sys.prefix, "share/icons/hicolor/scalable/apps"),
["install/org.onionshare.OnionShare.svg"],
),
(
os.path.join(sys.prefix, "share/metainfo"),
["install/org.onionshare.OnionShare.appdata.xml"],
),
(os.path.join(sys.prefix, "share/onionshare"), file_list("share")),
(os.path.join(sys.prefix, "share/onionshare/images"), file_list("share/images")),
(os.path.join(sys.prefix, "share/onionshare/locale"), file_list("share/locale")),
(
os.path.join(sys.prefix, "share/onionshare/templates"),
file_list("share/templates"),
),
(
os.path.join(sys.prefix, "share/onionshare/static/css"),
file_list("share/static/css"),
),
(
os.path.join(sys.prefix, "share/onionshare/static/img"),
file_list("share/static/img"),
),
(
os.path.join(sys.prefix, "share/onionshare/static/js"),
file_list("share/static/js"),
),
]
if not platform.system().endswith('BSD') and platform.system() != 'DragonFly':
data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']))
if not platform.system().endswith("BSD") and platform.system() != "DragonFly":
data_files.append(
(
"/usr/share/nautilus-python/extensions/",
["install/scripts/onionshare-nautilus.py"],
)
)
setup(
name='onionshare', version=version,
description=description, long_description=long_description,
author=author, author_email=author_email, maintainer=author, maintainer_email=author_email,
url=url, license=license, keywords=keywords, classifiers=classifiers,
name="onionshare",
version=version,
description=description,
long_description=long_description,
author=author,
author_email=author_email,
maintainer=author,
maintainer_email=author_email,
url=url,
license=license,
keywords=keywords,
classifiers=classifiers,
packages=[
'onionshare',
'onionshare.web',
'onionshare_gui',
'onionshare_gui.mode',
'onionshare_gui.mode.share_mode',
'onionshare_gui.mode.receive_mode',
'onionshare_gui.mode.website_mode'
"onionshare",
"onionshare.web",
"onionshare_gui",
"onionshare_gui.mode",
"onionshare_gui.mode.share_mode",
"onionshare_gui.mode.receive_mode",
"onionshare_gui.mode.website_mode",
],
include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],
data_files=data_files
scripts=["install/scripts/onionshare", "install/scripts/onionshare-gui"],
data_files=data_files,
)

View File

@ -20,17 +20,17 @@ from onionshare_gui.mode.website_mode import WebsiteMode
class GuiBaseTest(object):
@staticmethod
def set_up(test_settings):
'''Create GUI with given settings'''
"""Create GUI with given settings"""
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
# Create a test dir and files
if not os.path.exists('/tmp/testdir'):
testdir = os.mkdir('/tmp/testdir')
testfile = open('/tmp/testdir/test', 'w')
testfile.write('onionshare')
if not os.path.exists("/tmp/testdir"):
testdir = os.mkdir("/tmp/testdir")
testfile = open("/tmp/testdir/test", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
@ -39,7 +39,7 @@ class GuiBaseTest(object):
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings['data_dir'] = '/tmp/OnionShare'
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
@ -51,53 +51,55 @@ class GuiBaseTest(object):
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt', '/tmp/testdir'], '/tmp/settings.json', True)
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/test.txt", "/tmp/testdir"],
"/tmp/settings.json",
True,
)
return gui
@staticmethod
def tear_down():
'''Clean up after tests'''
"""Clean up after tests"""
try:
os.remove('/tmp/test.txt')
os.remove('/tmp/settings.json')
os.remove('/tmp/large_file')
os.remove('/tmp/download.zip')
os.remove('/tmp/webpage')
shutil.rmtree('/tmp/testdir')
shutil.rmtree('/tmp/OnionShare')
os.remove("/tmp/test.txt")
os.remove("/tmp/settings.json")
os.remove("/tmp/large_file")
os.remove("/tmp/download.zip")
os.remove("/tmp/webpage")
shutil.rmtree("/tmp/testdir")
shutil.rmtree("/tmp/OnionShare")
except:
pass
def gui_loaded(self):
'''Test that the GUI actually is shown'''
"""Test that the GUI actually is shown"""
self.assertTrue(self.gui.show)
def windowTitle_seen(self):
'''Test that the window title is OnionShare'''
self.assertEqual(self.gui.windowTitle(), 'OnionShare')
"""Test that the window title is OnionShare"""
self.assertEqual(self.gui.windowTitle(), "OnionShare")
def settings_button_is_visible(self):
'''Test that the settings button is visible'''
"""Test that the settings button is visible"""
self.assertTrue(self.gui.settings_button.isVisible())
def settings_button_is_hidden(self):
'''Test that the settings button is hidden when the server starts'''
"""Test that the settings button is hidden when the server starts"""
self.assertFalse(self.gui.settings_button.isVisible())
def server_status_bar_is_visible(self):
'''Test that the status bar is visible'''
"""Test that the status bar is visible"""
self.assertTrue(self.gui.status_bar.isVisible())
def click_mode(self, mode):
'''Test that we can switch Mode by clicking the button'''
"""Test that we can switch Mode by clicking the button"""
if type(mode) == ReceiveMode:
QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE)
@ -108,16 +110,14 @@ class GuiBaseTest(object):
QtTest.QTest.mouseClick(self.gui.website_mode_button, QtCore.Qt.LeftButton)
self.assertTrue(self.gui.mode, self.gui.MODE_WEBSITE)
def click_toggle_history(self, mode):
'''Test that we can toggle Download or Upload history by clicking the toggle button'''
"""Test that we can toggle Download or Upload history by clicking the toggle button"""
currently_visible = mode.history.isVisible()
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
self.assertEqual(mode.history.isVisible(), not currently_visible)
def history_indicator(self, mode, public_mode, indicator_count="1"):
'''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
"""Test that we can make sure the history is toggled off, do an action, and the indiciator works"""
# Make sure history is toggled off
if mode.history.isVisible():
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
@ -128,12 +128,16 @@ class GuiBaseTest(object):
if type(mode) == ReceiveMode:
# Upload a file
files = {'file[]': open('/tmp/test.txt', 'rb')}
url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
files = {"file[]": open("/tmp/test.txt", "rb")}
url = "http://127.0.0.1:{}/upload".format(self.gui.app.port)
if public_mode:
r = requests.post(url, files=files)
else:
r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password),
)
QtTest.QTest.qWait(2000)
if type(mode) == ShareMode:
@ -142,7 +146,10 @@ class GuiBaseTest(object):
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password),
)
QtTest.QTest.qWait(2000)
# Indicator should be visible, have a value of "1"
@ -153,19 +160,16 @@ class GuiBaseTest(object):
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def history_is_not_visible(self, mode):
'''Test that the History section is not visible'''
"""Test that the History section is not visible"""
self.assertFalse(mode.history.isVisible())
def history_is_visible(self, mode):
'''Test that the History section is visible'''
"""Test that the History section is visible"""
self.assertTrue(mode.history.isVisible())
def server_working_on_start_button_pressed(self, mode):
'''Test we can start the service'''
"""Test we can start the service"""
# Should be in SERVER_WORKING state
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
self.assertEqual(mode.server_status.status, 1)
@ -175,115 +179,143 @@ class GuiBaseTest(object):
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def server_status_indicator_says_starting(self, mode):
'''Test that the Server Status indicator shows we are Starting'''
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_share_working'))
"""Test that the Server Status indicator shows we are Starting"""
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_working"),
)
def server_status_indicator_says_scheduled(self, mode):
'''Test that the Server Status indicator shows we are Scheduled'''
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_share_scheduled'))
"""Test that the Server Status indicator shows we are Scheduled"""
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_scheduled"),
)
def server_is_started(self, mode, startup_time=2000):
'''Test that the server has started'''
"""Test that the server has started"""
QtTest.QTest.qWait(startup_time)
# Should now be in SERVER_STARTED state
self.assertEqual(mode.server_status.status, 2)
def web_server_is_running(self):
'''Test that the web server has started'''
"""Test that the web server has started"""
try:
r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port))
r = requests.get("http://127.0.0.1:{}/".format(self.gui.app.port))
self.assertTrue(True)
except requests.exceptions.ConnectionError:
self.assertTrue(False)
def have_a_password(self, mode, public_mode):
'''Test that we have a valid password'''
"""Test that we have a valid password"""
if not public_mode:
self.assertRegex(mode.server_status.web.password, r'(\w+)-(\w+)')
self.assertRegex(mode.server_status.web.password, r"(\w+)-(\w+)")
else:
self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)')
self.assertIsNone(mode.server_status.web.password, r"(\w+)-(\w+)")
def add_button_visible(self, mode):
'''Test that the add button should be visible'''
"""Test that the add button should be visible"""
self.assertTrue(mode.server_status.file_selection.add_button.isVisible())
def url_description_shown(self, mode):
'''Test that the URL label is showing'''
"""Test that the URL label is showing"""
self.assertTrue(mode.server_status.url_description.isVisible())
def have_copy_url_button(self, mode, public_mode):
'''Test that the Copy URL button is shown and that the clipboard is correct'''
"""Test that the Copy URL button is shown and that the clipboard is correct"""
self.assertTrue(mode.server_status.copy_url_button.isVisible())
QtTest.QTest.mouseClick(mode.server_status.copy_url_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
mode.server_status.copy_url_button, QtCore.Qt.LeftButton
)
clipboard = self.gui.qtapp.clipboard()
if public_mode:
self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port))
self.assertEqual(
clipboard.text(), "http://127.0.0.1:{}".format(self.gui.app.port)
)
else:
self.assertEqual(clipboard.text(), 'http://onionshare:{}@127.0.0.1:{}'.format(mode.server_status.web.password, self.gui.app.port))
self.assertEqual(
clipboard.text(),
"http://onionshare:{}@127.0.0.1:{}".format(
mode.server_status.web.password, self.gui.app.port
),
)
def server_status_indicator_says_started(self, mode):
'''Test that the Server Status indicator shows we are started'''
"""Test that the Server Status indicator shows we are started"""
if type(mode) == ReceiveMode:
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_receive_started'))
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_receive_started"),
)
if type(mode) == ShareMode:
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_share_started'))
self.assertEqual(
mode.server_status_label.text(),
strings._("gui_status_indicator_share_started"),
)
def web_page(self, mode, string, public_mode):
'''Test that the web page contains a string'''
"""Test that the web page contains a string"""
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password))
r = requests.get(
url, auth=requests.auth.HTTPBasicAuth("onionshare", mode.web.password)
)
self.assertTrue(string in r.text)
def history_widgets_present(self, mode):
'''Test that the relevant widgets are present in the history view after activity has taken place'''
"""Test that the relevant widgets are present in the history view after activity has taken place"""
self.assertFalse(mode.history.empty.isVisible())
self.assertTrue(mode.history.not_empty.isVisible())
def counter_incremented(self, mode, count):
'''Test that the counter has incremented'''
"""Test that the counter has incremented"""
self.assertEqual(mode.history.completed_count, count)
def server_is_stopped(self, mode, stay_open):
'''Test that the server stops when we click Stop'''
if type(mode) == ReceiveMode or (type(mode) == ShareMode and stay_open) or (type(mode) == WebsiteMode):
QtTest.QTest.mouseClick(mode.server_status.server_button, QtCore.Qt.LeftButton)
"""Test that the server stops when we click Stop"""
if (
type(mode) == ReceiveMode
or (type(mode) == ShareMode and stay_open)
or (type(mode) == WebsiteMode)
):
QtTest.QTest.mouseClick(
mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(mode.server_status.status, 0)
def web_server_is_stopped(self):
'''Test that the web server also stopped'''
"""Test that the web server also stopped"""
QtTest.QTest.qWait(2000)
try:
r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port))
r = requests.get("http://127.0.0.1:{}/".format(self.gui.app.port))
self.assertTrue(False)
except requests.exceptions.ConnectionError:
self.assertTrue(True)
def server_status_indicator_says_closed(self, mode, stay_open):
'''Test that the Server Status indicator shows we closed'''
"""Test that the Server Status indicator shows we closed"""
if type(mode) == ReceiveMode:
self.assertEqual(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped'))
self.assertEqual(
self.gui.receive_mode.server_status_label.text(),
strings._("gui_status_indicator_receive_stopped"),
)
if type(mode) == ShareMode:
if stay_open:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped'))
self.assertEqual(
self.gui.share_mode.server_status_label.text(),
strings._("gui_status_indicator_share_stopped"),
)
else:
self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically'))
self.assertEqual(
self.gui.share_mode.server_status_label.text(),
strings._("closing_automatically"),
)
def clear_all_history_items(self, mode, count):
if count == 0:
@ -292,41 +324,40 @@ class GuiBaseTest(object):
# Auto-stop timer tests
def set_timeout(self, mode, timeout):
'''Test that the timeout can be set'''
"""Test that the timeout can be set"""
timer = QtCore.QDateTime.currentDateTime().addSecs(timeout)
mode.server_status.autostop_timer_widget.setDateTime(timer)
self.assertTrue(mode.server_status.autostop_timer_widget.dateTime(), timer)
def autostop_timer_widget_hidden(self, mode):
'''Test that the auto-stop timer widget is hidden when share has started'''
"""Test that the auto-stop timer widget is hidden when share has started"""
self.assertFalse(mode.server_status.autostop_timer_container.isVisible())
def server_timed_out(self, mode, wait):
'''Test that the server has timed out after the timer ran out'''
"""Test that the server has timed out after the timer ran out"""
QtTest.QTest.qWait(wait)
# We should have timed out now
self.assertEqual(mode.server_status.status, 0)
# Auto-start timer tests
def set_autostart_timer(self, mode, timer):
'''Test that the timer can be set'''
"""Test that the timer can be set"""
schedule = QtCore.QDateTime.currentDateTime().addSecs(timer)
mode.server_status.autostart_timer_widget.setDateTime(schedule)
self.assertTrue(mode.server_status.autostart_timer_widget.dateTime(), schedule)
def autostart_timer_widget_hidden(self, mode):
'''Test that the auto-start timer widget is hidden when share has started'''
"""Test that the auto-start timer widget is hidden when share has started"""
self.assertFalse(mode.server_status.autostart_timer_container.isVisible())
def scheduled_service_started(self, mode, wait):
'''Test that the server has timed out after the timer ran out'''
"""Test that the server has timed out after the timer ran out"""
QtTest.QTest.qWait(wait)
# We should have started now
self.assertEqual(mode.server_status.status, 2)
def cancel_the_share(self, mode):
'''Test that we can cancel a share before it's started up '''
"""Test that we can cancel a share before it's started up """
self.server_working_on_start_button_pressed(mode)
self.server_status_indicator_says_scheduled(mode)
self.add_delete_buttons_hidden()
@ -334,7 +365,9 @@ class GuiBaseTest(object):
self.set_autostart_timer(mode, 10)
QtTest.QTest.mousePress(mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.qWait(2000)
QtTest.QTest.mouseRelease(mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseRelease(
mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(mode.server_status.status, 0)
self.server_is_stopped(mode, False)
self.web_server_is_stopped()

View File

@ -4,25 +4,44 @@ from datetime import datetime, timedelta
from PyQt5 import QtCore, QtTest
from .GuiBaseTest import GuiBaseTest
class GuiReceiveTest(GuiBaseTest):
def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False):
'''Test that we can upload the file'''
def upload_file(
self,
public_mode,
file_to_upload,
expected_basename,
identical_files_at_once=False,
):
"""Test that we can upload the file"""
# Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused
QtTest.QTest.qWait(2000)
files = {'file[]': open(file_to_upload, 'rb')}
url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
files = {"file[]": open(file_to_upload, "rb")}
url = "http://127.0.0.1:{}/upload".format(self.gui.app.port)
if public_mode:
r = requests.post(url, files=files)
if identical_files_at_once:
# Send a duplicate upload to test for collisions
r = requests.post(url, files=files)
else:
r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password))
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
if identical_files_at_once:
# Send a duplicate upload to test for collisions
r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password))
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
QtTest.QTest.qWait(2000)
@ -35,7 +54,9 @@ class GuiReceiveTest(GuiBaseTest):
time_dir = now.strftime("%H.%M.%S-1")
else:
time_dir = now.strftime("%H.%M.%S")
receive_mode_dir = os.path.join(self.gui.common.settings.get('data_dir'), date_dir, time_dir)
receive_mode_dir = os.path.join(
self.gui.common.settings.get("data_dir"), date_dir, time_dir
)
expected_filename = os.path.join(receive_mode_dir, expected_basename)
if os.path.exists(expected_filename):
exists = True
@ -45,31 +66,37 @@ class GuiReceiveTest(GuiBaseTest):
self.assertTrue(exists)
def upload_file_should_fail(self, public_mode):
'''Test that we can't upload the file when permissions are wrong, and expected content is shown'''
files = {'file[]': open('/tmp/test.txt', 'rb')}
url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port)
"""Test that we can't upload the file when permissions are wrong, and expected content is shown"""
files = {"file[]": open("/tmp/test.txt", "rb")}
url = "http://127.0.0.1:{}/upload".format(self.gui.app.port)
if public_mode:
r = requests.post(url, files=files)
else:
r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password))
r = requests.post(
url,
files=files,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.receive_mode.web.password
),
)
QtCore.QTimer.singleShot(1000, self.accept_dialog)
self.assertTrue('Error uploading, please inform the OnionShare user' in r.text)
self.assertTrue("Error uploading, please inform the OnionShare user" in r.text)
def upload_dir_permissions(self, mode=0o755):
'''Manipulate the permissions on the upload dir in between tests'''
os.chmod('/tmp/OnionShare', mode)
"""Manipulate the permissions on the upload dir in between tests"""
os.chmod("/tmp/OnionShare", mode)
def try_without_auth_in_non_public_mode(self):
r = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port))
r = requests.post("http://127.0.0.1:{}/upload".format(self.gui.app.port))
self.assertEqual(r.status_code, 401)
r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port))
r = requests.get("http://127.0.0.1:{}/close".format(self.gui.app.port))
self.assertEqual(r.status_code, 401)
# 'Grouped' tests follow from here
def run_all_receive_mode_setup_tests(self, public_mode):
'''Set up a share in Receive mode and start it'''
"""Set up a share in Receive mode and start it"""
self.click_mode(self.gui.receive_mode)
self.history_is_not_visible(self.gui.receive_mode)
self.click_toggle_history(self.gui.receive_mode)
@ -83,24 +110,28 @@ class GuiReceiveTest(GuiBaseTest):
self.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_mode)
self.web_page(self.gui.receive_mode, 'Select the files you want to send, then click', public_mode)
self.web_page(
self.gui.receive_mode,
"Select the files you want to send, then click",
public_mode,
)
def run_all_receive_mode_tests(self, public_mode):
'''Upload files in receive mode and stop the share'''
"""Upload files in receive mode and stop the share"""
self.run_all_receive_mode_setup_tests(public_mode)
if not public_mode:
self.try_without_auth_in_non_public_mode()
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.counter_incremented(self.gui.receive_mode, 1)
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.counter_incremented(self.gui.receive_mode, 2)
self.upload_file(public_mode, '/tmp/testdir/test', 'test')
self.upload_file(public_mode, "/tmp/testdir/test", "test")
self.counter_incremented(self.gui.receive_mode, 3)
self.upload_file(public_mode, '/tmp/testdir/test', 'test')
self.upload_file(public_mode, "/tmp/testdir/test", "test")
self.counter_incremented(self.gui.receive_mode, 4)
# Test uploading the same file twice at the same time, and make sure no collisions
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt', True)
self.upload_file(public_mode, "/tmp/test.txt", "test.txt", True)
self.counter_incremented(self.gui.receive_mode, 6)
self.history_indicator(self.gui.receive_mode, public_mode, "2")
self.server_is_stopped(self.gui.receive_mode, False)
@ -111,7 +142,7 @@ class GuiReceiveTest(GuiBaseTest):
self.history_indicator(self.gui.receive_mode, public_mode, "2")
def run_all_receive_mode_unwritable_dir_tests(self, public_mode):
'''Attempt to upload (unwritable) files in receive mode and stop the share'''
"""Attempt to upload (unwritable) files in receive mode and stop the share"""
self.run_all_receive_mode_setup_tests(public_mode)
self.upload_dir_permissions(0o400)
self.upload_file_should_fail(public_mode)
@ -131,8 +162,8 @@ class GuiReceiveTest(GuiBaseTest):
def run_all_clear_all_button_tests(self, public_mode):
"""Test the Clear All history button"""
self.run_all_receive_mode_setup_tests(public_mode)
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.clear_all_history_items(self.gui.receive_mode, 0)
self.upload_file(public_mode, '/tmp/test.txt', 'test.txt')
self.upload_file(public_mode, "/tmp/test.txt", "test.txt")
self.clear_all_history_items(self.gui.receive_mode, 2)

View File

@ -6,89 +6,137 @@ import tempfile
from PyQt5 import QtCore, QtTest
from .GuiBaseTest import GuiBaseTest
class GuiShareTest(GuiBaseTest):
# Persistence tests
def have_same_password(self, password):
'''Test that we have the same password'''
"""Test that we have the same password"""
self.assertEqual(self.gui.share_mode.server_status.web.password, password)
# Share-specific tests
def file_selection_widget_has_files(self, num=2):
'''Test that the number of items in the list is as expected'''
self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), num)
"""Test that the number of items in the list is as expected"""
self.assertEqual(
self.gui.share_mode.server_status.file_selection.get_num_files(), num
)
def deleting_all_files_hides_delete_button(self):
'''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button'''
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0))
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center())
"""Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button"""
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(
self.gui.share_mode.server_status.file_selection.file_list.item(0)
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.file_list.viewport(),
QtCore.Qt.LeftButton,
pos=rect.center(),
)
# Delete button should be visible
self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
self.assertTrue(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
# Click delete, delete button should still be visible since we have one more file
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.delete_button,
QtCore.Qt.LeftButton,
)
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0))
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center())
self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton)
rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(
self.gui.share_mode.server_status.file_selection.file_list.item(0)
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.file_list.viewport(),
QtCore.Qt.LeftButton,
pos=rect.center(),
)
self.assertTrue(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.delete_button,
QtCore.Qt.LeftButton,
)
# No more files, the delete button should be hidden
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
self.assertFalse(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
def add_a_file_and_delete_using_its_delete_widget(self):
'''Test that we can also delete a file by clicking on its [X] widget'''
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton)
"""Test that we can also delete a file by clicking on its [X] widget"""
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/etc/hosts"
)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.file_selection.file_list.item(
0
).item_button,
QtCore.Qt.LeftButton,
)
self.file_selection_widget_has_files(0)
def file_selection_widget_read_files(self):
'''Re-add some files to the list so we can share'''
self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts')
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt')
"""Re-add some files to the list so we can share"""
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/etc/hosts"
)
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/tmp/test.txt"
)
self.file_selection_widget_has_files(2)
def add_large_file(self):
'''Add a large file to the share'''
"""Add a large file to the share"""
size = 1024 * 1024 * 155
with open('/tmp/large_file', 'wb') as fout:
with open("/tmp/large_file", "wb") as fout:
fout.write(os.urandom(size))
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/large_file')
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/tmp/large_file"
)
def add_delete_buttons_hidden(self):
'''Test that the add and delete buttons are hidden when the server starts'''
self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible())
self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible())
"""Test that the add and delete buttons are hidden when the server starts"""
self.assertFalse(
self.gui.share_mode.server_status.file_selection.add_button.isVisible()
)
self.assertFalse(
self.gui.share_mode.server_status.file_selection.delete_button.isVisible()
)
def download_share(self, public_mode):
'''Test that we can download the share'''
"""Test that we can download the share"""
url = "http://127.0.0.1:{}/download".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
tmp_file = tempfile.NamedTemporaryFile()
with open(tmp_file.name, 'wb') as f:
with open(tmp_file.name, "wb") as f:
f.write(r.content)
zip = zipfile.ZipFile(tmp_file.name)
QtTest.QTest.qWait(2000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8"))
def individual_file_is_viewable_or_not(self, public_mode, stay_open):
'''Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)'''
"""Test whether an individual file is viewable (when in stay_open mode) and that it isn't (when not in stay_open mode)"""
url = "http://127.0.0.1:{}".format(self.gui.app.port)
download_file_url = "http://127.0.0.1:{}/test.txt".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
if stay_open:
self.assertTrue('a href="test.txt"' in r.text)
@ -96,32 +144,44 @@ class GuiShareTest(GuiBaseTest):
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
r = requests.get(
download_file_url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
tmp_file = tempfile.NamedTemporaryFile()
with open(tmp_file.name, 'wb') as f:
with open(tmp_file.name, "wb") as f:
f.write(r.content)
with open(tmp_file.name, 'r') as f:
self.assertEqual('onionshare', f.read())
with open(tmp_file.name, "r") as f:
self.assertEqual("onionshare", f.read())
else:
self.assertFalse('a href="/test.txt"' in r.text)
if public_mode:
r = requests.get(download_file_url)
else:
r = requests.get(download_file_url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password))
r = requests.get(
download_file_url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.share_mode.server_status.web.password
),
)
self.assertEqual(r.status_code, 404)
self.download_share(public_mode)
QtTest.QTest.qWait(2000)
def hit_401(self, public_mode):
'''Test that the server stops after too many 401s, or doesn't when in public_mode'''
"""Test that the server stops after too many 401s, or doesn't when in public_mode"""
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
for _ in range(20):
password_guess = self.gui.common.build_password()
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', password_guess))
r = requests.get(
url, auth=requests.auth.HTTPBasicAuth("onionshare", password_guess)
)
# A nasty hack to avoid the Alert dialog that blocks the rest of the test
if not public_mode:
@ -134,7 +194,6 @@ class GuiShareTest(GuiBaseTest):
else:
self.web_server_is_stopped()
# 'Grouped' tests follow from here
def run_all_share_mode_setup_tests(self):
@ -148,7 +207,6 @@ class GuiShareTest(GuiBaseTest):
self.add_a_file_and_delete_using_its_delete_widget()
self.file_selection_widget_read_files()
def run_all_share_mode_started_tests(self, public_mode, startup_time=2000):
"""Tests in share mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.share_mode)
@ -162,10 +220,9 @@ class GuiShareTest(GuiBaseTest):
self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_mode)
def run_all_share_mode_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, 'Total size', public_mode)
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.download_share(public_mode)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
@ -179,7 +236,7 @@ class GuiShareTest(GuiBaseTest):
def run_all_share_mode_individual_file_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, 'Total size', public_mode)
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.individual_file_is_viewable_or_not(public_mode, stay_open)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
@ -222,7 +279,6 @@ class GuiShareTest(GuiBaseTest):
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.share_mode, stay_open)
def run_all_share_mode_persistent_tests(self, public_mode, stay_open):
"""Same as end-to-end share tests but also test the password is the same on multiple shared"""
self.run_all_share_mode_setup_tests()
@ -231,7 +287,6 @@ class GuiShareTest(GuiBaseTest):
self.run_all_share_mode_download_tests(public_mode, stay_open)
self.have_same_password(password)
def run_all_share_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()
@ -258,12 +313,16 @@ class GuiShareTest(GuiBaseTest):
self.set_autostart_timer(self.gui.share_mode, 15)
self.set_timeout(self.gui.share_mode, 5)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.server_is_stopped(self.gui.share_mode, False)
def run_all_share_mode_unreadable_file_tests(self):
'''Attempt to share an unreadable file'''
"""Attempt to share an unreadable file"""
self.run_all_share_mode_setup_tests()
QtCore.QTimer.singleShot(1000, self.accept_dialog)
self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/nonexistent.txt')
self.gui.share_mode.server_status.file_selection.file_list.add_file(
"/tmp/nonexistent.txt"
)
self.file_selection_widget_has_files(2)

View File

@ -13,13 +13,16 @@ from onionshare.web import Web
from onionshare_gui import Application, OnionShare, OnionShareGui
from .GuiShareTest import GuiShareTest
class GuiWebsiteTest(GuiShareTest):
@staticmethod
def set_up(test_settings):
'''Create GUI with given settings'''
"""Create GUI with given settings"""
# Create our test file
testfile = open('/tmp/index.html', 'w')
testfile.write('<html><body><p>This is a test website hosted by OnionShare</p></body></html>')
testfile = open("/tmp/index.html", "w")
testfile.write(
"<html><body><p>This is a test website hosted by OnionShare</p></body></html>"
)
testfile.close()
common = Common()
@ -28,7 +31,7 @@ class GuiWebsiteTest(GuiShareTest):
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings['data_dir'] = '/tmp/OnionShare'
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
@ -40,44 +43,62 @@ class GuiWebsiteTest(GuiShareTest):
app = OnionShare(common, testonion, True, 0)
web = Web(common, False, True)
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/index.html'], '/tmp/settings.json', True)
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/index.html"],
"/tmp/settings.json",
True,
)
return gui
@staticmethod
def tear_down():
'''Clean up after tests'''
"""Clean up after tests"""
try:
os.remove('/tmp/index.html')
os.remove('/tmp/settings.json')
os.remove("/tmp/index.html")
os.remove("/tmp/settings.json")
except:
pass
def view_website(self, public_mode):
'''Test that we can download the share'''
"""Test that we can download the share"""
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password))
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.website_mode.server_status.web.password
),
)
QtTest.QTest.qWait(2000)
self.assertTrue('This is a test website hosted by OnionShare' in r.text)
self.assertTrue("This is a test website hosted by OnionShare" in r.text)
def check_csp_header(self, public_mode, csp_header_disabled):
'''Test that the CSP header is present when enabled or vice versa'''
"""Test that the CSP header is present when enabled or vice versa"""
url = "http://127.0.0.1:{}/".format(self.gui.app.port)
if public_mode:
r = requests.get(url)
else:
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password))
r = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(
"onionshare", self.gui.website_mode.server_status.web.password
),
)
QtTest.QTest.qWait(2000)
if csp_header_disabled:
self.assertFalse('Content-Security-Policy' in r.headers)
self.assertFalse("Content-Security-Policy" in r.headers)
else:
self.assertTrue('Content-Security-Policy' in r.headers)
self.assertTrue("Content-Security-Policy" in r.headers)
def run_all_website_mode_setup_tests(self):
"""Tests in website mode prior to starting a share"""
@ -100,16 +121,16 @@ class GuiWebsiteTest(GuiShareTest):
self.have_copy_url_button(self.gui.website_mode, public_mode)
self.server_status_indicator_says_started(self.gui.website_mode)
def run_all_website_mode_download_tests(self, public_mode):
"""Tests in website mode after viewing the site"""
self.run_all_website_mode_setup_tests()
self.run_all_website_mode_started_tests(public_mode, startup_time=2000)
self.view_website(public_mode)
self.check_csp_header(public_mode, self.gui.common.settings.get('csp_header_disabled'))
self.check_csp_header(
public_mode, self.gui.common.settings.get("csp_header_disabled")
)
self.history_widgets_present(self.gui.website_mode)
self.server_is_stopped(self.gui.website_mode, False)
self.web_server_is_stopped()
self.server_status_indicator_says_closed(self.gui.website_mode, False)
self.add_button_visible(self.gui.website_mode)

View File

@ -23,7 +23,7 @@ class OnionStub(object):
class SettingsGuiBaseTest(object):
@staticmethod
def set_up():
'''Create the GUI'''
"""Create the GUI"""
# Default settings for the settings GUI tests
test_settings = {
@ -32,8 +32,8 @@ class SettingsGuiBaseTest(object):
}
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
@ -51,22 +51,22 @@ class SettingsGuiBaseTest(object):
if key not in test_settings:
test_settings[key] = val
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = SettingsDialog(common, testonion, qtapp, '/tmp/settings.json', True)
gui = SettingsDialog(common, testonion, qtapp, "/tmp/settings.json", True)
return gui
@staticmethod
def tear_down():
'''Clean up after tests'''
os.remove('/tmp/settings.json')
"""Clean up after tests"""
os.remove("/tmp/settings.json")
def run_settings_gui_tests(self):
self.gui.show()
# Window is shown
self.assertTrue(self.gui.isVisible())
self.assertEqual(self.gui.windowTitle(), strings._('gui_settings_window_title'))
self.assertEqual(self.gui.windowTitle(), strings._("gui_settings_window_title"))
# Check for updates button is hidden
self.assertFalse(self.gui.check_for_updates_button.isVisible())
@ -74,13 +74,21 @@ class SettingsGuiBaseTest(object):
# public mode is off
self.assertFalse(self.gui.public_mode_checkbox.isChecked())
# enable public mode
QtTest.QTest.mouseClick(self.gui.public_mode_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.public_mode_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.public_mode_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.public_mode_checkbox.height() / 2),
)
self.assertTrue(self.gui.public_mode_checkbox.isChecked())
# autostop timer is off
self.assertFalse(self.gui.autostop_timer_checkbox.isChecked())
# enable autostop timer
QtTest.QTest.mouseClick(self.gui.autostop_timer_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.autostop_timer_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.autostop_timer_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.autostop_timer_checkbox.height() / 2),
)
self.assertTrue(self.gui.autostop_timer_checkbox.isChecked())
# legacy mode checkbox and related widgets
@ -96,32 +104,70 @@ class SettingsGuiBaseTest(object):
self.assertFalse(self.gui.hidservauth_copy_button.isVisible())
# enable legacy mode
QtTest.QTest.mouseClick(self.gui.use_legacy_v2_onions_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.use_legacy_v2_onions_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isChecked())
self.assertTrue(self.gui.save_private_key_checkbox.isVisible())
self.assertTrue(self.gui.use_stealth_widget.isVisible())
# enable persistent mode
QtTest.QTest.mouseClick(self.gui.save_private_key_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.save_private_key_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.save_private_key_checkbox.height() / 2
),
)
self.assertTrue(self.gui.save_private_key_checkbox.isChecked())
# enable stealth mode
QtTest.QTest.mouseClick(self.gui.stealth_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.stealth_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
self.assertTrue(self.gui.stealth_checkbox.isChecked())
# now that stealth is enabled, we can't turn off legacy mode
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isEnabled())
# disable stealth, persistence
QtTest.QTest.mouseClick(self.gui.save_private_key_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.save_private_key_checkbox.height()/2))
QtTest.QTest.mouseClick(self.gui.stealth_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.stealth_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.save_private_key_checkbox.height() / 2
),
)
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
# legacy mode checkbox is enabled again
self.assertTrue(self.gui.use_legacy_v2_onions_checkbox.isEnabled())
# uncheck legacy mode
QtTest.QTest.mouseClick(self.gui.use_legacy_v2_onions_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.use_legacy_v2_onions_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
# legacy options hidden again
self.assertTrue(self.gui.save_private_key_widget.isVisible())
self.assertFalse(self.gui.use_stealth_widget.isVisible())
# re-enable legacy mode
QtTest.QTest.mouseClick(self.gui.use_legacy_v2_onions_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.use_legacy_v2_onions_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.use_legacy_v2_onions_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.use_legacy_v2_onions_checkbox.height() / 2
),
)
else:
# legacy mode setting is hidden
@ -131,8 +177,16 @@ class SettingsGuiBaseTest(object):
self.assertTrue(self.gui.use_stealth_widget.isVisible())
# enable them all again so that we can see the setting stick in settings.json
QtTest.QTest.mouseClick(self.gui.save_private_key_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.save_private_key_checkbox.height()/2))
QtTest.QTest.mouseClick(self.gui.stealth_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.stealth_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.save_private_key_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.save_private_key_checkbox.height() / 2),
)
QtTest.QTest.mouseClick(
self.gui.stealth_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.stealth_checkbox.height() / 2),
)
else:
# None of the onion settings should appear
self.assertFalse(self.gui.use_legacy_v2_onions_checkbox.isVisible())
@ -144,12 +198,17 @@ class SettingsGuiBaseTest(object):
# stay open toggled off, on
self.assertTrue(self.gui.close_after_first_download_checkbox.isChecked())
QtTest.QTest.mouseClick(self.gui.close_after_first_download_checkbox, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.close_after_first_download_checkbox.height()/2))
QtTest.QTest.mouseClick(
self.gui.close_after_first_download_checkbox,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.close_after_first_download_checkbox.height() / 2
),
)
self.assertFalse(self.gui.close_after_first_download_checkbox.isChecked())
# receive mode
self.gui.data_dir_lineedit.setText('/tmp/OnionShareSettingsTest')
self.gui.data_dir_lineedit.setText("/tmp/OnionShareSettingsTest")
# bundled mode is enabled
self.assertTrue(self.gui.connection_type_bundled_radio.isEnabled())
@ -161,7 +220,11 @@ class SettingsGuiBaseTest(object):
self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked())
# switch to obfs4
QtTest.QTest.mouseClick(self.gui.tor_bridges_use_obfs4_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.tor_bridges_use_obfs4_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.tor_bridges_use_obfs4_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.tor_bridges_use_obfs4_radio.height() / 2),
)
self.assertTrue(self.gui.tor_bridges_use_obfs4_radio.isChecked())
# custom bridges are hidden
@ -175,7 +238,11 @@ class SettingsGuiBaseTest(object):
self.assertFalse(self.gui.connection_type_socket_file_radio.isChecked())
# enable automatic mode
QtTest.QTest.mouseClick(self.gui.connection_type_automatic_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.connection_type_automatic_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.connection_type_automatic_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.connection_type_automatic_radio.height() / 2),
)
self.assertTrue(self.gui.connection_type_automatic_radio.isChecked())
# bundled is off
self.assertFalse(self.gui.connection_type_bundled_radio.isChecked())
@ -187,7 +254,13 @@ class SettingsGuiBaseTest(object):
self.assertFalse(self.gui.authenticate_password_radio.isVisible())
# enable control port mode
QtTest.QTest.mouseClick(self.gui.connection_type_control_port_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.connection_type_control_port_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.connection_type_control_port_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.connection_type_control_port_radio.height() / 2
),
)
self.assertTrue(self.gui.connection_type_control_port_radio.isChecked())
# automatic is off
self.assertFalse(self.gui.connection_type_automatic_radio.isChecked())
@ -196,7 +269,13 @@ class SettingsGuiBaseTest(object):
self.assertTrue(self.gui.authenticate_password_radio.isVisible())
# enable socket mode
QtTest.QTest.mouseClick(self.gui.connection_type_socket_file_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.connection_type_socket_file_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.connection_type_socket_file_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(
2, self.gui.connection_type_socket_file_radio.height() / 2
),
)
self.assertTrue(self.gui.connection_type_socket_file_radio.isChecked())
# control port is off
self.assertFalse(self.gui.connection_type_control_port_radio.isChecked())
@ -205,20 +284,30 @@ class SettingsGuiBaseTest(object):
self.assertTrue(self.gui.authenticate_password_radio.isVisible())
# re-enable bundled mode
QtTest.QTest.mouseClick(self.gui.connection_type_bundled_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.connection_type_bundled_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.connection_type_bundled_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.connection_type_bundled_radio.height() / 2),
)
# go back to custom bridges
QtTest.QTest.mouseClick(self.gui.tor_bridges_use_custom_radio, QtCore.Qt.LeftButton, pos=QtCore.QPoint(2,self.gui.tor_bridges_use_custom_radio.height()/2))
QtTest.QTest.mouseClick(
self.gui.tor_bridges_use_custom_radio,
QtCore.Qt.LeftButton,
pos=QtCore.QPoint(2, self.gui.tor_bridges_use_custom_radio.height() / 2),
)
self.assertTrue(self.gui.tor_bridges_use_custom_radio.isChecked())
self.assertTrue(self.gui.tor_bridges_use_custom_textbox.isVisible())
self.assertFalse(self.gui.tor_bridges_use_obfs4_radio.isChecked())
self.gui.tor_bridges_use_custom_textbox.setPlainText('94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\n148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\n93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3')
self.gui.tor_bridges_use_custom_textbox.setPlainText(
"94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\n148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\n93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3"
)
# Test that the Settings Dialog can save the settings and close itself
QtTest.QTest.mouseClick(self.gui.save_button, QtCore.Qt.LeftButton)
self.assertFalse(self.gui.isVisible())
# Test our settings are reflected in the settings json
with open('/tmp/settings.json') as f:
with open("/tmp/settings.json") as f:
data = json.load(f)
self.assertTrue(data["public_mode"])
@ -238,4 +327,7 @@ class SettingsGuiBaseTest(object):
self.assertFalse(data["close_after_first_download"])
self.assertEqual(data["connection_type"], "bundled")
self.assertFalse(data["tor_bridges_use_obfs4"])
self.assertEqual(data["tor_bridges_use_custom_bridges"], "Bridge 94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\nBridge 148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\nBridge 93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3\n")
self.assertEqual(
data["tor_bridges_use_custom_bridges"],
"Bridge 94.242.249.2:83 E25A95F1DADB739F0A83EB0223A37C02FD519306\nBridge 148.251.90.59:7510 019F727CA6DCA6CA5C90B55E477B7D87981E75BC\nBridge 93.80.47.217:41727 A6A0D497D98097FCFE91D639548EE9E34C15CDD3\n",
)

View File

@ -16,20 +16,21 @@ from onionshare_gui.mode.receive_mode import ReceiveMode
from .GuiBaseTest import GuiBaseTest
class TorGuiBaseTest(GuiBaseTest):
@staticmethod
def set_up(test_settings):
'''Create GUI with given settings'''
"""Create GUI with given settings"""
# Create our test file
testfile = open('/tmp/test.txt', 'w')
testfile.write('onionshare')
testfile = open("/tmp/test.txt", "w")
testfile.write("onionshare")
testfile.close()
# Create a test dir and files
if not os.path.exists('/tmp/testdir'):
testdir = os.mkdir('/tmp/testdir')
testfile = open('/tmp/testdir/test.txt', 'w')
testfile.write('onionshare')
if not os.path.exists("/tmp/testdir"):
testdir = os.mkdir("/tmp/testdir")
testfile = open("/tmp/testdir/test.txt", "w")
testfile.write("onionshare")
testfile.close()
common = Common()
@ -38,8 +39,8 @@ class TorGuiBaseTest(GuiBaseTest):
strings.load_strings(common)
# Get all of the settings in test_settings
test_settings['connection_type'] = 'automatic'
test_settings['data_dir'] = '/tmp/OnionShare'
test_settings["connection_type"] = "automatic"
test_settings["data_dir"] = "/tmp/OnionShare"
for key, val in common.settings.default_settings.items():
if key not in test_settings:
test_settings[key] = val
@ -51,13 +52,21 @@ class TorGuiBaseTest(GuiBaseTest):
app = OnionShare(common, testonion, False, 0)
web = Web(common, False, False)
open('/tmp/settings.json', 'w').write(json.dumps(test_settings))
open("/tmp/settings.json", "w").write(json.dumps(test_settings))
gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt', '/tmp/testdir'], '/tmp/settings.json', False)
gui = OnionShareGui(
common,
testonion,
qtapp,
app,
["/tmp/test.txt", "/tmp/testdir"],
"/tmp/settings.json",
False,
)
return gui
def history_indicator(self, mode, public_mode):
'''Test that we can make sure the history is toggled off, do an action, and the indiciator works'''
"""Test that we can make sure the history is toggled off, do an action, and the indiciator works"""
# Make sure history is toggled off
if mode.history.isVisible():
QtTest.QTest.mouseClick(mode.toggle_history, QtCore.Qt.LeftButton)
@ -70,15 +79,17 @@ class TorGuiBaseTest(GuiBaseTest):
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port)
session.proxies["http"] = "socks5h://{}:{}".format(socks_address, socks_port)
if type(mode) == ReceiveMode:
# Upload a file
files = {'file[]': open('/tmp/test.txt', 'rb')}
files = {"file[]": open("/tmp/test.txt", "rb")}
if not public_mode:
path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, mode.web.password)
path = "http://{}/{}/upload".format(
self.gui.app.onion_host, mode.web.password
)
else:
path = 'http://{}/upload'.format(self.gui.app.onion_host)
path = "http://{}/upload".format(self.gui.app.onion_host)
response = session.post(path, files=files)
QtTest.QTest.qWait(4000)
@ -87,7 +98,9 @@ class TorGuiBaseTest(GuiBaseTest):
if public_mode:
path = "http://{}/download".format(self.gui.app.onion_host)
else:
path = "http://{}/{}/download".format(self.gui.app.onion_host, mode.web.password)
path = "http://{}/{}/download".format(
self.gui.app.onion_host, mode.web.password
)
response = session.get(path)
QtTest.QTest.qWait(4000)
@ -100,61 +113,72 @@ class TorGuiBaseTest(GuiBaseTest):
self.assertFalse(mode.toggle_history.indicator_label.isVisible())
def have_an_onion_service(self):
'''Test that we have a valid Onion URL'''
self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion')
"""Test that we have a valid Onion URL"""
self.assertRegex(self.gui.app.onion_host, r"[a-z2-7].onion")
def web_page(self, mode, string, public_mode):
'''Test that the web page contains a string'''
"""Test that the web page contains a string"""
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port)
s = socks.socksocket()
s.settimeout(60)
s.connect((self.gui.app.onion_host, 80))
if not public_mode:
path = '/{}'.format(mode.server_status.web.password)
path = "/{}".format(mode.server_status.web.password)
else:
path = '/'
http_request = 'GET {} HTTP/1.0\r\n'.format(path)
http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host)
http_request += '\r\n'
s.sendall(http_request.encode('utf-8'))
with open('/tmp/webpage', 'wb') as file_to_write:
path = "/"
http_request = "GET {} HTTP/1.0\r\n".format(path)
http_request += "Host: {}\r\n".format(self.gui.app.onion_host)
http_request += "\r\n"
s.sendall(http_request.encode("utf-8"))
with open("/tmp/webpage", "wb") as file_to_write:
while True:
data = s.recv(1024)
if not data:
break
file_to_write.write(data)
file_to_write.close()
f = open('/tmp/webpage')
f = open("/tmp/webpage")
self.assertTrue(string in f.read())
f.close()
def have_copy_url_button(self, mode, public_mode):
'''Test that the Copy URL button is shown and that the clipboard is correct'''
"""Test that the Copy URL button is shown and that the clipboard is correct"""
self.assertTrue(mode.server_status.copy_url_button.isVisible())
QtTest.QTest.mouseClick(mode.server_status.copy_url_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
mode.server_status.copy_url_button, QtCore.Qt.LeftButton
)
clipboard = self.gui.qtapp.clipboard()
if public_mode:
self.assertEqual(clipboard.text(), 'http://{}'.format(self.gui.app.onion_host))
self.assertEqual(
clipboard.text(), "http://{}".format(self.gui.app.onion_host)
)
else:
self.assertEqual(clipboard.text(), 'http://{}/{}'.format(self.gui.app.onion_host, mode.server_status.web.password))
self.assertEqual(
clipboard.text(),
"http://{}/{}".format(
self.gui.app.onion_host, mode.server_status.web.password
),
)
# Stealth tests
def copy_have_hidserv_auth_button(self, mode):
'''Test that the Copy HidservAuth button is shown'''
"""Test that the Copy HidservAuth button is shown"""
self.assertTrue(mode.server_status.copy_hidservauth_button.isVisible())
def hidserv_auth_string(self):
'''Test the validity of the HidservAuth string'''
self.assertRegex(self.gui.app.auth_string, r'HidServAuth {} [a-zA-Z1-9]'.format(self.gui.app.onion_host))
"""Test the validity of the HidservAuth string"""
self.assertRegex(
self.gui.app.auth_string,
r"HidServAuth {} [a-zA-Z1-9]".format(self.gui.app.onion_host),
)
# Miscellaneous tests
def tor_killed_statusbar_message_shown(self, mode):
'''Test that the status bar message shows Tor was disconnected'''
"""Test that the status bar message shows Tor was disconnected"""
self.gui.app.onion.c = None
QtTest.QTest.qWait(1000)
self.assertTrue(mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost'))
self.assertTrue(
mode.status_bar.currentMessage(), strings._("gui_tor_connection_lost")
)

View File

@ -3,28 +3,29 @@ import requests
from PyQt5 import QtTest
from .TorGuiBaseTest import TorGuiBaseTest
class TorGuiReceiveTest(TorGuiBaseTest):
class TorGuiReceiveTest(TorGuiBaseTest):
def upload_file(self, public_mode, file_to_upload, expected_file):
'''Test that we can upload the file'''
"""Test that we can upload the file"""
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port)
files = {'file[]': open(file_to_upload, 'rb')}
session.proxies["http"] = "socks5h://{}:{}".format(socks_address, socks_port)
files = {"file[]": open(file_to_upload, "rb")}
if not public_mode:
path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.password)
path = "http://{}/{}/upload".format(
self.gui.app.onion_host, self.gui.receive_mode.web.password
)
else:
path = 'http://{}/upload'.format(self.gui.app.onion_host)
path = "http://{}/upload".format(self.gui.app.onion_host)
response = session.post(path, files=files)
QtTest.QTest.qWait(4000)
self.assertTrue(os.path.isfile(expected_file))
# 'Grouped' tests follow from here
def run_all_receive_mode_tests(self, public_mode, receive_allow_receiver_shutdown):
'''Run a full suite of tests in Receive mode'''
"""Run a full suite of tests in Receive mode"""
self.click_mode(self.gui.receive_mode)
self.history_is_not_visible(self.gui.receive_mode)
self.click_toggle_history(self.gui.receive_mode)
@ -39,15 +40,19 @@ class TorGuiReceiveTest(TorGuiBaseTest):
self.url_description_shown(self.gui.receive_mode)
self.have_copy_url_button(self.gui.receive_mode, public_mode)
self.server_status_indicator_says_started(self.gui.receive_mode)
self.web_page(self.gui.receive_mode, 'Select the files you want to send, then click', public_mode)
self.upload_file(public_mode, '/tmp/test.txt', '/tmp/OnionShare/test.txt')
self.web_page(
self.gui.receive_mode,
"Select the files you want to send, then click",
public_mode,
)
self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test.txt")
self.history_widgets_present(self.gui.receive_mode)
self.counter_incremented(self.gui.receive_mode, 1)
self.upload_file(public_mode, '/tmp/test.txt', '/tmp/OnionShare/test-2.txt')
self.upload_file(public_mode, "/tmp/test.txt", "/tmp/OnionShare/test-2.txt")
self.counter_incremented(self.gui.receive_mode, 2)
self.upload_file(public_mode, '/tmp/testdir/test', '/tmp/OnionShare/test')
self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test")
self.counter_incremented(self.gui.receive_mode, 3)
self.upload_file(public_mode, '/tmp/testdir/test', '/tmp/OnionShare/test-2')
self.upload_file(public_mode, "/tmp/testdir/test", "/tmp/OnionShare/test-2")
self.counter_incremented(self.gui.receive_mode, 4)
self.history_indicator(self.gui.receive_mode, public_mode)
self.server_is_stopped(self.gui.receive_mode, False)

View File

@ -4,48 +4,50 @@ from PyQt5 import QtTest
from .TorGuiBaseTest import TorGuiBaseTest
from .GuiShareTest import GuiShareTest
class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
def download_share(self, public_mode):
'''Test downloading a share'''
"""Test downloading a share"""
# Set up connecting to the onion
(socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port()
session = requests.session()
session.proxies = {}
session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port)
session.proxies["http"] = "socks5h://{}:{}".format(socks_address, socks_port)
# Download files
if public_mode:
path = "http://{}/download".format(self.gui.app.onion_host)
else:
path = "http://{}/{}/download".format(self.gui.app.onion_host, self.gui.share_mode.web.password)
path = "http://{}/{}/download".format(
self.gui.app.onion_host, self.gui.share_mode.web.password
)
response = session.get(path, stream=True)
QtTest.QTest.qWait(4000)
if response.status_code == 200:
with open('/tmp/download.zip', 'wb') as file_to_write:
with open("/tmp/download.zip", "wb") as file_to_write:
for chunk in response.iter_content(chunk_size=128):
file_to_write.write(chunk)
file_to_write.close()
zip = zipfile.ZipFile('/tmp/download.zip')
zip = zipfile.ZipFile("/tmp/download.zip")
QtTest.QTest.qWait(4000)
self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8'))
self.assertEqual("onionshare", zip.read("test.txt").decode("utf-8"))
# Persistence tests
def have_same_onion(self, onion):
'''Test that we have the same onion'''
"""Test that we have the same onion"""
self.assertEqual(self.gui.app.onion_host, onion)
# legacy v2 onion test
def have_v2_onion(self):
'''Test that the onion is a v2 style onion'''
self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion')
"""Test that the onion is a v2 style onion"""
self.assertRegex(self.gui.app.onion_host, r"[a-z2-7].onion")
self.assertEqual(len(self.gui.app.onion_host), 22)
# 'Grouped' tests follow from here
def run_all_share_mode_started_tests(self, public_mode):
'''Tests in share mode after starting a share'''
"""Tests in share mode after starting a share"""
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.server_status_indicator_says_starting(self.gui.share_mode)
self.add_delete_buttons_hidden()
@ -58,10 +60,9 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.have_copy_url_button(self.gui.share_mode, public_mode)
self.server_status_indicator_says_started(self.gui.share_mode)
def run_all_share_mode_download_tests(self, public_mode, stay_open):
"""Tests in share mode after downloading a share"""
self.web_page(self.gui.share_mode, 'Total size', public_mode)
self.web_page(self.gui.share_mode, "Total size", public_mode)
self.download_share(public_mode)
self.history_widgets_present(self.gui.share_mode)
self.server_is_stopped(self.gui.share_mode, stay_open)
@ -72,7 +73,6 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.server_is_started(self.gui.share_mode, startup_time=45000)
self.history_indicator(self.gui.share_mode, public_mode)
def run_all_share_mode_persistent_tests(self, public_mode, stay_open):
"""Same as end-to-end share tests but also test the password is the same on multiple shared"""
self.run_all_share_mode_setup_tests()
@ -83,7 +83,6 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest):
self.have_same_onion(onion)
self.have_same_password(password)
def run_all_share_mode_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()

View File

@ -1,4 +1,5 @@
import sys
# Force tests to look for resources in the source code tree
sys.onionshare_dev_mode = True
@ -10,6 +11,7 @@ import pytest
from onionshare import common, web, settings, strings
def pytest_addoption(parser):
parser.addoption(
"--rungui", action="store_true", default=False, help="run GUI tests"
@ -27,7 +29,7 @@ def pytest_collection_modifyitems(config, items):
if "tor" in item.keywords:
item.add_marker(skip_tor)
if not config.getoption('--rungui'):
if not config.getoption("--rungui"):
# --rungui given in cli: do not skip GUI tests
skip_gui = pytest.mark.skip(reason="need --rungui option to run")
for item in items:
@ -43,8 +45,8 @@ def temp_dir_1024():
tmp_dir = tempfile.mkdtemp()
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
with open(tmp_file, "wb") as f:
f.write(b"*" * 1024)
return tmp_dir
@ -58,8 +60,8 @@ def temp_dir_1024_delete():
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir)
with open(tmp_file, 'wb') as f:
f.write(b'*' * 1024)
with open(tmp_file, "wb") as f:
f.write(b"*" * 1024)
yield tmp_dir
@ -68,7 +70,7 @@ def temp_file_1024():
""" Create a temporary file of a particular size (1024 bytes). """
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024)
tmp_file.write(b"*" * 1024)
return tmp_file.name
@ -81,18 +83,18 @@ def temp_file_1024_delete():
"""
with tempfile.NamedTemporaryFile() as tmp_file:
tmp_file.write(b'*' * 1024)
tmp_file.write(b"*" * 1024)
tmp_file.flush()
yield tmp_file.name
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
@pytest.yield_fixture(scope="session")
def custom_zw():
zw = web.share_mode.ZipWriter(
common.Common(),
zip_filename=common.Common.random_string(4, 6),
processed_size_callback=lambda _: 'custom_callback'
processed_size_callback=lambda _: "custom_callback",
)
yield zw
zw.close()
@ -100,7 +102,7 @@ def custom_zw():
# pytest > 2.9 only needs @pytest.fixture
@pytest.yield_fixture(scope='session')
@pytest.yield_fixture(scope="session")
def default_zw():
zw = web.share_mode.ZipWriter(common.Common())
yield zw
@ -111,76 +113,77 @@ def default_zw():
@pytest.fixture
def locale_en(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8'))
monkeypatch.setattr("locale.getdefaultlocale", lambda: ("en_US", "UTF-8"))
@pytest.fixture
def locale_fr(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8'))
monkeypatch.setattr("locale.getdefaultlocale", lambda: ("fr_FR", "UTF-8"))
@pytest.fixture
def locale_invalid(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8'))
monkeypatch.setattr("locale.getdefaultlocale", lambda: ("xx_XX", "UTF-8"))
@pytest.fixture
def locale_ru(monkeypatch):
monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8'))
monkeypatch.setattr("locale.getdefaultlocale", lambda: ("ru_RU", "UTF-8"))
@pytest.fixture
def platform_darwin(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Darwin')
monkeypatch.setattr("platform.system", lambda: "Darwin")
@pytest.fixture # (scope="session")
def platform_linux(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Linux')
monkeypatch.setattr("platform.system", lambda: "Linux")
@pytest.fixture
def platform_windows(monkeypatch):
monkeypatch.setattr('platform.system', lambda: 'Windows')
monkeypatch.setattr("platform.system", lambda: "Windows")
@pytest.fixture
def sys_argv_sys_prefix(monkeypatch):
monkeypatch.setattr('sys.argv', [sys.prefix])
monkeypatch.setattr("sys.argv", [sys.prefix])
@pytest.fixture
def sys_frozen(monkeypatch):
monkeypatch.setattr('sys.frozen', True, raising=False)
monkeypatch.setattr("sys.frozen", True, raising=False)
@pytest.fixture
def sys_meipass(monkeypatch):
monkeypatch.setattr(
'sys._MEIPASS', os.path.expanduser('~'), raising=False)
monkeypatch.setattr("sys._MEIPASS", os.path.expanduser("~"), raising=False)
@pytest.fixture # (scope="session")
def sys_onionshare_dev_mode(monkeypatch):
monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False)
monkeypatch.setattr("sys.onionshare_dev_mode", True, raising=False)
@pytest.fixture
def time_time_100(monkeypatch):
monkeypatch.setattr('time.time', lambda: 100)
monkeypatch.setattr("time.time", lambda: 100)
@pytest.fixture
def time_strftime(monkeypatch):
monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00')
monkeypatch.setattr("time.strftime", lambda _: "Jun 06 2013 11:05:00")
@pytest.fixture
def common_obj():
return common.Common()
@pytest.fixture
def settings_obj(sys_onionshare_dev_mode, platform_linux):
_common = common.Common()
_common.version = 'DUMMY_VERSION_1.2.3'
_common.version = "DUMMY_VERSION_1.2.3"
strings.load_strings(_common)
return settings.Settings(_common)

View File

@ -4,13 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
"public_mode": True
}
test_settings = {"close_after_first_download": False, "public_mode": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -18,11 +16,12 @@ class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, True)
self.hit_401(True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class Local401RateLimitTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False
}
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,11 +16,12 @@ class Local401RateLimitTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
self.hit_401(False)
if __name__ == "__main__":
unittest.main()

View File

@ -5,12 +5,11 @@ from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalQuittingDuringSharePromptsWarningTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False
}
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -18,7 +17,7 @@ class LocalQuittingDuringSharePromptsWarningTest(unittest.TestCase, GuiShareTest
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
@ -30,5 +29,6 @@ class LocalQuittingDuringSharePromptsWarningTest(unittest.TestCase, GuiShareTest
self.server_is_started(self.gui.share_mode, 0)
self.web_server_is_running()
if __name__ == "__main__":
unittest.main()

View File

@ -4,11 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -16,10 +16,11 @@ class LocalReceiveModeClearAllButtonTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeTimerTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostop_timer": True,
}
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -18,10 +16,11 @@ class LocalReceiveModeTimerTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"receive_allow_receiver_shutdown": True
}
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_unwritable_dir_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": True,
"receive_allow_receiver_shutdown": True
}
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -18,10 +16,11 @@ class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_unwritable_dir_tests(True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": True,
"receive_allow_receiver_shutdown": True
}
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -18,10 +16,11 @@ class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiReceiveTest import GuiReceiveTest
class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"receive_allow_receiver_shutdown": True
}
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = GuiReceiveTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest):
GuiReceiveTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -16,7 +16,7 @@ class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_legacy_tor(self):
self.gui.onion = OnionStub(True, False)
self.gui.reload_settings()

View File

@ -16,7 +16,7 @@ class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_no_tor(self):
self.gui.onion = OnionStub(False, False)
self.gui.reload_settings()

View File

@ -16,7 +16,7 @@ class SettingsGuiTest(unittest.TestCase, SettingsGuiBaseTest):
SettingsGuiBaseTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui_v3_tor(self):
self.gui.onion = OnionStub(True, True)
self.gui.reload_settings()

View File

@ -4,6 +4,7 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
@ -19,10 +20,11 @@ class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_autostop_autostart_mismatch_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostart_timer": True,
}
test_settings = {"public_mode": False, "autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -18,10 +16,11 @@ class LocalShareModeAutoStartTimerTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_autostart_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -5,13 +5,11 @@ from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalShareModeAutoStartTimerTooShortTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostart_timer": True,
}
test_settings = {"public_mode": False, "autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -19,7 +17,7 @@ class LocalShareModeAutoStartTimerTooShortTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
@ -27,8 +25,11 @@ class LocalShareModeAutoStartTimerTooShortTest(unittest.TestCase, GuiShareTest):
self.set_autostart_timer(self.gui.share_mode, 2)
QtTest.QTest.qWait(3000)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(self.gui.share_mode.server_status.status, 0)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeCancelTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"autostart_timer": True,
}
test_settings = {"autostart_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,11 +16,12 @@ class LocalShareModeCancelTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.cancel_the_share(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalShareModeClearAllButtonTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_clear_all_button_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModePublicModeTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": True,
}
test_settings = {"public_mode": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalShareModePublicModeTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeStayOpenTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalShareModeStayOpenTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,11 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -16,10 +16,11 @@ class LocalShareModeTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
test_settings = {"close_after_first_download": False}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalShareModeIndividualFileViewStayOpenTest(unittest.TestCase, GuiShareTe
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": True,
}
test_settings = {"close_after_first_download": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalShareModeIndividualFileViewTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_individual_file_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,11 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeLargeDownloadTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -16,10 +16,11 @@ class LocalShareModeLargeDownloadTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_large_file_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,6 +4,7 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
@ -20,10 +21,11 @@ class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_persistent_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostop_timer": True,
}
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -18,10 +16,11 @@ class LocalShareModeTimerTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -5,13 +5,11 @@ from PyQt5 import QtCore, QtTest
from .GuiShareTest import GuiShareTest
class LocalShareModeTimerTooShortTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostop_timer": True,
}
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -19,7 +17,7 @@ class LocalShareModeTimerTooShortTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
@ -27,8 +25,11 @@ class LocalShareModeTimerTooShortTest(unittest.TestCase, GuiShareTest):
self.set_timeout(self.gui.share_mode, 2)
QtTest.QTest.qWait(3000)
QtCore.QTimer.singleShot(4000, self.accept_dialog)
QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton)
QtTest.QTest.mouseClick(
self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton
)
self.assertEqual(self.gui.share_mode.server_status.status, 0)
if __name__ == "__main__":
unittest.main()

View File

@ -4,11 +4,11 @@ import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeUnReadableFileTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
@ -16,10 +16,11 @@ class LocalShareModeUnReadableFileTest(unittest.TestCase, GuiShareTest):
GuiShareTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_unreadable_file_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiWebsiteTest import GuiWebsiteTest
class LocalWebsiteModeCSPEnabledTest(unittest.TestCase, GuiWebsiteTest):
@classmethod
def setUpClass(cls):
test_settings = {
"csp_header_disabled": False,
}
test_settings = {"csp_header_disabled": False}
cls.gui = GuiWebsiteTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalWebsiteModeCSPEnabledTest(unittest.TestCase, GuiWebsiteTest):
GuiWebsiteTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
# self.run_all_common_setup_tests()
self.run_all_website_mode_download_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .GuiWebsiteTest import GuiWebsiteTest
class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
@classmethod
def setUpClass(cls):
test_settings = {
"csp_header_disabled": True
}
test_settings = {"csp_header_disabled": True}
cls.gui = GuiWebsiteTest.set_up(test_settings)
@classmethod
@ -17,10 +16,11 @@ class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest):
GuiWebsiteTest.tear_down()
@pytest.mark.gui
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
# self.run_all_common_setup_tests()
self.run_all_website_mode_download_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -8,9 +8,7 @@ from .TorGuiShareTest import TorGuiShareTest
class ShareModeCancelSecondShareTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": True
}
test_settings = {"close_after_first_download": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -19,7 +17,7 @@ class ShareModeCancelSecondShareTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
@ -27,5 +25,6 @@ class ShareModeCancelSecondShareTest(unittest.TestCase, TorGuiShareTest):
self.server_is_stopped(self.gui.share_mode, False)
self.web_server_is_stopped()
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .TorGuiReceiveTest import TorGuiReceiveTest
class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": True,
"receive_allow_receiver_shutdown": True
}
test_settings = {"public_mode": True, "receive_allow_receiver_shutdown": True}
cls.gui = TorGuiReceiveTest.set_up(test_settings)
@classmethod
@ -19,10 +17,11 @@ class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(True, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .TorGuiReceiveTest import TorGuiReceiveTest
class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@classmethod
def setUpClass(cls):
test_settings = {
"receive_allow_receiver_shutdown": True
}
test_settings = {"receive_allow_receiver_shutdown": True}
cls.gui = TorGuiReceiveTest.set_up(test_settings)
@classmethod
@ -18,10 +17,11 @@ class ReceiveModeTest(unittest.TestCase, TorGuiReceiveTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_receive_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeCancelTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"autostart_timer": True,
}
test_settings = {"autostart_timer": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -18,11 +17,12 @@ class ShareModeCancelTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
self.cancel_the_share(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModePublicModeTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": True,
}
test_settings = {"public_mode": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -18,10 +17,11 @@ class ShareModePublicModeTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(True, False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,12 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeStayOpenTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"close_after_first_download": False,
}
test_settings = {"close_after_first_download": False}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -18,10 +17,11 @@ class ShareModeStayOpenTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,11 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -17,10 +17,11 @@ class ShareModeTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,6 +4,7 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
@ -22,10 +23,11 @@ class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_persistent_tests(False, True)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeStealthTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"use_legacy_v2_onions": True,
"use_stealth": True,
}
test_settings = {"use_legacy_v2_onions": True, "use_stealth": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -19,7 +17,7 @@ class ShareModeStealthTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()
@ -27,5 +25,6 @@ class ShareModeStealthTest(unittest.TestCase, TorGuiShareTest):
self.hidserv_auth_string()
self.copy_have_hidserv_auth_button(self.gui.share_mode)
if __name__ == "__main__":
unittest.main()

View File

@ -4,13 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTimerTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"autostop_timer": True,
}
test_settings = {"public_mode": False, "autostop_timer": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -19,10 +17,11 @@ class ShareModeTimerTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -4,16 +4,16 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeTorConnectionKilledTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
}
test_settings = {}
cls.gui = TorGuiShareTest.set_up(test_settings)
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_setup_tests()

View File

@ -4,12 +4,11 @@ import unittest
from .TorGuiShareTest import TorGuiShareTest
class ShareModeV2OnionTest(unittest.TestCase, TorGuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"use_legacy_v2_onions": True,
}
test_settings = {"use_legacy_v2_onions": True}
cls.gui = TorGuiShareTest.set_up(test_settings)
@classmethod
@ -18,11 +17,12 @@ class ShareModeV2OnionTest(unittest.TestCase, TorGuiShareTest):
@pytest.mark.gui
@pytest.mark.tor
@pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest")
@pytest.mark.skipif(pytest.__version__ < "2.9", reason="requires newer pytest")
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_tests(False, False)
self.have_v2_onion()
if __name__ == "__main__":
unittest.main()

View File

@ -20,7 +20,7 @@ import tempfile
import os
class MockSubprocess():
class MockSubprocess:
def __init__(self):
self.last_call = None

View File

@ -27,14 +27,14 @@ from onionshare.common import Common
class MyOnion:
def __init__(self, stealth=False):
self.auth_string = 'TestHidServAuth'
self.private_key = ''
self.auth_string = "TestHidServAuth"
self.private_key = ""
self.stealth = stealth
self.scheduled_key = None
@staticmethod
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
return 'test_service_id.onion'
return "test_service_id.onion"
@pytest.fixture
@ -65,18 +65,17 @@ class TestOnionShare:
onionshare_obj.set_stealth(False)
onionshare_obj.start_onion_service()
assert 17600 <= onionshare_obj.port <= 17650
assert onionshare_obj.onion_host == 'test_service_id.onion'
assert onionshare_obj.onion_host == "test_service_id.onion"
def test_start_onion_service_stealth(self, onionshare_obj):
onionshare_obj.set_stealth(True)
onionshare_obj.start_onion_service()
assert onionshare_obj.auth_string == 'TestHidServAuth'
assert onionshare_obj.auth_string == "TestHidServAuth"
def test_start_onion_service_local_only(self, onionshare_obj):
onionshare_obj.local_only = True
onionshare_obj.start_onion_service()
assert onionshare_obj.onion_host == '127.0.0.1:{}'.format(
onionshare_obj.port)
assert onionshare_obj.onion_host == "127.0.0.1:{}".format(onionshare_obj.port)
def test_cleanup(self, onionshare_obj, temp_dir_1024, temp_file_1024):
onionshare_obj.cleanup_filenames = [temp_dir_1024, temp_file_1024]

View File

@ -29,37 +29,40 @@ import zipfile
import pytest
LOG_MSG_REGEX = re.compile(r"""
LOG_MSG_REGEX = re.compile(
r"""
^\[Jun\ 06\ 2013\ 11:05:00\]
\ TestModule\.<function\ TestLog\.test_output\.<locals>\.dummy_func
\ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE)
PASSWORD_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$')
\ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""",
re.VERBOSE,
)
PASSWORD_REGEX = re.compile(r"^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$")
# TODO: Improve the Common tests to test it all as a single class
class TestBuildPassword:
@pytest.mark.parametrize('test_input,expected', (
@pytest.mark.parametrize(
"test_input,expected",
(
# VALID, two lowercase words, separated by a hyphen
('syrup-enzyme', True),
('caution-friday', True),
("syrup-enzyme", True),
("caution-friday", True),
# VALID, two lowercase words, with one hyphenated compound word
('drop-down-thimble', True),
('unmixed-yo-yo', True),
("drop-down-thimble", True),
("unmixed-yo-yo", True),
# VALID, two lowercase hyphenated compound words, separated by hyphen
('yo-yo-drop-down', True),
('felt-tip-t-shirt', True),
('hello-world', True),
("yo-yo-drop-down", True),
("felt-tip-t-shirt", True),
("hello-world", True),
# INVALID
('Upper-Case', False),
('digits-123', False),
('too-many-hyphens-', False),
('symbols-!@#$%', False)
))
("Upper-Case", False),
("digits-123", False),
("too-many-hyphens-", False),
("symbols-!@#$%", False),
),
)
def test_build_password_regex(self, test_input, expected):
""" Test that `PASSWORD_REGEX` accounts for the following patterns
@ -92,79 +95,87 @@ class TestDirSize:
class TestEstimatedTimeRemaining:
@pytest.mark.parametrize('test_input,expected', (
((2, 676, 12), '8h14m16s'),
((14, 1049, 30), '1h26m15s'),
((21, 450, 1), '33m42s'),
((31, 1115, 80), '11m39s'),
((336, 989, 32), '2m12s'),
((603, 949, 38), '36s'),
((971, 1009, 83), '1s')
))
@pytest.mark.parametrize(
"test_input,expected",
(
((2, 676, 12), "8h14m16s"),
((14, 1049, 30), "1h26m15s"),
((21, 450, 1), "33m42s"),
((31, 1115, 80), "11m39s"),
((336, 989, 32), "2m12s"),
((603, 949, 38), "36s"),
((971, 1009, 83), "1s"),
),
)
def test_estimated_time_remaining(
self, common_obj, test_input, expected, time_time_100):
self, common_obj, test_input, expected, time_time_100
):
assert common_obj.estimated_time_remaining(*test_input) == expected
@pytest.mark.parametrize('test_input', (
@pytest.mark.parametrize(
"test_input",
(
(10, 20, 100), # if `time_elapsed == 0`
(0, 37, 99) # if `download_rate == 0`
))
(0, 37, 99), # if `download_rate == 0`
),
)
def test_raises_zero_division_error(self, common_obj, test_input, time_time_100):
with pytest.raises(ZeroDivisionError):
common_obj.estimated_time_remaining(*test_input)
class TestFormatSeconds:
@pytest.mark.parametrize('test_input,expected', (
(0, '0s'),
(26, '26s'),
(60, '1m'),
(947.35, '15m47s'),
(1847, '30m47s'),
(2193.94, '36m34s'),
(3600, '1h'),
(13426.83, '3h43m47s'),
(16293, '4h31m33s'),
(18392.14, '5h6m32s'),
(86400, '1d'),
(129674, '1d12h1m14s'),
(56404.12, '15h40m4s')
))
@pytest.mark.parametrize(
"test_input,expected",
(
(0, "0s"),
(26, "26s"),
(60, "1m"),
(947.35, "15m47s"),
(1847, "30m47s"),
(2193.94, "36m34s"),
(3600, "1h"),
(13426.83, "3h43m47s"),
(16293, "4h31m33s"),
(18392.14, "5h6m32s"),
(86400, "1d"),
(129674, "1d12h1m14s"),
(56404.12, "15h40m4s"),
),
)
def test_format_seconds(self, common_obj, test_input, expected):
assert common_obj.format_seconds(test_input) == expected
# TODO: test negative numbers?
@pytest.mark.parametrize('test_input', (
'string', lambda: None, [], {}, set()
))
@pytest.mark.parametrize("test_input", ("string", lambda: None, [], {}, set()))
def test_invalid_input_types(self, common_obj, test_input):
with pytest.raises(TypeError):
common_obj.format_seconds(test_input)
class TestGetAvailablePort:
@pytest.mark.parametrize('port_min,port_max', (
(random.randint(1024, 1500),
random.randint(1800, 2048)) for _ in range(50)
))
@pytest.mark.parametrize(
"port_min,port_max",
((random.randint(1024, 1500), random.randint(1800, 2048)) for _ in range(50)),
)
def test_returns_an_open_port(self, common_obj, port_min, port_max):
""" get_available_port() should return an open port within the range """
port = common_obj.get_available_port(port_min, port_max)
assert port_min <= port <= port_max
with socket.socket() as tmpsock:
tmpsock.bind(('127.0.0.1', port))
tmpsock.bind(("127.0.0.1", port))
class TestGetPlatform:
def test_darwin(self, platform_darwin, common_obj):
assert common_obj.platform == 'Darwin'
assert common_obj.platform == "Darwin"
def test_linux(self, platform_linux, common_obj):
assert common_obj.platform == 'Linux'
assert common_obj.platform == "Linux"
def test_windows(self, platform_windows, common_obj):
assert common_obj.platform == 'Windows'
assert common_obj.platform == "Windows"
# TODO: double-check these tests
@ -173,94 +184,114 @@ class TestGetResourcePath:
prefix = os.path.join(
os.path.dirname(
os.path.dirname(
os.path.abspath(
inspect.getfile(
inspect.currentframe())))), 'share')
assert (
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
os.path.abspath(inspect.getfile(inspect.currentframe()))
)
),
"share",
)
assert common_obj.get_resource_path(
os.path.join(prefix, "test_filename")
) == os.path.join(prefix, "test_filename")
def test_linux(self, common_obj, platform_linux, sys_argv_sys_prefix):
prefix = os.path.join(sys.prefix, 'share/onionshare')
assert (
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
prefix = os.path.join(sys.prefix, "share/onionshare")
assert common_obj.get_resource_path(
os.path.join(prefix, "test_filename")
) == os.path.join(prefix, "test_filename")
def test_frozen_darwin(self, common_obj, platform_darwin, sys_frozen, sys_meipass):
prefix = os.path.join(sys._MEIPASS, 'share')
assert (
common_obj.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
prefix = os.path.join(sys._MEIPASS, "share")
assert common_obj.get_resource_path(
os.path.join(prefix, "test_filename")
) == os.path.join(prefix, "test_filename")
class TestGetTorPaths:
# @pytest.mark.skipif(sys.platform != 'Darwin', reason='requires MacOS') ?
def test_get_tor_paths_darwin(self, platform_darwin, common_obj, sys_frozen, sys_meipass):
def test_get_tor_paths_darwin(
self, platform_darwin, common_obj, sys_frozen, sys_meipass
):
base_path = os.path.dirname(
os.path.dirname(
os.path.dirname(
common_obj.get_resource_path(''))))
tor_path = os.path.join(
base_path, 'Resources', 'Tor', 'tor')
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')
assert (common_obj.get_tor_paths() ==
(tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path))
os.path.dirname(os.path.dirname(common_obj.get_resource_path("")))
)
tor_path = os.path.join(base_path, "Resources", "Tor", "tor")
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")
assert common_obj.get_tor_paths() == (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
)
# @pytest.mark.skipif(sys.platform != 'Linux', reason='requires Linux') ?
def test_get_tor_paths_linux(self, platform_linux, common_obj):
assert (common_obj.get_tor_paths() ==
('/usr/bin/tor', '/usr/share/tor/geoip', '/usr/share/tor/geoip6', '/usr/bin/obfs4proxy'))
assert common_obj.get_tor_paths() == (
"/usr/bin/tor",
"/usr/share/tor/geoip",
"/usr/share/tor/geoip6",
"/usr/bin/obfs4proxy",
)
# @pytest.mark.skipif(sys.platform != 'Windows', reason='requires Windows') ?
def test_get_tor_paths_windows(self, platform_windows, common_obj, sys_frozen):
base_path = os.path.join(
os.path.dirname(
os.path.dirname(
common_obj.get_resource_path(''))), 'tor')
tor_path = os.path.join(
os.path.join(base_path, 'Tor'), 'tor.exe')
os.path.dirname(os.path.dirname(common_obj.get_resource_path(""))), "tor"
)
tor_path = os.path.join(os.path.join(base_path, "Tor"), "tor.exe")
obfs4proxy_file_path = os.path.join(
os.path.join(base_path, 'Tor'), 'obfs4proxy.exe')
os.path.join(base_path, "Tor"), "obfs4proxy.exe"
)
tor_geo_ip_file_path = os.path.join(
os.path.join(
os.path.join(base_path, 'Data'), 'Tor'), 'geoip')
os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip"
)
tor_geo_ipv6_file_path = os.path.join(
os.path.join(
os.path.join(base_path, 'Data'), 'Tor'), 'geoip6')
assert (common_obj.get_tor_paths() ==
(tor_path, tor_geo_ip_file_path, tor_geo_ipv6_file_path, obfs4proxy_file_path))
os.path.join(os.path.join(base_path, "Data"), "Tor"), "geoip6"
)
assert common_obj.get_tor_paths() == (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
)
class TestHumanReadableFilesize:
@pytest.mark.parametrize('test_input,expected', (
(1024 ** 0, '1.0 B'),
(1024 ** 1, '1.0 KiB'),
(1024 ** 2, '1.0 MiB'),
(1024 ** 3, '1.0 GiB'),
(1024 ** 4, '1.0 TiB'),
(1024 ** 5, '1.0 PiB'),
(1024 ** 6, '1.0 EiB'),
(1024 ** 7, '1.0 ZiB'),
(1024 ** 8, '1.0 YiB')
))
@pytest.mark.parametrize(
"test_input,expected",
(
(1024 ** 0, "1.0 B"),
(1024 ** 1, "1.0 KiB"),
(1024 ** 2, "1.0 MiB"),
(1024 ** 3, "1.0 GiB"),
(1024 ** 4, "1.0 TiB"),
(1024 ** 5, "1.0 PiB"),
(1024 ** 6, "1.0 EiB"),
(1024 ** 7, "1.0 ZiB"),
(1024 ** 8, "1.0 YiB"),
),
)
def test_human_readable_filesize(self, common_obj, test_input, expected):
assert common_obj.human_readable_filesize(test_input) == expected
class TestLog:
@pytest.mark.parametrize('test_input', (
('[Jun 06 2013 11:05:00]'
' TestModule.<function TestLog.test_output.<locals>.dummy_func'
' at 0xdeadbeef>'),
('[Jun 06 2013 11:05:00]'
' TestModule.<function TestLog.test_output.<locals>.dummy_func'
' at 0xdeadbeef>: TEST_MSG')
))
@pytest.mark.parametrize(
"test_input",
(
(
"[Jun 06 2013 11:05:00]"
" TestModule.<function TestLog.test_output.<locals>.dummy_func"
" at 0xdeadbeef>"
),
(
"[Jun 06 2013 11:05:00]"
" TestModule.<function TestLog.test_output.<locals>.dummy_func"
" at 0xdeadbeef>: TEST_MSG"
),
),
)
def test_log_msg_regex(self, test_input):
assert bool(LOG_MSG_REGEX.match(test_input))
@ -272,10 +303,10 @@ class TestLog:
# From: https://stackoverflow.com/questions/1218933
with io.StringIO() as buf, contextlib.redirect_stdout(buf):
common_obj.log('TestModule', dummy_func)
common_obj.log('TestModule', dummy_func, 'TEST_MSG')
common_obj.log("TestModule", dummy_func)
common_obj.log("TestModule", dummy_func, "TEST_MSG")
output = buf.getvalue()
line_one, line_two, _ = output.split('\n')
line_one, line_two, _ = output.split("\n")
assert LOG_MSG_REGEX.match(line_one)
assert LOG_MSG_REGEX.match(line_two)

View File

@ -28,86 +28,86 @@ from onionshare import common, settings, strings
@pytest.fixture
def os_path_expanduser(monkeypatch):
monkeypatch.setattr('os.path.expanduser', lambda path: path)
monkeypatch.setattr("os.path.expanduser", lambda path: path)
@pytest.fixture
def settings_obj(sys_onionshare_dev_mode, platform_linux):
_common = common.Common()
_common.version = 'DUMMY_VERSION_1.2.3'
_common.version = "DUMMY_VERSION_1.2.3"
return settings.Settings(_common)
class TestSettings:
def test_init(self, settings_obj):
expected_settings = {
'version': 'DUMMY_VERSION_1.2.3',
'connection_type': 'bundled',
'control_port_address': '127.0.0.1',
'control_port_port': 9051,
'socks_address': '127.0.0.1',
'socks_port': 9050,
'socket_file_path': '/var/run/tor/control',
'auth_type': 'no_auth',
'auth_password': '',
'close_after_first_download': True,
'autostop_timer': False,
'autostart_timer': False,
'use_stealth': False,
'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': '',
'use_legacy_v2_onions': False,
'save_private_key': False,
'private_key': '',
'password': '',
'hidservauth_string': '',
'data_dir': os.path.expanduser('~/OnionShare'),
'public_mode': False,
'csp_header_disabled': False
"version": "DUMMY_VERSION_1.2.3",
"connection_type": "bundled",
"control_port_address": "127.0.0.1",
"control_port_port": 9051,
"socks_address": "127.0.0.1",
"socks_port": 9050,
"socket_file_path": "/var/run/tor/control",
"auth_type": "no_auth",
"auth_password": "",
"close_after_first_download": True,
"autostop_timer": False,
"autostart_timer": False,
"use_stealth": False,
"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": "",
"use_legacy_v2_onions": False,
"save_private_key": False,
"private_key": "",
"password": "",
"hidservauth_string": "",
"data_dir": os.path.expanduser("~/OnionShare"),
"public_mode": False,
"csp_header_disabled": False,
}
for key in settings_obj._settings:
# Skip locale, it will not always default to the same thing
if key != 'locale':
if key != "locale":
assert settings_obj._settings[key] == settings_obj.default_settings[key]
assert settings_obj._settings[key] == expected_settings[key]
def test_fill_in_defaults(self, settings_obj):
del settings_obj._settings['version']
del settings_obj._settings["version"]
settings_obj.fill_in_defaults()
assert settings_obj._settings['version'] == 'DUMMY_VERSION_1.2.3'
assert settings_obj._settings["version"] == "DUMMY_VERSION_1.2.3"
def test_load(self, settings_obj):
custom_settings = {
'version': 'CUSTOM_VERSION',
'socks_port': 9999,
'use_stealth': True
"version": "CUSTOM_VERSION",
"socks_port": 9999,
"use_stealth": True,
}
tmp_file, tmp_file_path = tempfile.mkstemp()
with open(tmp_file, 'w') as f:
with open(tmp_file, "w") as f:
json.dump(custom_settings, f)
settings_obj.filename = tmp_file_path
settings_obj.load()
assert settings_obj._settings['version'] == 'CUSTOM_VERSION'
assert settings_obj._settings['socks_port'] == 9999
assert settings_obj._settings['use_stealth'] is True
assert settings_obj._settings["version"] == "CUSTOM_VERSION"
assert settings_obj._settings["socks_port"] == 9999
assert settings_obj._settings["use_stealth"] is True
os.remove(tmp_file_path)
assert os.path.exists(tmp_file_path) is False
def test_save(self, monkeypatch, settings_obj):
monkeypatch.setattr(strings, '_', lambda _: '')
monkeypatch.setattr(strings, "_", lambda _: "")
settings_filename = 'default_settings.json'
settings_filename = "default_settings.json"
tmp_dir = tempfile.gettempdir()
settings_path = os.path.join(tmp_dir, settings_filename)
settings_obj.filename = settings_path
settings_obj.save()
with open(settings_path, 'r') as f:
with open(settings_path, "r") as f:
settings = json.load(f)
assert settings_obj._settings == settings
@ -116,69 +116,64 @@ class TestSettings:
assert os.path.exists(settings_path) is False
def test_get(self, settings_obj):
assert settings_obj.get('version') == 'DUMMY_VERSION_1.2.3'
assert settings_obj.get('connection_type') == 'bundled'
assert settings_obj.get('control_port_address') == '127.0.0.1'
assert settings_obj.get('control_port_port') == 9051
assert settings_obj.get('socks_address') == '127.0.0.1'
assert settings_obj.get('socks_port') == 9050
assert settings_obj.get('socket_file_path') == '/var/run/tor/control'
assert settings_obj.get('auth_type') == 'no_auth'
assert settings_obj.get('auth_password') == ''
assert settings_obj.get('close_after_first_download') is True
assert settings_obj.get('use_stealth') is False
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("version") == "DUMMY_VERSION_1.2.3"
assert settings_obj.get("connection_type") == "bundled"
assert settings_obj.get("control_port_address") == "127.0.0.1"
assert settings_obj.get("control_port_port") == 9051
assert settings_obj.get("socks_address") == "127.0.0.1"
assert settings_obj.get("socks_port") == 9050
assert settings_obj.get("socket_file_path") == "/var/run/tor/control"
assert settings_obj.get("auth_type") == "no_auth"
assert settings_obj.get("auth_password") == ""
assert settings_obj.get("close_after_first_download") is True
assert settings_obj.get("use_stealth") is False
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") == ""
def test_set_version(self, settings_obj):
settings_obj.set('version', 'CUSTOM_VERSION')
assert settings_obj._settings['version'] == 'CUSTOM_VERSION'
settings_obj.set("version", "CUSTOM_VERSION")
assert settings_obj._settings["version"] == "CUSTOM_VERSION"
def test_set_control_port_port(self, settings_obj):
settings_obj.set('control_port_port', 999)
assert settings_obj._settings['control_port_port'] == 999
settings_obj.set("control_port_port", 999)
assert settings_obj._settings["control_port_port"] == 999
settings_obj.set('control_port_port', 'NON_INTEGER')
assert settings_obj._settings['control_port_port'] == 9051
settings_obj.set("control_port_port", "NON_INTEGER")
assert settings_obj._settings["control_port_port"] == 9051
def test_set_socks_port(self, settings_obj):
settings_obj.set('socks_port', 888)
assert settings_obj._settings['socks_port'] == 888
settings_obj.set("socks_port", 888)
assert settings_obj._settings["socks_port"] == 888
settings_obj.set('socks_port', 'NON_INTEGER')
assert settings_obj._settings['socks_port'] == 9050
settings_obj.set("socks_port", "NON_INTEGER")
assert settings_obj._settings["socks_port"] == 9050
def test_filename_darwin(
self,
monkeypatch,
os_path_expanduser,
platform_darwin):
def test_filename_darwin(self, monkeypatch, os_path_expanduser, platform_darwin):
obj = settings.Settings(common.Common())
assert (obj.filename ==
'~/Library/Application Support/OnionShare/onionshare.json')
assert (
obj.filename == "~/Library/Application Support/OnionShare/onionshare.json"
)
def test_filename_linux(
self,
monkeypatch,
os_path_expanduser,
platform_linux):
def test_filename_linux(self, monkeypatch, os_path_expanduser, platform_linux):
obj = settings.Settings(common.Common())
assert obj.filename == '~/.config/onionshare/onionshare.json'
assert obj.filename == "~/.config/onionshare/onionshare.json"
def test_filename_windows(
self,
monkeypatch,
platform_windows):
monkeypatch.setenv('APPDATA', 'C:')
def test_filename_windows(self, monkeypatch, platform_windows):
monkeypatch.setenv("APPDATA", "C:")
obj = settings.Settings(common.Common())
assert obj.filename.replace('/', '\\') == 'C:\\OnionShare\\onionshare.json'
assert obj.filename.replace("/", "\\") == "C:\\OnionShare\\onionshare.json"
def test_set_custom_bridge(self, settings_obj):
settings_obj.set('tor_bridges_use_custom_bridges', 'Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E')
assert settings_obj._settings['tor_bridges_use_custom_bridges'] == 'Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E'
settings_obj.set(
"tor_bridges_use_custom_bridges",
"Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E",
)
assert (
settings_obj._settings["tor_bridges_use_custom_bridges"]
== "Bridge 45.3.20.65:9050 21300AD88890A49C429A6CB9959CFD44490A8F6E"
)

View File

@ -32,31 +32,34 @@ from onionshare.settings import Settings
# return path
# common.get_resource_path = get_resource_path
def test_underscore_is_function():
assert callable(strings._) and isinstance(strings._, types.FunctionType)
class TestLoadStrings:
def test_load_strings_defaults_to_english(
self, common_obj, locale_en, sys_onionshare_dev_mode):
self, common_obj, locale_en, sys_onionshare_dev_mode
):
""" load_strings() loads English by default """
common_obj.settings = Settings(common_obj)
strings.load_strings(common_obj)
assert strings._('preparing_files') == "Compressing files."
assert strings._("preparing_files") == "Compressing files."
def test_load_strings_loads_other_languages(
self, common_obj, locale_fr, sys_onionshare_dev_mode):
self, common_obj, locale_fr, sys_onionshare_dev_mode
):
""" load_strings() loads other languages in different locales """
common_obj.settings = Settings(common_obj)
common_obj.settings.set('locale', 'fr')
common_obj.settings.set("locale", "fr")
strings.load_strings(common_obj)
assert strings._('preparing_files') == "Compression des fichiers."
assert strings._("preparing_files") == "Compression des fichiers."
def test_load_invalid_locale(
self, common_obj, locale_invalid, sys_onionshare_dev_mode):
self, common_obj, locale_invalid, sys_onionshare_dev_mode
):
""" load_strings() raises a KeyError for an invalid locale """
with pytest.raises(KeyError):
common_obj.settings = Settings(common_obj)
common_obj.settings.set('locale', 'XX')
common_obj.settings.set("locale", "XX")
strings.load_strings(common_obj)

View File

@ -37,8 +37,8 @@ from onionshare import strings
from onionshare.web import Web
from onionshare.settings import Settings
DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$')
RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$')
DEFAULT_ZW_FILENAME_REGEX = re.compile(r"^onionshare_[a-z2-7]{6}.zip$")
RANDOM_STR_REGEX = re.compile(r"^[a-z2-7]+$")
def web_obj(common_obj, mode, num_files=0):
@ -53,12 +53,12 @@ def web_obj(common_obj, mode, num_files=0):
web.app.testing = True
# Share mode
if mode == 'share':
if mode == "share":
# Add files
files = []
for i in range(num_files):
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
tmp_file.write(b'*' * 1024)
tmp_file.write(b"*" * 1024)
files.append(tmp_file.name)
web.share_mode.set_file_info(files)
# Receive mode
@ -70,122 +70,130 @@ def web_obj(common_obj, mode, num_files=0):
class TestWeb:
def test_share_mode(self, common_obj):
web = web_obj(common_obj, 'share', 3)
assert web.mode is 'share'
web = web_obj(common_obj, "share", 3)
assert web.mode is "share"
with web.app.test_client() as c:
# Load / without auth
res = c.get('/')
res = c.get("/")
res.get_data()
assert res.status_code == 401
# Load / with invalid auth
res = c.get('/', headers=self._make_auth_headers('invalid'))
res = c.get("/", headers=self._make_auth_headers("invalid"))
res.get_data()
assert res.status_code == 401
# Load / with valid auth
res = c.get('/', headers=self._make_auth_headers(web.password))
res = c.get("/", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
# Download
res = c.get('/download', headers=self._make_auth_headers(web.password))
res = c.get("/download", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
assert res.mimetype == 'application/zip'
assert res.mimetype == "application/zip"
def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024):
web = web_obj(common_obj, 'share', 3)
web = web_obj(common_obj, "share", 3)
web.stay_open = False
assert web.running == True
with web.app.test_client() as c:
# Download the first time
res = c.get('/download', headers=self._make_auth_headers(web.password))
res = c.get("/download", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
assert res.mimetype == 'application/zip'
assert res.mimetype == "application/zip"
assert web.running == False
def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024):
web = web_obj(common_obj, 'share', 3)
def test_share_mode_close_after_first_download_off(
self, common_obj, temp_file_1024
):
web = web_obj(common_obj, "share", 3)
web.stay_open = True
assert web.running == True
with web.app.test_client() as c:
# Download the first time
res = c.get('/download', headers=self._make_auth_headers(web.password))
res = c.get("/download", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
assert res.mimetype == 'application/zip'
assert res.mimetype == "application/zip"
assert web.running == True
def test_receive_mode(self, common_obj):
web = web_obj(common_obj, 'receive')
assert web.mode is 'receive'
web = web_obj(common_obj, "receive")
assert web.mode is "receive"
with web.app.test_client() as c:
# Load / without auth
res = c.get('/')
res = c.get("/")
res.get_data()
assert res.status_code == 401
# Load / with invalid auth
res = c.get('/', headers=self._make_auth_headers('invalid'))
res = c.get("/", headers=self._make_auth_headers("invalid"))
res.get_data()
assert res.status_code == 401
# Load / with valid auth
res = c.get('/', headers=self._make_auth_headers(web.password))
res = c.get("/", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
def test_public_mode_on(self, common_obj):
web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', True)
web = web_obj(common_obj, "receive")
common_obj.settings.set("public_mode", True)
with web.app.test_client() as c:
# Loading / should work without auth
res = c.get('/')
res = c.get("/")
data1 = res.get_data()
assert res.status_code == 200
def test_public_mode_off(self, common_obj):
web = web_obj(common_obj, 'receive')
common_obj.settings.set('public_mode', False)
web = web_obj(common_obj, "receive")
common_obj.settings.set("public_mode", False)
with web.app.test_client() as c:
# Load / without auth
res = c.get('/')
res = c.get("/")
res.get_data()
assert res.status_code == 401
# But static resources should work without auth
res = c.get('{}/css/style.css'.format(web.static_url_path))
res = c.get("{}/css/style.css".format(web.static_url_path))
res.get_data()
assert res.status_code == 200
# Load / with valid auth
res = c.get('/', headers=self._make_auth_headers(web.password))
res = c.get("/", headers=self._make_auth_headers(web.password))
res.get_data()
assert res.status_code == 200
def _make_auth_headers(self, password):
auth = base64.b64encode(b'onionshare:'+password.encode()).decode()
auth = base64.b64encode(b"onionshare:" + password.encode()).decode()
h = Headers()
h.add('Authorization', 'Basic ' + auth)
h.add("Authorization", "Basic " + auth)
return h
class TestZipWriterDefault:
@pytest.mark.parametrize('test_input', (
'onionshare_{}.zip'.format(''.join(
random.choice('abcdefghijklmnopqrstuvwxyz234567') for _ in range(6)
)) for _ in range(50)
))
@pytest.mark.parametrize(
"test_input",
(
"onionshare_{}.zip".format(
"".join(
random.choice("abcdefghijklmnopqrstuvwxyz234567") for _ in range(6)
)
)
for _ in range(50)
),
)
def test_default_zw_filename_regex(self, test_input):
assert bool(DEFAULT_ZW_FILENAME_REGEX.match(test_input))
@ -200,15 +208,14 @@ class TestZipWriterDefault:
assert default_zw.z._allowZip64 is True
def test_zipfile_mode(self, default_zw):
assert default_zw.z.mode == 'w'
assert default_zw.z.mode == "w"
def test_callback(self, default_zw):
assert default_zw.processed_size_callback(None) is None
def test_add_file(self, default_zw, temp_file_1024_delete):
default_zw.add_file(temp_file_1024_delete)
zipfile_info = default_zw.z.getinfo(
os.path.basename(temp_file_1024_delete))
zipfile_info = default_zw.z.getinfo(os.path.basename(temp_file_1024_delete))
assert zipfile_info.compress_type == zipfile.ZIP_DEFLATED
assert zipfile_info.file_size == 1024
@ -220,12 +227,15 @@ class TestZipWriterDefault:
class TestZipWriterCustom:
@pytest.mark.parametrize('test_input', (
@pytest.mark.parametrize(
"test_input",
(
Common.random_string(
random.randint(2, 50),
random.choice((None, random.randint(2, 50)))
) for _ in range(50)
))
random.randint(2, 50), random.choice((None, random.randint(2, 50)))
)
for _ in range(50)
),
)
def test_random_string_regex(self, test_input):
assert bool(RANDOM_STR_REGEX.match(test_input))
@ -233,4 +243,4 @@ class TestZipWriterCustom:
assert bool(RANDOM_STR_REGEX.match(custom_zw.zip_filename))
def test_custom_callback(self, custom_zw):
assert custom_zw.processed_size_callback(None) == 'custom_callback'
assert custom_zw.processed_size_callback(None) == "custom_callback"