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 + + # 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) + + # 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) - self.status_bar.setStyleSheet( - "QStatusBar::item { border: 0px; }") - version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version())) - version_label.setStyleSheet('color: #666666') - self.settings_button = QtWidgets.QPushButton() - self.settings_button.setDefault(False) - self.settings_button.setFlat(True) - 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) + 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 + # 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) - # 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() + # 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(' {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(' {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) + 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() - # add the widgets - self.addLayout(shutdown_timeout_layout_group) - self.addLayout(server_layout) - self.addLayout(url_layout) + 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: + close_forced_update_thread() + + def update_invalid_version(): Alert(strings._('update_error_invalid_latest_version', True).format(e.latest_version), QtWidgets.QMessageBox.Warning) + close_forced_update_thread() - # Enable buttons - self._enable_buttons() - - # 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 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 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 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 . """ 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 Binary files a/screenshots/appdata-client.png and b/screenshots/appdata-client.png differ diff --git a/screenshots/appdata-server.png b/screenshots/appdata-server.png index 864d21e8..abdb2198 100644 Binary files a/screenshots/appdata-server.png and b/screenshots/appdata-server.png differ diff --git a/screenshots/client.png b/screenshots/client.png index ba7098f4..fdf69fd0 100644 Binary files a/screenshots/client.png and b/screenshots/client.png differ diff --git a/screenshots/server.png b/screenshots/server.png index 0a90ea4a..8bdf2971 100644 Binary files a/screenshots/server.png and b/screenshots/server.png 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 @@ Error 404 + -

{{ filename }} ▼

-

{{ filesize_human }} (compressed)

-

This zip file contains the following contents:

- + +
+
+ +
+ +

OnionShare

+
+ +
- - - + + + {% for info in file_info.dirs %} - - + + {% endfor %} {% for info in file_info.files %} - - + + {% endfor %}
TypeNameSizeFilenameSize
{{ info.basename }} + + {{ info.basename }} + {{ info.size_human }}
{{ info.basename }} + + {{ info.basename }} + {{ info.size_human }}
+ diff --git a/share/images/download_completed.png b/share/images/download_completed.png new file mode 100644 index 00000000..e68fe5a2 Binary files /dev/null and b/share/images/download_completed.png differ diff --git a/share/images/download_completed_none.png b/share/images/download_completed_none.png new file mode 100644 index 00000000..8dbd6939 Binary files /dev/null and b/share/images/download_completed_none.png differ diff --git a/share/images/download_in_progress.png b/share/images/download_in_progress.png new file mode 100644 index 00000000..19694659 Binary files /dev/null and b/share/images/download_in_progress.png 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 Binary files /dev/null and b/share/images/download_in_progress_none.png differ diff --git a/share/images/drop_files.png b/share/images/drop_files.png deleted file mode 100644 index 1e2ea7d6..00000000 Binary files a/share/images/drop_files.png and /dev/null differ diff --git a/share/images/favicon.ico b/share/images/favicon.ico new file mode 100644 index 00000000..63e65d8b Binary files /dev/null and b/share/images/favicon.ico differ diff --git a/share/images/file_delete.png b/share/images/file_delete.png new file mode 100644 index 00000000..b9057df5 Binary files /dev/null and b/share/images/file_delete.png differ diff --git a/share/images/info.png b/share/images/info.png new file mode 100644 index 00000000..4be4e65e Binary files /dev/null and b/share/images/info.png differ diff --git a/share/images/logo_transparent.png b/share/images/logo_transparent.png new file mode 100644 index 00000000..1e8ed196 Binary files /dev/null and b/share/images/logo_transparent.png differ diff --git a/share/images/server_started.png b/share/images/server_started.png index 833387e1..9c0c3176 100644 Binary files a/share/images/server_started.png and b/share/images/server_started.png differ diff --git a/share/images/server_stopped.png b/share/images/server_stopped.png index 414ce81e..5c5b2ec0 100644 Binary files a/share/images/server_stopped.png and b/share/images/server_stopped.png differ diff --git a/share/images/server_working.png b/share/images/server_working.png index c6db94ff..e5c8b318 100644 Binary files a/share/images/server_working.png and b/share/images/server_working.png differ diff --git a/share/images/settings_inactive.png b/share/images/settings_inactive.png deleted file mode 100644 index 1b35201b..00000000 Binary files a/share/images/settings_inactive.png and /dev/null differ diff --git a/share/images/web_file.png b/share/images/web_file.png new file mode 100644 index 00000000..1931aff0 Binary files /dev/null and b/share/images/web_file.png differ diff --git a/share/images/web_folder.png b/share/images/web_folder.png new file mode 100644 index 00000000..3ca5df21 Binary files /dev/null and b/share/images/web_folder.png 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.
Mere information.", + "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 https://bridges.torproject.org", + "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. Klik her for at downloade den.

Installeret version: {}
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 https://bridges.torproject.org", - "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 https://bridges.torproject.org", + "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": "Anyone with this link can download your files using Tor Browser: ", + "gui_url_label_persistent": "This share will not stop automatically unless a timer is set.

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

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.
Meer informatie.", 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') == ''