summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMicah Lee <micah@micahflee.com>2018-02-25 17:45:27 -0800
committerMicah Lee <micah@micahflee.com>2018-02-25 17:45:27 -0800
commitf7dba635ebd140298b9c7c7067ad3de51c228071 (patch)
tree07a748775c9f8ea3d9b05475f7d1d7c85a46985c
parent29ad669e543661b3ce6ec5ec60a8213b73486142 (diff)
parent88a765b7e1c230760654797edb9b17d1dfbf9191 (diff)
downloadonionshare-f7dba635ebd140298b9c7c7067ad3de51c228071.tar.gz
onionshare-f7dba635ebd140298b9c7c7067ad3de51c228071.zip
Merge branch 'develop'
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml7
-rw-r--r--BUILD.md2
-rw-r--r--CHANGELOG.md15
-rw-r--r--README.md2
-rw-r--r--install/get-tor-osx.py6
-rw-r--r--install/get-tor-windows.py6
-rw-r--r--install/onionshare.desktop2
-rw-r--r--install/onionshare.nsi34
-rw-r--r--install/pyinstaller.spec2
-rw-r--r--install/scripts/onionshare-nautilus.py2
-rw-r--r--onionshare/common.py13
-rw-r--r--onionshare/onion.py48
-rw-r--r--onionshare/settings.py3
-rw-r--r--onionshare/web.py32
-rw-r--r--onionshare_gui/__init__.py4
-rw-r--r--onionshare_gui/downloads.py8
-rw-r--r--onionshare_gui/file_selection.py238
-rw-r--r--onionshare_gui/onionshare_gui.py291
-rw-r--r--onionshare_gui/server_status.py208
-rw-r--r--onionshare_gui/settings_dialog.py213
-rw-r--r--onionshare_gui/update_checker.py29
-rw-r--r--screenshots/appdata-client.pngbin87558 -> 57679 bytes
-rw-r--r--screenshots/appdata-server.pngbin107845 -> 119402 bytes
-rw-r--r--screenshots/client.pngbin70862 -> 40602 bytes
-rw-r--r--screenshots/server.pngbin45317 -> 45923 bytes
-rw-r--r--setup.py22
-rw-r--r--share/html/404.html1
-rw-r--r--share/html/denied.html3
-rw-r--r--share/html/index.html263
-rw-r--r--share/images/download_completed.pngbin0 -> 646 bytes
-rw-r--r--share/images/download_completed_none.pngbin0 -> 437 bytes
-rw-r--r--share/images/download_in_progress.pngbin0 -> 638 bytes
-rw-r--r--share/images/download_in_progress_none.pngbin0 -> 412 bytes
-rw-r--r--share/images/drop_files.pngbin2035 -> 0 bytes
-rw-r--r--share/images/favicon.icobin0 -> 4286 bytes
-rw-r--r--share/images/file_delete.pngbin0 -> 182 bytes
-rw-r--r--share/images/info.pngbin0 -> 435 bytes
-rw-r--r--share/images/logo_transparent.pngbin0 -> 3740 bytes
-rw-r--r--share/images/server_started.pngbin346 -> 347 bytes
-rw-r--r--share/images/server_stopped.pngbin286 -> 342 bytes
-rw-r--r--share/images/server_working.pngbin338 -> 349 bytes
-rw-r--r--share/images/settings_inactive.pngbin513 -> 0 bytes
-rw-r--r--share/images/web_file.pngbin0 -> 251 bytes
-rw-r--r--share/images/web_folder.pngbin0 -> 338 bytes
-rw-r--r--share/locale/da.json136
-rw-r--r--share/locale/en.json66
-rw-r--r--share/locale/nl.json1
-rw-r--r--share/torrc_template-meek_lite_amazon2
-rw-r--r--share/torrc_template-meek_lite_azure2
-rw-r--r--share/version.txt2
-rw-r--r--test/conftest.py6
-rw-r--r--test/test_onionshare_common.py6
-rw-r--r--test/test_onionshare_settings.py5
54 files changed, 1260 insertions, 421 deletions
diff --git a/.gitignore b/.gitignore
index 712484d6..b202993b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ pip-log.txt
.tox
nosetests.xml
.cache
+.pytest_cache
# Translations
*.mo
diff --git a/.travis.yml b/.travis.yml
index 6d324010..9010e77a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,12 @@ python:
- "nightly"
# command to install dependencies
install:
- - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls
+ - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8
+before_script:
+ # stop the build if there are Python syntax errors or undefined names
+ - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
# command to run tests
script: pytest --cov=onionshare test/
after_success:
diff --git a/BUILD.md b/BUILD.md
index 55355aad..e6e54951 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -38,6 +38,8 @@ Install Xcode from the Mac App Store. Once it's installed, run it for the first
Download and install Python 3.6.4 from https://www.python.org/downloads/release/python-364/. I downloaded `python-3.6.4-macosx10.6.pkg`.
+You may also need to run the command `/Applications/Python\ 3.6/Install\ Certificates.command` to update Python 3.6's internal certificate store. Otherwise, you may find that fetching the Tor Browser .dmg file fails later due to a certificate validation error.
+
Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-mac-x64-3.0.2-online.dmg`. There's no need to login to a Qt account during installation. Make sure you install the latest Qt 5.x. I installed Qt 5.10.0 -- all you need is to check `Qt > Qt 5.10.0 > macOS`.
Now install some python dependencies with pip (note, there's issues building a .app if you install this in a virtualenv):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecd0549d..6169840b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
# OnionShare Changelog
+## 1.3
+
+* Major UI redesign, introducing many UX improvements
+* Client-side web interfact redesigned
+* New feature: Support for meek_lite pluggable transports (Amazon and Azure)
+* New feature: Support for custom obfs4 and meek-lite bridges
+* New feature: ability to cancel share before it starts
+* Bug fix: the UpdateChecker no longer blocks the UI when checking
+* Bug fix: simultaneous downloads (broken in 1.2)
+* Update Tor to 0.2.3.9
+* Improved support for BSD
+* Updated French and Danish translations
+* Minor build script and build documentation fixes
+* Add flake8 tests
+
## 1.2
* New feature: Support for Tor bridges, including obfs4proxy
diff --git a/README.md b/README.md
index c83636b0..55487045 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ You can download OnionShare for Windows and macOS from the [OnionShare website](
## Developing OnionShare
-You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md).
+You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md). You may also subscribe to our developers mailing list [here](https://lists.riseup.net/www/info/onionshare-dev).
# Screenshots
diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py
index f6b04aa4..f6cac62f 100644
--- a/install/get-tor-osx.py
+++ b/install/get-tor-osx.py
@@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, zipfile, io, shutil, subprocess
import urllib.request
def main():
- dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/TorBrowser-7.0.11-osx64_en-US.dmg'
- dmg_filename = 'TorBrowser-7.0.11-osx64_en-US.dmg'
- expected_dmg_sha256 = '5143e4a2141a69f66869be13eef4bcaac4e6c27c78383fc8a4c38b334759f3a2'
+ dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/TorBrowser-7.5-osx64_en-US.dmg'
+ dmg_filename = 'TorBrowser-7.5-osx64_en-US.dmg'
+ expected_dmg_sha256 = '43a8dc0afd0a77e42766311eb54ad9fc8714f67fcd2d3582a3bcb98b22c2e629'
# Build paths
root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py
index b1a6665d..f5aeb3f7 100644
--- a/install/get-tor-windows.py
+++ b/install/get-tor-windows.py
@@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, shutil, subprocess
import urllib.request
def main():
- exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/torbrowser-install-7.0.11_en-US.exe'
- exe_filename = 'torbrowser-install-7.0.11_en-US.exe'
- expected_exe_sha256 = 'a033eb9b9ed2ad389169b36a90946a8af8f05bd0c7bbd3e37678041331096624'
+ exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5/torbrowser-install-7.5_en-US.exe'
+ exe_filename = 'torbrowser-install-7.5_en-US.exe'
+ expected_exe_sha256 = '81ccb9456118cf8fa755a3eafb5c514665fc69599cdd41e9eb36baa335ebe233'
# 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')
diff --git a/install/onionshare.desktop b/install/onionshare.desktop
index 256a7b50..fbac3660 100644
--- a/install/onionshare.desktop
+++ b/install/onionshare.desktop
@@ -1,9 +1,11 @@
[Desktop Entry]
Name=OnionShare
Comment=Share a file securely and anonymously over Tor
+Comment[da]=Del en fil sikkert og anonymt over Tor
Exec=/usr/bin/onionshare-gui
Terminal=false
Type=Application
Icon=/usr/share/pixmaps/onionshare80.xpm
Categories=Network;
Keywords=tor;anonymity;privacy;onion service;file sharing;file hosting;
+Keywords[da]=tor;anonymitet;privatliv;onion-tjeneste;fildeling;filhosting;
diff --git a/install/onionshare.nsi b/install/onionshare.nsi
index 9c15de47..4030636a 100644
--- a/install/onionshare.nsi
+++ b/install/onionshare.nsi
@@ -3,10 +3,10 @@
!define ABOUTURL "https:\\onionshare.org\"
# change these with each release
-!define INSTALLSIZE 66525
+!define INSTALLSIZE 66537
!define VERSIONMAJOR 1
-!define VERSIONMINOR 2
-!define VERSIONSTRING "1.2"
+!define VERSIONMINOR 3
+!define VERSIONSTRING "1.3"
RequestExecutionLevel admin
@@ -192,6 +192,8 @@ Section "install"
File "${BINPATH}\share\torrc_template"
File "${BINPATH}\share\torrc_template-windows"
File "${BINPATH}\share\torrc_template-obfs4"
+ File "${BINPATH}\share\torrc_template-meek_lite_amazon"
+ File "${BINPATH}\share\torrc_template-meek_lite_azure"
File "${BINPATH}\share\version.txt"
File "${BINPATH}\share\wordlist.txt"
@@ -201,14 +203,22 @@ Section "install"
File "${BINPATH}\share\html\index.html"
SetOutPath "$INSTDIR\share\images"
- File "${BINPATH}\share\images\drop_files.png"
+ File "${BINPATH}\share\images\download_completed.png"
+ File "${BINPATH}\share\images\download_completed_none.png"
+ File "${BINPATH}\share\images\download_in_progress.png"
+ File "${BINPATH}\share\images\download_in_progress_none.png"
+ File "${BINPATH}\share\images\favicon.ico"
+ File "${BINPATH}\share\images\file_delete.png"
+ File "${BINPATH}\share\images\info.png"
File "${BINPATH}\share\images\logo.png"
+ File "${BINPATH}\share\images\logo_transparent.png"
File "${BINPATH}\share\images\logo_grayscale.png"
File "${BINPATH}\share\images\server_started.png"
File "${BINPATH}\share\images\server_stopped.png"
File "${BINPATH}\share\images\server_working.png"
File "${BINPATH}\share\images\settings.png"
- File "${BINPATH}\share\images\settings_inactive.png"
+ File "${BINPATH}\share\images\web_file.png"
+ File "${BINPATH}\share\images\web_folder.png"
SetOutPath "$INSTDIR\share\locale"
File "${BINPATH}\share\locale\cs.json"
@@ -379,14 +389,22 @@ FunctionEnd
Delete "$INSTDIR\share\html\404.html"
Delete "$INSTDIR\share\html\denied.html"
Delete "$INSTDIR\share\html\index.html"
- Delete "$INSTDIR\share\images\drop_files.png"
+ Delete "$INSTDIR\share\images\download_completed.png"
+ Delete "$INSTDIR\share\images\download_completed_none.png"
+ Delete "$INSTDIR\share\images\download_in_progress.png"
+ Delete "$INSTDIR\share\images\download_in_progress_none.png"
+ Delete "$INSTDIR\share\images\favicon.ico"
+ Delete "$INSTDIR\share\images\file_delete.png"
+ Delete "$INSTDIR\share\images\info.png"
Delete "$INSTDIR\share\images\logo.png"
+ Delete "$INSTDIR\share\images\logo_transparent.png"
Delete "$INSTDIR\share\images\logo_grayscale.png"
Delete "$INSTDIR\share\images\server_started.png"
Delete "$INSTDIR\share\images\server_stopped.png"
Delete "$INSTDIR\share\images\server_working.png"
Delete "$INSTDIR\share\images\settings.png"
- Delete "$INSTDIR\share\images\settings_inactive.png"
+ Delete "$INSTDIR\share\images\web_file.png"
+ Delete "$INSTDIR\share\images\web_folder.png"
Delete "$INSTDIR\share\license.txt"
Delete "$INSTDIR\share\locale\cs.json"
Delete "$INSTDIR\share\locale\de.json"
@@ -404,6 +422,8 @@ FunctionEnd
Delete "$INSTDIR\share\torrc_template"
Delete "$INSTDIR\share\torrc_template-windows"
Delete "$INSTDIR\share\torrc_template-obfs4"
+ Delete "$INSTDIR\share\torrc_template-meek_lite_amazon"
+ Delete "$INSTDIR\share\torrc_template-meek_lite_azure"
Delete "$INSTDIR\share\version.txt"
Delete "$INSTDIR\share\wordlist.txt"
Delete "$INSTDIR\sip.pyd"
diff --git a/install/pyinstaller.spec b/install/pyinstaller.spec
index a38aaf6e..6ca2fdbe 100644
--- a/install/pyinstaller.spec
+++ b/install/pyinstaller.spec
@@ -15,6 +15,8 @@ a = Analysis(
('../share/wordlist.txt', 'share'),
('../share/torrc_template', 'share'),
('../share/torrc_template-obfs4', 'share'),
+ ('../share/torrc_template-meek_lite_amazon', 'share'),
+ ('../share/torrc_template-meek_lite_azure', 'share'),
('../share/torrc_template-windows', 'share'),
('../share/images/*', 'share/images'),
('../share/locale/*', 'share/locale'),
diff --git a/install/scripts/onionshare-nautilus.py b/install/scripts/onionshare-nautilus.py
index 6674dd18..ed50fb23 100644
--- a/install/scripts/onionshare-nautilus.py
+++ b/install/scripts/onionshare-nautilus.py
@@ -64,7 +64,7 @@ class OnionShareExtension(GObject.GObject, Nautilus.MenuProvider):
"""
def url2path(self,url):
- file_uri = url.get_activation_uri()
+ file_uri = url.get_activation_uri()
arg_uri = file_uri[7:]
path = urllib.url2pathname(arg_uri)
return path
diff --git a/onionshare/common.py b/onionshare/common.py
index 25b901ee..0d00c7b1 100644
--- a/onionshare/common.py
+++ b/onionshare/common.py
@@ -56,7 +56,10 @@ def get_platform():
"""
Returns the platform OnionShare is running on.
"""
- return platform.system()
+ plat = platform.system()
+ if plat.endswith('BSD'):
+ plat = 'BSD'
+ return plat
def get_resource_path(filename):
@@ -66,6 +69,10 @@ def get_resource_path(filename):
"""
p = get_platform()
+ # On Windows, and in Windows dev mode, switch slashes in incoming filename to backslackes
+ if p == 'Windows':
+ filename = filename.replace('/', '\\')
+
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')
@@ -73,7 +80,7 @@ def get_resource_path(filename):
# 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')
- elif p == 'Linux':
+ elif p == 'BSD' or p == 'Linux':
# Assume OnionShare is installed systemwide in Linux, since we're not running in dev mode
prefix = os.path.join(sys.prefix, 'share/onionshare')
@@ -107,7 +114,7 @@ def get_tor_paths():
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 p == 'OpenBSD' or p == 'FreeBSD':
+ elif p == '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'
diff --git a/onionshare/onion.py b/onionshare/onion.py
index bae90f4c..068648ba 100644
--- a/onionshare/onion.py
+++ b/onionshare/onion.py
@@ -131,7 +131,7 @@ class Onion(object):
self.stealth = False
self.service_id = None
- self.system = platform.system()
+ self.system = common.get_platform()
# Is bundled tor supported?
if (self.system == 'Windows' or self.system == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
@@ -183,7 +183,7 @@ class Onion(object):
raise OSError(strings._('no_available_port'))
self.tor_torrc = os.path.join(self.tor_data_directory.name, 'torrc')
else:
- # Linux and Mac can use unix sockets
+ # Linux, Mac and BSD can use unix sockets
with open(common.get_resource_path('torrc_template')) as f:
torrc_template = f.read()
self.tor_control_port = None
@@ -211,7 +211,22 @@ class Onion(object):
with open(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_amazon'):
+ f.write('ClientTransportPlugin meek_lite exec {}\n'.format(self.obfs4proxy_file_path))
+ with open(common.get_resource_path('torrc_template-meek_lite_amazon')) 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(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')
@@ -265,7 +280,10 @@ class Onion(object):
time.sleep(0.2)
# If using bridges, it might take a bit longer to connect to Tor
- if self.settings.get('tor_bridges_use_custom_bridges') or self.settings.get('tor_bridges_use_obfs4'):
+ 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_amazon') or \
+ self.settings.get('tor_bridges_use_meek_lite_azure'):
connect_timeout = 150
else:
# Timeout after 120 seconds
@@ -316,7 +334,7 @@ class Onion(object):
# guessing the socket file name next
if not found_tor:
try:
- if self.system == 'Linux':
+ if self.system == 'Linux' or self.system == 'BSD':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
elif self.system == 'Darwin':
socket_file_path = '/run/user/{}/Tor/control.socket'.format(os.geteuid())
@@ -470,8 +488,8 @@ class Onion(object):
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
- self.settings.save()
if onion_host is not None:
+ self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._('error_tor_protocol_error'))
@@ -482,13 +500,19 @@ class Onion(object):
"""
common.log('Onion', 'cleanup')
- # Cleanup the ephemeral onion service
- if self.service_id:
- try:
- self.c.remove_ephemeral_hidden_service(self.service_id)
- except:
- pass
- self.service_id = None
+ # Cleanup the ephemeral onion services, if we have any
+ try:
+ onions = self.c.list_ephemeral_hidden_services()
+ for onion in onions:
+ try:
+ common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
+ self.c.remove_ephemeral_hidden_service(onion)
+ except:
+ common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
+ pass
+ except:
+ pass
+ self.service_id = None
if stop_tor:
# Stop tor process
diff --git a/onionshare/settings.py b/onionshare/settings.py
index 94b5dde5..545915e8 100644
--- a/onionshare/settings.py
+++ b/onionshare/settings.py
@@ -58,11 +58,14 @@ class Settings(object):
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
+ 'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
'no_bridges': True,
'tor_bridges_use_obfs4': False,
+ 'tor_bridges_use_meek_lite_amazon': False,
+ 'tor_bridges_use_meek_lite_azure': False,
'tor_bridges_use_custom_bridges': '',
'save_private_key': False,
'private_key': '',
diff --git a/onionshare/web.py b/onionshare/web.py
index 3eef67c7..d16ca251 100644
--- a/onionshare/web.py
+++ b/onionshare/web.py
@@ -26,6 +26,7 @@ import queue
import socket
import sys
import tempfile
+import base64
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
@@ -58,7 +59,7 @@ zip_filename = None
zip_filesize = None
security_headers = [
- ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; img-src \'self\' data:;'),
+ ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
@@ -125,6 +126,12 @@ def add_request(request_type, path, data=None):
})
+# Load and base64 encode images to pass into templates
+favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode()
+logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode()
+folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode()
+file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode()
+
slug = None
@@ -206,7 +213,10 @@ def index(slug_candidate):
global stay_open, download_in_progress
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header, value in security_headers:
r.headers.set(header, value)
return r
@@ -215,6 +225,10 @@ def index(slug_candidate):
r = make_response(render_template_string(
open(common.get_resource_path('html/index.html')).read(),
+ favicon_b64=favicon_b64,
+ logo_b64=logo_b64,
+ folder_b64=folder_b64,
+ file_b64=file_b64,
slug=slug,
file_info=file_info,
filename=os.path.basename(zip_filename),
@@ -243,7 +257,10 @@ def download(slug_candidate):
global stay_open, download_in_progress, done
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header,value in security_headers:
r.headers.set(header, value)
return r
@@ -298,12 +315,14 @@ def download(slug_candidate):
percent = (1.0 * downloaded_bytes / zip_filesize) * 100
# only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304)
- if not gui_mode or common.get_platform() == 'Linux':
+ plat = common.get_platform()
+ if not gui_mode or plat == 'Linux' or plat == 'BSD':
sys.stdout.write(
"\r{0:s}, {1:.2f}% ".format(common.human_readable_filesize(downloaded_bytes), percent))
sys.stdout.flush()
add_request(REQUEST_PROGRESS, path, {'id': download_id, 'bytes': downloaded_bytes})
+ done = False
except:
# looks like the download was canceled
done = True
@@ -355,7 +374,10 @@ def page_not_found(e):
force_shutdown()
print(strings._('error_rate_limit'))
- r = make_response(render_template_string(open(common.get_resource_path('html/404.html')).read()), 404)
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/404.html')).read(),
+ favicon_b64=favicon_b64
+ ), 404)
for header, value in security_headers:
r.headers.set(header, value)
return r
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py
index 14c76617..24e627bb 100644
--- a/onionshare_gui/__init__.py
+++ b/onionshare_gui/__init__.py
@@ -35,8 +35,8 @@ class Application(QtWidgets.QApplication):
and the quick keyboard shortcut.
"""
def __init__(self):
- system = platform.system()
- if system == 'Linux':
+ system = common.get_platform()
+ if system == 'Linux' or system == 'BSD':
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
QtWidgets.QApplication.__init__(self, sys.argv)
self.installEventFilter(self)
diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py
index 60bd59ac..166f14a4 100644
--- a/onionshare_gui/downloads.py
+++ b/onionshare_gui/downloads.py
@@ -34,13 +34,15 @@ class Download(object):
# make a new progress bar
cssStyleData ="""
QProgressBar {
- border: 2px solid grey;
- border-radius: 5px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
+ font-size: 12px;
}
QProgressBar::chunk {
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.progress_bar = QtWidgets.QProgressBar()
diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py
index da03d24d..29bcc592 100644
--- a/onionshare_gui/file_selection.py
+++ b/onionshare_gui/file_selection.py
@@ -23,6 +23,50 @@ from .alert import Alert
from onionshare import strings, common
+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, parent, image=False):
+ self.parent = parent
+ super(DropHereLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+
+ if image:
+ self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png'))))
+ else:
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #999999;')
+
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.parent.drop_here_image.hide()
+ self.parent.drop_here_text.hide()
+ event.accept()
+
+
+class DropCountLabel(QtWidgets.QLabel):
+ """
+ While dragging files over the FileList, this counter displays the
+ number of files you're dragging.
+ """
+ def __init__(self, parent):
+ self.parent = parent
+ super(DropCountLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;')
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.hide()
+ event.accept()
+
+
class FileList(QtWidgets.QListWidget):
"""
The list of files and folders in the GUI.
@@ -35,63 +79,82 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
- self.setMinimumHeight(200)
+ self.setMinimumHeight(205)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
-
- 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, parent, image=False):
- self.parent = parent
- super(DropHereLabel, self).__init__(parent=parent)
- self.setAcceptDrops(True)
- self.setAlignment(QtCore.Qt.AlignCenter)
-
- if image:
- self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/drop_files.png'))))
- else:
- self.setText(strings._('gui_drag_and_drop', True))
- self.setStyleSheet('color: #999999;')
-
- self.hide()
-
- def dragEnterEvent(self, event):
- self.parent.drop_here_image.hide()
- self.parent.drop_here_text.hide()
- event.ignore()
-
self.drop_here_image = DropHereLabel(self, True)
self.drop_here_text = DropHereLabel(self, False)
-
- self.filenames = []
- self.update()
+ self.drop_count = DropCountLabel(self)
+ self.resizeEvent(None)
+ self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
def update(self):
"""
Update the GUI elements based on the current state.
"""
# file list should have a background image if empty
- if len(self.filenames) == 0:
+ if self.count() == 0:
self.drop_here_image.show()
self.drop_here_text.show()
else:
self.drop_here_image.hide()
self.drop_here_text.hide()
+ def server_started(self):
+ """
+ Update the GUI when the server starts, by hiding delete buttons.
+ """
+ self.setAcceptDrops(False)
+ self.setCurrentItem(None)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+ for index in range(self.count()):
+ self.item(index).item_button.hide()
+
+ def server_stopped(self):
+ """
+ Update the GUI when the server stops, by showing delete buttons.
+ """
+ self.setAcceptDrops(True)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ for index in range(self.count()):
+ self.item(index).item_button.show()
+
def resizeEvent(self, event):
"""
When the widget is resized, resize the drop files image and text.
"""
- self.drop_here_image.setGeometry(0, 0, self.width(), self.height())
- self.drop_here_text.setGeometry(0, 0, self.width(), self.height())
+ offset = 70
+ self.drop_here_image.setGeometry(0, 0, self.width(), self.height() - offset)
+ self.drop_here_text.setGeometry(0, offset, self.width(), self.height() - offset)
+
+ 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')
+ self.addItem(item)
+ self.takeItem(self.row(item))
+ self.update()
+
+ # Extend any filenames that were truncated to fit the window
+ # We use 200 as a rough guess at how wide the 'file size + delete button' widget is
+ # 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)
+ self.item(index).setText(elided)
+
def dragEnterEvent(self, event):
"""
dragEnterEvent for dragging files and directories into the widget.
"""
if event.mimeData().hasUrls:
+ self.setStyleSheet('FileList { border: 3px solid #538ad0; }')
+ count = len(event.mimeData().urls())
+ 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.show()
event.accept()
else:
event.ignore()
@@ -100,6 +163,8 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
+ self.setStyleSheet('FileList { border: none; }')
+ self.drop_count.hide()
event.accept()
self.update()
@@ -125,36 +190,84 @@ class FileList(QtWidgets.QListWidget):
self.add_file(filename)
else:
event.ignore()
+
+ self.setStyleSheet('border: none;')
+ self.drop_count.hide()
+
self.files_dropped.emit()
def add_file(self, filename):
"""
Add a file or directory to this widget.
"""
- if filename not in self.filenames:
+ filenames = []
+ for index in range(self.count()):
+ filenames.append(self.item(index).filename)
+
+ if filename not in filenames:
if not os.access(filename, os.R_OK):
Alert(strings._("not_a_readable_file", True).format(filename))
return
- self.filenames.append(filename)
- # Re-sort the list internally
- self.filenames.sort()
-
fileinfo = QtCore.QFileInfo(filename)
- basename = os.path.basename(filename.rstrip('/'))
ip = QtWidgets.QFileIconProvider()
icon = ip.icon(fileinfo)
if os.path.isfile(filename):
- size = common.human_readable_filesize(fileinfo.size())
+ size_bytes = fileinfo.size()
+ size_readable = common.human_readable_filesize(size_bytes)
else:
- size = common.human_readable_filesize(common.dir_size(filename))
- item_name = '{0:s} ({1:s})'.format(basename, size)
- item = QtWidgets.QListWidgetItem(item_name)
- item.setToolTip(size)
+ size_bytes = common.dir_size(filename)
+ size_readable = common.human_readable_filesize(size_bytes)
+ # Create a new item
+ item = QtWidgets.QListWidgetItem()
item.setIcon(icon)
+ item.size_bytes = size_bytes
+
+ # Item's filename attribute and size labels
+ item.filename = filename
+ item_size = QtWidgets.QLabel(size_readable)
+ item_size.setStyleSheet('QLabel { color: #666666; font-size: 11px; }')
+
+ 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())
+ item.setData(QtCore.Qt.DisplayRole, elided)
+
+ # Item's delete button
+ def delete_item():
+ itemrow = self.row(item)
+ self.takeItem(itemrow)
+ self.files_updated.emit()
+
+ item.item_button = QtWidgets.QPushButton()
+ item.item_button.setDefault(False)
+ item.item_button.setFlat(True)
+ item.item_button.setIcon( QtGui.QIcon(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 info widget, with a white background
+ item_info_layout = QtWidgets.QHBoxLayout()
+ item_info_layout.addWidget(item_size)
+ item_info_layout.addWidget(item.item_button)
+ item_info = QtWidgets.QWidget()
+ item_info.setObjectName('item-info')
+ item_info.setLayout(item_info_layout)
+
+ # Create the item's widget and layouts
+ item_hlayout = QtWidgets.QHBoxLayout()
+ item_hlayout.addStretch()
+ item_hlayout.addWidget(item_info)
+ widget = QtWidgets.QWidget()
+ widget.setLayout(item_hlayout)
+
+ item.setSizeHint(widget.sizeHint())
+
self.addItem(item)
+ self.setItemWidget(item, widget)
self.files_updated.emit()
@@ -168,21 +281,23 @@ class FileSelection(QtWidgets.QVBoxLayout):
super(FileSelection, self).__init__()
self.server_on = False
- # file list
+ # File list
self.file_list = FileList()
- self.file_list.currentItemChanged.connect(self.update)
+ self.file_list.itemSelectionChanged.connect(self.update)
self.file_list.files_dropped.connect(self.update)
+ self.file_list.files_updated.connect(self.update)
- # buttons
+ # Buttons
self.add_button = QtWidgets.QPushButton(strings._('gui_add', True))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True))
self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout()
+ button_layout.addStretch()
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
- # add the widgets
+ # Add the widgets
self.addWidget(self.file_list)
self.addLayout(button_layout)
@@ -192,21 +307,20 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Update the GUI elements based on the current state.
"""
- # all buttons should be disabled if the server is on
+ # All buttons should be hidden if the server is on
if self.server_on:
- self.add_button.setEnabled(False)
- self.delete_button.setEnabled(False)
+ self.add_button.hide()
+ self.delete_button.hide()
else:
- self.add_button.setEnabled(True)
+ self.add_button.show()
- # delete button should be disabled if item isn't selected
- current_item = self.file_list.currentItem()
- if not current_item:
- self.delete_button.setEnabled(False)
+ # Delete button should be hidden if item isn't selected
+ if len(self.file_list.selectedItems()) == 0:
+ self.delete_button.hide()
else:
- self.delete_button.setEnabled(True)
+ self.delete_button.show()
- # update the file list
+ # Update the file list
self.file_list.update()
def add(self):
@@ -218,6 +332,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
for filename in file_dialog.selectedFiles():
self.file_list.add_file(filename)
+ self.file_list.setCurrentItem(None)
self.update()
def delete(self):
@@ -227,9 +342,10 @@ class FileSelection(QtWidgets.QVBoxLayout):
selected = self.file_list.selectedItems()
for item in selected:
itemrow = self.file_list.row(item)
- self.file_list.filenames.pop(itemrow)
self.file_list.takeItem(itemrow)
self.file_list.files_updated.emit()
+
+ self.file_list.setCurrentItem(None)
self.update()
def server_started(self):
@@ -237,7 +353,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server starts.
"""
self.server_on = True
- self.file_list.setAcceptDrops(False)
+ self.file_list.server_started()
self.update()
def server_stopped(self):
@@ -245,14 +361,14 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server stops.
"""
self.server_on = False
- self.file_list.setAcceptDrops(True)
+ self.file_list.server_stopped()
self.update()
def get_num_files(self):
"""
Returns the total number of files and folders in the list.
"""
- return len(self.file_list.filenames)
+ return len(range(self.file_list.count()))
def setFocus(self):
"""
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index 582ebdb3..f38dd727 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -56,6 +56,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
+ self.setMinimumWidth(430)
# Load settings
self.config = config
@@ -72,20 +73,31 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_started.connect(self.start_server)
+ self.server_status.server_started.connect(self.update_server_status_indicator)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.stop_server)
+ self.server_status.server_stopped.connect(self.update_server_status_indicator)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.cancel_server)
+ self.server_status.server_canceled.connect(self.file_selection.server_stopped)
+ self.server_status.server_canceled.connect(self.update_primary_action)
self.start_server_finished.connect(self.clear_message)
self.start_server_finished.connect(self.server_status.start_server_finished)
+ self.start_server_finished.connect(self.update_server_status_indicator)
self.stop_server_finished.connect(self.server_status.stop_server_finished)
+ self.stop_server_finished.connect(self.update_server_status_indicator)
self.file_selection.file_list.files_updated.connect(self.server_status.update)
+ self.file_selection.file_list.files_updated.connect(self.update_primary_action)
self.server_status.url_copied.connect(self.copy_url)
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.starting_server_step2.connect(self.start_server_step2)
self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error)
+ self.server_status.button_clicked.connect(self.clear_message)
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
+ self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
self.filesize_warning.hide()
@@ -99,38 +111,95 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.vbar = self.downloads_container.verticalScrollBar()
self.downloads_container.hide() # downloads start out hidden
self.new_download = False
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
- # Status bar
- self.status_bar = QtWidgets.QStatusBar()
- self.status_bar.setSizeGripEnabled(False)
- self.status_bar.setStyleSheet(
- "QStatusBar::item { border: 0px; }")
- version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version()))
- version_label.setStyleSheet('color: #666666')
+ # Info label along top of screen
+ self.info_layout = QtWidgets.QHBoxLayout()
+ self.info_label = QtWidgets.QLabel()
+ self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_in_progress_downloads_count = QtWidgets.QLabel()
+ self.info_in_progress_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_completed_downloads_count = QtWidgets.QLabel()
+ self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.update_downloads_completed(self.downloads_in_progress)
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
+ self.info_layout.addWidget(self.info_label)
+ self.info_layout.addStretch()
+ self.info_layout.addWidget(self.info_in_progress_downloads_count)
+ self.info_layout.addWidget(self.info_completed_downloads_count)
+
+ self.info_widget = QtWidgets.QWidget()
+ self.info_widget.setLayout(self.info_layout)
+ self.info_widget.hide()
+
+ # Settings button on the status bar
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFlat(True)
+ self.settings_button.setFixedWidth(40)
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.clicked.connect(self.open_settings)
- self.status_bar.addPermanentWidget(version_label)
+
+ # Server status indicator on the status bar
+ self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
+ self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
+ self.server_status_image_started = QtGui.QImage(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('QLabel { font-style: italic; color: #666666; }')
+ 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)
+ self.server_status_indicator = QtWidgets.QWidget()
+ self.server_status_indicator.setLayout(server_status_indicator_layout)
+ self.update_server_status_indicator()
+
+ # Status bar
+ self.status_bar = QtWidgets.QStatusBar()
+ self.status_bar.setSizeGripEnabled(False)
+ statusBar_cssStyleData ="""
+ QStatusBar {
+ font-style: italic;
+ color: #666666;
+ }
+
+ QStatusBar::item {
+ border: 0px;
+ }"""
+
+ self.status_bar.setStyleSheet(statusBar_cssStyleData)
+ self.status_bar.addPermanentWidget(self.server_status_indicator)
self.status_bar.addPermanentWidget(self.settings_button)
self.setStatusBar(self.status_bar)
# Status bar, zip progress bar
self._zip_progress_bar = None
-
- # Persistent URL notification
- self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True))
- self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
- self.persistent_url_label.hide()
+ # Status bar, sharing messages
+ self.server_share_status_label = QtWidgets.QLabel('')
+ self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }')
+ self.status_bar.insertWidget(0, self.server_share_status_label)
+
+ # Primary action layout
+ primary_action_layout = QtWidgets.QVBoxLayout()
+ primary_action_layout.addWidget(self.server_status)
+ primary_action_layout.addWidget(self.filesize_warning)
+ primary_action_layout.addWidget(self.downloads_container)
+ self.primary_action = QtWidgets.QWidget()
+ self.primary_action.setLayout(primary_action_layout)
+ self.primary_action.hide()
+ self.update_primary_action()
# Main layout
self.layout = QtWidgets.QVBoxLayout()
+ self.layout.addWidget(self.info_widget)
self.layout.addLayout(self.file_selection)
- self.layout.addLayout(self.server_status)
- self.layout.addWidget(self.filesize_warning)
- self.layout.addWidget(self.persistent_url_label)
- self.layout.addWidget(self.downloads_container)
+ self.layout.addWidget(self.primary_action)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(self.layout)
self.setCentralWidget(central_widget)
@@ -158,6 +227,46 @@ class OnionShareGui(QtWidgets.QMainWindow):
# After connecting to Tor, check for updates
self.check_for_updates()
+ def update_primary_action(self):
+ # Show or hide primary action layout
+ file_count = self.file_selection.file_list.count()
+ if file_count > 0:
+ self.primary_action.show()
+ self.info_widget.show()
+
+ # Update the file count in the info label
+ total_size_bytes = 0
+ for index in range(self.file_selection.file_list.count()):
+ item = self.file_selection.file_list.item(index)
+ total_size_bytes += item.size_bytes
+ total_size_readable = common.human_readable_filesize(total_size_bytes)
+
+ if file_count > 1:
+ self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
+ else:
+ self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
+
+ else:
+ self.primary_action.hide()
+ self.info_widget.hide()
+
+ # Resize window
+ self.adjustSize()
+
+ def update_server_status_indicator(self):
+ common.log('OnionShareGui', 'update_server_status_indicator')
+
+ # Set the status image
+ if self.server_status.status == self.server_status.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_stopped', True))
+ elif self.server_status.status == self.server_status.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_working', True))
+ elif self.server_status.status == self.server_status.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_started', True))
+
def _initSystemTray(self):
system = common.get_platform()
@@ -238,11 +347,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.server_status.file_selection.get_num_files() > 0:
self.server_status.server_button.setEnabled(True)
self.status_bar.clearMessage()
+ # If we switched off the shutdown timeout setting, ensure the widget is hidden.
+ if not self.settings.get('shutdown_timeout'):
+ self.server_status.shutdown_timeout_container.hide()
d = SettingsDialog(self.onion, self.qtapp, self.config)
d.settings_saved.connect(reload_settings)
d.exec_()
+ # When settings close, refresh the server status UI
+ self.server_status.update()
+
def start_server(self):
"""
Start the onionshare server. This uses multiple threads to start the Tor onion
@@ -257,7 +372,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Hide and reset the downloads if we have previously shared
self.downloads_container.hide()
self.downloads.reset_downloads()
+ self.reset_info_counters()
self.status_bar.clearMessage()
+ self.server_share_status_label.setText('')
# Reset web counters
web.download_count = 0
@@ -284,9 +401,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
- t = threading.Thread(target=start_onion_service, kwargs={'self': self})
- t.daemon = True
- t.start()
+ common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
+ self.t = OnionThread(function=start_onion_service, kwargs={'self': self})
+ self.t.daemon = True
+ self.t.start()
def start_server_step2(self):
"""
@@ -296,8 +414,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
# add progress bar to the status bar, indicating the crunching of files.
self._zip_progress_bar = ZipProgressBar(0)
- self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(
- self.file_selection.file_list.filenames)
+ self.filenames = []
+ 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 = OnionShareGui._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar)
# prepare the files for sending in a new thread
@@ -307,7 +428,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self._zip_progress_bar != None:
self._zip_progress_bar.update_processed_size_signal.emit(x)
try:
- web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size)
+ web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(web.zip_filename)
self.starting_server_step3.emit()
@@ -317,7 +438,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.starting_server_error.emit(e.strerror)
return
- #self.status_bar.showMessage(strings._('gui_starting_server2', True))
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
t.daemon = True
t.start()
@@ -339,7 +459,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
- if self.server_status.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.timeout = now.secsTo(self.server_status.timeout)
@@ -352,9 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.stop_server()
self.start_server_error(strings._('gui_server_started_after_timeout'))
- if self.settings.get('save_private_key'):
- self.persistent_url_label.show()
-
def start_server_error(self, error):
"""
If there's an error when trying to start the onion service
@@ -370,6 +487,14 @@ class OnionShareGui(QtWidgets.QMainWindow):
self._zip_progress_bar = None
self.status_bar.clearMessage()
+ def cancel_server(self):
+ """
+ Cancel the server while it is preparing to start
+ """
+ if self.t:
+ self.t.quit()
+ self.stop_server()
+
def stop_server(self):
"""
Stop the onionshare server.
@@ -386,10 +511,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Remove ephemeral service, but don't disconnect from Tor
self.onion.cleanup(stop_tor=False)
self.filesize_warning.hide()
- self.persistent_url_label.hide()
- self.stop_server_finished.emit()
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
+ self.update_downloads_in_progress(0)
+ self.file_selection.file_list.adjustSize()
self.set_server_active(False)
+ self.stop_server_finished.emit()
def check_for_updates(self):
"""
@@ -455,6 +583,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.downloads_container.show() # show the downloads layout
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
self.new_download = True
+ self.downloads_in_progress += 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@@ -469,16 +599,30 @@ class OnionShareGui(QtWidgets.QMainWindow):
if event["data"]["bytes"] == web.zip_filesize:
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
+ # Update the total 'completed downloads' info
+ self.downloads_completed += 1
+ self.update_downloads_completed(self.downloads_completed)
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
# close on finish?
if not web.get_stay_open():
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('closing_automatically', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('closing_automatically', True))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.downloads.cancel_download(event["data"]["id"])
+ self.downloads_in_progress = 0
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
elif event["type"] == web.REQUEST_CANCELED:
self.downloads.cancel_download(event["data"]["id"])
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
@@ -487,30 +631,37 @@ class OnionShareGui(QtWidgets.QMainWindow):
# If the auto-shutdown timer has stopped, stop the server
if self.server_status.status == self.server_status.STATUS_STARTED:
- if self.app.shutdown_timer and self.server_status.timer_enabled:
+ if self.app.shutdown_timer and self.settings.get('shutdown_timeout'):
if self.timeout > 0:
+ now = QtCore.QDateTime.currentDateTime()
+ seconds_remaining = now.secsTo(self.server_status.timeout)
+ self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
if not self.app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.download_count == 0 or web.done:
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('close_on_timeout', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('close_on_timeout', True))
# A download is probably still running - hold off on stopping the share
else:
- self.status_bar.showMessage(strings._('timeout_download_still_running', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_url')
- self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_hidservauth')
- self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
def clear_message(self):
"""
@@ -522,22 +673,52 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Disable the Settings button while an OnionShare server is active.
"""
- self.settings_button.setEnabled(not active)
if active:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings_inactive.png')) )
+ self.settings_button.hide()
else:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
+ self.settings_button.show()
# Disable settings menu action when server is active
self.settingsAction.setEnabled(not active)
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.update_downloads_completed(0)
+ self.update_downloads_in_progress(0)
+
+ def update_downloads_completed(self, count):
+ """
+ Update the 'Downloads completed' info widget.
+ """
+ if count == 0:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png')
+ else:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png')
+ self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_completed_downloads_image, count))
+ self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
+
+ def update_downloads_in_progress(self, count):
+ """
+ Update the 'Downloads in progress' info widget.
+ """
+ if count == 0:
+ self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress_none.png')
+ else:
+ self.info_in_progress_downloads_image = common.get_resource_path('images/download_in_progress.png')
+ self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(self.info_in_progress_downloads_image, count))
+ self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
+
def closeEvent(self, e):
common.log('OnionShareGui', 'closeEvent')
try:
if self.server_status.status != self.server_status.STATUS_STOPPED:
+ common.log('OnionShareGui', 'closeEvent, opening warning dialog')
dialog = QtWidgets.QMessageBox()
- dialog.setWindowTitle("OnionShare")
+ dialog.setWindowTitle(strings._('gui_quit_title', True))
dialog.setText(strings._('gui_quit_warning', True))
+ dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
dialog.setDefaultButton(dont_quit_button)
@@ -566,14 +747,15 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setFormat(strings._('zip_progress_bar_format'))
cssStyleData ="""
QProgressBar {
- background-color: rgba(255, 255, 255, 0.0) !important;
- border: 0px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.setStyleSheet(cssStyleData)
@@ -607,3 +789,26 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setValue(100)
else:
self.setValue(0)
+
+
+class OnionThread(QtCore.QThread):
+ """
+ A QThread for starting our Onion Service.
+ By using QThread rather than threading.Thread, we are able
+ to call quit() or terminate() on the startup if the user
+ decided to cancel (in which case do not proceed with obtaining
+ the Onion address and starting the web server).
+ """
+ def __init__(self, function, kwargs=None):
+ super(OnionThread, self).__init__()
+ common.log('OnionThread', '__init__')
+ self.function = function
+ if not kwargs:
+ self.kwargs = {}
+ else:
+ self.kwargs = kwargs
+
+ def run(self):
+ common.log('OnionThread', 'run')
+
+ self.function(**self.kwargs)
diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py
index 442ae440..03540415 100644
--- a/onionshare_gui/server_status.py
+++ b/onionshare_gui/server_status.py
@@ -23,12 +23,14 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, settings
-class ServerStatus(QtWidgets.QVBoxLayout):
+class ServerStatus(QtWidgets.QWidget):
"""
The server status chunk of the GUI.
"""
server_started = QtCore.pyqtSignal()
server_stopped = QtCore.pyqtSignal()
+ server_canceled = QtCore.pyqtSignal()
+ button_clicked = QtCore.pyqtSignal()
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
@@ -47,100 +49,103 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings = settings
- # Helper boolean as this is used in a few places
- self.timer_enabled = False
# Shutdown timeout layout
- self.server_shutdown_timeout_checkbox = QtWidgets.QCheckBox()
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout_checkbox.toggled.connect(self.shutdown_timeout_toggled)
- self.server_shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_choice", True))
- self.server_shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
- self.server_shutdown_timeout = QtWidgets.QDateTimeEdit()
+ self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
+ self.shutdown_timeout = QtWidgets.QDateTimeEdit()
# Set proposed timeout to be 5 minutes into the future
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
+ self.shutdown_timeout.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 2 min from now
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
- self.server_shutdown_timeout.setCurrentSectionIndex(4)
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
- shutdown_timeout_layout_group = QtWidgets.QHBoxLayout()
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_checkbox)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_label)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout)
- # server layout
- self.status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
- self.status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
- self.status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
- self.status_image_label = QtWidgets.QLabel()
- self.status_image_label.setFixedWidth(30)
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
+ shutdown_timeout_layout = QtWidgets.QHBoxLayout()
+ shutdown_timeout_layout.addWidget(self.shutdown_timeout_label)
+ shutdown_timeout_layout.addWidget(self.shutdown_timeout)
+
+ # Shutdown timeout container, so it can all be hidden and shown as a group
+ shutdown_timeout_container_layout = QtWidgets.QVBoxLayout()
+ shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout)
+ self.shutdown_timeout_container = QtWidgets.QWidget()
+ self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout)
+ self.shutdown_timeout_container.hide()
+
+
+ # Server layout
self.server_button = QtWidgets.QPushButton()
self.server_button.clicked.connect(self.server_button_clicked)
- server_layout = QtWidgets.QHBoxLayout()
- server_layout.addWidget(self.status_image_label)
- server_layout.addWidget(self.server_button)
- # url layout
+ # URL layout
url_font = QtGui.QFont()
- self.url_label = QtWidgets.QLabel()
- self.url_label.setFont(url_font)
- self.url_label.setWordWrap(False)
- self.url_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True))
+ self.url_description.setWordWrap(True)
+ self.url_description.setMinimumHeight(50)
+ self.url = QtWidgets.QLabel()
+ self.url.setFont(url_font)
+ self.url.setWordWrap(True)
+ self.url.setMinimumHeight(60)
+ self.url.setMinimumSize(self.url.sizeHint())
+ self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }')
+
+ url_buttons_style = 'QPushButton { color: #3f7fcf; }'
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
+ self.copy_url_button.setFlat(True)
+ self.copy_url_button.setStyleSheet(url_buttons_style)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
+ self.copy_hidservauth_button.setFlat(True)
+ self.copy_hidservauth_button.setStyleSheet(url_buttons_style)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
- url_layout = QtWidgets.QHBoxLayout()
- url_layout.addWidget(self.url_label)
- url_layout.addWidget(self.copy_url_button)
- url_layout.addWidget(self.copy_hidservauth_button)
-
- # add the widgets
- self.addLayout(shutdown_timeout_layout_group)
- self.addLayout(server_layout)
- self.addLayout(url_layout)
+ url_buttons_layout = QtWidgets.QHBoxLayout()
+ url_buttons_layout.addWidget(self.copy_url_button)
+ url_buttons_layout.addWidget(self.copy_hidservauth_button)
+ url_buttons_layout.addStretch()
+
+ url_layout = QtWidgets.QVBoxLayout()
+ url_layout.addWidget(self.url_description)
+ url_layout.addWidget(self.url)
+ url_layout.addLayout(url_buttons_layout)
+
+ # Add the widgets
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.server_button)
+ layout.addLayout(url_layout)
+ layout.addWidget(self.shutdown_timeout_container)
+ self.setLayout(layout)
self.update()
- def shutdown_timeout_toggled(self, checked):
- """
- Shutdown timer option was toggled. If checked, show the timer settings.
- """
- if checked:
- self.timer_enabled = True
- # Hide the checkbox, show the options
- self.server_shutdown_timeout_label.show()
- # Reset the default timer to 5 minutes into the future after toggling the option on
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.show()
- else:
- self.timer_enabled = False
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
-
def shutdown_timeout_reset(self):
"""
Reset the timeout in the UI after stopping a share
"""
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
def update(self):
"""
Update the GUI elements based on the current state.
"""
- # set the status image
- if self.status == self.STATUS_STOPPED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_stopped))
- elif self.status == self.STATUS_WORKING:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_working))
- elif self.status == self.STATUS_STARTED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_started))
-
- # set the URL fields
+ # Set the URL fields
if self.status == self.STATUS_STARTED:
- self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
- self.url_label.show()
+ self.url_description.show()
+
+ info_image = common.get_resource_path('images/info.png')
+ self.url_description.setText(strings._('gui_url_description', True).format(info_image))
+ # Show a Tool Tip explaining the lifecycle of this URL
+ if self.settings.get('save_private_key'):
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
+ else:
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
+
+ self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
+ self.url.show()
+
self.copy_url_button.show()
if self.settings.get('save_private_key'):
@@ -148,53 +153,63 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings.set('slug', self.web.slug)
self.settings.save()
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+
if self.app.stealth:
self.copy_hidservauth_button.show()
else:
self.copy_hidservauth_button.hide()
-
- # resize parent widget
- p = self.parentWidget()
- p.resize(p.sizeHint())
else:
- self.url_label.hide()
+ self.url_description.hide()
+ self.url.hide()
self.copy_url_button.hide()
self.copy_hidservauth_button.hide()
- # button
+ # Button
+ button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
+ button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }'
+ button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
if self.file_selection.get_num_files() == 0:
- self.server_button.setEnabled(False)
- self.server_button.setText(strings._('gui_start_server', True))
+ self.server_button.hide()
else:
+ self.server_button.show()
+
if self.status == self.STATUS_STOPPED:
+ self.server_button.setStyleSheet(button_stopped_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_start_server', True))
- self.server_shutdown_timeout.setEnabled(True)
- self.server_shutdown_timeout_checkbox.setEnabled(True)
+ self.server_button.setToolTip('')
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
+ self.server_button.setStyleSheet(button_started_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_stop_server', True))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+ self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
elif self.status == self.STATUS_WORKING:
- self.server_button.setEnabled(False)
+ self.server_button.setStyleSheet(button_working_style)
+ self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
else:
+ self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
def server_button_clicked(self):
"""
Toggle starting or stopping the server.
"""
if self.status == self.STATUS_STOPPED:
- if self.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
- self.timeout = self.server_shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
+ self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
@@ -204,6 +219,9 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.start_server()
elif self.status == self.STATUS_STARTED:
self.stop_server()
+ elif self.status == self.STATUS_WORKING:
+ self.cancel_server()
+ self.button_clicked.emit()
def start_server(self):
"""
@@ -230,6 +248,16 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.update()
self.server_stopped.emit()
+ def cancel_server(self):
+ """
+ Cancel the server.
+ """
+ common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
+ self.status = self.STATUS_WORKING
+ self.shutdown_timeout_reset()
+ self.update()
+ self.server_canceled.emit()
+
def stop_server_finished(self):
"""
The server has finished stopping.
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index 18372a47..5666400c 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -60,6 +60,11 @@ class SettingsDialog(QtWidgets.QDialog):
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True))
+ # Whether or not to use a shutdown timer
+ self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
+
# Whether or not to save the Onion private key for reuse
self.save_private_key_checkbox = QtWidgets.QCheckBox()
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
@@ -69,6 +74,7 @@ class SettingsDialog(QtWidgets.QDialog):
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(self.systray_notifications_checkbox)
+ sharing_group_layout.addWidget(self.shutdown_timeout_checkbox)
sharing_group_layout.addWidget(self.save_private_key_checkbox)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
sharing_group.setLayout(sharing_group_layout)
@@ -80,12 +86,14 @@ class SettingsDialog(QtWidgets.QDialog):
stealth_details.setWordWrap(True)
stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
stealth_details.setOpenExternalLinks(True)
+ stealth_details.setMinimumSize(stealth_details.sizeHint())
self.stealth_checkbox = QtWidgets.QCheckBox()
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
hidservauth_details.setWordWrap(True)
+ hidservauth_details.setMinimumSize(hidservauth_details.sizeHint())
hidservauth_details.hide()
self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
@@ -156,6 +164,26 @@ class SettingsDialog(QtWidgets.QDialog):
self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option', True))
self.tor_bridges_use_obfs4_radio.toggled.connect(self.tor_bridges_use_obfs4_radio_toggled)
+ # meek_lite-amazon option radio
+ # if the obfs4proxy binary is missing, we can't use meek_lite-amazon transports
+ (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
+ if not os.path.isfile(self.obfs4proxy_file_path):
+ self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy', True))
+ self.tor_bridges_use_meek_lite_amazon_radio.setEnabled(False)
+ else:
+ self.tor_bridges_use_meek_lite_amazon_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_amazon_radio_option', True))
+ self.tor_bridges_use_meek_lite_amazon_radio.toggled.connect(self.tor_bridges_use_meek_lite_amazon_radio_toggled)
+
+ # meek_lite-azure option radio
+ # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports
+ (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = common.get_tor_paths()
+ if not os.path.isfile(self.obfs4proxy_file_path):
+ self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True))
+ self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False)
+ else:
+ self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option', True))
+ self.tor_bridges_use_meek_lite_azure_radio.toggled.connect(self.tor_bridges_use_meek_lite_azure_radio_toggled)
+
# Custom bridges radio and textbox
self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_custom_radio_option', True))
self.tor_bridges_use_custom_radio.toggled.connect(self.tor_bridges_use_custom_radio_toggled)
@@ -179,6 +207,8 @@ class SettingsDialog(QtWidgets.QDialog):
bridges_layout = QtWidgets.QVBoxLayout()
bridges_layout.addWidget(self.tor_bridges_no_bridges_radio)
bridges_layout.addWidget(self.tor_bridges_use_obfs4_radio)
+ bridges_layout.addWidget(self.tor_bridges_use_meek_lite_amazon_radio)
+ bridges_layout.addWidget(self.tor_bridges_use_meek_lite_azure_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_radio)
bridges_layout.addWidget(self.tor_bridges_use_custom_textbox_options)
@@ -295,9 +325,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.save_button.clicked.connect(self.save_clicked)
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
self.cancel_button.clicked.connect(self.cancel_clicked)
+ version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version()))
+ version_label.setStyleSheet('color: #666666')
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
self.help_button.clicked.connect(self.help_clicked)
buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addWidget(version_label)
buttons_layout.addWidget(self.help_button)
buttons_layout.addStretch()
buttons_layout.addWidget(self.save_button)
@@ -349,6 +382,12 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ shutdown_timeout = self.old_settings.get('shutdown_timeout')
+ if shutdown_timeout:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
save_private_key = self.old_settings.get('save_private_key')
if save_private_key:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
@@ -401,10 +440,15 @@ class SettingsDialog(QtWidgets.QDialog):
if self.old_settings.get('no_bridges'):
self.tor_bridges_no_bridges_radio.setChecked(True)
self.tor_bridges_use_obfs4_radio.setChecked(False)
+ self.tor_bridges_use_meek_lite_amazon_radio.setChecked(False)
+ self.tor_bridges_use_meek_lite_azure_radio.setChecked(False)
self.tor_bridges_use_custom_radio.setChecked(False)
else:
self.tor_bridges_no_bridges_radio.setChecked(False)
self.tor_bridges_use_obfs4_radio.setChecked(self.old_settings.get('tor_bridges_use_obfs4'))
+ self.tor_bridges_use_meek_lite_amazon_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_amazon'))
+ self.tor_bridges_use_meek_lite_azure_radio.setChecked(self.old_settings.get('tor_bridges_use_meek_lite_azure'))
+
if self.old_settings.get('tor_bridges_use_custom_bridges'):
self.tor_bridges_use_custom_radio.setChecked(True)
# Remove the 'Bridge' lines at the start of each bridge.
@@ -441,6 +485,20 @@ class SettingsDialog(QtWidgets.QDialog):
if checked:
self.tor_bridges_use_custom_textbox_options.hide()
+ def tor_bridges_use_meek_lite_amazon_radio_toggled(self, checked):
+ """
+ meek_lite-amazon bridges option was toggled. If checked, disable custom bridge options.
+ """
+ if checked:
+ self.tor_bridges_use_custom_textbox_options.hide()
+
+ def tor_bridges_use_meek_lite_azure_radio_toggled(self, checked):
+ """
+ meek_lite_azure bridges option was toggled. If checked, disable custom bridge options.
+ """
+ if checked:
+ self.tor_bridges_use_custom_textbox_options.hide()
+
def tor_bridges_use_custom_radio_toggled(self, checked):
"""
Custom bridges option was toggled. If checked, show custom bridge options.
@@ -557,31 +615,43 @@ class SettingsDialog(QtWidgets.QDialog):
self._disable_buttons()
self.qtapp.processEvents()
+ def update_timestamp():
+ # Update the last checked label
+ settings = Settings(self.config)
+ settings.load()
+ autoupdate_timestamp = settings.get('autoupdate_timestamp')
+ self._update_autoupdate_timestamp(autoupdate_timestamp)
+
+ def close_forced_update_thread():
+ forced_update_thread.quit()
+ # Enable buttons
+ self._enable_buttons()
+ # Update timestamp
+ update_timestamp()
+
# Check for updates
def update_available(update_url, installed_version, latest_version):
Alert(strings._("update_available", True).format(update_url, installed_version, latest_version))
+ close_forced_update_thread()
+
def update_not_available():
Alert(strings._('update_not_available', True))
+ close_forced_update_thread()
- u = UpdateChecker(self.onion)
- u.update_available.connect(update_available)
- u.update_not_available.connect(update_not_available)
-
- try:
- u.check(force=True)
- except UpdateCheckerCheckError:
+ def update_error():
Alert(strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning)
- except UpdateCheckerInvalidLatestVersion as e:
- Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
+ close_forced_update_thread()
- # Enable buttons
- self._enable_buttons()
+ def update_invalid_version():
+ Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning)
+ close_forced_update_thread()
- # Update the last checked label
- settings = Settings(self.config)
- settings.load()
- autoupdate_timestamp = settings.get('autoupdate_timestamp')
- self._update_autoupdate_timestamp(autoupdate_timestamp)
+ forced_update_thread = UpdateThread(self.onion, self.config, force=True)
+ forced_update_thread.update_available.connect(update_available)
+ forced_update_thread.update_not_available.connect(update_not_available)
+ forced_update_thread.update_error.connect(update_error)
+ forced_update_thread.update_invalid_version.connect(update_invalid_version)
+ forced_update_thread.start()
def save_clicked(self):
"""
@@ -590,56 +660,58 @@ class SettingsDialog(QtWidgets.QDialog):
common.log('SettingsDialog', 'save_clicked')
settings = self.settings_from_fields()
- settings.save()
-
- # If Tor isn't connected, or if Tor settings have changed, Reinitialize
- # the Onion object
- reboot_onion = False
- if self.onion.is_authenticated():
- common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
- def changed(s1, s2, keys):
- """
- Compare the Settings objects s1 and s2 and return true if any values
- have changed for the given keys.
- """
- for key in keys:
- if s1.get(key) != s2.get(key):
- return True
- return False
-
- if changed(settings, self.old_settings, [
- 'connection_type', 'control_port_address',
- 'control_port_port', 'socks_address', 'socks_port',
- 'socket_file_path', 'auth_type', 'auth_password',
- 'no_bridges', 'tor_bridges_use_obfs4',
- 'tor_bridges_use_custom_bridges']):
+ if settings:
+ settings.save()
+
+ # If Tor isn't connected, or if Tor settings have changed, Reinitialize
+ # the Onion object
+ reboot_onion = False
+ if self.onion.is_authenticated():
+ common.log('SettingsDialog', 'save_clicked', 'Connected to Tor')
+ def changed(s1, s2, keys):
+ """
+ Compare the Settings objects s1 and s2 and return true if any values
+ have changed for the given keys.
+ """
+ for key in keys:
+ if s1.get(key) != s2.get(key):
+ return True
+ return False
+
+ if changed(settings, self.old_settings, [
+ 'connection_type', 'control_port_address',
+ 'control_port_port', 'socks_address', 'socks_port',
+ 'socket_file_path', 'auth_type', 'auth_password',
+ 'no_bridges', 'tor_bridges_use_obfs4',
+ 'tor_bridges_use_meek_lite_amazon', 'tor_bridges_use_meek_lite_azure',
+ 'tor_bridges_use_custom_bridges']):
+
+ reboot_onion = True
+ else:
+ common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
+ # Tor isn't connected, so try connecting
reboot_onion = True
- else:
- common.log('SettingsDialog', 'save_clicked', 'Not connected to Tor')
- # Tor isn't connected, so try connecting
- reboot_onion = True
+ # Do we need to reinitialize Tor?
+ if reboot_onion:
+ # Reinitialize the Onion object
+ common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
+ self.onion.cleanup()
- # Do we need to reinitialize Tor?
- if reboot_onion:
- # Reinitialize the Onion object
- common.log('SettingsDialog', 'save_clicked', 'rebooting the Onion')
- self.onion.cleanup()
+ tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
+ tor_con.start()
- tor_con = TorConnectionDialog(self.qtapp, settings, self.onion)
- tor_con.start()
+ common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
- common.log('SettingsDialog', 'save_clicked', 'Onion done rebooting, connected to Tor: {}'.format(self.onion.connected_to_tor))
+ if self.onion.is_authenticated() and not tor_con.wasCanceled():
+ self.settings_saved.emit()
+ self.close()
- if self.onion.is_authenticated() and not tor_con.wasCanceled():
+ else:
self.settings_saved.emit()
self.close()
- else:
- self.settings_saved.emit()
- self.close()
-
def cancel_clicked(self):
"""
Cancel button clicked.
@@ -669,6 +741,7 @@ class SettingsDialog(QtWidgets.QDialog):
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked())
+ settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
if self.save_private_key_checkbox.isChecked():
settings.set('save_private_key', True)
settings.set('private_key', self.old_settings.get('private_key'))
@@ -717,14 +790,33 @@ class SettingsDialog(QtWidgets.QDialog):
if self.tor_bridges_no_bridges_radio.isChecked():
settings.set('no_bridges', True)
settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_obfs4_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', True)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+ settings.set('tor_bridges_use_custom_bridges', '')
+ if self.tor_bridges_use_meek_lite_amazon_radio.isChecked():
+ settings.set('no_bridges', False)
+ settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', True)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+ settings.set('tor_bridges_use_custom_bridges', '')
+ if self.tor_bridges_use_meek_lite_azure_radio.isChecked():
+ settings.set('no_bridges', False)
+ settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', True)
settings.set('tor_bridges_use_custom_bridges', '')
if self.tor_bridges_use_custom_radio.isChecked():
settings.set('no_bridges', False)
settings.set('tor_bridges_use_obfs4', False)
+ settings.set('tor_bridges_use_meek_lite_amazon', False)
+ settings.set('tor_bridges_use_meek_lite_azure', False)
+
# Insert a 'Bridge' line at the start of each bridge.
# This makes it easier to copy/paste a set of bridges
# provided from https://bridges.torproject.org
@@ -734,8 +826,12 @@ class SettingsDialog(QtWidgets.QDialog):
for bridge in bridges:
if bridge != '':
# Check the syntax of the custom bridge to make sure it looks legitimate
- pattern = re.compile("[0-9.]+:[0-9]+\s[A-Z0-9]+$")
- if pattern.match(bridge):
+ ipv4_pattern = re.compile("(obfs4\s+)?(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):([0-9]+)(\s+)([A-Z0-9]+)(.+)$")
+ ipv6_pattern = re.compile("(obfs4\s+)?\[(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]:[0-9]+\s+[A-Z0-9]+(.+)$")
+ meek_lite_pattern = re.compile("(meek_lite)(\s)+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)(\s)+([0-9A-Z]+)(\s)+url=(.+)(\s)+front=(.+)")
+ if ipv4_pattern.match(bridge) or \
+ ipv6_pattern.match(bridge) or \
+ meek_lite_pattern.match(bridge):
new_bridges.append(''.join(['Bridge ', bridge, '\n']))
bridges_valid = True
if bridges_valid:
@@ -744,6 +840,7 @@ class SettingsDialog(QtWidgets.QDialog):
else:
Alert(strings._('gui_settings_tor_bridges_invalid', True))
settings.set('no_bridges', True)
+ return False
return settings
diff --git a/onionshare_gui/update_checker.py b/onionshare_gui/update_checker.py
index ca2eb48a..8b4884a2 100644
--- a/onionshare_gui/update_checker.py
+++ b/onionshare_gui/update_checker.py
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5 import QtCore
import datetime, time, socket, re, platform
+from distutils.version import LooseVersion as Version
from onionshare import socks
from onionshare.settings import Settings
@@ -51,6 +52,8 @@ class UpdateChecker(QtCore.QObject):
"""
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
+ update_error = QtCore.pyqtSignal()
+ update_invalid_version = QtCore.pyqtSignal()
def __init__(self, onion, config=False):
super(UpdateChecker, self).__init__()
@@ -96,7 +99,10 @@ class UpdateChecker(QtCore.QObject):
if force:
path += '?force=1'
- onion_domain = 'elx57ue5uyfplgva.onion'
+ if Version(self.onion.tor_version) >= Version('0.3.2.9'):
+ onion_domain = 'lldan5gahapx5k7iafb3s4ikijc4ni7gx5iywdflkba5y2ezyg6sjgyd.onion'
+ else:
+ onion_domain = 'elx57ue5uyfplgva.onion'
common.log('UpdateChecker', 'check', 'loading http://{}{}'.format(onion_domain, path))
@@ -120,12 +126,14 @@ class UpdateChecker(QtCore.QObject):
except Exception as e:
common.log('UpdateChecker', 'check', '{}'.format(e))
+ self.update_error.emit()
raise UpdateCheckerCheckError
# Validate that latest_version looks like a version string
# This regex is: 1-3 dot-separated numeric components
version_re = r"^(\d+\.)?(\d+\.)?(\d+)$"
if not re.match(version_re, latest_version):
+ self.update_invalid_version.emit()
raise UpdateCheckerInvalidLatestVersion(latest_version)
# Update the last checked timestamp (dropping the seconds and milliseconds)
@@ -148,12 +156,15 @@ class UpdateChecker(QtCore.QObject):
class UpdateThread(QtCore.QThread):
update_available = QtCore.pyqtSignal(str, str, str)
update_not_available = QtCore.pyqtSignal()
+ update_error = QtCore.pyqtSignal()
+ update_invalid_version = QtCore.pyqtSignal()
- def __init__(self, onion, config=False):
+ def __init__(self, onion, config=False, force=False):
super(UpdateThread, self).__init__()
common.log('UpdateThread', '__init__')
self.onion = onion
self.config = config
+ self.force = force
def run(self):
common.log('UpdateThread', 'run')
@@ -161,9 +172,11 @@ class UpdateThread(QtCore.QThread):
u = UpdateChecker(self.onion, self.config)
u.update_available.connect(self._update_available)
u.update_not_available.connect(self._update_not_available)
+ u.update_error.connect(self._update_error)
+ u.update_invalid_version.connect(self._update_invalid_version)
try:
- u.check(config=self.config)
+ u.check(config=self.config,force=self.force)
except Exception as e:
# If update check fails, silently ignore
common.log('UpdateThread', 'run', '{}'.format(e))
@@ -178,3 +191,13 @@ class UpdateThread(QtCore.QThread):
common.log('UpdateThread', '_update_not_available')
self.active = False
self.update_not_available.emit()
+
+ def _update_error(self):
+ common.log('UpdateThread', '_update_error')
+ self.active = False
+ self.update_error.emit()
+
+ def _update_invalid_version(self):
+ common.log('UpdateThread', '_update_invalid_version')
+ self.active = False
+ self.update_invalid_version.emit()
diff --git a/screenshots/appdata-client.png b/screenshots/appdata-client.png
index 0ddcfc46..8fe99b11 100644
--- a/screenshots/appdata-client.png
+++ b/screenshots/appdata-client.png
Binary files differ
diff --git a/screenshots/appdata-server.png b/screenshots/appdata-server.png
index 864d21e8..abdb2198 100644
--- a/screenshots/appdata-server.png
+++ b/screenshots/appdata-server.png
Binary files differ
diff --git a/screenshots/client.png b/screenshots/client.png
index ba7098f4..fdf69fd0 100644
--- a/screenshots/client.png
+++ b/screenshots/client.png
Binary files differ
diff --git a/screenshots/server.png b/screenshots/server.png
index 0a90ea4a..8bdf2971 100644
--- a/screenshots/server.png
+++ b/screenshots/server.png
Binary files differ
diff --git a/setup.py b/setup.py
index dcbe42fb..23e1ea17 100644
--- a/setup.py
+++ b/setup.py
@@ -45,6 +45,17 @@ author_email = 'micah@micahflee.com'
url = 'https://github.com/micahflee/onionshare'
license = 'GPL v3'
keywords = 'onion, share, onionshare, tor, anonymous, web server'
+data_files=[
+ (os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']),
+ (os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']),
+ (os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']),
+ (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/html'), file_list('share/html')),
+ ]
+if platform.system() != 'OpenBSD':
+ data_files.append(('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py']))
setup(
name='onionshare', version=version,
@@ -54,14 +65,5 @@ setup(
packages=['onionshare', 'onionshare_gui'],
include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],
- data_files=[
- (os.path.join(sys.prefix, 'share/applications'), ['install/onionshare.desktop']),
- (os.path.join(sys.prefix, 'share/appdata'), ['install/onionshare.appdata.xml']),
- (os.path.join(sys.prefix, 'share/pixmaps'), ['install/onionshare80.xpm']),
- (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/html'), file_list('share/html')),
- ('/usr/share/nautilus-python/extensions/', ['install/scripts/onionshare-nautilus.py'])
- ]
+ data_files=data_files
)
diff --git a/share/html/404.html b/share/html/404.html
index 2e2b06f0..09d0fc3c 100644
--- a/share/html/404.html
+++ b/share/html/404.html
@@ -2,6 +2,7 @@
<html>
<head>
<title>Error 404</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
body {
background-color: #FFC4D5;
diff --git a/share/html/denied.html b/share/html/denied.html
index ef77642e..a82ac027 100644
--- a/share/html/denied.html
+++ b/share/html/denied.html
@@ -2,6 +2,7 @@
<html>
<head>
<title>OnionShare</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style>
body {
background-color: #222222;
@@ -15,4 +16,4 @@
<body>
<p>OnionShare download in progress</p>
</body>
-</html>
+</html>
diff --git a/share/html/index.html b/share/html/index.html
index aa91174e..57711e02 100644
--- a/share/html/index.html
+++ b/share/html/index.html
@@ -2,106 +2,207 @@
<html>
<head>
<title>OnionShare</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
- body {
- background-color: #222222;
- color: #ffffff;
- text-align: center;
- font-family: sans-serif;
- padding: 5em 1em;
- }
- .button {
- -moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
- -webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
- box-shadow:inset 0px 1px 0px 0px #cae3fc;
- background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #79bbff), color-stop(1, #4197ee) );
- background:-moz-linear-gradient( center top, #79bbff 5%, #4197ee 100% );
- filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#79bbff', endColorstr='#4197ee');
- background-color:#79bbff;
- -webkit-border-top-left-radius:12px;
- -moz-border-radius-topleft:12px;
- border-top-left-radius:12px;
- -webkit-border-top-right-radius:12px;
- -moz-border-radius-topright:12px;
- border-top-right-radius:12px;
- -webkit-border-bottom-right-radius:12px;
- -moz-border-radius-bottomright:12px;
- border-bottom-right-radius:12px;
- -webkit-border-bottom-left-radius:12px;
- -moz-border-radius-bottomleft:12px;
- border-bottom-left-radius:12px;
- text-indent:0;
- border:1px solid #469df5;
- display:inline-block;
- color:#ffffff;
- font-size:29px;
- font-weight:bold;
- font-style:normal;
- height:50px;
- line-height:50px;
- text-decoration:none;
- text-align:center;
- text-shadow:1px 1px 0px #287ace;
- padding: 0 20px;
- }
- .button:hover {
- background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #4197ee), color-stop(1, #79bbff) );
- background:-moz-linear-gradient( center top, #4197ee 5%, #79bbff 100% );
- filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4197ee', endColorstr='#79bbff');
- background-color:#4197ee;
- }.button:active {
- position:relative;
- top:1px;
- }
+ .clearfix:after {
+ content: ".";
+ display: block;
+ clear: both;
+ visibility: hidden;
+ line-height: 0;
+ height: 0;
+ }
- .download-size {
- color: #999999;
- }
- .download-description {
- padding: 10px;
- }
- .file-list {
- margin: 50px auto 0 auto;
- padding: 10px;
- text-align: left;
- background-color: #333333;
- }
- .file-list th {
- padding: 5px;
- font-weight: bold;
- color: #999999;
- }
- .file-list td {
- padding: 5px;
- }
+ body {
+ margin: 0;
+ font-family: Helvetica;
+ }
+
+ header {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ background: #fcfcfc;
+ background: -webkit-linear-gradient(top, #fcfcfc 0%, #f2f2f2 100%);
+ padding: 0.8rem;
+ }
+
+ header .logo {
+ vertical-align: middle;
+ width: 45px;
+ height: 45px;
+ }
+
+ header h1 {
+ display: inline-block;
+ margin: 0 0 0 0.5rem;
+ vertical-align: middle;
+ font-weight: normal;
+ font-size: 1.5rem;
+ color: #666666;
+ }
+
+ header .right {
+ float: right;
+ font-size: .75rem;
+ }
+
+ header .right ul li {
+ display: inline;
+ margin: 0 0 0 .5rem;
+ font-size: 1rem;
+ }
+
+ header .button {
+ color: #ffffff;
+ background-color: #4e064f;
+ padding: 10px;
+ border-radius: 5px;
+ text-decoration: none;
+ margin-left: 1rem;
+ cursor: pointer;
+ }
+
+ table.file-list {
+ width: 100%;
+ margin: 0 auto;
+ border-collapse: collapse;
+ }
+
+ table.file-list th {
+ text-align: left;
+ text-transform: uppercase;
+ font-weight: normal;
+ color: #666666;
+ padding: 0.5rem;
+ }
+
+ table.file-list tr {
+ border-bottom: 1px solid #e0e0e0;
+ }
+
+ table.file-list td {
+ white-space: nowrap;
+ padding: 0.5rem 10rem 0.5rem 0.8rem;
+ }
+
+ table.file-list td img {
+ vertical-align: middle;
+ margin-right: 0.5rem;
+ }
+
+ table.file-list td:last-child {
+ width: 100%;
+ }
</style>
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
<body>
- <p><a class="button" href='/{{ slug }}/download'>{{ filename }} &#x25BC;</a></p>
- <p class="download-size"><strong title="{{ filesize }} bytes">{{ filesize_human }} (compressed)</strong></p>
- <p class="download-description">This zip file contains the following contents:</p>
- <table class="file-list">
+
+ <header class="clearfix">
+ <div class="right">
+ <ul>
+ <li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
+ <li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
+ </ul>
+ </div>
+ <img class="logo" src="data:image/png;base64,{{logo_b64}}" title="OnionShare">
+ <h1>OnionShare</h1>
+ </header>
+
+ <table class="file-list" id="file-list">
<tr>
- <th>Type</th>
- <th>Name</th>
- <th>Size</th>
+ <th onclick="sortTable(0)">Filename</th>
+ <th onclick="sortTable(1)">Size</th>
+ <th></th>
</tr>
{% for info in file_info.dirs %}
<tr>
- <td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQXOpwjbQAAAM5JREFUSMftlksOwyAMRDFCbNjkDL1kz9K75Cq9AZF3kYIM7oqoP/KpcFK1mRUrnsbW2Fbq0N+JiPhZRMS1OZAfzLzocwCAGmC9V2Vhjduarj8CzynGqIwxsDl4SXV267E4uFTN33X8deCxDyV1XXeRYJo1UUDEa9M0pxoRK+b4HiqRcz3nVAJKRK+TSxqaGbrkNKUkBn0oNSK2+T0MA1dau9NzO4QwuvPen1lAxQtEsq/vNpSWhvZ9v/3IRMTWOQezkyuEoKy14tfHoU11A6Mr5AxrpuMVAAAAAElFTkSuQmCC" /></td>
- <td>{{ info.basename }}</td>
+ <td>
+ <img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ folder_b64 }}" />
+ {{ info.basename }}
+ </td>
<td>{{ info.size_human }}</td>
+ <td></td>
</tr>
{% endfor %}
{% for info in file_info.files %}
<tr>
- <td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQqYvRvfAAAAJFJREFUSMftlUkOwCAIAEWJ//+uIUivtYmtG7VN8KYHRhlA5zYtuB6klCTG2BcEAHrBeN4ws3jvX3lxQZmFiogMgVesVrhKXlvgakKf4KqVdAdXL+Ea3O8aILh0GnUMEhjtw9mLbUv1tx33fgItynBVoN+k2hybY3Nsjg08Bs45q4GYuQ4OIQARLYcSkUPEoiUP4j5IFasKOnUAAAAASUVORK5CYII=" /></td>
- <td>{{ info.basename }}</td>
+ <td>
+ <img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ file_b64 }}" />
+ {{ info.basename }}
+ </td>
<td>{{ info.size_human }}</td>
+ <td></td>
</tr>
{% endfor %}
</table>
+ <script>
+ // Function to convert human-readable sizes back to bytes, for sorting
+ function unhumanize(text) {
+ var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4};
+ var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i;
+ var res = regex.exec(text);
+ if(res[2] === undefined) {
+ // Account for alphabetical words (file/dir names)
+ return text;
+ } else {
+ return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
+ }
+ }
+ function sortTable(n) {
+ var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
+ table = document.getElementById("file-list");
+ switching = true;
+ // Set the sorting direction to ascending:
+ dir = "asc";
+ /* Make a loop that will continue until
+ no switching has been done: */
+ while (switching) {
+ // Start by saying: no switching is done:
+ switching = false;
+ rows = table.getElementsByTagName("TR");
+ /* Loop through all table rows (except the
+ first, which contains table headers): */
+ for (i = 1; i < (rows.length - 1); i++) {
+ // Start by saying there should be no switching:
+ shouldSwitch = false;
+ /* Get the two elements you want to compare,
+ one from current row and one from the next: */
+ x = rows[i].getElementsByTagName("TD")[n];
+ y = rows[i + 1].getElementsByTagName("TD")[n];
+ /* Check if the two rows should switch place,
+ based on the direction, asc or desc: */
+ if (dir == "asc") {
+ if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) {
+ // If so, mark as a switch and break the loop:
+ shouldSwitch= true;
+ break;
+ }
+ } else if (dir == "desc") {
+ if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) {
+ // If so, mark as a switch and break the loop:
+ shouldSwitch= true;
+ break;
+ }
+ }
+ }
+ if (shouldSwitch) {
+ /* If a switch has been marked, make the switch
+ and mark that a switch has been done: */
+ rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
+ switching = true;
+ // Each time a switch is done, increase this count by 1:
+ switchcount ++;
+ } else {
+ /* If no switching has been done AND the direction is "asc",
+ set the direction to "desc" and run the while loop again. */
+ if (switchcount == 0 && dir == "asc") {
+ dir = "desc";
+ switching = true;
+ }
+ }
+ }
+ }
+ </script>
</body>
</html>
diff --git a/share/images/download_completed.png b/share/images/download_completed.png
new file mode 100644
index 00000000..e68fe5a2
--- /dev/null
+++ b/share/images/download_completed.png
Binary files differ
diff --git a/share/images/download_completed_none.png b/share/images/download_completed_none.png
new file mode 100644
index 00000000..8dbd6939
--- /dev/null
+++ b/share/images/download_completed_none.png
Binary files differ
diff --git a/share/images/download_in_progress.png b/share/images/download_in_progress.png
new file mode 100644
index 00000000..19694659
--- /dev/null
+++ b/share/images/download_in_progress.png
Binary files differ
diff --git a/share/images/download_in_progress_none.png b/share/images/download_in_progress_none.png
new file mode 100644
index 00000000..2d61dba4
--- /dev/null
+++ b/share/images/download_in_progress_none.png
Binary files differ
diff --git a/share/images/drop_files.png b/share/images/drop_files.png
deleted file mode 100644
index 1e2ea7d6..00000000
--- a/share/images/drop_files.png
+++ /dev/null
Binary files differ
diff --git a/share/images/favicon.ico b/share/images/favicon.ico
new file mode 100644
index 00000000..63e65d8b
--- /dev/null
+++ b/share/images/favicon.ico
Binary files differ
diff --git a/share/images/file_delete.png b/share/images/file_delete.png
new file mode 100644
index 00000000..b9057df5
--- /dev/null
+++ b/share/images/file_delete.png
Binary files differ
diff --git a/share/images/info.png b/share/images/info.png
new file mode 100644
index 00000000..4be4e65e
--- /dev/null
+++ b/share/images/info.png
Binary files differ
diff --git a/share/images/logo_transparent.png b/share/images/logo_transparent.png
new file mode 100644
index 00000000..1e8ed196
--- /dev/null
+++ b/share/images/logo_transparent.png
Binary files differ
diff --git a/share/images/server_started.png b/share/images/server_started.png
index 833387e1..9c0c3176 100644
--- a/share/images/server_started.png
+++ b/share/images/server_started.png
Binary files differ
diff --git a/share/images/server_stopped.png b/share/images/server_stopped.png
index 414ce81e..5c5b2ec0 100644
--- a/share/images/server_stopped.png
+++ b/share/images/server_stopped.png
Binary files differ
diff --git a/share/images/server_working.png b/share/images/server_working.png
index c6db94ff..e5c8b318 100644
--- a/share/images/server_working.png
+++ b/share/images/server_working.png
Binary files differ
diff --git a/share/images/settings_inactive.png b/share/images/settings_inactive.png
deleted file mode 100644
index 1b35201b..00000000
--- a/share/images/settings_inactive.png
+++ /dev/null
Binary files differ
diff --git a/share/images/web_file.png b/share/images/web_file.png
new file mode 100644
index 00000000..1931aff0
--- /dev/null
+++ b/share/images/web_file.png
Binary files differ
diff --git a/share/images/web_folder.png b/share/images/web_folder.png
new file mode 100644
index 00000000..3ca5df21
--- /dev/null
+++ b/share/images/web_folder.png
Binary files differ
diff --git a/share/locale/da.json b/share/locale/da.json
new file mode 100644
index 00000000..2c8d0c78
--- /dev/null
+++ b/share/locale/da.json
@@ -0,0 +1,136 @@
+{
+ "config_onion_service": "Konfigurerer onion-tjeneste på port {0:d}.",
+ "preparing_files": "Forbereder filer som skal deles.",
+ "wait_for_hs": "Venter på at HS bliver klar:",
+ "wait_for_hs_trying": "Prøver...",
+ "wait_for_hs_nope": "Endnu ikke klar.",
+ "wait_for_hs_yup": "Klar!",
+ "give_this_url": "Giv denne URL til personen du sender filen til:",
+ "give_this_url_stealth": "Giv denne URL og HidServAuth-linje til personen du sender filen til:",
+ "ctrlc_to_stop": "Tryk på Ctrl-C for at stoppe serveren",
+ "not_a_file": "{0:s} er ikke en gyldig fil.",
+ "not_a_readable_file": "{0:s} er ikke en læsbar fil.",
+ "no_available_port": "Kunne ikke starte onion-tjenesten da der ikke var nogen tilgængelig port.",
+ "download_page_loaded": "Downloadside indlæst",
+ "other_page_loaded": "URL indlæst",
+ "close_on_timeout": "Lukker automatisk da timeout er nået",
+ "closing_automatically": "Lukker automatisk da download er færdig",
+ "timeout_download_still_running": "Venter på at download skal blive færdig inden automatisk stop",
+ "large_filesize": "Advarsel: Det kan tage timer at sende store filer",
+ "error_tails_invalid_port": "Ugyldig værdi, port skal være et heltal",
+ "error_tails_unknown_root": "Ukendt fejl med Tails-rodproces",
+ "systray_menu_exit": "Afslut",
+ "systray_download_started_title": "OnionShare-download startet",
+ "systray_download_started_message": "En bruger startede download af dine filer",
+ "systray_download_completed_title": "OnionShare-download færdig",
+ "systray_download_completed_message": "Brugeren er færdig med at downloade dine filer",
+ "systray_download_canceled_title": "OnionShare-download annulleret",
+ "systray_download_canceled_message": "Brugeren annullerede downloaden",
+ "help_local_only": "Undlad at bruge tor: kun til udvikling",
+ "help_stay_open": "Hold onion-tjeneste kørende efter download er færdig",
+ "help_shutdown_timeout": "Luk onion-tjenesten efter N sekunder",
+ "help_transparent_torification": "Mit system er gennemsigtigt torifiseret",
+ "help_stealth": "Opret usynlig onion-tjeneste (avanceret)",
+ "help_debug": "Log programfejl til stdout, og log webfejl til disk",
+ "help_filename": "Liste over filer eller mapper som skal deles",
+ "help_config": "Sti til en brugerdefineret JSON-konfigurationsfil (valgfri)",
+ "gui_drag_and_drop": "Træk og slip\nfiler her",
+ "gui_add": "Tilføj",
+ "gui_delete": "Slet",
+ "gui_choose_items": "Vælg",
+ "gui_start_server": "Start deling",
+ "gui_stop_server": "Stop deling",
+ "gui_copy_url": "Kopiér URL",
+ "gui_copy_hidservauth": "Kopiér HidServAuth",
+ "gui_downloads": "Downloads:",
+ "gui_canceled": "Annulleret",
+ "gui_copied_url": "Kopierede URL til udklipsholder",
+ "gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder",
+ "gui_starting_server1": "Starter Tor onion-tjeneste...",
+ "gui_starting_server2": "Databehandler filer...",
+ "gui_please_wait": "Vent venligst...",
+ "error_hs_dir_cannot_create": "Kan ikke oprette onion-tjenestens mappe {0:s}",
+ "error_hs_dir_not_writable": "onion-tjenestens mappe {0:s} er skrivebeskyttet",
+ "using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse",
+ "gui_download_progress_complete": "%p%, tid forløbet: {0:s}",
+ "gui_download_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)",
+ "gui_download_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%",
+ "version_string": "Onionshare {0:s} | https://onionshare.org/",
+ "gui_quit_warning": "Er du sikker på, at du vil afslutte?\nURL'en som du deler vil ikke eksistere længere.",
+ "gui_quit_warning_quit": "Afslut",
+ "gui_quit_warning_dont_quit": "Afslut ikke",
+ "error_rate_limit": "En angriber forsøger måske at gætte din URL. For at forhindre det, har OnionShare automatisk stoppet serveren. For at dele filerne skal du starte den igen og dele den nye URL.",
+ "zip_progress_bar_format": "Databehandler filer: %p%",
+ "error_stealth_not_supported": "For at oprette usynlige onion-tjenester, skal du mindst have Tor 0.2.9.1-alpha (eller Tor Browser 6.5) og mindst python3-stem 1.5.0.",
+ "error_ephemeral_not_supported": "OnionShare kræver mindst Tor 0.2.7.1 og mindst python3-stem 1.4.0.",
+ "gui_settings_window_title": "Indstillinger",
+ "gui_settings_stealth_label": "Usynlig (avanceret)",
+ "gui_settings_stealth_option": "Opret usynlige onion-tjenester",
+ "gui_settings_stealth_option_details": "Det gør OnionShare mere sikker, men også mere besværlig for modtageren at oprette forbindelse til den.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Mere information</a>.",
+ "gui_settings_stealth_hidservauth_string": "Du har gemt den private nøgle til at blive brugt igen, så din HidServAuth-streng bruges også igen.\nKlik nedenfor, for at kopiere HidServAuth.",
+ "gui_settings_autoupdate_label": "Søg efter opdateringer",
+ "gui_settings_autoupdate_option": "Giv mig besked når der findes opdateringer",
+ "gui_settings_autoupdate_timestamp": "Sidste søgning: {}",
+ "gui_settings_autoupdate_timestamp_never": "Aldrig",
+ "gui_settings_autoupdate_check_button": "Søg efter opdateringer",
+ "gui_settings_sharing_label": "Valgmuligheder for deling",
+ "gui_settings_close_after_first_download_option": "Stop deling efter første download",
+ "gui_settings_systray_notifications": "Vis skrivebordsnotifikationer",
+ "gui_settings_connection_type_label": "Hvordan skal OnionShare oprette forbindelse til Tor?",
+ "gui_settings_connection_type_bundled_option": "Brug Tor som er bundet med OnionShare",
+ "gui_settings_connection_type_automatic_option": "Prøv automatisk konfiguration med Tor Browser",
+ "gui_settings_connection_type_control_port_option": "Opret forbindelse med kontrolport",
+ "gui_settings_connection_type_socket_file_option": "Opret forbindelse med sokkelfil",
+ "gui_settings_connection_type_test_button": "Test Tor-indstillinger",
+ "gui_settings_control_port_label": "Kontrolport",
+ "gui_settings_socket_file_label": "Sokkelfil",
+ "gui_settings_socks_label": "SOCKS-port",
+ "gui_settings_authenticate_label": "Valgmuligheder for Tor-autentifikation",
+ "gui_settings_authenticate_no_auth_option": "Ingen autentifikation, eller cookieautentifikation",
+ "gui_settings_authenticate_password_option": "Adgangskode",
+ "gui_settings_authenticate_cookie_option": "Cookie",
+ "gui_settings_password_label": "Adgangskode",
+ "gui_settings_cookie_label": "Cookiesti",
+ "gui_settings_tor_bridges": "Understøttelse af Tor-bro",
+ "gui_settings_tor_bridges_no_bridges_radio_option": "Brug ikke broer",
+ "gui_settings_tor_bridges_obfs4_radio_option": "Brug indbygget obfs4 udskiftelige transporter",
+ "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Brug indbygget obfs4 udskiftelige transporter (kræver obfs4proxy)",
+ "gui_settings_tor_bridges_custom_radio_option": "Brug brugerdefinerede broer",
+ "gui_settings_tor_bridges_custom_label": "Du kan få broer fra <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
+ "gui_settings_tor_bridges_invalid": "Ingen af broerne du leverede ser ud til at være gyldige, så de ignoreres.\nPrøv venligst igen med gyldige broer.",
+ "gui_settings_button_save": "Gem",
+ "gui_settings_button_cancel": "Annuller",
+ "gui_settings_button_help": "Hjælp",
+ "gui_settings_shutdown_timeout_choice": "Sæt timer til automatisk stop?",
+ "gui_settings_shutdown_timeout": "Stop delingen ved:",
+ "settings_saved": "Indstillinger gemt til {}",
+ "settings_error_unknown": "Kan ikke oprette forbindelse til Tor-kontroller da indstillingerne ikke giver mening.",
+ "settings_error_automatic": "Kan ikke oprette forbindelse til Tor-kontroller. Kører Tor Browser i baggrunden? Hvis du ikke har den kan du få den fra:\nhttps://www.torproject.org/.",
+ "settings_error_socket_port": "Kan ikke oprette forbindelse til Tor-kontroller på {}:{}",
+ "settings_error_socket_file": "Kan ikke oprette forbindelse til Tor-kontroller med sokkelfilen {}",
+ "settings_error_auth": "Forbundet til {}:{}, men kan ikke autentificere. Er det en Tor-kontroller?",
+ "settings_error_missing_password": "Forbundet til Tor-kontroller, men den kræver en adgangskode for at autentificere",
+ "settings_error_unreadable_cookie_file": "Forbundet til Tor-kontroller, men kan ikke autentificere da din adgangskode kan være forkert, og din bruger ikke har tilladelse til at læse cookiefilen.",
+ "settings_error_bundled_tor_not_supported": "Bundet Tor understøttes ikke når der ikke bruges udviklertilstand i Windows eller MacOS.",
+ "settings_error_bundled_tor_timeout": "Det tager for længe at oprette forbindelse til Tor. Din computer er måske offline, eller dit ur går forkert.",
+ "settings_error_bundled_tor_canceled": "Tor-processen lukkede inden den blev færdig med at oprette forbindelse.",
+ "settings_error_bundled_tor_broken": "Der er noget galt med OnionShare som opretter forbindelse til Tor i baggrunden:\n{}",
+ "settings_test_success": "Tillykke, OnionShare kan oprette forbindelse til Tor-kontrolleren.\n\nTor version: {}\nUnderstøtter kortvarige onion-tjenester: {}\nUnderstøtter usynlige onion-tjenester: {}",
+ "error_tor_protocol_error": "Fejl under snak med Tor-kontrolleren.\nHvis du bruger Whonix, så tjek https://www.whonix.org/wiki/onionshare for at få OnionShare til at virke.",
+ "connecting_to_tor": "Forbundet til Tor-netværket",
+ "update_available": "Der findes en OnionShare-opdatering. <a href='{}'>Klik her</a> for at downloade den.<br><br>Installeret version: {}<br>Seneste version: {}",
+ "update_error_check_error": "Fejl under søgning efter opdateringer: Du er måske ikke forbundet til Tor, eller måske er OnionShare-webstedet nede.",
+ "update_error_invalid_latest_version": "Fejl under søgning efter opdateringer: OnionShare-webstedet svarende ved at sige at den seneste version er '{}', men det ser ikke ud til at være en gyldig versionsstreng.",
+ "update_not_available": "Du kører den seneste version af OnionShare.",
+ "gui_tor_connection_ask": "Vil du åbne OnionShare-indstillinger for at fejlsøge forbindelsen til Tor?",
+ "gui_tor_connection_ask_open_settings": "Åbn indstillinger",
+ "gui_tor_connection_ask_quit": "Afslut",
+ "gui_tor_connection_error_settings": "Prøv at justere måden hvorpå OnionShare opretter forbindelse til Tor-netværket i Indstillinger.",
+ "gui_tor_connection_canceled": "OnionShare kan ikke oprette forbindelse til Tor.\n\nSørg for at du har forbindelse til internettet, og åbn herefter OnionShare igen for at konfigurere Tor-forbindelsen.",
+ "gui_tor_connection_lost": "Afbryder forbindelsen fra Tor.",
+ "gui_server_started_after_timeout": "Serveren startede efter dit valgte automatiske timeout.\nStart venligst en ny deling.",
+ "gui_server_timeout_expired": "Den valgte timeout er allerede udløbet.\nOpdater venligst timeouten og herefter kan du starte deling.",
+ "share_via_onionshare": "Del via OnionShare",
+ "gui_save_private_key_checkbox": "Brug en vedvarende URL\n(fravalg vil slette gemte URL)",
+ "persistent_url_in_use": "Denne deling bruger en vedvarende URL"
+}
diff --git a/share/locale/en.json b/share/locale/en.json
index cdd89c1e..09ead591 100644
--- a/share/locale/en.json
+++ b/share/locale/en.json
@@ -5,17 +5,17 @@
"wait_for_hs_trying": "Trying...",
"wait_for_hs_nope": "Not ready yet.",
"wait_for_hs_yup": "Ready!",
- "give_this_url": "Give this URL to the person you're sending the file to:",
- "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
+ "give_this_url": "Give this address to the person you're sending the file to:",
+ "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
"ctrlc_to_stop": "Press Ctrl-C to stop server",
"not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.",
"no_available_port": "Could not start the Onion service as there was no available port.",
"download_page_loaded": "Download page loaded",
- "other_page_loaded": "URL loaded",
- "close_on_timeout": "Closing automatically because timeout was reached",
- "closing_automatically": "Closing automatically because download finished",
- "timeout_download_still_running": "Waiting for download to complete before auto-stopping",
+ "other_page_loaded": "Address loaded",
+ "close_on_timeout": "Stopped because timer expired",
+ "closing_automatically": "Stopped because download finished",
+ "timeout_download_still_running": "Waiting for download to complete",
"large_filesize": "Warning: Sending large files could take hours",
"error_tails_invalid_port": "Invalid value, port must be an integer",
"error_tails_unknown_root": "Unknown error with Tails root process",
@@ -34,21 +34,25 @@
"help_debug": "Log application errors to stdout, and log web errors to disk",
"help_filename": "List of files or folders to share",
"help_config": "Path to a custom JSON config file (optional)",
- "gui_drag_and_drop": "Drag and drop\nfiles here",
+ "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add",
"gui_delete": "Delete",
"gui_choose_items": "Choose",
"gui_start_server": "Start Sharing",
"gui_stop_server": "Stop Sharing",
- "gui_copy_url": "Copy URL",
+ "gui_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
+ "gui_stop_server_shutdown_timeout_tooltip": "Share will stop automatically at {}",
+ "gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Downloads:",
"gui_canceled": "Canceled",
- "gui_copied_url": "Copied URL to clipboard",
- "gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
+ "gui_copied_url_title": "Copied OnionShare address",
+ "gui_copied_url": "The OnionShare address has been copied to clipboard",
+ "gui_copied_hidservauth_title": "Copied HidServAuth",
+ "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
"gui_starting_server1": "Starting Tor onion service...",
"gui_starting_server2": "Crunching files...",
- "gui_please_wait": "Please wait...",
+ "gui_please_wait": "Starting... Click to cancel",
"error_hs_dir_cannot_create": "Cannot create onion service dir {0:s}",
"error_hs_dir_not_writable": "onion service dir {0:s} is not writable",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
@@ -56,13 +60,14 @@
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "Onionshare {0:s} | https://onionshare.org/",
- "gui_quit_warning": "Are you sure you want to quit?\nThe URL you are sharing won't exist anymore.",
+ "gui_quit_title": "Transfer in Progress",
+ "gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
- "gui_quit_warning_dont_quit": "Don't Quit",
- "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.",
+ "gui_quit_warning_dont_quit": "Cancel",
+ "error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
"zip_progress_bar_format": "Crunching files: %p%",
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
- "error_ephemeral_not_supported": "OnionShare requires at least at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
+ "error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
"gui_settings_window_title": "Settings",
"gui_settings_stealth_label": "Stealth (advanced)",
"gui_settings_stealth_option": "Create stealth onion services",
@@ -93,15 +98,19 @@
"gui_settings_cookie_label": "Cookie path",
"gui_settings_tor_bridges": "Tor Bridge support",
"gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges",
- "gui_settings_tor_bridges_obfs4_radio_option": "Use obfs4 pluggable transports",
- "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use obfs4 pluggable transports (requires obfs4proxy)",
- "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges (non-pluggable transports)",
- "gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/bridges\">https://bridges.torproject.org</a>",
- "gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to be valid, so they've been ignored.\nPlease try again with valid bridges.",
+ "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports",
+ "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)",
+ "gui_settings_tor_bridges_meek_lite_amazon_radio_option": "Use built-in meek_lite (Amazon) pluggable transports",
+ "gui_settings_tor_bridges_meek_lite_amazon_radio_option_no_obfs4proxy": "Use built-in meek_lite (Amazon) pluggable transports (requires obfs4proxy)",
+ "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports",
+ "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)",
+ "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges",
+ "gui_settings_tor_bridges_custom_label": "You can get bridges from <a href=\"https://bridges.torproject.org/options\">https://bridges.torproject.org</a>",
+ "gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to be valid.\nPlease try again with valid bridges.",
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
- "gui_settings_shutdown_timeout_choice": "Set auto-stop timer?",
+ "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:",
"settings_saved": "Settings saved to {}",
"settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.",
@@ -131,6 +140,17 @@
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
"share_via_onionshare": "Share via OnionShare",
- "gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)",
- "persistent_url_in_use": "This share is using a persistent URL"
+ "gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved address)",
+ "gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using <b>Tor Browser</b>: <img src='{}' />",
+ "gui_url_label_persistent": "This share will not stop automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
+ "gui_url_label_stay_open": "This share will not stop automatically unless a timer is set.",
+ "gui_url_label_onetime": "This share will stop after the first download",
+ "gui_url_label_onetime_and_persistent": "This share will stop after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
+ "gui_status_indicator_stopped": "Ready to Share",
+ "gui_status_indicator_working": "Starting...",
+ "gui_status_indicator_started": "Sharing",
+ "gui_file_info": "{} Files, {}",
+ "gui_file_info_single": "{} File, {}",
+ "info_in_progress_downloads_tooltip": "{} download(s) in progress",
+ "info_completed_downloads_tooltip": "{} download(s) completed"
}
diff --git a/share/locale/nl.json b/share/locale/nl.json
index 1203f2ee..3dd74664 100644
--- a/share/locale/nl.json
+++ b/share/locale/nl.json
@@ -64,7 +64,6 @@
"error_stealth_not_supported": "Om een geheime onion service te maken heb je minstens Tor 0.2.9.1-alpha (of Tor Browser 6.5) en minstens python3-stem 1.5.0 nodig.",
"error_ephemeral_not_supported": "OnionShare vereist minstens Tor 0.2.7.1 en minstens python3-stem 1.4.0.",
"gui_settings_window_title": "Instellingen",
- "gui_settings_window_title": "Instellingen",
"gui_settings_stealth_label": "Stealth (geavanceerd)",
"gui_settings_stealth_option": "Maak stealth onion services",
"gui_settings_stealth_option_details": "Dit maakt OnionShare veiliger, maar ook lastiger voor de ontvanger om te verbinden.<br><a href=\"https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services\">Meer informatie</a>.",
diff --git a/share/torrc_template-meek_lite_amazon b/share/torrc_template-meek_lite_amazon
new file mode 100644
index 00000000..606ae889
--- /dev/null
+++ b/share/torrc_template-meek_lite_amazon
@@ -0,0 +1,2 @@
+Bridge meek_lite 0.0.2.0:2 B9E7141C594AF25699E0079C1F0146F409495296 url=https://d2cly7j4zqgua7.cloudfront.net/ front=a0.awsstatic.com
+UseBridges 1 \ No newline at end of file
diff --git a/share/torrc_template-meek_lite_azure b/share/torrc_template-meek_lite_azure
new file mode 100644
index 00000000..a9b374ba
--- /dev/null
+++ b/share/torrc_template-meek_lite_azure
@@ -0,0 +1,2 @@
+Bridge meek_lite 0.0.2.0:3 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
+UseBridges 1 \ No newline at end of file
diff --git a/share/version.txt b/share/version.txt
index 5625e59d..7e32cd56 100644
--- a/share/version.txt
+++ b/share/version.txt
@@ -1 +1 @@
-1.2
+1.3
diff --git a/test/conftest.py b/test/conftest.py
index 0a3bc806..8f10162b 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,13 +1,15 @@
+import sys
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
import os
import shutil
-import sys
import tempfile
import pytest
from onionshare import common
-
@pytest.fixture
def temp_dir_1024():
""" Create a temporary directory that has a single file of a
diff --git a/test/test_onionshare_common.py b/test/test_onionshare_common.py
index f574ad7f..cb864313 100644
--- a/test/test_onionshare_common.py
+++ b/test/test_onionshare_common.py
@@ -193,12 +193,6 @@ class TestGetResourcePath:
common.get_resource_path(os.path.join(prefix, 'test_filename')) ==
os.path.join(prefix, 'test_filename'))
- def test_frozen_windows(self, platform_windows, sys_frozen):
- prefix = os.path.join(os.path.dirname(sys.executable), 'share')
- assert (
- common.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') ?
diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py
index b17ce30a..e50eee41 100644
--- a/test/test_onionshare_settings.py
+++ b/test/test_onionshare_settings.py
@@ -55,11 +55,14 @@ class TestSettings:
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
+ 'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
'no_bridges': True,
'tor_bridges_use_obfs4': False,
+ 'tor_bridges_use_meek_lite_amazon': False,
+ 'tor_bridges_use_meek_lite_azure': False,
'tor_bridges_use_custom_bridges': '',
'save_private_key': False,
'private_key': '',
@@ -125,6 +128,8 @@ class TestSettings:
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_amazon') is False
+ assert settings_obj.get('tor_bridges_use_meek_lite_azure') is False
assert settings_obj.get('tor_bridges_use_custom_bridges') == ''