From 2d43588a3bfd6afee8bcc4239a3259870bfc504b Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 19 Apr 2019 14:25:42 +0200 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 f56b148ddb4d922c595400226263bea6d2a97fe8 Mon Sep 17 00:00:00 2001 From: hiro Date: Fri, 24 May 2019 10:08:51 +0200 Subject: [PATCH 11/11] 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_()