Merge pull request #961 from hiromipaw/static-websites

Static websites
This commit is contained in:
Micah Lee 2019-05-24 10:21:34 -07:00 committed by GitHub
commit 35c373b5a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 689 additions and 22 deletions

View File

@ -9,7 +9,7 @@ VERSION=`cat share/version.txt`
rm -r build dist >/dev/null 2>&1 rm -r build dist >/dev/null 2>&1
# build binary package # 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 # install it
echo "" echo ""

View File

@ -59,6 +59,7 @@ def main():
files_in(dir, 'onionshare_gui/mode') + \ files_in(dir, 'onionshare_gui/mode') + \
files_in(dir, 'onionshare_gui/mode/share_mode') + \ files_in(dir, 'onionshare_gui/mode/share_mode') + \
files_in(dir, 'onionshare_gui/mode/receive_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, 'install/scripts') + \
files_in(dir, 'tests') files_in(dir, 'tests')
pysrc = [p for p in src if p.endswith('.py')] pysrc = [p for p in src if p.endswith('.py')]

View File

@ -3,6 +3,7 @@ certifi==2019.3.9
chardet==3.0.4 chardet==3.0.4
Click==7.0 Click==7.0
Flask==1.0.2 Flask==1.0.2
Flask-HTTPAuth==3.2.4
future==0.17.1 future==0.17.1
idna==2.8 idna==2.8
itsdangerous==1.1.0 itsdangerous==1.1.0

View File

@ -51,6 +51,7 @@ def main(cwd=None):
parser.add_argument('--connect-timeout', metavar='<int>', dest='connect_timeout', default=120, help="Give up connecting to Tor after a given amount of seconds (default: 120)") parser.add_argument('--connect-timeout', metavar='<int>', 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('--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('--receive', action='store_true', dest='receive', help="Receive shares instead of sending them")
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('--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('-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") 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) connect_timeout = int(args.connect_timeout)
stealth = bool(args.stealth) stealth = bool(args.stealth)
receive = bool(args.receive) receive = bool(args.receive)
website = bool(args.website)
config = args.config config = args.config
if receive: if receive:
mode = 'receive' mode = 'receive'
elif website:
mode = 'website'
else: else:
mode = 'share' mode = 'share'
@ -168,6 +172,15 @@ def main(cwd=None):
print(e.args[0]) print(e.args[0])
sys.exit() sys.exit()
if mode == 'website':
# Prepare files to share
print("Preparing files to publish website...")
try:
web.website_mode.set_file_info(filenames)
except OSError as e:
print(e.strerror)
sys.exit(1)
if mode == 'share': if mode == 'share':
# Prepare files to share # Prepare files to share
print("Compressing files.") print("Compressing files.")
@ -206,6 +219,8 @@ def main(cwd=None):
# Build the URL # Build the URL
if common.settings.get('public_mode'): if common.settings.get('public_mode'):
url = 'http://{0:s}'.format(app.onion_host) 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: else:
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
@ -242,7 +257,7 @@ def main(cwd=None):
if app.autostop_timer > 0: if app.autostop_timer > 0:
# if the auto-stop timer was set and has run out, stop the server # if the auto-stop timer was set and has run out, stop the server
if not app.autostop_timer_thread.is_alive(): 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 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: if web.share_mode.download_count == 0 or web.done:
print("Stopped because auto-stop timer ran out") print("Stopped because auto-stop timer ran out")

View File

@ -18,6 +18,9 @@ class ReceiveModeWeb(object):
self.web = web self.web = web
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.can_upload = True self.can_upload = True
self.upload_count = 0 self.upload_count = 0
self.uploads_in_progress = [] self.uploads_in_progress = []

View File

@ -34,6 +34,10 @@ class ShareModeWeb(object):
# one download at a time. # one download at a time.
self.download_in_progress = False self.download_in_progress = False
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
self.define_routes() self.define_routes()
def define_routes(self): def define_routes(self):

View File

@ -15,7 +15,7 @@ from .. import strings
from .share_mode import ShareModeWeb from .share_mode import ShareModeWeb
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest 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 # Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare # are not applicable to OnionShare
@ -111,13 +111,15 @@ class Web(object):
self.receive_mode = None self.receive_mode = None
if self.mode == 'receive': if self.mode == 'receive':
self.receive_mode = ReceiveModeWeb(self.common, self) self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == 'website':
self.website_mode = WebsiteModeWeb(self.common, self)
elif self.mode == 'share': elif self.mode == 'share':
self.share_mode = ShareModeWeb(self.common, self) self.share_mode = ShareModeWeb(self.common, self)
def define_common_routes(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) @self.app.errorhandler(404)
def page_not_found(e): def page_not_found(e):

View File

@ -0,0 +1,202 @@
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()
# Dictionary mapping file paths to filenames on disk
self.files = {}
self.visit_count = 0
# Reset assets path
self.web.app.static_folder=self.common.get_resource_path('static')
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 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('/<path:path>')
def path_public(path):
return path_logic(path)
def path_logic(path=''):
"""
Render the onionshare website.
"""
# 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, path, {
'id': visit_id,
'action': 'visit'
})
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 = []
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)
# 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:
# 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):
"""
Build a data structure that describes the list of files that make up
the static website.
"""
self.common.log("WebsiteModeWeb", "set_file_info")
# 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])]
# Loop through the files
for filename in filenames:
basename = os.path.basename(filename.rstrip('/'))
# 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".
# The normalized_root should be "some_folder/foobar"
normalized_root = os.path.join(basename, root[len(filename):].lstrip('/')).rstrip('/')
# Add the dir itself
self.files[normalized_root + '/'] = root
# 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

View File

@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from ...widgets import Alert, AddFileDialog from ..widgets import Alert, AddFileDialog
class DropHereLabel(QtWidgets.QLabel): class DropHereLabel(QtWidgets.QLabel):
""" """

View File

@ -341,6 +341,35 @@ class ReceiveHistoryItem(HistoryItem):
self.label.setText(self.get_canceled_label_text(self.started)) 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.status = HistoryItem.STATUS_STARTED
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.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): class HistoryItemList(QtWidgets.QScrollArea):
""" """
List of items List of items
@ -404,7 +433,6 @@ class HistoryItemList(QtWidgets.QScrollArea):
Reset all items, emptying the list. Override this method. Reset all items, emptying the list. Override this method.
""" """
for key, item in self.items.copy().items(): for key, item in self.items.copy().items():
if item.status != HistoryItem.STATUS_STARTED:
self.items_layout.removeWidget(item) self.items_layout.removeWidget(item)
item.close() item.close()
del self.items[key] del self.items[key]
@ -414,9 +442,10 @@ class History(QtWidgets.QWidget):
A history of what's happened so far in this mode. This contains an internal A history of what's happened so far in this mode. This contains an internal
object full of a scrollable list of items. 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__() super(History, self).__init__()
self.common = common self.common = common
self.mode = mode
self.setMinimumWidth(350) self.setMinimumWidth(350)
@ -535,10 +564,12 @@ class History(QtWidgets.QWidget):
""" """
Update the 'in progress' widget. Update the 'in progress' widget.
""" """
if self.mode != 'website':
if self.in_progress_count == 0: if self.in_progress_count == 0:
image = self.common.get_resource_path('images/share_in_progress_none.png') image = self.common.get_resource_path('images/share_in_progress_none.png')
else: else:
image = self.common.get_resource_path('images/share_in_progress.png') image = self.common.get_resource_path('images/share_in_progress.png')
self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count)) self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count))
self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count)) self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count))

View File

@ -25,7 +25,7 @@ from onionshare.onion import *
from onionshare.common import Common from onionshare.common import Common
from onionshare.web import Web from onionshare.web import Web
from .file_selection import FileSelection from ..file_selection import FileSelection
from .threads import CompressThread from .threads import CompressThread
from .. import Mode from .. import Mode
from ..history import History, ToggleHistory, ShareHistoryItem from ..history import History, ToggleHistory, ShareHistoryItem

View File

@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
import os
import random
import string
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, VisitHistoryItem
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.
"""
# 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/share_icon_transparent.png'))),
strings._('gui_website_mode_no_files'),
strings._('gui_all_modes_history'),
'website'
)
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/share_icon_toggle.png')),
QtGui.QIcon(self.common.get_resource_path('images/share_icon_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, stretch=1)
self.setLayout(self.wrapper_layout)
# Always start with focus on file selection
self.file_selection.setFocus()
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._('gui_share_stop_server_autostop_timer')
def autostop_timer_finished_should_stop_server(self):
"""
The auto-stop timer expired, should we stop the server? Returns a bool
"""
self.server_status.stop_server()
self.server_status_label.setText(strings._('close_on_autostop_timer'))
return True
def start_server_custom(self):
"""
Starting the server.
"""
# Reset web counters
self.web.website_mode.visit_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.
"""
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.completed_count = 0
self.file_selection.file_list.adjustSize()
def cancel_server_custom(self):
"""
Log that the server has been cancelled
"""
self.common.log('WebsiteMode', 'cancel_server')
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_site_loaded_title'), strings._('systray_site_loaded_message'))
def handle_request_started(self, event):
"""
Handle REQUEST_STARTED event.
"""
if ( (event["path"] == '') or (event["path"].find(".htm") != -1 ) ):
item = VisitHistoryItem(self.common, event["data"]["id"], 0)
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'))
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

View File

@ -25,6 +25,7 @@ from onionshare.web import Web
from .mode.share_mode import ShareMode from .mode.share_mode import ShareMode
from .mode.receive_mode import ReceiveMode from .mode.receive_mode import ReceiveMode
from .mode.website_mode import WebsiteMode
from .tor_connection_dialog import TorConnectionDialog from .tor_connection_dialog import TorConnectionDialog
from .settings_dialog import SettingsDialog from .settings_dialog import SettingsDialog
@ -39,6 +40,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
""" """
MODE_SHARE = 'share' MODE_SHARE = 'share'
MODE_RECEIVE = 'receive' MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False): def __init__(self, common, onion, qtapp, app, filenames, config=False, local_only=False):
super(OnionShareGui, self).__init__() 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 = QtWidgets.QPushButton(strings._('gui_mode_receive_button'));
self.receive_mode_button.setFixedHeight(50) self.receive_mode_button.setFixedHeight(50)
self.receive_mode_button.clicked.connect(self.receive_mode_clicked) 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 = QtWidgets.QPushButton()
self.settings_button.setDefault(False) self.settings_button.setDefault(False)
self.settings_button.setFixedWidth(40) self.settings_button.setFixedWidth(40)
@ -103,6 +108,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
mode_switcher_layout.setSpacing(0) mode_switcher_layout.setSpacing(0)
mode_switcher_layout.addWidget(self.share_mode_button) mode_switcher_layout.addWidget(self.share_mode_button)
mode_switcher_layout.addWidget(self.receive_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) mode_switcher_layout.addWidget(self.settings_button)
# Server status indicator on the status bar # 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.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.receive_mode.set_server_active.connect(self.set_server_active) 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_mode_switcher()
self.update_server_status_indicator() self.update_server_status_indicator()
@ -162,6 +182,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
contents_layout.setContentsMargins(10, 0, 10, 0) contents_layout.setContentsMargins(10, 0, 10, 0)
contents_layout.addWidget(self.receive_mode) contents_layout.addWidget(self.receive_mode)
contents_layout.addWidget(self.share_mode) contents_layout.addWidget(self.share_mode)
contents_layout.addWidget(self.website_mode)
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
@ -199,15 +220,27 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.mode == self.MODE_SHARE: if self.mode == self.MODE_SHARE:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) 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.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.receive_mode.hide()
self.share_mode.show() 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: else:
self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) 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.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.share_mode.hide()
self.receive_mode.show() self.receive_mode.show()
self.website_mode.hide()
self.update_server_status_indicator() self.update_server_status_indicator()
@ -223,6 +256,12 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.mode = self.MODE_RECEIVE self.mode = self.MODE_RECEIVE
self.update_mode_switcher() 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): def update_server_status_indicator(self):
# Set the status image # Set the status image
if self.mode == self.MODE_SHARE: if self.mode == self.MODE_SHARE:
@ -239,6 +278,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: 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_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
self.server_status_label.setText(strings._('gui_status_indicator_share_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: else:
# Receive mode # Receive mode
if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED:
@ -317,18 +367,22 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.timer.start(500) self.timer.start(500)
self.share_mode.on_reload_settings() self.share_mode.on_reload_settings()
self.receive_mode.on_reload_settings() self.receive_mode.on_reload_settings()
self.website_mode.on_reload_settings()
self.status_bar.clearMessage() self.status_bar.clearMessage()
# If we switched off the auto-stop timer setting, ensure the widget is hidden. # If we switched off the auto-stop timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostop_timer'): if not self.common.settings.get('autostop_timer'):
self.share_mode.server_status.autostop_timer_container.hide() self.share_mode.server_status.autostop_timer_container.hide()
self.receive_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 we switched off the auto-start timer setting, ensure the widget is hidden.
if not self.common.settings.get('autostart_timer'): if not self.common.settings.get('autostart_timer'):
self.share_mode.server_status.autostart_timer_datetime = None self.share_mode.server_status.autostart_timer_datetime = None
self.receive_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.share_mode.server_status.autostart_timer_container.hide()
self.receive_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 = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings) d.settings_saved.connect(reload_settings)
@ -337,6 +391,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
# When settings close, refresh the server status UI # When settings close, refresh the server status UI
self.share_mode.server_status.update() self.share_mode.server_status.update()
self.receive_mode.server_status.update() self.receive_mode.server_status.update()
self.website_mode.server_status.update()
def check_for_updates(self): def check_for_updates(self):
""" """
@ -367,10 +422,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.share_mode.handle_tor_broke() self.share_mode.handle_tor_broke()
self.receive_mode.handle_tor_broke() self.receive_mode.handle_tor_broke()
self.website_mode.handle_tor_broke()
# Process events from the web object # Process events from the web object
if self.mode == self.MODE_SHARE: if self.mode == self.MODE_SHARE:
mode = self.share_mode mode = self.share_mode
elif self.mode == self.MODE_WEBSITE:
mode = self.website_mode
else: else:
mode = self.receive_mode mode = self.receive_mode
@ -450,13 +508,20 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.mode == self.MODE_SHARE: if self.mode == self.MODE_SHARE:
self.share_mode_button.show() self.share_mode_button.show()
self.receive_mode_button.hide() 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: else:
self.share_mode_button.hide() self.share_mode_button.hide()
self.receive_mode_button.show() self.receive_mode_button.show()
self.website_mode_button.hide()
else: else:
self.settings_button.show() self.settings_button.show()
self.share_mode_button.show() self.share_mode_button.show()
self.receive_mode_button.show() self.receive_mode_button.show()
self.website_mode_button.show()
# Disable settings menu action when server is active # Disable settings menu action when server is active
self.settings_action.setEnabled(not active) self.settings_action.setEnabled(not active)
@ -466,6 +531,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
try: try:
if self.mode == OnionShareGui.MODE_SHARE: if self.mode == OnionShareGui.MODE_SHARE:
server_status = self.share_mode.server_status server_status = self.share_mode.server_status
if self.mode == OnionShareGui.MODE_WEBSITE:
server_status = self.website_mode.server_status
else: else:
server_status = self.receive_mode.server_status server_status = self.receive_mode.server_status
if server_status.status != server_status.STATUS_STOPPED: if server_status.status != server_status.STATUS_STOPPED:

View File

@ -39,6 +39,7 @@ class ServerStatus(QtWidgets.QWidget):
MODE_SHARE = 'share' MODE_SHARE = 'share'
MODE_RECEIVE = 'receive' MODE_RECEIVE = 'receive'
MODE_WEBSITE = 'website'
STATUS_STOPPED = 0 STATUS_STOPPED = 0
STATUS_WORKING = 1 STATUS_WORKING = 1
@ -159,7 +160,7 @@ class ServerStatus(QtWidgets.QWidget):
""" """
self.mode = share_mode 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.file_selection = file_selection
self.update() self.update()
@ -207,6 +208,8 @@ class ServerStatus(QtWidgets.QWidget):
if self.mode == ServerStatus.MODE_SHARE: if self.mode == ServerStatus.MODE_SHARE:
self.url_description.setText(strings._('gui_share_url_description').format(info_image)) 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: else:
self.url_description.setText(strings._('gui_receive_url_description').format(info_image)) self.url_description.setText(strings._('gui_receive_url_description').format(info_image))
@ -258,6 +261,8 @@ class ServerStatus(QtWidgets.QWidget):
# Button # Button
if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0: if self.mode == ServerStatus.MODE_SHARE and self.file_selection.get_num_files() == 0:
self.server_button.hide() self.server_button.hide()
elif self.mode == ServerStatus.MODE_WEBSITE and self.file_selection.get_num_files() == 0:
self.server_button.hide()
else: else:
self.server_button.show() self.server_button.show()
@ -266,6 +271,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True) self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE: if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_start_server')) 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: else:
self.server_button.setText(strings._('gui_receive_start_server')) self.server_button.setText(strings._('gui_receive_start_server'))
self.server_button.setToolTip('') self.server_button.setToolTip('')
@ -278,6 +285,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setEnabled(True) self.server_button.setEnabled(True)
if self.mode == ServerStatus.MODE_SHARE: if self.mode == ServerStatus.MODE_SHARE:
self.server_button.setText(strings._('gui_share_stop_server')) 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: else:
self.server_button.setText(strings._('gui_receive_stop_server')) self.server_button.setText(strings._('gui_receive_stop_server'))
if self.common.settings.get('autostart_timer'): if self.common.settings.get('autostart_timer'):
@ -411,6 +420,8 @@ class ServerStatus(QtWidgets.QWidget):
""" """
if self.common.settings.get('public_mode'): if self.common.settings.get('public_mode'):
url = 'http://{0:s}'.format(self.app.onion_host) 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: else:
url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)
return url return url

View File

@ -88,7 +88,8 @@ setup(
'onionshare_gui', 'onionshare_gui',
'onionshare_gui.mode', 'onionshare_gui.mode',
'onionshare_gui.mode.share_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, include_package_data=True,
scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'], scripts=['install/scripts/onionshare', 'install/scripts/onionshare-gui'],

View File

@ -114,6 +114,7 @@
"gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses",
"gui_save_private_key_checkbox": "Use a persistent address", "gui_save_private_key_checkbox": "Use a persistent address",
"gui_share_url_description": "<b>Anyone</b> with this OnionShare address can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />", "gui_share_url_description": "<b>Anyone</b> with this OnionShare address can <b>download</b> your files using the <b>Tor Browser</b>: <img src='{}' />",
"gui_website_url_description": "<b>Anyone</b> with this OnionShare address can <b>visit</b> your website using the <b>Tor Browser</b>: <img src='{}' />",
"gui_receive_url_description": "<b>Anyone</b> with this OnionShare address can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />", "gui_receive_url_description": "<b>Anyone</b> with this OnionShare address can <b>upload</b> files to your computer using the <b>Tor Browser</b>: <img src='{}' />",
"gui_url_label_persistent": "This share will not auto-stop.<br><br>Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)", "gui_url_label_persistent": "This share will not auto-stop.<br><br>Every subsequent share reuses the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
"gui_url_label_stay_open": "This share will not auto-stop.", "gui_url_label_stay_open": "This share will not auto-stop.",
@ -135,6 +136,7 @@
"gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>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.</b>", "gui_receive_mode_warning": "Receive mode lets people upload files to your computer.<br><br><b>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.</b>",
"gui_mode_share_button": "Share Files", "gui_mode_share_button": "Share Files",
"gui_mode_receive_button": "Receive Files", "gui_mode_receive_button": "Receive Files",
"gui_mode_website_button": "Publish Website",
"gui_settings_receiving_label": "Receiving settings", "gui_settings_receiving_label": "Receiving settings",
"gui_settings_data_dir_label": "Save files to", "gui_settings_data_dir_label": "Save files to",
"gui_settings_data_dir_browse_button": "Browse", "gui_settings_data_dir_browse_button": "Browse",
@ -145,6 +147,8 @@
"systray_menu_exit": "Quit", "systray_menu_exit": "Quit",
"systray_page_loaded_title": "Page Loaded", "systray_page_loaded_title": "Page Loaded",
"systray_page_loaded_message": "OnionShare address 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_title": "Sharing Started",
"systray_share_started_message": "Starting to send files to someone", "systray_share_started_message": "Starting to send files to someone",
"systray_share_completed_title": "Sharing Complete", "systray_share_completed_title": "Sharing Complete",
@ -153,6 +157,8 @@
"systray_share_canceled_message": "Someone canceled receiving your files", "systray_share_canceled_message": "Someone canceled receiving your files",
"systray_receive_started_title": "Receiving Started", "systray_receive_started_title": "Receiving Started",
"systray_receive_started_message": "Someone is sending files to you", "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_history": "History",
"gui_all_modes_clear_history": "Clear All", "gui_all_modes_clear_history": "Clear All",
"gui_all_modes_transfer_started": "Started {}", "gui_all_modes_transfer_started": "Started {}",
@ -165,8 +171,10 @@
"gui_all_modes_progress_eta": "{0:s}, ETA: {1:s}, %p%", "gui_all_modes_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"gui_share_mode_no_files": "No Files Sent Yet", "gui_share_mode_no_files": "No Files Sent Yet",
"gui_share_mode_autostop_timer_waiting": "Waiting to finish sending", "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_no_files": "No Files Received Yet",
"gui_receive_mode_autostop_timer_waiting": "Waiting to finish receiving", "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", "receive_mode_upload_starting": "Upload of total size {} is starting",
"days_first_letter": "d", "days_first_letter": "d",
"hours_first_letter": "h", "hours_first_letter": "h",

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
<link href="/static/css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header class="clearfix">
<img class="logo" src="/static/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<table class="file-list" id="file-list">
<tr>
<th id="filename-header">Filename</th>
<th id="size-header">Size</th>
<th></th>
</tr>
{% for info in dirs %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="/static/img/web_folder.png" />
<a href="{{ info.basename }}">
{{ info.basename }}
</a>
</td>
<td>&mdash;</td>
</tr>
{% endfor %}
{% for info in files %}
<tr>
<td>
<img width="30" height="30" title="" alt="" src="/static/img/web_file.png" />
<a href="{{ info.basename }}">
{{ info.basename }}
</a>
</td>
<td>{{ info.size_human }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -1,6 +1,6 @@
[DEFAULT] [DEFAULT]
Package3: onionshare Package3: onionshare
Depends3: python3, python3-flask, python3-stem, python3-pyqt5, python3-crypto, python3-socks, 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-stem, python3-pyqt5, python3-crypto, python3-socks, python3-requests, 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 Suite: cosmic
X-Python3-Version: >= 3.5.3 X-Python3-Version: >= 3.5.3