From 2d43588a3bfd6afee8bcc4239a3259870bfc504b Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 19 Apr 2019 14:25:42 +0200 Subject: [PATCH 01/33] Add website sharing and directory listing cli-only --- install/check_lacked_trans.py | 1 + install/requirements.txt | 1 + onionshare/__init__.py | 15 ++++ onionshare/web/web.py | 6 +- onionshare/web/website_mode.py | 154 +++++++++++++++++++++++++++++++++ share/templates/listing.html | 40 +++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 onionshare/web/website_mode.py create mode 100644 share/templates/listing.html diff --git a/install/check_lacked_trans.py b/install/check_lacked_trans.py index 010cdb7a..5ccce923 100755 --- a/install/check_lacked_trans.py +++ b/install/check_lacked_trans.py @@ -59,6 +59,7 @@ def main(): files_in(dir, 'onionshare_gui/mode') + \ files_in(dir, 'onionshare_gui/mode/share_mode') + \ files_in(dir, 'onionshare_gui/mode/receive_mode') + \ + files_in(dir, 'onionshare_gui/mode/website_mode') + \ files_in(dir, 'install/scripts') + \ files_in(dir, 'tests') pysrc = [p for p in src if p.endswith('.py')] diff --git a/install/requirements.txt b/install/requirements.txt index 0abd773f..fff0b009 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -3,6 +3,7 @@ certifi==2019.3.9 chardet==3.0.4 Click==7.0 Flask==1.0.2 +Flask-HTTPAuth future==0.17.1 idna==2.8 itsdangerous==1.1.0 diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 620ada98..dad092ed 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -51,6 +51,7 @@ def main(cwd=None): parser.add_argument('--connect-timeout', metavar='', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)") parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)") parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them") + parser.add_argument('--website', action='store_true', dest='website', help=strings._("help_website")) parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)") parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk") parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share") @@ -68,10 +69,13 @@ def main(cwd=None): connect_timeout = int(args.connect_timeout) stealth = bool(args.stealth) receive = bool(args.receive) + website = bool(args.website) config = args.config if receive: mode = 'receive' + elif website: + mode = 'website' else: mode = 'share' @@ -168,6 +172,15 @@ def main(cwd=None): print(e.args[0]) sys.exit() + if mode == 'website': + # Prepare files to share + print(strings._("preparing_website")) + try: + web.website_mode.set_file_info(filenames) + except OSError as e: + print(e.strerror) + sys.exit(1) + if mode == 'share': # Prepare files to share print("Compressing files.") @@ -206,6 +219,8 @@ def main(cwd=None): # Build the URL if common.settings.get('public_mode'): url = 'http://{0:s}'.format(app.onion_host) + elif mode == 'website': + url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) else: url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index edaf75f1..0ba8c6b3 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -15,7 +15,7 @@ from .. import strings from .share_mode import ShareModeWeb from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest - +from .website_mode import WebsiteModeWeb # Stub out flask's show_server_banner function, to avoiding showing warnings that # are not applicable to OnionShare @@ -111,13 +111,15 @@ class Web(object): self.receive_mode = None if self.mode == 'receive': self.receive_mode = ReceiveModeWeb(self.common, self) + elif self.mode == 'website': + self.website_mode = WebsiteModeWeb(self.common, self) elif self.mode == 'share': self.share_mode = ShareModeWeb(self.common, self) def define_common_routes(self): """ - Common web app routes between sending and receiving + Common web app routes between sending, receiving and website modes. """ @self.app.errorhandler(404) def page_not_found(e): diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py new file mode 100644 index 00000000..7b8429ae --- /dev/null +++ b/onionshare/web/website_mode.py @@ -0,0 +1,154 @@ +import os +import sys +import tempfile +import mimetypes +from flask import Response, request, render_template, make_response, send_from_directory +from flask_httpauth import HTTPBasicAuth + +from .. import strings + + +class WebsiteModeWeb(object): + """ + All of the web logic for share mode + """ + def __init__(self, common, web): + self.common = common + self.common.log('WebsiteModeWeb', '__init__') + + self.web = web + self.auth = HTTPBasicAuth() + + # Information about the file to be shared + self.file_info = [] + self.website_folder = '' + self.download_filesize = 0 + self.visit_count = 0 + + self.users = { } + + self.define_routes() + + def define_routes(self): + """ + The web app routes for sharing a website + """ + + @self.auth.get_password + def get_pw(username): + self.users['onionshare'] = self.web.slug + + if self.common.settings.get('public_mode'): + return True # let the request through, no questions asked! + elif username in self.users: + return self.users.get(username) + else: + return None + + @self.web.app.route('/download/') + @self.auth.login_required + def path_download(page_path): + return path_download(page_path) + + @self.web.app.route('/') + @self.auth.login_required + def path_public(page_path): + return path_logic(page_path) + + @self.web.app.route("/") + @self.auth.login_required + def index_public(): + return path_logic('') + + def path_download(file_path=''): + """ + Render the download links. + """ + self.web.add_request(self.web.REQUEST_LOAD, request.path) + if not os.path.isfile(os.path.join(self.website_folder, file_path)): + return self.web.error404() + + return send_from_directory(self.website_folder, file_path) + + def path_logic(page_path=''): + """ + Render the onionshare website. + """ + + self.web.add_request(self.web.REQUEST_LOAD, request.path) + + if self.file_info['files']: + self.website_folder = os.path.dirname(self.file_info['files'][0]['filename']) + elif self.file_info['dirs']: + self.website_folder = self.file_info['dirs'][0]['filename'] + else: + return self.web.error404() + + if any((fname == 'index.html') for fname in os.listdir(self.website_folder)): + self.web.app.static_url_path = self.website_folder + self.web.app.static_folder = self.website_folder + if not os.path.isfile(os.path.join(self.website_folder, page_path)): + page_path = os.path.join(page_path, 'index.html') + + return send_from_directory(self.website_folder, page_path) + + elif any(os.path.isfile(os.path.join(self.website_folder, i)) for i in os.listdir(self.website_folder)): + filenames = [] + for i in os.listdir(self.website_folder): + filenames.append(os.path.join(self.website_folder, i)) + + self.set_file_info(filenames) + + r = make_response(render_template( + 'listing.html', + file_info=self.file_info, + filesize=self.download_filesize, + filesize_human=self.common.human_readable_filesize(self.download_filesize))) + + return self.web.add_security_headers(r) + + else: + return self.web.error404() + + + 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("WebsiteModeWeb", "set_file_info") + self.web.cancel_compression = True + + self.cleanup_filenames = [] + + # 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.download_filesize += info['size'] + + self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) + self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) + + # Check if there's only 1 file and no folders + if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: + self.download_filename = self.file_info['files'][0]['filename'] + self.download_filesize = self.file_info['files'][0]['size'] + + self.download_filesize = os.path.getsize(self.download_filename) + + + return True diff --git a/share/templates/listing.html b/share/templates/listing.html new file mode 100644 index 00000000..a514e5d2 --- /dev/null +++ b/share/templates/listing.html @@ -0,0 +1,40 @@ + + + + OnionShare + + + + + +
+
+
    +
  • Total size: {{ filesize_human }}
  • +
+
+ +

OnionShare

+
+ + + + + + + + + {% for info in file_info.files %} + + + + + + {% endfor %} +
FilenameSize
+ + {{ info.basename }} + {{ info.size_human }}download
+ + + From 391c82f2a6ac7e0260f06d6018df57bc52da95da Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 19 Apr 2019 16:52:43 +0200 Subject: [PATCH 02/33] Add gui for website sharing and listing --- onionshare/__init__.py | 2 +- onionshare/web/website_mode.py | 16 +- .../mode/{share_mode => }/file_selection.py | 2 +- onionshare_gui/mode/share_mode/__init__.py | 2 +- onionshare_gui/mode/website_mode/__init__.py | 323 ++++++++++++++++++ onionshare_gui/onionshare_gui.py | 64 ++++ onionshare_gui/server_status.py | 25 +- setup.py | 1 + share/locale/en.json | 1 + 9 files changed, 426 insertions(+), 10 deletions(-) rename onionshare_gui/mode/{share_mode => }/file_selection.py (99%) create mode 100644 onionshare_gui/mode/website_mode/__init__.py diff --git a/onionshare/__init__.py b/onionshare/__init__.py index dad092ed..a96f2fca 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -257,7 +257,7 @@ def main(cwd=None): if app.autostop_timer > 0: # if the auto-stop timer was set and has run out, stop the server if not app.autostop_timer_thread.is_alive(): - if mode == 'share': + if mode == 'share' or (mode == 'website'): # If there were no attempts to download the share, or all downloads are done, we can stop if web.share_mode.download_count == 0 or web.done: print("Stopped because auto-stop timer ran out") diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 7b8429ae..51137183 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -38,25 +38,29 @@ class WebsiteModeWeb(object): def get_pw(username): self.users['onionshare'] = self.web.slug - if self.common.settings.get('public_mode'): - return True # let the request through, no questions asked! - elif username in self.users: + if username in self.users: return self.users.get(username) else: return None + @self.web.app.before_request + def conditional_auth_check(): + if not self.common.settings.get('public_mode'): + @self.auth.login_required + def _check_login(): + return None + + return _check_login() + @self.web.app.route('/download/') - @self.auth.login_required def path_download(page_path): return path_download(page_path) @self.web.app.route('/') - @self.auth.login_required def path_public(page_path): return path_logic(page_path) @self.web.app.route("/") - @self.auth.login_required def index_public(): return path_logic('') diff --git a/onionshare_gui/mode/share_mode/file_selection.py b/onionshare_gui/mode/file_selection.py similarity index 99% rename from onionshare_gui/mode/share_mode/file_selection.py rename to onionshare_gui/mode/file_selection.py index 0d4229fe..a7af61f8 100644 --- a/onionshare_gui/mode/share_mode/file_selection.py +++ b/onionshare_gui/mode/file_selection.py @@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from ...widgets import Alert, AddFileDialog +from ..widgets import Alert, AddFileDialog class DropHereLabel(QtWidgets.QLabel): """ diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 6cb50b2b..1ee40ca3 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -25,7 +25,7 @@ from onionshare.onion import * from onionshare.common import Common from onionshare.web import Web -from .file_selection import FileSelection +from ..file_selection import FileSelection from .threads import CompressThread from .. import Mode from ..history import History, ToggleHistory, ShareHistoryItem diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py new file mode 100644 index 00000000..e10da8b9 --- /dev/null +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -0,0 +1,323 @@ +# -*- 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 os +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings +from onionshare.onion import * +from onionshare.common import Common +from onionshare.web import Web + +from ..file_selection import FileSelection +from .. import Mode +from ..history import History, ToggleHistory, DownloadHistoryItem +from ...widgets import Alert + +class WebsiteMode(Mode): + """ + Parts of the main window UI for sharing files. + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def init(self): + """ + Custom initialization for ReceiveMode. + """ + # Threads start out as None + self.compress_thread = None + + # Create the Web object + self.web = Web(self.common, True, 'website') + + # File selection + self.file_selection = FileSelection(self.common, self) + if self.filenames: + for filename in self.filenames: + self.file_selection.file_list.add_file(filename) + + # Server status + self.server_status.set_mode('website', self.file_selection) + self.server_status.server_started.connect(self.file_selection.server_started) + self.server_status.server_stopped.connect(self.file_selection.server_stopped) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.file_selection.server_stopped) + self.server_status.server_canceled.connect(self.update_primary_action) + self.file_selection.file_list.files_updated.connect(self.server_status.update) + self.file_selection.file_list.files_updated.connect(self.update_primary_action) + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Filesize warning + self.filesize_warning = QtWidgets.QLabel() + self.filesize_warning.setWordWrap(True) + self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning']) + self.filesize_warning.hide() + + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))), + strings._('gui_no_downloads'), + strings._('gui_downloads') + ) + self.history.hide() + + # Info label + self.info_label = QtWidgets.QLabel() + self.info_label.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, self, self.history, + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) + ) + + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(self.info_label) + top_bar_layout.addStretch() + top_bar_layout.addWidget(self.toggle_history) + + # Primary action layout + self.primary_action_layout.addWidget(self.filesize_warning) + self.primary_action.hide() + self.update_primary_action() + + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(top_bar_layout) + self.main_layout.addLayout(self.file_selection) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addWidget(self.min_width_widget) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QHBoxLayout() + self.wrapper_layout.addLayout(self.main_layout) + self.wrapper_layout.addWidget(self.history) + self.setLayout(self.wrapper_layout) + + # Always start with focus on file selection + self.file_selection.setFocus() + + def get_stop_server_shutdown_timeout_text(self): + """ + Return the string to put on the stop server button, if there's a shutdown timeout + """ + return strings._('gui_share_stop_server_shutdown_timeout') + + def timeout_finished_should_stop_server(self): + """ + 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.website_mode.download_count == 0 or self.web.done: + self.server_status.stop_server() + self.server_status_label.setText(strings._('close_on_timeout')) + return True + # A download is probably still running - hold off on stopping the share + else: + self.server_status_label.setText(strings._('timeout_download_still_running')) + return False + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.website_mode.download_count = 0 + self.web.error404_count = 0 + + # Hide and reset the downloads if we have previously shared + self.reset_info_counters() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. Zipping up files. + """ + self.filenames = [] + for index in range(self.file_selection.file_list.count()): + self.filenames.append(self.file_selection.file_list.item(index).filename) + + # Continue + self.starting_server_step3.emit() + self.start_server_finished.emit() + + + def start_server_step3_custom(self): + """ + Step 3 in starting the server. Display large filesize + warning, if applicable. + """ + + # Warn about sending large files over Tor + if self.web.website_mode.download_filesize >= 157286400: # 150mb + self.filesize_warning.setText(strings._("large_filesize")) + self.filesize_warning.show() + + if self.web.website_mode.set_file_info(self.filenames): + self.success.emit() + else: + # Cancelled + pass + + def start_server_error_custom(self): + """ + Start server error. + """ + if self._zip_progress_bar is not None: + self.status_bar.removeWidget(self._zip_progress_bar) + self._zip_progress_bar = None + + def stop_server_custom(self): + """ + Stop server. + """ + + self.filesize_warning.hide() + self.history.in_progress_count = 0 + self.history.completed_count = 0 + self.history.update_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('WebsiteMode', 'cancel_server: quitting compress thread') + self.compress_thread.quit() + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def handle_request_load(self, event): + """ + Handle REQUEST_LOAD event. + """ + self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_download_page_loaded_message')) + + def handle_request_started(self, event): + """ + Handle REQUEST_STARTED event. + """ + + filesize = self.web.website_mode.download_filesize + + item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + self.history.in_progress_count += 1 + self.history.update_in_progress() + + self.system_tray.showMessage(strings._('systray_download_started_title'), strings._('systray_download_started_message')) + + def handle_request_progress(self, event): + """ + Handle REQUEST_PROGRESS event. + """ + self.history.update(event["data"]["id"], event["data"]["bytes"]) + + # Is the download complete? + if event["data"]["bytes"] == self.web.website_mode.filesize: + self.system_tray.showMessage(strings._('systray_download_completed_title'), strings._('systray_download_completed_message')) + + # Update completed and in progress labels + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() + + # Close on finish? + if self.common.settings.get('close_after_first_download'): + self.server_status.stop_server() + self.status_bar.clearMessage() + self.server_status_label.setText(strings._('closing_automatically')) + else: + if self.server_status.status == self.server_status.STATUS_STOPPED: + self.history.cancel(event["data"]["id"]) + self.history.in_progress_count = 0 + self.history.update_in_progress() + + def handle_request_canceled(self, event): + """ + Handle REQUEST_CANCELED event. + """ + self.history.cancel(event["data"]["id"]) + + # Update in progress count + self.history.in_progress_count -= 1 + self.history.update_in_progress() + self.system_tray.showMessage(strings._('systray_download_canceled_title'), strings._('systray_download_canceled_message')) + + def on_reload_settings(self): + """ + If there were some files listed for sharing, we should be ok to re-enable + the 'Start Sharing' button now. + """ + if self.server_status.file_selection.get_num_files() > 0: + self.primary_action.show() + self.info_label.show() + + def update_primary_action(self): + self.common.log('WebsiteMode', 'update_primary_action') + + # Show or hide primary action layout + file_count = self.file_selection.file_list.count() + if file_count > 0: + self.primary_action.show() + self.info_label.show() + + # Update the file count in the info label + total_size_bytes = 0 + for index in range(self.file_selection.file_list.count()): + item = self.file_selection.file_list.item(index) + total_size_bytes += item.size_bytes + total_size_readable = self.common.human_readable_filesize(total_size_bytes) + + if file_count > 1: + self.info_label.setText(strings._('gui_file_info').format(file_count, total_size_readable)) + else: + self.info_label.setText(strings._('gui_file_info_single').format(file_count, total_size_readable)) + + else: + self.primary_action.hide() + self.info_label.hide() + + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.history.reset() + + @staticmethod + def _compute_total_size(filenames): + total_size = 0 + for filename in filenames: + if os.path.isfile(filename): + total_size += os.path.getsize(filename) + if os.path.isdir(filename): + total_size += Common.dir_size(filename) + return total_size diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 17839669..9fdf9395 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -25,6 +25,7 @@ from onionshare.web import Web from .mode.share_mode import ShareMode from .mode.receive_mode import ReceiveMode +from .mode.website_mode import WebsiteMode from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog @@ -39,6 +40,7 @@ class OnionShareGui(QtWidgets.QMainWindow): """ MODE_SHARE = 'share' MODE_RECEIVE = 'receive' + MODE_WEBSITE = 'website' def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False): super(OnionShareGui, self).__init__() @@ -92,6 +94,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button')); self.receive_mode_button.setFixedHeight(50) self.receive_mode_button.clicked.connect(self.receive_mode_clicked) + self.website_mode_button = QtWidgets.QPushButton(strings._('gui_mode_website_button')); + self.website_mode_button.setFixedHeight(50) + self.website_mode_button.clicked.connect(self.website_mode_clicked) self.settings_button = QtWidgets.QPushButton() self.settings_button.setDefault(False) self.settings_button.setFixedWidth(40) @@ -103,6 +108,7 @@ class OnionShareGui(QtWidgets.QMainWindow): mode_switcher_layout.setSpacing(0) mode_switcher_layout.addWidget(self.share_mode_button) mode_switcher_layout.addWidget(self.receive_mode_button) + mode_switcher_layout.addWidget(self.website_mode_button) mode_switcher_layout.addWidget(self.settings_button) # Server status indicator on the status bar @@ -154,6 +160,20 @@ class OnionShareGui(QtWidgets.QMainWindow): self.receive_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) self.receive_mode.set_server_active.connect(self.set_server_active) + # Website mode + self.website_mode = WebsiteMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) + self.website_mode.init() + self.website_mode.server_status.server_started.connect(self.update_server_status_indicator) + self.website_mode.server_status.server_stopped.connect(self.update_server_status_indicator) + self.website_mode.start_server_finished.connect(self.update_server_status_indicator) + self.website_mode.stop_server_finished.connect(self.update_server_status_indicator) + self.website_mode.stop_server_finished.connect(self.stop_server_finished) + self.website_mode.start_server_finished.connect(self.clear_message) + self.website_mode.server_status.button_clicked.connect(self.clear_message) + self.website_mode.server_status.url_copied.connect(self.copy_url) + self.website_mode.server_status.hidservauth_copied.connect(self.copy_hidservauth) + self.website_mode.set_server_active.connect(self.set_server_active) + self.update_mode_switcher() self.update_server_status_indicator() @@ -162,6 +182,7 @@ class OnionShareGui(QtWidgets.QMainWindow): contents_layout.setContentsMargins(10, 0, 10, 0) contents_layout.addWidget(self.receive_mode) contents_layout.addWidget(self.share_mode) + contents_layout.addWidget(self.website_mode) layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -199,15 +220,27 @@ class OnionShareGui(QtWidgets.QMainWindow): if self.mode == self.MODE_SHARE: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) + self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode.hide() self.share_mode.show() + self.website_mode.hide() + elif self.mode == self.MODE_WEBSITE: + self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) + self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) + self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) + + self.receive_mode.hide() + self.share_mode.hide() + self.website_mode.show() else: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) + self.website_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.share_mode.hide() self.receive_mode.show() + self.website_mode.hide() self.update_server_status_indicator() @@ -223,6 +256,12 @@ class OnionShareGui(QtWidgets.QMainWindow): self.mode = self.MODE_RECEIVE self.update_mode_switcher() + def website_mode_clicked(self): + if self.mode != self.MODE_WEBSITE: + self.common.log('OnionShareGui', 'website_mode_clicked') + self.mode = self.MODE_WEBSITE + self.update_mode_switcher() + def update_server_status_indicator(self): # Set the status image if self.mode == self.MODE_SHARE: @@ -239,6 +278,17 @@ class OnionShareGui(QtWidgets.QMainWindow): elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) self.server_status_label.setText(strings._('gui_status_indicator_share_started')) + elif self.mode == self.MODE_WEBSITE: + # Website mode + if self.website_mode.server_status.status == ServerStatus.STATUS_STOPPED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) + self.server_status_label.setText(strings._('gui_status_indicator_share_stopped')) + elif self.website_mode.server_status.status == ServerStatus.STATUS_WORKING: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) + self.server_status_label.setText(strings._('gui_status_indicator_share_working')) + elif self.website_mode.server_status.status == ServerStatus.STATUS_STARTED: + self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) + self.server_status_label.setText(strings._('gui_status_indicator_share_started')) else: # Receive mode if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: @@ -317,6 +367,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.timer.start(500) self.share_mode.on_reload_settings() self.receive_mode.on_reload_settings() + self.website_mode.on_reload_settings() self.status_bar.clearMessage() # If we switched off the auto-stop timer setting, ensure the widget is hidden. @@ -337,6 +388,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # When settings close, refresh the server status UI self.share_mode.server_status.update() self.receive_mode.server_status.update() + self.website_mode.server_status.update() def check_for_updates(self): """ @@ -367,10 +419,13 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.handle_tor_broke() self.receive_mode.handle_tor_broke() + self.website_mode.handle_tor_broke() # Process events from the web object if self.mode == self.MODE_SHARE: mode = self.share_mode + elif self.mode == self.MODE_WEBSITE: + mode = self.website_mode else: mode = self.receive_mode @@ -450,13 +505,20 @@ class OnionShareGui(QtWidgets.QMainWindow): if self.mode == self.MODE_SHARE: self.share_mode_button.show() self.receive_mode_button.hide() + self.website_mode_button.hide() + elif self.mode == self.MODE_WEBSITE: + self.share_mode_button.hide() + self.receive_mode_button.hide() + self.website_mode_button.show() else: self.share_mode_button.hide() self.receive_mode_button.show() + self.website_mode_button.hide() else: self.settings_button.show() self.share_mode_button.show() self.receive_mode_button.show() + self.website_mode_button.show() # Disable settings menu action when server is active self.settings_action.setEnabled(not active) @@ -466,6 +528,8 @@ class OnionShareGui(QtWidgets.QMainWindow): try: if self.mode == OnionShareGui.MODE_SHARE: server_status = self.share_mode.server_status + if self.mode == OnionShareGui.MODE_WEBSITE: + server_status = self.website_mode.server_status else: server_status = self.receive_mode.server_status if server_status.status != server_status.STATUS_STOPPED: diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 0c51119e..755904ea 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -39,6 +39,7 @@ class ServerStatus(QtWidgets.QWidget): MODE_SHARE = 'share' MODE_RECEIVE = 'receive' + MODE_WEBSITE = 'website' STATUS_STOPPED = 0 STATUS_WORKING = 1 @@ -159,7 +160,7 @@ class ServerStatus(QtWidgets.QWidget): """ self.mode = share_mode - if self.mode == ServerStatus.MODE_SHARE: + if (self.mode == ServerStatus.MODE_SHARE) or (self.mode == ServerStatus.MODE_WEBSITE): self.file_selection = file_selection self.update() @@ -207,6 +208,8 @@ class ServerStatus(QtWidgets.QWidget): if self.mode == ServerStatus.MODE_SHARE: self.url_description.setText(strings._('gui_share_url_description').format(info_image)) + elif self.mode == ServerStatus.MODE_WEBSITE: + self.url_description.setText(strings._('gui_share_url_description').format(info_image)) else: self.url_description.setText(strings._('gui_receive_url_description').format(info_image)) @@ -258,6 +261,8 @@ class ServerStatus(QtWidgets.QWidget): # Button if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0: self.server_button.hide() + elif self.mode == ServerStatus.MODE_WEBSITE and self.file_selection.get_num_files() == 0: + self.server_button.hide() else: self.server_button.show() @@ -266,6 +271,8 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: self.server_button.setText(strings._('gui_share_start_server')) + elif self.mode == ServerStatus.MODE_WEBSITE: + self.server_button.setText(strings._('gui_share_start_server')) else: self.server_button.setText(strings._('gui_receive_start_server')) self.server_button.setToolTip('') @@ -278,13 +285,27 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: self.server_button.setText(strings._('gui_share_stop_server')) + if self.mode == ServerStatus.MODE_WEBSITE: + self.server_button.setText(strings._('gui_share_stop_server')) else: self.server_button.setText(strings._('gui_receive_stop_server')) +<<<<<<< HEAD if self.common.settings.get('autostart_timer'): self.autostart_timer_container.hide() if self.common.settings.get('autostop_timer'): self.autostop_timer_container.hide() self.server_button.setToolTip(strings._('gui_stop_server_autostop_timer_tooltip').format(self.autostop_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy"))) +======= + if self.common.settings.get('shutdown_timeout'): + self.shutdown_timeout_container.hide() + if self.mode == ServerStatus.MODE_SHARE: + self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) + if self.mode == ServerStatus.MODE_WEBSITE: + self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) + else: + self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) + +>>>>>>> Add gui for website sharing and listing elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet(self.common.css['server_status_button_working']) self.server_button.setEnabled(True) @@ -411,6 +432,8 @@ class ServerStatus(QtWidgets.QWidget): """ if self.common.settings.get('public_mode'): url = 'http://{0:s}'.format(self.app.onion_host) + elif self.mode == ServerStatus.MODE_WEBSITE: + url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.slug, self.app.onion_host) else: url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) return url diff --git a/setup.py b/setup.py index f482abb6..7d5d6620 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ setup( 'onionshare_gui.mode', 'onionshare_gui.mode.share_mode', 'onionshare_gui.mode.receive_mode' + 'onionshare_gui.mode.website_mode' ], include_package_data=True, scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], diff --git a/share/locale/en.json b/share/locale/en.json index 03e7ec1a..926f6f43 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -135,6 +135,7 @@ "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.", "gui_mode_share_button": "Share Files", "gui_mode_receive_button": "Receive Files", + "gui_mode_website_button": "Publish Website", "gui_settings_receiving_label": "Receiving settings", "gui_settings_data_dir_label": "Save files to", "gui_settings_data_dir_browse_button": "Browse", From 0c6dbe4c8a08c3372fa1707a4afc2611e2bdd535 Mon Sep 17 00:00:00 2001 From: hiro Date: Tue, 23 Apr 2019 16:03:50 +0200 Subject: [PATCH 03/33] Clean ui, add strings, fix web listing logic --- onionshare/web/website_mode.py | 13 +++-- onionshare_gui/mode/history.py | 21 ++++++++ onionshare_gui/mode/website_mode/__init__.py | 57 +++----------------- share/locale/en.json | 2 + 4 files changed, 39 insertions(+), 54 deletions(-) diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 51137183..65e486e6 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -81,24 +81,27 @@ class WebsiteModeWeb(object): self.web.add_request(self.web.REQUEST_LOAD, request.path) + print(self.file_info) + + filelist = [] if self.file_info['files']: self.website_folder = os.path.dirname(self.file_info['files'][0]['filename']) + filelist = [v['basename'] for v in self.file_info['files']] elif self.file_info['dirs']: self.website_folder = self.file_info['dirs'][0]['filename'] + filelist = os.listdir(self.website_folder) else: return self.web.error404() - if any((fname == 'index.html') for fname in os.listdir(self.website_folder)): + if any((fname == 'index.html') for fname in filelist): self.web.app.static_url_path = self.website_folder self.web.app.static_folder = self.website_folder if not os.path.isfile(os.path.join(self.website_folder, page_path)): page_path = os.path.join(page_path, 'index.html') - return send_from_directory(self.website_folder, page_path) - - elif any(os.path.isfile(os.path.join(self.website_folder, i)) for i in os.listdir(self.website_folder)): + elif any(os.path.isfile(os.path.join(self.website_folder, i)) for i in filelist): filenames = [] - for i in os.listdir(self.website_folder): + for i in filelist: filenames.append(os.path.join(self.website_folder, i)) self.set_file_info(filenames) diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 1546cb68..34cd8306 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -341,6 +341,27 @@ class ReceiveHistoryItem(HistoryItem): self.label.setText(self.get_canceled_label_text(self.started)) +class VisitHistoryItem(HistoryItem): + """ + Download history item, for share mode + """ + def __init__(self, common, id, total_bytes): + super(VisitHistoryItem, self).__init__() + self.common = common + + self.id = id + self.visited = time.time() + self.visited_dt = datetime.fromtimestamp(self.visited) + + # Label + self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.started_dt.strftime("%b %d, %I:%M%p"))) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + self.setLayout(layout) + + class HistoryItemList(QtWidgets.QScrollArea): """ List of items diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index e10da8b9..e2c5bd72 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -27,7 +27,7 @@ from onionshare.web import Web from ..file_selection import FileSelection from .. import Mode -from ..history import History, ToggleHistory, DownloadHistoryItem +from ..history import History, ToggleHistory, VisitHistoryItem from ...widgets import Alert class WebsiteMode(Mode): @@ -130,7 +130,7 @@ class WebsiteMode(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.website_mode.download_count == 0 or self.web.done: + if self.web.website_mode.visit_count == 0 or self.web.done: self.server_status.stop_server() self.server_status_label.setText(strings._('close_on_timeout')) return True @@ -144,7 +144,7 @@ class WebsiteMode(Mode): Starting the server. """ # Reset web counters - self.web.website_mode.download_count = 0 + self.web.website_mode.visit_count = 0 self.web.error404_count = 0 # Hide and reset the downloads if we have previously shared @@ -201,11 +201,10 @@ class WebsiteMode(Mode): def cancel_server_custom(self): """ - Stop the compression thread on cancel + Log that the server has been cancelled """ - if self.compress_thread: - self.common.log('WebsiteMode', 'cancel_server: quitting compress thread') - self.compress_thread.quit() + self.common.log('WebsiteMode', 'cancel_server') + def handle_tor_broke_custom(self): """ @@ -217,7 +216,7 @@ class WebsiteMode(Mode): """ Handle REQUEST_LOAD event. """ - self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_download_page_loaded_message')) + self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_page_loaded_message')) def handle_request_started(self, event): """ @@ -226,52 +225,12 @@ class WebsiteMode(Mode): filesize = self.web.website_mode.download_filesize - item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) + item = VisitHistoryItem(self.common, event["data"]["id"], filesize) self.history.add(event["data"]["id"], item) self.toggle_history.update_indicator(True) self.history.in_progress_count += 1 self.history.update_in_progress() - self.system_tray.showMessage(strings._('systray_download_started_title'), strings._('systray_download_started_message')) - - def handle_request_progress(self, event): - """ - Handle REQUEST_PROGRESS event. - """ - self.history.update(event["data"]["id"], event["data"]["bytes"]) - - # Is the download complete? - if event["data"]["bytes"] == self.web.website_mode.filesize: - self.system_tray.showMessage(strings._('systray_download_completed_title'), strings._('systray_download_completed_message')) - - # Update completed and in progress labels - self.history.completed_count += 1 - self.history.in_progress_count -= 1 - self.history.update_completed() - self.history.update_in_progress() - - # Close on finish? - if self.common.settings.get('close_after_first_download'): - self.server_status.stop_server() - self.status_bar.clearMessage() - self.server_status_label.setText(strings._('closing_automatically')) - else: - if self.server_status.status == self.server_status.STATUS_STOPPED: - self.history.cancel(event["data"]["id"]) - self.history.in_progress_count = 0 - self.history.update_in_progress() - - def handle_request_canceled(self, event): - """ - Handle REQUEST_CANCELED event. - """ - self.history.cancel(event["data"]["id"]) - - # Update in progress count - self.history.in_progress_count -= 1 - self.history.update_in_progress() - self.system_tray.showMessage(strings._('systray_download_canceled_title'), strings._('systray_download_canceled_message')) - def on_reload_settings(self): """ If there were some files listed for sharing, we should be ok to re-enable diff --git a/share/locale/en.json b/share/locale/en.json index 926f6f43..dafd20d1 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -114,6 +114,7 @@ "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_save_private_key_checkbox": "Use a persistent address", "gui_share_url_description": "Anyone with this OnionShare address can download your files using the Tor Browser: ", + "gui_website_url_description": "Anyone with this OnionShare address can visit your website 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.", @@ -168,6 +169,7 @@ "gui_share_mode_autostop_timer_waiting": "Waiting to finish sending", "gui_receive_mode_no_files": "No Files Received Yet", "gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving", + "gui_visit_started": "Someone has visited your website {}", "receive_mode_upload_starting": "Upload of total size {} is starting", "days_first_letter": "d", "hours_first_letter": "h", From 357374c147071b217f1480dbda67d3dfa6a091c2 Mon Sep 17 00:00:00 2001 From: hiro Date: Tue, 23 Apr 2019 16:20:33 +0200 Subject: [PATCH 04/33] Fix merge conflicts with upstream --- onionshare/web/website_mode.py | 2 -- onionshare_gui/mode/website_mode/__init__.py | 8 ++++---- onionshare_gui/server_status.py | 12 ------------ share/locale/en.json | 2 ++ 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 65e486e6..dd7be1d5 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -81,8 +81,6 @@ class WebsiteModeWeb(object): self.web.add_request(self.web.REQUEST_LOAD, request.path) - print(self.file_info) - filelist = [] if self.file_info['files']: self.website_folder = os.path.dirname(self.file_info['files'][0]['filename']) diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index e2c5bd72..156f578e 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -75,9 +75,9 @@ class WebsiteMode(Mode): # Download history self.history = History( self.common, - QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))), - strings._('gui_no_downloads'), - strings._('gui_downloads') + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/share_icon_transparent.png'))), + strings._('gui_share_mode_no_files'), + strings._('gui_all_modes_history') ) self.history.hide() @@ -216,7 +216,7 @@ class WebsiteMode(Mode): """ Handle REQUEST_LOAD event. """ - self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_page_loaded_message')) + self.system_tray.showMessage(strings._('systray_site_loaded_title'), strings._('systray_site_loaded_message')) def handle_request_started(self, event): """ diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 755904ea..e8385e64 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -289,23 +289,11 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setText(strings._('gui_share_stop_server')) else: self.server_button.setText(strings._('gui_receive_stop_server')) -<<<<<<< HEAD if self.common.settings.get('autostart_timer'): self.autostart_timer_container.hide() if self.common.settings.get('autostop_timer'): self.autostop_timer_container.hide() self.server_button.setToolTip(strings._('gui_stop_server_autostop_timer_tooltip').format(self.autostop_timer_widget.dateTime().toString("h:mm AP, MMMM dd, yyyy"))) -======= - if self.common.settings.get('shutdown_timeout'): - self.shutdown_timeout_container.hide() - if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - if self.mode == ServerStatus.MODE_WEBSITE: - self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - else: - self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) - ->>>>>>> Add gui for website sharing and listing elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet(self.common.css['server_status_button_working']) self.server_button.setEnabled(True) diff --git a/share/locale/en.json b/share/locale/en.json index dafd20d1..de6639a6 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -147,6 +147,8 @@ "systray_menu_exit": "Quit", "systray_page_loaded_title": "Page Loaded", "systray_page_loaded_message": "OnionShare address loaded", + "systray_site_loaded_title": "Site Loaded", + "systray_site_loaded_message": "OnionShare site loaded", "systray_share_started_title": "Sharing Started", "systray_share_started_message": "Starting to send files to someone", "systray_share_completed_title": "Sharing Complete", From 8f7e52e4eee84d31b1b568ba43db23f6f5f49f1d Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 3 May 2019 19:29:58 +0200 Subject: [PATCH 05/33] Add version for Flask-HTTPAuth --- install/requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/requirements.txt b/install/requirements.txt index fff0b009..ce5464cf 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -3,7 +3,7 @@ certifi==2019.3.9 chardet==3.0.4 Click==7.0 Flask==1.0.2 -Flask-HTTPAuth +Flask-HTTPAuth==3.2.4 future==0.17.1 idna==2.8 itsdangerous==1.1.0 diff --git a/setup.py b/setup.py index 7d5d6620..347ff366 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ setup( 'onionshare_gui', 'onionshare_gui.mode', 'onionshare_gui.mode.share_mode', - 'onionshare_gui.mode.receive_mode' + 'onionshare_gui.mode.receive_mode', 'onionshare_gui.mode.website_mode' ], include_package_data=True, From abc30b315ce77a6a2dd4b8a8d24f7c478a33c7c5 Mon Sep 17 00:00:00 2001 From: hiro Date: Wed, 8 May 2019 00:04:09 +0200 Subject: [PATCH 06/33] Clean code and fix UI bugs --- install/build_rpm.sh | 2 +- onionshare/web/receive_mode.py | 3 + onionshare/web/share_mode.py | 4 ++ onionshare/web/website_mode.py | 14 ++++- onionshare_gui/mode/history.py | 34 +++++++---- onionshare_gui/mode/website_mode/__init__.py | 59 ++++++++++---------- share/locale/en.json | 3 + stdeb.cfg | 4 +- 8 files changed, 77 insertions(+), 46 deletions(-) diff --git a/install/build_rpm.sh b/install/build_rpm.sh index 0872a447..22153c6d 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, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4" +python3 setup.py bdist_rpm --requires="python3-flask, python3-flask-httpauth, python3-stem, python3-qt5, python3-crypto, python3-pysocks, nautilus-python, tor, obfs4" # install it echo "" diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index bc805445..e7f3b3ae 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -18,6 +18,9 @@ class ReceiveModeWeb(object): self.web = web + # Reset assets path + self.web.app.static_folder=self.common.get_resource_path('static') + self.can_upload = True self.upload_count = 0 self.uploads_in_progress = [] diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 560a8ba4..a0c8dc90 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -34,6 +34,10 @@ class ShareModeWeb(object): # one download at a time. self.download_in_progress = False + # Reset assets path + self.web.app.static_folder=self.common.get_resource_path('static') + + self.define_routes() def define_routes(self): diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index dd7be1d5..b9fe74e0 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -25,6 +25,9 @@ class WebsiteModeWeb(object): self.download_filesize = 0 self.visit_count = 0 + # Reset assets path + self.web.app.static_folder=self.common.get_resource_path('static') + self.users = { } self.define_routes() @@ -79,7 +82,15 @@ class WebsiteModeWeb(object): Render the onionshare website. """ - self.web.add_request(self.web.REQUEST_LOAD, request.path) + # Each download has a unique id + visit_id = self.visit_count + self.visit_count += 1 + + # Tell GUI the page has been visited + self.web.add_request(self.web.REQUEST_STARTED, page_path, { + 'id': visit_id, + 'action': 'visit' + }) filelist = [] if self.file_info['files']: @@ -102,6 +113,7 @@ class WebsiteModeWeb(object): for i in filelist: filenames.append(os.path.join(self.website_folder, i)) + self.web.app.static_folder=self.common.get_resource_path('static') self.set_file_info(filenames) r = make_response(render_template( diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 34cd8306..51b36f9a 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -347,6 +347,7 @@ class VisitHistoryItem(HistoryItem): """ def __init__(self, common, id, total_bytes): super(VisitHistoryItem, self).__init__() + self.status = HistoryItem.STATUS_STARTED self.common = common self.id = id @@ -354,13 +355,20 @@ class VisitHistoryItem(HistoryItem): self.visited_dt = datetime.fromtimestamp(self.visited) # Label - self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.started_dt.strftime("%b %d, %I:%M%p"))) + self.label = QtWidgets.QLabel(strings._('gui_visit_started').format(self.visited_dt.strftime("%b %d, %I:%M%p"))) # Layout layout = QtWidgets.QVBoxLayout() layout.addWidget(self.label) self.setLayout(layout) + def update(self): + self.label.setText(self.get_finished_label_text(self.started_dt)) + self.status = HistoryItem.STATUS_FINISHED + + def cancel(self): + self.progress_bar.setFormat(strings._('gui_canceled')) + self.status = HistoryItem.STATUS_CANCELED class HistoryItemList(QtWidgets.QScrollArea): """ @@ -425,19 +433,19 @@ class HistoryItemList(QtWidgets.QScrollArea): Reset all items, emptying the list. Override this method. """ for key, item in self.items.copy().items(): - if item.status != HistoryItem.STATUS_STARTED: - self.items_layout.removeWidget(item) - item.close() - del self.items[key] + self.items_layout.removeWidget(item) + item.close() + del self.items[key] class History(QtWidgets.QWidget): """ A history of what's happened so far in this mode. This contains an internal object full of a scrollable list of items. """ - def __init__(self, common, empty_image, empty_text, header_text): + def __init__(self, common, empty_image, empty_text, header_text, mode=''): super(History, self).__init__() self.common = common + self.mode = mode self.setMinimumWidth(350) @@ -556,12 +564,14 @@ class History(QtWidgets.QWidget): """ Update the 'in progress' widget. """ - if self.in_progress_count == 0: - 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.in_progress_label.setText(' {1:d}'.format(image, self.in_progress_count)) - self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count)) + if self.mode != 'website': + if self.in_progress_count == 0: + 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.in_progress_label.setText(' {1:d}'.format(image, self.in_progress_count)) + self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count)) class ToggleHistory(QtWidgets.QPushButton): diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 156f578e..06212b02 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -18,6 +18,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import os +import secrets +import random +import string + from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings @@ -41,9 +45,6 @@ class WebsiteMode(Mode): """ Custom initialization for ReceiveMode. """ - # Threads start out as None - self.compress_thread = None - # Create the Web object self.web = Web(self.common, True, 'website') @@ -76,8 +77,9 @@ class WebsiteMode(Mode): self.history = History( self.common, QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/share_icon_transparent.png'))), - strings._('gui_share_mode_no_files'), - strings._('gui_all_modes_history') + strings._('gui_website_mode_no_files'), + strings._('gui_all_modes_history'), + 'website' ) self.history.hide() @@ -88,8 +90,8 @@ class WebsiteMode(Mode): # Toggle history self.toggle_history = ToggleHistory( self.common, self, self.history, - QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), - QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) + QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/share_icon_toggle_selected.png')) ) # Top bar @@ -113,31 +115,27 @@ class WebsiteMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.history) + self.wrapper_layout.addWidget(self.history, stretch=1) self.setLayout(self.wrapper_layout) # Always start with focus on file selection self.file_selection.setFocus() - def get_stop_server_shutdown_timeout_text(self): + def get_stop_server_autostop_timer_text(self): """ - Return the string to put on the stop server button, if there's a shutdown timeout + Return the string to put on the stop server button, if there's an auto-stop timer """ - return strings._('gui_share_stop_server_shutdown_timeout') + return strings._('gui_share_stop_server_autostop_timer') - def timeout_finished_should_stop_server(self): + def autostop_timer_finished_should_stop_server(self): """ - The shutdown timer expired, should we stop the server? Returns a bool + The auto-stop 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.website_mode.visit_count == 0 or self.web.done: - self.server_status.stop_server() - self.server_status_label.setText(strings._('close_on_timeout')) - return True - # A download is probably still running - hold off on stopping the share - else: - self.server_status_label.setText(strings._('timeout_download_still_running')) - return False + + self.server_status.stop_server() + self.server_status_label.setText(strings._('close_on_autostop_timer')) + return True + def start_server_custom(self): """ @@ -194,9 +192,7 @@ class WebsiteMode(Mode): """ self.filesize_warning.hide() - self.history.in_progress_count = 0 self.history.completed_count = 0 - self.history.update_in_progress() self.file_selection.file_list.adjustSize() def cancel_server_custom(self): @@ -222,14 +218,17 @@ class WebsiteMode(Mode): """ Handle REQUEST_STARTED event. """ + if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ): + filesize = self.web.website_mode.download_filesize + item = VisitHistoryItem(self.common, event["data"]["id"], filesize) - filesize = self.web.website_mode.download_filesize + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + self.history.completed_count += 1 + self.history.update_completed() + + self.system_tray.showMessage(strings._('systray_website_started_title'), strings._('systray_website_started_message')) - item = VisitHistoryItem(self.common, event["data"]["id"], filesize) - self.history.add(event["data"]["id"], item) - self.toggle_history.update_indicator(True) - self.history.in_progress_count += 1 - self.history.update_in_progress() def on_reload_settings(self): """ diff --git a/share/locale/en.json b/share/locale/en.json index de6639a6..0c26a9d5 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -157,6 +157,8 @@ "systray_share_canceled_message": "Someone canceled receiving your files", "systray_receive_started_title": "Receiving Started", "systray_receive_started_message": "Someone is sending files to you", + "systray_website_started_title": "Starting sharing website", + "systray_website_started_message": "Someone is visiting your website", "gui_all_modes_history": "History", "gui_all_modes_clear_history": "Clear All", "gui_all_modes_transfer_started": "Started {}", @@ -169,6 +171,7 @@ "gui_all_modes_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_share_mode_no_files": "No Files Sent Yet", "gui_share_mode_autostop_timer_waiting": "Waiting to finish sending", + "gui_website_mode_no_files": "No Website Shared Yet", "gui_receive_mode_no_files": "No Files Received Yet", "gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving", "gui_visit_started": "Someone has visited your website {}", diff --git a/stdeb.cfg b/stdeb.cfg index 0adbac43..451520af 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: onionshare -Depends3: python3, python3-flask, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python-nautilus, tor, obfs4proxy -Build-Depends: python3, python3-pytest, python3-flask, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, python-nautilus, tor, obfs4proxy +Depends3: python3, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python-nautilus, tor, obfs4proxy +Build-Depends: python3, python3-pytest, python3-flask, python3-flask-httpauth, python3-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, python-nautilus, tor, obfs4proxy Suite: cosmic X-Python3-Version: >= 3.5.3 From 65a7a1790c54f3986d7d6683e06dc1092fff2e6c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 10 May 2019 13:43:46 -0700 Subject: [PATCH 07/33] Starting to refactor website sharing so set_file_info builds a dict of all files, and path_logic will display any arbitrary file, or directory listing if no index.html is present --- onionshare/web/website_mode.py | 167 +++++++++++++++++---------------- share/templates/listing.html | 25 +++-- 2 files changed, 102 insertions(+), 90 deletions(-) diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index b9fe74e0..e337c368 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -19,10 +19,8 @@ class WebsiteModeWeb(object): self.web = web self.auth = HTTPBasicAuth() - # Information about the file to be shared - self.file_info = [] - self.website_folder = '' - self.download_filesize = 0 + # Dictionary mapping file paths to filenames on disk + self.files = {} self.visit_count = 0 # Reset assets path @@ -55,10 +53,6 @@ class WebsiteModeWeb(object): return _check_login() - @self.web.app.route('/download/') - def path_download(page_path): - return path_download(page_path) - @self.web.app.route('/') def path_public(page_path): return path_logic(page_path) @@ -67,17 +61,7 @@ class WebsiteModeWeb(object): def index_public(): return path_logic('') - def path_download(file_path=''): - """ - Render the download links. - """ - self.web.add_request(self.web.REQUEST_LOAD, request.path) - if not os.path.isfile(os.path.join(self.website_folder, file_path)): - return self.web.error404() - - return send_from_directory(self.website_folder, file_path) - - def path_logic(page_path=''): + def path_logic(path=''): """ Render the onionshare website. """ @@ -87,85 +71,106 @@ class WebsiteModeWeb(object): self.visit_count += 1 # Tell GUI the page has been visited - self.web.add_request(self.web.REQUEST_STARTED, page_path, { + self.web.add_request(self.web.REQUEST_STARTED, path, { 'id': visit_id, 'action': 'visit' }) - filelist = [] - if self.file_info['files']: - self.website_folder = os.path.dirname(self.file_info['files'][0]['filename']) - filelist = [v['basename'] for v in self.file_info['files']] - elif self.file_info['dirs']: - self.website_folder = self.file_info['dirs'][0]['filename'] - filelist = os.listdir(self.website_folder) - else: - return self.web.error404() - - if any((fname == 'index.html') for fname in filelist): - self.web.app.static_url_path = self.website_folder - self.web.app.static_folder = self.website_folder - if not os.path.isfile(os.path.join(self.website_folder, page_path)): - page_path = os.path.join(page_path, 'index.html') - return send_from_directory(self.website_folder, page_path) - elif any(os.path.isfile(os.path.join(self.website_folder, i)) for i in filelist): - filenames = [] - for i in filelist: - filenames.append(os.path.join(self.website_folder, i)) - - self.web.app.static_folder=self.common.get_resource_path('static') - self.set_file_info(filenames) - - r = make_response(render_template( - 'listing.html', - file_info=self.file_info, - filesize=self.download_filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize))) - - return self.web.add_security_headers(r) - + # Removing trailing slashes, because self.files doesn't have them + path = path.rstrip('/') + + if path in self.files: + filesystem_path = self.files[path] + + # If it's a directory + if os.path.isdir(filesystem_path): + # Is there an index.html? + index_path = os.path.join(path, 'index.html') + if index_path in self.files: + # Render it + dirname = os.path.dirname(self.files[index_path]) + basename = os.path.basename(self.files[index_path]) + return send_from_directory(dirname, basename) + + else: + # Otherwise, render directory listing + filenames = os.listdir(filesystem_path) + filenames.sort() + + files = [] + dirs = [] + + for filename in filenames: + this_filesystem_path = os.path.join(filesystem_path, filename) + is_dir = os.path.isdir(this_filesystem_path) + + if is_dir: + dirs.append({ + 'basename': filename + }) + else: + size = os.path.getsize(this_filesystem_path) + size_human = self.common.human_readable_filesize(size) + files.append({ + 'basename': filename, + 'size_human': size_human + }) + + r = make_response(render_template('listing.html', + path=path, + files=files, + dirs=dirs)) + return self.web.add_security_headers(r) + + # If it's a file + elif os.path.isfile(filesystem_path): + dirname = os.path.dirname(filesystem_path) + basename = os.path.basename(filesystem_path) + return send_from_directory(dirname, basename) + + # If it's not a directory or file, throw a 404 + else: + return self.web.error404() else: + # If the path isn't found, throw a 404 return self.web.error404() - def set_file_info(self, filenames, processed_size_callback=None): + def set_file_info(self, filenames): """ - 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. + Build a data structure that describes the list of files that make up + the static website. """ self.common.log("WebsiteModeWeb", "set_file_info") - self.web.cancel_compression = True - self.cleanup_filenames = [] + # This is a dictionary that maps HTTP routes to filenames on disk + self.files = {} - # build file info list - self.file_info = {'files': [], 'dirs': []} + # If there's just one folder, replace filenames with a list of files inside that folder + if len(filenames) == 1 and os.path.isdir(filenames[0]): + filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])] + + # Loop through the files for filename in filenames: - info = { - 'filename': filename, - 'basename': os.path.basename(filename.rstrip('/')) - } + basename = os.path.basename(filename.rstrip('/')) + + # If it's a filename, add it 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.files[basename] = filename - self.download_filesize += info['size'] + # If it's a directory, add it recursively + elif os.path.isdir(filename): + for root, _, nested_filenames in os.walk(filename): + # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder", + # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar". + # The normalized_root should be "some_folder/foobar" + normalized_root = os.path.join(basename, root.lstrip(filename)).rstrip('/') - self.file_info['files'] = sorted(self.file_info['files'], key=lambda k: k['basename']) - self.file_info['dirs'] = sorted(self.file_info['dirs'], key=lambda k: k['basename']) - - # Check if there's only 1 file and no folders - if len(self.file_info['files']) == 1 and len(self.file_info['dirs']) == 0: - self.download_filename = self.file_info['files'][0]['filename'] - self.download_filesize = self.file_info['files'][0]['size'] - - self.download_filesize = os.path.getsize(self.download_filename) + # Add the dir itself + self.files[normalized_root] = filename + # Add the files in this dir + for nested_filename in nested_filenames: + self.files[os.path.join(normalized_root, nested_filename)] = os.path.join(root, nested_filename) return True diff --git a/share/templates/listing.html b/share/templates/listing.html index a514e5d2..8bb4062d 100644 --- a/share/templates/listing.html +++ b/share/templates/listing.html @@ -8,11 +8,6 @@
-
-
    -
  • Total size: {{ filesize_human }}
  • -
-

OnionShare

@@ -24,17 +19,29 @@ - {% for info in file_info.files %} + {% for info in dirs %} + + + + + {{ info.basename }} + + + + + {% endfor %} + + {% for info in files %} - {{ info.basename }} + + {{ info.basename }} + {{ info.size_human }} - download {% endfor %} - From 818f2bac2c792f5c99c6442c1a5dc4126be64915 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 10 May 2019 14:04:13 -0700 Subject: [PATCH 08/33] Make it so directory listings work, including root directory listing --- onionshare/web/website_mode.py | 86 ++++++++++++++++++++++------------ share/templates/listing.html | 6 +-- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index e337c368..99b3f0f2 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -76,9 +76,6 @@ class WebsiteModeWeb(object): 'action': 'visit' }) - # Removing trailing slashes, because self.files doesn't have them - path = path.rstrip('/') - if path in self.files: filesystem_path = self.files[path] @@ -96,31 +93,7 @@ class WebsiteModeWeb(object): # Otherwise, render directory listing filenames = os.listdir(filesystem_path) filenames.sort() - - files = [] - dirs = [] - - for filename in filenames: - this_filesystem_path = os.path.join(filesystem_path, filename) - is_dir = os.path.isdir(this_filesystem_path) - - if is_dir: - dirs.append({ - 'basename': filename - }) - else: - size = os.path.getsize(this_filesystem_path) - size_human = self.common.human_readable_filesize(size) - files.append({ - 'basename': filename, - 'size_human': size_human - }) - - r = make_response(render_template('listing.html', - path=path, - files=files, - dirs=dirs)) - return self.web.add_security_headers(r) + return self.directory_listing(path, filenames, filesystem_path) # If it's a file elif os.path.isfile(filesystem_path): @@ -132,9 +105,54 @@ class WebsiteModeWeb(object): else: return self.web.error404() else: - # If the path isn't found, throw a 404 - return self.web.error404() + # Special case loading / + if path == '': + index_path = 'index.html' + if index_path in self.files: + # Render it + dirname = os.path.dirname(self.files[index_path]) + basename = os.path.basename(self.files[index_path]) + return send_from_directory(dirname, basename) + else: + # Root directory listing + filenames = list(self.root_files) + filenames.sort() + return self.directory_listing(path, filenames) + else: + # If the path isn't found, throw a 404 + return self.web.error404() + + def directory_listing(self, path, filenames, filesystem_path=None): + # If filesystem_path is None, this is the root directory listing + files = [] + dirs = [] + + for filename in filenames: + if filesystem_path: + this_filesystem_path = os.path.join(filesystem_path, filename) + else: + this_filesystem_path = self.files[filename] + + is_dir = os.path.isdir(this_filesystem_path) + + if is_dir: + dirs.append({ + 'basename': filename + }) + else: + size = os.path.getsize(this_filesystem_path) + size_human = self.common.human_readable_filesize(size) + files.append({ + 'basename': filename, + 'size_human': size_human + }) + + r = make_response(render_template('listing.html', + path=path, + files=files, + dirs=dirs)) + return self.web.add_security_headers(r) def set_file_info(self, filenames): """ @@ -146,6 +164,9 @@ class WebsiteModeWeb(object): # This is a dictionary that maps HTTP routes to filenames on disk self.files = {} + # This is only the root files and dirs, as opposed to all of them + self.root_files = {} + # If there's just one folder, replace filenames with a list of files inside that folder if len(filenames) == 1 and os.path.isdir(filenames[0]): filenames = [os.path.join(filenames[0], x) for x in os.listdir(filenames[0])] @@ -157,9 +178,12 @@ class WebsiteModeWeb(object): # If it's a filename, add it if os.path.isfile(filename): self.files[basename] = filename + self.root_files[basename] = filename # If it's a directory, add it recursively elif os.path.isdir(filename): + self.root_files[basename + '/'] = filename + for root, _, nested_filenames in os.walk(filename): # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder", # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar". @@ -167,7 +191,7 @@ class WebsiteModeWeb(object): normalized_root = os.path.join(basename, root.lstrip(filename)).rstrip('/') # Add the dir itself - self.files[normalized_root] = filename + self.files[normalized_root + '/'] = filename # Add the files in this dir for nested_filename in nested_filenames: diff --git a/share/templates/listing.html b/share/templates/listing.html index 8bb4062d..8883eea9 100644 --- a/share/templates/listing.html +++ b/share/templates/listing.html @@ -23,11 +23,11 @@ - + {{ info.basename }} - + — {% endfor %} @@ -35,7 +35,7 @@ - + {{ info.basename }} From ab930011ad42e0eed074f1a23be6bb3b506d719e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 10 May 2019 14:52:07 -0700 Subject: [PATCH 09/33] Fix bugs in how self.file was building the dictionary, so now browsing works --- onionshare/web/website_mode.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 99b3f0f2..39f41b3e 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -53,13 +53,10 @@ class WebsiteModeWeb(object): return _check_login() - @self.web.app.route('/') - def path_public(page_path): - return path_logic(page_path) - - @self.web.app.route("/") - def index_public(): - return path_logic('') + @self.web.app.route('/', defaults={'path': ''}) + @self.web.app.route('/') + def path_public(path): + return path_logic(path) def path_logic(path=''): """ @@ -91,7 +88,12 @@ class WebsiteModeWeb(object): else: # Otherwise, render directory listing - filenames = os.listdir(filesystem_path) + filenames = [] + for filename in os.listdir(filesystem_path): + if os.path.isdir(os.path.join(filesystem_path, filename)): + filenames.append(filename + '/') + else: + filenames.append(filename) filenames.sort() return self.directory_listing(path, filenames, filesystem_path) @@ -188,10 +190,10 @@ class WebsiteModeWeb(object): # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder", # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar". # The normalized_root should be "some_folder/foobar" - normalized_root = os.path.join(basename, root.lstrip(filename)).rstrip('/') + normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/') # Add the dir itself - self.files[normalized_root + '/'] = filename + self.files[normalized_root + '/'] = root # Add the files in this dir for nested_filename in nested_filenames: From 915ff0f4f3725695eae230165a3670fdc827fb22 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 10 May 2019 14:57:41 -0700 Subject: [PATCH 10/33] Remove references to self.web.website_mode.download_filesize because that variable no longer exists --- onionshare_gui/mode/website_mode/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 06212b02..9018f5cb 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -167,11 +167,6 @@ class WebsiteMode(Mode): warning, if applicable. """ - # Warn about sending large files over Tor - if self.web.website_mode.download_filesize >= 157286400: # 150mb - self.filesize_warning.setText(strings._("large_filesize")) - self.filesize_warning.show() - if self.web.website_mode.set_file_info(self.filenames): self.success.emit() else: @@ -219,8 +214,7 @@ class WebsiteMode(Mode): Handle REQUEST_STARTED event. """ if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ): - filesize = self.web.website_mode.download_filesize - item = VisitHistoryItem(self.common, event["data"]["id"], filesize) + item = VisitHistoryItem(self.common, event["data"]["id"], 0) self.history.add(event["data"]["id"], item) self.toggle_history.update_indicator(True) From 2a50bbc3bc3324d9aca3b1ac3e57ae8fd9eee08c Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 17:59:20 -0700 Subject: [PATCH 11/33] Move HTTP basic auth logic from WebsiteMode to Web, so it applies to all modes --- onionshare/__init__.py | 9 +++------ onionshare/web/base_share_mode.py | 19 +++++++++++++++++++ onionshare/web/web.py | 21 ++++++++++++++++++++- onionshare/web/website_mode.py | 22 ---------------------- 4 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 onionshare/web/base_share_mode.py diff --git a/onionshare/__init__.py b/onionshare/__init__.py index a96f2fca..5df59975 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -50,8 +50,8 @@ def main(cwd=None): parser.add_argument('--auto-stop-timer', metavar='', dest='autostop_timer', default=0, help="Stop sharing after a given amount of seconds") parser.add_argument('--connect-timeout', metavar='', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)") parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)") - parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them") - parser.add_argument('--website', action='store_true', dest='website', help=strings._("help_website")) + parser.add_argument('--receive', action='store_true', dest='receive', help="Receive files instead of sending them") + parser.add_argument('--website', action='store_true', dest='website', help="Host a static website as an onion service") parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)") parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk") parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share") @@ -174,7 +174,6 @@ def main(cwd=None): if mode == 'website': # Prepare files to share - print(strings._("preparing_website")) try: web.website_mode.set_file_info(filenames) except OSError as e: @@ -219,10 +218,8 @@ def main(cwd=None): # Build the URL if common.settings.get('public_mode'): url = 'http://{0:s}'.format(app.onion_host) - elif mode == 'website': - url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) else: - url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) + url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) print('') if autostart_timer > 0: diff --git a/onionshare/web/base_share_mode.py b/onionshare/web/base_share_mode.py new file mode 100644 index 00000000..64cf3dce --- /dev/null +++ b/onionshare/web/base_share_mode.py @@ -0,0 +1,19 @@ +import os +import sys +import tempfile +import zipfile +import mimetypes +import gzip +from flask import Response, request, render_template, make_response + +from .. import strings + + +class ShareModeWeb(object): + """ + This is the base class that includes shared functionality between share mode + and website mode + """ + def __init__(self, common, web): + self.common = common + self.web = web diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 0ba8c6b3..83c441d7 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -10,6 +10,7 @@ from urllib.request import urlopen import flask from flask import Flask, request, render_template, abort, make_response, __version__ as flask_version +from flask_httpauth import HTTPBasicAuth from .. import strings @@ -53,6 +54,7 @@ class Web(object): static_folder=self.common.get_resource_path('static'), template_folder=self.common.get_resource_path('templates')) self.app.secret_key = self.common.random_string(8) + self.auth = HTTPBasicAuth() # Verbose mode? if self.common.verbose: @@ -119,8 +121,25 @@ class Web(object): def define_common_routes(self): """ - Common web app routes between sending, receiving and website modes. + Common web app routes between all modes. """ + + @self.auth.get_password + def get_pw(username): + if username == 'onionshare': + return self.slug + else: + return None + + @self.app.before_request + def conditional_auth_check(): + if not self.common.settings.get('public_mode'): + @self.auth.login_required + def _check_login(): + return None + + return _check_login() + @self.app.errorhandler(404) def page_not_found(e): """ diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 39f41b3e..354c5aa7 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -3,7 +3,6 @@ import sys import tempfile import mimetypes from flask import Response, request, render_template, make_response, send_from_directory -from flask_httpauth import HTTPBasicAuth from .. import strings @@ -17,7 +16,6 @@ class WebsiteModeWeb(object): self.common.log('WebsiteModeWeb', '__init__') self.web = web - self.auth = HTTPBasicAuth() # Dictionary mapping file paths to filenames on disk self.files = {} @@ -26,8 +24,6 @@ class WebsiteModeWeb(object): # Reset assets path self.web.app.static_folder=self.common.get_resource_path('static') - self.users = { } - self.define_routes() def define_routes(self): @@ -35,24 +31,6 @@ class WebsiteModeWeb(object): The web app routes for sharing a website """ - @self.auth.get_password - def get_pw(username): - self.users['onionshare'] = self.web.slug - - if username in self.users: - return self.users.get(username) - else: - return None - - @self.web.app.before_request - def conditional_auth_check(): - if not self.common.settings.get('public_mode'): - @self.auth.login_required - def _check_login(): - return None - - return _check_login() - @self.web.app.route('/', defaults={'path': ''}) @self.web.app.route('/') def path_public(path): From 79b87c3e30480708af6d824a19430d24d2693dd4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 19:04:50 -0700 Subject: [PATCH 12/33] Add an error 401 handler, and make it start counting invalid password guesses instead of 404 errors for rate limiting --- onionshare/web/web.py | 51 +++++++++++--------- onionshare_gui/mode/receive_mode/__init__.py | 2 +- onionshare_gui/mode/share_mode/__init__.py | 2 +- onionshare_gui/mode/website_mode/__init__.py | 2 +- onionshare_gui/onionshare_gui.py | 5 +- share/locale/en.json | 3 +- share/templates/401.html | 19 ++++++++ 7 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 share/templates/401.html diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 83c441d7..14e2f9b3 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -44,6 +44,7 @@ class Web(object): REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_CANCELED = 9 REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 + REQUEST_INVALID_SLUG = 11 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -55,6 +56,7 @@ class Web(object): template_folder=self.common.get_resource_path('templates')) self.app.secret_key = self.common.random_string(8) self.auth = HTTPBasicAuth() + self.auth.error_handler(self.error401) # Verbose mode? if self.common.verbose: @@ -95,7 +97,8 @@ class Web(object): self.q = queue.Queue() self.slug = None - self.error404_count = 0 + + self.reset_invalid_slugs() self.done = False @@ -141,10 +144,7 @@ class Web(object): return _check_login() @self.app.errorhandler(404) - def page_not_found(e): - """ - 404 error page. - """ + def not_found(e): return self.error404() @self.app.route("//shutdown") @@ -164,18 +164,26 @@ class Web(object): r = make_response(render_template('receive_noscript_xss.html')) return self.add_security_headers(r) + def error401(self): + auth = request.authorization + if auth: + if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_slugs: + print('Invalid password guess: {}'.format(auth['password'])) + self.add_request(Web.REQUEST_INVALID_SLUG, data=auth['password']) + + self.invalid_slugs.append(auth['password']) + self.invalid_slugs_count += 1 + + if self.invalid_slugs_count == 20: + self.add_request(Web.REQUEST_RATE_LIMIT) + self.force_shutdown() + print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") + + r = make_response(render_template('401.html'), 401) + return self.add_security_headers(r) + 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("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 recipient a new address to share.") - r = make_response(render_template('404.html'), 404) return self.add_security_headers(r) @@ -198,7 +206,7 @@ class Web(object): return True return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - def add_request(self, request_type, path, data=None): + def add_request(self, request_type, path=None, data=None): """ Add a request to the queue, to communicate with the GUI. """ @@ -226,18 +234,15 @@ class Web(object): 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 reset_invalid_slugs(self): + self.invalid_slugs_count = 0 + self.invalid_slugs = [] + def force_shutdown(self): """ Stop the flask web server, from the context of the flask app. diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 4c0b49ba..d6b1c0f3 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -113,7 +113,7 @@ class ReceiveMode(Mode): """ # Reset web counters self.web.receive_mode.upload_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the uploads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 1ee40ca3..f51fd0bb 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -147,7 +147,7 @@ class ShareMode(Mode): """ # Reset web counters self.web.share_mode.download_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 9018f5cb..c6009ebe 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -143,7 +143,7 @@ class WebsiteMode(Mode): """ # Reset web counters self.web.website_mode.visit_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 9fdf9395..4945ca7e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -472,7 +472,10 @@ class OnionShareGui(QtWidgets.QMainWindow): if event["type"] == Web.REQUEST_OTHER: if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded'), event["path"])) + self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"])) + + if event["type"] == Web.REQUEST_INVALID_SLUG: + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_slugs_count, strings._('invalid_slug_guess'), event["data"])) mode.timer_callback() diff --git a/share/locale/en.json b/share/locale/en.json index 7183e734..6dea9860 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -3,6 +3,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": "Address loaded", + "invalid_slug_guess": "Invalid password guess", "close_on_autostop_timer": "Stopped because auto-stop timer ran out", "closing_automatically": "Stopped because transfer is complete", "large_filesize": "Warning: Sending a large share could take hours", @@ -34,7 +35,7 @@ "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": "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 recipient a new address to share.", + "error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", "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.", diff --git a/share/templates/401.html b/share/templates/401.html new file mode 100644 index 00000000..9d3989a3 --- /dev/null +++ b/share/templates/401.html @@ -0,0 +1,19 @@ + + + + + OnionShare: 401 Unauthorized Access + + + + + +
+
+

+

401 Unauthorized Access

+
+
+ + + From 7fe733e9fc8aca764f2a20c970620bcf0354f54f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 19:11:24 -0700 Subject: [PATCH 13/33] Simplify share and receive mode so they no longer need to worry about slug_candidates --- onionshare/web/receive_mode.py | 48 ++++------------------------------ onionshare/web/share_mode.py | 24 ++--------------- 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index e7f3b3ae..99451fc3 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -31,7 +31,8 @@ class ReceiveModeWeb(object): """ The web app routes for receiving files """ - def index_logic(): + @self.web.app.route("/") + def index(): self.web.add_request(self.web.REQUEST_LOAD, request.path) if self.common.settings.get('public_mode'): @@ -44,23 +45,8 @@ class ReceiveModeWeb(object): upload_action=upload_action)) return self.web.add_security_headers(r) - @self.web.app.route("/") - def index(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return index_logic() - - @self.web.app.route("/") - def index_public(): - if not self.can_upload: - return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return index_logic() - - - def upload_logic(slug_candidate='', ajax=False): + @self.web.app.route("/upload", methods=['POST']) + def upload(ajax=False): """ Handle the upload files POST request, though at this point, the files have already been uploaded and saved to their correct locations. @@ -141,35 +127,11 @@ class ReceiveModeWeb(object): r = make_response(render_template('thankyou.html')) return self.web.add_security_headers(r) - @self.web.app.route("//upload", methods=['POST']) - def upload(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate) - - @self.web.app.route("/upload", methods=['POST']) - def upload_public(): - if not self.can_upload: - return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return upload_logic() - - @self.web.app.route("//upload-ajax", methods=['POST']) - def upload_ajax(slug_candidate): - if not self.can_upload: - return self.web.error403() - self.web.check_slug_candidate(slug_candidate) - return upload_logic(slug_candidate, ajax=True) - @self.web.app.route("/upload-ajax", methods=['POST']) def upload_ajax_public(): if not self.can_upload: return self.web.error403() - if not self.common.settings.get('public_mode'): - return self.web.error404() - return upload_logic(ajax=True) + return upload(ajax=True) class ReceiveModeWSGIMiddleware(object): diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index a0c8dc90..d5d3280f 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -44,18 +44,8 @@ class ShareModeWeb(object): """ The web app routes for sharing files """ - @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.common.settings.get('public_mode'): - return self.web.error404() - return index_logic() - - def index_logic(slug_candidate=''): + def index(): """ Render the template for the onionshare landing page. """ @@ -94,18 +84,8 @@ class ShareModeWeb(object): is_zipped=self.is_zipped)) return self.web.add_security_headers(r) - @self.web.app.route("//download") - def download(slug_candidate): - self.web.check_slug_candidate(slug_candidate) - return download_logic() - @self.web.app.route("/download") - def download_public(): - if not self.common.settings.get('public_mode'): - return self.web.error404() - return download_logic() - - def download_logic(slug_candidate=''): + def download(): """ Download the zip file. """ From 29abfd8f876e2474a119aa13d957a6a84ebbd3c3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 19:14:04 -0700 Subject: [PATCH 14/33] This should be an elif, not an if, because otherwise the share mode stop button says "Stop Receive Mode" --- onionshare_gui/server_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index e8385e64..3b3a7794 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -285,7 +285,7 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: self.server_button.setText(strings._('gui_share_stop_server')) - if self.mode == ServerStatus.MODE_WEBSITE: + elif self.mode == ServerStatus.MODE_WEBSITE: self.server_button.setText(strings._('gui_share_stop_server')) else: self.server_button.setText(strings._('gui_receive_stop_server')) From b667fcc4f8ca6d78ffda0e3b2faa9b606b733825 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 19:22:03 -0700 Subject: [PATCH 15/33] Fix onionshare URLs non-public mode is always http basic auth --- onionshare/__init__.py | 17 +++++++---------- onionshare_gui/server_status.py | 4 +--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 5df59975..765a083e 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -127,6 +127,13 @@ def main(cwd=None): app = OnionShare(common, onion, local_only, autostop_timer) app.set_stealth(stealth) app.choose_port() + + # Build the URL + if common.settings.get('public_mode'): + url = 'http://{0:s}'.format(app.onion_host) + else: + url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) + # Delay the startup if a startup timer was set if autostart_timer > 0: # Can't set a schedule that is later than the auto-stop timer @@ -135,10 +142,6 @@ def main(cwd=None): sys.exit() app.start_onion_service(False, True) - if common.settings.get('public_mode'): - url = 'http://{0:s}'.format(app.onion_host) - else: - url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) schedule = datetime.now() + timedelta(seconds=autostart_timer) if mode == 'receive': print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir'))) @@ -215,12 +218,6 @@ def main(cwd=None): common.settings.set('slug', web.slug) common.settings.save() - # Build the URL - if common.settings.get('public_mode'): - url = 'http://{0:s}'.format(app.onion_host) - else: - url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) - print('') if autostart_timer > 0: print("Server started") diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 3b3a7794..b23a89a8 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -420,8 +420,6 @@ class ServerStatus(QtWidgets.QWidget): """ if self.common.settings.get('public_mode'): url = 'http://{0:s}'.format(self.app.onion_host) - elif self.mode == ServerStatus.MODE_WEBSITE: - url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.slug, self.app.onion_host) else: - url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) + url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.slug, self.app.onion_host) return url From 322921142299604a6e5ad41a1252c2eb5493d4d3 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 20:01:14 -0700 Subject: [PATCH 16/33] Make shutdown slug use http basic auth also, so the shutdown command doesnt fail with a 401 --- onionshare/web/web.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 14e2f9b3..f8f8f6ca 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -5,6 +5,7 @@ import queue import socket import sys import tempfile +import requests from distutils.version import LooseVersion as Version from urllib.request import urlopen @@ -131,6 +132,8 @@ class Web(object): def get_pw(username): if username == 'onionshare': return self.slug + elif username == 'shutdown': + return self.shutdown_slug else: return None @@ -293,14 +296,8 @@ class Web(object): # Reset any slug that was in use self.slug = None - # To stop flask, load http://127.0.0.1://shutdown + # To stop flask, load http://shutdown:[shutdown_slug]@127.0.0.1/[shutdown_slug]/shutdown + # (We're putting the shutdown_slug in the path as well to make routing simpler) 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 + requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_slug), + auth=requests.auth.HTTPBasicAuth('shutdown', self.shutdown_slug)) From fe64a5a059642aae13a505bdbba8808e199d8d43 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 22:02:43 -0700 Subject: [PATCH 17/33] Make the shutdown get request use the onionshare user for basic auth --- onionshare/web/web.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index f8f8f6ca..43316b43 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -132,8 +132,6 @@ class Web(object): def get_pw(username): if username == 'onionshare': return self.slug - elif username == 'shutdown': - return self.shutdown_slug else: return None @@ -155,9 +153,10 @@ class Web(object): """ Stop the flask web server, from the context of an http request. """ - self.check_shutdown_slug_candidate(slug_candidate) - self.force_shutdown() - return "" + if slug_candidate == self.shutdown_slug: + self.force_shutdown() + return "" + abort(404) @self.app.route("/noscript-xss-instructions") def noscript_xss_instructions(): @@ -237,11 +236,6 @@ class Web(object): log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - 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 reset_invalid_slugs(self): self.invalid_slugs_count = 0 self.invalid_slugs = [] @@ -293,11 +287,11 @@ class Web(object): # Let the mode know that the user stopped the server self.stop_q.put(True) - # Reset any slug that was in use - self.slug = None - # To stop flask, load http://shutdown:[shutdown_slug]@127.0.0.1/[shutdown_slug]/shutdown # (We're putting the shutdown_slug in the path as well to make routing simpler) if self.running: requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_slug), - auth=requests.auth.HTTPBasicAuth('shutdown', self.shutdown_slug)) + auth=requests.auth.HTTPBasicAuth('onionshare', self.slug)) + + # Reset any slug that was in use + self.slug = None From 7d89f80f2079b331221528124ce0bd9cdd8a891e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 22:18:49 -0700 Subject: [PATCH 18/33] Rename "slug" to "password" --- onionshare/__init__.py | 16 ++--- onionshare/common.py | 2 +- onionshare/settings.py | 2 +- onionshare/web/receive_mode.py | 8 +-- onionshare/web/share_mode.py | 4 +- onionshare/web/web.py | 62 ++++++++++---------- onionshare_gui/mode/__init__.py | 4 +- onionshare_gui/mode/receive_mode/__init__.py | 2 +- onionshare_gui/mode/share_mode/__init__.py | 2 +- onionshare_gui/mode/website_mode/__init__.py | 2 +- onionshare_gui/onionshare_gui.py | 6 +- onionshare_gui/server_status.py | 6 +- onionshare_gui/settings_dialog.py | 6 +- onionshare_gui/threads.py | 8 +-- share/locale/en.json | 2 +- 15 files changed, 66 insertions(+), 66 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 765a083e..e2ff50a2 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -121,9 +121,9 @@ def main(cwd=None): try: common.settings.load() if not common.settings.get('public_mode'): - web.generate_slug(common.settings.get('slug')) + web.generate_password(common.settings.get('password')) else: - web.slug = None + web.password = None app = OnionShare(common, onion, local_only, autostop_timer) app.set_stealth(stealth) app.choose_port() @@ -132,7 +132,7 @@ def main(cwd=None): if common.settings.get('public_mode'): url = 'http://{0:s}'.format(app.onion_host) else: - url = 'http://onionshare:{0:s}@{1:s}'.format(web.slug, app.onion_host) + url = 'http://onionshare:{0:s}@{1:s}'.format(web.password, app.onion_host) # Delay the startup if a startup timer was set if autostart_timer > 0: @@ -200,22 +200,22 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.slug)) + t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.password)) t.daemon = True t.start() try: # Trap Ctrl-C - # Wait for web.generate_slug() to finish running + # Wait for web.generate_password() to finish running time.sleep(0.2) # start auto-stop timer thread if app.autostop_timer > 0: app.autostop_timer_thread.start() - # Save the web slug if we are using a persistent private key + # Save the web password if we are using a persistent private key if common.settings.get('save_private_key'): - if not common.settings.get('slug'): - common.settings.set('slug', web.slug) + if not common.settings.get('password'): + common.settings.set('password', web.password) common.settings.save() print('') diff --git a/onionshare/common.py b/onionshare/common.py index 325f11d4..9b871f04 100644 --- a/onionshare/common.py +++ b/onionshare/common.py @@ -143,7 +143,7 @@ class Common(object): os.makedirs(onionshare_data_dir, 0o700, True) return onionshare_data_dir - def build_slug(self): + def build_password(self): """ Returns a random string made from two words from the wordlist, such as "deter-trig". """ diff --git a/onionshare/settings.py b/onionshare/settings.py index 16b64a05..762c6dc2 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -111,7 +111,7 @@ class Settings(object): 'save_private_key': False, 'private_key': '', 'public_mode': False, - 'slug': '', + 'password': '', 'hidservauth_string': '', 'data_dir': self.build_default_data_dir(), 'locale': None # this gets defined in fill_in_defaults() diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 99451fc3..af146cb0 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -38,7 +38,7 @@ class ReceiveModeWeb(object): if self.common.settings.get('public_mode'): upload_action = '/upload' else: - upload_action = '/{}/upload'.format(self.web.slug) + upload_action = '/{}/upload'.format(self.web.password) r = make_response(render_template( 'receive.html', @@ -87,7 +87,7 @@ class ReceiveModeWeb(object): if self.common.settings.get('public_mode'): return redirect('/') else: - return redirect('/{}'.format(slug_candidate)) + return redirect('/{}'.format(password_candidate)) # Note that flash strings are in English, and not translated, on purpose, # to avoid leaking the locale of the OnionShare user @@ -117,7 +117,7 @@ class ReceiveModeWeb(object): if self.common.settings.get('public_mode'): path = '/' else: - path = '/{}'.format(slug_candidate) + path = '/{}'.format(password_candidate) return redirect('{}'.format(path)) else: if ajax: @@ -238,7 +238,7 @@ class ReceiveModeRequest(Request): if self.path == '/upload' or self.path == '/upload-ajax': self.upload_request = True else: - if self.path == '/{}/upload'.format(self.web.slug) or self.path == '/{}/upload-ajax'.format(self.web.slug): + if self.path == '/{}/upload'.format(self.web.password) or self.path == '/{}/upload-ajax'.format(self.web.password): self.upload_request = True if self.upload_request: diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index d5d3280f..bede4a36 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -64,10 +64,10 @@ class ShareModeWeb(object): else: self.filesize = self.download_filesize - if self.web.slug: + if self.web.password: r = make_response(render_template( 'send.html', - slug=self.web.slug, + password=self.web.password, file_info=self.file_info, filename=os.path.basename(self.download_filename), filesize=self.filesize, diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 43316b43..eb4c34a9 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -45,7 +45,7 @@ class Web(object): REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_CANCELED = 9 REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 - REQUEST_INVALID_SLUG = 11 + REQUEST_INVALID_PASSWORD = 11 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -97,14 +97,14 @@ class Web(object): ] self.q = queue.Queue() - self.slug = None + self.password = None - self.reset_invalid_slugs() + self.reset_invalid_passwords() self.done = 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) + self.shutdown_password = self.common.random_string(16) # Keep track if the server is running self.running = False @@ -131,7 +131,7 @@ class Web(object): @self.auth.get_password def get_pw(username): if username == 'onionshare': - return self.slug + return self.password else: return None @@ -148,12 +148,12 @@ class Web(object): def not_found(e): return self.error404() - @self.app.route("//shutdown") - def shutdown(slug_candidate): + @self.app.route("//shutdown") + def shutdown(password_candidate): """ Stop the flask web server, from the context of an http request. """ - if slug_candidate == self.shutdown_slug: + if password_candidate == self.shutdown_password: self.force_shutdown() return "" abort(404) @@ -169,14 +169,14 @@ class Web(object): def error401(self): auth = request.authorization if auth: - if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_slugs: + if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_passwords: print('Invalid password guess: {}'.format(auth['password'])) - self.add_request(Web.REQUEST_INVALID_SLUG, data=auth['password']) + self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth['password']) - self.invalid_slugs.append(auth['password']) - self.invalid_slugs_count += 1 + self.invalid_passwords.append(auth['password']) + self.invalid_passwords_count += 1 - if self.invalid_slugs_count == 20: + if self.invalid_passwords_count == 20: self.add_request(Web.REQUEST_RATE_LIMIT) self.force_shutdown() print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") @@ -218,14 +218,14 @@ class Web(object): '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)) + def generate_password(self, persistent_password=None): + self.common.log('Web', 'generate_password', 'persistent_password={}'.format(persistent_password)) + if persistent_password != None and persistent_password != '': + self.password = persistent_password + self.common.log('Web', 'generate_password', 'persistent_password sent, so password is: "{}"'.format(self.password)) else: - self.slug = self.common.build_slug() - self.common.log('Web', 'generate_slug', 'built random slug: "{}"'.format(self.slug)) + self.password = self.common.build_password() + self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password)) def verbose_mode(self): """ @@ -236,9 +236,9 @@ class Web(object): log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - def reset_invalid_slugs(self): - self.invalid_slugs_count = 0 - self.invalid_slugs = [] + def reset_invalid_passwords(self): + self.invalid_passwords_count = 0 + self.invalid_passwords = [] def force_shutdown(self): """ @@ -254,11 +254,11 @@ class Web(object): pass self.running = False - def start(self, port, stay_open=False, public_mode=False, slug=None): + def start(self, port, stay_open=False, public_mode=False, password=None): """ Start the flask web server. """ - self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, slug={}'.format(port, stay_open, public_mode, slug)) + self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, password={}'.format(port, stay_open, public_mode, password)) self.stay_open = stay_open @@ -287,11 +287,11 @@ class Web(object): # Let the mode know that the user stopped the server self.stop_q.put(True) - # To stop flask, load http://shutdown:[shutdown_slug]@127.0.0.1/[shutdown_slug]/shutdown - # (We're putting the shutdown_slug in the path as well to make routing simpler) + # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown + # (We're putting the shutdown_password in the path as well to make routing simpler) if self.running: - requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_slug), - auth=requests.auth.HTTPBasicAuth('onionshare', self.slug)) + requests.get('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_password), + auth=requests.auth.HTTPBasicAuth('onionshare', self.password)) - # Reset any slug that was in use - self.slug = None + # Reset any password that was in use + self.password = None diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py index 8f5ff32b..e92e36f8 100644 --- a/onionshare_gui/mode/__init__.py +++ b/onionshare_gui/mode/__init__.py @@ -24,7 +24,7 @@ from onionshare.common import AutoStopTimer from ..server_status import ServerStatus from ..threads import OnionThread -from ..threads import AutoStartTimer +from ..threads import AutoStartTimer from ..widgets import Alert class Mode(QtWidgets.QWidget): @@ -181,7 +181,7 @@ class Mode(QtWidgets.QWidget): self.app.port = None # Start the onion thread. If this share was scheduled for a future date, - # the OnionThread will start and exit 'early' to obtain the port, slug + # the OnionThread will start and exit 'early' to obtain the port, password # and onion address, but it will not start the WebThread yet. if self.server_status.autostart_timer_datetime: self.start_onion_thread(obtain_onion_early=True) diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index d6b1c0f3..dbc0bc73 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -113,7 +113,7 @@ class ReceiveMode(Mode): """ # Reset web counters self.web.receive_mode.upload_count = 0 - self.web.reset_invalid_slugs() + self.web.reset_invalid_passwords() # Hide and reset the uploads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index f51fd0bb..143fd577 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -147,7 +147,7 @@ class ShareMode(Mode): """ # Reset web counters self.web.share_mode.download_count = 0 - self.web.reset_invalid_slugs() + self.web.reset_invalid_passwords() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index c6009ebe..ef7df94e 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -143,7 +143,7 @@ class WebsiteMode(Mode): """ # Reset web counters self.web.website_mode.visit_count = 0 - self.web.reset_invalid_slugs() + self.web.reset_invalid_passwords() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 4945ca7e..c5e7dc24 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -471,11 +471,11 @@ class OnionShareGui(QtWidgets.QMainWindow): Alert(self.common, strings._('error_cannot_create_data_dir').format(event["data"]["receive_mode_dir"])) if event["type"] == Web.REQUEST_OTHER: - if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): + if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_password): self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"])) - if event["type"] == Web.REQUEST_INVALID_SLUG: - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_slugs_count, strings._('invalid_slug_guess'), event["data"])) + if event["type"] == Web.REQUEST_INVALID_PASSWORD: + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_passwords_count, strings._('invalid_password_guess'), event["data"])) mode.timer_callback() diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index b23a89a8..3a6e31cc 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -243,8 +243,8 @@ class ServerStatus(QtWidgets.QWidget): self.show_url() if self.common.settings.get('save_private_key'): - if not self.common.settings.get('slug'): - self.common.settings.set('slug', self.web.slug) + if not self.common.settings.get('password'): + self.common.settings.set('password', self.web.password) self.common.settings.save() if self.common.settings.get('autostart_timer'): @@ -421,5 +421,5 @@ class ServerStatus(QtWidgets.QWidget): if self.common.settings.get('public_mode'): url = 'http://{0:s}'.format(self.app.onion_host) else: - url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.slug, self.app.onion_host) + url = 'http://onionshare:{0:s}@{1:s}'.format(self.web.password, self.app.onion_host) return url diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 3c0b83f4..ae5f5acf 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -54,7 +54,7 @@ class SettingsDialog(QtWidgets.QDialog): # General settings - # Use a slug or not ('public mode') + # Use a password or not ('public mode') self.public_mode_checkbox = QtWidgets.QCheckBox() self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox")) @@ -968,12 +968,12 @@ class SettingsDialog(QtWidgets.QDialog): if self.save_private_key_checkbox.isChecked(): settings.set('save_private_key', True) settings.set('private_key', self.old_settings.get('private_key')) - settings.set('slug', self.old_settings.get('slug')) + settings.set('password', self.old_settings.get('password')) settings.set('hidservauth_string', self.old_settings.get('hidservauth_string')) else: settings.set('save_private_key', False) settings.set('private_key', '') - settings.set('slug', '') + settings.set('password', '') # Also unset the HidServAuth if we are removing our reusable private key settings.set('hidservauth_string', '') diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index 26a9ee6b..bee1b6bc 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -42,13 +42,13 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') - # Choose port and slug early, because we need them to exist in advance for scheduled shares + # Choose port and password early, because we need them to exist in advance for scheduled shares self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') if not self.mode.app.port: self.mode.app.choose_port() if not self.mode.common.settings.get('public_mode'): - if not self.mode.web.slug: - self.mode.web.generate_slug(self.mode.common.settings.get('slug')) + if not self.mode.web.password: + self.mode.web.generate_password(self.mode.common.settings.get('password')) try: if self.mode.obtain_onion_early: @@ -86,7 +86,7 @@ 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('public_mode'), self.mode.web.slug) + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.password) self.success.emit() diff --git a/share/locale/en.json b/share/locale/en.json index 6dea9860..2063a415 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -3,7 +3,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": "Address loaded", - "invalid_slug_guess": "Invalid password guess", + "invalid_password_guess": "Invalid password guess", "close_on_autostop_timer": "Stopped because auto-stop timer ran out", "closing_automatically": "Stopped because transfer is complete", "large_filesize": "Warning: Sending a large share could take hours", From 97b5984afff1b00743380e775b961eac5ff3b3a7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 21 May 2019 10:08:54 -0700 Subject: [PATCH 19/33] Fix building the URL in CLI mode, and removing the slug from the download button in share mode --- onionshare/__init__.py | 19 +++++++++++++------ onionshare/web/share_mode.py | 25 +++++++------------------ share/templates/send.html | 4 ---- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index e2ff50a2..f0889263 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -27,6 +27,15 @@ from .web import Web from .onion import * from .onionshare import OnionShare + +def build_url(common, app, web): + # Build the URL + if common.settings.get('public_mode'): + return 'http://{0:s}'.format(app.onion_host) + else: + return 'http://onionshare:{0:s}@{1:s}'.format(web.password, app.onion_host) + + def main(cwd=None): """ The main() function implements all of the logic that the command-line version of @@ -128,12 +137,6 @@ def main(cwd=None): app.set_stealth(stealth) app.choose_port() - # Build the URL - if common.settings.get('public_mode'): - url = 'http://{0:s}'.format(app.onion_host) - else: - url = 'http://onionshare:{0:s}@{1:s}'.format(web.password, app.onion_host) - # Delay the startup if a startup timer was set if autostart_timer > 0: # Can't set a schedule that is later than the auto-stop timer @@ -142,6 +145,7 @@ def main(cwd=None): sys.exit() app.start_onion_service(False, True) + url = build_url(common, app, web) schedule = datetime.now() + timedelta(seconds=autostart_timer) if mode == 'receive': print("Files sent to you appear in this folder: {}".format(common.settings.get('data_dir'))) @@ -218,6 +222,9 @@ def main(cwd=None): common.settings.set('password', web.password) common.settings.save() + # Build the URL + url = build_url(common, app, web) + print('') if autostart_timer > 0: print("Server started") diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index bede4a36..22c58559 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -64,24 +64,13 @@ class ShareModeWeb(object): else: self.filesize = self.download_filesize - if self.web.password: - r = make_response(render_template( - 'send.html', - password=self.web.password, - file_info=self.file_info, - filename=os.path.basename(self.download_filename), - filesize=self.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.filesize, - filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) + r = make_response(render_template( + 'send.html', + file_info=self.file_info, + filename=os.path.basename(self.download_filename), + 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) @self.web.app.route("/download") diff --git a/share/templates/send.html b/share/templates/send.html index 2a56829a..7be9e100 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -15,11 +15,7 @@
  • Total size: {{ filesize_human }} {% if is_zipped %} (compressed){% endif %}
  • - {% if slug %} -
  • Download Files
  • - {% else %}
  • Download Files
  • - {% endif %}
From 63ced5625028f42331eab729639534c8064ba352 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 21 May 2019 10:18:40 -0700 Subject: [PATCH 20/33] Update ReceiveMode to no longer rely on slugs --- onionshare/web/receive_mode.py | 30 +++++------------------------- share/static/js/receive.js | 2 +- share/templates/receive.html | 2 +- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index af146cb0..60f421fa 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -34,15 +34,7 @@ class ReceiveModeWeb(object): @self.web.app.route("/") def index(): self.web.add_request(self.web.REQUEST_LOAD, request.path) - - if self.common.settings.get('public_mode'): - upload_action = '/upload' - else: - upload_action = '/{}/upload'.format(self.web.password) - - r = make_response(render_template( - 'receive.html', - upload_action=upload_action)) + r = make_response(render_template('receive.html')) return self.web.add_security_headers(r) @self.web.app.route("/upload", methods=['POST']) @@ -83,11 +75,7 @@ class ReceiveModeWeb(object): return json.dumps({"error_flashes": [msg]}) else: flash(msg, 'error') - - if self.common.settings.get('public_mode'): - return redirect('/') - else: - return redirect('/{}'.format(password_candidate)) + return redirect('/') # Note that flash strings are in English, and not translated, on purpose, # to avoid leaking the locale of the OnionShare user @@ -114,11 +102,7 @@ class ReceiveModeWeb(object): if ajax: return json.dumps({"info_flashes": info_flashes}) else: - if self.common.settings.get('public_mode'): - path = '/' - else: - path = '/{}'.format(password_candidate) - return redirect('{}'.format(path)) + return redirect('/') else: if ajax: return json.dumps({"new_body": render_template('thankyou.html')}) @@ -234,12 +218,8 @@ class ReceiveModeRequest(Request): # Is this a valid upload request? self.upload_request = False if self.method == 'POST': - if self.web.common.settings.get('public_mode'): - if self.path == '/upload' or self.path == '/upload-ajax': - self.upload_request = True - else: - if self.path == '/{}/upload'.format(self.web.password) or self.path == '/{}/upload-ajax'.format(self.web.password): - self.upload_request = True + if self.path == '/upload' or self.path == '/upload-ajax': + self.upload_request = True if self.upload_request: # No errors yet diff --git a/share/static/js/receive.js b/share/static/js/receive.js index c29c726c..cbd60954 100644 --- a/share/static/js/receive.js +++ b/share/static/js/receive.js @@ -121,7 +121,7 @@ $(function(){ $('#uploads').append($upload_div); // Send the request - ajax.open('POST', window.location.pathname.replace(/\/$/, '') + '/upload-ajax', true); + ajax.open('POST', '/upload-ajax', true); ajax.send(formData); }); }); diff --git a/share/templates/receive.html b/share/templates/receive.html index 4f207a03..dd36ac72 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -45,7 +45,7 @@ -
+

From 91238366b1f54568270ff0132f5b9c50cdbd0b5f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 22 May 2019 19:55:55 -0700 Subject: [PATCH 21/33] Don't need BaseShareMode yet --- onionshare/web/base_share_mode.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 onionshare/web/base_share_mode.py diff --git a/onionshare/web/base_share_mode.py b/onionshare/web/base_share_mode.py deleted file mode 100644 index 64cf3dce..00000000 --- a/onionshare/web/base_share_mode.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -import sys -import tempfile -import zipfile -import mimetypes -import gzip -from flask import Response, request, render_template, make_response - -from .. import strings - - -class ShareModeWeb(object): - """ - This is the base class that includes shared functionality between share mode - and website mode - """ - def __init__(self, common, web): - self.common = common - self.web = web From 41be429b91f8b323644fe200f696df1890ac3de7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 22 May 2019 20:07:35 -0700 Subject: [PATCH 22/33] Make static folder URL have a high-entropy random path, to avoid filename collisions with files getting shared --- onionshare/web/receive_mode.py | 9 ++++++--- onionshare/web/share_mode.py | 9 ++++++--- onionshare/web/web.py | 14 ++++++++++---- onionshare/web/website_mode.py | 3 ++- share/templates/401.html | 6 +++--- share/templates/403.html | 6 +++--- share/templates/404.html | 6 +++--- share/templates/denied.html | 2 +- share/templates/listing.html | 10 +++++----- share/templates/receive.html | 16 ++++++++-------- share/templates/receive_noscript_xss.html | 6 +++--- share/templates/send.html | 12 ++++++------ share/templates/thankyou.html | 8 ++++---- 13 files changed, 60 insertions(+), 47 deletions(-) diff --git a/onionshare/web/receive_mode.py b/onionshare/web/receive_mode.py index 60f421fa..3f848d2f 100644 --- a/onionshare/web/receive_mode.py +++ b/onionshare/web/receive_mode.py @@ -34,7 +34,8 @@ class ReceiveModeWeb(object): @self.web.app.route("/") def index(): self.web.add_request(self.web.REQUEST_LOAD, request.path) - r = make_response(render_template('receive.html')) + r = make_response(render_template('receive.html', + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) @self.web.app.route("/upload", methods=['POST']) @@ -105,10 +106,12 @@ class ReceiveModeWeb(object): return redirect('/') else: if ajax: - return json.dumps({"new_body": render_template('thankyou.html')}) + return json.dumps({ + "new_body": render_template('thankyou.html', static_url_path=self.web.static_url_path) + }) else: # It was the last upload and the timer ran out - r = make_response(render_template('thankyou.html')) + r = make_response(render_template('thankyou.html'), static_url_path=self.web.static_url_path) return self.web.add_security_headers(r) @self.web.app.route("/upload-ajax", methods=['POST']) diff --git a/onionshare/web/share_mode.py b/onionshare/web/share_mode.py index 22c58559..0dfa7e0a 100644 --- a/onionshare/web/share_mode.py +++ b/onionshare/web/share_mode.py @@ -55,7 +55,8 @@ class ShareModeWeb(object): # currently a download deny_download = not self.web.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template('denied.html')) + r = make_response(render_template('denied.html'), + static_url_path=self.web.static_url_path) return self.web.add_security_headers(r) # If download is allowed to continue, serve download page @@ -70,7 +71,8 @@ class ShareModeWeb(object): filename=os.path.basename(self.download_filename), filesize=self.filesize, filesize_human=self.common.human_readable_filesize(self.download_filesize), - is_zipped=self.is_zipped)) + is_zipped=self.is_zipped, + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) @self.web.app.route("/download") @@ -82,7 +84,8 @@ class ShareModeWeb(object): # currently a download deny_download = not self.web.stay_open and self.download_in_progress if deny_download: - r = make_response(render_template('denied.html')) + r = make_response(render_template('denied.html', + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) # Each download has a unique id diff --git a/onionshare/web/web.py b/onionshare/web/web.py index eb4c34a9..1500a23c 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -51,8 +51,13 @@ class Web(object): self.common = common self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode)) + # The static URL path has a 128-bit random number in it to avoid having name + # collisions with files that might be getting shared + self.static_url_path = '/static_{}'.format(self.common.random_string(16)) + # The flask app self.app = Flask(__name__, + static_url_path=self.static_url_path, 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) @@ -163,7 +168,8 @@ class Web(object): """ Display instructions for disabling Tor Browser's NoScript XSS setting """ - r = make_response(render_template('receive_noscript_xss.html')) + r = make_response(render_template('receive_noscript_xss.html', + static_url_path=self.static_url_path)) return self.add_security_headers(r) def error401(self): @@ -181,18 +187,18 @@ class Web(object): self.force_shutdown() print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") - r = make_response(render_template('401.html'), 401) + r = make_response(render_template('401.html', static_url_path=self.static_url_path), 401) return self.add_security_headers(r) def error404(self): self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response(render_template('404.html'), 404) + r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404) return self.add_security_headers(r) def error403(self): self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response(render_template('403.html'), 403) + r = make_response(render_template('403.html', static_url_path=self.static_url_path), 403) return self.add_security_headers(r) def add_security_headers(self, r): diff --git a/onionshare/web/website_mode.py b/onionshare/web/website_mode.py index 354c5aa7..d2cd6db9 100644 --- a/onionshare/web/website_mode.py +++ b/onionshare/web/website_mode.py @@ -131,7 +131,8 @@ class WebsiteModeWeb(object): r = make_response(render_template('listing.html', path=path, files=files, - dirs=dirs)) + dirs=dirs, + static_url_path=self.web.static_url_path)) return self.web.add_security_headers(r) def set_file_info(self, filenames): diff --git a/share/templates/401.html b/share/templates/401.html index 9d3989a3..dc50f534 100644 --- a/share/templates/401.html +++ b/share/templates/401.html @@ -3,14 +3,14 @@ OnionShare: 401 Unauthorized Access - - + +
-

+

401 Unauthorized Access

diff --git a/share/templates/403.html b/share/templates/403.html index f3ea4e0e..2ebab09a 100644 --- a/share/templates/403.html +++ b/share/templates/403.html @@ -3,14 +3,14 @@ OnionShare: 403 Forbidden - - + +
-

+

You are not allowed to perform that action at this time.

diff --git a/share/templates/404.html b/share/templates/404.html index 1c5d7d2d..375c125d 100644 --- a/share/templates/404.html +++ b/share/templates/404.html @@ -3,14 +3,14 @@ OnionShare: 404 Not Found - - + +
-

+

404 Not Found

diff --git a/share/templates/denied.html b/share/templates/denied.html index 94fb379b..ad4d0b21 100644 --- a/share/templates/denied.html +++ b/share/templates/denied.html @@ -3,7 +3,7 @@ OnionShare - + diff --git a/share/templates/listing.html b/share/templates/listing.html index 8883eea9..e394f842 100644 --- a/share/templates/listing.html +++ b/share/templates/listing.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

@@ -22,7 +22,7 @@ {% for info in dirs %} - + {{ info.basename }} @@ -34,7 +34,7 @@ {% for info in files %} - + {{ info.basename }} diff --git a/share/templates/receive.html b/share/templates/receive.html index dd36ac72..23242501 100644 --- a/share/templates/receive.html +++ b/share/templates/receive.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

@@ -19,14 +19,14 @@ -->

- Warning: Due to a bug in Tor Browser and Firefox, uploads + Warning: Due to a bug in Tor Browser and Firefox, uploads sometimes never finish. To upload reliably, either set your Tor Browser security slider to Standard or turn off your Tor Browser's NoScript XSS setting.

-

+

Send Files

Select the files you want to send, then click "Send Files"...

@@ -51,8 +51,8 @@ - - - + + + diff --git a/share/templates/receive_noscript_xss.html b/share/templates/receive_noscript_xss.html index bce78524..84d35ba1 100644 --- a/share/templates/receive_noscript_xss.html +++ b/share/templates/receive_noscript_xss.html @@ -2,13 +2,13 @@ OnionShare - - + +
- +

OnionShare

diff --git a/share/templates/send.html b/share/templates/send.html index 7be9e100..e0076c0f 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -3,8 +3,8 @@ OnionShare - - + + @@ -18,7 +18,7 @@
  • Download Files
  • - +

    OnionShare

    @@ -31,7 +31,7 @@ {% for info in file_info.dirs %} - + {{ info.basename }} {{ info.size_human }} @@ -41,7 +41,7 @@ {% for info in file_info.files %} - + {{ info.basename }} {{ info.size_human }} @@ -49,7 +49,7 @@ {% endfor %} - + diff --git a/share/templates/thankyou.html b/share/templates/thankyou.html index c4b39cde..b7e2b97c 100644 --- a/share/templates/thankyou.html +++ b/share/templates/thankyou.html @@ -3,19 +3,19 @@ OnionShare is closed - - + +
    - +

    OnionShare

    -

    +

    Thank you for using OnionShare

    You may now close this window.

    From 7ac81e6b7e8163a8bdaa32d2b874287028f56bcf Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 22 May 2019 20:15:49 -0700 Subject: [PATCH 23/33] Allow static resources without basic auth --- onionshare/web/web.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 1500a23c..c6e902ed 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -142,6 +142,11 @@ class Web(object): @self.app.before_request def conditional_auth_check(): + # Allow static files without basic authentication + if(request.path.startswith(self.static_url_path + '/')): + return None + + # If public mode is disabled, require authentication if not self.common.settings.get('public_mode'): @self.auth.login_required def _check_login(): From 44431becc1602a3178538f2a619abe6ebae29237 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 22 May 2019 20:46:23 -0700 Subject: [PATCH 24/33] Give xvfb-run a screen to floods of Qt logs in CircleCI tests, so the test output is readable --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index accbc808..3a63d743 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: - run: name: run tests command: | - xvfb-run pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv tests/ + xvfb-run -s "-screen 0 1280x1024x24" pytest --rungui --cov=onionshare --cov=onionshare_gui --cov-report=term-missing -vvv --no-qt-log tests/ test-3.6: <<: *test-template From 4df989dc77b2ffee26afd529dee312665c526a9b Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 22 May 2019 20:55:31 -0700 Subject: [PATCH 25/33] Rename slugs to passwords in the tests --- tests/GuiBaseTest.py | 16 +++++----- tests/GuiReceiveTest.py | 8 ++--- tests/GuiShareTest.py | 32 +++++++++---------- tests/TorGuiBaseTest.py | 8 ++--- tests/TorGuiReceiveTest.py | 5 ++- tests/TorGuiShareTest.py | 13 ++++---- ...re_share_mode_password_persistent_test.py} | 4 +-- .../onionshare_share_mode_persistent_test.py | 4 +-- tests/test_onionshare_common.py | 16 +++++----- tests/test_onionshare_settings.py | 2 +- tests/test_onionshare_web.py | 24 +++++++------- 11 files changed, 65 insertions(+), 67 deletions(-) rename tests/{local_onionshare_share_mode_slug_persistent_test.py => local_onionshare_share_mode_password_persistent_test.py} (87%) diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py index d3fc9945..65178f46 100644 --- a/tests/GuiBaseTest.py +++ b/tests/GuiBaseTest.py @@ -127,7 +127,7 @@ class GuiBaseTest(object): # Upload a file files = {'file[]': open('/tmp/test.txt', 'rb')} if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.slug) + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.password) else: path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) response = requests.post(path, files=files) @@ -138,7 +138,7 @@ class GuiBaseTest(object): if public_mode: url = "http://127.0.0.1:{}/download".format(self.gui.app.port) else: - url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, mode.web.slug) + url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, mode.web.password) r = requests.get(url) QtTest.QTest.qWait(2000) @@ -190,12 +190,12 @@ class GuiBaseTest(object): self.assertEqual(sock.connect_ex(('127.0.0.1',self.gui.app.port)), 0) - def have_a_slug(self, mode, public_mode): - '''Test that we have a valid slug''' + def have_a_password(self, mode, public_mode): + '''Test that we have a valid password''' if not public_mode: - self.assertRegex(mode.server_status.web.slug, r'(\w+)-(\w+)') + self.assertRegex(mode.server_status.web.password, r'(\w+)-(\w+)') else: - self.assertIsNone(mode.server_status.web.slug, r'(\w+)-(\w+)') + self.assertIsNone(mode.server_status.web.password, r'(\w+)-(\w+)') def url_description_shown(self, mode): @@ -212,7 +212,7 @@ class GuiBaseTest(object): if public_mode: self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port)) else: - self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, mode.server_status.web.slug)) + self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, mode.server_status.web.password)) def server_status_indicator_says_started(self, mode): @@ -230,7 +230,7 @@ class GuiBaseTest(object): s.connect(('127.0.0.1', self.gui.app.port)) if not public_mode: - path = '/{}'.format(mode.server_status.web.slug) + path = '/{}'.format(mode.server_status.web.password) else: path = '/' diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py index 40c3de95..6ecf608c 100644 --- a/tests/GuiReceiveTest.py +++ b/tests/GuiReceiveTest.py @@ -9,7 +9,7 @@ class GuiReceiveTest(GuiBaseTest): '''Test that we can upload the file''' files = {'file[]': open(file_to_upload, 'rb')} if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) else: path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) response = requests.post(path, files=files) @@ -40,7 +40,7 @@ class GuiReceiveTest(GuiBaseTest): '''Test that we can't upload the file when permissions are wrong, and expected content is shown''' 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) + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) else: path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) response = requests.post(path, files=files) @@ -61,7 +61,7 @@ class GuiReceiveTest(GuiBaseTest): def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode): '''If you submit the receive mode form without selecting any files, the UI shouldn't get updated''' if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.slug) + path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) else: path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) @@ -93,7 +93,7 @@ class GuiReceiveTest(GuiBaseTest): self.settings_button_is_hidden() self.server_is_started(self.gui.receive_mode) self.web_server_is_running() - self.have_a_slug(self.gui.receive_mode, public_mode) + self.have_a_password(self.gui.receive_mode, public_mode) self.url_description_shown(self.gui.receive_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode) self.server_status_indicator_says_started(self.gui.receive_mode) diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py index 29661712..02ae0eea 100644 --- a/tests/GuiShareTest.py +++ b/tests/GuiShareTest.py @@ -7,9 +7,9 @@ from .GuiBaseTest import GuiBaseTest class GuiShareTest(GuiBaseTest): # Persistence tests - def have_same_slug(self, slug): - '''Test that we have the same slug''' - self.assertEqual(self.gui.share_mode.server_status.web.slug, slug) + def have_same_password(self, password): + '''Test that we have the same password''' + self.assertEqual(self.gui.share_mode.server_status.web.password, password) # Share-specific tests @@ -17,7 +17,7 @@ class GuiShareTest(GuiBaseTest): '''Test that the number of items in the list is as expected''' self.assertEqual(self.gui.share_mode.server_status.file_selection.get_num_files(), num) - + def deleting_all_files_hides_delete_button(self): '''Test that clicking on the file item shows the delete button. Test that deleting the only item in the list hides the delete button''' rect = self.gui.share_mode.server_status.file_selection.file_list.visualItemRect(self.gui.share_mode.server_status.file_selection.file_list.item(0)) @@ -35,14 +35,14 @@ class GuiShareTest(GuiBaseTest): # No more files, the delete button should be hidden self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - + def add_a_file_and_delete_using_its_delete_widget(self): '''Test that we can also delete a file by clicking on its [X] widget''' self.gui.share_mode.server_status.file_selection.file_list.add_file('/etc/hosts') QtTest.QTest.mouseClick(self.gui.share_mode.server_status.file_selection.file_list.item(0).item_button, QtCore.Qt.LeftButton) self.file_selection_widget_has_files(0) - + def 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') @@ -56,14 +56,14 @@ class GuiShareTest(GuiBaseTest): with open('/tmp/large_file', 'wb') as fout: fout.write(os.urandom(size)) self.gui.share_mode.server_status.file_selection.file_list.add_file('/tmp/large_file') - + def add_delete_buttons_hidden(self): '''Test that the add and delete buttons are hidden when the server starts''' self.assertFalse(self.gui.share_mode.server_status.file_selection.add_button.isVisible()) self.assertFalse(self.gui.share_mode.server_status.file_selection.delete_button.isVisible()) - + def download_share(self, public_mode): '''Test that we can download the share''' s = socks.socksocket() @@ -73,7 +73,7 @@ class GuiShareTest(GuiBaseTest): if public_mode: path = '/download' else: - path = '{}/download'.format(self.gui.share_mode.web.slug) + path = '{}/download'.format(self.gui.share_mode.web.password) http_request = 'GET {} HTTP/1.0\r\n'.format(path) http_request += 'Host: 127.0.0.1\r\n' @@ -130,7 +130,7 @@ class GuiShareTest(GuiBaseTest): self.add_a_file_and_delete_using_its_delete_widget() self.file_selection_widget_readd_files() - + def run_all_share_mode_started_tests(self, public_mode, startup_time=2000): """Tests in share mode after starting a share""" self.server_working_on_start_button_pressed(self.gui.share_mode) @@ -139,12 +139,12 @@ class GuiShareTest(GuiBaseTest): self.settings_button_is_hidden() self.server_is_started(self.gui.share_mode, startup_time) self.web_server_is_running() - self.have_a_slug(self.gui.share_mode, public_mode) + self.have_a_password(self.gui.share_mode, public_mode) self.url_description_shown(self.gui.share_mode) self.have_copy_url_button(self.gui.share_mode, public_mode) self.server_status_indicator_says_started(self.gui.share_mode) - + def run_all_share_mode_download_tests(self, public_mode, stay_open): """Tests in share mode after downloading a share""" self.web_page(self.gui.share_mode, 'Total size', public_mode) @@ -158,7 +158,7 @@ class GuiShareTest(GuiBaseTest): self.server_is_started(self.gui.share_mode) self.history_indicator(self.gui.share_mode, public_mode) - + def run_all_share_mode_tests(self, public_mode, stay_open): """End-to-end share tests""" self.run_all_share_mode_setup_tests() @@ -178,12 +178,12 @@ class GuiShareTest(GuiBaseTest): def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the slug is the same on multiple shared""" + """Same as end-to-end share tests but also test the password is the same on multiple shared""" self.run_all_share_mode_setup_tests() self.run_all_share_mode_started_tests(public_mode) - slug = self.gui.share_mode.server_status.web.slug + password = self.gui.share_mode.server_status.web.password self.run_all_share_mode_download_tests(public_mode, stay_open) - self.have_same_slug(slug) + self.have_same_password(password) def run_all_share_mode_timer_tests(self, public_mode): diff --git a/tests/TorGuiBaseTest.py b/tests/TorGuiBaseTest.py index 8bd963bd..3f9952d0 100644 --- a/tests/TorGuiBaseTest.py +++ b/tests/TorGuiBaseTest.py @@ -76,7 +76,7 @@ class TorGuiBaseTest(GuiBaseTest): # Upload a file files = {'file[]': open('/tmp/test.txt', 'rb')} if not public_mode: - path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, mode.web.slug) + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, mode.web.password) else: path = 'http://{}/upload'.format(self.gui.app.onion_host) response = session.post(path, files=files) @@ -87,7 +87,7 @@ class TorGuiBaseTest(GuiBaseTest): if public_mode: path = "http://{}/download".format(self.gui.app.onion_host) else: - path = "http://{}/{}/download".format(self.gui.app.onion_host, mode.web.slug) + path = "http://{}/{}/download".format(self.gui.app.onion_host, mode.web.password) response = session.get(path) QtTest.QTest.qWait(4000) @@ -111,7 +111,7 @@ class TorGuiBaseTest(GuiBaseTest): s.settimeout(60) s.connect((self.gui.app.onion_host, 80)) if not public_mode: - path = '/{}'.format(mode.server_status.web.slug) + path = '/{}'.format(mode.server_status.web.password) else: path = '/' http_request = 'GET {} HTTP/1.0\r\n'.format(path) @@ -138,7 +138,7 @@ class TorGuiBaseTest(GuiBaseTest): if public_mode: self.assertEqual(clipboard.text(), 'http://{}'.format(self.gui.app.onion_host)) else: - self.assertEqual(clipboard.text(), 'http://{}/{}'.format(self.gui.app.onion_host, mode.server_status.web.slug)) + self.assertEqual(clipboard.text(), 'http://{}/{}'.format(self.gui.app.onion_host, mode.server_status.web.password)) # Stealth tests diff --git a/tests/TorGuiReceiveTest.py b/tests/TorGuiReceiveTest.py index a21dd4fc..601f34b6 100644 --- a/tests/TorGuiReceiveTest.py +++ b/tests/TorGuiReceiveTest.py @@ -13,7 +13,7 @@ class TorGuiReceiveTest(TorGuiBaseTest): session.proxies['http'] = 'socks5h://{}:{}'.format(socks_address, socks_port) files = {'file[]': open(file_to_upload, 'rb')} if not public_mode: - path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.slug) + path = 'http://{}/{}/upload'.format(self.gui.app.onion_host, self.gui.receive_mode.web.password) else: path = 'http://{}/upload'.format(self.gui.app.onion_host) response = session.post(path, files=files) @@ -35,7 +35,7 @@ class TorGuiReceiveTest(TorGuiBaseTest): self.server_is_started(self.gui.receive_mode, startup_time=45000) self.web_server_is_running() self.have_an_onion_service() - self.have_a_slug(self.gui.receive_mode, public_mode) + self.have_a_password(self.gui.receive_mode, public_mode) self.url_description_shown(self.gui.receive_mode) self.have_copy_url_button(self.gui.receive_mode, public_mode) self.server_status_indicator_says_started(self.gui.receive_mode) @@ -56,4 +56,3 @@ class TorGuiReceiveTest(TorGuiBaseTest): self.server_working_on_start_button_pressed(self.gui.receive_mode) self.server_is_started(self.gui.receive_mode, startup_time=45000) self.history_indicator(self.gui.receive_mode, public_mode) - diff --git a/tests/TorGuiShareTest.py b/tests/TorGuiShareTest.py index 36efacd1..352707eb 100644 --- a/tests/TorGuiShareTest.py +++ b/tests/TorGuiShareTest.py @@ -17,7 +17,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): if public_mode: path = "http://{}/download".format(self.gui.app.onion_host) else: - path = "http://{}/{}/download".format(self.gui.app.onion_host, self.gui.share_mode.web.slug) + path = "http://{}/{}/download".format(self.gui.app.onion_host, self.gui.share_mode.web.password) response = session.get(path, stream=True) QtTest.QTest.qWait(4000) @@ -53,7 +53,7 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): self.server_is_started(self.gui.share_mode, startup_time=45000) self.web_server_is_running() self.have_an_onion_service() - self.have_a_slug(self.gui.share_mode, public_mode) + self.have_a_password(self.gui.share_mode, public_mode) self.url_description_shown(self.gui.share_mode) self.have_copy_url_button(self.gui.share_mode, public_mode) self.server_status_indicator_says_started(self.gui.share_mode) @@ -74,16 +74,16 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): def run_all_share_mode_persistent_tests(self, public_mode, stay_open): - """Same as end-to-end share tests but also test the slug is the same on multiple shared""" + """Same as end-to-end share tests but also test the password is the same on multiple shared""" self.run_all_share_mode_setup_tests() self.run_all_share_mode_started_tests(public_mode) - slug = self.gui.share_mode.server_status.web.slug + password = self.gui.share_mode.server_status.web.password onion = self.gui.app.onion_host self.run_all_share_mode_download_tests(public_mode, stay_open) self.have_same_onion(onion) - self.have_same_slug(slug) + self.have_same_password(password) + - def run_all_share_mode_timer_tests(self, public_mode): """Auto-stop timer tests in share mode""" self.run_all_share_mode_setup_tests() @@ -92,4 +92,3 @@ class TorGuiShareTest(TorGuiBaseTest, GuiShareTest): self.autostop_timer_widget_hidden(self.gui.share_mode) self.server_timed_out(self.gui.share_mode, 125000) self.web_server_is_stopped() - diff --git a/tests/local_onionshare_share_mode_slug_persistent_test.py b/tests/local_onionshare_share_mode_password_persistent_test.py similarity index 87% rename from tests/local_onionshare_share_mode_slug_persistent_test.py rename to tests/local_onionshare_share_mode_password_persistent_test.py index 58e1cfeb..5b515ca1 100644 --- a/tests/local_onionshare_share_mode_slug_persistent_test.py +++ b/tests/local_onionshare_share_mode_password_persistent_test.py @@ -4,12 +4,12 @@ import unittest from .GuiShareTest import GuiShareTest -class LocalShareModePersistentSlugTest(unittest.TestCase, GuiShareTest): +class LocalShareModePersistentPasswordTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { "public_mode": False, - "slug": "", + "password": "", "save_private_key": True, "close_after_first_download": False, } diff --git a/tests/onionshare_share_mode_persistent_test.py b/tests/onionshare_share_mode_persistent_test.py index d0fb5b22..0e461f7e 100644 --- a/tests/onionshare_share_mode_persistent_test.py +++ b/tests/onionshare_share_mode_persistent_test.py @@ -4,13 +4,13 @@ import unittest from .TorGuiShareTest import TorGuiShareTest -class ShareModePersistentSlugTest(unittest.TestCase, TorGuiShareTest): +class ShareModePersistentPasswordTest(unittest.TestCase, TorGuiShareTest): @classmethod def setUpClass(cls): test_settings = { "use_legacy_v2_onions": True, "public_mode": False, - "slug": "", + "password": "", "save_private_key": True, "close_after_first_download": False, } diff --git a/tests/test_onionshare_common.py b/tests/test_onionshare_common.py index f975dce7..d5e67381 100644 --- a/tests/test_onionshare_common.py +++ b/tests/test_onionshare_common.py @@ -33,13 +33,13 @@ LOG_MSG_REGEX = re.compile(r""" ^\[Jun\ 06\ 2013\ 11:05:00\] \ TestModule\.\.dummy_func \ at\ 0x[a-f0-9]+>(:\ TEST_MSG)?$""", re.VERBOSE) -SLUG_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$') +PASSWORD_REGEX = re.compile(r'^([a-z]+)(-[a-z]+)?-([a-z]+)(-[a-z]+)?$') # TODO: Improve the Common tests to test it all as a single class -class TestBuildSlug: +class TestBuildPassword: @pytest.mark.parametrize('test_input,expected', ( # VALID, two lowercase words, separated by a hyphen ('syrup-enzyme', True), @@ -60,8 +60,8 @@ class TestBuildSlug: ('too-many-hyphens-', False), ('symbols-!@#$%', False) )) - def test_build_slug_regex(self, test_input, expected): - """ Test that `SLUG_REGEX` accounts for the following patterns + def test_build_password_regex(self, test_input, expected): + """ Test that `PASSWORD_REGEX` accounts for the following patterns There are a few hyphenated words in `wordlist.txt`: * drop-down @@ -69,17 +69,17 @@ class TestBuildSlug: * t-shirt * yo-yo - These words cause a few extra potential slug patterns: + These words cause a few extra potential password patterns: * word-word * hyphenated-word-word * word-hyphenated-word * hyphenated-word-hyphenated-word """ - assert bool(SLUG_REGEX.match(test_input)) == expected + assert bool(PASSWORD_REGEX.match(test_input)) == expected - def test_build_slug_unique(self, common_obj, sys_onionshare_dev_mode): - assert common_obj.build_slug() != common_obj.build_slug() + def test_build_password_unique(self, common_obj, sys_onionshare_dev_mode): + assert common_obj.build_password() != common_obj.build_password() class TestDirSize: diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index bcc2f7cb..05878899 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -63,7 +63,7 @@ class TestSettings: 'use_legacy_v2_onions': False, 'save_private_key': False, 'private_key': '', - 'slug': '', + 'password': '', 'hidservauth_string': '', 'data_dir': os.path.expanduser('~/OnionShare'), 'public_mode': False diff --git a/tests/test_onionshare_web.py b/tests/test_onionshare_web.py index 0c29859b..f9c6c2ec 100644 --- a/tests/test_onionshare_web.py +++ b/tests/test_onionshare_web.py @@ -44,7 +44,7 @@ def web_obj(common_obj, mode, num_files=0): common_obj.settings = Settings(common_obj) strings.load_strings(common_obj) web = Web(common_obj, False, mode) - web.generate_slug() + web.generate_password() web.stay_open = True web.running = True @@ -76,17 +76,17 @@ class TestWeb: res.get_data() assert res.status_code == 404 - res = c.get('/invalidslug'.format(web.slug)) + res = c.get('/invalidpassword'.format(web.password)) res.get_data() assert res.status_code == 404 # Load download page - res = c.get('/{}'.format(web.slug)) + res = c.get('/{}'.format(web.password)) res.get_data() assert res.status_code == 200 # Download - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/{}/download'.format(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -99,7 +99,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/{}/download'.format(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -114,7 +114,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.slug)) + res = c.get('/{}/download'.format(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -130,12 +130,12 @@ class TestWeb: res.get_data() assert res.status_code == 404 - res = c.get('/invalidslug'.format(web.slug)) + res = c.get('/invalidpassword'.format(web.password)) res.get_data() assert res.status_code == 404 # Load upload page - res = c.get('/{}'.format(web.slug)) + res = c.get('/{}'.format(web.password)) res.get_data() assert res.status_code == 200 @@ -149,8 +149,8 @@ class TestWeb: data1 = res.get_data() assert res.status_code == 200 - # /[slug] should be a 404 - res = c.get('/{}'.format(web.slug)) + # /[password] should be a 404 + res = c.get('/{}'.format(web.password)) data2 = res.get_data() assert res.status_code == 404 @@ -164,8 +164,8 @@ class TestWeb: data1 = res.get_data() assert res.status_code == 404 - # Upload page should be accessible from /[slug] - res = c.get('/{}'.format(web.slug)) + # Upload page should be accessible from /[password] + res = c.get('/{}'.format(web.password)) data2 = res.get_data() assert res.status_code == 200 From 18961fea2dda64bcda6c461818901fd2e73576b1 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 23 May 2019 09:53:18 -0700 Subject: [PATCH 26/33] Fix web tests to use basic auth and passwords instead of slugs --- tests/test_onionshare_web.py | 64 +++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/tests/test_onionshare_web.py b/tests/test_onionshare_web.py index f9c6c2ec..313dbcea 100644 --- a/tests/test_onionshare_web.py +++ b/tests/test_onionshare_web.py @@ -27,8 +27,10 @@ import socket import sys import zipfile import tempfile +import base64 import pytest +from werkzeug.datastructures import Headers from onionshare.common import Common from onionshare import strings @@ -71,22 +73,23 @@ class TestWeb: web = web_obj(common_obj, 'share', 3) assert web.mode is 'share' with web.app.test_client() as c: - # Load 404 pages + # Load / without auth res = c.get('/') res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - res = c.get('/invalidpassword'.format(web.password)) + # Load / with invalid auth + res = c.get('/', headers=self._make_auth_headers('invalid')) res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - # Load download page - res = c.get('/{}'.format(web.password)) + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 # Download - res = c.get('/{}/download'.format(web.password)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -99,7 +102,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.password)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -114,7 +117,7 @@ class TestWeb: with web.app.test_client() as c: # Download the first time - res = c.get('/{}/download'.format(web.password)) + res = c.get('/download', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 assert res.mimetype == 'application/zip' @@ -125,17 +128,18 @@ class TestWeb: assert web.mode is 'receive' with web.app.test_client() as c: - # Load 404 pages + # Load / without auth res = c.get('/') res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - res = c.get('/invalidpassword'.format(web.password)) + # Load / with invalid auth + res = c.get('/', headers=self._make_auth_headers('invalid')) res.get_data() - assert res.status_code == 404 + assert res.status_code == 401 - # Load upload page - res = c.get('/{}'.format(web.password)) + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) res.get_data() assert res.status_code == 200 @@ -144,31 +148,37 @@ class TestWeb: common_obj.settings.set('public_mode', True) with web.app.test_client() as c: - # Upload page should be accessible from / + # Loading / should work without auth res = c.get('/') data1 = res.get_data() assert res.status_code == 200 - # /[password] should be a 404 - res = c.get('/{}'.format(web.password)) - data2 = res.get_data() - assert res.status_code == 404 - def test_public_mode_off(self, common_obj): web = web_obj(common_obj, 'receive') common_obj.settings.set('public_mode', False) with web.app.test_client() as c: - # / should be a 404 + # Load / without auth res = c.get('/') - data1 = res.get_data() - assert res.status_code == 404 + res.get_data() + assert res.status_code == 401 - # Upload page should be accessible from /[password] - res = c.get('/{}'.format(web.password)) - data2 = res.get_data() + # But static resources should work without auth + res = c.get('{}/css/style.css'.format(web.static_url_path)) + res.get_data() assert res.status_code == 200 + # Load / with valid auth + res = c.get('/', headers=self._make_auth_headers(web.password)) + res.get_data() + assert res.status_code == 200 + + def _make_auth_headers(self, password): + auth = base64.b64encode(b'onionshare:'+password.encode()).decode() + h = Headers() + h.add('Authorization', 'Basic ' + auth) + return h + class TestZipWriterDefault: @pytest.mark.parametrize('test_input', ( From f56b148ddb4d922c595400226263bea6d2a97fe8 Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 24 May 2019 10:08:51 +0200 Subject: [PATCH 27/33] Resolve bugs from initial PR --- onionshare/__init__.py | 4 ++-- onionshare_gui/mode/website_mode/__init__.py | 1 - onionshare_gui/onionshare_gui.py | 5 ++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index a96f2fca..1b099a1d 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -51,7 +51,7 @@ def main(cwd=None): parser.add_argument('--connect-timeout', metavar='', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)") parser.add_argument('--stealth', action='store_true', dest='stealth', help="Use client authorization (advanced)") parser.add_argument('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them") - parser.add_argument('--website', action='store_true', dest='website', help=strings._("help_website")) + parser.add_argument('--website', action='store_true', dest='website', help="Publish a static website") parser.add_argument('--config', metavar='config', default=False, help="Custom JSON config file location (optional)") parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help="Log OnionShare errors to stdout, and web errors to disk") parser.add_argument('filename', metavar='filename', nargs='*', help="List of files or folders to share") @@ -174,7 +174,7 @@ def main(cwd=None): if mode == 'website': # Prepare files to share - print(strings._("preparing_website")) + print("Preparing files to publish website...") try: web.website_mode.set_file_info(filenames) except OSError as e: diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 9018f5cb..50d72564 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -18,7 +18,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import os -import secrets import random import string diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 9fdf9395..6dec82b2 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -374,13 +374,16 @@ class OnionShareGui(QtWidgets.QMainWindow): if not self.common.settings.get('autostop_timer'): self.share_mode.server_status.autostop_timer_container.hide() self.receive_mode.server_status.autostop_timer_container.hide() + self.website_mode.server_status.autostop_timer_container.hide() # If we switched off the auto-start timer setting, ensure the widget is hidden. if not self.common.settings.get('autostart_timer'): self.share_mode.server_status.autostart_timer_datetime = None self.receive_mode.server_status.autostart_timer_datetime = None + self.website_mode.server_status.autostart_timer_datetime = None self.share_mode.server_status.autostart_timer_container.hide() self.receive_mode.server_status.autostart_timer_container.hide() - + self.website_mode.server_status.autostart_timer_container.hide() + d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only) d.settings_saved.connect(reload_settings) d.exec_() From 9785be0375b36d377fd1f0e4b43e0f52189c263f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 24 May 2019 13:38:41 -0700 Subject: [PATCH 28/33] Replace URLs that have slugs with basic auth in tests --- tests/GuiBaseTest.py | 22 +++++++----- tests/GuiReceiveTest.py | 35 ++++++++++--------- tests/GuiShareTest.py | 10 +++--- ...e_401_public_mode_skips_ratelimit_test.py} | 4 +-- ...onionshare_401_triggers_ratelimit_test.py} | 4 +-- 5 files changed, 41 insertions(+), 34 deletions(-) rename tests/{local_onionshare_404_public_mode_skips_ratelimit_test.py => local_onionshare_401_public_mode_skips_ratelimit_test.py} (87%) rename tests/{local_onionshare_404_triggers_ratelimit_test.py => local_onionshare_401_triggers_ratelimit_test.py} (87%) diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py index 65178f46..659ea052 100644 --- a/tests/GuiBaseTest.py +++ b/tests/GuiBaseTest.py @@ -4,6 +4,7 @@ import requests import shutil import socket import socks +import base64 from PyQt5 import QtCore, QtTest @@ -126,20 +127,20 @@ class GuiBaseTest(object): if type(mode) == ReceiveMode: # Upload a file files = {'file[]': open('/tmp/test.txt', 'rb')} - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, mode.web.password) + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + if public_mode: + response = requests.post(url, files=files) else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) + response = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtTest.QTest.qWait(2000) if type(mode) == ShareMode: # Download files + url = "http://127.0.0.1:{}/download".format(self.gui.app.port) if public_mode: - url = "http://127.0.0.1:{}/download".format(self.gui.app.port) + r = requests.get(url) else: - url = "http://127.0.0.1:{}/{}/download".format(self.gui.app.port, mode.web.password) - r = requests.get(url) + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtTest.QTest.qWait(2000) # Indicator should be visible, have a value of "1" @@ -212,7 +213,7 @@ class GuiBaseTest(object): if public_mode: self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}'.format(self.gui.app.port)) else: - self.assertEqual(clipboard.text(), 'http://127.0.0.1:{}/{}'.format(self.gui.app.port, mode.server_status.web.password)) + self.assertEqual(clipboard.text(), 'http://onionshare:{}@127.0.0.1:{}'.format(mode.server_status.web.password, self.gui.app.port)) def server_status_indicator_says_started(self, mode): @@ -234,8 +235,11 @@ class GuiBaseTest(object): else: path = '/' - http_request = 'GET {} HTTP/1.0\r\n'.format(path) + http_request = 'GET / HTTP/1.0\r\n' http_request += 'Host: 127.0.0.1\r\n' + if not public_mode: + auth = base64.b64encode(b'onionshare:'+password.encode()).decode() + http_request += 'Authorization: Basic {}'.format(auth) http_request += '\r\n' s.sendall(http_request.encode('utf-8')) diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py index 6ecf608c..0d413c4f 100644 --- a/tests/GuiReceiveTest.py +++ b/tests/GuiReceiveTest.py @@ -8,14 +8,14 @@ class GuiReceiveTest(GuiBaseTest): def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False): '''Test that we can upload the file''' files = {'file[]': open(file_to_upload, 'rb')} + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) + r = requests.post(url, files=files) else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) if identical_files_at_once: # Send a duplicate upload to test for collisions - response = requests.post(path, files=files) + r = requests.post(path, files=files) QtTest.QTest.qWait(2000) # Make sure the file is within the last 10 seconds worth of filenames @@ -39,11 +39,11 @@ class GuiReceiveTest(GuiBaseTest): def upload_file_should_fail(self, public_mode): '''Test that we can't upload the file when permissions are wrong, and expected content is shown''' files = {'file[]': open('/tmp/test.txt', 'rb')} + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) + r = requests.post(url, files=files) else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - response = requests.post(path, files=files) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtCore.QTimer.singleShot(1000, self.accept_dialog) self.assertTrue('Error uploading, please inform the OnionShare user' in response.text) @@ -53,17 +53,14 @@ class GuiReceiveTest(GuiBaseTest): os.chmod('/tmp/OnionShare', mode) def try_public_paths_in_non_public_mode(self): - response = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) + r = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) self.assertEqual(response.status_code, 404) - response = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) + r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) self.assertEqual(response.status_code, 404) def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode): '''If you submit the receive mode form without selecting any files, the UI shouldn't get updated''' - if not public_mode: - path = 'http://127.0.0.1:{}/{}/upload'.format(self.gui.app.port, self.gui.receive_mode.web.password) - else: - path = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) # What were the counts before submitting the form? before_in_progress_count = mode.history.in_progress_count @@ -71,9 +68,15 @@ class GuiReceiveTest(GuiBaseTest): before_number_of_history_items = len(mode.history.item_list.items) # Click submit without including any files a few times - response = requests.post(path, files={}) - response = requests.post(path, files={}) - response = requests.post(path, files={}) + if not public_mode: + r = requests.post(url, files={}) + r = requests.post(url, files={}) + r = requests.post(url, files={}) + else: + auth = requests.auth.HTTPBasicAuth('onionshare', mode.web.password) + r = requests.post(url, files={}, auth=auth) + r = requests.post(url, files={}, auth=auth) + r = requests.post(url, files={}, auth=auth) # The counts shouldn't change self.assertEqual(mode.history.in_progress_count, before_in_progress_count) diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py index 02ae0eea..9b0bb70b 100644 --- a/tests/GuiShareTest.py +++ b/tests/GuiShareTest.py @@ -92,13 +92,13 @@ class GuiShareTest(GuiBaseTest): QtTest.QTest.qWait(2000) self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) - def hit_404(self, public_mode): - '''Test that the server stops after too many 404s, or doesn't when in public_mode''' - bogus_path = '/gimme' - url = "http://127.0.0.1:{}/{}".format(self.gui.app.port, bogus_path) + def hit_401(self, public_mode): + '''Test that the server stops after too many 401s, or doesn't when in public_mode''' + url = "http://127.0.0.1:{}/".format(self.gui.app.port) for _ in range(20): - r = requests.get(url) + password_guess = self.gui.common.build_password() + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', password)) # A nasty hack to avoid the Alert dialog that blocks the rest of the test if not public_mode: diff --git a/tests/local_onionshare_404_public_mode_skips_ratelimit_test.py b/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py similarity index 87% rename from tests/local_onionshare_404_public_mode_skips_ratelimit_test.py rename to tests/local_onionshare_401_public_mode_skips_ratelimit_test.py index 4fad5532..f06ea37b 100644 --- a/tests/local_onionshare_404_public_mode_skips_ratelimit_test.py +++ b/tests/local_onionshare_401_public_mode_skips_ratelimit_test.py @@ -4,7 +4,7 @@ import unittest from .GuiShareTest import GuiShareTest -class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): +class Local401PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { @@ -22,7 +22,7 @@ class Local404PublicModeRateLimitTest(unittest.TestCase, GuiShareTest): def test_gui(self): self.run_all_common_setup_tests() self.run_all_share_mode_tests(True, True) - self.hit_404(True) + self.hit_401(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_404_triggers_ratelimit_test.py b/tests/local_onionshare_401_triggers_ratelimit_test.py similarity index 87% rename from tests/local_onionshare_404_triggers_ratelimit_test.py rename to tests/local_onionshare_401_triggers_ratelimit_test.py index 49be0f5b..4100657b 100644 --- a/tests/local_onionshare_404_triggers_ratelimit_test.py +++ b/tests/local_onionshare_401_triggers_ratelimit_test.py @@ -4,7 +4,7 @@ import unittest from .GuiShareTest import GuiShareTest -class Local404RateLimitTest(unittest.TestCase, GuiShareTest): +class Local401RateLimitTest(unittest.TestCase, GuiShareTest): @classmethod def setUpClass(cls): test_settings = { @@ -21,7 +21,7 @@ class Local404RateLimitTest(unittest.TestCase, GuiShareTest): def test_gui(self): self.run_all_common_setup_tests() self.run_all_share_mode_tests(False, True) - self.hit_404(False) + self.hit_401(False) if __name__ == "__main__": unittest.main() From 15d66c1a6f559df09016b5a4805a8c85f6453d66 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 24 May 2019 17:59:04 -0700 Subject: [PATCH 29/33] Fix tests --- tests/GuiBaseTest.py | 52 ++++++------------- tests/GuiReceiveTest.py | 46 +++++++++------- tests/GuiShareTest.py | 29 ++++------- ...ceive_mode_upload_non_writable_dir_test.py | 2 +- ...pload_public_mode_non_writable_dir_test.py | 2 +- ...re_receive_mode_upload_public_mode_test.py | 2 +- ...cal_onionshare_receive_mode_upload_test.py | 2 +- 7 files changed, 56 insertions(+), 79 deletions(-) diff --git a/tests/GuiBaseTest.py b/tests/GuiBaseTest.py index 659ea052..2f340396 100644 --- a/tests/GuiBaseTest.py +++ b/tests/GuiBaseTest.py @@ -2,8 +2,6 @@ import json import os import requests import shutil -import socket -import socks import base64 from PyQt5 import QtCore, QtTest @@ -129,9 +127,9 @@ class GuiBaseTest(object): files = {'file[]': open('/tmp/test.txt', 'rb')} url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) if public_mode: - response = requests.post(url, files=files) + r = requests.post(url, files=files) else: - response = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) QtTest.QTest.qWait(2000) if type(mode) == ShareMode: @@ -186,9 +184,11 @@ class GuiBaseTest(object): def 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) + try: + r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port)) + self.assertTrue(True) + except requests.exceptions.ConnectionError: + self.assertTrue(False) def have_a_password(self, mode, public_mode): @@ -226,34 +226,14 @@ class GuiBaseTest(object): def 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: - path = '/{}'.format(mode.server_status.web.password) + url = "http://127.0.0.1:{}/".format(self.gui.app.port) + if public_mode: + r = requests.get(url) else: - path = '/' + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) - http_request = 'GET / HTTP/1.0\r\n' - http_request += 'Host: 127.0.0.1\r\n' - if not public_mode: - auth = base64.b64encode(b'onionshare:'+password.encode()).decode() - http_request += 'Authorization: Basic {}'.format(auth) - 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() + self.assertTrue(string in r.text) def history_widgets_present(self, mode): @@ -277,10 +257,12 @@ class GuiBaseTest(object): def web_server_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) + try: + r = requests.get('http://127.0.0.1:{}/'.format(self.gui.app.port)) + self.assertTrue(False) + except requests.exceptions.ConnectionError: + self.assertTrue(True) def server_status_indicator_says_closed(self, mode, stay_open): diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py index 0d413c4f..442aa56f 100644 --- a/tests/GuiReceiveTest.py +++ b/tests/GuiReceiveTest.py @@ -7,18 +7,24 @@ from .GuiBaseTest import GuiBaseTest class GuiReceiveTest(GuiBaseTest): def upload_file(self, public_mode, file_to_upload, expected_basename, identical_files_at_once=False): '''Test that we can upload the file''' - files = {'file[]': open(file_to_upload, 'rb')} - url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - if not public_mode: - r = requests.post(url, files=files) - else: - r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) - if identical_files_at_once: - # Send a duplicate upload to test for collisions - r = requests.post(path, files=files) + + # Wait 2 seconds to make sure the filename, based on timestamp, isn't accidentally reused QtTest.QTest.qWait(2000) - # Make sure the file is within the last 10 seconds worth of filenames + files = {'file[]': open(file_to_upload, 'rb')} + url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) + if public_mode: + r = requests.post(url, files=files) + else: + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) + + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files) + + QtTest.QTest.qWait(2000) + + # Make sure the file is within the last 10 seconds worth of fileames exists = False now = datetime.now() for i in range(10): @@ -40,23 +46,23 @@ class GuiReceiveTest(GuiBaseTest): '''Test that we can't upload the file when permissions are wrong, and expected content is shown''' files = {'file[]': open('/tmp/test.txt', 'rb')} url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) - if not public_mode: + if public_mode: r = requests.post(url, files=files) else: - r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', mode.web.password)) + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) QtCore.QTimer.singleShot(1000, self.accept_dialog) - self.assertTrue('Error uploading, please inform the OnionShare user' in response.text) + self.assertTrue('Error uploading, please inform the OnionShare user' in r.text) def upload_dir_permissions(self, mode=0o755): '''Manipulate the permissions on the upload dir in between tests''' os.chmod('/tmp/OnionShare', mode) - def try_public_paths_in_non_public_mode(self): + def try_without_auth_in_non_public_mode(self): r = requests.post('http://127.0.0.1:{}/upload'.format(self.gui.app.port)) - self.assertEqual(response.status_code, 404) + self.assertEqual(r.status_code, 401) r = requests.get('http://127.0.0.1:{}/close'.format(self.gui.app.port)) - self.assertEqual(response.status_code, 404) + self.assertEqual(r.status_code, 401) def uploading_zero_files_shouldnt_change_ui(self, mode, public_mode): '''If you submit the receive mode form without selecting any files, the UI shouldn't get updated''' @@ -68,7 +74,7 @@ class GuiReceiveTest(GuiBaseTest): before_number_of_history_items = len(mode.history.item_list.items) # Click submit without including any files a few times - if not public_mode: + if public_mode: r = requests.post(url, files={}) r = requests.post(url, files={}) r = requests.post(url, files={}) @@ -102,11 +108,11 @@ class GuiReceiveTest(GuiBaseTest): self.server_status_indicator_says_started(self.gui.receive_mode) self.web_page(self.gui.receive_mode, 'Select the files you want to send, then click', public_mode) - def run_all_receive_mode_tests(self, public_mode, receive_allow_receiver_shutdown): + def run_all_receive_mode_tests(self, public_mode): '''Upload files in receive mode and stop the share''' self.run_all_receive_mode_setup_tests(public_mode) if not public_mode: - self.try_public_paths_in_non_public_mode() + self.try_without_auth_in_non_public_mode() self.upload_file(public_mode, '/tmp/test.txt', 'test.txt') self.history_widgets_present(self.gui.receive_mode) self.counter_incremented(self.gui.receive_mode, 1) @@ -128,7 +134,7 @@ class GuiReceiveTest(GuiBaseTest): self.server_is_started(self.gui.receive_mode) self.history_indicator(self.gui.receive_mode, public_mode) - def run_all_receive_mode_unwritable_dir_tests(self, public_mode, receive_allow_receiver_shutdown): + def run_all_receive_mode_unwritable_dir_tests(self, public_mode): '''Attempt to upload (unwritable) files in receive mode and stop the share''' self.run_all_receive_mode_setup_tests(public_mode) self.upload_dir_permissions(0o400) diff --git a/tests/GuiShareTest.py b/tests/GuiShareTest.py index 9b0bb70b..64e57b9f 100644 --- a/tests/GuiShareTest.py +++ b/tests/GuiShareTest.py @@ -2,6 +2,7 @@ import os import requests import socks import zipfile +import tempfile from PyQt5 import QtCore, QtTest from .GuiBaseTest import GuiBaseTest @@ -66,29 +67,17 @@ class GuiShareTest(GuiBaseTest): def 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)) - + url = "http://127.0.0.1:{}/download".format(self.gui.app.port) if public_mode: - path = '/download' + r = requests.get(url) else: - path = '{}/download'.format(self.gui.share_mode.web.password) + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.share_mode.server_status.web.password)) - 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')) + tmp_file = tempfile.NamedTemporaryFile() + with open(tmp_file.name, 'wb') as f: + f.write(r.content) - 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') + zip = zipfile.ZipFile(tmp_file.name) QtTest.QTest.qWait(2000) self.assertEqual('onionshare', zip.read('test.txt').decode('utf-8')) @@ -98,7 +87,7 @@ class GuiShareTest(GuiBaseTest): for _ in range(20): password_guess = self.gui.common.build_password() - r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', password)) + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', password_guess)) # A nasty hack to avoid the Alert dialog that blocks the rest of the test if not public_mode: diff --git a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py index 5737bae3..26feacc3 100644 --- a/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py +++ b/tests/local_onionshare_receive_mode_upload_non_writable_dir_test.py @@ -20,7 +20,7 @@ class LocalReceiveModeUnwritableTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(False, True) + self.run_all_receive_mode_unwritable_dir_tests(False) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py index e6024352..601c4bd2 100644 --- a/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py +++ b/tests/local_onionshare_receive_mode_upload_public_mode_non_writable_dir_test.py @@ -21,7 +21,7 @@ class LocalReceivePublicModeUnwritableTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_unwritable_dir_tests(True, True) + self.run_all_receive_mode_unwritable_dir_tests(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_public_mode_test.py b/tests/local_onionshare_receive_mode_upload_public_mode_test.py index 885ae4fe..1f3a8331 100644 --- a/tests/local_onionshare_receive_mode_upload_public_mode_test.py +++ b/tests/local_onionshare_receive_mode_upload_public_mode_test.py @@ -21,7 +21,7 @@ class LocalReceiveModePublicModeTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(True, True) + self.run_all_receive_mode_tests(True) if __name__ == "__main__": unittest.main() diff --git a/tests/local_onionshare_receive_mode_upload_test.py b/tests/local_onionshare_receive_mode_upload_test.py index 3d23730c..16036893 100644 --- a/tests/local_onionshare_receive_mode_upload_test.py +++ b/tests/local_onionshare_receive_mode_upload_test.py @@ -20,7 +20,7 @@ class LocalReceiveModeTest(unittest.TestCase, GuiReceiveTest): @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") def test_gui(self): self.run_all_common_setup_tests() - self.run_all_receive_mode_tests(False, True) + self.run_all_receive_mode_tests(False) if __name__ == "__main__": unittest.main() From dc556d89f51d94c007fdda9a9e2c859176f12eea Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 24 May 2019 18:07:57 -0700 Subject: [PATCH 30/33] Make GuiReceiveTest.upload_test use basic auth when identical_files_at_once is True --- tests/GuiReceiveTest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/GuiReceiveTest.py b/tests/GuiReceiveTest.py index 442aa56f..c4bfa884 100644 --- a/tests/GuiReceiveTest.py +++ b/tests/GuiReceiveTest.py @@ -15,12 +15,14 @@ class GuiReceiveTest(GuiBaseTest): url = 'http://127.0.0.1:{}/upload'.format(self.gui.app.port) if public_mode: r = requests.post(url, files=files) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files) else: r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) - - if identical_files_at_once: - # Send a duplicate upload to test for collisions - r = requests.post(url, files=files) + if identical_files_at_once: + # Send a duplicate upload to test for collisions + r = requests.post(url, files=files, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.receive_mode.web.password)) QtTest.QTest.qWait(2000) From 50b2311409cd93814324a4570e8bdc5d032748c8 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 29 May 2019 18:21:53 -0700 Subject: [PATCH 31/33] Generate a new static_url_path each time the server is stopped and started again --- onionshare/web/web.py | 18 +++++++++++++----- onionshare_gui/threads.py | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/onionshare/web/web.py b/onionshare/web/web.py index c6e902ed..1e040b54 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -51,16 +51,12 @@ class Web(object): self.common = common self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode)) - # The static URL path has a 128-bit random number in it to avoid having name - # collisions with files that might be getting shared - self.static_url_path = '/static_{}'.format(self.common.random_string(16)) - # The flask app self.app = Flask(__name__, - static_url_path=self.static_url_path, static_folder=self.common.get_resource_path('static'), template_folder=self.common.get_resource_path('templates')) self.app.secret_key = self.common.random_string(8) + self.generate_static_url_path() self.auth = HTTPBasicAuth() self.auth.error_handler(self.error401) @@ -238,6 +234,18 @@ class Web(object): self.password = self.common.build_password() self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password)) + def generate_static_url_path(self): + # The static URL path has a 128-bit random number in it to avoid having name + # collisions with files that might be getting shared + self.static_url_path = '/static_{}'.format(self.common.random_string(16)) + self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path)) + + # Update the flask route to handle the new static URL path + self.app.static_url_path = self.static_url_path + self.app.add_url_rule( + self.static_url_path + '/', + endpoint='static', view_func=self.app.send_static_file) + def verbose_mode(self): """ Turn on verbose mode, which will log flask errors to a file. diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py index bee1b6bc..57e0f0af 100644 --- a/onionshare_gui/threads.py +++ b/onionshare_gui/threads.py @@ -42,6 +42,9 @@ class OnionThread(QtCore.QThread): def run(self): self.mode.common.log('OnionThread', 'run') + # Make a new static URL path for each new share + self.mode.web.generate_static_url_path() + # Choose port and password early, because we need them to exist in advance for scheduled shares self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') if not self.mode.app.port: From c3ba542ecb1aaa3e8b7b5cab4395595e184a860a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 29 May 2019 19:27:21 -0700 Subject: [PATCH 32/33] Strip NoScript XSS warning, because the Tor Browser bug it addressed has been fixed --- onionshare/web/web.py | 9 ------ share/static/img/warning.png | Bin 804 -> 0 bytes share/static/js/receive-noscript.js | 2 -- share/templates/receive.html | 14 --------- share/templates/receive_noscript_xss.html | 35 ---------------------- 5 files changed, 60 deletions(-) delete mode 100644 share/static/img/warning.png delete mode 100644 share/static/js/receive-noscript.js delete mode 100644 share/templates/receive_noscript_xss.html diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 1e040b54..1d2a3fec 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -164,15 +164,6 @@ class Web(object): return "" abort(404) - @self.app.route("/noscript-xss-instructions") - def noscript_xss_instructions(): - """ - Display instructions for disabling Tor Browser's NoScript XSS setting - """ - r = make_response(render_template('receive_noscript_xss.html', - static_url_path=self.static_url_path)) - return self.add_security_headers(r) - def error401(self): auth = request.authorization if auth: diff --git a/share/static/img/warning.png b/share/static/img/warning.png deleted file mode 100644 index 9be8cbaf1a8f5fbbb20c742d51302b22c3eca975..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 804 zcmV+<1Ka$GP)ZAngDE0*OgPK~zYIt(8wm6mb;C zKW}!{c2>j`(zH$2#aI=hLb@b`R5vey6hRPCco0t!Q6SXWAiPG_RfFmfQA7kDqAn$b z2iL8|T|?{-WG?9LtnSY1urAcH|J41$!0_IDzn}Mp-!L*;zt~`IX0D6M5YR;R^h-r2 z(r(wi!>et7@|$MOre0H((JBISY7&*vnoV_p+xbmHRj*bYrMw0n19630!0)n_BKZc; zTE_bVWKdw$sWC4)24g7W-3k8~!b}C@7R)!wxm+2!*_CX64m#Nr_pKPpSyR%FB6X2{ zD1Cra0erh)eSav`9twM-1E}!~!G$BOor%t+bjE#2#OYUIu=-$?2>?gsRM2jp4tt{m zV(80kk6>^;@JXO~Ct$NgFTQT?c@ptPd|)o4s9G}c!;XzOmYxLOZ4EHA6L8Uu6()PN zC9$b4m%7b|U%X2P#E_r;+MgCN&ehrOr*pN7$AI4If+hQ!yOSM@J#pWP>DgG@aLagD z!gb)JeCkVz>W;PrkdPJC0`=Lhg#Sok2QtHV3!v6YEd%xCAYBQ+__;W*SZzTaCZ|uT`ZG2>;mmX^~^4z04GuwQwWAkGi^3o{y~9ZVE-
    - -
    -

    - Warning: Due to a bug in Tor Browser and Firefox, uploads - sometimes never finish. To upload reliably, either set your Tor Browser - security slider - to Standard or - turn off your Tor Browser's NoScript XSS setting.

    -
    -

    Send Files

    @@ -51,7 +38,6 @@
    - diff --git a/share/templates/receive_noscript_xss.html b/share/templates/receive_noscript_xss.html deleted file mode 100644 index 84d35ba1..00000000 --- a/share/templates/receive_noscript_xss.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - OnionShare - - - - - -
    - -

    OnionShare

    -
    - -
    -

    Disable your Tor Browser's NoScript XSS setting

    - -

    If your security slider is set to Safest, JavaScript is disabled so XSS vulnerabilities won't affect you, - which makes it safe to disable NoScript's XSS protections.

    - -

    Here is how to disable this setting:

    - -
      -
    1. Click the menu icon in the top-right of Tor Browser and open "Add-ons"
    2. -
    3. Next to the NoScript add-on, click the "Preferences" button
    4. -
    5. Switch to the "Advanced" tab
    6. -
    7. Uncheck "Sanitize cross-site suspicious requests"
    8. -
    - -

    If you'd like to learn technical details about this issue, check - this issue - on GitHub.

    -
    - - From 726f174dea6a6550a6631e2511ad7a080145975d Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Thu, 30 May 2019 17:55:58 -0700 Subject: [PATCH 33/33] Remove old noscript css styles --- share/static/css/style.css | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/share/static/css/style.css b/share/static/css/style.css index e445e5de..f2ded524 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -222,20 +222,3 @@ li.info { color: #666666; margin: 0 0 20px 0; } - -div#noscript { - text-align: center; - color: #d709df; - padding: 1em; - line-height: 150%; - margin: 0 auto; -} - -div#noscript a, div#noscript a:visited { - color: #d709df; -} - -.disable-noscript-xss-wrapper { - max-width: 900px; - margin: 0 auto; -}