From d43a9ac31b99ee9b17f2b309c8054c6e553597b9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 15:07:19 -0700 Subject: [PATCH 01/87] Update Windows deps to python 3.7.0, pywin32 223, and Qt 5.11.1 --- BUILD.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index 7d8350a6..f9c97b46 100644 --- a/BUILD.md +++ b/BUILD.md @@ -77,7 +77,7 @@ Now you should have `dist/OnionShare.pkg`. ### Setting up your dev environment -Download Python 3.6.4, 32-bit (x86) from https://www.python.org/downloads/release/python-364/. I downloaded `python-3.6.4.exe`. When installing it, make sure to check the "Add Python 3.6 to PATH" checkbox on the first page of the installer. +Download Python 3.7.0, 32-bit (x86) from https://www.python.org/downloads/release/python-370/. I downloaded `python-3.7.0.exe`. When installing it, make sure to check the "Add Python 3.7 to PATH" checkbox on the first page of the installer. Open a command prompt, cd to the onionshare folder, and install dependencies with pip: @@ -85,9 +85,9 @@ Open a command prompt, cd to the onionshare folder, and install dependencies wit pip3 install -r install\requirements-windows.txt ``` -Download and install pywin32 (build 221, x86, for python 3.6) from https://sourceforge.net/projects/pywin32/files/pywin32/Build%20221/. I downloaded `pywin32-221.win32-py3.6.exe`. +Download and install pywin32 (build 223, x86, for python 3.7) from https://github.com/mhammond/pywin32/releases/tag/b223. I downloaded `pywin32-223.win32-py3.7.exe`. -Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-3.0.4-online.exe`. 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.11.0. You only need to install the `MSVC 2015 32-bit` component, as well as all of the the `Qt` components, for that that version. +Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-3.0.5-online.exe`. There's no need to login to a Qt account during installation. When you can select components, install the `MSVC 2015 32-bit` component from Qt 5.11.1 (or whatever the latest Qt version is). After that you can try both the CLI and the GUI version of OnionShare: From 33643fc44434c95785c383074a2c0a15acdc9b7d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 15:15:16 -0700 Subject: [PATCH 02/87] Get tor binary from Tor Browser 8.0 --- install/get-tor-osx.py | 2 +- install/get-tor-windows.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index 3c498dfe..5b8078c5 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -37,7 +37,7 @@ import requests def main(): dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5.5/TorBrowser-7.5.5-osx64_en-US.dmg' dmg_filename = 'TorBrowser-7.5.5-osx64_en-US.dmg' - expected_dmg_sha256 = '2b445e4237cdd9be0e71e65f76db5d36f0d6c37532982d642803b57e388e4636' + expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088' # 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 44c4ac23..67e41362 100644 --- a/install/get-tor-windows.py +++ b/install/get-tor-windows.py @@ -33,9 +33,9 @@ import subprocess import requests def main(): - exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5.5/torbrowser-install-7.5.5_en-US.exe' - exe_filename = 'torbrowser-install-7.5.5_en-US.exe' - expected_exe_sha256 = '992f9a6658001c3419ed3695a908eef4fb7feb1cd549389bdacbadb7f8cb08a7' + exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/torbrowser-install-8.0_en-US.exe' + exe_filename = 'torbrowser-install-8.0_en-US.exe' + expected_exe_sha256 = '0682b44eff5877dfc2fe2fdd5b46e678d47adad86d564e7cb6654c5f60eb1ed2' # 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') From 56b4bf08a806bf2664b70ce128776f7c195d34c3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 15:37:48 -0700 Subject: [PATCH 03/87] Update Mac deps to python 3.7.0 and Qt 5.11.1 --- BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index f9c97b46..2cae8f20 100644 --- a/BUILD.md +++ b/BUILD.md @@ -38,11 +38,11 @@ If you find that these instructions don't work for your Linux distribution or ve Install Xcode from the Mac App Store. Once it's installed, run it for the first time to set it up. Also, run this to make sure command line tools are installed: `xcode-select --install`. And finally, open Xcode, go to Preferences > Locations, and make sure under Command Line Tools you select an installed version from the dropdown. (This is required for installing Qt5.) -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`. +Download and install Python 3.7.0 from https://www.python.org/downloads/release/python-370/. I downloaded `python-3.7.0-macosx10.9.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`. +Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-mac-x64-3.0.5-online.dmg`. There's no need to login to a Qt account during installation. When you select components, install the `macOS` component from Qt 5.11.1 (or whatever the latest Qt version is). Now install some python dependencies with pip (note, there's issues building a .app if you install this in a virtualenv): From ba1a33e1ff5bc3e13b23221461aad3900c1aae18 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 15:54:37 -0700 Subject: [PATCH 04/87] Update travis config to use python 3.6+, and to install proper versions of dependencies, and to use bionic instead of trusty --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9010e77a..afbaa887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,15 @@ language: python # sudo: required -dist: trusty +dist: bionic python: - - "3.4" - - "3.5" - "3.6" - "3.6-dev" - "3.7-dev" - "nightly" # command to install dependencies install: - - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8 + - pip install -r install/requirements.txt + - pip install 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 From 06463bab7bd3ce6eea28513cc0668cf9e37a6e43 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 17:42:21 -0700 Subject: [PATCH 05/87] Make separate function for comparing the slug and comparing the shutdown_slug, to prevent 404 errors on the shutdown request --- onionshare/web.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index e3e965da..10c130cb 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -483,7 +483,7 @@ class Web(object): """ Stop the flask web server, from the context of an http request. """ - self.check_slug_candidate(slug_candidate, self.shutdown_slug) + self.check_shutdown_slug_candidate(slug_candidate) self.force_shutdown() return "" @@ -578,15 +578,17 @@ class Web(object): log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - def check_slug_candidate(self, slug_candidate, slug_compare=None): - self.common.log('Web', 'check_slug_candidate: slug_candidate={}, slug_compare={}'.format(slug_candidate, slug_compare)) + def check_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) if self.common.settings.get('public_mode'): abort(404) - else: - if not slug_compare: - slug_compare = self.slug - if not hmac.compare_digest(slug_compare, slug_candidate): - abort(404) + if not hmac.compare_digest(self.slug, slug_candidate): + abort(404) + + def check_shutdown_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) + if not hmac.compare_digest(self.shutdown_slug, slug_candidate): + abort(404) def force_shutdown(self): """ From bc097c738e1ae4ce46bbb01eaced92127fcef14f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 11:19:20 +1000 Subject: [PATCH 06/87] Increment/decrement the upload counters --- onionshare_gui/receive_mode/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 6a8d3835..d414f3b0 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -166,6 +166,12 @@ class ReceiveMode(Mode): Handle REQUEST_UPLOAD_FINISHED event. """ self.uploads.finished(event["data"]["id"]) + # Update the total 'completed uploads' info + self.uploads_completed += 1 + self.update_uploads_completed() + # Update the 'in progress uploads' info + self.uploads_in_progress -= 1 + self.update_uploads_in_progress() def on_reload_settings(self): """ @@ -187,7 +193,7 @@ class ReceiveMode(Mode): def update_uploads_completed(self): """ - Update the 'Downloads completed' info widget. + Update the 'Uploads completed' info widget. """ if self.uploads_completed == 0: image = self.common.get_resource_path('images/share_completed_none.png') @@ -198,7 +204,7 @@ class ReceiveMode(Mode): def update_uploads_in_progress(self): """ - Update the 'Downloads in progress' info widget. + Update the 'Uploads in progress' info widget. """ if self.uploads_in_progress == 0: image = self.common.get_resource_path('images/share_in_progress_none.png') From 35154b8591cc881b054073361921da615f763c5f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 11:51:32 +1000 Subject: [PATCH 07/87] Close the upload widget on reset so that it properly disappears from the Uploads window. --- onionshare_gui/receive_mode/uploads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 09834156..e928bd62 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -290,6 +290,7 @@ class Uploads(QtWidgets.QScrollArea): """ self.common.log('Uploads', 'reset') for upload in self.uploads.values(): + upload.close() self.uploads_layout.removeWidget(upload) self.uploads = {} From 2bded19462ecc36dc0b89293fc523f06662d6429 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 12:59:01 +1000 Subject: [PATCH 08/87] Fix the auto-scrolling to bottom of Download and Upload windows --- onionshare_gui/receive_mode/uploads.py | 10 +++++++--- onionshare_gui/share_mode/downloads.py | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 09834156..1aebe902 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -227,6 +227,7 @@ class Uploads(QtWidgets.QScrollArea): self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) @@ -243,6 +244,12 @@ class Uploads(QtWidgets.QScrollArea): widget.setLayout(layout) self.setWidget(widget) + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + def add(self, upload_id, content_length): """ Add a new upload. @@ -256,9 +263,6 @@ class Uploads(QtWidgets.QScrollArea): self.uploads[upload_id] = upload self.uploads_layout.addWidget(upload) - # Scroll to the bottom - self.vbar.setValue(self.vbar.maximum()) - def update(self, upload_id, progress): """ Update the progress of an upload. diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index f5e8512e..538ddfd0 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -97,6 +97,7 @@ class Downloads(QtWidgets.QScrollArea): self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) @@ -113,6 +114,12 @@ class Downloads(QtWidgets.QScrollArea): widget.setLayout(layout) self.setWidget(widget) + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + def add(self, download_id, total_bytes): """ Add a new download progress bar. @@ -125,9 +132,6 @@ class Downloads(QtWidgets.QScrollArea): self.downloads[download_id] = download self.downloads_layout.addWidget(download.progress_bar) - # Scroll to the bottom - self.vbar.setValue(self.vbar.maximum()) - def update(self, download_id, downloaded_bytes): """ Update the progress of a download progress bar. From 0dc03ecd4cbf9ec2a7ed4ff7ae3d28a77730b6a0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 20:55:54 -0700 Subject: [PATCH 09/87] Refactor all of the threading.Threads into QThreads, and quit them all when canceling the server. When canceling the compression thread, specifically mass a cancel message into the Web and ZipWriter objects to make the bail out on compression early --- onionshare/web.py | 25 ++++++--- onionshare_gui/mode.py | 55 +++++++++----------- onionshare_gui/onion_thread.py | 45 ---------------- onionshare_gui/share_mode/__init__.py | 43 ++++++++-------- onionshare_gui/share_mode/threads.py | 60 ++++++++++++++++++++++ onionshare_gui/threads.py | 74 +++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 106 deletions(-) delete mode 100644 onionshare_gui/onion_thread.py create mode 100644 onionshare_gui/share_mode/threads.py create mode 100644 onionshare_gui/threads.py diff --git a/onionshare/web.py b/onionshare/web.py index 10c130cb..e24e4665 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -104,6 +104,8 @@ class Web(object): self.file_info = [] self.zip_filename = None self.zip_filesize = None + self.zip_writer = None + self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -534,14 +536,20 @@ class Web(object): self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - # zip up the files and folders - z = ZipWriter(self.common, processed_size_callback=processed_size_callback) + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.zip_filename = self.zip_writer.zip_filename for info in self.file_info['files']: - z.add_file(info['filename']) + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return + for info in self.file_info['dirs']: - z.add_dir(info['filename']) - z.close() - self.zip_filename = z.zip_filename + self.zip_writer.add_dir(info['filename']) + + self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) def _safe_select_jinja_autoescape(self, filename): @@ -653,6 +661,7 @@ class ZipWriter(object): """ def __init__(self, common, zip_filename=None, processed_size_callback=None): self.common = common + self.cancel_compression = False if zip_filename: self.zip_filename = zip_filename @@ -681,6 +690,10 @@ class ZipWriter(object): dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' for dirpath, dirnames, filenames in os.walk(filename): for f in filenames: + # Canceling early? + if self.cancel_compression: + return + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 418afffd..feb2f5b6 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -17,15 +17,13 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import time -import threading from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer from .server_status import ServerStatus -from .onion_thread import OnionThread +from .threads import OnionThread from .widgets import Alert class Mode(QtWidgets.QWidget): @@ -56,6 +54,10 @@ class Mode(QtWidgets.QWidget): # The web object gets created in init() self.web = None + # Threads start out as None + self.onion_thread = None + self.web_thread = None + # Server status self.server_status = ServerStatus(self.common, self.qtapp, self.app) self.server_status.server_started.connect(self.start_server) @@ -138,34 +140,11 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') - # Start the onion service in a new thread - def start_onion_service(self): - # Choose a port for the web app - self.app.choose_port() - - # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) - t.daemon = True - t.start() - - # Wait for the web app slug to generate before continuing - if not self.common.settings.get('public_mode'): - while self.web.slug == None: - time.sleep(0.1) - - # Now start the onion service - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except Exception as e: - self.starting_server_error.emit(e.args[0]) - return - self.common.log('Mode', 'start_server', 'Starting an onion thread') - self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) - self.t.daemon = True - self.t.start() + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() def start_server_custom(self): """ @@ -243,10 +222,22 @@ class Mode(QtWidgets.QWidget): """ Cancel the server while it is preparing to start """ - if self.t: - self.t.quit() + self.cancel_server_custom() + + if self.onion_thread: + self.common.log('Mode', 'cancel_server: quitting onion thread') + self.onion_thread.quit() + if self.web_thread: + self.common.log('Mode', 'cancel_server: quitting web thread') + self.web_thread.quit() self.stop_server() + def cancel_server_custom(self): + """ + Add custom initialization here. + """ + pass + def stop_server(self): """ Stop the onionshare server. diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py deleted file mode 100644 index 0a25e891..00000000 --- a/onionshare_gui/onion_thread.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -from PyQt5 import QtCore - -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, common, function, kwargs=None): - super(OnionThread, self).__init__() - - self.common = common - - self.common.log('OnionThread', '__init__') - self.function = function - if not kwargs: - self.kwargs = {} - else: - self.kwargs = kwargs - - def run(self): - self.common.log('OnionThread', 'run') - - self.function(**self.kwargs) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 37315bbe..d43fe99f 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -17,7 +17,6 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import threading import os from PyQt5 import QtCore, QtWidgets, QtGui @@ -28,6 +27,7 @@ from onionshare.web import Web from .file_selection import FileSelection from .downloads import Downloads +from .threads import CompressThread from ..mode import Mode from ..widgets import Alert @@ -39,6 +39,9 @@ class ShareMode(Mode): """ Custom initialization for ReceiveMode. """ + # Threads start out as None + self.compress_thread = None + # Create the Web object self.web = Web(self.common, True, False) @@ -161,28 +164,13 @@ class ShareMode(Mode): self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) - # Prepare the files for sending in a new thread - def finish_starting_server(self): - # Prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - - try: - self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(self.web.zip_filename) - - # Only continue if the server hasn't been canceled - if self.server_status.status != self.server_status.STATUS_STOPPED: - self.starting_server_step3.emit() - self.start_server_finished.emit() - except OSError as e: - self.starting_server_error.emit(e.strerror) - return - - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() + # prepare the files for sending in a new thread + self.compress_thread = CompressThread(self) + self.compress_thread.success.connect(self.starting_server_step3.emit) + self.compress_thread.success.connect(self.start_server_finished.emit) + self.compress_thread.error.connect(self.starting_server_error.emit) + self.server_status.server_canceled.connect(self.compress_thread.cancel) + self.compress_thread.start() def start_server_step3_custom(self): """ @@ -222,6 +210,15 @@ class ShareMode(Mode): self.update_downloads_in_progress() self.file_selection.file_list.adjustSize() + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') + self.compress_thread.cancel() + self.compress_thread.quit() + def handle_tor_broke_custom(self): """ Connection to Tor broke. diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py new file mode 100644 index 00000000..50789049 --- /dev/null +++ b/onionshare_gui/share_mode/threads.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from PyQt5 import QtCore + + +class CompressThread(QtCore.QThread): + """ + Compresses files to be shared + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(CompressThread, self).__init__() + self.mode = mode + self.mode.common.log('CompressThread', '__init__') + + # prepare files to share + def set_processed_size(self, x): + if self.mode._zip_progress_bar != None: + self.mode._zip_progress_bar.update_processed_size_signal.emit(x) + + def run(self): + self.mode.common.log('CompressThread', 'run') + + try: + if self.mode.web.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + self.success.emit() + else: + # Cancelled + pass + + self.mode.app.cleanup_filenames.append(self.mode.web.zip_filename) + except OSError as e: + self.error.emit(e.strerror) + + def cancel(self): + self.mode.common.log('CompressThread', 'cancel') + + # Let the Web and ZipWriter objects know that we're canceling compression early + self.mode.web.cancel_compression = True + if self.mode.web.zip_writer: + self.mode.web.zip_writer.cancel_compression = True diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py new file mode 100644 index 00000000..3c99d395 --- /dev/null +++ b/onionshare_gui/threads.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import time +from PyQt5 import QtCore + + +class OnionThread(QtCore.QThread): + """ + Starts the onion service, and waits for it to finish + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(OnionThread, self).__init__() + self.mode = mode + self.mode.common.log('OnionThread', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + self.mode.common.log('OnionThread', 'run') + + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + + self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') + + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + +class WebThread(QtCore.QThread): + """ + Starts the web service + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(WebThread, self).__init__() + self.mode = mode + self.mode.common.log('WebThread', '__init__') + + def run(self): + self.mode.common.log('WebThread', 'run') + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('slug')) From afdcfffc1e4b7ebe4b44164731d1900ea5ca3328 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 17 Sep 2018 21:02:39 -0700 Subject: [PATCH 10/87] Oops, update URL and filename for Tor Browser 8.0 dmg as well as the sha256 checksum --- install/get-tor-osx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index 5b8078c5..452fadf3 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -35,8 +35,8 @@ import subprocess import requests def main(): - dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.5.5/TorBrowser-7.5.5-osx64_en-US.dmg' - dmg_filename = 'TorBrowser-7.5.5-osx64_en-US.dmg' + dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/TorBrowser-8.0-osx64_en-US.dmg' + dmg_filename = 'TorBrowser-8.0-osx64_en-US.dmg' expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088' # Build paths From 0cbbd5a923eb19fd363daa671a124887d95a5f4f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 15:35:26 +1000 Subject: [PATCH 11/87] Re-add the python dependencies necessary for v3 onion support, which got lost in the merge --- install/requirements-windows.txt | 4 ++++ install/requirements.txt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/install/requirements-windows.txt b/install/requirements-windows.txt index 005acc9f..ee593b31 100644 --- a/install/requirements-windows.txt +++ b/install/requirements-windows.txt @@ -2,6 +2,7 @@ altgraph==0.16.1 certifi==2018.8.24 chardet==3.0.4 click==6.7 +cryptography==2.3.1 Flask==1.0.2 future==0.16.0 idna==2.7 @@ -16,6 +17,9 @@ pyparsing==2.2.0 pypiwin32==223 PyQt5==5.11.2 PySocks==1.6.8 +pynacl==1.2.1 +pycrypto==2.6.1 +pysha3==1.0.2 python-dateutil==2.7.3 pywin32==223 requests==2.19.1 diff --git a/install/requirements.txt b/install/requirements.txt index 16179eb7..567b62e5 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -2,6 +2,7 @@ altgraph==0.16.1 certifi==2018.8.24 chardet==3.0.4 click==6.7 +cryptography==2.3.1 Flask==1.0.2 future==0.16.0 idna==2.7 @@ -14,6 +15,9 @@ PyInstaller==3.4 PyQt5==5.11.2 PyQt5-sip==4.19.12 PySocks==1.6.8 +pycrypto==2.6.1 +pynacl==1.2.1 +pysha3==1.0.2 requests==2.19.1 sip==4.19.8 stem==1.6.0 From 8253990eed2cac8d28321647ea42eb116be8ee75 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 18 Sep 2018 15:36:00 +1000 Subject: [PATCH 12/87] Update the path to the Tor data from inside the mounted .dmg, and the libevent version name --- install/get-tor-osx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index 845f887c..1d2c6f56 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -42,7 +42,7 @@ def main(): # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) working_path = os.path.join(root_path, 'build', 'tor') - dmg_tor_path = os.path.join('/Volumes', 'Tor Browser', 'TorBrowser.app', 'Contents') + dmg_tor_path = os.path.join('/Volumes', 'Tor Browser', 'Tor Browser.app', 'Contents') dmg_path = os.path.join(working_path, dmg_filename) dist_path = os.path.join(root_path, 'dist', 'OnionShare.app', 'Contents') @@ -88,7 +88,7 @@ def main(): shutil.copyfile(os.path.join(dmg_tor_path, 'Resources', 'TorBrowser', 'Tor', 'geoip6'), os.path.join(dist_path, 'Resources', 'Tor', 'geoip6')) os.chmod(os.path.join(dist_path, 'Resources', 'Tor', 'tor'), 0o755) shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'tor.real'), os.path.join(dist_path, 'MacOS', 'Tor', 'tor.real')) - shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'libevent-2.0.5.dylib'), os.path.join(dist_path, 'MacOS', 'Tor', 'libevent-2.0.5.dylib')) + shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'libevent-2.1.6.dylib'), os.path.join(dist_path, 'MacOS', 'Tor', 'libevent-2.1.6.dylib')) os.chmod(os.path.join(dist_path, 'MacOS', 'Tor', 'tor.real'), 0o755) # obfs4proxy binary shutil.copyfile(os.path.join(dmg_tor_path, 'MacOS', 'Tor', 'PluggableTransports', 'obfs4proxy'), os.path.join(dist_path, 'Resources', 'Tor', 'obfs4proxy')) From c146d1a352b0cc7d30b81ac80510d6515eb9948a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:39:09 -0700 Subject: [PATCH 13/87] We shouldn't call CompressThread.cancel() there because it's already called in a signal --- onionshare_gui/share_mode/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index d43fe99f..65ce1d52 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -216,7 +216,6 @@ class ShareMode(Mode): """ if self.compress_thread: self.common.log('OnionShareGui', 'cancel_server: quitting compress thread') - self.compress_thread.cancel() self.compress_thread.quit() def handle_tor_broke_custom(self): From 1dab03abbde0686be4aedc67477e13e53c31010c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 13:42:13 -0700 Subject: [PATCH 14/87] Make Web.set_file_info return False on cancel --- onionshare/web.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index e24e4665..dc0effdb 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -544,13 +544,15 @@ class Web(object): # Canceling early? if self.cancel_compression: self.zip_writer.close() - return + return False for info in self.file_info['dirs']: - self.zip_writer.add_dir(info['filename']) + if not self.zip_writer.add_dir(info['filename']): + return False self.zip_writer.close() self.zip_filesize = os.path.getsize(self.zip_filename) + return True def _safe_select_jinja_autoescape(self, filename): if filename is None: @@ -692,8 +694,8 @@ class ZipWriter(object): for f in filenames: # Canceling early? if self.cancel_compression: - return - + return False + full_filename = os.path.join(dirpath, f) if not os.path.islink(full_filename): arc_filename = full_filename[len(dir_to_strip):] @@ -701,6 +703,8 @@ class ZipWriter(object): self._size += os.path.getsize(full_filename) self.processed_size_callback(self._size) + return True + def close(self): """ Close the zip archive. From ed6d2ee3184ff109b642373ada16c8c04b4b443c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 15:35:10 -0700 Subject: [PATCH 15/87] Generated a new requirements.txt by installing the latest version of these pip packages: pip3 install certifi cryptography Flask PyInstaller PyQt5 PySocks pycrypto pynacl requests stem Then running "pip3 freeze". Also, deleted requirements-windows.txt. Mac and Windows can share. --- install/requirements-windows.txt | 30 ------------------------------ install/requirements.txt | 10 ++++++---- 2 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 install/requirements-windows.txt diff --git a/install/requirements-windows.txt b/install/requirements-windows.txt deleted file mode 100644 index ee593b31..00000000 --- a/install/requirements-windows.txt +++ /dev/null @@ -1,30 +0,0 @@ -altgraph==0.16.1 -certifi==2018.8.24 -chardet==3.0.4 -click==6.7 -cryptography==2.3.1 -Flask==1.0.2 -future==0.16.0 -idna==2.7 -itsdangerous==0.24 -Jinja2==2.10 -macholib==1.11 -MarkupSafe==1.0 -packaging==17.1 -pefile==2018.8.8 -PyInstaller==3.4 -pyparsing==2.2.0 -pypiwin32==223 -PyQt5==5.11.2 -PySocks==1.6.8 -pynacl==1.2.1 -pycrypto==2.6.1 -pysha3==1.0.2 -python-dateutil==2.7.3 -pywin32==223 -requests==2.19.1 -sip==4.19.8 -six==1.11.0 -stem==1.6.0 -urllib3==1.23 -Werkzeug==0.14.1 diff --git a/install/requirements.txt b/install/requirements.txt index 567b62e5..32ec6887 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -1,5 +1,7 @@ altgraph==0.16.1 +asn1crypto==0.24.0 certifi==2018.8.24 +cffi==1.11.5 chardet==3.0.4 click==6.7 cryptography==2.3.1 @@ -11,15 +13,15 @@ Jinja2==2.10 macholib==1.11 MarkupSafe==1.0 pefile==2018.8.8 +pycparser==2.18 +pycryptodome==3.6.6 PyInstaller==3.4 +PyNaCl==1.2.1 PyQt5==5.11.2 PyQt5-sip==4.19.12 PySocks==1.6.8 -pycrypto==2.6.1 -pynacl==1.2.1 -pysha3==1.0.2 requests==2.19.1 -sip==4.19.8 +six==1.11.0 stem==1.6.0 urllib3==1.23 Werkzeug==0.14.1 From 3afa9b6b191c99b0b10737bdce986d72e59b95b4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 15:37:33 -0700 Subject: [PATCH 16/87] For Mac build instructions, update a command to use Python 3.7 instead of 3.6, and install pip dependencies without sudo. For Windows, install requirements.txt instead of requirements-windows.txt. Also pywin32 is no longer a dependency, so remove it from build instructions. --- BUILD.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 9d5f73c3..51f5cadd 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,14 +42,14 @@ Install Xcode from the Mac App Store. Once it's installed, run it for the first Download and install Python 3.7.0 from https://www.python.org/downloads/release/python-370/. I downloaded `python-3.7.0-macosx10.9.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. +You may also need to run the command `/Applications/Python\ 3.7/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.5-online.dmg`. There's no need to login to a Qt account during installation. When you select components, install the `macOS` component from Qt 5.11.1 (or whatever the latest Qt version is). Now install some python dependencies with pip (note, there's issues building a .app if you install this in a virtualenv): ```sh -sudo pip3 install -r install/requirements.txt +pip3 install -r install/requirements.txt ``` You can run both the CLI and GUI versions of OnionShare without building an bundle: @@ -84,11 +84,9 @@ Download Python 3.7.0, 32-bit (x86) from https://www.python.org/downloads/releas Open a command prompt, cd to the onionshare folder, and install dependencies with pip: ```cmd -pip3 install -r install\requirements-windows.txt +pip install -r install\requirements.txt ``` -Download and install pywin32 (build 223, x86, for python 3.7) from https://github.com/mhammond/pywin32/releases/tag/b223. I downloaded `pywin32-223.win32-py3.7.exe`. - Download and install Qt5 from https://www.qt.io/download-open-source/. I downloaded `qt-unified-windows-x86-3.0.5-online.exe`. There's no need to login to a Qt account during installation. When you can select components, install the `MSVC 2015 32-bit` component from Qt 5.11.1 (or whatever the latest Qt version is). After that you can try both the CLI and the GUI version of OnionShare: From 0ec2e67f2f5e59f3e21a48cdf2dc3ff036b0eb78 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:05:54 -0700 Subject: [PATCH 17/87] Update NSIS file to no longer have to specify each individual file (omg I should have done this foreever ago) --- install/onionshare.nsi | 394 +---------------------------------------- 1 file changed, 2 insertions(+), 392 deletions(-) diff --git a/install/onionshare.nsi b/install/onionshare.nsi index 134ff8d2..f0b28535 100644 --- a/install/onionshare.nsi +++ b/install/onionshare.nsi @@ -59,203 +59,7 @@ FunctionEnd Section "install" SetOutPath "$INSTDIR" File "onionshare.ico" - File "${BINPATH}\api-ms-win-core-console-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-datetime-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-debug-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-errorhandling-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-file-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-file-l1-2-0.dll" - File "${BINPATH}\api-ms-win-core-file-l2-1-0.dll" - File "${BINPATH}\api-ms-win-core-handle-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-heap-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-interlocked-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-libraryloader-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-localization-l1-2-0.dll" - File "${BINPATH}\api-ms-win-core-memory-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-namedpipe-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-processenvironment-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-processthreads-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-processthreads-l1-1-1.dll" - File "${BINPATH}\api-ms-win-core-profile-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-rtlsupport-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-string-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-synch-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-synch-l1-2-0.dll" - File "${BINPATH}\api-ms-win-core-sysinfo-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-timezone-l1-1-0.dll" - File "${BINPATH}\api-ms-win-core-util-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-conio-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-convert-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-environment-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-filesystem-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-heap-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-locale-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-math-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-multibyte-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-process-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-runtime-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-stdio-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-string-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-time-l1-1-0.dll" - File "${BINPATH}\api-ms-win-crt-utility-l1-1-0.dll" - File "${BINPATH}\base_library.zip" - File "${BINPATH}\mfc140u.dll" - File "${BINPATH}\MSVCP140.dll" - File "${BINPATH}\onionshare-gui.exe" - File "${BINPATH}\onionshare-gui.exe.manifest" - File "${BINPATH}\pyexpat.pyd" - File "${BINPATH}\PyQt5.Qt.pyd" - File "${BINPATH}\PyQt5.QtCore.pyd" - File "${BINPATH}\PyQt5.QtGui.pyd" - File "${BINPATH}\PyQt5.QtPrintSupport.pyd" - File "${BINPATH}\PyQt5.QtWidgets.pyd" - File "${BINPATH}\python3.dll" - File "${BINPATH}\python36.dll" - File "${BINPATH}\pythoncom36.dll" - File "${BINPATH}\pywintypes36.dll" - File "${BINPATH}\Qt5Core.dll" - File "${BINPATH}\Qt5Gui.dll" - File "${BINPATH}\Qt5PrintSupport.dll" - File "${BINPATH}\Qt5Svg.dll" - File "${BINPATH}\Qt5Widgets.dll" - File "${BINPATH}\select.pyd" - File "${BINPATH}\sip.pyd" - File "${BINPATH}\ucrtbase.dll" - File "${BINPATH}\unicodedata.pyd" - File "${BINPATH}\VCRUNTIME140.dll" - File "${BINPATH}\win32api.pyd" - File "${BINPATH}\win32com.shell.shell.pyd" - File "${BINPATH}\win32trace.pyd" - File "${BINPATH}\win32ui.pyd" - File "${BINPATH}\win32wnet.pyd" - File "${BINPATH}\_asyncio.pyd" - File "${BINPATH}\_bz2.pyd" - File "${BINPATH}\_ctypes.pyd" - File "${BINPATH}\_decimal.pyd" - File "${BINPATH}\_hashlib.pyd" - File "${BINPATH}\_lzma.pyd" - File "${BINPATH}\_multiprocessing.pyd" - File "${BINPATH}\_overlapped.pyd" - File "${BINPATH}\_socket.pyd" - File "${BINPATH}\_ssl.pyd" - File "${BINPATH}\_win32sysloader.pyd" - - SetOutPath "$INSTDIR\Include" - File "${BINPATH}\Include\pyconfig.h" - - SetOutPath "$INSTDIR\lib2to3" - File "${BINPATH}\lib2to3\Grammar.txt" - File "${BINPATH}\lib2to3\Grammar3.6.2.candidate.2.pickle" - File "${BINPATH}\lib2to3\Grammar3.6.2.final.0.pickle" - File "${BINPATH}\lib2to3\Grammar3.6.3.candidate.1.pickle" - File "${BINPATH}\lib2to3\Grammar3.6.3.final.0.pickle" - File "${BINPATH}\lib2to3\Grammar3.6.4.candidate.1.pickle" - File "${BINPATH}\lib2to3\Grammar3.6.4.final.0.pickle" - File "${BINPATH}\lib2to3\PatternGrammar.txt" - File "${BINPATH}\lib2to3\PatternGrammar3.6.2.candidate.2.pickle" - File "${BINPATH}\lib2to3\PatternGrammar3.6.2.final.0.pickle" - File "${BINPATH}\lib2to3\PatternGrammar3.6.3.candidate.1.pickle" - File "${BINPATH}\lib2to3\PatternGrammar3.6.3.final.0.pickle" - File "${BINPATH}\lib2to3\PatternGrammar3.6.4.candidate.1.pickle" - File "${BINPATH}\lib2to3\PatternGrammar3.6.4.final.0.pickle" - - SetOutPath "$INSTDIR\lib2to3\tests\data" - File "${BINPATH}\lib2to3\tests\data\README" - - SetOutPath "$INSTDIR\licenses" - File "${BINPATH}\licenses\license-obfs4.txt" - File "${BINPATH}\licenses\license-onionshare.txt" - File "${BINPATH}\licenses\license-tor.txt" - File "${BINPATH}\licenses\readme.txt" - - SetOutPath "$INSTDIR\PyQt5\Qt\bin" - File "${BINPATH}\PyQt5\Qt\bin\qt.conf" - - SetOutPath "$INSTDIR\PyQt5\Qt\plugins\iconengines" - File "${BINPATH}\PyQt5\Qt\plugins\iconengines\qsvgicon.dll" - - SetOutPath "$INSTDIR\PyQt5\Qt\plugins\imageformats" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qgif.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qicns.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qico.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qjpeg.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qsvg.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qtga.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qtiff.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qwbmp.dll" - File "${BINPATH}\PyQt5\Qt\plugins\imageformats\qwebp.dll" - - SetOutPath "$INSTDIR\PyQt5\Qt\plugins\platforms" - File "${BINPATH}\PyQt5\Qt\plugins\platforms\qminimal.dll" - File "${BINPATH}\PyQt5\Qt\plugins\platforms\qoffscreen.dll" - File "${BINPATH}\PyQt5\Qt\plugins\platforms\qwindows.dll" - - SetOutPath "$INSTDIR\PyQt5\Qt\plugins\printsupport" - File "${BINPATH}\PyQt5\Qt\plugins\printsupport\windowsprintersupport.dll" - - SetOutPath "$INSTDIR\share" - File "${BINPATH}\share\torrc_template" - File "${BINPATH}\share\torrc_template-windows" - File "${BINPATH}\share\torrc_template-obfs4" - File "${BINPATH}\share\torrc_template-meek_lite_azure" - File "${BINPATH}\share\version.txt" - File "${BINPATH}\share\wordlist.txt" - - SetOutPath "$INSTDIR\share\html" - File "${BINPATH}\share\html\404.html" - File "${BINPATH}\share\html\denied.html" - File "${BINPATH}\share\html\index.html" - - SetOutPath "$INSTDIR\share\images" - 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\download_window_gray.png" - File "${BINPATH}\share\images\download_window_green.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\web_file.png" - File "${BINPATH}\share\images\web_folder.png" - - SetOutPath "$INSTDIR\share\locale" - File "${BINPATH}\share\locale\cs.json" - File "${BINPATH}\share\locale\de.json" - File "${BINPATH}\share\locale\en.json" - File "${BINPATH}\share\locale\eo.json" - File "${BINPATH}\share\locale\es.json" - File "${BINPATH}\share\locale\fi.json" - File "${BINPATH}\share\locale\fr.json" - File "${BINPATH}\share\locale\it.json" - File "${BINPATH}\share\locale\nl.json" - File "${BINPATH}\share\locale\no.json" - File "${BINPATH}\share\locale\pt.json" - File "${BINPATH}\share\locale\ru.json" - File "${BINPATH}\share\locale\tr.json" - - SetOutPath "$INSTDIR\tor\Data\Tor" - File "${BINPATH}\tor\Data\Tor\geoip" - File "${BINPATH}\tor\Data\Tor\geoip6" - - SetOutPath "$INSTDIR\tor\Tor" - File "${BINPATH}\tor\Tor\libeay32.dll" - File "${BINPATH}\tor\Tor\libevent-2-0-5.dll" - File "${BINPATH}\tor\Tor\libevent_core-2-0-5.dll" - File "${BINPATH}\tor\Tor\libevent_extra-2-0-5.dll" - File "${BINPATH}\tor\Tor\libgcc_s_sjlj-1.dll" - File "${BINPATH}\tor\Tor\libssp-0.dll" - File "${BINPATH}\tor\Tor\obfs4proxy.exe" - File "${BINPATH}\tor\Tor\ssleay32.dll" - File "${BINPATH}\tor\Tor\tor.exe" - File "${BINPATH}\tor\Tor\zlib1.dll" + File /a /r "${BINPATH}\" # uninstaller !ifndef INNER @@ -299,201 +103,7 @@ FunctionEnd Delete "$SMPROGRAMS\${APPNAME}.lnk" # remove files - Delete "$INSTDIR\api-ms-win-core-console-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-datetime-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-debug-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-errorhandling-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-file-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-file-l1-2-0.dll" - Delete "$INSTDIR\api-ms-win-core-file-l2-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-handle-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-heap-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-interlocked-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-libraryloader-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-localization-l1-2-0.dll" - Delete "$INSTDIR\api-ms-win-core-memory-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-namedpipe-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-processenvironment-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-processthreads-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-processthreads-l1-1-1.dll" - Delete "$INSTDIR\api-ms-win-core-profile-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-rtlsupport-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-string-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-synch-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-synch-l1-2-0.dll" - Delete "$INSTDIR\api-ms-win-core-sysinfo-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-timezone-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-core-util-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-conio-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-convert-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-environment-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-filesystem-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-heap-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-locale-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-math-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-multibyte-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-process-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-runtime-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-stdio-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-string-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-time-l1-1-0.dll" - Delete "$INSTDIR\api-ms-win-crt-utility-l1-1-0.dll" - Delete "$INSTDIR\base_library.zip" - Delete "$INSTDIR\Include\pyconfig.h" - Delete "$INSTDIR\lib2to3\Grammar.txt" - Delete "$INSTDIR\lib2to3\Grammar3.6.2.candidate.2.pickle" - Delete "$INSTDIR\lib2to3\Grammar3.6.2.final.0.pickle" - Delete "$INSTDIR\lib2to3\Grammar3.6.3.candidate.1.pickle" - Delete "$INSTDIR\lib2to3\Grammar3.6.3.final.0.pickle" - Delete "$INSTDIR\lib2to3\Grammar3.6.4.candidate.1.pickle" - Delete "$INSTDIR\lib2to3\Grammar3.6.4.final.0.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar.txt" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.2.candidate.2.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.2.final.0.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.3.candidate.1.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.3.final.0.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.4.candidate.1.pickle" - Delete "$INSTDIR\lib2to3\PatternGrammar3.6.4.final.0.pickle" - Delete "$INSTDIR\lib2to3\tests" - Delete "$INSTDIR\lib2to3\tests\data" - Delete "$INSTDIR\lib2to3\tests\data\README" - Delete "$INSTDIR\licenses\license-obfs4.txt" - Delete "$INSTDIR\licenses\license-onionshare.txt" - Delete "$INSTDIR\licenses\license-tor.txt" - Delete "$INSTDIR\licenses\readme.txt" - Delete "$INSTDIR\mfc140u.dll" - Delete "$INSTDIR\MSVCP140.dll" - Delete "$INSTDIR\onionshare-gui.exe" - Delete "$INSTDIR\onionshare-gui.exe.manifest" - Delete "$INSTDIR\pyexpat.pyd" - Delete "$INSTDIR\PyQt5\Qt\bin\qt.conf" - Delete "$INSTDIR\PyQt5\Qt\plugins\iconengines\qsvgicon.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qgif.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qicns.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qico.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qjpeg.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qsvg.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qtga.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qtiff.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qwbmp.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\imageformats\qwebp.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\platforms\qminimal.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\platforms\qoffscreen.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\platforms\qwindows.dll" - Delete "$INSTDIR\PyQt5\Qt\plugins\printsupport\windowsprintersupport.dll" - Delete "$INSTDIR\PyQt5.Qt.pyd" - Delete "$INSTDIR\PyQt5.QtCore.pyd" - Delete "$INSTDIR\PyQt5.QtGui.pyd" - Delete "$INSTDIR\PyQt5.QtPrintSupport.pyd" - Delete "$INSTDIR\PyQt5.QtWidgets.pyd" - Delete "$INSTDIR\python3.dll" - Delete "$INSTDIR\python36.dll" - Delete "$INSTDIR\pythoncom36.dll" - Delete "$INSTDIR\pywintypes36.dll" - Delete "$INSTDIR\Qt5Core.dll" - Delete "$INSTDIR\Qt5Gui.dll" - Delete "$INSTDIR\Qt5PrintSupport.dll" - Delete "$INSTDIR\Qt5Svg.dll" - Delete "$INSTDIR\Qt5Widgets.dll" - Delete "$INSTDIR\select.pyd" - Delete "$INSTDIR\share\html\404.html" - Delete "$INSTDIR\share\html\denied.html" - Delete "$INSTDIR\share\html\index.html" - 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\download_window_gray.png" - Delete "$INSTDIR\share\images\download_window_green.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\web_file.png" - Delete "$INSTDIR\share\images\web_folder.png" - Delete "$INSTDIR\share\locale\cs.json" - Delete "$INSTDIR\share\locale\de.json" - Delete "$INSTDIR\share\locale\en.json" - Delete "$INSTDIR\share\locale\eo.json" - Delete "$INSTDIR\share\locale\es.json" - Delete "$INSTDIR\share\locale\fi.json" - Delete "$INSTDIR\share\locale\fr.json" - Delete "$INSTDIR\share\locale\it.json" - Delete "$INSTDIR\share\locale\nl.json" - Delete "$INSTDIR\share\locale\no.json" - Delete "$INSTDIR\share\locale\pt.json" - Delete "$INSTDIR\share\locale\ru.json" - Delete "$INSTDIR\share\locale\tr.json" - Delete "$INSTDIR\share\torrc_template" - Delete "$INSTDIR\share\torrc_template-windows" - Delete "$INSTDIR\share\torrc_template-obfs4" - Delete "$INSTDIR\share\torrc_template-meek_lite_azure" - Delete "$INSTDIR\share\version.txt" - Delete "$INSTDIR\share\wordlist.txt" - Delete "$INSTDIR\sip.pyd" - Delete "$INSTDIR\tor\Data\Tor\geoip" - Delete "$INSTDIR\tor\Data\Tor\geoip6" - Delete "$INSTDIR\tor\Tor\libeay32.dll" - Delete "$INSTDIR\tor\Tor\libevent-2-0-5.dll" - Delete "$INSTDIR\tor\Tor\libevent_core-2-0-5.dll" - Delete "$INSTDIR\tor\Tor\libevent_extra-2-0-5.dll" - Delete "$INSTDIR\tor\Tor\libgcc_s_sjlj-1.dll" - Delete "$INSTDIR\tor\Tor\libssp-0.dll" - Delete "$INSTDIR\tor\Tor\obfs4proxy.exe" - Delete "$INSTDIR\tor\Tor\ssleay32.dll" - Delete "$INSTDIR\tor\Tor\tor.exe" - Delete "$INSTDIR\tor\Tor\zlib1.dll" - Delete "$INSTDIR\ucrtbase.dll" - Delete "$INSTDIR\unicodedata.pyd" - Delete "$INSTDIR\VCRUNTIME140.dll" - Delete "$INSTDIR\win32api.pyd" - Delete "$INSTDIR\win32com.shell.shell.pyd" - Delete "$INSTDIR\win32trace.pyd" - Delete "$INSTDIR\win32ui.pyd" - Delete "$INSTDIR\win32wnet.pyd" - Delete "$INSTDIR\_asyncio.pyd" - Delete "$INSTDIR\_bz2.pyd" - Delete "$INSTDIR\_ctypes.pyd" - Delete "$INSTDIR\_decimal.pyd" - Delete "$INSTDIR\_hashlib.pyd" - Delete "$INSTDIR\_lzma.pyd" - Delete "$INSTDIR\_multiprocessing.pyd" - Delete "$INSTDIR\_overlapped.pyd" - Delete "$INSTDIR\_socket.pyd" - Delete "$INSTDIR\_ssl.pyd" - Delete "$INSTDIR\_win32sysloader.pyd" - - Delete "$INSTDIR\onionshare.ico" - Delete "$INSTDIR\uninstall.exe" - - rmDir "$INSTDIR\Include" - rmDir "$INSTDIR\lib2to3\tests\data" - rmDir "$INSTDIR\lib2to3\tests" - rmDir "$INSTDIR\lib2to3" - rmDir "$INSTDIR\licenses" - rmDir "$INSTDIR\PyQt5\Qt\bin" - rmDir "$INSTDIR\PyQt5\Qt\plugins\iconengines" - rmDir "$INSTDIR\PyQt5\Qt\plugins\imageformats" - rmDir "$INSTDIR\PyQt5\Qt\plugins\platforms" - rmDir "$INSTDIR\PyQt5\Qt\plugins\printsupport" - rmDir "$INSTDIR\PyQt5\Qt\plugins" - rmDir "$INSTDIR\PyQt5\Qt" - rmDir "$INSTDIR\PyQt5" - rmDir "$INSTDIR\share\html" - rmDir "$INSTDIR\share\images" - rmDir "$INSTDIR\share\locale" - rmDir "$INSTDIR\share" - rmDir "$INSTDIR\tor\Data\Tor" - rmDir "$INSTDIR\tor\Data" - rmDir "$INSTDIR\tor\Tor" - rmDir "$INSTDIR\tor" - rmDir "$INSTDIR" + RMDir /r $INSTDIR # remove uninstaller information from the registry DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" From 2f1e86665205e91d68431ed47a7c707711033ee3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:19:07 -0700 Subject: [PATCH 18/87] While I'm at it, bump the version to 2.0.dev --- share/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/version.txt b/share/version.txt index 3a3cd8cc..22351bb8 100644 --- a/share/version.txt +++ b/share/version.txt @@ -1 +1 @@ -1.3.1 +2.0.dev From 901ecb6adca97086c3079c7c6fa4ff0655acfd9a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:28:54 -0700 Subject: [PATCH 19/87] Set self.cancel_compression to false in the set_file_info() function instead of Web's constructor, so it gets reset every time --- onionshare/web.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onionshare/web.py b/onionshare/web.py index dc0effdb..38ad398e 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -105,7 +105,6 @@ class Web(object): self.zip_filename = None self.zip_filesize = None self.zip_writer = None - self.cancel_compression = False self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), @@ -518,6 +517,8 @@ class Web(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ + self.cancel_compression = False + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: From f3998d2f3d3dbf2c0168578e5fd6c5982c9d24c0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 16:58:16 -0700 Subject: [PATCH 20/87] Remove some debug logs --- onionshare/onion.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index a10dc53c..9aef3191 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -457,7 +457,6 @@ class Onion(object): # key_type = "ED25519-V3" else: raise TorErrorProtocolError(strings._('error_invalid_private_key')) - self.common.log('Onion', 'Starting a hidden service with a saved private key') else: # Work out if we can support v3 onion services, which are preferred if Version(self.tor_version) >= Version('0.3.2.9') and not self.settings.get('use_legacy_v2_onions'): @@ -467,7 +466,6 @@ class Onion(object): # fall back to v2 onion services key_type = "RSA1024" key_content = onionkey.generate_v2_private_key()[0] - self.common.log('Onion', 'Starting a hidden service with a new private key') # v3 onions don't yet support basic auth. Our ticket: # https://github.com/micahflee/onionshare/issues/697 From 739d48a183ba71de5814f99af830beacd3fdd36a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 17:17:25 -0700 Subject: [PATCH 21/87] Properly handle exceptions in CLI, and pass the actual exception message in TorErrorProtocolError exceptions --- onionshare/__init__.py | 4 ++++ onionshare/onion.py | 31 ++++++++++++++++--------------- onionshare/onionshare.py | 1 + share/locale/en.json | 3 ++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index becca93f..51210b6b 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -111,6 +111,10 @@ def main(cwd=None): except KeyboardInterrupt: print("") sys.exit() + except (TorTooOld, TorErrorProtocolError) as e: + print("") + print(e.args[0]) + sys.exit() # Prepare files to share print(strings._("preparing_files")) diff --git a/onionshare/onion.py b/onionshare/onion.py index 9aef3191..d213cd6a 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -443,18 +443,18 @@ class Onion(object): # is the key a v2 key? if onionkey.is_v2_key(key_content): key_type = "RSA1024" - # The below section is commented out because re-publishing - # a pre-prepared v3 private key is currently unstable in Tor. - # This is fixed upstream but won't reach stable until 0.3.5 - # (expected in December 2018) - # See https://trac.torproject.org/projects/tor/ticket/25552 - # Until then, we will deliberately not work with 'persistent' - # v3 onions, which should not be possible via the GUI settings - # anyway. - # Our ticket: https://github.com/micahflee/onionshare/issues/677 - # - # Assume it was a v3 key - # key_type = "ED25519-V3" + # The below section is commented out because re-publishing + # a pre-prepared v3 private key is currently unstable in Tor. + # This is fixed upstream but won't reach stable until 0.3.5 + # (expected in December 2018) + # See https://trac.torproject.org/projects/tor/ticket/25552 + # Until then, we will deliberately not work with 'persistent' + # v3 onions, which should not be possible via the GUI settings + # anyway. + # Our ticket: https://github.com/micahflee/onionshare/issues/677 + # + # Assume it was a v3 key + # key_type = "ED25519-V3" else: raise TorErrorProtocolError(strings._('error_invalid_private_key')) else: @@ -473,6 +473,7 @@ class Onion(object): basic_auth = None self.stealth = False + self.common.log('Onion', 'start_onion_service', 'key_type={}'.format(key_type)) try: if basic_auth != None: res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth, key_type=key_type, key_content=key_content) @@ -480,8 +481,8 @@ class Onion(object): # if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, key_type=key_type, key_content=key_content) - except ProtocolError: - raise TorErrorProtocolError(strings._('error_tor_protocol_error')) + except ProtocolError as e: + raise TorErrorProtocolError(strings._('error_tor_protocol_error').format(e.args[0])) self.service_id = res.service_id onion_host = self.service_id + '.onion' @@ -512,7 +513,7 @@ class Onion(object): self.settings.save() return onion_host else: - raise TorErrorProtocolError(strings._('error_tor_protocol_error')) + raise TorErrorProtocolError(strings._('error_tor_protocol_error_unknown')) def cleanup(self, stop_tor=True): """ diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index b710fa3c..32e56ba0 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -21,6 +21,7 @@ along with this program. If not, see . import os, shutil from . import common, strings +from .onion import TorTooOld, TorErrorProtocolError from .common import ShutdownTimer class OnionShare(object): diff --git a/share/locale/en.json b/share/locale/en.json index 4e7143d3..7d3daba8 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -122,7 +122,8 @@ "settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.", "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}", - "error_tor_protocol_error": "Could not communicate with the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.", + "error_tor_protocol_error": "There was an error with Tor: {}", + "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", "connecting_to_tor": "Connecting to the Tor network", "update_available": "A new version of OnionShare is available. Click here to download it.

Installed version: {}
Latest version: {}", From 04432a5cfab268fd1a8ed0ac4ce8cf693c4245fe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 17:31:40 -0700 Subject: [PATCH 22/87] Tor version must actually be 0.3.3.1 for ephemeral v3 onion services --- onionshare/onion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index d213cd6a..7a111eff 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -459,7 +459,7 @@ class Onion(object): raise TorErrorProtocolError(strings._('error_invalid_private_key')) else: # Work out if we can support v3 onion services, which are preferred - if Version(self.tor_version) >= Version('0.3.2.9') and not self.settings.get('use_legacy_v2_onions'): + if Version(self.tor_version) >= Version('0.3.3.1') and not self.settings.get('use_legacy_v2_onions'): key_type = "ED25519-V3" key_content = onionkey.generate_v3_private_key()[0] else: From 066e8c58edb6b764997be3ce67f97a649c001dc3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 18 Sep 2018 17:44:54 -0700 Subject: [PATCH 23/87] Import onion exceptions that were missing --- onionshare_gui/threads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 3c99d395..f4acc5e1 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -20,6 +20,8 @@ along with this program. If not, see . import time from PyQt5 import QtCore +from onionshare.onion import * + class OnionThread(QtCore.QThread): """ From baa75cc93ca0f1f0845b066fb375bbccf1c4457b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 10:57:12 +1000 Subject: [PATCH 24/87] #704 wrap the upload filename label if too long --- onionshare_gui/receive_mode/uploads.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index 1aebe902..e04e0269 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -19,6 +19,7 @@ along with this program. If not, see . """ import os import subprocess +import textwrap from datetime import datetime from PyQt5 import QtCore, QtWidgets, QtGui @@ -305,10 +306,7 @@ class Uploads(QtWidgets.QScrollArea): try: for upload in self.uploads.values(): for item in upload.files.values(): - if item.filename_label_width > width: - item.filename_label.setText(item.filename[:25] + '[...]') - item.adjustSize() - if width > item.filename_label_width: - item.filename_label.setText(item.filename) + item.filename_label.setText(textwrap.fill(item.filename, 30)) + item.adjustSize() except: pass From 10f94de2377c90ab3e6641091c65f41ed87edbdd Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:37:07 +1000 Subject: [PATCH 25/87] Add 'clear history' button to downloads and uploads windows --- onionshare_gui/receive_mode/uploads.py | 8 ++++++++ onionshare_gui/share_mode/downloads.py | 7 +++++++ share/locale/en.json | 1 + 3 files changed, 16 insertions(+) diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index d52626c5..1c9afdd2 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -232,6 +232,10 @@ class Uploads(QtWidgets.QScrollArea): uploads_label = QtWidgets.QLabel(strings._('gui_uploads', True)) uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() + self.uploads_layout = QtWidgets.QVBoxLayout() @@ -239,6 +243,7 @@ class Uploads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(uploads_label) layout.addWidget(self.no_uploads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.uploads_layout) layout.addStretch() widget.setLayout(layout) @@ -257,6 +262,8 @@ class Uploads(QtWidgets.QScrollArea): self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) # Hide the no_uploads_label self.no_uploads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list upload = Upload(self.common, upload_id, content_length) @@ -299,6 +306,7 @@ class Uploads(QtWidgets.QScrollArea): self.uploads = {} self.no_uploads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) def resizeEvent(self, event): diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 538ddfd0..9aeef576 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -102,6 +102,9 @@ class Downloads(QtWidgets.QScrollArea): downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True)) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() self.downloads_layout = QtWidgets.QVBoxLayout() @@ -109,6 +112,7 @@ class Downloads(QtWidgets.QScrollArea): layout = QtWidgets.QVBoxLayout() layout.addWidget(downloads_label) layout.addWidget(self.no_downloads_label) + layout.addWidget(self.clear_history_button) layout.addLayout(self.downloads_layout) layout.addStretch() widget.setLayout(layout) @@ -126,6 +130,8 @@ class Downloads(QtWidgets.QScrollArea): """ # Hide the no_downloads_label self.no_downloads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() # Add it to the list download = Download(self.common, download_id, total_bytes) @@ -154,4 +160,5 @@ class Downloads(QtWidgets.QScrollArea): self.downloads = {} self.no_downloads_label.show() + self.clear_history_button.hide() self.resize(self.sizeHint()) diff --git a/share/locale/en.json b/share/locale/en.json index 7d3daba8..250524be 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -181,6 +181,7 @@ "gui_uploads": "Upload History", "gui_uploads_window_tooltip": "Show/hide uploads", "gui_no_uploads": "No uploads yet.", + "gui_clear_history": "Clear history", "gui_upload_in_progress": "Upload Started {}", "gui_upload_finished_range": "Uploaded {} to {}", "gui_upload_finished": "Uploaded {}", From 70b0add9efa04f1553d9ed86eec87051acbe2b89 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:40:30 +1000 Subject: [PATCH 26/87] Send the public_mode argument to web.start in the WebThread --- onionshare_gui/threads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index f4acc5e1..9129600d 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -73,4 +73,4 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log('WebThread', 'run') - self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('slug')) + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug')) From 4ebb0a3d57fe83c7c9d3225884e693116943bc8c Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 14:53:33 +1000 Subject: [PATCH 27/87] Log the public_mode flag as well --- onionshare/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/web.py b/onionshare/web.py index 38ad398e..067c5e07 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -619,7 +619,7 @@ class Web(object): """ Start the flask web server. """ - self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug)) + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) if not public_mode: self.generate_slug(persistent_slug) From 34e7290b06d560a85d4824e3a47af2abaee5c76b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 19 Sep 2018 15:07:04 +1000 Subject: [PATCH 28/87] Start the WebThread earlier than the Onion service. This gives it a chance to generate its slug before the Onion Service finishes starting up, which can otherwise lead to a crash --- onionshare_gui/threads.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index f4acc5e1..2af0120d 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -41,14 +41,6 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') - try: - self.mode.app.start_onion_service() - self.success.emit() - - except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: - self.error.emit(e.args[0]) - return - self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') # start onionshare http service in new thread @@ -58,6 +50,14 @@ class OnionThread(QtCore.QThread): # wait for modules in thread to load, preventing a thread-related cx_Freeze crash time.sleep(0.2) + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + class WebThread(QtCore.QThread): """ From b8d24515e8532affb5ff75d052d543115f9e3017 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 18:14:42 -0700 Subject: [PATCH 29/87] Choose a port *before* starting the web service --- onionshare_gui/threads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 2af0120d..83114aea 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -73,4 +73,5 @@ class WebThread(QtCore.QThread): def run(self): self.mode.common.log('WebThread', 'run') + self.mode.app.choose_port() self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('slug')) From f32f71028043309ec9d9542c4c80fe1586b8d010 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 18:50:42 -0700 Subject: [PATCH 30/87] Add a horizontal layout wrapper around the vertical layout, in order to optionally add horizontal widgets to Modes --- onionshare_gui/mode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index feb2f5b6..7ae8d315 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,7 +78,11 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) - self.setLayout(self.layout) + + self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() + self.horizontal_layout_wrapper.addLayout(self.layout) + + self.setLayout(self.horizontal_layout_wrapper) def init(self): """ From c7a9db84d85975915f0c2240c7159c2c73f810e7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:00:12 -0700 Subject: [PATCH 31/87] Always show uploads and downloads --- onionshare_gui/mode.py | 4 ++++ onionshare_gui/receive_mode/__init__.py | 3 ++- onionshare_gui/receive_mode/uploads.py | 1 - onionshare_gui/share_mode/__init__.py | 1 + onionshare_gui/share_mode/downloads.py | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 7ae8d315..4c21de76 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,6 +78,10 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) + # Hack to allow a minimum width on self.layout + min_width_widget = QtWidgets.QWidget() + min_width_widget.setMinimumWidth(450) + self.layout.addWidget(min_width_widget) self.horizontal_layout_wrapper = QtWidgets.QHBoxLayout() self.horizontal_layout_wrapper.addLayout(self.layout) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index d414f3b0..0f3105e1 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -46,7 +46,7 @@ class ReceiveMode(Mode): self.server_status.web = self.web self.server_status.update() - # Downloads + # Uploads self.uploads = Uploads(self.common) self.uploads_in_progress = 0 self.uploads_completed = 0 @@ -86,6 +86,7 @@ class ReceiveMode(Mode): # Layout self.layout.insertWidget(0, self.receive_info) self.layout.insertWidget(0, self.info_widget) + self.horizontal_layout_wrapper.addWidget(self.uploads) def get_stop_server_shutdown_timeout_text(self): """ diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py index f77cdbf4..48574cc7 100644 --- a/onionshare_gui/receive_mode/uploads.py +++ b/onionshare_gui/receive_mode/uploads.py @@ -222,7 +222,6 @@ class Uploads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_uploads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 65ce1d52..28881220 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -116,6 +116,7 @@ class ShareMode(Mode): # Layout self.layout.insertLayout(0, self.file_selection) self.layout.insertWidget(0, self.info_widget) + self.horizontal_layout_wrapper.addWidget(self.downloads) # Always start with focus on file selection self.file_selection.setFocus() diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py index 9aeef576..a34796f1 100644 --- a/onionshare_gui/share_mode/downloads.py +++ b/onionshare_gui/share_mode/downloads.py @@ -91,7 +91,6 @@ class Downloads(QtWidgets.QScrollArea): self.setWindowTitle(strings._('gui_downloads', True)) self.setWidgetResizable(True) - self.setMaximumHeight(600) self.setMinimumHeight(150) self.setMinimumWidth(350) self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) From 1860fb3a9275be1ed3567d561030f478fcbb2351 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:36:16 -0700 Subject: [PATCH 32/87] Remove upload/download toggle buttons --- onionshare_gui/mode.py | 1 + onionshare_gui/receive_mode/__init__.py | 19 ------------------- onionshare_gui/share_mode/__init__.py | 19 ------------------- share/images/download_window_gray.png | Bin 440 -> 0 bytes share/images/download_window_green.png | Bin 761 -> 0 bytes share/images/upload_window_gray.png | Bin 298 -> 0 bytes share/images/upload_window_green.png | Bin 483 -> 0 bytes 7 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 share/images/download_window_gray.png delete mode 100644 share/images/download_window_green.png delete mode 100644 share/images/upload_window_gray.png delete mode 100644 share/images/upload_window_green.png diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 4c21de76..dad58bf3 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,6 +78,7 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) + self.layout.addStretch() # Hack to allow a minimum width on self.layout min_width_widget = QtWidgets.QWidget() min_width_widget.setMinimumWidth(450) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 0f3105e1..74b67e32 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -53,12 +53,6 @@ class ReceiveMode(Mode): self.new_upload = False # For scrolling to the bottom of the uploads list # Information about share, and show uploads button - self.info_show_uploads = QtWidgets.QToolButton() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) - self.info_show_uploads.setCheckable(True) - self.info_show_uploads.toggled.connect(self.uploads_toggled) - self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True)) - self.info_in_progress_uploads_count = QtWidgets.QLabel() self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -72,7 +66,6 @@ class ReceiveMode(Mode): self.info_layout.addStretch() self.info_layout.addWidget(self.info_in_progress_uploads_count) self.info_layout.addWidget(self.info_completed_uploads_count) - self.info_layout.addWidget(self.info_show_uploads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -189,7 +182,6 @@ class ReceiveMode(Mode): self.uploads_in_progress = 0 self.update_uploads_completed() self.update_uploads_in_progress() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) self.uploads.reset() def update_uploads_completed(self): @@ -211,7 +203,6 @@ class ReceiveMode(Mode): image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png'))) self.info_in_progress_uploads_count.setText(' {1:d}'.format(image, self.uploads_in_progress)) self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress)) @@ -226,13 +217,3 @@ class ReceiveMode(Mode): # Resize window self.adjustSize() - - def uploads_toggled(self, checked): - """ - When the 'Show/hide uploads' button is toggled, show or hide the uploads window. - """ - self.common.log('ReceiveMode', 'toggle_uploads') - if checked: - self.uploads.show() - else: - self.uploads.hide() diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 28881220..a9c6e8d7 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -79,12 +79,6 @@ class ShareMode(Mode): self.info_label = QtWidgets.QLabel() self.info_label.setStyleSheet(self.common.css['mode_info_label']) - self.info_show_downloads = QtWidgets.QToolButton() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) - self.info_show_downloads.setCheckable(True) - self.info_show_downloads.toggled.connect(self.downloads_toggled) - self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) - self.info_in_progress_downloads_count = QtWidgets.QLabel() self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) @@ -99,7 +93,6 @@ class ShareMode(Mode): 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_layout.addWidget(self.info_show_downloads) self.info_widget = QtWidgets.QWidget() self.info_widget.setLayout(self.info_layout) @@ -316,16 +309,6 @@ class ShareMode(Mode): # Resize window self.adjustSize() - def downloads_toggled(self, checked): - """ - When the 'Show/hide downloads' button is toggled, show or hide the downloads window. - """ - self.common.log('ShareMode', 'toggle_downloads') - if checked: - self.downloads.show() - else: - self.downloads.hide() - def reset_info_counters(self): """ Set the info counters back to zero. @@ -334,7 +317,6 @@ class ShareMode(Mode): self.downloads_in_progress = 0 self.update_downloads_completed() self.update_downloads_in_progress() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) self.downloads.reset() def update_downloads_completed(self): @@ -356,7 +338,6 @@ class ShareMode(Mode): image = self.common.get_resource_path('images/share_in_progress_none.png') else: image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png'))) self.info_in_progress_downloads_count.setText(' {1:d}'.format(image, self.downloads_in_progress)) self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) diff --git a/share/images/download_window_gray.png b/share/images/download_window_gray.png deleted file mode 100644 index bf9c168ef72f8c49afdaa4ba0be7eac407d072db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$0wn*`OvwRKEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE?e4U2r-o{kZwy(p zCYaOiKy8DKpM!wl(NmY)1w?eWcv^9@Dn_`syoq|kUUbUxumPh|b)L+Ft2_Jcgo3TS ze0`UmGnvut!*KA_CJt`Fw6h+ES533jo&E88eAI^bhi2+BI=(!zIk0JA^v|TM!*dy5 zb}#z&?DWLaKQDF$eO>zT!igih{f{53S{EwiD)?yex_lKsRk@HGW%EzIoyAe%B)wI( zGVR5s2;NsWH=8!s*EMM$P<85`5A=&_iEBhjN@7W>RdP`(kYX@0FtpG$Fw!+L4>2^e zGPSfaG|)CQv@$SApZeeliiX_$l+3hBbPX0(rsfb0JDxKO0W~mqy85}Sb4q9e0MrVb Ay8r+H diff --git a/share/images/download_window_green.png b/share/images/download_window_green.png deleted file mode 100644 index 8f9a899b1d3b4535755ae69894e61a3a27c8c4a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 761 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#N0|O&>fKP}kP(j!s{ZaJ) z|Nkp?lWzfoBfKQYFBlk-$A0`crr`7Imrua&-+_UD{sgh{#r^pc&&Q^@o;*sX2&I%JiVDDj$bks6%!K{785nPwEFU?lP6D}IC<*w zYUi_yKvBY&kSaCQELJ2)*&9;qb~sPuXnZSl@hR zZ*tI(=`3*NN(72z;YDq^~Y?{>8a1}n4b+yV@msCD=RF}|}tqkgn+WyjaZh4ciur=E^o7HPh zS?PMrnRSDW`FGr%r-lbFL|pz~Ri5Lbabv^F3#}q^i^J`*o*a?Chc6!YYg-TgQu&X%Q~loCIHg9Y|H=v diff --git a/share/images/upload_window_green.png b/share/images/upload_window_green.png deleted file mode 100644 index 652ddaffab94dc23d69039aaf2099bf00585d5fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 483 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uer*=z{ncl6XF_nNPmQ*|NsBz#!j9G z3>U_dAirP+hi5m^K%69RcNc~ZR#^`qhqJ&VvY3H^TNs2H8D`Cq01C2~c>21sKVoHN zFqe6gFj)X76z=Kb7{YNq`NOAgAHRO)|G>?|#h17ty@A=Et(fIFpOd7da?2Jc=dFo3 zQv(zNS~o0BO>uU9`b2a=(4vJ4lL~DAEL_M`z*nB3!9VA)oYX1C=LSZGn;SA1X0!9? U^LuFs01boFyt=akR{0Du#)E&u=k From a42c70bcc0e0bffb1f4477266ec24f7b18d4eb39 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:38:29 -0700 Subject: [PATCH 33/87] Only add a stretch at the bottom of the layout in receive mode, not share mode --- onionshare_gui/mode.py | 1 - onionshare_gui/receive_mode/__init__.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index dad58bf3..4c21de76 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -78,7 +78,6 @@ class Mode(QtWidgets.QWidget): # Layout self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.primary_action) - self.layout.addStretch() # Hack to allow a minimum width on self.layout min_width_widget = QtWidgets.QWidget() min_width_widget.setMinimumWidth(450) diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 74b67e32..8712653b 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -79,6 +79,7 @@ class ReceiveMode(Mode): # Layout self.layout.insertWidget(0, self.receive_info) self.layout.insertWidget(0, self.info_widget) + self.layout.addStretch() self.horizontal_layout_wrapper.addWidget(self.uploads) def get_stop_server_shutdown_timeout_text(self): From 15cb534dc71cbb88b1e1cf0467b6ef4bf377cd4e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 19:39:59 -0700 Subject: [PATCH 34/87] Set minimum width for whole application --- onionshare_gui/onionshare_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index b63119bb..07c82e50 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -55,7 +55,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - self.setMinimumWidth(450) + self.setMinimumWidth(850) # Load settings self.config = config From 5dcb06ae1ea8863b5416fcc1182e177ecf2a954a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 20:17:23 -0700 Subject: [PATCH 35/87] Remove adjustSize that was causing issues --- onionshare_gui/onionshare_gui.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 07c82e50..8b61a18e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -205,9 +205,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_server_status_indicator() - # Wait 1ms for the event loop to finish, then adjust size - QtCore.QTimer.singleShot(1, self.adjustSize) - def share_mode_clicked(self): if self.mode != self.MODE_SHARE: self.common.log('OnionShareGui', 'share_mode_clicked') From dd1538733999b4267aeb015f12c4cfda4aa73357 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 19 Sep 2018 21:44:37 -0700 Subject: [PATCH 36/87] Remove obsolete strings (#770) --- share/locale/en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index 250524be..e6b2b2c0 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -48,7 +48,6 @@ "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", "gui_downloads": "Download History", - "gui_downloads_window_tooltip": "Show/hide downloads", "gui_no_downloads": "No downloads yet.", "gui_canceled": "Canceled", "gui_copied_url_title": "Copied OnionShare address", @@ -179,7 +178,6 @@ "systray_download_page_loaded_message": "A user loaded the download page", "systray_upload_page_loaded_message": "A user loaded the upload page", "gui_uploads": "Upload History", - "gui_uploads_window_tooltip": "Show/hide uploads", "gui_no_uploads": "No uploads yet.", "gui_clear_history": "Clear history", "gui_upload_in_progress": "Upload Started {}", From 017ee5250d68d2b8f4f9b50f2ccc58d15016b69f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:14:56 -0700 Subject: [PATCH 37/87] When there is only 1 file being shared, don't zip it --- onionshare/__init__.py | 4 +- onionshare/web.py | 68 ++++++++++++++++----------- onionshare_gui/share_mode/threads.py | 2 +- screenshots/server.png | Bin 45923 -> 0 bytes share/templates/send.html | 24 +++++----- 5 files changed, 55 insertions(+), 43 deletions(-) delete mode 100644 screenshots/server.png diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 51210b6b..e04836b7 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -120,13 +120,13 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.set_file_info(filenames) - app.cleanup_filenames.append(web.zip_filename) + app.cleanup_filenames.append(web.download_filename) except OSError as e: print(e.strerror) sys.exit(1) # Warn about sending large files over Tor - if web.zip_filesize >= 157286400: # 150mb + if web.download_filesize >= 157286400: # 150mb print('') print(strings._("large_filesize")) print('') diff --git a/onionshare/web.py b/onionshare/web.py index 067c5e07..2575230f 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -102,8 +102,9 @@ class Web(object): # Information about the file self.file_info = [] - self.zip_filename = None - self.zip_filesize = None + self.is_zipped = False + self.download_filename = None + self.download_filesize = None self.zip_writer = None self.security_headers = [ @@ -182,17 +183,19 @@ class Web(object): 'send.html', slug=self.slug, file_info=self.file_info, - filename=os.path.basename(self.zip_filename), - filesize=self.zip_filesize, - filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page r = make_response(render_template( 'send.html', file_info=self.file_info, - filename=os.path.basename(self.zip_filename), - filesize=self.zip_filesize, - filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) return self.add_security_headers(r) @self.app.route("//download") @@ -231,8 +234,8 @@ class Web(object): 'id': download_id} ) - dirname = os.path.dirname(self.zip_filename) - basename = os.path.basename(self.zip_filename) + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) def generate(): # The user hasn't canceled the download @@ -244,7 +247,7 @@ class Web(object): chunk_size = 102400 # 100kb - fp = open(self.zip_filename, 'rb') + fp = open(self.download_filename, 'rb') self.done = False canceled = False while not self.done: @@ -264,7 +267,7 @@ class Web(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.zip_filesize) * 100 + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -308,7 +311,7 @@ class Web(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.zip_filesize) + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.add_security_headers(r) # guess content type @@ -517,8 +520,9 @@ class Web(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ + self.common.log("Web", "set_file_info") self.cancel_compression = False - + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: @@ -537,22 +541,30 @@ class Web(object): self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.zip_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return False - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): + return False + + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True - self.zip_writer.close() - self.zip_filesize = os.path.getsize(self.zip_filename) return True def _safe_select_jinja_autoescape(self, filename): diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 50789049..9cda76b1 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,7 +47,7 @@ class CompressThread(QtCore.QThread): # Cancelled pass - self.mode.app.cleanup_filenames.append(self.mode.web.zip_filename) + self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) except OSError as e: self.error.emit(e.strerror) diff --git a/screenshots/server.png b/screenshots/server.png deleted file mode 100644 index 8bdf29719f9e03b139b323383fdb649eada21e94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45923 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 Bxw8NO diff --git a/share/templates/send.html b/share/templates/send.html index df1d3563..e7e1fde0 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -10,18 +10,18 @@

- - -

OnionShare

+
+
    +
  • Total size: {{ filesize_human }} {% if is_zipped %} (compressed){% endif %}
  • + {% if slug %} +
  • Download Files
  • + {% else %} +
  • Download Files
  • + {% endif %} +
+
+ +

OnionShare

From a7453bf4625ad1360fd6e682bb3f13561f9ed743 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:26:11 -0700 Subject: [PATCH 38/87] Make fedora and debian dependencies get added to packages built --- BUILD.md | 4 ++-- install/build_rpm.sh | 2 +- stdeb.cfg | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 51f5cadd..1feedf49 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,11 +11,11 @@ cd onionshare Install the needed dependencies: -For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3` +For Debian-like distros: `apt install -y python3-flask python3-stem python3-pyqt5 python3-cryptography python3-crypto python3-nacl python3-socks python-nautilus tor obfs4proxy python3-pytest build-essential fakeroot python3-all python3-stdeb dh-python` On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available. -For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks` +For Fedora-like distros: `dnf install -y python3-flask python3-stem python3-qt5 python3-pynacl python3-cryptography python3-crypto python3-pysocks nautilus-python tor obfs4 python3-pytest rpm-build` After that you can try both the CLI and the GUI version of OnionShare: diff --git a/install/build_rpm.sh b/install/build_rpm.sh index c103262c..3f7a68ac 100755 --- a/install/build_rpm.sh +++ b/install/build_rpm.sh @@ -9,7 +9,7 @@ VERSION=`cat share/version.txt` rm -r build dist >/dev/null 2>&1 # build binary package -python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, nautilus-python, tor, obfs4" +python3 setup.py bdist_rpm --requires="python3-flask, python3-stem, python3-qt5, python3-pynacl, python3-cryptography, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4" # install it echo "" diff --git a/stdeb.cfg b/stdeb.cfg index e190fe8b..2fc3d3bf 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: onionshare -Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy -Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5 +Depends3: python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy +Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5, python3-cryptography, python3-crypto, python3-nacl, python3-socks, python-nautilus, tor, obfs4proxy Suite: bionic X-Python3-Version: >= 3.6 From c3a46981539bc55cfe57d446526fe6282f01569c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 09:30:44 -0700 Subject: [PATCH 39/87] Also package the new python modules --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a665085..a36fecab 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,12 @@ setup( description=description, long_description=long_description, author=author, author_email=author_email, url=url, license=license, keywords=keywords, - packages=['onionshare', 'onionshare_gui'], + packages=[ + 'onionshare', + 'onionshare_gui', + 'onionshare_gui.share_mode', + 'onionshare_gui.receive_mode' + ], include_package_data=True, scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], data_files=data_files From 574ef19515f64eefbc2877ba48247606f19b08a7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 12:25:07 -0700 Subject: [PATCH 40/87] Change more references to web.zip_filesize to be refer to web.download_filesize --- onionshare_gui/share_mode/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index a9c6e8d7..aec32305 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -177,7 +177,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.zip_filesize >= 157286400: # 150mb + if self.web.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -229,7 +229,7 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.zip_filesize) + self.downloads.add(event["data"]["id"], self.web.download_filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -242,7 +242,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.zip_filesize: + if event["data"]["bytes"] == self.web.download_filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info From 948b406c2a267e5dae061af6fb0cd38b770806ad Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:09:14 -0700 Subject: [PATCH 41/87] Smoothly quit when Ctrl-C is pressed --- onionshare_gui/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 13f0e8c7..99db635a 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ from __future__ import division -import os, sys, platform, argparse +import os +import sys +import platform +import argparse +import signal from .widgets import Alert from PyQt5 import QtCore, QtWidgets @@ -58,6 +62,10 @@ def main(): strings.load_strings(common) print(strings._('version_string').format(common.version)) + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Start the Qt app global qtapp qtapp = Application(common) From 0ea31d39f778d22ab5fe8a4f600c6c8bcfbc8586 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:18:17 -0700 Subject: [PATCH 42/87] Only add the download_filename to cleanup_filenames (which get deleted) if the file is zipped up. Otherwise, OnionShare deletes the original file --- onionshare/__init__.py | 3 ++- onionshare_gui/share_mode/threads.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index e04836b7..2f57ccf2 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -120,7 +120,8 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.set_file_info(filenames) - app.cleanup_filenames.append(web.download_filename) + if web.is_zipped: + app.cleanup_filenames.append(web.download_filename) except OSError as e: print(e.strerror) sys.exit(1) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 9cda76b1..dc43bf0a 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,7 +47,8 @@ class CompressThread(QtCore.QThread): # Cancelled pass - self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) + if self.mode.web.is_zipped: + self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) except OSError as e: self.error.emit(e.strerror) From fe0657128bf96a7d28093280f8e65159fcd0d8aa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:31:52 -0700 Subject: [PATCH 43/87] Make web a module, so I can split it into multiple files --- onionshare/{web.py => web/__init__.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename onionshare/{web.py => web/__init__.py} (99%) diff --git a/onionshare/web.py b/onionshare/web/__init__.py similarity index 99% rename from onionshare/web.py rename to onionshare/web/__init__.py index 2575230f..5a7b297f 100644 --- a/onionshare/web.py +++ b/onionshare/web/__init__.py @@ -40,8 +40,8 @@ from flask import ( ) from werkzeug.utils import secure_filename -from . import strings -from .common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable +from .. import strings +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable # Stub out flask's show_server_banner function, to avoiding showing warnings that From 48ec4ad583f69034c824f7abc0bc7cf1f4d9ffbe Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:43:04 -0700 Subject: [PATCH 44/87] Split the web module into different files for receive mode and share mode logic --- onionshare/web/__init__.py | 857 +-------------------------------- onionshare/web/receive_mode.py | 156 ++++++ onionshare/web/share_mode.py | 60 +++ onionshare/web/web.py | 647 +++++++++++++++++++++++++ 4 files changed, 864 insertions(+), 856 deletions(-) create mode 100644 onionshare/web/receive_mode.py create mode 100644 onionshare/web/share_mode.py create mode 100644 onionshare/web/web.py diff --git a/onionshare/web/__init__.py b/onionshare/web/__init__.py index 5a7b297f..d45b4983 100644 --- a/onionshare/web/__init__.py +++ b/onionshare/web/__init__.py @@ -18,859 +18,4 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import hmac -import logging -import mimetypes -import os -import queue -import socket -import sys -import tempfile -import zipfile -import re -import io -from distutils.version import LooseVersion as Version -from urllib.request import urlopen -from datetime import datetime - -import flask -from flask import ( - Flask, Response, Request, request, render_template, abort, make_response, - flash, redirect, __version__ as flask_version -) -from werkzeug.utils import secure_filename - -from .. import strings -from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable - - -# Stub out flask's show_server_banner function, to avoiding showing warnings that -# are not applicable to OnionShare -def stubbed_show_server_banner(env, debug, app_import_path, eager_loading): - pass - -flask.cli.show_server_banner = stubbed_show_server_banner - - -class Web(object): - """ - The Web object is the OnionShare web server, powered by flask - """ - REQUEST_LOAD = 0 - REQUEST_STARTED = 1 - REQUEST_PROGRESS = 2 - REQUEST_OTHER = 3 - REQUEST_CANCELED = 4 - REQUEST_RATE_LIMIT = 5 - REQUEST_CLOSE_SERVER = 6 - REQUEST_UPLOAD_FILE_RENAMED = 7 - REQUEST_UPLOAD_FINISHED = 8 - REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 - REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 - - def __init__(self, common, gui_mode, receive_mode=False): - self.common = common - - # The flask app - self.app = Flask(__name__, - static_folder=self.common.get_resource_path('static'), - template_folder=self.common.get_resource_path('templates')) - self.app.secret_key = self.common.random_string(8) - - # Debug mode? - if self.common.debug: - self.debug_mode() - - # Are we running in GUI mode? - self.gui_mode = gui_mode - - # Are we using receive mode? - self.receive_mode = receive_mode - if self.receive_mode: - # Use custom WSGI middleware, to modify environ - self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) - # Use a custom Request class to track upload progess - self.app.request_class = ReceiveModeRequest - - # Starting in Flask 0.11, render_template_string autoescapes template variables - # by default. To prevent content injection through template variables in - # earlier versions of Flask, we force autoescaping in the Jinja2 template - # engine if we detect a Flask version with insecure default behavior. - if Version(flask_version) < Version('0.11'): - # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc - Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape - - # Information about the file - self.file_info = [] - self.is_zipped = False - self.download_filename = None - self.download_filesize = None - self.zip_writer = None - - self.security_headers = [ - ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), - ('X-Frame-Options', 'DENY'), - ('X-Xss-Protection', '1; mode=block'), - ('X-Content-Type-Options', 'nosniff'), - ('Referrer-Policy', 'no-referrer'), - ('Server', 'OnionShare') - ] - - self.q = queue.Queue() - - self.slug = None - - self.download_count = 0 - self.upload_count = 0 - - self.error404_count = 0 - - # If "Stop After First Download" is checked (stay_open == False), only allow - # one download at a time. - self.download_in_progress = False - - self.done = False - - # If the client closes the OnionShare window while a download is in progress, - # it should immediately stop serving the file. The client_cancel global is - # used to tell the download function that the client is canceling the download. - self.client_cancel = False - - # shutting down the server only works within the context of flask, so the easiest way to do it is over http - self.shutdown_slug = self.common.random_string(16) - - # Keep track if the server is running - self.running = False - - # Define the ewb app routes - self.common_routes() - if self.receive_mode: - self.receive_routes() - else: - self.send_routes() - - def send_routes(self): - """ - The web app routes for sharing files - """ - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - def index_logic(slug_candidate=''): - """ - Render the template for the onionshare landing page. - """ - self.add_request(Web.REQUEST_LOAD, request.path) - - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # If download is allowed to continue, serve download page - if self.slug: - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - return self.add_security_headers(r) - - @self.app.route("//download") - def download(slug_candidate): - self.check_slug_candidate(slug_candidate) - return download_logic() - - @self.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return download_logic() - - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # Each download has a unique id - download_id = self.download_count - self.download_count += 1 - - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # Tell GUI the download started - self.add_request(Web.REQUEST_STARTED, path, { - 'id': download_id} - ) - - dirname = os.path.dirname(self.download_filename) - basename = os.path.basename(self.download_filename) - - def generate(): - # The user hasn't canceled the download - self.client_cancel = False - - # Starting a new download - if not self.stay_open: - self.download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(self.download_filename, 'rb') - self.done = False - canceled = False - while not self.done: - # The user has canceled the download, so stop serving the file - if self.client_cancel: - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - self.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - self.add_request(Web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - self.done = False - except: - # looks like the download was canceled - self.done = True - canceled = True - - # tell the GUI the download has canceled - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - - fp.close() - - if self.common.platform != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not self.stay_open: - self.download_in_progress = False - - # Close the server, if necessary - if not self.stay_open and not canceled: - print(strings._("closing_automatically")) - self.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass - - r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = self.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - def receive_routes(self): - """ - The web app routes for receiving files - """ - def index_logic(): - self.add_request(Web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(self.slug) - close_action = '/{}/close'.format(self.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) - return self.add_security_headers(r) - - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): - """ - Upload files. - """ - # Make sure downloads_dir exists - valid = True - try: - self.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] - - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return upload_logic() - - - def close_logic(slug_candidate=''): - if self.common.settings.get('receive_allow_receiver_shutdown'): - self.force_shutdown() - r = make_response(render_template('closed.html')) - self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) - return self.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//close", methods=['POST']) - def close(slug_candidate): - self.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) - - @self.app.route("/close", methods=['POST']) - def close_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return close_logic() - - def common_routes(self): - """ - Common web app routes between sending and receiving - """ - @self.app.errorhandler(404) - def page_not_found(e): - """ - 404 error page. - """ - return self.error404() - - @self.app.route("//shutdown") - def shutdown(slug_candidate): - """ - Stop the flask web server, from the context of an http request. - """ - self.check_shutdown_slug_candidate(slug_candidate) - self.force_shutdown() - return "" - - def error404(self): - self.add_request(Web.REQUEST_OTHER, request.path) - if request.path != '/favicon.ico': - self.error404_count += 1 - - # In receive mode, with public mode enabled, skip rate limiting 404s - if not self.common.settings.get('public_mode'): - if self.error404_count == 20: - self.add_request(Web.REQUEST_RATE_LIMIT, request.path) - self.force_shutdown() - print(strings._('error_rate_limit')) - - r = make_response(render_template('404.html'), 404) - return self.add_security_headers(r) - - def add_security_headers(self, r): - """ - Add security headers to a request - """ - for header, value in self.security_headers: - r.headers.set(header, value) - return r - - def set_file_info(self, filenames, processed_size_callback=None): - """ - Using the list of filenames being shared, fill in details that the web - page will need to display. This includes zipping up the file in order to - get the zip file's name and size. - """ - self.common.log("Web", "set_file_info") - self.cancel_compression = False - - # build file info list - self.file_info = {'files': [], 'dirs': []} - for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } - if os.path.isfile(filename): - info['size'] = os.path.getsize(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['files'].append(info) - if os.path.isdir(filename): - info['size'] = self.common.dir_size(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['dirs'].append(info) - self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) - self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - - # Check if there's only 1 file and no folders - if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False - self.download_filename = self.file_info['files'][0]['filename'] - self.download_filesize = self.file_info['files'][0]['size'] - else: - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.download_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False - - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False - - self.zip_writer.close() - self.download_filesize = os.path.getsize(self.download_filename) - self.is_zipped = True - - return True - - def _safe_select_jinja_autoescape(self, filename): - if filename is None: - return True - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - - def add_request(self, request_type, path, data=None): - """ - Add a request to the queue, to communicate with the GUI. - """ - self.q.put({ - 'type': request_type, - 'path': path, - 'data': data - }) - - def generate_slug(self, persistent_slug=None): - self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) - if persistent_slug != None and persistent_slug != '': - self.slug = persistent_slug - self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) - else: - self.slug = self.common.build_slug() - self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) - - def debug_mode(self): - """ - Turn on debugging mode, which will log flask errors to a debug file. - """ - temp_dir = tempfile.gettempdir() - log_handler = logging.FileHandler( - os.path.join(temp_dir, 'onionshare_server.log')) - log_handler.setLevel(logging.WARNING) - self.app.logger.addHandler(log_handler) - - def check_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if self.common.settings.get('public_mode'): - abort(404) - if not hmac.compare_digest(self.slug, slug_candidate): - abort(404) - - def check_shutdown_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if not hmac.compare_digest(self.shutdown_slug, slug_candidate): - abort(404) - - def force_shutdown(self): - """ - Stop the flask web server, from the context of the flask app. - """ - # Shutdown the flask service - try: - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running with the Werkzeug Server') - func() - except: - pass - self.running = False - - def start(self, port, stay_open=False, public_mode=False, persistent_slug=None): - """ - Start the flask web server. - """ - self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) - if not public_mode: - self.generate_slug(persistent_slug) - - self.stay_open = stay_open - - # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) - if os.path.exists('/usr/share/anon-ws-base-files/workstation'): - host = '0.0.0.0' - else: - host = '127.0.0.1' - - self.running = True - self.app.run(host=host, port=port, threaded=True) - - def stop(self, port): - """ - Stop the flask web server by loading /shutdown. - """ - - # If the user cancels the download, let the download function know to stop - # serving the file - self.client_cancel = True - - # To stop flask, load http://127.0.0.1://shutdown - if self.running: - try: - s = socket.socket() - s.connect(('127.0.0.1', port)) - s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) - except: - try: - urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() - except: - pass - - -class ZipWriter(object): - """ - ZipWriter accepts files and directories and compresses them into a zip file - with. If a zip_filename is not passed in, it will use the default onionshare - filename. - """ - def __init__(self, common, zip_filename=None, processed_size_callback=None): - self.common = common - self.cancel_compression = False - - if zip_filename: - self.zip_filename = zip_filename - else: - self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6)) - - self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) - self.processed_size_callback = processed_size_callback - if self.processed_size_callback is None: - self.processed_size_callback = lambda _: None - self._size = 0 - self.processed_size_callback(self._size) - - def add_file(self, filename): - """ - Add a file to the zip archive. - """ - self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(filename) - self.processed_size_callback(self._size) - - def add_dir(self, filename): - """ - Add a directory, and all of its children, to the zip archive. - """ - dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' - for dirpath, dirnames, filenames in os.walk(filename): - for f in filenames: - # Canceling early? - if self.cancel_compression: - return False - - full_filename = os.path.join(dirpath, f) - if not os.path.islink(full_filename): - arc_filename = full_filename[len(dir_to_strip):] - self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) - self._size += os.path.getsize(full_filename) - self.processed_size_callback(self._size) - - return True - - def close(self): - """ - Close the zip archive. - """ - self.z.close() - - -class ReceiveModeWSGIMiddleware(object): - """ - Custom WSGI middleware in order to attach the Web object to environ, so - ReceiveModeRequest can access it. - """ - def __init__(self, app, web): - self.app = app - self.web = web - - def __call__(self, environ, start_response): - environ['web'] = self.web - return self.app(environ, start_response) - - -class ReceiveModeTemporaryFile(object): - """ - A custom TemporaryFile that tells ReceiveModeRequest every time data gets - written to it, in order to track the progress of uploads. - """ - def __init__(self, filename, write_func, close_func): - self.onionshare_filename = filename - self.onionshare_write_func = write_func - self.onionshare_close_func = close_func - - # Create a temporary file - self.f = tempfile.TemporaryFile('wb+') - - # Make all the file-like methods and attributes actually access the - # TemporaryFile, except for write - attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode', - 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', - 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell', - 'truncate', 'writable', 'writelines'] - for attr in attrs: - setattr(self, attr, getattr(self.f, attr)) - - def write(self, b): - """ - Custom write method that calls out to onionshare_write_func - """ - bytes_written = self.f.write(b) - self.onionshare_write_func(self.onionshare_filename, bytes_written) - - def close(self): - """ - Custom close method that calls out to onionshare_close_func - """ - self.f.close() - self.onionshare_close_func(self.onionshare_filename) - - -class ReceiveModeRequest(Request): - """ - A custom flask Request object that keeps track of how much data has been - uploaded for each file, for receive mode. - """ - def __init__(self, environ, populate_request=True, shallow=False): - super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) - self.web = environ['web'] - - # Is this a valid upload request? - self.upload_request = False - if self.method == 'POST': - if self.path == '/{}/upload'.format(self.web.slug): - self.upload_request = True - else: - if self.web.common.settings.get('public_mode'): - if self.path == '/upload': - self.upload_request = True - - if self.upload_request: - # A dictionary that maps filenames to the bytes uploaded so far - self.progress = {} - - # Create an upload_id, attach it to the request - self.upload_id = self.web.upload_count - self.web.upload_count += 1 - - # Figure out the content length - try: - self.content_length = int(self.headers['Content-Length']) - except: - self.content_length = 0 - - print("{}: {}".format( - datetime.now().strftime("%b %d, %I:%M%p"), - strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length)) - )) - - # Tell the GUI - self.web.add_request(Web.REQUEST_STARTED, self.path, { - 'id': self.upload_id, - 'content_length': self.content_length - }) - - self.previous_file = None - - def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None): - """ - This gets called for each file that gets uploaded, and returns an file-like - writable stream. - """ - if self.upload_request: - self.progress[filename] = { - 'uploaded_bytes': 0, - 'complete': False - } - - return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func) - - def close(self): - """ - Closing the request. - """ - super(ReceiveModeRequest, self).close() - if self.upload_request: - # Inform the GUI that the upload has finished - self.web.add_request(Web.REQUEST_UPLOAD_FINISHED, self.path, { - 'id': self.upload_id - }) - - def file_write_func(self, filename, length): - """ - This function gets called when a specific file is written to. - """ - if self.upload_request: - self.progress[filename]['uploaded_bytes'] += length - - if self.previous_file != filename: - if self.previous_file is not None: - print('') - self.previous_file = filename - - print('\r=> {:15s} {}'.format( - self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']), - filename - ), end='') - - # Update the GUI on the upload progress - self.web.add_request(Web.REQUEST_PROGRESS, self.path, { - 'id': self.upload_id, - 'progress': self.progress - }) - - def file_close_func(self, filename): - """ - This function gets called when a specific file is closed. - """ - self.progress[filename]['complete'] = True +from .web import Web diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py new file mode 100644 index 00000000..90accc8c --- /dev/null +++ b/onionshare/web/receive_mode.py @@ -0,0 +1,156 @@ +import tempfile +from datetime import datetime +from flask import Request + +from .. import strings + + +class ReceiveModeWSGIMiddleware(object): + """ + Custom WSGI middleware in order to attach the Web object to environ, so + ReceiveModeRequest can access it. + """ + def __init__(self, app, web): + self.app = app + self.web = web + + def __call__(self, environ, start_response): + environ['web'] = self.web + return self.app(environ, start_response) + + +class ReceiveModeTemporaryFile(object): + """ + A custom TemporaryFile that tells ReceiveModeRequest every time data gets + written to it, in order to track the progress of uploads. + """ + def __init__(self, filename, write_func, close_func): + self.onionshare_filename = filename + self.onionshare_write_func = write_func + self.onionshare_close_func = close_func + + # Create a temporary file + self.f = tempfile.TemporaryFile('wb+') + + # Make all the file-like methods and attributes actually access the + # TemporaryFile, except for write + attrs = ['closed', 'detach', 'fileno', 'flush', 'isatty', 'mode', + 'name', 'peek', 'raw', 'read', 'read1', 'readable', 'readinto', + 'readinto1', 'readline', 'readlines', 'seek', 'seekable', 'tell', + 'truncate', 'writable', 'writelines'] + for attr in attrs: + setattr(self, attr, getattr(self.f, attr)) + + def write(self, b): + """ + Custom write method that calls out to onionshare_write_func + """ + bytes_written = self.f.write(b) + self.onionshare_write_func(self.onionshare_filename, bytes_written) + + def close(self): + """ + Custom close method that calls out to onionshare_close_func + """ + self.f.close() + self.onionshare_close_func(self.onionshare_filename) + + +class ReceiveModeRequest(Request): + """ + A custom flask Request object that keeps track of how much data has been + uploaded for each file, for receive mode. + """ + def __init__(self, environ, populate_request=True, shallow=False): + super(ReceiveModeRequest, self).__init__(environ, populate_request, shallow) + self.web = environ['web'] + + # Is this a valid upload request? + self.upload_request = False + if self.method == 'POST': + if self.path == '/{}/upload'.format(self.web.slug): + self.upload_request = True + else: + if self.web.common.settings.get('public_mode'): + if self.path == '/upload': + self.upload_request = True + + if self.upload_request: + # A dictionary that maps filenames to the bytes uploaded so far + self.progress = {} + + # Create an upload_id, attach it to the request + self.upload_id = self.web.upload_count + self.web.upload_count += 1 + + # Figure out the content length + try: + self.content_length = int(self.headers['Content-Length']) + except: + self.content_length = 0 + + print("{}: {}".format( + datetime.now().strftime("%b %d, %I:%M%p"), + strings._("receive_mode_upload_starting").format(self.web.common.human_readable_filesize(self.content_length)) + )) + + # Tell the GUI + self.web.add_request(self.web.REQUEST_STARTED, self.path, { + 'id': self.upload_id, + 'content_length': self.content_length + }) + + self.previous_file = None + + def _get_file_stream(self, total_content_length, content_type, filename=None, content_length=None): + """ + This gets called for each file that gets uploaded, and returns an file-like + writable stream. + """ + if self.upload_request: + self.progress[filename] = { + 'uploaded_bytes': 0, + 'complete': False + } + + return ReceiveModeTemporaryFile(filename, self.file_write_func, self.file_close_func) + + def close(self): + """ + Closing the request. + """ + super(ReceiveModeRequest, self).close() + if self.upload_request: + # Inform the GUI that the upload has finished + self.web.add_request(self.web.REQUEST_UPLOAD_FINISHED, self.path, { + 'id': self.upload_id + }) + + def file_write_func(self, filename, length): + """ + This function gets called when a specific file is written to. + """ + if self.upload_request: + self.progress[filename]['uploaded_bytes'] += length + + if self.previous_file != filename: + if self.previous_file is not None: + print('') + self.previous_file = filename + + print('\r=> {:15s} {}'.format( + self.web.common.human_readable_filesize(self.progress[filename]['uploaded_bytes']), + filename + ), end='') + + # Update the GUI on the upload progress + self.web.add_request(self.web.REQUEST_PROGRESS, self.path, { + 'id': self.upload_id, + 'progress': self.progress + }) + + def file_close_func(self, filename): + """ + This function gets called when a specific file is closed. + """ + self.progress[filename]['complete'] = True diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py new file mode 100644 index 00000000..f066bde4 --- /dev/null +++ b/onionshare/web/share_mode.py @@ -0,0 +1,60 @@ +import os +import tempfile +import zipfile + + +class ZipWriter(object): + """ + ZipWriter accepts files and directories and compresses them into a zip file + with. If a zip_filename is not passed in, it will use the default onionshare + filename. + """ + def __init__(self, common, zip_filename=None, processed_size_callback=None): + self.common = common + self.cancel_compression = False + + if zip_filename: + self.zip_filename = zip_filename + else: + self.zip_filename = '{0:s}/onionshare_{1:s}.zip'.format(tempfile.mkdtemp(), self.common.random_string(4, 6)) + + self.z = zipfile.ZipFile(self.zip_filename, 'w', allowZip64=True) + self.processed_size_callback = processed_size_callback + if self.processed_size_callback is None: + self.processed_size_callback = lambda _: None + self._size = 0 + self.processed_size_callback(self._size) + + def add_file(self, filename): + """ + Add a file to the zip archive. + """ + self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(filename) + self.processed_size_callback(self._size) + + def add_dir(self, filename): + """ + Add a directory, and all of its children, to the zip archive. + """ + dir_to_strip = os.path.dirname(filename.rstrip('/'))+'/' + for dirpath, dirnames, filenames in os.walk(filename): + for f in filenames: + # Canceling early? + if self.cancel_compression: + return False + + full_filename = os.path.join(dirpath, f) + if not os.path.islink(full_filename): + arc_filename = full_filename[len(dir_to_strip):] + self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED) + self._size += os.path.getsize(full_filename) + self.processed_size_callback(self._size) + + return True + + def close(self): + """ + Close the zip archive. + """ + self.z.close() diff --git a/onionshare/web/web.py b/onionshare/web/web.py new file mode 100644 index 00000000..ff149f21 --- /dev/null +++ b/onionshare/web/web.py @@ -0,0 +1,647 @@ +import hmac +import logging +import mimetypes +import os +import queue +import socket +import sys +import tempfile +from distutils.version import LooseVersion as Version +from urllib.request import urlopen + +import flask +from flask import ( + Flask, Response, request, render_template, abort, make_response, + flash, redirect, __version__ as flask_version +) +from werkzeug.utils import secure_filename + +from .. import strings +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable + +from .share_mode import ZipWriter +from .receive_mode import ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest + + +# Stub out flask's show_server_banner function, to avoiding showing warnings that +# are not applicable to OnionShare +def stubbed_show_server_banner(env, debug, app_import_path, eager_loading): + pass + +flask.cli.show_server_banner = stubbed_show_server_banner + + +class Web(object): + """ + The Web object is the OnionShare web server, powered by flask + """ + REQUEST_LOAD = 0 + REQUEST_STARTED = 1 + REQUEST_PROGRESS = 2 + REQUEST_OTHER = 3 + REQUEST_CANCELED = 4 + REQUEST_RATE_LIMIT = 5 + REQUEST_CLOSE_SERVER = 6 + REQUEST_UPLOAD_FILE_RENAMED = 7 + REQUEST_UPLOAD_FINISHED = 8 + REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 + REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 + + def __init__(self, common, gui_mode, receive_mode=False): + self.common = common + + # The flask app + self.app = Flask(__name__, + static_folder=self.common.get_resource_path('static'), + template_folder=self.common.get_resource_path('templates')) + self.app.secret_key = self.common.random_string(8) + + # Debug mode? + if self.common.debug: + self.debug_mode() + + # Are we running in GUI mode? + self.gui_mode = gui_mode + + # Are we using receive mode? + self.receive_mode = receive_mode + if self.receive_mode: + # Use custom WSGI middleware, to modify environ + self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) + # Use a custom Request class to track upload progess + self.app.request_class = ReceiveModeRequest + + # Starting in Flask 0.11, render_template_string autoescapes template variables + # by default. To prevent content injection through template variables in + # earlier versions of Flask, we force autoescaping in the Jinja2 template + # engine if we detect a Flask version with insecure default behavior. + if Version(flask_version) < Version('0.11'): + # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc + Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape + + # Information about the file + self.file_info = [] + self.is_zipped = False + self.download_filename = None + self.download_filesize = None + self.zip_writer = None + + self.security_headers = [ + ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), + ('X-Frame-Options', 'DENY'), + ('X-Xss-Protection', '1; mode=block'), + ('X-Content-Type-Options', 'nosniff'), + ('Referrer-Policy', 'no-referrer'), + ('Server', 'OnionShare') + ] + + self.q = queue.Queue() + + self.slug = None + + self.download_count = 0 + self.upload_count = 0 + + self.error404_count = 0 + + # If "Stop After First Download" is checked (stay_open == False), only allow + # one download at a time. + self.download_in_progress = False + + self.done = False + + # If the client closes the OnionShare window while a download is in progress, + # it should immediately stop serving the file. The client_cancel global is + # used to tell the download function that the client is canceling the download. + self.client_cancel = False + + # shutting down the server only works within the context of flask, so the easiest way to do it is over http + self.shutdown_slug = self.common.random_string(16) + + # Keep track if the server is running + self.running = False + + # Define the ewb app routes + self.common_routes() + if self.receive_mode: + self.receive_routes() + else: + self.send_routes() + + def send_routes(self): + """ + The web app routes for sharing files + """ + @self.app.route("/") + def index(slug_candidate): + self.check_slug_candidate(slug_candidate) + return index_logic() + + @self.app.route("/") + def index_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + self.add_request(Web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) + + # If download is allowed to continue, serve download page + if self.slug: + r = make_response(render_template( + 'send.html', + slug=self.slug, + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) + return self.add_security_headers(r) + + @self.app.route("//download") + def download(slug_candidate): + self.check_slug_candidate(slug_candidate) + return download_logic() + + @self.app.route("/download") + def download_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return download_logic() + + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.stay_open and self.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.add_security_headers(r) + + # Each download has a unique id + download_id = self.download_count + self.download_count += 1 + + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path + + # Tell GUI the download started + self.add_request(Web.REQUEST_STARTED, path, { + 'id': download_id} + ) + + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) + + def generate(): + # The user hasn't canceled the download + self.client_cancel = False + + # Starting a new download + if not self.stay_open: + self.download_in_progress = True + + chunk_size = 102400 # 100kb + + fp = open(self.download_filename, 'rb') + self.done = False + canceled = False + while not self.done: + # The user has canceled the download, so stop serving the file + if self.client_cancel: + self.add_request(Web.REQUEST_CANCELED, path, { + 'id': download_id + }) + break + + chunk = fp.read(chunk_size) + if chunk == b'': + self.done = True + else: + try: + yield chunk + + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 + + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() + + self.add_request(Web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + self.done = False + except: + # looks like the download was canceled + self.done = True + canceled = True + + # tell the GUI the download has canceled + self.add_request(Web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if self.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not self.stay_open: + self.download_in_progress = False + + # Close the server, if necessary + if not self.stay_open and not canceled: + print(strings._("closing_automatically")) + self.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', self.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = self.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r + + def receive_routes(self): + """ + The web app routes for receiving files + """ + def index_logic(): + self.add_request(Web.REQUEST_LOAD, request.path) + + if self.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(self.slug) + close_action = '/{}/close'.format(self.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) + return self.add_security_headers(r) + + @self.app.route("/") + def index(slug_candidate): + self.check_slug_candidate(slug_candidate) + return index_logic() + + @self.app.route("/") + def index_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + self.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if self.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if self.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + @self.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + self.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) + + @self.app.route("/upload", methods=['POST']) + def upload_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return upload_logic() + + + def close_logic(slug_candidate=''): + if self.common.settings.get('receive_allow_receiver_shutdown'): + self.force_shutdown() + r = make_response(render_template('closed.html')) + self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) + return self.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) + + @self.app.route("//close", methods=['POST']) + def close(slug_candidate): + self.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) + + @self.app.route("/close", methods=['POST']) + def close_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return close_logic() + + def common_routes(self): + """ + Common web app routes between sending and receiving + """ + @self.app.errorhandler(404) + def page_not_found(e): + """ + 404 error page. + """ + return self.error404() + + @self.app.route("//shutdown") + def shutdown(slug_candidate): + """ + Stop the flask web server, from the context of an http request. + """ + self.check_shutdown_slug_candidate(slug_candidate) + self.force_shutdown() + return "" + + def error404(self): + self.add_request(Web.REQUEST_OTHER, request.path) + if request.path != '/favicon.ico': + self.error404_count += 1 + + # In receive mode, with public mode enabled, skip rate limiting 404s + if not self.common.settings.get('public_mode'): + if self.error404_count == 20: + self.add_request(Web.REQUEST_RATE_LIMIT, request.path) + self.force_shutdown() + print(strings._('error_rate_limit')) + + r = make_response(render_template('404.html'), 404) + return self.add_security_headers(r) + + def add_security_headers(self, r): + """ + Add security headers to a request + """ + for header, value in self.security_headers: + r.headers.set(header, value) + return r + + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + self.common.log("Web", "set_file_info") + self.cancel_compression = False + + # build file info list + self.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) + self.file_info['dirs'].append(info) + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) + # Canceling early? + if self.cancel_compression: + self.zip_writer.close() + return False + + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): + return False + + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True + + return True + + def _safe_select_jinja_autoescape(self, filename): + if filename is None: + return True + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def add_request(self, request_type, path, data=None): + """ + Add a request to the queue, to communicate with the GUI. + """ + self.q.put({ + 'type': request_type, + 'path': path, + 'data': data + }) + + def generate_slug(self, persistent_slug=None): + self.common.log('Web', 'generate_slug', 'persistent_slug={}'.format(persistent_slug)) + if persistent_slug != None and persistent_slug != '': + self.slug = persistent_slug + self.common.log('Web', 'generate_slug', 'persistent_slug sent, so slug is: "{}"'.format(self.slug)) + else: + self.slug = self.common.build_slug() + self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) + + def debug_mode(self): + """ + Turn on debugging mode, which will log flask errors to a debug file. + """ + temp_dir = tempfile.gettempdir() + log_handler = logging.FileHandler( + os.path.join(temp_dir, 'onionshare_server.log')) + log_handler.setLevel(logging.WARNING) + self.app.logger.addHandler(log_handler) + + def check_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) + if self.common.settings.get('public_mode'): + abort(404) + if not hmac.compare_digest(self.slug, slug_candidate): + abort(404) + + def check_shutdown_slug_candidate(self, slug_candidate): + self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) + if not hmac.compare_digest(self.shutdown_slug, slug_candidate): + abort(404) + + def force_shutdown(self): + """ + Stop the flask web server, from the context of the flask app. + """ + # Shutdown the flask service + try: + func = request.environ.get('werkzeug.server.shutdown') + if func is None: + raise RuntimeError('Not running with the Werkzeug Server') + func() + except: + pass + self.running = False + + def start(self, port, stay_open=False, public_mode=False, persistent_slug=None): + """ + Start the flask web server. + """ + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug)) + if not public_mode: + self.generate_slug(persistent_slug) + + self.stay_open = stay_open + + # In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220) + if os.path.exists('/usr/share/anon-ws-base-files/workstation'): + host = '0.0.0.0' + else: + host = '127.0.0.1' + + self.running = True + self.app.run(host=host, port=port, threaded=True) + + def stop(self, port): + """ + Stop the flask web server by loading /shutdown. + """ + + # If the user cancels the download, let the download function know to stop + # serving the file + self.client_cancel = True + + # To stop flask, load http://127.0.0.1://shutdown + if self.running: + try: + s = socket.socket() + s.connect(('127.0.0.1', port)) + s.sendall('GET /{0:s}/shutdown HTTP/1.1\r\n\r\n'.format(self.shutdown_slug)) + except: + try: + urlopen('http://127.0.0.1:{0:d}/{1:s}/shutdown'.format(port, self.shutdown_slug)).read() + except: + pass From d699a7f6419674ffba81b161f5ef04d3a0140da0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:45:13 -0700 Subject: [PATCH 45/87] Fix tests to point to new location of ZipWriter class --- test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 610a43ea..8ac7efb8 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -64,7 +64,7 @@ def temp_file_1024_delete(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def custom_zw(): - zw = web.ZipWriter( + zw = web.share_mode.ZipWriter( common.Common(), zip_filename=common.Common.random_string(4, 6), processed_size_callback=lambda _: 'custom_callback' @@ -77,7 +77,7 @@ def custom_zw(): # pytest > 2.9 only needs @pytest.fixture @pytest.yield_fixture(scope='session') def default_zw(): - zw = web.ZipWriter(common.Common()) + zw = web.share_mode.ZipWriter(common.Common()) yield zw zw.close() tmp_dir = os.path.dirname(zw.zip_filename) From 4f27fac8408383b17326c07909b525cd72f0f9ac Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 20 Sep 2018 23:58:27 -0700 Subject: [PATCH 46/87] Refactor web to push share and receive mode logic into their respective files --- onionshare/web/receive_mode.py | 157 ++++++++++++++- onionshare/web/share_mode.py | 177 +++++++++++++++++ onionshare/web/web.py | 338 +-------------------------------- 3 files changed, 338 insertions(+), 334 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 90accc8c..0ebc9ccd 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -1,10 +1,165 @@ +import os import tempfile from datetime import datetime -from flask import Request +from flask import Request, request, render_template, make_response, flash, redirect +from werkzeug.utils import secure_filename +from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable from .. import strings +def receive_routes(web): + """ + The web app routes for receiving files + """ + def index_logic(): + web.add_request(web.REQUEST_LOAD, request.path) + + if web.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(web.slug) + close_action = '/{}/close'.format(web.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=web.common.settings.get('receive_allow_receiver_shutdown'))) + return web.add_security_headers(r) + + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + web.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(web.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(web.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(web.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + web.add_request(web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + web.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) + + @web.app.route("/upload", methods=['POST']) + def upload_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return upload_logic() + + + def close_logic(slug_candidate=''): + if web.common.settings.get('receive_allow_receiver_shutdown'): + web.force_shutdown() + r = make_response(render_template('closed.html')) + web.add_request(web.REQUEST_CLOSE_SERVER, request.path) + return web.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) + + @web.app.route("//close", methods=['POST']) + def close(slug_candidate): + web.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) + + @web.app.route("/close", methods=['POST']) + def close_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return close_logic() + + class ReceiveModeWSGIMiddleware(object): """ Custom WSGI middleware in order to attach the Web object to environ, so diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index f066bde4..58cc9b99 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -1,6 +1,183 @@ import os +import sys import tempfile import zipfile +import mimetypes +from flask import Response, request, render_template, make_response + +from .. import strings + + +def share_routes(web): + """ + The web app routes for sharing files + """ + @web.app.route("/") + def index(slug_candidate): + web.check_slug_candidate(slug_candidate) + return index_logic() + + @web.app.route("/") + def index_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + web.add_request(web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # If download is allowed to continue, serve download page + if web.slug: + r = make_response(render_template( + 'send.html', + slug=web.slug, + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=web.file_info, + filename=os.path.basename(web.download_filename), + filesize=web.download_filesize, + filesize_human=web.common.human_readable_filesize(web.download_filesize), + is_zipped=web.is_zipped)) + return web.add_security_headers(r) + + @web.app.route("//download") + def download(slug_candidate): + web.check_slug_candidate(slug_candidate) + return download_logic() + + @web.app.route("/download") + def download_public(): + if not web.common.settings.get('public_mode'): + return web.error404() + return download_logic() + + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not web.stay_open and web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return web.add_security_headers(r) + + # Each download has a unique id + download_id = web.download_count + web.download_count += 1 + + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path + + # Tell GUI the download started + web.add_request(web.REQUEST_STARTED, path, { + 'id': download_id} + ) + + dirname = os.path.dirname(web.download_filename) + basename = os.path.basename(web.download_filename) + + def generate(): + # The user hasn't canceled the download + web.client_cancel = False + + # Starting a new download + if not web.stay_open: + web.download_in_progress = True + + chunk_size = 102400 # 100kb + + fp = open(web.download_filename, 'rb') + web.done = False + canceled = False + while not web.done: + # The user has canceled the download, so stop serving the file + if web.client_cancel: + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + break + + chunk = fp.read(chunk_size) + if chunk == b'': + web.done = True + else: + try: + yield chunk + + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / web.download_filesize) * 100 + + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not web.gui_mode or web.common.platform == 'Linux' or web.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(web.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() + + web.add_request(web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + web.done = False + except: + # looks like the download was canceled + web.done = True + canceled = True + + # tell the GUI the download has canceled + web.add_request(web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if web.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not web.stay_open: + web.download_in_progress = False + + # Close the server, if necessary + if not web.stay_open and not canceled: + print(strings._("closing_automatically")) + web.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', web.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = web.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r class ZipWriter(object): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index ff149f21..0a6e6964 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -1,6 +1,5 @@ import hmac import logging -import mimetypes import os import queue import socket @@ -10,17 +9,12 @@ from distutils.version import LooseVersion as Version from urllib.request import urlopen import flask -from flask import ( - Flask, Response, request, render_template, abort, make_response, - flash, redirect, __version__ as flask_version -) -from werkzeug.utils import secure_filename +from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version from .. import strings -from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable -from .share_mode import ZipWriter -from .receive_mode import ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest +from .share_mode import share_routes, ZipWriter +from .receive_mode import receive_routes, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest # Stub out flask's show_server_banner function, to avoiding showing warnings that @@ -124,331 +118,9 @@ class Web(object): # Define the ewb app routes self.common_routes() if self.receive_mode: - self.receive_routes() + receive_routes(self) else: - self.send_routes() - - def send_routes(self): - """ - The web app routes for sharing files - """ - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - def index_logic(slug_candidate=''): - """ - Render the template for the onionshare landing page. - """ - self.add_request(Web.REQUEST_LOAD, request.path) - - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # If download is allowed to continue, serve download page - if self.slug: - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - else: - # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) - return self.add_security_headers(r) - - @self.app.route("//download") - def download(slug_candidate): - self.check_slug_candidate(slug_candidate) - return download_logic() - - @self.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return download_logic() - - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not self.stay_open and self.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return self.add_security_headers(r) - - # Each download has a unique id - download_id = self.download_count - self.download_count += 1 - - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path - - # Tell GUI the download started - self.add_request(Web.REQUEST_STARTED, path, { - 'id': download_id} - ) - - dirname = os.path.dirname(self.download_filename) - basename = os.path.basename(self.download_filename) - - def generate(): - # The user hasn't canceled the download - self.client_cancel = False - - # Starting a new download - if not self.stay_open: - self.download_in_progress = True - - chunk_size = 102400 # 100kb - - fp = open(self.download_filename, 'rb') - self.done = False - canceled = False - while not self.done: - # The user has canceled the download, so stop serving the file - if self.client_cancel: - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - self.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.gui_mode or self.common.platform == 'Linux' or self.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - self.add_request(Web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - self.done = False - except: - # looks like the download was canceled - self.done = True - canceled = True - - # tell the GUI the download has canceled - self.add_request(Web.REQUEST_CANCELED, path, { - 'id': download_id - }) - - fp.close() - - if self.common.platform != 'Darwin': - sys.stdout.write("\n") - - # Download is finished - if not self.stay_open: - self.download_in_progress = False - - # Close the server, if necessary - if not self.stay_open and not canceled: - print(strings._("closing_automatically")) - self.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass - - r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = self.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r - - def receive_routes(self): - """ - The web app routes for receiving files - """ - def index_logic(): - self.add_request(Web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(self.slug) - close_action = '/{}/close'.format(self.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) - return self.add_security_headers(r) - - @self.app.route("/") - def index(slug_candidate): - self.check_slug_candidate(slug_candidate) - return index_logic() - - @self.app.route("/") - def index_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): - """ - Upload files. - """ - # Make sure downloads_dir exists - valid = True - try: - self.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - self.add_request(Web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] - - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - self.add_request(Web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - self.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return upload_logic() - - - def close_logic(slug_candidate=''): - if self.common.settings.get('receive_allow_receiver_shutdown'): - self.force_shutdown() - r = make_response(render_template('closed.html')) - self.add_request(Web.REQUEST_CLOSE_SERVER, request.path) - return self.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) - - @self.app.route("//close", methods=['POST']) - def close(slug_candidate): - self.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) - - @self.app.route("/close", methods=['POST']) - def close_public(): - if not self.common.settings.get('public_mode'): - return self.error404() - return close_logic() + share_routes(self) def common_routes(self): """ From 916c5ed1971c7f514a37bbd20c4a45e8d0a7c208 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:14:32 -0700 Subject: [PATCH 47/87] Refactor web even more to all of the share and receive web logic into ShareModeWeb and ReceiveModeWeb classes --- onionshare/__init__.py | 2 +- onionshare/web/receive_mode.py | 280 ++++++++++--------- onionshare/web/share_mode.py | 349 ++++++++++++++---------- onionshare/web/web.py | 84 ++---- onionshare_gui/receive_mode/__init__.py | 2 +- onionshare_gui/share_mode/__init__.py | 2 +- onionshare_gui/share_mode/threads.py | 2 +- test/test_onionshare_web.py | 32 +-- 8 files changed, 387 insertions(+), 366 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 2f57ccf2..9c390fa8 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -119,7 +119,7 @@ def main(cwd=None): # Prepare files to share print(strings._("preparing_files")) try: - web.set_file_info(filenames) + web.share_mode.set_file_info(filenames) if web.is_zipped: app.cleanup_filenames.append(web.download_filename) except OSError as e: diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 0ebc9ccd..ab5f5f13 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -8,156 +8,164 @@ from ..common import DownloadsDirErrorCannotCreate, DownloadsDirErrorNotWritable from .. import strings -def receive_routes(web): +class ReceiveModeWeb(object): """ - The web app routes for receiving files + All of the web logic for receive mode """ - def index_logic(): - web.add_request(web.REQUEST_LOAD, request.path) + def __init__(self, web): + self.web = web + self.define_routes() - if web.common.settings.get('public_mode'): - upload_action = '/upload' - close_action = '/close' - else: - upload_action = '/{}/upload'.format(web.slug) - close_action = '/{}/close'.format(web.slug) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action, - close_action=close_action, - receive_allow_receiver_shutdown=web.common.settings.get('receive_allow_receiver_shutdown'))) - return web.add_security_headers(r) - - @web.app.route("/") - def index(slug_candidate): - web.check_slug_candidate(slug_candidate) - return index_logic() - - @web.app.route("/") - def index_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return index_logic() - - - def upload_logic(slug_candidate=''): + def define_routes(self): """ - Upload files. + The web app routes for receiving files """ - # Make sure downloads_dir exists - valid = True - try: - web.common.validate_downloads_dir() - except DownloadsDirErrorCannotCreate: - web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(web.common.settings.get('downloads_dir'))) - valid = False - except DownloadsDirErrorNotWritable: - web.add_request(web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(web.common.settings.get('downloads_dir'))) - valid = False - if not valid: - flash('Error uploading, please inform the OnionShare user', 'error') - if web.common.settings.get('public_mode'): + def index_logic(): + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + if self.web.common.settings.get('public_mode'): + upload_action = '/upload' + close_action = '/close' + else: + upload_action = '/{}/upload'.format(self.web.slug) + close_action = '/{}/close'.format(self.web.slug) + + r = make_response(render_template( + 'receive.html', + upload_action=upload_action, + close_action=close_action, + receive_allow_receiver_shutdown=self.web.common.settings.get('receive_allow_receiver_shutdown'))) + return self.web.add_security_headers(r) + + @self.web.app.route("/") + def index(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return index_logic() + + @self.web.app.route("/") + def index_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return index_logic() + + + def upload_logic(slug_candidate=''): + """ + Upload files. + """ + # Make sure downloads_dir exists + valid = True + try: + self.web.common.validate_downloads_dir() + except DownloadsDirErrorCannotCreate: + self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) + print(strings._('error_cannot_create_downloads_dir').format(self.web.common.settings.get('downloads_dir'))) + valid = False + except DownloadsDirErrorNotWritable: + self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) + print(strings._('error_downloads_dir_not_writable').format(self.web.common.settings.get('downloads_dir'))) + valid = False + if not valid: + flash('Error uploading, please inform the OnionShare user', 'error') + if self.web.common.settings.get('public_mode'): + return redirect('/') + else: + return redirect('/{}'.format(slug_candidate)) + + files = request.files.getlist('file[]') + filenames = [] + print('') + for f in files: + if f.filename != '': + # Automatically rename the file, if a file of the same name already exists + filename = secure_filename(f.filename) + filenames.append(filename) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), filename) + if os.path.exists(local_path): + if '.' in filename: + # Add "-i", e.g. change "foo.txt" to "foo-2.txt" + parts = filename.split('.') + name = parts[:-1] + ext = parts[-1] + + i = 2 + valid = False + while not valid: + new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + else: + # If no extension, just add "-i", e.g. change "foo" to "foo-2" + i = 2 + valid = False + while not valid: + new_filename = '{}-{}'.format(filename, i) + local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + if os.path.exists(local_path): + i += 1 + else: + valid = True + + basename = os.path.basename(local_path) + if f.filename != basename: + # Tell the GUI that the file has changed names + self.web.add_request(self.web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { + 'id': request.upload_id, + 'old_filename': f.filename, + 'new_filename': basename + }) + + self.web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + print(strings._('receive_mode_received_file').format(local_path)) + f.save(local_path) + + # Note that flash strings are on English, and not translated, on purpose, + # to avoid leaking the locale of the OnionShare user + if len(filenames) == 0: + flash('No files uploaded', 'info') + else: + for filename in filenames: + flash('Sent {}'.format(filename), 'info') + + if self.web.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) - files = request.files.getlist('file[]') - filenames = [] - print('') - for f in files: - if f.filename != '': - # Automatically rename the file, if a file of the same name already exists - filename = secure_filename(f.filename) - filenames.append(filename) - local_path = os.path.join(web.common.settings.get('downloads_dir'), filename) - if os.path.exists(local_path): - if '.' in filename: - # Add "-i", e.g. change "foo.txt" to "foo-2.txt" - parts = filename.split('.') - name = parts[:-1] - ext = parts[-1] + @self.web.app.route("//upload", methods=['POST']) + def upload(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return upload_logic(slug_candidate) - i = 2 - valid = False - while not valid: - new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - else: - # If no extension, just add "-i", e.g. change "foo" to "foo-2" - i = 2 - valid = False - while not valid: - new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(web.common.settings.get('downloads_dir'), new_filename) - if os.path.exists(local_path): - i += 1 - else: - valid = True - - basename = os.path.basename(local_path) - if f.filename != basename: - # Tell the GUI that the file has changed names - web.add_request(web.REQUEST_UPLOAD_FILE_RENAMED, request.path, { - 'id': request.upload_id, - 'old_filename': f.filename, - 'new_filename': basename - }) - - web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) - print(strings._('receive_mode_received_file').format(local_path)) - f.save(local_path) - - # Note that flash strings are on English, and not translated, on purpose, - # to avoid leaking the locale of the OnionShare user - if len(filenames) == 0: - flash('No files uploaded', 'info') - else: - for filename in filenames: - flash('Sent {}'.format(filename), 'info') - - if web.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(slug_candidate)) - - @web.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @web.app.route("/upload", methods=['POST']) - def upload_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return upload_logic() + @self.web.app.route("/upload", methods=['POST']) + def upload_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return upload_logic() - def close_logic(slug_candidate=''): - if web.common.settings.get('receive_allow_receiver_shutdown'): - web.force_shutdown() - r = make_response(render_template('closed.html')) - web.add_request(web.REQUEST_CLOSE_SERVER, request.path) - return web.add_security_headers(r) - else: - return redirect('/{}'.format(slug_candidate)) + def close_logic(slug_candidate=''): + if self.web.common.settings.get('receive_allow_receiver_shutdown'): + self.web.force_shutdown() + r = make_response(render_template('closed.html')) + self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path) + return self.web.add_security_headers(r) + else: + return redirect('/{}'.format(slug_candidate)) - @web.app.route("//close", methods=['POST']) - def close(slug_candidate): - web.check_slug_candidate(slug_candidate) - return close_logic(slug_candidate) + @self.web.app.route("//close", methods=['POST']) + def close(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return close_logic(slug_candidate) - @web.app.route("/close", methods=['POST']) - def close_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return close_logic() + @self.web.app.route("/close", methods=['POST']) + def close_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return close_logic() class ReceiveModeWSGIMiddleware(object): diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 58cc9b99..c8a411bb 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -8,176 +8,237 @@ from flask import Response, request, render_template, make_response from .. import strings -def share_routes(web): +class ShareModeWeb(object): """ - The web app routes for sharing files + All of the web logic for share mode """ - @web.app.route("/") - def index(slug_candidate): - web.check_slug_candidate(slug_candidate) - return index_logic() + def __init__(self, web): + self.web = web + self.define_routes() - @web.app.route("/") - def index_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return index_logic() - - def index_logic(slug_candidate=''): + def define_routes(self): """ - Render the template for the onionshare landing page. + The web app routes for sharing files """ - web.add_request(web.REQUEST_LOAD, request.path) + @self.web.app.route("/") + def index(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return index_logic() - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not web.stay_open and web.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return web.add_security_headers(r) + @self.web.app.route("/") + def index_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return index_logic() + + def index_logic(slug_candidate=''): + """ + Render the template for the onionshare landing page. + """ + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.web.stay_open and self.web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.web.add_security_headers(r) - # If download is allowed to continue, serve download page - if web.slug: - r = make_response(render_template( - 'send.html', - slug=web.slug, - file_info=web.file_info, - filename=os.path.basename(web.download_filename), - filesize=web.download_filesize, - filesize_human=web.common.human_readable_filesize(web.download_filesize), - is_zipped=web.is_zipped)) - else: # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - file_info=web.file_info, - filename=os.path.basename(web.download_filename), - filesize=web.download_filesize, - filesize_human=web.common.human_readable_filesize(web.download_filesize), - is_zipped=web.is_zipped)) - return web.add_security_headers(r) + if self.web.slug: + r = make_response(render_template( + 'send.html', + slug=self.web.slug, + file_info=self.web.file_info, + filename=os.path.basename(self.web.download_filename), + filesize=self.web.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), + is_zipped=self.web.is_zipped)) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=self.web.file_info, + filename=os.path.basename(self.web.download_filename), + filesize=self.web.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), + is_zipped=self.web.is_zipped)) + return self.web.add_security_headers(r) - @web.app.route("//download") - def download(slug_candidate): - web.check_slug_candidate(slug_candidate) - return download_logic() + @self.web.app.route("//download") + def download(slug_candidate): + self.web.check_slug_candidate(slug_candidate) + return download_logic() - @web.app.route("/download") - def download_public(): - if not web.common.settings.get('public_mode'): - return web.error404() - return download_logic() + @self.web.app.route("/download") + def download_public(): + if not self.web.common.settings.get('public_mode'): + return self.web.error404() + return download_logic() - def download_logic(slug_candidate=''): - """ - Download the zip file. - """ - # Deny new downloads if "Stop After First Download" is checked and there is - # currently a download - deny_download = not web.stay_open and web.download_in_progress - if deny_download: - r = make_response(render_template('denied.html')) - return web.add_security_headers(r) + def download_logic(slug_candidate=''): + """ + Download the zip file. + """ + # Deny new downloads if "Stop After First Download" is checked and there is + # currently a download + deny_download = not self.web.stay_open and self.web.download_in_progress + if deny_download: + r = make_response(render_template('denied.html')) + return self.web.add_security_headers(r) - # Each download has a unique id - download_id = web.download_count - web.download_count += 1 + # Each download has a unique id + download_id = self.web.download_count + self.web.download_count += 1 - # Prepare some variables to use inside generate() function below - # which is outside of the request context - shutdown_func = request.environ.get('werkzeug.server.shutdown') - path = request.path + # Prepare some variables to use inside generate() function below + # which is outside of the request context + shutdown_func = request.environ.get('werkzeug.server.shutdown') + path = request.path - # Tell GUI the download started - web.add_request(web.REQUEST_STARTED, path, { - 'id': download_id} - ) + # Tell GUI the download started + self.web.add_request(self.web.REQUEST_STARTED, path, { + 'id': download_id} + ) - dirname = os.path.dirname(web.download_filename) - basename = os.path.basename(web.download_filename) + dirname = os.path.dirname(self.web.download_filename) + basename = os.path.basename(self.web.download_filename) - def generate(): - # The user hasn't canceled the download - web.client_cancel = False + def generate(): + # The user hasn't canceled the download + self.web.client_cancel = False - # Starting a new download - if not web.stay_open: - web.download_in_progress = True + # Starting a new download + if not self.web.stay_open: + self.web.download_in_progress = True - chunk_size = 102400 # 100kb + chunk_size = 102400 # 100kb - fp = open(web.download_filename, 'rb') - web.done = False - canceled = False - while not web.done: - # The user has canceled the download, so stop serving the file - if web.client_cancel: - web.add_request(web.REQUEST_CANCELED, path, { - 'id': download_id - }) - break - - chunk = fp.read(chunk_size) - if chunk == b'': - web.done = True - else: - try: - yield chunk - - # tell GUI the progress - downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / web.download_filesize) * 100 - - # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not web.gui_mode or web.common.platform == 'Linux' or web.common.platform == 'BSD': - sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(web.common.human_readable_filesize(downloaded_bytes), percent)) - sys.stdout.flush() - - web.add_request(web.REQUEST_PROGRESS, path, { - 'id': download_id, - 'bytes': downloaded_bytes - }) - web.done = False - except: - # looks like the download was canceled - web.done = True - canceled = True - - # tell the GUI the download has canceled - web.add_request(web.REQUEST_CANCELED, path, { + fp = open(self.web.download_filename, 'rb') + self.web.done = False + canceled = False + while not self.web.done: + # The user has canceled the download, so stop serving the file + if self.web.client_cancel: + self.web.add_request(self.web.REQUEST_CANCELED, path, { 'id': download_id }) + break - fp.close() + chunk = fp.read(chunk_size) + if chunk == b'': + self.web.done = True + else: + try: + yield chunk - if web.common.platform != 'Darwin': - sys.stdout.write("\n") + # tell GUI the progress + downloaded_bytes = fp.tell() + percent = (1.0 * downloaded_bytes / self.web.download_filesize) * 100 - # Download is finished - if not web.stay_open: - web.download_in_progress = False + # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) + if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': + sys.stdout.write( + "\r{0:s}, {1:.2f}% ".format(self.web.common.human_readable_filesize(downloaded_bytes), percent)) + sys.stdout.flush() - # Close the server, if necessary - if not web.stay_open and not canceled: - print(strings._("closing_automatically")) - web.running = False - try: - if shutdown_func is None: - raise RuntimeError('Not running with the Werkzeug Server') - shutdown_func() - except: - pass + self.web.add_request(self.web.REQUEST_PROGRESS, path, { + 'id': download_id, + 'bytes': downloaded_bytes + }) + self.web.done = False + except: + # looks like the download was canceled + self.web.done = True + canceled = True - r = Response(generate()) - r.headers.set('Content-Length', web.download_filesize) - r.headers.set('Content-Disposition', 'attachment', filename=basename) - r = web.add_security_headers(r) - # guess content type - (content_type, _) = mimetypes.guess_type(basename, strict=False) - if content_type is not None: - r.headers.set('Content-Type', content_type) - return r + # tell the GUI the download has canceled + self.web.add_request(self.web.REQUEST_CANCELED, path, { + 'id': download_id + }) + + fp.close() + + if self.web.common.platform != 'Darwin': + sys.stdout.write("\n") + + # Download is finished + if not self.web.stay_open: + self.web.download_in_progress = False + + # Close the server, if necessary + if not self.web.stay_open and not canceled: + print(strings._("closing_automatically")) + self.web.running = False + try: + if shutdown_func is None: + raise RuntimeError('Not running with the Werkzeug Server') + shutdown_func() + except: + pass + + r = Response(generate()) + r.headers.set('Content-Length', self.web.download_filesize) + r.headers.set('Content-Disposition', 'attachment', filename=basename) + r = self.web.add_security_headers(r) + # guess content type + (content_type, _) = mimetypes.guess_type(basename, strict=False) + if content_type is not None: + r.headers.set('Content-Type', content_type) + return r + + def set_file_info(self, filenames, processed_size_callback=None): + """ + Using the list of filenames being shared, fill in details that the web + page will need to display. This includes zipping up the file in order to + get the zip file's name and size. + """ + self.web.common.log("Web", "set_file_info") + self.web.cancel_compression = False + + # build file info list + self.web.file_info = {'files': [], 'dirs': []} + for filename in filenames: + info = { + 'filename': filename, + 'basename': os.path.basename(filename.rstrip('/')) + } + if os.path.isfile(filename): + info['size'] = os.path.getsize(filename) + info['size_human'] = self.web.common.human_readable_filesize(info['size']) + self.web.file_info['files'].append(info) + if os.path.isdir(filename): + info['size'] = self.web.common.dir_size(filename) + info['size_human'] = self.web.common.human_readable_filesize(info['size']) + self.web.file_info['dirs'].append(info) + self.web.file_info['files'] = sorted(self.web.file_info['files'], key=lambda k: k['basename']) + self.web.file_info['dirs'] = sorted(self.web.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.web.file_info['files']) == 1 and len(self.web.file_info['dirs']) == 0: + self.web.is_zipped = False + self.web.download_filename = self.web.file_info['files'][0]['filename'] + self.web.download_filesize = self.web.file_info['files'][0]['size'] + else: + # Zip up the files and folders + self.web.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.web.download_filename = self.web.zip_writer.zip_filename + for info in self.web.file_info['files']: + self.web.zip_writer.add_file(info['filename']) + # Canceling early? + if self.web.cancel_compression: + self.web.zip_writer.close() + return False + + for info in self.web.file_info['dirs']: + if not self.web.zip_writer.add_dir(info['filename']): + return False + + self.web.zip_writer.close() + self.web.download_filesize = os.path.getsize(self.web.download_filename) + self.web.is_zipped = True + + return True class ZipWriter(object): diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 0a6e6964..7959ae0f 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -13,8 +13,8 @@ from flask import Flask, request, render_template, abort, make_response, __versi from .. import strings -from .share_mode import share_routes, ZipWriter -from .receive_mode import receive_routes, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest +from .share_mode import ShareModeWeb +from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeTemporaryFile, ReceiveModeRequest # Stub out flask's show_server_banner function, to avoiding showing warnings that @@ -41,7 +41,7 @@ class Web(object): REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE = 9 REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE = 10 - def __init__(self, common, gui_mode, receive_mode=False): + def __init__(self, common, is_gui, mode='share'): self.common = common # The flask app @@ -55,11 +55,11 @@ class Web(object): self.debug_mode() # Are we running in GUI mode? - self.gui_mode = gui_mode + self.is_gui = is_gui # Are we using receive mode? - self.receive_mode = receive_mode - if self.receive_mode: + self.mode = mode + if self.mode == 'receive': # Use custom WSGI middleware, to modify environ self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self) # Use a custom Request class to track upload progess @@ -115,14 +115,19 @@ class Web(object): # Keep track if the server is running self.running = False - # Define the ewb app routes - self.common_routes() - if self.receive_mode: - receive_routes(self) - else: - share_routes(self) + # Define the web app routes + self.define_common_routes() - def common_routes(self): + # Create the mode web object, which defines its own routes + self.share_mode = None + self.receive_mode = None + if self.mode == 'receive': + self.receive_mode = ReceiveModeWeb(self) + elif self.mode == 'share': + self.share_mode = ShareModeWeb(self) + + + def define_common_routes(self): """ Common web app routes between sending and receiving """ @@ -165,59 +170,6 @@ class Web(object): r.headers.set(header, value) return r - def set_file_info(self, filenames, processed_size_callback=None): - """ - Using the list of filenames being shared, fill in details that the web - page will need to display. This includes zipping up the file in order to - get the zip file's name and size. - """ - self.common.log("Web", "set_file_info") - self.cancel_compression = False - - # build file info list - self.file_info = {'files': [], 'dirs': []} - for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } - if os.path.isfile(filename): - info['size'] = os.path.getsize(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['files'].append(info) - if os.path.isdir(filename): - info['size'] = self.common.dir_size(filename) - info['size_human'] = self.common.human_readable_filesize(info['size']) - self.file_info['dirs'].append(info) - self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) - self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - - # Check if there's only 1 file and no folders - if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False - self.download_filename = self.file_info['files'][0]['filename'] - self.download_filesize = self.file_info['files'][0]['size'] - else: - # Zip up the files and folders - self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) - self.download_filename = self.zip_writer.zip_filename - for info in self.file_info['files']: - self.zip_writer.add_file(info['filename']) - # Canceling early? - if self.cancel_compression: - self.zip_writer.close() - return False - - for info in self.file_info['dirs']: - if not self.zip_writer.add_dir(info['filename']): - return False - - self.zip_writer.close() - self.download_filesize = os.path.getsize(self.download_filename) - self.is_zipped = True - - return True - def _safe_select_jinja_autoescape(self, filename): if filename is None: return True diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 8712653b..5845b30a 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -34,7 +34,7 @@ class ReceiveMode(Mode): Custom initialization for ReceiveMode. """ # Create the Web object - self.web = Web(self.common, True, True) + self.web = Web(self.common, True, 'receive') # Server status self.server_status.set_mode('receive') diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index aec32305..d7ed74ed 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -43,7 +43,7 @@ class ShareMode(Mode): self.compress_thread = None # Create the Web object - self.web = Web(self.common, True, False) + self.web = Web(self.common, True, 'share') # File selection self.file_selection = FileSelection(self.common) diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index dc43bf0a..4fb40bd0 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -41,7 +41,7 @@ class CompressThread(QtCore.QThread): self.mode.common.log('CompressThread', 'run') try: - if self.mode.web.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): self.success.emit() else: # Cancelled diff --git a/test/test_onionshare_web.py b/test/test_onionshare_web.py index 2209a0fd..24a0e163 100644 --- a/test/test_onionshare_web.py +++ b/test/test_onionshare_web.py @@ -38,11 +38,11 @@ DEFAULT_ZW_FILENAME_REGEX = re.compile(r'^onionshare_[a-z2-7]{6}.zip$') RANDOM_STR_REGEX = re.compile(r'^[a-z2-7]+$') -def web_obj(common_obj, receive_mode, num_files=0): +def web_obj(common_obj, mode, num_files=0): """ Creates a Web object, in either share mode or receive mode, ready for testing """ common_obj.load_settings() - web = Web(common_obj, False, receive_mode) + web = Web(common_obj, False, mode) web.generate_slug() web.stay_open = True web.running = True @@ -50,14 +50,14 @@ def web_obj(common_obj, receive_mode, num_files=0): web.app.testing = True # Share mode - if not receive_mode: + if mode == 'share': # Add files files = [] for i in range(num_files): with tempfile.NamedTemporaryFile(delete=False) as tmp_file: tmp_file.write(b'*' * 1024) files.append(tmp_file.name) - web.set_file_info(files) + web.share_mode.set_file_info(files) # Receive mode else: pass @@ -67,8 +67,8 @@ def web_obj(common_obj, receive_mode, num_files=0): class TestWeb: def test_share_mode(self, common_obj): - web = web_obj(common_obj, False, 3) - assert web.receive_mode is False + web = web_obj(common_obj, 'share', 3) + assert web.mode is 'share' with web.app.test_client() as c: # Load 404 pages res = c.get('/') @@ -91,7 +91,7 @@ class TestWeb: assert res.mimetype == 'application/zip' def test_share_mode_close_after_first_download_on(self, common_obj, temp_file_1024): - web = web_obj(common_obj, False, 3) + web = web_obj(common_obj, 'share', 3) web.stay_open = False assert web.running == True @@ -106,7 +106,7 @@ class TestWeb: assert web.running == False def test_share_mode_close_after_first_download_off(self, common_obj, temp_file_1024): - web = web_obj(common_obj, False, 3) + web = web_obj(common_obj, 'share', 3) web.stay_open = True assert web.running == True @@ -120,8 +120,8 @@ class TestWeb: assert web.running == True def test_receive_mode(self, common_obj): - web = web_obj(common_obj, True) - assert web.receive_mode is True + web = web_obj(common_obj, 'receive') + assert web.mode is 'receive' with web.app.test_client() as c: # Load 404 pages @@ -139,7 +139,7 @@ class TestWeb: assert res.status_code == 200 def test_receive_mode_allow_receiver_shutdown_on(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('receive_allow_receiver_shutdown', True) @@ -154,7 +154,7 @@ class TestWeb: assert web.running == False def test_receive_mode_allow_receiver_shutdown_off(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('receive_allow_receiver_shutdown', False) @@ -167,9 +167,9 @@ class TestWeb: # Should redirect to index, and server should still be running assert res.status_code == 302 assert web.running == True - + def test_public_mode_on(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', True) with web.app.test_client() as c: @@ -182,9 +182,9 @@ class TestWeb: res = c.get('/{}'.format(web.slug)) data2 = res.get_data() assert res.status_code == 404 - + def test_public_mode_off(self, common_obj): - web = web_obj(common_obj, True) + web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', False) with web.app.test_client() as c: From 05a2ee0559ff968cfd87c125295c01c8b52ae6ca Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:19:36 -0700 Subject: [PATCH 48/87] Refactor the CLI main function to explicitly use 'share' or 'receive' mode --- onionshare/__init__.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 9c390fa8..64655f4a 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -65,13 +65,18 @@ def main(cwd=None): receive = bool(args.receive) config = args.config + if receive: + mode = 'receive' + else: + mode = 'share' + # Make sure filenames given if not using receiver mode - if not receive and len(filenames) == 0: + if mode == 'share' and len(filenames) == 0: print(strings._('no_filenames')) sys.exit() # Validate filenames - if not receive: + if mode == 'share': valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): @@ -90,7 +95,7 @@ def main(cwd=None): common.debug = debug # Create the Web object - web = Web(common, False, receive) + web = Web(common, False, mode) # Start the Onion object onion = Onion(common) @@ -116,21 +121,22 @@ def main(cwd=None): print(e.args[0]) sys.exit() - # Prepare files to share - print(strings._("preparing_files")) - try: - web.share_mode.set_file_info(filenames) - if web.is_zipped: - app.cleanup_filenames.append(web.download_filename) - except OSError as e: - print(e.strerror) - sys.exit(1) + if mode == 'share': + # Prepare files to share + print(strings._("preparing_files")) + try: + web.share_mode.set_file_info(filenames) + if web.is_zipped: + app.cleanup_filenames.append(web.download_filename) + except OSError as e: + print(e.strerror) + sys.exit(1) - # Warn about sending large files over Tor - if web.download_filesize >= 157286400: # 150mb - print('') - print(strings._("large_filesize")) - print('') + # Warn about sending large files over Tor + if web.download_filesize >= 157286400: # 150mb + print('') + print(strings._("large_filesize")) + print('') # Start OnionShare http service in new thread t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug'))) @@ -158,7 +164,7 @@ def main(cwd=None): url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) print('') - if receive: + if mode == 'receive': print(strings._('receive_mode_downloads_dir').format(common.settings.get('downloads_dir'))) print('') print(strings._('receive_mode_warning')) From 4127aa4d7130c70bd82f7433d15d3d96aadb16ae Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:36:19 -0700 Subject: [PATCH 49/87] Move more mode-specific logic out of the Web class and into the approprate mode web classes --- onionshare/__init__.py | 17 ++-- onionshare/web/receive_mode.py | 7 +- onionshare/web/share_mode.py | 103 ++++++++++++++---------- onionshare/web/web.py | 28 +------ onionshare_gui/receive_mode/__init__.py | 2 +- onionshare_gui/share_mode/__init__.py | 10 +-- onionshare_gui/share_mode/threads.py | 4 +- 7 files changed, 87 insertions(+), 84 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 64655f4a..9e3fefdd 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -126,14 +126,14 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.share_mode.set_file_info(filenames) - if web.is_zipped: - app.cleanup_filenames.append(web.download_filename) + if web.share_mode.is_zipped: + app.cleanup_filenames.append(web.share_mode.download_filename) except OSError as e: print(e.strerror) sys.exit(1) # Warn about sending large files over Tor - if web.download_filesize >= 157286400: # 150mb + if web.share_mode.download_filesize >= 157286400: # 150mb print('') print(strings._("large_filesize")) print('') @@ -193,11 +193,12 @@ def main(cwd=None): if app.shutdown_timeout > 0: # if the shutdown timer was set and has run out, stop the server if not 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: - print(strings._("close_on_timeout")) - web.stop(app.port) - break + if mode == 'share': + # If there were no attempts to download the share, or all downloads are done, we can stop + if web.share_mode.download_count == 0 or web.done: + print(strings._("close_on_timeout")) + web.stop(app.port) + break # Allow KeyboardInterrupt exception to be handled with threads # https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception time.sleep(0.2) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index ab5f5f13..3784ebf8 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -14,6 +14,9 @@ class ReceiveModeWeb(object): """ def __init__(self, web): self.web = web + + self.upload_count = 0 + self.define_routes() def define_routes(self): @@ -243,8 +246,8 @@ class ReceiveModeRequest(Request): self.progress = {} # Create an upload_id, attach it to the request - self.upload_id = self.web.upload_count - self.web.upload_count += 1 + self.upload_id = self.upload_count + self.upload_count += 1 # Figure out the content length try: diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index c8a411bb..21f0d1e5 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -14,6 +14,25 @@ class ShareModeWeb(object): """ def __init__(self, web): self.web = web + + # Information about the file to be shared + self.file_info = [] + self.is_zipped = False + self.download_filename = None + self.download_filesize = None + self.zip_writer = None + + self.download_count = 0 + + # If "Stop After First Download" is checked (stay_open == False), only allow + # one download at a time. + self.download_in_progress = False + + # If the client closes the OnionShare window while a download is in progress, + # it should immediately stop serving the file. The client_cancel global is + # used to tell the download function that the client is canceling the download. + self.client_cancel = False + self.define_routes() def define_routes(self): @@ -39,7 +58,7 @@ class ShareModeWeb(object): # Deny new downloads if "Stop After First Download" is checked and there is # currently a download - deny_download = not self.web.stay_open and self.web.download_in_progress + deny_download = not self.web.stay_open and self.download_in_progress if deny_download: r = make_response(render_template('denied.html')) return self.web.add_security_headers(r) @@ -49,20 +68,20 @@ class ShareModeWeb(object): r = make_response(render_template( 'send.html', slug=self.web.slug, - file_info=self.web.file_info, - filename=os.path.basename(self.web.download_filename), - filesize=self.web.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), - is_zipped=self.web.is_zipped)) + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page r = make_response(render_template( 'send.html', - file_info=self.web.file_info, - filename=os.path.basename(self.web.download_filename), - filesize=self.web.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.web.download_filesize), - is_zipped=self.web.is_zipped)) + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + filesize=self.download_filesize, + filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @self.web.app.route("//download") @@ -82,14 +101,14 @@ class ShareModeWeb(object): """ # Deny new downloads if "Stop After First Download" is checked and there is # currently a download - deny_download = not self.web.stay_open and self.web.download_in_progress + deny_download = not self.web.stay_open and self.download_in_progress if deny_download: r = make_response(render_template('denied.html')) return self.web.add_security_headers(r) # Each download has a unique id - download_id = self.web.download_count - self.web.download_count += 1 + download_id = self.download_count + self.download_count += 1 # Prepare some variables to use inside generate() function below # which is outside of the request context @@ -101,25 +120,25 @@ class ShareModeWeb(object): 'id': download_id} ) - dirname = os.path.dirname(self.web.download_filename) - basename = os.path.basename(self.web.download_filename) + dirname = os.path.dirname(self.download_filename) + basename = os.path.basename(self.download_filename) def generate(): # The user hasn't canceled the download - self.web.client_cancel = False + self.client_cancel = False # Starting a new download if not self.web.stay_open: - self.web.download_in_progress = True + self.download_in_progress = True chunk_size = 102400 # 100kb - fp = open(self.web.download_filename, 'rb') + fp = open(self.download_filename, 'rb') self.web.done = False canceled = False while not self.web.done: # The user has canceled the download, so stop serving the file - if self.web.client_cancel: + if self.client_cancel: self.web.add_request(self.web.REQUEST_CANCELED, path, { 'id': download_id }) @@ -134,7 +153,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.web.download_filesize) * 100 + percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': @@ -164,7 +183,7 @@ class ShareModeWeb(object): # Download is finished if not self.web.stay_open: - self.web.download_in_progress = False + self.download_in_progress = False # Close the server, if necessary if not self.web.stay_open and not canceled: @@ -178,7 +197,7 @@ class ShareModeWeb(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.web.download_filesize) + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type @@ -197,7 +216,7 @@ class ShareModeWeb(object): self.web.cancel_compression = False # build file info list - self.web.file_info = {'files': [], 'dirs': []} + self.file_info = {'files': [], 'dirs': []} for filename in filenames: info = { 'filename': filename, @@ -206,37 +225,37 @@ class ShareModeWeb(object): if os.path.isfile(filename): info['size'] = os.path.getsize(filename) info['size_human'] = self.web.common.human_readable_filesize(info['size']) - self.web.file_info['files'].append(info) + self.file_info['files'].append(info) if os.path.isdir(filename): info['size'] = self.web.common.dir_size(filename) info['size_human'] = self.web.common.human_readable_filesize(info['size']) - self.web.file_info['dirs'].append(info) - self.web.file_info['files'] = sorted(self.web.file_info['files'], key=lambda k: k['basename']) - self.web.file_info['dirs'] = sorted(self.web.file_info['dirs'], key=lambda k: k['basename']) + self.file_info['dirs'].append(info) + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) # Check if there's only 1 file and no folders - if len(self.web.file_info['files']) == 1 and len(self.web.file_info['dirs']) == 0: - self.web.is_zipped = False - self.web.download_filename = self.web.file_info['files'][0]['filename'] - self.web.download_filesize = self.web.file_info['files'][0]['size'] + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.is_zipped = False + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] else: # Zip up the files and folders - self.web.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) - self.web.download_filename = self.web.zip_writer.zip_filename - for info in self.web.file_info['files']: - self.web.zip_writer.add_file(info['filename']) + self.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.download_filename = self.zip_writer.zip_filename + for info in self.file_info['files']: + self.zip_writer.add_file(info['filename']) # Canceling early? if self.web.cancel_compression: - self.web.zip_writer.close() + self.zip_writer.close() return False - for info in self.web.file_info['dirs']: - if not self.web.zip_writer.add_dir(info['filename']): + for info in self.file_info['dirs']: + if not self.zip_writer.add_dir(info['filename']): return False - self.web.zip_writer.close() - self.web.download_filesize = os.path.getsize(self.web.download_filename) - self.web.is_zipped = True + self.zip_writer.close() + self.download_filesize = os.path.getsize(self.download_filename) + self.is_zipped = True return True diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 7959ae0f..9046154a 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -73,13 +73,6 @@ class Web(object): # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape - # Information about the file - self.file_info = [] - self.is_zipped = False - self.download_filename = None - self.download_filesize = None - self.zip_writer = None - self.security_headers = [ ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), ('X-Frame-Options', 'DENY'), @@ -90,25 +83,11 @@ class Web(object): ] self.q = queue.Queue() - self.slug = None - - self.download_count = 0 - self.upload_count = 0 - self.error404_count = 0 - # If "Stop After First Download" is checked (stay_open == False), only allow - # one download at a time. - self.download_in_progress = False - self.done = False - # If the client closes the OnionShare window while a download is in progress, - # it should immediately stop serving the file. The client_cancel global is - # used to tell the download function that the client is canceling the download. - self.client_cancel = False - # shutting down the server only works within the context of flask, so the easiest way to do it is over http self.shutdown_slug = self.common.random_string(16) @@ -254,9 +233,10 @@ class Web(object): Stop the flask web server by loading /shutdown. """ - # If the user cancels the download, let the download function know to stop - # serving the file - self.client_cancel = True + if self.mode == 'share': + # If the user cancels the download, let the download function know to stop + # serving the file + self.share_mode.client_cancel = True # To stop flask, load http://127.0.0.1://shutdown if self.running: diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py index 5845b30a..590dec65 100644 --- a/onionshare_gui/receive_mode/__init__.py +++ b/onionshare_gui/receive_mode/__init__.py @@ -100,7 +100,7 @@ class ReceiveMode(Mode): Starting the server. """ # Reset web counters - self.web.upload_count = 0 + self.web.receive_mode.upload_count = 0 self.web.error404_count = 0 # Hide and reset the uploads if we have previously shared diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index d7ed74ed..52ec672e 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -125,7 +125,7 @@ class ShareMode(Mode): The shutdown timer expired, should we stop the server? Returns a bool """ # If there were no attempts to download the share, or all downloads are done, we can stop - if self.web.download_count == 0 or self.web.done: + if self.web.share_mode.download_count == 0 or self.web.done: self.server_status.stop_server() self.server_status_label.setText(strings._('close_on_timeout', True)) return True @@ -139,7 +139,7 @@ class ShareMode(Mode): Starting the server. """ # Reset web counters - self.web.download_count = 0 + self.web.share_mode.download_count = 0 self.web.error404_count = 0 # Hide and reset the downloads if we have previously shared @@ -177,7 +177,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.download_filesize >= 157286400: # 150mb + if self.web.share_mode.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -229,7 +229,7 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.download_filesize) + self.downloads.add(event["data"]["id"], self.web.share_mode.download_filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -242,7 +242,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.download_filesize: + if event["data"]["bytes"] == self.web.share_mode.download_filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 4fb40bd0..6e114d62 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,8 +47,8 @@ class CompressThread(QtCore.QThread): # Cancelled pass - if self.mode.web.is_zipped: - self.mode.app.cleanup_filenames.append(self.mode.web.download_filename) + if self.mode.web.share_mode.is_zipped: + self.mode.app.cleanup_filenames.append(self.mode.web.share_mode.download_filename) except OSError as e: self.error.emit(e.strerror) From 5003d44cfb32ef78cceb1e6cdd8a316b830c2c16 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 11:41:49 -0700 Subject: [PATCH 50/87] Pass common into ShareModeWeb and ReceiveModeWeb --- onionshare/web/receive_mode.py | 35 ++++++++++++++++++---------------- onionshare/web/share_mode.py | 29 +++++++++++++++------------- onionshare/web/web.py | 5 +++-- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 3784ebf8..c422d74e 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -12,7 +12,10 @@ class ReceiveModeWeb(object): """ All of the web logic for receive mode """ - def __init__(self, web): + def __init__(self, common, web): + self.common = common + self.common.log('ReceiveModeWeb', '__init__') + self.web = web self.upload_count = 0 @@ -26,7 +29,7 @@ class ReceiveModeWeb(object): def index_logic(): self.web.add_request(self.web.REQUEST_LOAD, request.path) - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): upload_action = '/upload' close_action = '/close' else: @@ -37,7 +40,7 @@ class ReceiveModeWeb(object): 'receive.html', upload_action=upload_action, close_action=close_action, - receive_allow_receiver_shutdown=self.web.common.settings.get('receive_allow_receiver_shutdown'))) + receive_allow_receiver_shutdown=self.common.settings.get('receive_allow_receiver_shutdown'))) return self.web.add_security_headers(r) @self.web.app.route("/") @@ -47,7 +50,7 @@ class ReceiveModeWeb(object): @self.web.app.route("/") def index_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return index_logic() @@ -59,18 +62,18 @@ class ReceiveModeWeb(object): # Make sure downloads_dir exists valid = True try: - self.web.common.validate_downloads_dir() + self.common.validate_downloads_dir() except DownloadsDirErrorCannotCreate: self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_CANNOT_CREATE, request.path) - print(strings._('error_cannot_create_downloads_dir').format(self.web.common.settings.get('downloads_dir'))) + print(strings._('error_cannot_create_downloads_dir').format(self.common.settings.get('downloads_dir'))) valid = False except DownloadsDirErrorNotWritable: self.web.add_request(self.web.REQUEST_ERROR_DOWNLOADS_DIR_NOT_WRITABLE, request.path) - print(strings._('error_downloads_dir_not_writable').format(self.web.common.settings.get('downloads_dir'))) + print(strings._('error_downloads_dir_not_writable').format(self.common.settings.get('downloads_dir'))) valid = False if not valid: flash('Error uploading, please inform the OnionShare user', 'error') - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -83,7 +86,7 @@ class ReceiveModeWeb(object): # Automatically rename the file, if a file of the same name already exists filename = secure_filename(f.filename) filenames.append(filename) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), filename) if os.path.exists(local_path): if '.' in filename: # Add "-i", e.g. change "foo.txt" to "foo-2.txt" @@ -95,7 +98,7 @@ class ReceiveModeWeb(object): valid = False while not valid: new_filename = '{}-{}.{}'.format('.'.join(name), i, ext) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) if os.path.exists(local_path): i += 1 else: @@ -106,7 +109,7 @@ class ReceiveModeWeb(object): valid = False while not valid: new_filename = '{}-{}'.format(filename, i) - local_path = os.path.join(self.web.common.settings.get('downloads_dir'), new_filename) + local_path = os.path.join(self.common.settings.get('downloads_dir'), new_filename) if os.path.exists(local_path): i += 1 else: @@ -121,7 +124,7 @@ class ReceiveModeWeb(object): 'new_filename': basename }) - self.web.common.log('Web', 'receive_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) + self.common.log('ReceiveModeWeb', 'define_routes', '/upload, uploaded {}, saving to {}'.format(f.filename, local_path)) print(strings._('receive_mode_received_file').format(local_path)) f.save(local_path) @@ -133,7 +136,7 @@ class ReceiveModeWeb(object): for filename in filenames: flash('Sent {}'.format(filename), 'info') - if self.web.common.settings.get('public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -145,13 +148,13 @@ class ReceiveModeWeb(object): @self.web.app.route("/upload", methods=['POST']) def upload_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return upload_logic() def close_logic(slug_candidate=''): - if self.web.common.settings.get('receive_allow_receiver_shutdown'): + if self.common.settings.get('receive_allow_receiver_shutdown'): self.web.force_shutdown() r = make_response(render_template('closed.html')) self.web.add_request(self.web.REQUEST_CLOSE_SERVER, request.path) @@ -166,7 +169,7 @@ class ReceiveModeWeb(object): @self.web.app.route("/close", methods=['POST']) def close_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return close_logic() diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 21f0d1e5..81e5a5b9 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -12,7 +12,10 @@ class ShareModeWeb(object): """ All of the web logic for share mode """ - def __init__(self, web): + def __init__(self, common, web): + self.common = common + self.common.log('ShareModeWeb', '__init__') + self.web = web # Information about the file to be shared @@ -46,7 +49,7 @@ class ShareModeWeb(object): @self.web.app.route("/") def index_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return index_logic() @@ -71,7 +74,7 @@ class ShareModeWeb(object): file_info=self.file_info, filename=os.path.basename(self.download_filename), filesize=self.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: # If download is allowed to continue, serve download page @@ -80,7 +83,7 @@ class ShareModeWeb(object): file_info=self.file_info, filename=os.path.basename(self.download_filename), filesize=self.download_filesize, - filesize_human=self.web.common.human_readable_filesize(self.download_filesize), + filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -91,7 +94,7 @@ class ShareModeWeb(object): @self.web.app.route("/download") def download_public(): - if not self.web.common.settings.get('public_mode'): + if not self.common.settings.get('public_mode'): return self.web.error404() return download_logic() @@ -156,9 +159,9 @@ class ShareModeWeb(object): percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) - if not self.web.is_gui or self.web.common.platform == 'Linux' or self.web.common.platform == 'BSD': + if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': sys.stdout.write( - "\r{0:s}, {1:.2f}% ".format(self.web.common.human_readable_filesize(downloaded_bytes), percent)) + "\r{0:s}, {1:.2f}% ".format(self.common.human_readable_filesize(downloaded_bytes), percent)) sys.stdout.flush() self.web.add_request(self.web.REQUEST_PROGRESS, path, { @@ -178,7 +181,7 @@ class ShareModeWeb(object): fp.close() - if self.web.common.platform != 'Darwin': + if self.common.platform != 'Darwin': sys.stdout.write("\n") # Download is finished @@ -212,7 +215,7 @@ class ShareModeWeb(object): page will need to display. This includes zipping up the file in order to get the zip file's name and size. """ - self.web.common.log("Web", "set_file_info") + self.common.log("ShareModeWeb", "set_file_info") self.web.cancel_compression = False # build file info list @@ -224,11 +227,11 @@ class ShareModeWeb(object): } if os.path.isfile(filename): info['size'] = os.path.getsize(filename) - info['size_human'] = self.web.common.human_readable_filesize(info['size']) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['files'].append(info) if os.path.isdir(filename): - info['size'] = self.web.common.dir_size(filename) - info['size_human'] = self.web.common.human_readable_filesize(info['size']) + info['size'] = self.common.dir_size(filename) + info['size_human'] = self.common.human_readable_filesize(info['size']) self.file_info['dirs'].append(info) self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) @@ -240,7 +243,7 @@ class ShareModeWeb(object): self.download_filesize = self.file_info['files'][0]['size'] else: # Zip up the files and folders - self.zip_writer = ZipWriter(self.web.common, processed_size_callback=processed_size_callback) + self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) self.download_filename = self.zip_writer.zip_filename for info in self.file_info['files']: self.zip_writer.add_file(info['filename']) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 9046154a..52c4da16 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -43,6 +43,7 @@ class Web(object): def __init__(self, common, is_gui, mode='share'): self.common = common + self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode)) # The flask app self.app = Flask(__name__, @@ -101,9 +102,9 @@ class Web(object): self.share_mode = None self.receive_mode = None if self.mode == 'receive': - self.receive_mode = ReceiveModeWeb(self) + self.receive_mode = ReceiveModeWeb(self.common, self) elif self.mode == 'share': - self.share_mode = ShareModeWeb(self) + self.share_mode = ShareModeWeb(self.common, self) def define_common_routes(self): From e941ce0fd7eeacd0bc3ef26c00f863d4c306abca Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 12:29:23 -0700 Subject: [PATCH 51/87] If only sharing one file, compress it with gzip, and serve it with gzip compression if the browser supports it --- onionshare/__init__.py | 3 +- onionshare/web/share_mode.py | 62 +++++++++++++++++++++++++--- onionshare_gui/share_mode/threads.py | 3 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 9e3fefdd..4d6d77d0 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -126,8 +126,7 @@ def main(cwd=None): print(strings._("preparing_files")) try: web.share_mode.set_file_info(filenames) - if web.share_mode.is_zipped: - app.cleanup_filenames.append(web.share_mode.download_filename) + app.cleanup_filenames += web.share_mode.cleanup_filenames except OSError as e: print(e.strerror) sys.exit(1) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 81e5a5b9..95bc8443 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -3,6 +3,7 @@ import sys import tempfile import zipfile import mimetypes +import gzip from flask import Response, request, render_template, make_response from .. import strings @@ -23,6 +24,7 @@ class ShareModeWeb(object): self.is_zipped = False self.download_filename = None self.download_filesize = None + self.gzip_filename = None self.zip_writer = None self.download_count = 0 @@ -118,12 +120,20 @@ class ShareModeWeb(object): shutdown_func = request.environ.get('werkzeug.server.shutdown') path = request.path + # If this is a zipped file, then serve as-is. If it's not zipped, then, + # if the http client supports gzip compression, gzip the file first + # and serve that + use_gzip = (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + if use_gzip: + file_to_download = self.gzip_filename + else: + file_to_download = self.download_filename + # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { - 'id': download_id} - ) + 'id': download_id + }) - dirname = os.path.dirname(self.download_filename) basename = os.path.basename(self.download_filename) def generate(): @@ -136,7 +146,7 @@ class ShareModeWeb(object): chunk_size = 102400 # 100kb - fp = open(self.download_filename, 'rb') + fp = open(file_to_download, 'rb') self.web.done = False canceled = False while not self.web.done: @@ -200,7 +210,11 @@ class ShareModeWeb(object): pass r = Response(generate()) - r.headers.set('Content-Length', self.download_filesize) + if use_gzip: + r.headers.set('Content-Encoding', 'gzip') + r.headers.set('Content-Length', os.path.getsize(self.gzip_filename)) + else: + r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type @@ -218,6 +232,8 @@ class ShareModeWeb(object): self.common.log("ShareModeWeb", "set_file_info") self.web.cancel_compression = False + self.cleanup_filenames = [] + # build file info list self.file_info = {'files': [], 'dirs': []} for filename in filenames: @@ -238,9 +254,18 @@ class ShareModeWeb(object): # Check if there's only 1 file and no folders if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.is_zipped = False self.download_filename = self.file_info['files'][0]['filename'] self.download_filesize = self.file_info['files'][0]['size'] + + # Compress the file with gzip now, so we don't have to do it on each request + self.gzip_filename = tempfile.mkstemp('wb+')[1] + self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback) + + # Make sure the gzip file gets cleaned up when onionshare stops + self.cleanup_filenames.append(self.gzip_filename) + + self.is_zipped = False + else: # Zip up the files and folders self.zip_writer = ZipWriter(self.common, processed_size_callback=processed_size_callback) @@ -258,10 +283,35 @@ class ShareModeWeb(object): self.zip_writer.close() self.download_filesize = os.path.getsize(self.download_filename) + + # Make sure the zip file gets cleaned up when onionshare stops + self.cleanup_filenames.append(self.zip_writer.zip_filename) + self.is_zipped = True return True + def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None): + """ + Compress a file with gzip, without loading the whole thing into memory + Thanks: https://stackoverflow.com/questions/27035296/python-how-to-gzip-a-large-text-file-without-memoryerror + """ + bytes_processed = 0 + blocksize = 1 << 16 # 64kB + with open(input_filename, 'rb') as input_file: + output_file = gzip.open(output_filename, 'wb', level) + while True: + if processed_size_callback is not None: + processed_size_callback(bytes_processed) + + block = input_file.read(blocksize) + if len(block) == 0: + break + output_file.write(block) + bytes_processed += blocksize + + output_file.close() + class ZipWriter(object): """ diff --git a/onionshare_gui/share_mode/threads.py b/onionshare_gui/share_mode/threads.py index 6e114d62..d6022746 100644 --- a/onionshare_gui/share_mode/threads.py +++ b/onionshare_gui/share_mode/threads.py @@ -47,8 +47,7 @@ class CompressThread(QtCore.QThread): # Cancelled pass - if self.mode.web.share_mode.is_zipped: - self.mode.app.cleanup_filenames.append(self.mode.web.share_mode.download_filename) + self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames except OSError as e: self.error.emit(e.strerror) From 13791ec1abb7c3386ce3013f51e69f3e3a208a38 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 13:38:01 -0700 Subject: [PATCH 52/87] Remove unused wait_for_hs string, and change tests to test with a different string. Also remove a few other unused strings --- share/locale/cs.json | 1 - share/locale/da.json | 3 --- share/locale/de.json | 1 - share/locale/en.json | 1 - share/locale/eo.json | 1 - share/locale/es.json | 1 - share/locale/fi.json | 1 - share/locale/fr.json | 1 - share/locale/it.json | 1 - share/locale/nl.json | 3 --- share/locale/tr.json | 1 - test/test_onionshare_strings.py | 12 ++---------- 12 files changed, 2 insertions(+), 25 deletions(-) diff --git a/share/locale/cs.json b/share/locale/cs.json index 40e48f87..a29c8b8e 100644 --- a/share/locale/cs.json +++ b/share/locale/cs.json @@ -1,7 +1,6 @@ { "config_onion_service": "Nastavuji onion service na portu {0:d}.", "preparing_files": "Připravuji soubory ke sdílení.", - "wait_for_hs": "Čekám na HS až bude připravena:", "give_this_url": "Dejte tuto URL osobě, které dané soubory posíláte:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Stiskněte Ctrl-C pro zastavení serveru", diff --git a/share/locale/da.json b/share/locale/da.json index 00539212..75416989 100644 --- a/share/locale/da.json +++ b/share/locale/da.json @@ -1,7 +1,6 @@ { "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:", "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", @@ -53,9 +52,7 @@ "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", diff --git a/share/locale/de.json b/share/locale/de.json index 6c0fa861..1d0436a0 100644 --- a/share/locale/de.json +++ b/share/locale/de.json @@ -1,6 +1,5 @@ { "preparing_files": "Dateien werden vorbereitet.", - "wait_for_hs": "Warte auf HS:", "give_this_url": "Geben Sie diese URL der Person, der Sie die Datei zusenden möchten:", "ctrlc_to_stop": "Drücken Sie Strg+C um den Server anzuhalten", "not_a_file": "{0:s} ist keine Datei.", diff --git a/share/locale/en.json b/share/locale/en.json index e6b2b2c0..fbf83118 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,7 +1,6 @@ { "config_onion_service": "Configuring onion service on port {0:d}.", "preparing_files": "Preparing files to share.", - "wait_for_hs": "Waiting for HS to be ready:", "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:", "give_this_url_receive": "Give this address to the people sending you files:", diff --git a/share/locale/eo.json b/share/locale/eo.json index 18e73165..1fbda9bc 100644 --- a/share/locale/eo.json +++ b/share/locale/eo.json @@ -1,7 +1,6 @@ { "config_onion_service": "Agordas onion service je pordo {0:d}.", "preparing_files": "Preparas dosierojn por kundivido.", - "wait_for_hs": "Atendas al hidden sevice por esti preta:", "give_this_url": "Donu ĉi tiun URL al la persono al kiu vi sendas la dosieron:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Presu Ctrl-C por halti la servilon", diff --git a/share/locale/es.json b/share/locale/es.json index b829540a..8c9945a8 100644 --- a/share/locale/es.json +++ b/share/locale/es.json @@ -1,6 +1,5 @@ { "preparing_files": "Preparando los archivos para compartir.", - "wait_for_hs": "Esperando a que HS esté listo:", "give_this_url": "Entregue esta URL a la persona a la que está enviando el archivo:", "ctrlc_to_stop": "Pulse Ctrl-C para detener el servidor", "not_a_file": "{0:s} no es un archivo.", diff --git a/share/locale/fi.json b/share/locale/fi.json index 09186be8..8a360284 100644 --- a/share/locale/fi.json +++ b/share/locale/fi.json @@ -1,6 +1,5 @@ { "preparing_files": "Valmistellaan tiedostoja jaettavaksi.", - "wait_for_hs": "Odotetaan piilopalvelun valmistumista:", "give_this_url": "Anna tämä URL-osoite henkilölle, jolle lähetät tiedostot:", "ctrlc_to_stop": "Näppäin Ctrl-C pysäyttää palvelimen", "not_a_file": "{0:s} Ei ole tiedosto.", diff --git a/share/locale/fr.json b/share/locale/fr.json index b6f6eaa7..967e456e 100644 --- a/share/locale/fr.json +++ b/share/locale/fr.json @@ -1,6 +1,5 @@ { "preparing_files": "Préparation des fichiers à partager.", - "wait_for_hs": "En attente du HS:", "give_this_url": "Donnez cette URL à la personne qui doit recevoir le fichier :", "ctrlc_to_stop": "Ctrl-C arrête le serveur", "not_a_file": "{0:s} n'est pas un fichier.", diff --git a/share/locale/it.json b/share/locale/it.json index 304e0cb9..6c55ed20 100644 --- a/share/locale/it.json +++ b/share/locale/it.json @@ -1,6 +1,5 @@ { "preparing_files": "Preparazione dei files da condividere.", - "wait_for_hs": "In attesa che l'HS sia pronto:", "give_this_url": "Dai questo URL alla persona a cui vuoi inviare il file:", "ctrlc_to_stop": "Premi Ctrl-C per fermare il server", "not_a_file": "{0:s} non è un file.", diff --git a/share/locale/nl.json b/share/locale/nl.json index 67297ae0..833432ab 100644 --- a/share/locale/nl.json +++ b/share/locale/nl.json @@ -1,7 +1,6 @@ { "config_onion_service": "Onion service configureren op poort {0:d}.", "preparing_files": "Bestanden om te delen aan het voorbereiden.", - "wait_for_hs": "Wachten op gereed zijn van HS:", "give_this_url": "Geef deze URL aan de persoon aan wie je dit bestand verzend:", "give_this_url_stealth": "Geef deze URL en de HidServAuth regel aan de persoon aan wie je dit bestand verzend:", "ctrlc_to_stop": "Druk Ctrl-C om de server te stoppen", @@ -51,9 +50,7 @@ "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_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.", "gui_settings_autoupdate_label": "Controleer voor updates", "gui_settings_autoupdate_option": "Notificeer me als er updates beschikbaar zijn", "gui_settings_autoupdate_timestamp": "Laatste controle: {}", diff --git a/share/locale/tr.json b/share/locale/tr.json index 7b531bd6..71f16daa 100644 --- a/share/locale/tr.json +++ b/share/locale/tr.json @@ -1,6 +1,5 @@ { "preparing_files": "Paylaşmak için dosyalar hazırlanıyor.", - "wait_for_hs": "GH hazır olması bekleniyor:", "give_this_url": "Dosyayı gönderdiğin kişiye bu URL'i verin:", "ctrlc_to_stop": "Sunucuyu durdurmak için, Ctrl-C basın", "not_a_file": "{0:s} dosya değil.", diff --git a/test/test_onionshare_strings.py b/test/test_onionshare_strings.py index d1daa1e5..1d0b3206 100644 --- a/test/test_onionshare_strings.py +++ b/test/test_onionshare_strings.py @@ -47,22 +47,14 @@ class TestLoadStrings: self, common_obj, locale_en, sys_onionshare_dev_mode): """ load_strings() loads English by default """ strings.load_strings(common_obj) - assert strings._('wait_for_hs') == "Waiting for HS to be ready:" + assert strings._('preparing_files') == "Preparing files to share." def test_load_strings_loads_other_languages( self, common_obj, locale_fr, sys_onionshare_dev_mode): """ load_strings() loads other languages in different locales """ strings.load_strings(common_obj, "fr") - assert strings._('wait_for_hs') == "En attente du HS:" - - def test_load_partial_strings( - self, common_obj, locale_ru, sys_onionshare_dev_mode): - strings.load_strings(common_obj) - assert strings._("give_this_url") == ( - "Отправьте эту ссылку тому человеку, " - "которому вы хотите передать файл:") - assert strings._('wait_for_hs') == "Waiting for HS to be ready:" + assert strings._('preparing_files') == "Préparation des fichiers à partager." def test_load_invalid_locale( self, common_obj, locale_invalid, sys_onionshare_dev_mode): From 20d950e142c283d1f4b0f1fbbeb0cfe8e59a80e8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 13:58:42 -0700 Subject: [PATCH 53/87] Remove no_filenames string, and instead display CLI usage if you don't specify filenames --- onionshare/__init__.py | 2 +- share/locale/en.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 51210b6b..0bc0abe5 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -67,7 +67,7 @@ def main(cwd=None): # Make sure filenames given if not using receiver mode if not receive and len(filenames) == 0: - print(strings._('no_filenames')) + parser.print_help() sys.exit() # Validate filenames diff --git a/share/locale/en.json b/share/locale/en.json index fbf83118..512a69b4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -8,7 +8,6 @@ "ctrlc_to_stop": "Press Ctrl+C to stop the server", "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", - "no_filenames": "You must specify a list of files to share.", "no_available_port": "Could not start the Onion service as there was no available port.", "other_page_loaded": "Address loaded", "close_on_timeout": "Stopped because timer expired", From 5e942152a202b51f65b3446e8f9ed96c45dc4c73 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 15:11:18 -0700 Subject: [PATCH 54/87] Remove the 'using_ephemeral' string --- onionshare/onion.py | 1 - share/locale/cs.json | 1 - share/locale/da.json | 1 - share/locale/en.json | 1 - share/locale/eo.json | 1 - share/locale/fi.json | 1 - share/locale/it.json | 1 - share/locale/nl.json | 1 - share/locale/tr.json | 1 - 9 files changed, 9 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 7a111eff..81b82923 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -427,7 +427,6 @@ class Onion(object): raise TorTooOld(strings._('error_stealth_not_supported')) print(strings._("config_onion_service").format(int(port))) - print(strings._('using_ephemeral')) if self.stealth: if self.settings.get('hidservauth_string'): diff --git a/share/locale/cs.json b/share/locale/cs.json index a29c8b8e..a595ce67 100644 --- a/share/locale/cs.json +++ b/share/locale/cs.json @@ -26,7 +26,6 @@ "gui_copied_url": "URL zkopírováno do schránky", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_please_wait": "Prosím čekejte...", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Uplynulý čas: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/da.json b/share/locale/da.json index 75416989..d414695b 100644 --- a/share/locale/da.json +++ b/share/locale/da.json @@ -39,7 +39,6 @@ "gui_copied_url": "Kopierede URL til udklipsholder", "gui_copied_hidservauth": "Kopierede HidServAuth-linje til udklipsholder", "gui_please_wait": "Vent venligst...", - "using_ephemeral": "Starter kortvarig Tor onion-tjeneste og afventer udgivelse", "gui_download_upload_progress_complete": "%p%, tid forløbet: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (udregner anslået ankomsttid)", "gui_download_upload_progress_eta": "{0:s}, anslået ankomsttid: {1:s}, %p%", diff --git a/share/locale/en.json b/share/locale/en.json index 512a69b4..d6937544 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -53,7 +53,6 @@ "gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard", "gui_please_wait": "Starting… Click to cancel", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/eo.json b/share/locale/eo.json index 1fbda9bc..9902e4ae 100644 --- a/share/locale/eo.json +++ b/share/locale/eo.json @@ -26,7 +26,6 @@ "gui_copied_url": "URL kopiita en tondujon", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", "gui_please_wait": "Bonvolu atendi...", - "using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication", "gui_download_upload_progress_complete": "%p%, Tempo pasinta: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/fi.json b/share/locale/fi.json index 8a360284..d0ee80ff 100644 --- a/share/locale/fi.json +++ b/share/locale/fi.json @@ -21,6 +21,5 @@ "gui_canceled": "Peruutettu", "gui_copied_url": "URL-osoite kopioitu leikepöydälle", "gui_please_wait": "Odota...", - "using_ephemeral": "Käynnistetään lyhytaikainen Tor piilopalvelu ja odotetaan julkaisua", "zip_progress_bar_format": "Tiivistän tiedostoja: %p%" } diff --git a/share/locale/it.json b/share/locale/it.json index 6c55ed20..ebe2df4e 100644 --- a/share/locale/it.json +++ b/share/locale/it.json @@ -21,6 +21,5 @@ "gui_canceled": "Cancellati", "gui_copied_url": "URL Copiato nella clipboard", "gui_please_wait": "Attendere prego...", - "using_ephemeral": "Avviamento del servizio nascosto Tor ephemeral e attesa della pubblicazione", "zip_progress_bar_format": "Elaborazione files: %p%" } diff --git a/share/locale/nl.json b/share/locale/nl.json index 833432ab..abd14753 100644 --- a/share/locale/nl.json +++ b/share/locale/nl.json @@ -37,7 +37,6 @@ "gui_copied_url": "URL gekopieerd naar klembord", "gui_copied_hidservauth": "HidServAuth regel gekopieerd naar klembord", "gui_please_wait": "Moment geduld...", - "using_ephemeral": "Kortstondige Tor onion service gestart en in afwachting van publicatie", "gui_download_upload_progress_complete": "%p%, Tijd verstreken: {0:s}", "gui_download_upload_progress_starting": "{0:s}, %p% (ETA berekenen)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", diff --git a/share/locale/tr.json b/share/locale/tr.json index 71f16daa..68807410 100644 --- a/share/locale/tr.json +++ b/share/locale/tr.json @@ -21,6 +21,5 @@ "gui_canceled": "İptal edilen", "gui_copied_url": "Panoya kopyalanan URL", "gui_please_wait": "Lütfen bekleyin...", - "using_ephemeral": "Geçici Tor gizli hizmetine bakılıyor ve yayımı bekleniyor", "zip_progress_bar_format": "Dosyalar hazırlanıyor: %p%" } From fa20d7685b1c58242dba3fea897953fc4ad64946 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 16:22:37 -0700 Subject: [PATCH 55/87] Dynamically figure out the total size of the download based on the whether or not the client making the http request accepts gzip --- onionshare/web/share_mode.py | 24 +++++++++++++++++++----- onionshare_gui/share_mode/__init__.py | 7 ++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 95bc8443..2024e732 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -25,6 +25,7 @@ class ShareModeWeb(object): self.download_filename = None self.download_filesize = None self.gzip_filename = None + self.gzip_filesize = None self.zip_writer = None self.download_count = 0 @@ -69,13 +70,18 @@ class ShareModeWeb(object): return self.web.add_security_headers(r) # If download is allowed to continue, serve download page + if self.should_use_gzip(): + filesize = self.gzip_filesize + else: + filesize = self.download_filesize + if self.web.slug: r = make_response(render_template( 'send.html', slug=self.web.slug, file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, + filesize=filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: @@ -84,7 +90,7 @@ class ShareModeWeb(object): 'send.html', file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=self.download_filesize, + filesize=filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -123,7 +129,7 @@ class ShareModeWeb(object): # If this is a zipped file, then serve as-is. If it's not zipped, then, # if the http client supports gzip compression, gzip the file first # and serve that - use_gzip = (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename else: @@ -131,7 +137,8 @@ class ShareModeWeb(object): # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { - 'id': download_id + 'id': download_id, + 'use_gzip': use_gzip }) basename = os.path.basename(self.download_filename) @@ -212,7 +219,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', os.path.getsize(self.gzip_filename)) + r.headers.set('Content-Length', self.gzip_filesize) else: r.headers.set('Content-Length', self.download_filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) @@ -260,6 +267,7 @@ class ShareModeWeb(object): # Compress the file with gzip now, so we don't have to do it on each request self.gzip_filename = tempfile.mkstemp('wb+')[1] self._gzip_compress(self.download_filename, self.gzip_filename, 6, processed_size_callback) + self.gzip_filesize = os.path.getsize(self.gzip_filename) # Make sure the gzip file gets cleaned up when onionshare stops self.cleanup_filenames.append(self.gzip_filename) @@ -291,6 +299,12 @@ class ShareModeWeb(object): return True + def should_use_gzip(self): + """ + Should we use gzip for this browser? + """ + return (not self.is_zipped) and ('gzip' in request.headers.get('Accept-Encoding', '').lower()) + def _gzip_compress(self, input_filename, output_filename, level, processed_size_callback=None): """ Compress a file with gzip, without loading the whole thing into memory diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index 52ec672e..ac6a1373 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -229,7 +229,11 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.share_mode.download_filesize) + if event["data"]["use_gzip"]: + filesize = self.web.share_mode.gzip_filesize + else: + filesize = self.web.share_mode.download_filesize + self.downloads.add(event["data"]["id"], filesize) self.downloads_in_progress += 1 self.update_downloads_in_progress() @@ -388,6 +392,7 @@ class ZipProgressBar(QtWidgets.QProgressBar): def update_processed_size(self, val): self._processed_size = val + if self.processed_size < self.total_files_size: self.setValue(int((self.processed_size * 100) / self.total_files_size)) elif self.total_files_size != 0: From 711473f47eefc93ae771dfbe644e2cbda51712e5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 16:24:22 -0700 Subject: [PATCH 56/87] Include onionshare.web module in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a36fecab..94213f7c 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ setup( url=url, license=license, keywords=keywords, packages=[ 'onionshare', + 'onionshare.web', 'onionshare_gui', 'onionshare_gui.share_mode', 'onionshare_gui.receive_mode' From b96b36122dd8d97141fd20417ec60668bb149586 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 17:08:11 -0700 Subject: [PATCH 57/87] Actually tell the GUI the progess --- onionshare/web/share_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 2024e732..d4d6aed7 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -132,8 +132,10 @@ class ShareModeWeb(object): use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename + filesize = self.gzip_filesize else: file_to_download = self.download_filename + filesize = self.download_filesize # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { @@ -173,7 +175,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / self.download_filesize) * 100 + percent = (1.0 * downloaded_bytes / filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -219,9 +221,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', self.gzip_filesize) - else: - r.headers.set('Content-Length', self.download_filesize) + r.headers.set('Content-Length', filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type From a8779d9c4f9be3eecb3075e732b4a7fcd1bbcb8f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 21 Sep 2018 17:42:36 -0700 Subject: [PATCH 58/87] Access .upload_count from the correct object after the web refactor --- onionshare/web/receive_mode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index c422d74e..4a6934a1 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -249,8 +249,8 @@ class ReceiveModeRequest(Request): self.progress = {} # Create an upload_id, attach it to the request - self.upload_id = self.upload_count - self.upload_count += 1 + self.upload_id = self.web.receive_mode.upload_count + self.web.receive_mode.upload_count += 1 # Figure out the content length try: From 53c34259c0f7ae6ceb2b0bdf4936b9a701d64642 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:50:39 +1000 Subject: [PATCH 59/87] GUI unit tests in both share and receive mode --- .travis.yml | 3 +- unit_tests/__init__.py | 0 unit_tests/conftest.py | 160 ++++++++++ .../onionshare_receive_mode_upload_test.py | 258 ++++++++++++++++ ...re_receive_mode_upload_test_public_mode.py | 258 ++++++++++++++++ .../onionshare_share_mode_download_test.py | 287 ++++++++++++++++++ ...re_share_mode_download_test_public_mode.py | 287 ++++++++++++++++++ ...hare_share_mode_download_test_stay_open.py | 256 ++++++++++++++++ unit_tests/onionshare_timer_test.py | 142 +++++++++ unit_tests/run_unit_tests.sh | 5 + 10 files changed, 1655 insertions(+), 1 deletion(-) create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/conftest.py create mode 100644 unit_tests/onionshare_receive_mode_upload_test.py create mode 100644 unit_tests/onionshare_receive_mode_upload_test_public_mode.py create mode 100644 unit_tests/onionshare_share_mode_download_test.py create mode 100644 unit_tests/onionshare_share_mode_download_test_public_mode.py create mode 100644 unit_tests/onionshare_share_mode_download_test_stay_open.py create mode 100644 unit_tests/onionshare_timer_test.py create mode 100755 unit_tests/run_unit_tests.sh diff --git a/.travis.yml b/.travis.yml index afbaa887..71778af4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering 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 @@ -17,5 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ +script: cd unit_tests && bash run_unit_tests.sh after_success: - coveralls diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unit_tests/conftest.py b/unit_tests/conftest.py new file mode 100644 index 00000000..8ac7efb8 --- /dev/null +++ b/unit_tests/conftest.py @@ -0,0 +1,160 @@ +import sys +# Force tests to look for resources in the source code tree +sys.onionshare_dev_mode = True + +import os +import shutil +import tempfile + +import pytest + +from onionshare import common, web, settings + +@pytest.fixture +def temp_dir_1024(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). + """ + + tmp_dir = tempfile.mkdtemp() + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, 'wb') as f: + f.write(b'*' * 1024) + return tmp_dir + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_dir_1024_delete(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). The temporary directory (including + the file inside) will be deleted after fixture usage. + """ + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, 'wb') as f: + f.write(b'*' * 1024) + yield tmp_dir + + +@pytest.fixture +def temp_file_1024(): + """ Create a temporary file of a particular size (1024 bytes). """ + + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(b'*' * 1024) + return tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_file_1024_delete(): + """ + Create a temporary file of a particular size (1024 bytes). + The temporary file will be deleted after fixture usage. + """ + + with tempfile.NamedTemporaryFile() as tmp_file: + tmp_file.write(b'*' * 1024) + tmp_file.flush() + yield tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope='session') +def custom_zw(): + zw = web.share_mode.ZipWriter( + common.Common(), + zip_filename=common.Common.random_string(4, 6), + processed_size_callback=lambda _: 'custom_callback' + ) + yield zw + zw.close() + os.remove(zw.zip_filename) + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope='session') +def default_zw(): + zw = web.share_mode.ZipWriter(common.Common()) + yield zw + zw.close() + tmp_dir = os.path.dirname(zw.zip_filename) + shutil.rmtree(tmp_dir) + + +@pytest.fixture +def locale_en(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8')) + + +@pytest.fixture +def locale_fr(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8')) + + +@pytest.fixture +def locale_invalid(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8')) + + +@pytest.fixture +def locale_ru(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8')) + + +@pytest.fixture +def platform_darwin(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Darwin') + + +@pytest.fixture # (scope="session") +def platform_linux(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Linux') + + +@pytest.fixture +def platform_windows(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Windows') + + +@pytest.fixture +def sys_argv_sys_prefix(monkeypatch): + monkeypatch.setattr('sys.argv', [sys.prefix]) + + +@pytest.fixture +def sys_frozen(monkeypatch): + monkeypatch.setattr('sys.frozen', True, raising=False) + + +@pytest.fixture +def sys_meipass(monkeypatch): + monkeypatch.setattr( + 'sys._MEIPASS', os.path.expanduser('~'), raising=False) + + +@pytest.fixture # (scope="session") +def sys_onionshare_dev_mode(monkeypatch): + monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False) + + +@pytest.fixture +def time_time_100(monkeypatch): + monkeypatch.setattr('time.time', lambda: 100) + + +@pytest.fixture +def time_strftime(monkeypatch): + monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00') + +@pytest.fixture +def common_obj(): + return common.Common() + +@pytest.fixture +def settings_obj(sys_onionshare_dev_mode, platform_linux): + _common = common.Common() + _common.version = 'DUMMY_VERSION_1.2.3' + return settings.Settings(_common) diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/unit_tests/onionshare_receive_mode_upload_test.py new file mode 100644 index 00000000..b99faac0 --- /dev/null +++ b/unit_tests/onionshare_receive_mode_upload_test.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json +import requests + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + '''Test that the info widget along top of screen is not shown because we have a file''' + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + + @pytest.mark.run(order=6) + def test_click_receive_mode(self): + '''Test that we can switch to Receive Mode by clicking the button''' + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + + @pytest.mark.run(order=7) + def test_uploads_section_is_visible(self): + '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=13) + #def test_have_an_onion_service(self): + # '''Test that we have a valid Onion URL''' + # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=15) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=16) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Receiving''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + + @pytest.mark.run(order=18) + def test_web_page(self): + '''Test that the web page contains the term Select the files you want to send, then click''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.receive_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue('Select the files you want to send, then click "Send Files"' in f.read()) + f.close() + + @pytest.mark.run(order=19) + def test_upload_file(self): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + + @pytest.mark.run(order=20) + def test_uploads_widget_present(self): + '''Test that the No Uploads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + + @pytest.mark.run(order=21) + def test_upload_count_incremented(self): + '''Test that the Upload Count has incremented''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + + @pytest.mark.run(order=22) + def test_upload_same_file_is_renamed(self): + '''Test that we can upload the same file and that it gets renamed''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + + @pytest.mark.run(order=23) + def test_upload_count_incremented_again(self): + '''Test that the Upload Count has incremented again''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + + @pytest.mark.run(order=24) + def test_server_is_stopped(self): + '''Test that the server stops when we click Stop''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + + @pytest.mark.run(order=25) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py new file mode 100644 index 00000000..d309e5b1 --- /dev/null +++ b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json +import requests + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + '''Test that the info widget along top of screen is not shown because we have a file''' + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + + @pytest.mark.run(order=6) + def test_click_receive_mode(self): + '''Test that we can switch to Receive Mode by clicking the button''' + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + + @pytest.mark.run(order=7) + def test_uploads_section_is_visible(self): + '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=13) + #def test_have_an_onion_service(self): + # '''Test that we have a valid Onion URL''' + # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=14) + def test_have_no_slug(self): + '''Test that we have a valid slug''' + self.assertIsNone(self.gui.share_mode.server_status.web.slug) + + @pytest.mark.run(order=15) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=16) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Receiving''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + + @pytest.mark.run(order=18) + def test_web_page(self): + '''Test that the web page contains the term Select the files you want to send, then click''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET / HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue('Select the files you want to send, then click "Send Files"' in f.read()) + f.close() + + @pytest.mark.run(order=19) + def test_upload_file(self): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + + @pytest.mark.run(order=20) + def test_uploads_widget_present(self): + '''Test that the No Uploads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + + @pytest.mark.run(order=21) + def test_upload_count_incremented(self): + '''Test that the Upload Count has incremented''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + + @pytest.mark.run(order=22) + def test_upload_same_file_is_renamed(self): + '''Test that we can upload the same file and that it gets renamed''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + + @pytest.mark.run(order=23) + def test_upload_count_incremented_again(self): + '''Test that the Upload Count has incremented again''' + self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + + @pytest.mark.run(order=24) + def test_server_is_stopped(self): + '''Test that the server stops when we click Stop''' + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + + @pytest.mark.run(order=25) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed''' + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py new file mode 100644 index 00000000..aa8dcdaa --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) + # Delete button should be visible + self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + # Click delete, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + '''Test that we can also delete a file by clicking on its [X] widget''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + '''Re-add some files to the list so we can share''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_hidden(self): + '''Test that the add and delete buttons are hidden when the server starts''' + self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_have_an_onion_service(self): + # '''Test that we have a valid Onion URL''' + # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_web_page(self): + '''Test that the web page contains the term Total size''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue('Total size' in f.read()) + f.close() + + @pytest.mark.run(order=23) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=24) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + '''Test that the server stopped automatically when we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed because download occurred''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + @pytest.mark.run(order=28) + def test_add_button_visible_again(self): + '''Test that the add button should be visible again''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/unit_tests/onionshare_share_mode_download_test_public_mode.py new file mode 100644 index 00000000..59905149 --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test_public_mode.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) + # Delete button should be visible + self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + # Click delete, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + '''Test that we can also delete a file by clicking on its [X] widget''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + '''Re-add some files to the list so we can share''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_hidden(self): + '''Test that the add and delete buttons are hidden when the server starts''' + self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_have_an_onion_service(self): + # '''Test that we have a valid Onion URL''' + # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_no_slug(self): + '''Test that we have a valid slug''' + self.assertIsNone(self.gui.share_mode.server_status.web.slug) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_web_page(self): + '''Test that the web page contains the term Total size''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET / HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue('Total size' in f.read()) + f.close() + + @pytest.mark.run(order=23) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET /download HTTP/1.0\r\n' + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=24) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + '''Test that the server stopped automatically when we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + '''Test that the Server Status indicator shows we closed because download occurred''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + @pytest.mark.run(order=28) + def test_add_button_visible_again(self): + '''Test that the add button should be visible again''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/unit_tests/onionshare_share_mode_download_test_stay_open.py new file mode 100644 index 00000000..82a0ac87 --- /dev/null +++ b/unit_tests/onionshare_share_mode_download_test_stay_open.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": False, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded_and_tor_bootstrapped(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + '''Test that the info widget along top of screen is shown because we have a file''' + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + @pytest.mark.run(order=7) + def test_downloads_section_is_visible(self): + '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) + # Delete button should be visible + self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + # Click delete, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + '''Test that we can also delete a file by clicking on its [X] widget''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + '''Re-add some files to the list so we can share''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=13) + def test_add_delete_buttons_now_hidden(self): + '''Test that the add and delete buttons are hidden when the server starts''' + self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + # Running in local mode, so we have no .onion + #@pytest.mark.run(order=17) + #def test_have_an_onion_service(self): + # '''Test that we have a valid Onion URL''' + # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + # self.assertEqual(len(self.gui.app.onion_host), 62) + + @pytest.mark.run(order=18) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + '''Test that the URL label is showing''' + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + '''Test that the Copy URL button is shown''' + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_sharing(self): + '''Test that the Server Status indicator shows we are Sharing''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + @pytest.mark.run(order=22) + def test_download_share(self): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + @pytest.mark.run(order=23) + def test_downloads_widget_present(self): + QtTest.QTest.qWait(1000) + '''Test that the No Downloads Yet label is hidden, that Clear History is present''' + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + @pytest.mark.run(order=24) + def test_server_is_not_stopped(self): + '''Test that the server stayed open after we downloaded the share''' + self.assertEquals(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=25) + def test_web_service_is_running(self): + '''Test that the web server is still running''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.assertEquals(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=26) + def test_download_count_incremented(self): + '''Test that the Download Count has incremented''' + self.assertEquals(self.gui.share_mode.downloads_completed, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py new file mode 100644 index 00000000..ed20c1c0 --- /dev/null +++ b/unit_tests/onionshare_timer_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": True, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_set_timeout(self): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(120) + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + @pytest.mark.run(order=3) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=4) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=5) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=6) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=7) + def test_timeout_widget_hidden(self): + '''Test that the timeout widget is hidden when share has started''' + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + @pytest.mark.run(order=8) + def test_server_timed_out(self): + '''Test that the server has timed out after the timer ran out''' + QtTest.QTest.qWait(100000) + # We should have timed out now + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=9) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/run_unit_tests.sh b/unit_tests/run_unit_tests.sh new file mode 100755 index 00000000..d15f8a6e --- /dev/null +++ b/unit_tests/run_unit_tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + py.test-3 $test -vvv || exit 1 +done From a478c5bab31bbfd0c647e244423cbaff8b07a875 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:54:52 +1000 Subject: [PATCH 60/87] Try and make travis-friendly tests --- .travis.yml | 3 ++- unit_tests/run_unit_tests_travis.sh | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 unit_tests/run_unit_tests_travis.sh diff --git a/.travis.yml b/.travis.yml index 71778af4..03ae798a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering + - apt-get install xvfb 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 @@ -17,6 +18,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: cd unit_tests && bash run_unit_tests.sh +script: cd unit_tests && bash run_unit_tests_travis.sh after_success: - coveralls diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh new file mode 100755 index 00000000..236c48ff --- /dev/null +++ b/unit_tests/run_unit_tests_travis.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + xvfb-run pytest $test -vvv || exit 1 +done From 90a9d468acf933624f184517496f9f33c67b4f71 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 16:57:26 +1000 Subject: [PATCH 61/87] xvfb might already be installed? --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 03ae798a..6771eca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering - - apt-get install xvfb 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 From d9bb78417e6f98f22b4487b15f2ecfcf8a83d896 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:02:16 +1000 Subject: [PATCH 62/87] need pytest-qt --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6771eca8..c9c58e97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt 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 From 3138ca77d8d505021171b489a23cf91aea52935c Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:06:02 +1000 Subject: [PATCH 63/87] fighting with travis... --- .travis.yml | 1 + unit_tests/run_unit_tests_travis.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9c58e97..7df1d4c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r install/requirements.txt - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt + - sudo apt-get install python3-pytest 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 diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh index 236c48ff..ba9b5b4e 100755 --- a/unit_tests/run_unit_tests_travis.sh +++ b/unit_tests/run_unit_tests_travis.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - xvfb-run pytest $test -vvv || exit 1 + xvfb-run pytest-3 $test -vvv || exit 1 done From 4b04d5b002b68a0b1cd53e7bb7d9156fe037c37a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:47:38 +1000 Subject: [PATCH 64/87] Add persistent slug test. Add test of clipboard contents in Share mode. Remove travis stuff that I couldn't get to work --- .travis.yml | 4 +- .../onionshare_share_mode_download_test.py | 7 +- unit_tests/onionshare_slug_persistent_test.py | 161 ++++++++++++++++++ unit_tests/run_unit_tests_travis.sh | 5 - 4 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 unit_tests/onionshare_slug_persistent_test.py delete mode 100755 unit_tests/run_unit_tests_travis.sh diff --git a/.travis.yml b/.travis.yml index 7df1d4c4..afbaa887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt - - sudo apt-get install python3-pytest + - pip install 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 @@ -18,6 +17,5 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: cd unit_tests && bash run_unit_tests_travis.sh after_success: - coveralls diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py index aa8dcdaa..c4c8bee7 100644 --- a/unit_tests/onionshare_share_mode_download_test.py +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -8,7 +8,7 @@ import zipfile import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtCore, QtWidgets, QtTest, QtGui from onionshare.common import Common from onionshare.web import Web @@ -197,8 +197,11 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=20) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' + '''Test that the Copy URL button is shown and can be copied to clipboard''' self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.copy_url_button, QtCore.Qt.LeftButton) + clipboard = self.gui.qtapp.clipboard() + self.assertEquals(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, self.gui.share_mode.server_status.web.slug)) @pytest.mark.run(order=21) def test_server_status_indicator_says_sharing(self): diff --git a/unit_tests/onionshare_slug_persistent_test.py b/unit_tests/onionshare_slug_persistent_test.py new file mode 100644 index 00000000..d0dcd08a --- /dev/null +++ b/unit_tests/onionshare_slug_persistent_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import socket +import pytest +import zipfile +import socks +import json + +from PyQt5 import QtCore, QtWidgets, QtTest + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + slug = '' + + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": True, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + @pytest.mark.run(order=2) + def test_server_working_on_start_button_pressed(self): + '''Test we can start the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_WORKING state + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + @pytest.mark.run(order=3) + def test_server_status_indicator_says_starting(self): + '''Test that the Server Status indicator shows we are Starting''' + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + @pytest.mark.run(order=4) + def test_a_server_is_started(self): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=5) + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=6) + def test_have_a_slug(self): + '''Test that we have a valid slug''' + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + global slug + slug = self.gui.share_mode.server_status.web.slug + + @pytest.mark.run(order=7) + def test_server_can_be_stopped(self): + '''Test we can stop the service''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + + # Should be in SERVER_STOPPED state + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + @pytest.mark.run(order=8) + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + QtTest.QTest.qWait(4000) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + @pytest.mark.run(order=9) + def test_server_started_again(self): + '''Test we can start the service again''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + @pytest.mark.run(order=10) + def test_have_same_slug(self): + '''Test that we have the same slug''' + self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + + @pytest.mark.run(order=11) + def test_server_is_stopped_again(self): + '''Test that we can stop the server''' + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + self.assertEqual(self.gui.share_mode.server_status.status, 0) + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/run_unit_tests_travis.sh b/unit_tests/run_unit_tests_travis.sh deleted file mode 100755 index ba9b5b4e..00000000 --- a/unit_tests/run_unit_tests_travis.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -for test in `ls -1 | egrep ^onionshare_`; do - xvfb-run pytest-3 $test -vvv || exit 1 -done From 6e5d579cbbc2902a25ef0d2c50a0318b7e779593 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 17:57:53 +1000 Subject: [PATCH 65/87] One more travis test --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index afbaa887..65eac2c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt pytest 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 @@ -17,5 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ +script: xvfb-run py.test-3 unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From 50dc13d045f9e33bc1781b76fd51d2ef20eaee3c Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 18:01:17 +1000 Subject: [PATCH 66/87] pytest --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 65eac2c5..9aece101 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,6 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: xvfb-run py.test-3 unit_tests/onionshare_share_mode_download_test.py +script: xvfb-run pytest unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From c02088f366ff07c3aae09d888b29866e229f4103 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 22 Sep 2018 18:07:14 +1000 Subject: [PATCH 67/87] Nope --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9aece101..afbaa887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: # command to install dependencies install: - pip install -r install/requirements.txt - - pip install pytest-cov coveralls flake8 pytest-faulthandler pytest-ordering pytest-qt pytest + - pip install 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 @@ -17,6 +17,5 @@ before_script: - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # command to run tests script: pytest --cov=onionshare test/ -script: xvfb-run pytest unit_tests/onionshare_share_mode_download_test.py after_success: - coveralls From e737f2979dd01d502febc6888d3dde60683fcd1b Mon Sep 17 00:00:00 2001 From: Baccount Date: Sat, 22 Sep 2018 11:18:18 -0700 Subject: [PATCH 68/87] Update get-tor-windows.py --- install/get-tor-windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py index 0d16dd29..e5a24be9 100644 --- a/install/get-tor-windows.py +++ b/install/get-tor-windows.py @@ -33,9 +33,9 @@ import subprocess import requests def main(): - exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/torbrowser-install-8.0_en-US.exe' - exe_filename = 'torbrowser-install-8.0_en-US.exe' - expected_exe_sha256 = '0682b44eff5877dfc2fe2fdd5b46e678d47adad86d564e7cb6654c5f60eb1ed2' + exe_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/torbrowser-install-8.0.1_en-US.exe' + exe_filename = 'torbrowser-install-8.0.1_en-US.exe' + expected_exe_sha256 = 'bdf81d4282b991a6425c213c7b03b3f5c1f17bb02986b7fe9a1891e577e51639' # 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') From 79e21954c92fc01576b6bb45c0f2b135b4ce8a9f Mon Sep 17 00:00:00 2001 From: Baccount Date: Sat, 22 Sep 2018 11:20:55 -0700 Subject: [PATCH 69/87] Upgrade Tor to 0.3.4.8 --- install/get-tor-osx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index 1d2c6f56..ae20fd74 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -35,9 +35,9 @@ import subprocess import requests def main(): - dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0/TorBrowser-8.0-osx64_en-US.dmg' - dmg_filename = 'TorBrowser-8.0-osx64_en-US.dmg' - expected_dmg_sha256 = '15603ae7b3a1942863c98acc92f509e4409db48fe22c9acae6b15c9cb9bf3088' + dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/8.0.1/TorBrowser-8.0.1-osx64_en-US.dmg' + dmg_filename = 'TorBrowser-8.0.1-osx64_en-US.dmg' + expected_dmg_sha256 = 'fb1be2a0f850a65bae38747c3abbf9061742c5d7799e1693405078aaf38d2b08' # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) From d36182a09c3fbef12c5e7288a5efba79c3ae24fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:22:05 +0200 Subject: [PATCH 70/87] Language rework --- share/locale/en.json | 248 +++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index d6937544..a36fc0a4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,184 +1,184 @@ { - "config_onion_service": "Configuring onion service on port {0:d}.", - "preparing_files": "Preparing files to share.", - "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:", - "give_this_url_receive": "Give this address to the people sending you files:", - "give_this_url_receive_stealth": "Give this address and HidServAuth line to the people sending you files:", + "config_onion_service": "Setting up onion service on port {0:d}.", + "preparing_files": "Preparing share.", + "give_this_url": "Give this address to the recipient:", + "give_this_url_stealth": "Give this inbox address and HidServAuth line to the recipient:", + "give_this_url_receive": "Give this inbox address to the sender:", + "give_this_url_receive_stealth": "Give this inbox address and HidServAuth to the sender:", "ctrlc_to_stop": "Press Ctrl+C to stop the 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.", - "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", + "no_available_port": "Could not find an available port to start the onion service", + "other_page_loaded": "Inbox address loaded", + "close_on_timeout": "Stopped because timeframe expired", + "closing_automatically": "Stopped because you have the complete share", + "timeout_download_still_running": "Awaiting completion of share transfer to you", + "large_filesize": "Warning: Sending a large share could take hours", "systray_menu_exit": "Quit", - "systray_download_started_title": "OnionShare Download Started", - "systray_download_started_message": "A user started downloading your files", - "systray_download_completed_title": "OnionShare Download Finished", - "systray_download_completed_message": "The user finished downloading your files", - "systray_download_canceled_title": "OnionShare Download Canceled", - "systray_download_canceled_message": "The user canceled the download", - "systray_upload_started_title": "OnionShare Upload Started", - "systray_upload_started_message": "A user started uploading files to your computer", - "help_local_only": "Do not attempt to use Tor: For development only", - "help_stay_open": "Keep onion service running after download has finished", - "help_shutdown_timeout": "Shut down the onion service after N seconds", - "help_stealth": "Create stealth onion service (advanced)", - "help_receive": "Receive files instead of sending them", - "help_debug": "Log application errors to stdout, and log web errors to disk", + "systray_download_started_title": "Sending…", + "systray_download_started_message": "Your share is being sent.", + "systray_download_completed_title": "Share sent", + "systray_download_completed_message": "Your share has been sent.", + "systray_download_canceled_title": "Share cancelled", + "systray_download_canceled_message": "The recipient cancelled the share.", + "systray_upload_started_title": "Receiving…", + "systray_upload_started_message": "Started share transfer to inbox.", + "help_local_only": "Avoid using Tor: (Only for development)", + "help_stay_open": "Keep sending after one completion", + "help_shutdown_timeout": "Stop sharing after a given amount of seconds", + "help_stealth": "Make share that requires HidServAuth (advanced)", + "help_receive": "Receive shares instead of sending them", + "help_debug": "Log OnionShare errors to stdout, and web errors to disk", "help_filename": "List of files or folders to share", - "help_config": "Path to a custom JSON config file (optional)", + "help_config": "Custom JSON config file location (optional)", "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing", "gui_add": "Add", "gui_delete": "Delete", "gui_choose_items": "Choose", - "gui_share_start_server": "Start Sharing", - "gui_share_stop_server": "Stop Sharing", - "gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Share will expire automatically at {}", - "gui_receive_start_server": "Start Receive Mode", - "gui_receive_stop_server": "Stop Receive Mode", - "gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)", - "gui_receive_stop_server_shutdown_timeout_tooltip": "Receive mode will expire automatically at {}", + "gui_share_start_server": "Share", + "gui_share_stop_server": "Stop", + "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", + "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed share timeframe ends {}", + "gui_receive_start_server": "Receive", + "gui_receive_stop_server": "Stop", + "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", + "gui_receive_stop_server_shutdown_timeout_tooltip": "Allowed receive timeframe ends {}", "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", - "gui_downloads": "Download History", - "gui_no_downloads": "No downloads yet.", + "gui_downloads": "History of sent shares", + "gui_no_downloads": "None yet.", "gui_canceled": "Canceled", - "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_please_wait": "Starting… Click to cancel", - "gui_download_upload_progress_complete": "%p%, Time Elapsed: {0:s}", - "gui_download_upload_progress_starting": "{0:s}, %p% (Computing ETA)", + "gui_copied_url_title": "Copied", + "gui_copied_url": "OnionShare address copied to clipboard", + "gui_copied_hidservauth_title": "Copied", + "gui_copied_hidservauth": "HidServAuth line copied to clipboard", + "gui_please_wait": "Starting… Click to cancel.", + "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.", + "gui_download_upload_progress_starting": "{0:s}, %p% (calculating end time.)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "version_string": "OnionShare {0:s} | https://onionshare.org/", - "gui_quit_title": "Transfer in Progress", - "gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?", - "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", + "gui_quit_title": "Not so fast", + "gui_share_quit_warning": "Quitting now means files en route elsewhere won't finish.", + "gui_receive_quit_warning": "Quitting now means files en route to you won't finish.", "gui_quit_warning_quit": "Quit", "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": "Compressing 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 Tor 0.2.7.1 and at least python3-stem 1.4.0.", + "error_rate_limit": "Too many wrong attempts on your sharing address means someone could be trying to guess it. Start OnionShare again and send a new address to share.", + "zip_progress_bar_format": "Making share smaller: %p%", + "error_stealth_not_supported": "To create shares that require knowing HidServAuth, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", + "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", "gui_settings_window_title": "Settings", - "gui_settings_whats_this": "what's this?", - "gui_settings_stealth_option": "Create stealth onion services (legacy)", - "gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.", - "gui_settings_autoupdate_label": "Check for upgrades", - "gui_settings_autoupdate_option": "Notify me when upgrades are available", + "gui_settings_whats_this": "What's this?", + "gui_settings_stealth_option": "Create share that requires knowing HidServAuth (legacy)", + "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.", + "gui_settings_autoupdate_label": "Check for new version", + "gui_settings_autoupdate_option": "Notify me when a new version is available", "gui_settings_autoupdate_timestamp": "Last checked: {}", "gui_settings_autoupdate_timestamp_never": "Never", - "gui_settings_autoupdate_check_button": "Check For Upgrades", + "gui_settings_autoupdate_check_button": "Check for New Version", "gui_settings_general_label": "General settings", "gui_settings_sharing_label": "Sharing settings", - "gui_settings_close_after_first_download_option": "Stop sharing after first download", + "gui_settings_close_after_first_download_option": "Stop sharing after first completion", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", - "gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare", - "gui_settings_connection_type_automatic_option": "Attempt automatic configuration with Tor Browser", + "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", + "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", "gui_settings_connection_type_control_port_option": "Connect using control port", "gui_settings_connection_type_socket_file_option": "Connect using socket file", - "gui_settings_connection_type_test_button": "Test Tor Settings", + "gui_settings_connection_type_test_button": "Test Connection to Tor", "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor authentication settings", - "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", + "gui_settings_authenticate_label": "Tor identification settings", + "gui_settings_authenticate_no_auth_option": "None or cookie based", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", - "gui_settings_tor_bridges": "Tor Bridge support", + "gui_settings_tor_bridges": "Tor bridge support", "gui_settings_tor_bridges_no_bridges_radio_option": "Don't use bridges", "gui_settings_tor_bridges_obfs4_radio_option": "Use built-in obfs4 pluggable transports", "gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy": "Use built-in obfs4 pluggable transports (requires obfs4proxy)", "gui_settings_tor_bridges_meek_lite_azure_radio_option": "Use built-in meek_lite (Azure) pluggable transports", "gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy": "Use built-in meek_lite (Azure) pluggable transports (requires obfs4proxy)", - "gui_settings_meek_lite_expensive_warning": "Warning: the meek_lite bridges are very costly for the Tor Project to run!

You should only use meek_lite bridges if you are having trouble connecting to Tor directly, via obfs4 transports or other normal bridges.", + "gui_settings_meek_lite_expensive_warning": "Warning: The meek_lite bridges are very costly for the Tor Project to run.

Only use them if unable to connect to Tor directly, via obfs4 transports, or other normal bridges.", "gui_settings_tor_bridges_custom_radio_option": "Use custom bridges", "gui_settings_tor_bridges_custom_label": "You can get bridges from https://bridges.torproject.org", - "gui_settings_tor_bridges_invalid": "None of the bridges you supplied seem to work.\nPlease try again by double-checking them or adding other ones.", + "gui_settings_tor_bridges_invalid": "None of the bridges you added work.\nDouble-check them or add others.", "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", - "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.", - "settings_error_automatic": "Can't connect to Tor controller. Is Tor Browser running in the background? If you don't have it you can get it from:\nhttps://www.torproject.org/.", - "settings_error_socket_port": "Can't connect to Tor controller on {}:{}.", - "settings_error_socket_file": "Can't connect to Tor controller using socket file {}.", + "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", + "gui_settings_shutdown_timeout": "Stop sharing after:", + "settings_saved": "Settings saved in {}", + "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", + "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", + "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", + "settings_error_socket_file": "The Tor controller does not allow connection using the socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", - "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", - "settings_error_unreadable_cookie_file": "Connected to Tor controller, but can't authenticate because your password may be wrong, and your user lacks permission to read the cookie file.", - "settings_error_bundled_tor_not_supported": "Use of the Tor version bundled with OnionShare is not supported when using developer mode on Windows or macOS.", - "settings_error_bundled_tor_timeout": "Connecting to Tor is taking too long. Maybe your computer is offline, or your system clock isn't accurate.", + "settings_error_missing_password": "Connected to the Tor controller, type in the password for it.", + "settings_error_unreadable_cookie_file": "Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file.", + "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.", + "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?", "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", - "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}", + "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports shares that require HidServAuth: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", "connecting_to_tor": "Connecting to the Tor network", - "update_available": "A new version of OnionShare is available. Click here to download it.

Installed version: {}
Latest version: {}", - "update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.", - "update_error_invalid_latest_version": "Error checking for updates: The OnionShare website responded saying the latest version is '{}', but that doesn't appear to be a valid version string.", - "update_not_available": "You are running the latest version of OnionShare.", - "gui_tor_connection_ask": "Would you like to open OnionShare settings to troubleshoot connecting to Tor?", - "gui_tor_connection_ask_open_settings": "Open Settings", + "update_available": "New OnionShare out. Click here to get it.

You are using {} and the latest is {}.", + "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…", + "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?", + "update_not_available": "Running the latest OnionShare.", + "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?", + "gui_tor_connection_ask_open_settings": "Yes", "gui_tor_connection_ask_quit": "Quit", - "gui_tor_connection_error_settings": "Try adjusting how OnionShare connects to the Tor network in Settings.", - "gui_tor_connection_canceled": "OnionShare could not connect to Tor.\n\nMake sure you're connected to the Internet, then re-open OnionShare to set up the Tor connection.", + "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", + "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "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_server_started_after_timeout": "The set sharing timeframe ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The chosen sharing timeframe has already expired.\nPlease update it to start sharing.", + "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", - "gui_share_url_description": "Anyone with this link can download your files using the Tor Browser: ", - "gui_receive_url_description": "Anyone with this link can upload files to your computer using the Tor Browser: ", - "gui_url_label_persistent": "This share will not expire automatically unless a timer is set.

Every share will have the same address (to use one-time addresses, disable persistence in Settings)", - "gui_url_label_stay_open": "This share will not expire automatically unless a timer is set.", - "gui_url_label_onetime": "This share will expire after the first download", - "gui_url_label_onetime_and_persistent": "This share will expire after the first download

Every share will have the same address (to use one-time addresses, disable persistence in the Settings)", - "gui_status_indicator_share_stopped": "Ready to Share", + "gui_share_url_description": "Anyone with this OnionShare address can fetch your share using the Tor Browser: ", + "gui_receive_url_description": "Anyone with this link can upload files to your inbox folder using the Tor Browser: ", + "gui_url_label_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_url_label_stay_open": "This share will not auto-expire unless a timeframe is set.", + "gui_url_label_onetime": "This share will expire after first completion.", + "gui_url_label_onetime_and_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_status_indicator_share_stopped": "Ready to share", "gui_status_indicator_share_working": "Starting…", "gui_status_indicator_share_started": "Sharing", - "gui_status_indicator_receive_stopped": "Ready to Receive", + "gui_status_indicator_receive_stopped": "Ready to fetch", "gui_status_indicator_receive_working": "Starting…", - "gui_status_indicator_receive_started": "Receiving", - "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", - "info_in_progress_uploads_tooltip": "{} upload(s) in progress", - "info_completed_uploads_tooltip": "{} upload(s) completed", - "error_cannot_create_downloads_dir": "Error creating downloads folder: {}", - "error_downloads_dir_not_writable": "The downloads folder isn't writable: {}", - "receive_mode_downloads_dir": "Files people send you will appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can hack your computer if you open them! Only open files from people you trust, or if you know what you're doing.", - "gui_receive_mode_warning": "Some files can hack your computer if you open them!
Only open files from people you trust, or if you know what you're doing.", + "gui_status_indicator_receive_started": "Fetching", + "gui_file_info": "{} files, {}", + "gui_file_info_single": "{} file, {}", + "info_in_progress_downloads_tooltip": "{} incoming", + "info_completed_downloads_tooltip": "{} incoming completed", + "info_in_progress_uploads_tooltip": "{} outgoing", + "info_completed_uploads_tooltip": "{} outgoing completed", + "error_cannot_create_downloads_dir": "Could not create inbox: {}", + "error_downloads_dir_not_writable": "The inbox folder is write protected: {}", + "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", + "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer, potentially taking control of it if you open them. Only open things from people you trust to know what they are doing.", + "gui_receive_mode_warning": "A share could contain files that take control of your computer if opened!
Only open things from people you trust to know what they are doing.", "receive_mode_upload_starting": "Upload of total size {} is starting", - "receive_mode_received_file": "Received file: {}", - "gui_mode_share_button": "Share Files", - "gui_mode_receive_button": "Receive Files", - "gui_settings_receiving_label": "Receiving settings", - "gui_settings_downloads_label": "Save files to", + "receive_mode_received_file": "Received: {}", + "gui_mode_share_button": "Share", + "gui_mode_receive_button": "Receive", + "gui_settings_receiving_label": "Reception settings", + "gui_settings_downloads_label": "Save shares to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", "gui_settings_public_mode_checkbox": "Public mode", - "systray_close_server_title": "OnionShare Server Closed", + "systray_close_server_title": "OnionShare Server Shut Down", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", - "systray_download_page_loaded_message": "A user loaded the download page", - "systray_upload_page_loaded_message": "A user loaded the upload page", - "gui_uploads": "Upload History", - "gui_no_uploads": "No uploads yet.", - "gui_clear_history": "Clear history", - "gui_upload_in_progress": "Upload Started {}", - "gui_upload_finished_range": "Uploaded {} to {}", - "gui_upload_finished": "Uploaded {}", - "gui_open_folder_error_nautilus": "Cannot open folder the because nautilus is not available. You can find this file here: {}" + "systray_download_page_loaded_message": "Your page has been loaded", + "systray_upload_page_loaded_message": "Someone is browsing what you share", + "gui_uploads": "Sent Shares", + "gui_no_uploads": "Nothing sent yet.", + "gui_clear_history": "Clear", + "gui_upload_in_progress": "Delivery Started {}", + "gui_upload_finished_range": "Sent {} to {}", + "gui_upload_finished": "Sent {}", + "gui_open_folder_error_nautilus": "Cannot open folder without a file manager. The file is here: {}" } From 5c94e8dd1f0e9386f8e7ee5b49ff3a3de3316e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:28:31 +0200 Subject: [PATCH 71/87] No "expired", Allowed sharing timeframe --- share/locale/en.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index a36fc0a4..9269611d 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -10,7 +10,7 @@ "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not find an available port to start the onion service", "other_page_loaded": "Inbox address loaded", - "close_on_timeout": "Stopped because timeframe expired", + "close_on_timeout": "Stopped because timeframe ran out", "closing_automatically": "Stopped because you have the complete share", "timeout_download_still_running": "Awaiting completion of share transfer to you", "large_filesize": "Warning: Sending a large share could take hours", @@ -38,7 +38,7 @@ "gui_share_start_server": "Share", "gui_share_stop_server": "Stop", "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed share timeframe ends {}", + "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed sharing timeframe ends {}", "gui_receive_start_server": "Receive", "gui_receive_stop_server": "Stop", "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", @@ -105,7 +105,7 @@ "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", - "gui_settings_shutdown_timeout": "Stop sharing after:", + "gui_settings_shutdown_timeout": "Allowed sharing timeframe:", "settings_saved": "Settings saved in {}", "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", @@ -132,8 +132,8 @@ "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "gui_server_started_after_timeout": "The set sharing timeframe ran out before the server started.\nPlease make a new share.", - "gui_server_timeout_expired": "The chosen sharing timeframe has already expired.\nPlease update it to start sharing.", + "gui_server_started_after_timeout": "The chosen allowed sharing timeframe ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The chosen allowed sharing timeframe already ran out.\nPlease update it to start sharing.", "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", From 969f3cff6f71c42dbe4b4e7fc98c31ac7e365da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sun, 23 Sep 2018 06:38:39 +0200 Subject: [PATCH 72/87] Spelling cancelled ;) --- share/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/locale/en.json b/share/locale/en.json index 9269611d..bb6b1f37 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -47,7 +47,7 @@ "gui_copy_hidservauth": "Copy HidServAuth", "gui_downloads": "History of sent shares", "gui_no_downloads": "None yet.", - "gui_canceled": "Canceled", + "gui_canceled": "Cancelled", "gui_copied_url_title": "Copied", "gui_copied_url": "OnionShare address copied to clipboard", "gui_copied_hidservauth_title": "Copied", From 090e1aa10719fa51ea32e6bd4bdbb68402c0b221 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:00:13 -0700 Subject: [PATCH 73/87] Fix some of the language on strings, and move back to "upload" and "download" terminology --- share/locale/en.json | 162 +++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index bb6b1f37..adec5a20 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -1,32 +1,32 @@ { "config_onion_service": "Setting up onion service on port {0:d}.", - "preparing_files": "Preparing share.", + "preparing_files": "Compressing files.", "give_this_url": "Give this address to the recipient:", - "give_this_url_stealth": "Give this inbox address and HidServAuth line to the recipient:", - "give_this_url_receive": "Give this inbox address to the sender:", - "give_this_url_receive_stealth": "Give this inbox address and HidServAuth to the sender:", + "give_this_url_stealth": "Give this address and HidServAuth line to the recipient:", + "give_this_url_receive": "Give this address to the sender:", + "give_this_url_receive_stealth": "Give this address and HidServAuth to the sender:", "ctrlc_to_stop": "Press Ctrl+C to stop the 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 find an available port to start the onion service", - "other_page_loaded": "Inbox address loaded", - "close_on_timeout": "Stopped because timeframe ran out", - "closing_automatically": "Stopped because you have the complete share", - "timeout_download_still_running": "Awaiting completion of share transfer to you", + "other_page_loaded": "Address loaded", + "close_on_timeout": "Stopped because auto-stop timer ran out", + "closing_automatically": "Stopped because download finished", + "timeout_download_still_running": "Waiting for download to complete", "large_filesize": "Warning: Sending a large share could take hours", "systray_menu_exit": "Quit", - "systray_download_started_title": "Sending…", - "systray_download_started_message": "Your share is being sent.", - "systray_download_completed_title": "Share sent", - "systray_download_completed_message": "Your share has been sent.", - "systray_download_canceled_title": "Share cancelled", - "systray_download_canceled_message": "The recipient cancelled the share.", - "systray_upload_started_title": "Receiving…", - "systray_upload_started_message": "Started share transfer to inbox.", - "help_local_only": "Avoid using Tor: (Only for development)", - "help_stay_open": "Keep sending after one completion", + "systray_download_started_title": "OnionShare Download Started", + "systray_download_started_message": "A user started downloading your files", + "systray_download_completed_title": "OnionShare Download Finished", + "systray_download_completed_message": "The user finished downloading your files", + "systray_download_canceled_title": "OnionShare Download Canceled", + "systray_download_canceled_message": "The user canceled the download", + "systray_upload_started_title": "OnionShare Upload Started", + "systray_upload_started_message": "A user started uploading files to your computer", + "help_local_only": "Don't use Tor (only for development)", + "help_stay_open": "Keep sharing after first download", "help_shutdown_timeout": "Stop sharing after a given amount of seconds", - "help_stealth": "Make share that requires HidServAuth (advanced)", + "help_stealth": "Use client authorization (advanced)", "help_receive": "Receive shares instead of sending them", "help_debug": "Log OnionShare errors to stdout, and web errors to disk", "help_filename": "List of files or folders to share", @@ -35,40 +35,40 @@ "gui_add": "Add", "gui_delete": "Delete", "gui_choose_items": "Choose", - "gui_share_start_server": "Share", - "gui_share_stop_server": "Stop", - "gui_share_stop_server_shutdown_timeout": "Stop sharing ({}s remaining)", - "gui_share_stop_server_shutdown_timeout_tooltip": "Allowed sharing timeframe ends {}", - "gui_receive_start_server": "Receive", - "gui_receive_stop_server": "Stop", - "gui_receive_stop_server_shutdown_timeout": "Stop receiving ({}s remaining)", - "gui_receive_stop_server_shutdown_timeout_tooltip": "Allowed receive timeframe ends {}", + "gui_share_start_server": "Start sharing", + "gui_share_stop_server": "Stop sharing", + "gui_share_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)", + "gui_share_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}", + "gui_receive_start_server": "Start Receive Mode", + "gui_receive_stop_server": "Stop Receive Mode", + "gui_receive_stop_server_shutdown_timeout": "Stop Receive Mode ({}s remaining)", + "gui_receive_stop_server_shutdown_timeout_tooltip": "Auto-stop timer ends at {}", "gui_copy_url": "Copy Address", "gui_copy_hidservauth": "Copy HidServAuth", - "gui_downloads": "History of sent shares", - "gui_no_downloads": "None yet.", - "gui_canceled": "Cancelled", - "gui_copied_url_title": "Copied", + "gui_downloads": "Download History", + "gui_no_downloads": "No downloads yet.", + "gui_canceled": "Canceled", + "gui_copied_url_title": "Copied OnionShare Address", "gui_copied_url": "OnionShare address copied to clipboard", - "gui_copied_hidservauth_title": "Copied", + "gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth": "HidServAuth line copied to clipboard", "gui_please_wait": "Starting… Click to cancel.", "gui_download_upload_progress_complete": "%p%, {0:s} elapsed.", - "gui_download_upload_progress_starting": "{0:s}, %p% (calculating end time.)", + "gui_download_upload_progress_starting": "{0:s}, %p% (calculating)", "gui_download_upload_progress_eta": "{0:s}, ETA: {1:s}, %p%", "version_string": "OnionShare {0:s} | https://onionshare.org/", "gui_quit_title": "Not so fast", - "gui_share_quit_warning": "Quitting now means files en route elsewhere won't finish.", - "gui_receive_quit_warning": "Quitting now means files en route to you won't finish.", + "gui_share_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?", + "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Cancel", - "error_rate_limit": "Too many wrong attempts on your sharing address means someone could be trying to guess it. Start OnionShare again and send a new address to share.", - "zip_progress_bar_format": "Making share smaller: %p%", - "error_stealth_not_supported": "To create shares that require knowing HidServAuth, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", + "error_rate_limit": "Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the receipient a new address to share.", + "zip_progress_bar_format": "Compressing: %p%", + "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", "gui_settings_window_title": "Settings", "gui_settings_whats_this": "What's this?", - "gui_settings_stealth_option": "Create share that requires knowing HidServAuth (legacy)", + "gui_settings_stealth_option": "Use client authorization (legacy)", "gui_settings_stealth_hidservauth_string": "Having saved your private key for reuse, means you can now\nclick to copy your HidServAuth.", "gui_settings_autoupdate_label": "Check for new version", "gui_settings_autoupdate_option": "Notify me when a new version is available", @@ -77,7 +77,7 @@ "gui_settings_autoupdate_check_button": "Check for New Version", "gui_settings_general_label": "General settings", "gui_settings_sharing_label": "Sharing settings", - "gui_settings_close_after_first_download_option": "Stop sharing after first completion", + "gui_settings_close_after_first_download_option": "Stop sharing after first download", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", "gui_settings_connection_type_automatic_option": "Attempt auto-configuration with Tor Browser", @@ -87,8 +87,8 @@ "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor identification settings", - "gui_settings_authenticate_no_auth_option": "None or cookie based", + "gui_settings_authenticate_label": "Tor authentication settings", + "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", "gui_settings_tor_bridges": "Tor bridge support", @@ -104,15 +104,15 @@ "gui_settings_button_save": "Save", "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", - "gui_settings_shutdown_timeout_checkbox": "Stop sharing after a given timeframe", - "gui_settings_shutdown_timeout": "Allowed sharing timeframe:", + "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", + "gui_settings_shutdown_timeout": "Stop the share at:", "settings_saved": "Settings saved in {}", - "settings_error_unknown": "Can't connect to Tor controller because it knows your settings aren't making sense.", - "settings_error_automatic": "Could not connect to the Tor controller. The Tor Browser (available from https://www.torproject.org/)\nneeds to be running in the background.", + "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.", + "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from https://www.torproject.org/)\nrunning in the background?", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", - "settings_error_socket_file": "The Tor controller does not allow connection using the socket file {}.", + "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", - "settings_error_missing_password": "Connected to the Tor controller, type in the password for it.", + "settings_error_missing_password": "Connected to Tor controller, but it requires a password to authenticate.", "settings_error_unreadable_cookie_file": "Connected to the Tor controller, but password may be wrong, or your user is not permitted to read the cookie file.", "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.", "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?", @@ -125,60 +125,60 @@ "update_available": "New OnionShare out. Click here to get it.

You are using {} and the latest is {}.", "update_error_check_error": "Could not check for new versions: The OnionShare website is saying the latest version is the unrecognizable '{}'…", "update_error_invalid_latest_version": "Could not check for new version: Maybe you're not connected to Tor, or the OnionShare website is down?", - "update_not_available": "Running the latest OnionShare.", + "update_not_available": "You are running the latest OnionShare.", "gui_tor_connection_ask": "Open the settings to sort out connection to Tor?", "gui_tor_connection_ask_open_settings": "Yes", "gui_tor_connection_ask_quit": "Quit", "gui_tor_connection_error_settings": "Try changing how OnionShare connects to the Tor network in the settings.", "gui_tor_connection_canceled": "Could not connect to Tor.\n\nEnsure you are connected to the Internet, then re-open OnionShare and set up its connection to Tor.", "gui_tor_connection_lost": "Disconnected from Tor.", - "gui_server_started_after_timeout": "The chosen allowed sharing timeframe ran out before the server started.\nPlease make a new share.", - "gui_server_timeout_expired": "The chosen allowed sharing timeframe already ran out.\nPlease update it to start sharing.", + "gui_server_started_after_timeout": "The auto-stop timer ran out before the server started.\nPlease make a new share.", + "gui_server_timeout_expired": "The auto-stop timer already ran out.\nPlease update it to start sharing.", "share_via_onionshare": "OnionShare it", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address (legacy)", - "gui_share_url_description": "Anyone with this OnionShare address can fetch your share using the Tor Browser: ", - "gui_receive_url_description": "Anyone with this link can upload files to your inbox folder using the Tor Browser: ", - "gui_url_label_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", - "gui_url_label_stay_open": "This share will not auto-expire unless a timeframe is set.", - "gui_url_label_onetime": "This share will expire after first completion.", - "gui_url_label_onetime_and_persistent": "This share will not auto-expire unless a timeframe is set.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_share_url_description": "Anyone with this OnionShare address can download your files using the Tor Browser: ", + "gui_receive_url_description": "Anyone with this OnionShare address can upload files to your computer using the Tor Browser: ", + "gui_url_label_persistent": "This share will not auto-stop.

Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", + "gui_url_label_stay_open": "This share will not auto-stop.", + "gui_url_label_onetime": "This share will stop after first completion.", + "gui_url_label_onetime_and_persistent": "This share will not auto-stop.

Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", "gui_status_indicator_share_stopped": "Ready to share", "gui_status_indicator_share_working": "Starting…", "gui_status_indicator_share_started": "Sharing", - "gui_status_indicator_receive_stopped": "Ready to fetch", + "gui_status_indicator_receive_stopped": "Ready to receive", "gui_status_indicator_receive_working": "Starting…", - "gui_status_indicator_receive_started": "Fetching", + "gui_status_indicator_receive_started": "Receiving", "gui_file_info": "{} files, {}", "gui_file_info_single": "{} file, {}", - "info_in_progress_downloads_tooltip": "{} incoming", - "info_completed_downloads_tooltip": "{} incoming completed", - "info_in_progress_uploads_tooltip": "{} outgoing", - "info_completed_uploads_tooltip": "{} outgoing completed", - "error_cannot_create_downloads_dir": "Could not create inbox: {}", - "error_downloads_dir_not_writable": "The inbox folder is write protected: {}", + "info_in_progress_downloads_tooltip": "{} download(s) in progress", + "info_completed_downloads_tooltip": "{} download(s) completed", + "info_in_progress_uploads_tooltip": "{} upload(s) in progress", + "info_completed_uploads_tooltip": "{} upload(s) completed", + "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}", + "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}", "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer, potentially taking control of it if you open them. Only open things from people you trust to know what they are doing.", - "gui_receive_mode_warning": "A share could contain files that take control of your computer if opened!
Only open things from people you trust to know what they are doing.", + "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.", + "gui_receive_mode_warning": "Receive mode lets someone else upload files to your computer. Some files can potentially take control of your computer if you open them.
Only open things from people you trust, or if you know what you are doing.", "receive_mode_upload_starting": "Upload of total size {} is starting", "receive_mode_received_file": "Received: {}", - "gui_mode_share_button": "Share", - "gui_mode_receive_button": "Receive", - "gui_settings_receiving_label": "Reception settings", - "gui_settings_downloads_label": "Save shares to", + "gui_mode_share_button": "Share Files", + "gui_mode_receive_button": "Receive Files", + "gui_settings_receiving_label": "Receiving settings", + "gui_settings_downloads_label": "Save files to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", "gui_settings_public_mode_checkbox": "Public mode", - "systray_close_server_title": "OnionShare Server Shut Down", + "systray_close_server_title": "OnionShare Server Closed", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", - "systray_download_page_loaded_message": "Your page has been loaded", - "systray_upload_page_loaded_message": "Someone is browsing what you share", - "gui_uploads": "Sent Shares", - "gui_no_uploads": "Nothing sent yet.", - "gui_clear_history": "Clear", - "gui_upload_in_progress": "Delivery Started {}", - "gui_upload_finished_range": "Sent {} to {}", - "gui_upload_finished": "Sent {}", - "gui_open_folder_error_nautilus": "Cannot open folder without a file manager. The file is here: {}" + "systray_download_page_loaded_message": "A user loaded the download page", + "systray_upload_page_loaded_message": "A user loaded the upload page", + "gui_uploads": "Upload History", + "gui_no_uploads": "No uploads yet.", + "gui_clear_history": "Clear history", + "gui_upload_in_progress": "Upload Started {}", + "gui_upload_finished_range": "Uploaded {} to {}", + "gui_upload_finished": "Uploaded {}", + "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}" } From f340e55dc3fa61cffbfc33285c51ca56b236b539 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:36:36 -0700 Subject: [PATCH 74/87] Fixes a few strings after testing --- share/locale/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/share/locale/en.json b/share/locale/en.json index adec5a20..608fbfbc 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -108,7 +108,7 @@ "gui_settings_shutdown_timeout": "Stop the share at:", "settings_saved": "Settings saved in {}", "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.", - "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from https://www.torproject.org/)\nrunning in the background?", + "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", "settings_error_socket_file": "Can't connect to the Tor controller using socket file {}.", "settings_error_auth": "Connected to {}:{}, but can't authenticate. Maybe this isn't a Tor controller?", @@ -117,7 +117,7 @@ "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.", "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?", "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", - "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports shares that require HidServAuth: {}.", + "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", @@ -158,8 +158,8 @@ "error_cannot_create_downloads_dir": "Could not create receive mode folder: {}", "error_downloads_dir_not_writable": "The receive mode folder is write protected: {}", "receive_mode_downloads_dir": "Files sent to you appear in this folder: {}", - "receive_mode_warning": "Warning: Receive mode lets someone else upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.", - "gui_receive_mode_warning": "Receive mode lets someone else upload files to your computer. Some files can potentially take control of your computer if you open them.
Only open things from people you trust, or if you know what you are doing.", + "receive_mode_warning": "Warning: Receive mode lets people upload files to your computer. Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.", + "gui_receive_mode_warning": "Receive mode lets people upload files to your computer.

Some files can potentially take control of your computer if you open them. Only open things from people you trust, or if you know what you are doing.", "receive_mode_upload_starting": "Upload of total size {} is starting", "receive_mode_received_file": "Received: {}", "gui_mode_share_button": "Share Files", From 4b654214d98614f80985dc437e4e6e989825d9fd Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 23 Sep 2018 14:39:29 -0700 Subject: [PATCH 75/87] Fix locale test --- test/test_onionshare_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_onionshare_strings.py b/test/test_onionshare_strings.py index 1d0b3206..d3d40c8f 100644 --- a/test/test_onionshare_strings.py +++ b/test/test_onionshare_strings.py @@ -47,7 +47,7 @@ class TestLoadStrings: self, common_obj, locale_en, sys_onionshare_dev_mode): """ load_strings() loads English by default """ strings.load_strings(common_obj) - assert strings._('preparing_files') == "Preparing files to share." + assert strings._('preparing_files') == "Compressing files." def test_load_strings_loads_other_languages( From 1fa5deaaeba083d51f8c340e3d706fdfece3cfb6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 24 Sep 2018 10:41:48 +1000 Subject: [PATCH 76/87] Refactor the unit tests to use common, abstracted tests --- unit_tests/commontests.py | 307 ++++++++++++++++++ .../onionshare_receive_mode_upload_test.py | 142 ++------ ...re_receive_mode_upload_test_public_mode.py | 144 ++------ .../onionshare_share_mode_download_test.py | 189 +++-------- ...re_share_mode_download_test_public_mode.py | 186 +++-------- ...hare_share_mode_download_test_stay_open.py | 171 ++++------ unit_tests/onionshare_slug_persistent_test.py | 108 +++--- unit_tests/onionshare_timer_test.py | 86 ++--- 8 files changed, 625 insertions(+), 708 deletions(-) create mode 100644 unit_tests/commontests.py diff --git a/unit_tests/commontests.py b/unit_tests/commontests.py new file mode 100644 index 00000000..1f6f5896 --- /dev/null +++ b/unit_tests/commontests.py @@ -0,0 +1,307 @@ +import os +import requests +import socket +import socks +import zipfile + +from PyQt5 import QtCore, QtTest +from onionshare import strings + +class CommonTests(object): + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + def test_info_widget_is_not_visible(self, mode): + '''Test that the info widget along top of screen is not shown''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.info_widget.isVisible()) + + def test_info_widget_is_visible(self, mode): + '''Test that the info widget along top of screen is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + def test_click_mode(self, mode): + '''Test that we can switch Mode by clicking the button''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + + def test_history_is_visible(self, mode): + '''Test that the History section is visible and that the relevant widget is present''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + def test_server_working_on_start_button_pressed(self, mode): + '''Test we can start the service''' + # Should be in SERVER_WORKING state + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + def test_server_status_indicator_says_starting(self, mode): + '''Test that the Server Status indicator shows we are Starting''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + if mode == 'share': + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + def test_a_server_is_started(self, mode): + '''Test that the server has started''' + QtTest.QTest.qWait(2000) + # Should now be in SERVER_STARTED state + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_have_a_slug(self, mode, public_mode): + '''Test that we have a valid slug''' + if mode == 'receive': + if not public_mode: + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + if mode == 'share': + if not public_mode: + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + + def test_url_description_shown(self, mode): + '''Test that the URL label is showing''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + def test_have_copy_url_button(self, mode): + '''Test that the Copy URL button is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + def test_server_status_indicator_says_started(self, mode): + '''Test that the Server Status indicator shows we are started''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + if mode == 'share': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + def test_web_page(self, mode, string, public_mode): + '''Test that the web page contains a string''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + if not public_mode: + if mode == 'receive': + path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) + if mode == 'share': + path = '/{}'.format(self.gui.share_mode.server_status.web.slug) + else: + path = '/' + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue(string in f.read()) + f.close() + + def test_history_widgets_present(self, mode): + '''Test that the relevant widgets are present in the history view after activity has taken place''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + def test_counter_incremented(self, mode, count): + '''Test that the counter has incremented''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.uploads_completed, count) + if mode == 'share': + self.assertEquals(self.gui.share_mode.downloads_completed, count) + + def test_server_is_stopped(self, mode, stay_open): + '''Test that the server stops when we click Stop''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + if stay_open: + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_server_status_indicator_says_closed(self, mode, stay_open): + '''Test that the Server Status indicator shows we closed''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + if mode == 'share': + if stay_open: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + else: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + # Auto-stop timer tests + def test_set_timeout(self, mode): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(120) + if mode == 'receive': + self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) + if mode == 'share': + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + def test_timeout_widget_hidden(self, mode): + '''Test that the timeout widget is hidden when share has started''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + def test_server_timed_out(self, mode, wait): + '''Test that the server has timed out after the timer ran out''' + QtTest.QTest.qWait(wait) + # We should have timed out now + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + # Receive-specific tests + def test_upload_file(self, public_mode, expected_file): + '''Test that we can upload the file''' + files = {'file[]': open('/tmp/test.txt', 'rb')} + if not public_mode: + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + else: + path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + response = requests.post(path, files=files) + QtTest.QTest.qWait(2000) + self.assertTrue(os.path.isfile(expected_file)) + + # Share-specific tests + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + def test_deleting_only_file_hides_delete_button(self): + '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) + # Delete button should be visible + self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + # Click delete, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_add_a_file_and_delete_using_its_delete_widget(self): + '''Test that we can also delete a file by clicking on its [X] widget''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + def test_file_selection_widget_readd_files(self): + '''Re-add some files to the list so we can share''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + def test_add_delete_buttons_hidden(self): + '''Test that the add and delete buttons are hidden when the server starts''' + self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_download_share(self, public_mode): + '''Test that we can download the share''' + s = socks.socksocket() + s.settimeout(60) + s.connect(('127.0.0.1', self.gui.app.port)) + + if public_mode: + path = '/download' + else: + path = '{}/download'.format(self.gui.share_mode.web.slug) + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: 127.0.0.1\r\n' + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + QtTest.QTest.qWait(2000) + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + def test_add_button_visible(self): + '''Test that the add button should be visible''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/unit_tests/onionshare_receive_mode_upload_test.py index b99faac0..bac622fb 100644 --- a/unit_tests/onionshare_receive_mode_upload_test.py +++ b/unit_tests/onionshare_receive_mode_upload_test.py @@ -2,20 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -import requests -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,176 +81,104 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_info_widget_is_not_visible(self): - '''Test that the info widget along top of screen is not shown because we have a file''' - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_not_visible(self, 'receive') @pytest.mark.run(order=6) - def test_click_receive_mode(self): - '''Test that we can switch to Receive Mode by clicking the button''' - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') @pytest.mark.run(order=7) - def test_uploads_section_is_visible(self): - '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') @pytest.mark.run(order=8) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.receive_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') @pytest.mark.run(order=9) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'receive') @pytest.mark.run(order=10) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=11) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.receive_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'receive') @pytest.mark.run(order=12) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=13) - #def test_have_an_onion_service(self): - # '''Test that we have a valid Onion URL''' - # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') - # self.assertEqual(len(self.gui.app.onion_host), 62) + CommonTests.test_a_web_server_is_running(self) @pytest.mark.run(order=14) def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + CommonTests.test_have_a_slug(self, 'receive', False) @pytest.mark.run(order=15) def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + CommonTests.test_url_description_shown(self, 'receive') @pytest.mark.run(order=16) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + CommonTests.test_have_copy_url_button(self, 'receive') @pytest.mark.run(order=17) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Receiving''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') @pytest.mark.run(order=18) def test_web_page(self): - '''Test that the web page contains the term Select the files you want to send, then click''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.receive_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/webpage', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - f = open('/tmp/webpage') - self.assertTrue('Select the files you want to send, then click "Send Files"' in f.read()) - f.close() + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) @pytest.mark.run(order=19) def test_upload_file(self): - '''Test that we can upload the file''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt') @pytest.mark.run(order=20) - def test_uploads_widget_present(self): - '''Test that the No Uploads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') @pytest.mark.run(order=21) - def test_upload_count_incremented(self): - '''Test that the Upload Count has incremented''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): - '''Test that we can upload the same file and that it gets renamed''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - '''Test that the Upload Count has incremented again''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) def test_server_is_stopped(self): - '''Test that the server stops when we click Stop''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'receive', False) @pytest.mark.run(order=25) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py index d309e5b1..8ed385f5 100644 --- a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py +++ b/unit_tests/onionshare_receive_mode_upload_test_public_mode.py @@ -2,20 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -import requests -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,176 +81,104 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_info_widget_is_not_visible(self): - '''Test that the info widget along top of screen is not shown because we have a file''' - self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_not_visible(self, 'receive') @pytest.mark.run(order=6) - def test_click_receive_mode(self): - '''Test that we can switch to Receive Mode by clicking the button''' - QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) - self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') @pytest.mark.run(order=7) - def test_uploads_section_is_visible(self): - '''Test that the Uploads section is visible and that the No Uploads Yet label is present''' - self.assertTrue(self.gui.receive_mode.uploads.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') @pytest.mark.run(order=8) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.receive_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') @pytest.mark.run(order=9) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'receive') @pytest.mark.run(order=10) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=11) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.receive_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'receive') @pytest.mark.run(order=12) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=13) - #def test_have_an_onion_service(self): - # '''Test that we have a valid Onion URL''' - # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') - # self.assertEqual(len(self.gui.app.onion_host), 62) + CommonTests.test_a_web_server_is_running(self) @pytest.mark.run(order=14) - def test_have_no_slug(self): - '''Test that we have a valid slug''' - self.assertIsNone(self.gui.share_mode.server_status.web.slug) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', True) @pytest.mark.run(order=15) def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + CommonTests.test_url_description_shown(self, 'receive') @pytest.mark.run(order=16) def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + CommonTests.test_have_copy_url_button(self, 'receive') @pytest.mark.run(order=17) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Receiving''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') @pytest.mark.run(order=18) def test_web_page(self): - '''Test that the web page contains the term Select the files you want to send, then click''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET / HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/webpage', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - f = open('/tmp/webpage') - self.assertTrue('Select the files you want to send, then click "Send Files"' in f.read()) - f.close() + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) @pytest.mark.run(order=19) def test_upload_file(self): - '''Test that we can upload the file''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test.txt')) + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt') @pytest.mark.run(order=20) - def test_uploads_widget_present(self): - '''Test that the No Uploads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) - self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') @pytest.mark.run(order=21) - def test_upload_count_incremented(self): - '''Test that the Upload Count has incremented''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 1) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) @pytest.mark.run(order=22) def test_upload_same_file_is_renamed(self): - '''Test that we can upload the same file and that it gets renamed''' - files = {'file[]': open('/tmp/test.txt', 'rb')} - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port), files=files) - QtTest.QTest.qWait(2000) - self.assertTrue(os.path.isfile('/tmp/OnionShare/test-2.txt')) + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt') @pytest.mark.run(order=23) def test_upload_count_incremented_again(self): - '''Test that the Upload Count has incremented again''' - self.assertEquals(self.gui.receive_mode.uploads_completed, 2) + CommonTests.test_counter_incremented(self, 'receive', 2) @pytest.mark.run(order=24) def test_server_is_stopped(self): - '''Test that the server stops when we click Stop''' - QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.receive_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'receive', False) @pytest.mark.run(order=25) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - QtTest.QTest.qWait(2000) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed''' - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_share_mode_download_test.py b/unit_tests/onionshare_share_mode_download_test.py index c4c8bee7..0ef03e97 100644 --- a/unit_tests/onionshare_share_mode_download_test.py +++ b/unit_tests/onionshare_share_mode_download_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest, QtGui +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -80,210 +79,112 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_hides_delete_button(self): - '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) - # Delete button should be visible - self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - # Click delete, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_add_a_file_and_delete_using_its_delete_widget(self): - '''Test that we can also delete a file by clicking on its [X] widget''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_files(self): - '''Re-add some files to the list so we can share''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_hidden(self): - '''Test that the add and delete buttons are hidden when the server starts''' - self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_have_an_onion_service(self): - # '''Test that we have a valid Onion URL''' - # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) @pytest.mark.run(order=18) - def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown and can be copied to clipboard''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.copy_url_button, QtCore.Qt.LeftButton) - clipboard = self.gui.qtapp.clipboard() - self.assertEquals(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, self.gui.share_mode.server_status.web.slug)) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', False) @pytest.mark.run(order=22) - def test_web_page(self): - '''Test that the web page contains the term Total size''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {} HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/webpage', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - f = open('/tmp/webpage') - self.assertTrue('Total size' in f.read()) - f.close() + def test_download_share(self): + CommonTests.test_download_share(self, False) @pytest.mark.run(order=23) - def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) @pytest.mark.run(order=25) - def test_server_is_stopped(self): - '''Test that the server stopped automatically when we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 0) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) @pytest.mark.run(order=27) - def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed because download occurred''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) - - @pytest.mark.run(order=28) - def test_add_button_visible_again(self): - '''Test that the add button should be visible again''' - self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/unit_tests/onionshare_share_mode_download_test_public_mode.py index 59905149..417eb8b9 100644 --- a/unit_tests/onionshare_share_mode_download_test_public_mode.py +++ b/unit_tests/onionshare_share_mode_download_test_public_mode.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -80,207 +79,112 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_hides_delete_button(self): - '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) - # Delete button should be visible - self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - # Click delete, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_add_a_file_and_delete_using_its_delete_widget(self): - '''Test that we can also delete a file by clicking on its [X] widget''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_files(self): - '''Re-add some files to the list so we can share''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_hidden(self): - '''Test that the add and delete buttons are hidden when the server starts''' - self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_have_an_onion_service(self): - # '''Test that we have a valid Onion URL''' - # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) @pytest.mark.run(order=18) - def test_have_no_slug(self): - '''Test that we have a valid slug''' - self.assertIsNone(self.gui.share_mode.server_status.web.slug) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) @pytest.mark.run(order=22) - def test_web_page(self): - '''Test that the web page contains the term Total size''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET / HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/webpage', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - f = open('/tmp/webpage') - self.assertTrue('Total size' in f.read()) - f.close() + def test_download_share(self): + CommonTests.test_download_share(self, True) @pytest.mark.run(order=23) - def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET /download HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) @pytest.mark.run(order=25) - def test_server_is_stopped(self): - '''Test that the server stopped automatically when we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 0) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) @pytest.mark.run(order=26) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) @pytest.mark.run(order=27) - def test_server_status_indicator_says_closed(self): - '''Test that the Server Status indicator shows we closed because download occurred''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) - - @pytest.mark.run(order=28) - def test_add_button_visible_again(self): - '''Test that the add button should be visible again''' - self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/unit_tests/onionshare_share_mode_download_test_stay_open.py index 82a0ac87..c3177a38 100644 --- a/unit_tests/onionshare_share_mode_download_test_stay_open.py +++ b/unit_tests/onionshare_share_mode_download_test_stay_open.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -51,7 +50,7 @@ class OnionShareGuiTest(unittest.TestCase): "hidservauth_string": "", "no_bridges": True, "private_key": "", - "public_mode": False, + "public_mode": True, "receive_allow_receiver_shutdown": True, "save_private_key": False, "shutdown_timeout": False, @@ -80,176 +79,124 @@ class OnionShareGuiTest(unittest.TestCase): os.remove('/tmp/test.txt') @pytest.mark.run(order=1) - def test_gui_loaded_and_tor_bootstrapped(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) def test_windowTitle_seen(self): - '''Test that the window title is OnionShare''' - self.assertEqual(self.gui.windowTitle(), 'OnionShare') + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) def test_settings_button_is_visible(self): - '''Test that the settings button is visible''' - self.assertTrue(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) def test_server_status_bar_is_visible(self): - '''Test that the status bar is visible''' - self.assertTrue(self.gui.status_bar.isVisible()) + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) def test_file_selection_widget_has_a_file(self): - '''Test that the number of files in the list is 1''' - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) def test_info_widget_is_visible(self): - '''Test that the info widget along top of screen is shown because we have a file''' - self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_downloads_section_is_visible(self): - '''Test that the Downloads section is visible and that the No Downloads Yet label is present''' - self.assertTrue(self.gui.share_mode.downloads.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) def test_deleting_only_file_hides_delete_button(self): - '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' - rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) - # Delete button should be visible - self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - # Click delete, and since there's no more files, the delete button should be hidden - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + CommonTests.test_deleting_only_file_hides_delete_button(self) @pytest.mark.run(order=9) def test_add_a_file_and_delete_using_its_delete_widget(self): - '''Test that we can also delete a file by clicking on its [X] widget''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) - self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) @pytest.mark.run(order=10) def test_file_selection_widget_readd_files(self): - '''Re-add some files to the list so we can share''' - self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') - self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') - self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + CommonTests.test_file_selection_widget_readd_files(self) @pytest.mark.run(order=11) def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') @pytest.mark.run(order=12) def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + CommonTests.test_server_status_indicator_says_starting(self, 'share') @pytest.mark.run(order=13) - def test_add_delete_buttons_now_hidden(self): - '''Test that the add and delete buttons are hidden when the server starts''' - self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) - self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) @pytest.mark.run(order=14) def test_settings_button_is_hidden(self): - '''Test that the settings button is hidden when the server starts''' - self.assertFalse(self.gui.settings_button.isVisible()) + CommonTests.test_settings_button_is_hidden(self) @pytest.mark.run(order=15) def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_a_server_is_started(self, 'share') @pytest.mark.run(order=16) def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + CommonTests.test_a_web_server_is_running(self) - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - - # Running in local mode, so we have no .onion - #@pytest.mark.run(order=17) - #def test_have_an_onion_service(self): - # '''Test that we have a valid Onion URL''' - # self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') - # self.assertEqual(len(self.gui.app.onion_host), 62) + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) @pytest.mark.run(order=18) - def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') @pytest.mark.run(order=19) - def test_url_description_shown(self): - '''Test that the URL label is showing''' - self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') @pytest.mark.run(order=20) - def test_have_copy_url_button(self): - '''Test that the Copy URL button is shown''' - self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') @pytest.mark.run(order=21) - def test_server_status_indicator_says_sharing(self): - '''Test that the Server Status indicator shows we are Sharing''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) @pytest.mark.run(order=22) def test_download_share(self): - '''Test that we can download the share''' - s = socks.socksocket() - s.settimeout(60) - s.connect(('127.0.0.1', self.gui.app.port)) - - http_request = 'GET {}/download HTTP/1.0\r\n'.format(self.gui.share_mode.server_status.web.slug) - http_request += 'Host: 127.0.0.1\r\n' - http_request += '\r\n' - s.sendall(http_request.encode('utf-8')) - - with open('/tmp/download.zip', 'wb') as file_to_write: - while True: - data = s.recv(1024) - if not data: - break - file_to_write.write(data) - file_to_write.close() - - zip = zipfile.ZipFile('/tmp/download.zip') - self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + CommonTests.test_download_share(self, True) @pytest.mark.run(order=23) - def test_downloads_widget_present(self): - QtTest.QTest.qWait(1000) - '''Test that the No Downloads Yet label is hidden, that Clear History is present''' - self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) - self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') @pytest.mark.run(order=24) - def test_server_is_not_stopped(self): - '''Test that the server stayed open after we downloaded the share''' - self.assertEquals(self.gui.share_mode.server_status.status, 2) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'share', 1) @pytest.mark.run(order=25) - def test_web_service_is_running(self): - '''Test that the web server is still running''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.assertEquals(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_download_share_again(self): + CommonTests.test_download_share(self, True) @pytest.mark.run(order=26) - def test_download_count_incremented(self): - '''Test that the Download Count has incremented''' - self.assertEquals(self.gui.share_mode.downloads_completed, 1) + def test_counter_incremented_again(self): + CommonTests.test_counter_incremented(self, 'share', 2) + + @pytest.mark.run(order=27) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=28) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=29) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=30) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) if __name__ == "__main__": diff --git a/unit_tests/onionshare_slug_persistent_test.py b/unit_tests/onionshare_slug_persistent_test.py index d0dcd08a..05c9719a 100644 --- a/unit_tests/onionshare_slug_persistent_test.py +++ b/unit_tests/onionshare_slug_persistent_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -83,79 +82,86 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=1) def test_gui_loaded(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) - def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) - def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) - def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=6) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=8) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=9) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=10) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=11) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=12) def test_have_a_slug(self): - '''Test that we have a valid slug''' - self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + CommonTests.test_have_a_slug(self, 'share', False) global slug slug = self.gui.share_mode.server_status.web.slug - @pytest.mark.run(order=7) - def test_server_can_be_stopped(self): - '''Test we can stop the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + @pytest.mark.run(order=13) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') - # Should be in SERVER_STOPPED state - self.assertEqual(self.gui.share_mode.server_status.status, 0) + @pytest.mark.run(order=14) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) - @pytest.mark.run(order=8) + @pytest.mark.run(order=15) def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - QtTest.QTest.qWait(4000) + CommonTests.test_web_service_is_stopped(self) - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + @pytest.mark.run(order=16) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) - @pytest.mark.run(order=9) + @pytest.mark.run(order=17) def test_server_started_again(self): - '''Test we can start the service again''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') - @pytest.mark.run(order=10) + @pytest.mark.run(order=18) def test_have_same_slug(self): '''Test that we have the same slug''' self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) - @pytest.mark.run(order=11) + @pytest.mark.run(order=19) def test_server_is_stopped_again(self): - '''Test that we can stop the server''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - QtTest.QTest.qWait(1000) - self.assertEqual(self.gui.share_mode.server_status.status, 0) + CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_web_service_is_stopped(self) + if __name__ == "__main__": unittest.main() diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py index ed20c1c0..34c77f12 100644 --- a/unit_tests/onionshare_timer_test.py +++ b/unit_tests/onionshare_timer_test.py @@ -2,19 +2,18 @@ import os import sys import unittest -import socket import pytest -import zipfile -import socks import json -from PyQt5 import QtCore, QtWidgets, QtTest +from PyQt5 import QtWidgets from onionshare.common import Common from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * +from .commontests import CommonTests + app = QtWidgets.QApplication(sys.argv) class OnionShareGuiTest(unittest.TestCase): @@ -81,62 +80,63 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=1) def test_gui_loaded(self): - '''Test that the GUI actually is shown''' - self.assertTrue(self.gui.show) + CommonTests.test_gui_loaded(self) @pytest.mark.run(order=2) - def test_set_timeout(self): - '''Test that the timeout can be set''' - timer = QtCore.QDateTime.currentDateTime().addSecs(120) - self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) - self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) @pytest.mark.run(order=3) - def test_server_working_on_start_button_pressed(self): - '''Test we can start the service''' - QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) - - # Should be in SERVER_WORKING state - self.assertEqual(self.gui.share_mode.server_status.status, 1) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) @pytest.mark.run(order=4) - def test_server_status_indicator_says_starting(self): - '''Test that the Server Status indicator shows we are Starting''' - self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) @pytest.mark.run(order=5) - def test_a_server_is_started(self): - '''Test that the server has started''' - QtTest.QTest.qWait(2000) - # Should now be in SERVER_STARTED state - self.assertEqual(self.gui.share_mode.server_status.status, 2) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) @pytest.mark.run(order=6) - def test_a_web_server_is_running(self): - '''Test that the web server has started''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') @pytest.mark.run(order=7) - def test_timeout_widget_hidden(self): - '''Test that the timeout widget is hidden when share has started''' - self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') @pytest.mark.run(order=8) - def test_server_timed_out(self): - '''Test that the server has timed out after the timer ran out''' - QtTest.QTest.qWait(100000) - # We should have timed out now - self.assertEqual(self.gui.share_mode.server_status.status, 0) + def test_set_timeout(self): + CommonTests.test_set_timeout(self, 'share') @pytest.mark.run(order=9) - def test_web_service_is_stopped(self): - '''Test that the web server also stopped''' - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') - # We should be closed by now. Fail if not! - self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + @pytest.mark.run(order=10) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=13) + def test_timeout_widget_hidden(self): + CommonTests.test_timeout_widget_hidden(self, 'share') + + @pytest.mark.run(order=14) + def test_timeout(self): + CommonTests.test_server_timed_out(self, 'share', 120000) + + @pytest.mark.run(order=15) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) if __name__ == "__main__": unittest.main() From b465d9db85cbe033608da3dd38e62a242783981a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 25 Sep 2018 14:40:10 +1000 Subject: [PATCH 77/87] Analyse the right file size to determine if the download has finished in the UI (in order to decide whether to stop server yet) --- onionshare/web/share_mode.py | 16 ++++++++-------- onionshare_gui/share_mode/__init__.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index d4d6aed7..a57d0a39 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -71,9 +71,9 @@ class ShareModeWeb(object): # If download is allowed to continue, serve download page if self.should_use_gzip(): - filesize = self.gzip_filesize + self.filesize = self.gzip_filesize else: - filesize = self.download_filesize + self.filesize = self.download_filesize if self.web.slug: r = make_response(render_template( @@ -81,7 +81,7 @@ class ShareModeWeb(object): slug=self.web.slug, file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=filesize, + filesize=self.filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) else: @@ -90,7 +90,7 @@ class ShareModeWeb(object): 'send.html', file_info=self.file_info, filename=os.path.basename(self.download_filename), - filesize=filesize, + filesize=self.filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), is_zipped=self.is_zipped)) return self.web.add_security_headers(r) @@ -132,10 +132,10 @@ class ShareModeWeb(object): use_gzip = self.should_use_gzip() if use_gzip: file_to_download = self.gzip_filename - filesize = self.gzip_filesize + self.filesize = self.gzip_filesize else: file_to_download = self.download_filename - filesize = self.download_filesize + self.filesize = self.download_filesize # Tell GUI the download started self.web.add_request(self.web.REQUEST_STARTED, path, { @@ -175,7 +175,7 @@ class ShareModeWeb(object): # tell GUI the progress downloaded_bytes = fp.tell() - percent = (1.0 * downloaded_bytes / filesize) * 100 + percent = (1.0 * downloaded_bytes / self.filesize) * 100 # only output to stdout if running onionshare in CLI mode, or if using Linux (#203, #304) if not self.web.is_gui or self.common.platform == 'Linux' or self.common.platform == 'BSD': @@ -221,7 +221,7 @@ class ShareModeWeb(object): r = Response(generate()) if use_gzip: r.headers.set('Content-Encoding', 'gzip') - r.headers.set('Content-Length', filesize) + r.headers.set('Content-Length', self.filesize) r.headers.set('Content-Disposition', 'attachment', filename=basename) r = self.web.add_security_headers(r) # guess content type diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/share_mode/__init__.py index ac6a1373..90fce49a 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/share_mode/__init__.py @@ -246,7 +246,7 @@ class ShareMode(Mode): self.downloads.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.share_mode.download_filesize: + if event["data"]["bytes"] == self.web.share_mode.filesize: self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) # Update the total 'completed downloads' info From 112a7a25d17bcf2bec17a5320700f5443f3dee63 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 25 Sep 2018 15:26:19 +1000 Subject: [PATCH 78/87] Show whether Tor version supports next-gen onion support --- onionshare/onion.py | 2 ++ onionshare_gui/settings_dialog.py | 2 +- share/locale/en.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 81b82923..c45ae72e 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -402,6 +402,8 @@ class Onion(object): # ephemeral stealth onion services are not supported self.supports_stealth = False + # Does this version of Tor support next-gen ('v3') onions? + self.supports_next_gen_onions = self.tor_version > Version('0.3.3.1') def is_authenticated(self): """ diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index c31d4630..3cd25d31 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -746,7 +746,7 @@ class SettingsDialog(QtWidgets.QDialog): onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work - Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth)) + Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) # Clean up onion.cleanup() diff --git a/share/locale/en.json b/share/locale/en.json index 608fbfbc..0f0f0cf4 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -117,7 +117,7 @@ "settings_error_bundled_tor_not_supported": "Using the Tor version that comes with OnionShare does not work in developer mode on Windows or macOS.", "settings_error_bundled_tor_timeout": "Taking too long to connect to Tor. Maybe you aren't connected to the Internet, or have an inaccurate system clock?", "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", - "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.", + "settings_test_success": "Connected to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}.\nSupports client authentication: {}.\nSupports next-gen .onion addresses: {}.", "error_tor_protocol_error": "There was an error with Tor: {}", "error_tor_protocol_error_unknown": "There was an unknown error with Tor", "error_invalid_private_key": "This private key type is unsupported", From 3294cd8b101d40c24402c5e077c8539f7785fe8d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 15:43:59 +1000 Subject: [PATCH 79/87] Pass --local-only down to the ServerStatus and Mode so that we can set shorter timeouts for local GUI tests. Update the tests to use a very short timeout --- onionshare_gui/mode.py | 7 +++++-- onionshare_gui/onionshare_gui.py | 4 ++-- onionshare_gui/server_status.py | 26 ++++++++++++++++++-------- unit_tests/commontests.py | 4 ++-- unit_tests/onionshare_timer_test.py | 4 ++-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index 4c21de76..6b156f7e 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -37,7 +37,7 @@ class Mode(QtWidgets.QWidget): starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None): + def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() self.common = common self.qtapp = qtapp @@ -54,12 +54,15 @@ class Mode(QtWidgets.QWidget): # The web object gets created in init() self.web = None + # Local mode is passed from OnionShareGui + self.local_only = local_only + # Threads start out as None self.onion_thread = None self.web_thread = None # Server status - self.server_status = ServerStatus(self.common, self.qtapp, self.app) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_canceled.connect(self.cancel_server) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 8b61a18e..83f3a7e0 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -121,7 +121,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setStatusBar(self.status_bar) # Share mode - self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) + self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only) self.share_mode.init() self.share_mode.server_status.server_started.connect(self.update_server_status_indicator) self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator) @@ -135,7 +135,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.set_server_active.connect(self.set_server_active) # Receive mode - self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray) + self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only) self.receive_mode.init() self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator) self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 9afceb38..32135ca4 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, file_selection=None): + def __init__(self, common, qtapp, app, file_selection=None, local_only=False): super(ServerStatus, self).__init__() self.common = common @@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.local_only = local_only self.resizeEvent(None) # Shutdown timeout layout 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.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.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if self.local_only: + # For testing + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timeout to be 5 minutes into the future + 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 60s from now + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) shutdown_timeout_layout = QtWidgets.QHBoxLayout() shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) @@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget): Reset the timeout in the UI after stopping a share """ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if not self.local_only: + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) def update(self): """ @@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget): """ if self.status == self.STATUS_STOPPED: if self.common.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.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + if self.local_only: + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + else: + # 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.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(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) diff --git a/unit_tests/commontests.py b/unit_tests/commontests.py index 1f6f5896..de1ad9ab 100644 --- a/unit_tests/commontests.py +++ b/unit_tests/commontests.py @@ -203,9 +203,9 @@ class CommonTests(object): self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) # Auto-stop timer tests - def test_set_timeout(self, mode): + def test_set_timeout(self, mode, timeout): '''Test that the timeout can be set''' - timer = QtCore.QDateTime.currentDateTime().addSecs(120) + timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) if mode == 'receive': self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) diff --git a/unit_tests/onionshare_timer_test.py b/unit_tests/onionshare_timer_test.py index 34c77f12..f36331b8 100644 --- a/unit_tests/onionshare_timer_test.py +++ b/unit_tests/onionshare_timer_test.py @@ -108,7 +108,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=8) def test_set_timeout(self): - CommonTests.test_set_timeout(self, 'share') + CommonTests.test_set_timeout(self, 'share', 5) @pytest.mark.run(order=9) def test_server_working_on_start_button_pressed(self): @@ -132,7 +132,7 @@ class OnionShareGuiTest(unittest.TestCase): @pytest.mark.run(order=14) def test_timeout(self): - CommonTests.test_server_timed_out(self, 'share', 120000) + CommonTests.test_server_timed_out(self, 'share', 10000) @pytest.mark.run(order=15) def test_web_service_is_stopped(self): From f6fafc0f5bac40f1c7c5edee67be3cd5d69d8aa8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 15:54:46 +1000 Subject: [PATCH 80/87] Rename test dir to tests. Rename unit_tests to tests_gui_local. Add test dependencies. Update various paths. Add GUI unit tests docs to BUILD.md --- .travis.yml | 2 +- BUILD.md | 17 +++++++++++++++-- MANIFEST.in | 2 +- install/check_lacked_trans.py | 2 +- install/requirements-tests.txt | 11 +++++++++++ {test => tests}/__init__.py | 0 {test => tests}/conftest.py | 0 {test => tests}/test_helpers.py | 0 {test => tests}/test_onionshare.py | 0 {test => tests}/test_onionshare_common.py | 0 {test => tests}/test_onionshare_settings.py | 0 {test => tests}/test_onionshare_strings.py | 0 {test => tests}/test_onionshare_web.py | 0 {unit_tests => tests_gui_local}/__init__.py | 0 {unit_tests => tests_gui_local}/commontests.py | 0 {unit_tests => tests_gui_local}/conftest.py | 0 .../onionshare_receive_mode_upload_test.py | 0 ...hare_receive_mode_upload_test_public_mode.py | 0 .../onionshare_share_mode_download_test.py | 0 ...hare_share_mode_download_test_public_mode.py | 0 ...nshare_share_mode_download_test_stay_open.py | 0 .../onionshare_slug_persistent_test.py | 0 .../onionshare_timer_test.py | 0 .../run_unit_tests.sh | 0 24 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 install/requirements-tests.txt rename {test => tests}/__init__.py (100%) rename {test => tests}/conftest.py (100%) rename {test => tests}/test_helpers.py (100%) rename {test => tests}/test_onionshare.py (100%) rename {test => tests}/test_onionshare_common.py (100%) rename {test => tests}/test_onionshare_settings.py (100%) rename {test => tests}/test_onionshare_strings.py (100%) rename {test => tests}/test_onionshare_web.py (100%) rename {unit_tests => tests_gui_local}/__init__.py (100%) rename {unit_tests => tests_gui_local}/commontests.py (100%) rename {unit_tests => tests_gui_local}/conftest.py (100%) rename {unit_tests => tests_gui_local}/onionshare_receive_mode_upload_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_receive_mode_upload_test_public_mode.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test_public_mode.py (100%) rename {unit_tests => tests_gui_local}/onionshare_share_mode_download_test_stay_open.py (100%) rename {unit_tests => tests_gui_local}/onionshare_slug_persistent_test.py (100%) rename {unit_tests => tests_gui_local}/onionshare_timer_test.py (100%) rename {unit_tests => tests_gui_local}/run_unit_tests.sh (100%) diff --git a/.travis.yml b/.travis.yml index afbaa887..a41339cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,6 @@ before_script: # 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/ +script: pytest --cov=onionshare tests/ after_success: - coveralls diff --git a/BUILD.md b/BUILD.md index 1feedf49..308a186c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -137,8 +137,21 @@ This will prompt you to codesign three binaries and execute one unsigned binary. ## Tests -OnionShare includes PyTest unit tests. To run the tests: +OnionShare includes PyTest unit tests. To run the tests, first install some dependencies: ```sh -pytest test/ +pip3 install -r install/requirements-tests.txt +``` + +If you'd like to run the CLI-based tests that Travis runs: + +```sh +pytest tests/ +``` + +If you would like to run the GUI unit tests in 'local only mode': + +```sh +cd tests_gui_local/ +./run_unit_tests.sh ``` diff --git a/MANIFEST.in b/MANIFEST.in index c8a4d87c..71af3740 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,4 +10,4 @@ include install/onionshare.desktop include install/onionshare.appdata.xml include install/onionshare80.xpm include install/scripts/onionshare-nautilus.py -include test/*.py +include tests/*.py diff --git a/install/check_lacked_trans.py b/install/check_lacked_trans.py index 027edab1..1caa6b27 100644 --- a/install/check_lacked_trans.py +++ b/install/check_lacked_trans.py @@ -59,7 +59,7 @@ def main(): files_in(dir, 'onionshare_gui/share_mode') + \ files_in(dir, 'onionshare_gui/receive_mode') + \ files_in(dir, 'install/scripts') + \ - files_in(dir, 'test') + files_in(dir, 'tests') pysrc = [p for p in src if p.endswith('.py')] lang_code = args.lang_code diff --git a/install/requirements-tests.txt b/install/requirements-tests.txt new file mode 100644 index 00000000..0d9c1581 --- /dev/null +++ b/install/requirements-tests.txt @@ -0,0 +1,11 @@ +atomicwrites==1.2.1 +attrs==18.2.0 +more-itertools==4.3.0 +pluggy==0.6.0 +py==1.6.0 +pytest==3.4.2 +pytest-faulthandler==1.5.0 +pytest-ordering==0.5 +pytest-qt==3.1.0 +six==1.11.0 +urllib3==1.23 diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/conftest.py b/tests/conftest.py similarity index 100% rename from test/conftest.py rename to tests/conftest.py diff --git a/test/test_helpers.py b/tests/test_helpers.py similarity index 100% rename from test/test_helpers.py rename to tests/test_helpers.py diff --git a/test/test_onionshare.py b/tests/test_onionshare.py similarity index 100% rename from test/test_onionshare.py rename to tests/test_onionshare.py diff --git a/test/test_onionshare_common.py b/tests/test_onionshare_common.py similarity index 100% rename from test/test_onionshare_common.py rename to tests/test_onionshare_common.py diff --git a/test/test_onionshare_settings.py b/tests/test_onionshare_settings.py similarity index 100% rename from test/test_onionshare_settings.py rename to tests/test_onionshare_settings.py diff --git a/test/test_onionshare_strings.py b/tests/test_onionshare_strings.py similarity index 100% rename from test/test_onionshare_strings.py rename to tests/test_onionshare_strings.py diff --git a/test/test_onionshare_web.py b/tests/test_onionshare_web.py similarity index 100% rename from test/test_onionshare_web.py rename to tests/test_onionshare_web.py diff --git a/unit_tests/__init__.py b/tests_gui_local/__init__.py similarity index 100% rename from unit_tests/__init__.py rename to tests_gui_local/__init__.py diff --git a/unit_tests/commontests.py b/tests_gui_local/commontests.py similarity index 100% rename from unit_tests/commontests.py rename to tests_gui_local/commontests.py diff --git a/unit_tests/conftest.py b/tests_gui_local/conftest.py similarity index 100% rename from unit_tests/conftest.py rename to tests_gui_local/conftest.py diff --git a/unit_tests/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py similarity index 100% rename from unit_tests/onionshare_receive_mode_upload_test.py rename to tests_gui_local/onionshare_receive_mode_upload_test.py diff --git a/unit_tests/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py similarity index 100% rename from unit_tests/onionshare_receive_mode_upload_test_public_mode.py rename to tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py diff --git a/unit_tests/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test.py rename to tests_gui_local/onionshare_share_mode_download_test.py diff --git a/unit_tests/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test_public_mode.py rename to tests_gui_local/onionshare_share_mode_download_test_public_mode.py diff --git a/unit_tests/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py similarity index 100% rename from unit_tests/onionshare_share_mode_download_test_stay_open.py rename to tests_gui_local/onionshare_share_mode_download_test_stay_open.py diff --git a/unit_tests/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py similarity index 100% rename from unit_tests/onionshare_slug_persistent_test.py rename to tests_gui_local/onionshare_slug_persistent_test.py diff --git a/unit_tests/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py similarity index 100% rename from unit_tests/onionshare_timer_test.py rename to tests_gui_local/onionshare_timer_test.py diff --git a/unit_tests/run_unit_tests.sh b/tests_gui_local/run_unit_tests.sh similarity index 100% rename from unit_tests/run_unit_tests.sh rename to tests_gui_local/run_unit_tests.sh From 6e3ff0b5067673de1216c8646a56459ae3d9197d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 27 Sep 2018 17:33:15 +1000 Subject: [PATCH 81/87] Add Tor GUI unit tests --- BUILD.md | 9 + tests_gui_tor/__init__.py | 0 tests_gui_tor/commontests.py | 359 ++++++++++++++++++ tests_gui_tor/conftest.py | 160 ++++++++ .../onionshare_receive_mode_upload_test.py | 188 +++++++++ ...re_receive_mode_upload_test_public_mode.py | 188 +++++++++ ...onionshare_share_mode_cancel_share_test.py | 159 ++++++++ .../onionshare_share_mode_download_test.py | 195 ++++++++++ ...re_share_mode_download_test_public_mode.py | 195 ++++++++++ ...hare_share_mode_download_test_stay_open.py | 207 ++++++++++ .../onionshare_share_mode_persistent_test.py | 179 +++++++++ .../onionshare_share_mode_stealth_test.py | 174 +++++++++ ...e_share_mode_tor_connection_killed_test.py | 179 +++++++++ tests_gui_tor/onionshare_timer_test.py | 142 +++++++ .../onionshare_tor_connection_killed_test.py | 179 +++++++++ tests_gui_tor/run_unit_tests.sh | 5 + 16 files changed, 2518 insertions(+) create mode 100644 tests_gui_tor/__init__.py create mode 100644 tests_gui_tor/commontests.py create mode 100644 tests_gui_tor/conftest.py create mode 100644 tests_gui_tor/onionshare_receive_mode_upload_test.py create mode 100644 tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py create mode 100644 tests_gui_tor/onionshare_share_mode_cancel_share_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test_public_mode.py create mode 100644 tests_gui_tor/onionshare_share_mode_download_test_stay_open.py create mode 100644 tests_gui_tor/onionshare_share_mode_persistent_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_stealth_test.py create mode 100644 tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py create mode 100644 tests_gui_tor/onionshare_timer_test.py create mode 100644 tests_gui_tor/onionshare_tor_connection_killed_test.py create mode 100755 tests_gui_tor/run_unit_tests.sh diff --git a/BUILD.md b/BUILD.md index 308a186c..00d24cd2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -155,3 +155,12 @@ If you would like to run the GUI unit tests in 'local only mode': cd tests_gui_local/ ./run_unit_tests.sh ``` + +If you would like to run the GUI unit tests in 'tor' (bundled) mode: + +```sh +cd tests_gui_tor/ +./run_unit_tests.sh +``` + +Keep in mind that the Tor tests take a lot longer to run than local mode, but they are also more comprehensive. diff --git a/tests_gui_tor/__init__.py b/tests_gui_tor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py new file mode 100644 index 00000000..a0d9bf5f --- /dev/null +++ b/tests_gui_tor/commontests.py @@ -0,0 +1,359 @@ +import os +import requests +import socket +import socks +import zipfile + +from PyQt5 import QtCore, QtTest +from onionshare import strings + +class CommonTests(object): + def test_gui_loaded(self): + '''Test that the GUI actually is shown''' + self.assertTrue(self.gui.show) + + def test_windowTitle_seen(self): + '''Test that the window title is OnionShare''' + self.assertEqual(self.gui.windowTitle(), 'OnionShare') + + def test_settings_button_is_visible(self): + '''Test that the settings button is visible''' + self.assertTrue(self.gui.settings_button.isVisible()) + + def test_server_status_bar_is_visible(self): + '''Test that the status bar is visible''' + self.assertTrue(self.gui.status_bar.isVisible()) + + def test_info_widget_is_not_visible(self, mode): + '''Test that the info widget along top of screen is not shown''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.info_widget.isVisible()) + + def test_info_widget_is_visible(self, mode): + '''Test that the info widget along top of screen is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.info_widget.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.info_widget.isVisible()) + + def test_click_mode(self, mode): + '''Test that we can switch Mode by clicking the button''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_RECEIVE) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode_button, QtCore.Qt.LeftButton) + self.assertTrue(self.gui.mode, self.gui.MODE_SHARE) + + def test_history_is_visible(self, mode): + '''Test that the History section is visible and that the relevant widget is present''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.uploads.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.downloads.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + + def test_server_working_on_start_button_pressed(self, mode): + '''Test we can start the service''' + # Should be in SERVER_WORKING state + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 1) + if mode == 'share': + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 1) + + def test_server_status_indicator_says_starting(self, mode): + '''Test that the Server Status indicator shows we are Starting''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + if mode == 'share': + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_working', True)) + + def test_settings_button_is_hidden(self): + '''Test that the settings button is hidden when the server starts''' + self.assertFalse(self.gui.settings_button.isVisible()) + + def test_a_server_is_started(self, mode): + '''Test that the server has started''' + QtTest.QTest.qWait(45000) + # Should now be in SERVER_STARTED state + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 2) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 2) + + def test_a_web_server_is_running(self): + '''Test that the web server has started''' + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_have_a_slug(self, mode, public_mode): + '''Test that we have a valid slug''' + if mode == 'receive': + if not public_mode: + self.assertRegex(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.receive_mode.server_status.web.slug, r'(\w+)-(\w+)') + if mode == 'share': + if not public_mode: + self.assertRegex(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + else: + self.assertIsNone(self.gui.share_mode.server_status.web.slug, r'(\w+)-(\w+)') + + def test_have_an_onion_service(self): + '''Test that we have a valid Onion URL''' + self.assertRegex(self.gui.app.onion_host, r'[a-z2-7].onion') + + def test_url_description_shown(self, mode): + '''Test that the URL label is showing''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.url_description.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.url_description.isVisible()) + + def test_have_copy_url_button(self, mode): + '''Test that the Copy URL button is shown''' + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_url_button.isVisible()) + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_url_button.isVisible()) + + def test_server_status_indicator_says_started(self, mode): + '''Test that the Server Status indicator shows we are started''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_started', True)) + if mode == 'share': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_share_started', True)) + + def test_web_page(self, mode, string, public_mode): + '''Test that the web page contains a string''' + (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() + socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port) + + s = socks.socksocket() + s.settimeout(60) + s.connect((self.gui.app.onion_host, 80)) + + if not public_mode: + if mode == 'receive': + path = '/{}'.format(self.gui.receive_mode.server_status.web.slug) + if mode == 'share': + path = '/{}'.format(self.gui.share_mode.server_status.web.slug) + else: + path = '/' + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/webpage', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + f = open('/tmp/webpage') + self.assertTrue(string in f.read()) + f.close() + + def test_history_widgets_present(self, mode): + '''Test that the relevant widgets are present in the history view after activity has taken place''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.uploads.no_uploads_label.isVisible()) + self.assertTrue(self.gui.receive_mode.uploads.clear_history_button.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.downloads.no_downloads_label.isVisible()) + self.assertTrue(self.gui.share_mode.downloads.clear_history_button.isVisible()) + + def test_counter_incremented(self, mode, count): + '''Test that the counter has incremented''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.uploads_completed, count) + if mode == 'share': + self.assertEquals(self.gui.share_mode.downloads_completed, count) + + def test_server_is_stopped(self, mode, stay_open): + '''Test that the server stops when we click Stop''' + if mode == 'receive': + QtTest.QTest.mouseClick(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + if stay_open: + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.status, 0) + + def test_web_service_is_stopped(self): + '''Test that the web server also stopped''' + QtTest.QTest.qWait(2000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # We should be closed by now. Fail if not! + self.assertNotEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) + + def test_server_status_indicator_says_closed(self, mode, stay_open): + '''Test that the Server Status indicator shows we closed''' + if mode == 'receive': + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + if mode == 'share': + if stay_open: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + else: + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + + def test_cancel_the_share(self, mode): + '''Test that we can cancel this share before it's started up ''' + if mode == 'share': + QtTest.QTest.mousePress(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + QtTest.QTest.mouseRelease(self.gui.share_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + if mode == 'receive': + QtTest.QTest.mousePress(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + QtTest.QTest.qWait(1000) + QtTest.QTest.mouseRelease(self.gui.receive_mode.server_status.server_button, QtCore.Qt.LeftButton) + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + + + # Auto-stop timer tests + def test_set_timeout(self, mode, timeout): + '''Test that the timeout can be set''' + timer = QtCore.QDateTime.currentDateTime().addSecs(timeout) + if mode == 'receive': + self.gui.receive_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.receive_mode.server_status.shutdown_timeout.dateTime(), timer) + if mode == 'share': + self.gui.share_mode.server_status.shutdown_timeout.setDateTime(timer) + self.assertTrue(self.gui.share_mode.server_status.shutdown_timeout.dateTime(), timer) + + def test_timeout_widget_hidden(self, mode): + '''Test that the timeout widget is hidden when share has started''' + if mode == 'receive': + self.assertFalse(self.gui.receive_mode.server_status.shutdown_timeout_container.isVisible()) + if mode == 'share': + self.assertFalse(self.gui.share_mode.server_status.shutdown_timeout_container.isVisible()) + + def test_server_timed_out(self, mode, wait): + '''Test that the server has timed out after the timer ran out''' + QtTest.QTest.qWait(wait) + # We should have timed out now + if mode == 'receive': + self.assertEqual(self.gui.receive_mode.server_status.status, 0) + if mode == 'share': + self.assertEqual(self.gui.share_mode.server_status.status, 0) + + # Receive-specific tests + def test_upload_file(self, public_mode, expected_file): + '''Test that we can upload the file''' + (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() + session = requests.session() + session.proxies = {} + session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port) + + files = {'file[]': open('/tmp/test.txt', 'rb')} + if not public_mode: + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.slug) + else: + path = 'http://{}/upload'.format(self.gui.app.onion_host) + response = session.post(path, files=files) + QtTest.QTest.qWait(4000) + self.assertTrue(os.path.isfile(expected_file)) + + # Share-specific tests + def test_file_selection_widget_has_a_file(self): + '''Test that the number of files in the list is 1''' + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 1) + + def test_deleting_only_file_hides_delete_button(self): + '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' + rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.viewport(), QtCore.Qt.LeftButton, pos=rect.center()) + # Delete button should be visible + self.assertTrue(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + # Click delete, and since there's no more files, the delete button should be hidden + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.delete_button, QtCore.Qt.LeftButton) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_add_a_file_and_delete_using_its_delete_widget(self): + '''Test that we can also delete a file by clicking on its [X] widget''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) + self.assertEquals(self.gui.share_mode.server_status.file_selection.get_num_files(), 0) + + def test_file_selection_widget_readd_files(self): + '''Re-add some files to the list so we can share''' + self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') + self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/test.txt') + self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), 2) + + def test_add_delete_buttons_hidden(self): + '''Test that the add and delete buttons are hidden when the server starts''' + self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) + + def test_download_share(self, public_mode): + '''Test that we can download the share''' + (socks_address, socks_port) = self.gui.app.onion.get_tor_socks_port() + socks.set_default_proxy(socks.SOCKS5, socks_address, socks_port) + + s = socks.socksocket() + s.settimeout(60) + s.connect((self.gui.app.onion_host, 80)) + + if public_mode: + path = '/download' + else: + path = '{}/download'.format(self.gui.share_mode.web.slug) + + http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request += 'Host: {}\r\n'.format(self.gui.app.onion_host) + http_request += '\r\n' + s.sendall(http_request.encode('utf-8')) + + with open('/tmp/download.zip', 'wb') as file_to_write: + while True: + data = s.recv(1024) + if not data: + break + file_to_write.write(data) + file_to_write.close() + + zip = zipfile.ZipFile('/tmp/download.zip') + QtTest.QTest.qWait(4000) + self.assertEquals('onionshare', zip.read('test.txt').decode('utf-8')) + + def test_add_button_visible(self): + '''Test that the add button should be visible''' + self.assertTrue(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) + + + # Stealth tests + def test_copy_have_hidserv_auth_button(self, mode): + '''Test that the Copy HidservAuth button is shown''' + if mode == 'share': + self.assertTrue(self.gui.share_mode.server_status.copy_hidservauth_button.isVisible()) + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.server_status.copy_hidservauth_button.isVisible()) + + def test_hidserv_auth_string(self): + '''Test the validity of the HidservAuth string''' + self.assertRegex(self.gui.app.auth_string, r'HidServAuth %s [a-zA-Z1-9]' % self.gui.app.onion_host) + + + # Miscellaneous tests + def test_tor_killed_statusbar_message_shown(self, mode): + '''Test that the status bar message shows Tor was disconnected''' + self.gui.app.onion.cleanup(stop_tor=True) + QtTest.QTest.qWait(2500) + if mode == 'share': + self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) + if mode == 'receive': + self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) diff --git a/tests_gui_tor/conftest.py b/tests_gui_tor/conftest.py new file mode 100644 index 00000000..8ac7efb8 --- /dev/null +++ b/tests_gui_tor/conftest.py @@ -0,0 +1,160 @@ +import sys +# Force tests to look for resources in the source code tree +sys.onionshare_dev_mode = True + +import os +import shutil +import tempfile + +import pytest + +from onionshare import common, web, settings + +@pytest.fixture +def temp_dir_1024(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). + """ + + tmp_dir = tempfile.mkdtemp() + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, 'wb') as f: + f.write(b'*' * 1024) + return tmp_dir + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_dir_1024_delete(): + """ Create a temporary directory that has a single file of a + particular size (1024 bytes). The temporary directory (including + the file inside) will be deleted after fixture usage. + """ + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file, tmp_file_path = tempfile.mkstemp(dir=tmp_dir) + with open(tmp_file, 'wb') as f: + f.write(b'*' * 1024) + yield tmp_dir + + +@pytest.fixture +def temp_file_1024(): + """ Create a temporary file of a particular size (1024 bytes). """ + + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(b'*' * 1024) + return tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture +def temp_file_1024_delete(): + """ + Create a temporary file of a particular size (1024 bytes). + The temporary file will be deleted after fixture usage. + """ + + with tempfile.NamedTemporaryFile() as tmp_file: + tmp_file.write(b'*' * 1024) + tmp_file.flush() + yield tmp_file.name + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope='session') +def custom_zw(): + zw = web.share_mode.ZipWriter( + common.Common(), + zip_filename=common.Common.random_string(4, 6), + processed_size_callback=lambda _: 'custom_callback' + ) + yield zw + zw.close() + os.remove(zw.zip_filename) + + +# pytest > 2.9 only needs @pytest.fixture +@pytest.yield_fixture(scope='session') +def default_zw(): + zw = web.share_mode.ZipWriter(common.Common()) + yield zw + zw.close() + tmp_dir = os.path.dirname(zw.zip_filename) + shutil.rmtree(tmp_dir) + + +@pytest.fixture +def locale_en(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('en_US', 'UTF-8')) + + +@pytest.fixture +def locale_fr(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('fr_FR', 'UTF-8')) + + +@pytest.fixture +def locale_invalid(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('xx_XX', 'UTF-8')) + + +@pytest.fixture +def locale_ru(monkeypatch): + monkeypatch.setattr('locale.getdefaultlocale', lambda: ('ru_RU', 'UTF-8')) + + +@pytest.fixture +def platform_darwin(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Darwin') + + +@pytest.fixture # (scope="session") +def platform_linux(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Linux') + + +@pytest.fixture +def platform_windows(monkeypatch): + monkeypatch.setattr('platform.system', lambda: 'Windows') + + +@pytest.fixture +def sys_argv_sys_prefix(monkeypatch): + monkeypatch.setattr('sys.argv', [sys.prefix]) + + +@pytest.fixture +def sys_frozen(monkeypatch): + monkeypatch.setattr('sys.frozen', True, raising=False) + + +@pytest.fixture +def sys_meipass(monkeypatch): + monkeypatch.setattr( + 'sys._MEIPASS', os.path.expanduser('~'), raising=False) + + +@pytest.fixture # (scope="session") +def sys_onionshare_dev_mode(monkeypatch): + monkeypatch.setattr('sys.onionshare_dev_mode', True, raising=False) + + +@pytest.fixture +def time_time_100(monkeypatch): + monkeypatch.setattr('time.time', lambda: 100) + + +@pytest.fixture +def time_strftime(monkeypatch): + monkeypatch.setattr('time.strftime', lambda _: 'Jun 06 2013 11:05:00') + +@pytest.fixture +def common_obj(): + return common.Common() + +@pytest.fixture +def settings_obj(sys_onionshare_dev_mode, platform_linux): + _common = common.Common() + _common.version = 'DUMMY_VERSION_1.2.3' + return settings.Settings(_common) diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py new file mode 100644 index 00000000..5c2945f1 --- /dev/null +++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + CommonTests.test_info_widget_is_not_visible(self, 'receive') + + @pytest.mark.run(order=6) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'receive') + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'receive') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', False) + + @pytest.mark.run(order=15) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=16) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'receive') + + @pytest.mark.run(order=17) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'receive') + + @pytest.mark.run(order=18) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') + + @pytest.mark.run(order=19) + def test_web_page(self): + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', False) + + @pytest.mark.run(order=20) + def test_upload_file(self): + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test.txt') + + @pytest.mark.run(order=21) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') + + @pytest.mark.run(order=22) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) + + @pytest.mark.run(order=23) + def test_upload_same_file_is_renamed(self): + CommonTests.test_upload_file(self, False, '/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=24) + def test_upload_count_incremented_again(self): + CommonTests.test_counter_incremented(self, 'receive', 2) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'receive', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py new file mode 100644 index 00000000..86cde0d9 --- /dev/null +++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + os.remove('/tmp/OnionShare/test.txt') + os.remove('/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_not_visible(self): + CommonTests.test_info_widget_is_not_visible(self, 'receive') + + @pytest.mark.run(order=6) + def test_click_mode(self): + CommonTests.test_click_mode(self, 'receive') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'receive') + + @pytest.mark.run(order=8) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'receive') + + @pytest.mark.run(order=9) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'receive') + + @pytest.mark.run(order=10) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'receive') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=14) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'receive', True) + + @pytest.mark.run(order=15) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=16) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'receive') + + @pytest.mark.run(order=17) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'receive') + + @pytest.mark.run(order=18) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'receive') + + @pytest.mark.run(order=19) + def test_web_page(self): + CommonTests.test_web_page(self, 'receive', 'Select the files you want to send, then click', True) + + @pytest.mark.run(order=20) + def test_upload_file(self): + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test.txt') + + @pytest.mark.run(order=21) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'receive') + + @pytest.mark.run(order=22) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'receive', 1) + + @pytest.mark.run(order=23) + def test_upload_same_file_is_renamed(self): + CommonTests.test_upload_file(self, True, '/tmp/OnionShare/test-2.txt') + + @pytest.mark.run(order=24) + def test_upload_count_incremented_again(self): + CommonTests.test_counter_incremented(self, 'receive', 2) + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'receive', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'receive', False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py new file mode 100644 index 00000000..a2d1a06a --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_cancel_the_share(self): + CommonTests.test_cancel_the_share(self, 'share') + + @pytest.mark.run(order=18) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=19) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=20) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py new file mode 100644 index 00000000..d1eb5b54 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', False) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, False) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + + @pytest.mark.run(order=28) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py new file mode 100644 index 00000000..4e5f1114 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=26) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=27) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', False) + + @pytest.mark.run(order=28) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py new file mode 100644 index 00000000..78cd1578 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": False, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": True, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', True) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_web_page(self): + CommonTests.test_web_page(self, 'share', 'Total size', True) + + @pytest.mark.run(order=23) + def test_download_share(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=24) + def test_history_widgets_present(self): + CommonTests.test_history_widgets_present(self, 'share') + + @pytest.mark.run(order=25) + def test_counter_incremented(self): + CommonTests.test_counter_incremented(self, 'share', 1) + + @pytest.mark.run(order=26) + def test_download_share_again(self): + CommonTests.test_download_share(self, True) + + @pytest.mark.run(order=27) + def test_counter_incremented_again(self): + CommonTests.test_counter_incremented(self, 'share', 2) + + @pytest.mark.run(order=28) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=29) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=30) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=31) + def test_add_button_visible(self): + CommonTests.test_add_button_visible(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py new file mode 100644 index 00000000..a2d429b2 --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + slug = '' + onion_host = '' + + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": True, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=6) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=8) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=9) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=10) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=11) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=12) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + global slug + slug = self.gui.share_mode.server_status.web.slug + + @pytest.mark.run(order=13) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + global onion_host + onion_host = self.gui.app.onion_host + + @pytest.mark.run(order=14) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=15) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', True) + + @pytest.mark.run(order=16) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + @pytest.mark.run(order=17) + def test_server_status_indicator_says_closed(self): + CommonTests.test_server_status_indicator_says_closed(self, 'share', True) + + @pytest.mark.run(order=18) + def test_server_started_again(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + CommonTests.test_server_status_indicator_says_starting(self, 'share') + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=19) + def test_have_same_slug(self): + '''Test that we have the same slug''' + self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + + @pytest.mark.run(order=20) + def test_have_same_onion(self): + '''Test that we have the same onion''' + self.assertEqual(self.gui.app.onion_host, onion_host) + + @pytest.mark.run(order=21) + def test_server_is_stopped_again(self): + CommonTests.test_server_is_stopped(self, 'share', True) + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py new file mode 100644 index 00000000..948e834a --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": True, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_copy_have_hidserv_auth_button(self): + CommonTests.test_copy_have_hidserv_auth_button(self, 'share') + + @pytest.mark.run(order=23) + def test_hidserv_auth_string(self): + CommonTests.test_hidserv_auth_string(self) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py new file mode 100644 index 00000000..3eeea9bc --- /dev/null +++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_tor_killed_statusbar_message_shown(self): + CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') + + @pytest.mark.run(order=23) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=24) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py new file mode 100644 index 00000000..865b3a8b --- /dev/null +++ b/tests_gui_tor/onionshare_timer_test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": True, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_set_timeout(self): + CommonTests.test_set_timeout(self, 'share', 120) + + @pytest.mark.run(order=9) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=10) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=11) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=12) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=13) + def test_timeout_widget_hidden(self): + CommonTests.test_timeout_widget_hidden(self, 'share') + + @pytest.mark.run(order=14) + def test_timeout(self): + CommonTests.test_server_timed_out(self, 'share', 125000) + + @pytest.mark.run(order=15) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py new file mode 100644 index 00000000..3eeea9bc --- /dev/null +++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import os +import sys +import unittest +import pytest +import json + +from PyQt5 import QtWidgets + +from onionshare.common import Common +from onionshare.web import Web +from onionshare import onion, strings +from onionshare_gui import * + +from .commontests import CommonTests + +app = QtWidgets.QApplication(sys.argv) + +class OnionShareGuiTest(unittest.TestCase): + '''Test the OnionShare GUI''' + @classmethod + def setUpClass(cls): + '''Create the GUI''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + common = Common() + common.define_css() + + # Start the Onion + strings.load_strings(common) + + testonion = onion.Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, False, 0) + + web = Web(common, False, True) + + test_settings = { + "auth_password": "", + "auth_type": "no_auth", + "autoupdate_timestamp": "", + "close_after_first_download": True, + "connection_type": "bundled", + "control_port_address": "127.0.0.1", + "control_port_port": 9051, + "downloads_dir": "/tmp/OnionShare", + "hidservauth_string": "", + "no_bridges": True, + "private_key": "", + "public_mode": False, + "receive_allow_receiver_shutdown": True, + "save_private_key": False, + "shutdown_timeout": False, + "slug": "", + "socks_address": "127.0.0.1", + "socks_port": 9050, + "socket_file_path": "/var/run/tor/control", + "systray_notifications": True, + "tor_bridges_use_meek_lite_azure": False, + "tor_bridges_use_meek_lite_amazon": False, + "tor_bridges_use_custom_bridges": "", + "tor_bridges_use_obfs4": False, + "use_stealth": False, + "use_legacy_v2_onions": False, + "use_autoupdate": True, + "version": "1.3.1" + } + testsettings = '/tmp/testsettings.json' + open(testsettings, 'w').write(json.dumps(test_settings)) + + cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) + + @classmethod + def tearDownClass(cls): + '''Clean up after tests''' + os.remove('/tmp/test.txt') + + @pytest.mark.run(order=1) + def test_gui_loaded(self): + CommonTests.test_gui_loaded(self) + + @pytest.mark.run(order=2) + def test_windowTitle_seen(self): + CommonTests.test_windowTitle_seen(self) + + @pytest.mark.run(order=3) + def test_settings_button_is_visible(self): + CommonTests.test_settings_button_is_visible(self) + + @pytest.mark.run(order=4) + def test_server_status_bar_is_visible(self): + CommonTests.test_server_status_bar_is_visible(self) + + @pytest.mark.run(order=5) + def test_file_selection_widget_has_a_file(self): + CommonTests.test_file_selection_widget_has_a_file(self) + + @pytest.mark.run(order=6) + def test_info_widget_is_visible(self): + CommonTests.test_info_widget_is_visible(self, 'share') + + @pytest.mark.run(order=7) + def test_history_is_visible(self): + CommonTests.test_history_is_visible(self, 'share') + + @pytest.mark.run(order=8) + def test_deleting_only_file_hides_delete_button(self): + CommonTests.test_deleting_only_file_hides_delete_button(self) + + @pytest.mark.run(order=9) + def test_add_a_file_and_delete_using_its_delete_widget(self): + CommonTests.test_add_a_file_and_delete_using_its_delete_widget(self) + + @pytest.mark.run(order=10) + def test_file_selection_widget_readd_files(self): + CommonTests.test_file_selection_widget_readd_files(self) + + @pytest.mark.run(order=11) + def test_server_working_on_start_button_pressed(self): + CommonTests.test_server_working_on_start_button_pressed(self, 'share') + + @pytest.mark.run(order=12) + def test_server_status_indicator_says_starting(self): + CommonTests.test_server_status_indicator_says_starting(self, 'share') + + @pytest.mark.run(order=13) + def test_add_delete_buttons_hidden(self): + CommonTests.test_add_delete_buttons_hidden(self) + + @pytest.mark.run(order=14) + def test_settings_button_is_hidden(self): + CommonTests.test_settings_button_is_hidden(self) + + @pytest.mark.run(order=15) + def test_a_server_is_started(self): + CommonTests.test_a_server_is_started(self, 'share') + + @pytest.mark.run(order=16) + def test_a_web_server_is_running(self): + CommonTests.test_a_web_server_is_running(self) + + @pytest.mark.run(order=17) + def test_have_a_slug(self): + CommonTests.test_have_a_slug(self, 'share', False) + + @pytest.mark.run(order=18) + def test_have_an_onion(self): + CommonTests.test_have_an_onion_service(self) + + @pytest.mark.run(order=19) + def test_url_description_shown(self): + CommonTests.test_url_description_shown(self, 'share') + + @pytest.mark.run(order=20) + def test_have_copy_url_button(self): + CommonTests.test_have_copy_url_button(self, 'share') + + @pytest.mark.run(order=21) + def test_server_status_indicator_says_started(self): + CommonTests.test_server_status_indicator_says_started(self, 'share') + + @pytest.mark.run(order=22) + def test_tor_killed_statusbar_message_shown(self): + CommonTests.test_tor_killed_statusbar_message_shown(self, 'share') + + @pytest.mark.run(order=23) + def test_server_is_stopped(self): + CommonTests.test_server_is_stopped(self, 'share', False) + + @pytest.mark.run(order=24) + def test_web_service_is_stopped(self): + CommonTests.test_web_service_is_stopped(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests_gui_tor/run_unit_tests.sh b/tests_gui_tor/run_unit_tests.sh new file mode 100755 index 00000000..d15f8a6e --- /dev/null +++ b/tests_gui_tor/run_unit_tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +for test in `ls -1 | egrep ^onionshare_`; do + py.test-3 $test -vvv || exit 1 +done From 111f69f30e2d46ac033261e689b02c77ed1afac6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 17:34:46 -0700 Subject: [PATCH 82/87] Remove all the extra QApplications --- tests_gui_local/onionshare_receive_mode_upload_test.py | 6 ++---- .../onionshare_receive_mode_upload_test_public_mode.py | 6 ++---- tests_gui_local/onionshare_share_mode_download_test.py | 4 +--- .../onionshare_share_mode_download_test_public_mode.py | 4 +--- .../onionshare_share_mode_download_test_stay_open.py | 4 +--- tests_gui_local/onionshare_slug_persistent_test.py | 4 +--- tests_gui_local/onionshare_timer_test.py | 4 +--- tests_gui_tor/onionshare_receive_mode_upload_test.py | 6 ++---- .../onionshare_receive_mode_upload_test_public_mode.py | 6 ++---- tests_gui_tor/onionshare_share_mode_cancel_share_test.py | 4 +--- tests_gui_tor/onionshare_share_mode_download_test.py | 4 +--- .../onionshare_share_mode_download_test_public_mode.py | 4 +--- .../onionshare_share_mode_download_test_stay_open.py | 4 +--- tests_gui_tor/onionshare_share_mode_persistent_test.py | 4 +--- tests_gui_tor/onionshare_share_mode_stealth_test.py | 4 +--- .../onionshare_share_mode_tor_connection_killed_test.py | 4 +--- tests_gui_tor/onionshare_timer_test.py | 4 +--- tests_gui_tor/onionshare_tor_connection_killed_test.py | 4 +--- 18 files changed, 22 insertions(+), 58 deletions(-) diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index bac622fb..2aa2ed94 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index 8ed385f5..30a290e7 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index 0ef03e97..c546fb61 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index 417eb8b9..764b5885 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index c3177a38..b92ff097 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 05c9719a..1e5614dc 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' slug = '' @@ -50,7 +48,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py index f36331b8..1a5134e2 100644 --- a/tests_gui_local/onionshare_timer_test.py +++ b/tests_gui_local/onionshare_timer_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test.py b/tests_gui_tor/onionshare_receive_mode_upload_test.py index 5c2945f1..5be400e2 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py index 86cde0d9..9c9553a4 100644 --- a/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_tor/onionshare_receive_mode_upload_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, @@ -72,7 +70,7 @@ class OnionShareGuiTest(unittest.TestCase): open(testsettings, 'w').write(json.dumps(test_settings)) cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, False) - + @classmethod def tearDownClass(cls): '''Clean up after tests''' diff --git a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py index a2d1a06a..466109d7 100644 --- a/tests_gui_tor/onionshare_share_mode_cancel_share_test.py +++ b/tests_gui_tor/onionshare_share_mode_cancel_share_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test.py b/tests_gui_tor/onionshare_share_mode_download_test.py index d1eb5b54..1c8e1b6c 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test.py +++ b/tests_gui_tor/onionshare_share_mode_download_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py index 4e5f1114..c292e729 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_public_mode.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py index 78cd1578..7838316f 100644 --- a/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_tor/onionshare_share_mode_download_test_stay_open.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": True, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_persistent_test.py b/tests_gui_tor/onionshare_share_mode_persistent_test.py index a2d429b2..3cffaab6 100644 --- a/tests_gui_tor/onionshare_share_mode_persistent_test.py +++ b/tests_gui_tor/onionshare_share_mode_persistent_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' slug = '' @@ -51,7 +49,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_stealth_test.py b/tests_gui_tor/onionshare_share_mode_stealth_test.py index 948e834a..aaf6fbc6 100644 --- a/tests_gui_tor/onionshare_share_mode_stealth_test.py +++ b/tests_gui_tor/onionshare_share_mode_stealth_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py index 3eeea9bc..861b7ccc 100644 --- a/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_share_mode_tor_connection_killed_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_timer_test.py b/tests_gui_tor/onionshare_timer_test.py index 865b3a8b..b76106d9 100644 --- a/tests_gui_tor/onionshare_timer_test.py +++ b/tests_gui_tor/onionshare_timer_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, diff --git a/tests_gui_tor/onionshare_tor_connection_killed_test.py b/tests_gui_tor/onionshare_tor_connection_killed_test.py index 3eeea9bc..861b7ccc 100644 --- a/tests_gui_tor/onionshare_tor_connection_killed_test.py +++ b/tests_gui_tor/onionshare_tor_connection_killed_test.py @@ -14,8 +14,6 @@ from onionshare_gui import * from .commontests import CommonTests -app = QtWidgets.QApplication(sys.argv) - class OnionShareGuiTest(unittest.TestCase): '''Test the OnionShare GUI''' @classmethod @@ -48,7 +46,7 @@ class OnionShareGuiTest(unittest.TestCase): "control_port_port": 9051, "downloads_dir": "/tmp/OnionShare", "hidservauth_string": "", - "no_bridges": True, + "no_bridges": True, "private_key": "", "public_mode": False, "receive_allow_receiver_shutdown": True, From 04fbda9ca67aebb7243a83055564f4c0f731f005 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 17:51:16 -0700 Subject: [PATCH 83/87] Add @mig5 as a code owner for all tests, and add @emmapeel2 as a code owner for locales --- .github/CODEOWNERS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1cd5a1f6..42d1840f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,9 @@ * @micahflee + +# localization +/share/locale/ @emmapeel2 + +# tests +/tests/ @mig5 +/tests_gui_local/ @mig5 +/tests_gui_tor/ @mig5 From a72d315ae808cb1dbda7db25930c68c3383a2044 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:19:42 -0700 Subject: [PATCH 84/87] Update Travis CI to run GUI tests --- .travis.yml | 12 ++++++++---- tests_gui_local/run_unit_tests.sh | 2 +- tests_gui_tor/run_unit_tests.sh | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index a41339cc..aa1ff102 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python -# sudo: required -dist: bionic +dist: trusty +sudo: required python: - "3.6" - "3.6-dev" @@ -8,14 +8,18 @@ python: - "nightly" # command to install dependencies install: + - sudo apt-get update && sudo apt-get install python3-pyqt5 - pip install -r install/requirements.txt + - pip install -r install/requirements-tests.txt - pip install 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 tests/ +# run CLI tests and local GUI tests +script: + - pytest --cov=onionshare tests/ + - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh after_success: - coveralls diff --git a/tests_gui_local/run_unit_tests.sh b/tests_gui_local/run_unit_tests.sh index d15f8a6e..7d207a57 100755 --- a/tests_gui_local/run_unit_tests.sh +++ b/tests_gui_local/run_unit_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - py.test-3 $test -vvv || exit 1 + pytest $test -vvv || exit 1 done diff --git a/tests_gui_tor/run_unit_tests.sh b/tests_gui_tor/run_unit_tests.sh index d15f8a6e..7d207a57 100755 --- a/tests_gui_tor/run_unit_tests.sh +++ b/tests_gui_tor/run_unit_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash for test in `ls -1 | egrep ^onionshare_`; do - py.test-3 $test -vvv || exit 1 + pytest $test -vvv || exit 1 done From bdf91361b4b62a893dcc70ace2bdca0c0b3cf90d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:22:10 -0700 Subject: [PATCH 85/87] Keep trying ports until it finds a free one --- onionshare/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare/common.py b/onionshare/common.py index 0ce411e8..28b282c2 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -433,7 +433,7 @@ class Common(object): tmpsock.bind(("127.0.0.1", random.randint(min_port, max_port))) break except OSError as e: - raise OSError(e) + pass _, port = tmpsock.getsockname() return port From 522d8e2d785db04991902781845bc176c704f5e1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:27:29 -0700 Subject: [PATCH 86/87] Remove submitting to coveralls --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa1ff102..e0b5b822 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - sudo apt-get update && sudo apt-get install python3-pyqt5 - pip install -r install/requirements.txt - pip install -r install/requirements-tests.txt - - pip install pytest-cov coveralls flake8 + - pip install pytest-cov 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 @@ -21,5 +21,3 @@ before_script: script: - pytest --cov=onionshare tests/ - cd tests_gui_local/ && xvfb-run ./run_unit_tests.sh -after_success: - - coveralls From eaa3a152b0372580f558dfb143a61cbfe164ea00 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 27 Sep 2018 21:34:39 -0700 Subject: [PATCH 87/87] One script to run all tests --- dev_scripts/run_all_tests.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 dev_scripts/run_all_tests.sh diff --git a/dev_scripts/run_all_tests.sh b/dev_scripts/run_all_tests.sh new file mode 100755 index 00000000..90ef1dc0 --- /dev/null +++ b/dev_scripts/run_all_tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +ROOT="$( dirname $(cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd ))" + +# CLI tests +cd $ROOT +pytest tests/ + +# Local GUI tests +cd $ROOT/tests_gui_local +./run_unit_tests.sh + +# Tor GUI tests +cd $ROOT/tests_gui_tor +./run_unit_tests.sh