summaryrefslogtreecommitdiff
path: root/onionshare_gui
diff options
context:
space:
mode:
authorMiguel Jacq <mig@mig5.net>2019-03-05 10:28:27 +1100
committerMiguel Jacq <mig@mig5.net>2019-03-05 10:28:27 +1100
commit31c360b44d411fc9507eeb5815728a26af6781cf (patch)
tree5ecc6d80b80989fdbd064611d7b5bfd9742f359a /onionshare_gui
parent3af05dcc2041026380a71b8aed1af9af849416a5 (diff)
downloadonionshare-31c360b44d411fc9507eeb5815728a26af6781cf.tar.gz
onionshare-31c360b44d411fc9507eeb5815728a26af6781cf.zip
Add a Startup Timer feature (scheduled start / dead man's switch)
Diffstat (limited to 'onionshare_gui')
-rw-r--r--onionshare_gui/mode/__init__.py52
-rw-r--r--onionshare_gui/onionshare_gui.py14
-rw-r--r--onionshare_gui/server_status.py70
-rw-r--r--onionshare_gui/settings_dialog.py25
-rw-r--r--onionshare_gui/threads.py69
5 files changed, 210 insertions, 20 deletions
diff --git a/onionshare_gui/mode/__init__.py b/onionshare_gui/mode/__init__.py
index 4fe335e7..d4f0cd09 100644
--- a/onionshare_gui/mode/__init__.py
+++ b/onionshare_gui/mode/__init__.py
@@ -24,6 +24,7 @@ from onionshare.common import ShutdownTimer
from ..server_status import ServerStatus
from ..threads import OnionThread
+from ..threads import StartupTimer
from ..widgets import Alert
class Mode(QtWidgets.QWidget):
@@ -35,6 +36,7 @@ class Mode(QtWidgets.QWidget):
starting_server_step2 = QtCore.pyqtSignal()
starting_server_step3 = QtCore.pyqtSignal()
starting_server_error = QtCore.pyqtSignal(str)
+ starting_server_early = QtCore.pyqtSignal()
set_server_active = QtCore.pyqtSignal(bool)
def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False):
@@ -58,6 +60,7 @@ class Mode(QtWidgets.QWidget):
# Threads start out as None
self.onion_thread = None
self.web_thread = None
+ self.startup_thread = None
# Server status
self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only)
@@ -68,6 +71,7 @@ class Mode(QtWidgets.QWidget):
self.stop_server_finished.connect(self.server_status.stop_server_finished)
self.starting_server_step2.connect(self.start_server_step2)
self.starting_server_step3.connect(self.start_server_step3)
+ self.starting_server_early.connect(self.start_server_early)
self.starting_server_error.connect(self.start_server_error)
# Primary action
@@ -142,7 +146,41 @@ class Mode(QtWidgets.QWidget):
self.status_bar.clearMessage()
self.server_status_label.setText('')
+ # Ensure we always get a new random port each time we might launch an OnionThread
+ self.app.port = None
+
+ # Start the onion thread. If this share was scheduled for a future date,
+ # the OnionThread will start and exit 'early' to obtain the port, slug
+ # and onion address, but it will not start the WebThread yet.
+ if self.server_status.scheduled_start:
+ self.start_onion_thread(obtain_onion_early=True)
+ else:
+ self.start_onion_thread()
+
+ # If scheduling a share, delay starting the real share
+ if self.server_status.scheduled_start:
+ self.common.log('Mode', 'start_server', 'Starting startup timer')
+ self.startup_thread = StartupTimer(self)
+ # Once the timer has finished, start the real share, with a WebThread
+ self.startup_thread.success.connect(self.start_scheduled_service)
+ self.startup_thread.error.connect(self.start_server_error)
+ self.startup_thread.canceled = False
+ self.startup_thread.start()
+
+ def start_onion_thread(self, obtain_onion_early=False):
self.common.log('Mode', 'start_server', 'Starting an onion thread')
+ self.obtain_onion_early = obtain_onion_early
+ self.onion_thread = OnionThread(self)
+ self.onion_thread.success.connect(self.starting_server_step2.emit)
+ self.onion_thread.success_early.connect(self.starting_server_early.emit)
+ self.onion_thread.error.connect(self.starting_server_error.emit)
+ self.onion_thread.start()
+
+ def start_scheduled_service(self, obtain_onion_early=False):
+ # We start a new OnionThread with the saved scheduled key from settings
+ self.common.settings.load()
+ self.obtain_onion_early = obtain_onion_early
+ self.common.log('Mode', 'start_server', 'Starting a scheduled onion thread')
self.onion_thread = OnionThread(self)
self.onion_thread.success.connect(self.starting_server_step2.emit)
self.onion_thread.error.connect(self.starting_server_error.emit)
@@ -154,6 +192,14 @@ class Mode(QtWidgets.QWidget):
"""
pass
+ def start_server_early(self):
+ """
+ An 'early' start of an onion service in order to obtain the onion
+ address for a scheduled start. Shows the onion address in the UI
+ in advance of actually starting the share.
+ """
+ self.server_status.show_url()
+
def start_server_step2(self):
"""
Step 2 in starting the onionshare server.
@@ -225,7 +271,11 @@ class Mode(QtWidgets.QWidget):
Cancel the server while it is preparing to start
"""
self.cancel_server_custom()
-
+ if self.startup_thread:
+ self.common.log('Mode', 'cancel_server: quitting startup thread')
+ self.startup_thread.canceled = True
+ self.app.onion.scheduled_key = None
+ self.startup_thread.quit()
if self.onion_thread:
self.common.log('Mode', 'cancel_server: quitting onion thread')
self.onion_thread.quit()
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index 27abf5e5..8e4a3338 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -228,7 +228,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status_label.setText(strings._('gui_status_indicator_share_stopped'))
elif self.share_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'))
+ if self.share_mode.server_status.scheduled_start:
+ self.server_status_label.setText(strings._('gui_status_indicator_share_scheduled'))
+ else:
+ self.server_status_label.setText(strings._('gui_status_indicator_share_working'))
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'))
@@ -239,7 +242,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped'))
elif self.receive_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_receive_working'))
+ if self.receive_mode.server_status.scheduled_start:
+ self.server_status_label.setText(strings._('gui_status_indicator_receive_scheduled'))
+ else:
+ self.server_status_label.setText(strings._('gui_status_indicator_receive_working'))
elif self.receive_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_receive_started'))
@@ -313,6 +319,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
if not self.common.settings.get('shutdown_timeout'):
self.share_mode.server_status.shutdown_timeout_container.hide()
self.receive_mode.server_status.shutdown_timeout_container.hide()
+ # If we switched off the startup timer setting, ensure the widget is hidden.
+ if not self.common.settings.get('startup_timer'):
+ self.share_mode.server_status.startup_timer_container.hide()
+ self.receive_mode.server_status.startup_timer_container.hide()
d = SettingsDialog(self.common, self.onion, self.qtapp, self.config, self.local_only)
d.settings_saved.connect(reload_settings)
diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py
index e34a3d16..4bd9241b 100644
--- a/onionshare_gui/server_status.py
+++ b/onionshare_gui/server_status.py
@@ -56,10 +56,36 @@ class ServerStatus(QtWidgets.QWidget):
self.app = app
self.web = None
+ self.scheduled_start = None
self.local_only = local_only
self.resizeEvent(None)
+ # Startup timer layout
+ self.startup_timer_label = QtWidgets.QLabel(strings._('gui_settings_startup_timer'))
+ self.startup_timer = QtWidgets.QDateTimeEdit()
+ self.startup_timer.setDisplayFormat("hh:mm A MMM d, yy")
+ if self.local_only:
+ # For testing
+ self.startup_timer.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15))
+ self.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime())
+ else:
+ # Set proposed timer to be 5 minutes into the future
+ self.startup_timer.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now
+ self.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
+ self.startup_timer.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
+ startup_timer_layout = QtWidgets.QHBoxLayout()
+ startup_timer_layout.addWidget(self.startup_timer_label)
+ startup_timer_layout.addWidget(self.startup_timer)
+
+ # Startup timer container, so it can all be hidden and shown as a group
+ startup_timer_container_layout = QtWidgets.QVBoxLayout()
+ startup_timer_container_layout.addLayout(startup_timer_layout)
+ self.startup_timer_container = QtWidgets.QWidget()
+ self.startup_timer_container.setLayout(startup_timer_container_layout)
+ self.startup_timer_container.hide()
+
# Shutdown timeout layout
self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout'))
self.shutdown_timeout = QtWidgets.QDateTimeEdit()
@@ -123,6 +149,7 @@ class ServerStatus(QtWidgets.QWidget):
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.server_button)
layout.addLayout(url_layout)
+ layout.addWidget(self.startup_timer_container)
layout.addWidget(self.shutdown_timeout_container)
self.setLayout(layout)
@@ -154,6 +181,13 @@ class ServerStatus(QtWidgets.QWidget):
except:
pass
+ def startup_timer_reset(self):
+ """
+ Reset the timer in the UI after stopping a share
+ """
+ self.startup_timer.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ if not self.local_only:
+ self.startup_timer.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
def shutdown_timeout_reset(self):
"""
@@ -163,6 +197,14 @@ class ServerStatus(QtWidgets.QWidget):
if not self.local_only:
self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60))
+ def show_url(self):
+ """
+ Show the URL in the UI.
+ """
+ self.url.setText(self.get_url())
+ self.url.show()
+ self.copy_url_button.show()
+
def update(self):
"""
Update the GUI elements based on the current state.
@@ -190,16 +232,16 @@ class ServerStatus(QtWidgets.QWidget):
else:
self.url_description.setToolTip(strings._('gui_url_label_stay_open'))
- self.url.setText(self.get_url())
- self.url.show()
-
- self.copy_url_button.show()
+ self.show_url()
if self.common.settings.get('save_private_key'):
if not self.common.settings.get('slug'):
self.common.settings.set('slug', self.web.slug)
self.common.settings.save()
+ if self.common.settings.get('startup_timer'):
+ self.startup_timer_container.hide()
+
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
@@ -227,6 +269,8 @@ class ServerStatus(QtWidgets.QWidget):
else:
self.server_button.setText(strings._('gui_receive_start_server'))
self.server_button.setToolTip('')
+ if self.common.settings.get('startup_timer'):
+ self.startup_timer_container.show()
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
@@ -236,23 +280,30 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setText(strings._('gui_share_stop_server'))
else:
self.server_button.setText(strings._('gui_receive_stop_server'))
+ if self.common.settings.get('startup_timer'):
+ self.startup_timer_container.hide()
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))
else:
self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout))
-
elif self.status == self.STATUS_WORKING:
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
self.server_button.setEnabled(True)
- self.server_button.setText(strings._('gui_please_wait'))
+ if self.scheduled_start:
+ self.server_button.setText(strings._('gui_waiting_to_start').format(self.scheduled_start))
+ self.startup_timer_container.hide()
+ else:
+ self.server_button.setText(strings._('gui_please_wait'))
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
else:
self.server_button.setStyleSheet(self.common.css['server_status_button_working'])
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
+ if self.common.settings.get('startup_timer'):
+ self.startup_timer_container.hide()
if self.common.settings.get('shutdown_timeout'):
self.shutdown_timeout_container.hide()
@@ -261,6 +312,11 @@ class ServerStatus(QtWidgets.QWidget):
Toggle starting or stopping the server.
"""
if self.status == self.STATUS_STOPPED:
+ if self.common.settings.get('startup_timer'):
+ if self.local_only:
+ self.scheduled_start = self.startup_timer.dateTime().toPyDateTime()
+ else:
+ self.scheduled_start = self.startup_timer.dateTime().toPyDateTime().replace(second=0, microsecond=0)
if self.common.settings.get('shutdown_timeout'):
if self.local_only:
self.timeout = self.shutdown_timeout.dateTime().toPyDateTime()
@@ -302,6 +358,7 @@ class ServerStatus(QtWidgets.QWidget):
Stop the server.
"""
self.status = self.STATUS_WORKING
+ self.startup_timer_reset()
self.shutdown_timeout_reset()
self.update()
self.server_stopped.emit()
@@ -312,6 +369,7 @@ class ServerStatus(QtWidgets.QWidget):
"""
self.common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
self.status = self.STATUS_WORKING
+ self.startup_timer_reset()
self.shutdown_timeout_reset()
self.update()
self.server_canceled.emit()
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index 2933784c..f29915a7 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -71,6 +71,23 @@ class SettingsDialog(QtWidgets.QDialog):
self.public_mode_widget = QtWidgets.QWidget()
self.public_mode_widget.setLayout(public_mode_layout)
+ # Whether or not to use a startup ('auto-start') timer
+ self.startup_timer_checkbox = QtWidgets.QCheckBox()
+ self.startup_timer_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.startup_timer_checkbox.setText(strings._("gui_settings_startup_timer_checkbox"))
+ startup_timer_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer"))
+ startup_timer_label.setStyleSheet(self.common.css['settings_whats_this'])
+ startup_timer_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ startup_timer_label.setOpenExternalLinks(True)
+ startup_timer_label.setMinimumSize(public_mode_label.sizeHint())
+ startup_timer_layout = QtWidgets.QHBoxLayout()
+ startup_timer_layout.addWidget(self.startup_timer_checkbox)
+ startup_timer_layout.addWidget(startup_timer_label)
+ startup_timer_layout.addStretch()
+ startup_timer_layout.setContentsMargins(0,0,0,0)
+ self.startup_timer_widget = QtWidgets.QWidget()
+ self.startup_timer_widget.setLayout(startup_timer_layout)
+
# Whether or not to use a shutdown ('auto-stop') timer
self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
@@ -91,6 +108,7 @@ class SettingsDialog(QtWidgets.QDialog):
# General settings layout
general_group_layout = QtWidgets.QVBoxLayout()
general_group_layout.addWidget(self.public_mode_widget)
+ general_group_layout.addWidget(self.startup_timer_widget)
general_group_layout.addWidget(self.shutdown_timeout_widget)
general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label"))
general_group.setLayout(general_group_layout)
@@ -488,6 +506,12 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ startup_timer = self.old_settings.get('startup_timer')
+ if startup_timer:
+ self.startup_timer_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.startup_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
shutdown_timeout = self.old_settings.get('shutdown_timeout')
if shutdown_timeout:
self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
@@ -932,6 +956,7 @@ class SettingsDialog(QtWidgets.QDialog):
settings.load() # To get the last update timestamp
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
+ settings.set('startup_timer', self.startup_timer_checkbox.isChecked())
settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
# Complicated logic here to force v2 onion mode on or off depending on other settings
diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py
index 3b05bebf..fff56bc2 100644
--- a/onionshare_gui/threads.py
+++ b/onionshare_gui/threads.py
@@ -28,6 +28,7 @@ class OnionThread(QtCore.QThread):
Starts the onion service, and waits for it to finish
"""
success = QtCore.pyqtSignal()
+ success_early = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def __init__(self, mode):
@@ -41,18 +42,30 @@ class OnionThread(QtCore.QThread):
def run(self):
self.mode.common.log('OnionThread', 'run')
+ # Choose port and slug early, because we need them to exist in advance for scheduled shares
self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download')
-
- # start onionshare http service in new thread
- self.mode.web_thread = WebThread(self.mode)
- self.mode.web_thread.start()
-
- # wait for modules in thread to load, preventing a thread-related cx_Freeze crash
- time.sleep(0.2)
+ if not self.mode.app.port:
+ self.mode.app.choose_port()
+ if not self.mode.common.settings.get('public_mode'):
+ if not self.mode.web.slug:
+ self.mode.web.generate_slug(self.mode.common.settings.get('slug'))
try:
- self.mode.app.start_onion_service()
- self.success.emit()
+ if self.mode.obtain_onion_early:
+ self.mode.app.start_onion_service(await_publication=False, save_scheduled_key=True)
+ # wait for modules in thread to load, preventing a thread-related cx_Freeze crash
+ time.sleep(0.2)
+ self.success_early.emit()
+ # Unregister the onion so we can use it in the next OnionThread
+ self.mode.app.onion.cleanup()
+ else:
+ self.mode.app.start_onion_service(await_publication=True)
+ # wait for modules in thread to load, preventing a thread-related cx_Freeze crash
+ time.sleep(0.2)
+ # start onionshare http service in new thread
+ self.mode.web_thread = WebThread(self.mode)
+ self.mode.web_thread.start()
+ self.success.emit()
except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e:
self.error.emit(e.args[0])
@@ -73,5 +86,39 @@ class WebThread(QtCore.QThread):
def run(self):
self.mode.common.log('WebThread', 'run')
- self.mode.app.choose_port()
- self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug'))
+ self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.web.slug)
+ self.success.emit()
+
+
+class StartupTimer(QtCore.QThread):
+ """
+ Waits for a prescribed time before allowing a share to start
+ """
+ success = QtCore.pyqtSignal()
+ error = QtCore.pyqtSignal(str)
+ def __init__(self, mode, canceled=False):
+ super(StartupTimer, self).__init__()
+ self.mode = mode
+ self.canceled = canceled
+ self.mode.common.log('StartupTimer', '__init__')
+
+ # allow this thread to be terminated
+ self.setTerminationEnabled()
+
+ def run(self):
+ now = QtCore.QDateTime.currentDateTime()
+ scheduled_start = now.secsTo(self.mode.server_status.scheduled_start)
+ try:
+ # Sleep until scheduled time
+ while scheduled_start > 0 and self.canceled == False:
+ time.sleep(0.1)
+ now = QtCore.QDateTime.currentDateTime()
+ scheduled_start = now.secsTo(self.mode.server_status.scheduled_start)
+ # Timer has now finished
+ self.mode.server_status.server_button.setText(strings._('gui_please_wait'))
+ self.mode.server_status_label.setText(strings._('gui_status_indicator_share_working'))
+ if self.canceled == False:
+ self.success.emit()
+ except ValueError as e:
+ self.error.emit(e.args[0])
+ return