Add a Startup Timer feature (scheduled start / dead man's switch)

This commit is contained in:
Miguel Jacq 2019-03-05 10:28:27 +11:00
parent d86b13d91c
commit 26d262ccfc
16 changed files with 326 additions and 34 deletions

View File

@ -53,6 +53,7 @@ def main(cwd=None):
parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=28))
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
parser.add_argument('--startup-timer', metavar='<int>', dest='startup_timer', default=0, help=strings._("help_startup_timer"))
parser.add_argument('--shutdown-timeout', metavar='<int>', dest='shutdown_timeout', default=0, help=strings._("help_shutdown_timeout"))
parser.add_argument('--stealth', action='store_true', dest='stealth', help=strings._("help_stealth"))
parser.add_argument('--receive', action='store_true', dest='receive', help=strings._("help_receive"))
@ -68,6 +69,7 @@ def main(cwd=None):
local_only = bool(args.local_only)
debug = bool(args.debug)
stay_open = bool(args.stay_open)
startup_timer = int(args.startup_timer)
shutdown_timeout = int(args.shutdown_timeout)
stealth = bool(args.stealth)
receive = bool(args.receive)
@ -120,10 +122,28 @@ def main(cwd=None):
# Start the onionshare app
try:
common.settings.load()
if not common.settings.get('public_mode'):
web.generate_slug(common.settings.get('slug'))
else:
web.slug = None
app = OnionShare(common, onion, local_only, shutdown_timeout)
app.set_stealth(stealth)
app.choose_port()
app.start_onion_service()
# Delay the startup if a startup timer was set
if startup_timer > 0:
app.start_onion_service(False, True)
if common.settings.get('public_mode'):
url = 'http://{0:s}'.format(app.onion_host)
else:
url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug)
print(strings._("scheduled_onion_service").format(url))
app.onion.cleanup()
print(strings._("waiting_for_startup_timer"))
time.sleep(startup_timer)
app.start_onion_service()
else:
app.start_onion_service()
except KeyboardInterrupt:
print("")
sys.exit()
@ -149,7 +169,7 @@ def main(cwd=None):
print('')
# Start OnionShare http service in new thread
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug')))
t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), web.slug))
t.daemon = True
t.start()

View File

@ -133,6 +133,7 @@ class Onion(object):
self.stealth = False
self.service_id = None
self.scheduled_key = None
# Is bundled tor supported?
if (self.common.platform == 'Windows' or self.common.platform == 'Darwin') and getattr(sys, 'onionshare_dev_mode', False):
@ -423,7 +424,7 @@ class Onion(object):
return False
def start_onion_service(self, port):
def start_onion_service(self, port, await_publication, save_scheduled_key=False):
"""
Start a onion service on port 80, pointing to the given port, and
return the onion hostname.
@ -455,6 +456,14 @@ class Onion(object):
# Assume it was a v3 key. Stem will throw an error if it's something illegible
key_type = "ED25519-V3"
elif self.scheduled_key:
key_content = self.scheduled_key
if self.is_v2_key(key_content):
key_type = "RSA1024"
else:
# Assume it was a v3 key. Stem will throw an error if it's something illegible
key_type = "ED25519-V3"
else:
key_type = "NEW"
# Work out if we can support v3 onion services, which are preferred
@ -474,7 +483,6 @@ class Onion(object):
if key_type == "NEW":
debug_message += ', key_content={}'.format(key_content)
self.common.log('Onion', 'start_onion_service', '{}'.format(debug_message))
await_publication = True
try:
if basic_auth != None:
res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=await_publication, basic_auth=basic_auth, key_type=key_type, key_content=key_content)
@ -493,6 +501,12 @@ class Onion(object):
if not self.settings.get('private_key'):
self.settings.set('private_key', res.private_key)
# If we were scheduling a future share, register the private key for later re-use
if save_scheduled_key:
self.scheduled_key = res.private_key
else:
self.scheduled_key = None
if self.stealth:
# Similar to the PrivateKey, the Control port only returns the ClientAuth
# in the response if it was responsible for creating the basic_auth password

View File

@ -41,6 +41,7 @@ class OnionShare(object):
self.onion_host = None
self.port = None
self.stealth = None
self.scheduled_key = None
# files and dirs to delete on shutdown
self.cleanup_filenames = []
@ -68,7 +69,7 @@ class OnionShare(object):
except:
raise OSError(strings._('no_available_port'))
def start_onion_service(self):
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
"""
Start the onionshare onion service.
"""
@ -84,16 +85,20 @@ class OnionShare(object):
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
return
self.onion_host = self.onion.start_onion_service(self.port)
self.onion_host = self.onion.start_onion_service(self.port, await_publication, save_scheduled_key)
if self.stealth:
self.auth_string = self.onion.auth_string
if self.onion.scheduled_key:
self.scheduled_key = self.onion.scheduled_key
def cleanup(self):
"""
Shut everything down and clean up temporary files, etc.
"""
self.common.log('OnionShare', 'cleanup')
self.scheduled_key = None
# Cleanup files
try:

View File

@ -85,6 +85,7 @@ class Settings(object):
'auth_password': '',
'close_after_first_download': True,
'shutdown_timeout': False,
'startup_timer': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,

View File

@ -228,13 +228,11 @@ class Web(object):
pass
self.running = False
def start(self, port, stay_open=False, public_mode=False, persistent_slug=None):
def start(self, port, stay_open=False, public_mode=False, slug=None):
"""
Start the flask web server.
"""
self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, persistent_slug={}'.format(port, stay_open, public_mode, persistent_slug))
if not public_mode:
self.generate_slug(persistent_slug)
self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, slug={}'.format(port, stay_open, public_mode, slug))
self.stay_open = stay_open
@ -264,7 +262,7 @@ class Web(object):
self.stop_q.put(True)
# Reset any slug that was in use
self.slug = ''
self.slug = None
# To stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
if self.running:

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -15,6 +15,7 @@
"large_filesize": "Warning: Sending a large share could take hours",
"help_local_only": "Don't use Tor (only for development)",
"help_stay_open": "Continue sharing after files have been sent",
"help_startup_timer": "Schedule this share to start N seconds from now",
"help_shutdown_timeout": "Stop sharing after a given amount of seconds",
"help_stealth": "Use client authorization (advanced)",
"help_receive": "Receive shares instead of sending them",
@ -42,6 +43,7 @@
"gui_copied_url": "OnionShare address copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth",
"gui_copied_hidservauth": "HidServAuth line copied to clipboard",
"gui_waiting_to_start": "Scheduled for {}. Click to cancel.",
"gui_please_wait": "Starting… Click to cancel.",
"version_string": "OnionShare {0:s} | https://onionshare.org/",
"gui_quit_title": "Not so fast",
@ -94,6 +96,8 @@
"gui_settings_button_help": "Help",
"gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:",
"gui_settings_startup_timer_checkbox": "Use auto-start timer",
"gui_settings_startup_timer": "Start the share at:",
"settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.",
"settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?",
"settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.",
@ -133,9 +137,11 @@
"gui_url_label_onetime_and_persistent": "This share will not auto-stop.<br><br>Every subsequent share will reuse the address. (To use one-time addresses, turn off \"Use persistent address\" in the settings.)",
"gui_status_indicator_share_stopped": "Ready to share",
"gui_status_indicator_share_working": "Starting…",
"gui_status_indicator_share_scheduled": "Scheduled…",
"gui_status_indicator_share_started": "Sharing",
"gui_status_indicator_receive_stopped": "Ready to receive",
"gui_status_indicator_receive_working": "Starting…",
"gui_status_indicator_receive_scheduled": "Scheduled…",
"gui_status_indicator_receive_started": "Receiving",
"gui_file_info": "{} files, {}",
"gui_file_info_single": "{} file, {}",
@ -180,5 +186,7 @@
"gui_share_mode_no_files": "No Files Sent Yet",
"gui_share_mode_timeout_waiting": "Waiting to finish sending",
"gui_receive_mode_no_files": "No Files Received Yet",
"gui_receive_mode_timeout_waiting": "Waiting to finish receiving"
"gui_receive_mode_timeout_waiting": "Waiting to finish receiving",
"waiting_for_startup_timer": "Waiting for the timer to run down before starting...",
"scheduled_onion_service": "Your OnionShare URL will be: {}"
}

View File

@ -172,6 +172,9 @@ class GuiBaseTest(object):
'''Test that the Server Status indicator shows we are Starting'''
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_share_working'))
def server_status_indicator_says_scheduled(self, mode):
'''Test that the Server Status indicator shows we are Scheduled'''
self.assertEqual(mode.server_status_label.text(), strings._('gui_status_indicator_share_scheduled'))
def server_is_started(self, mode, startup_time=2000):
'''Test that the server has started'''
@ -294,7 +297,6 @@ class GuiBaseTest(object):
mode.server_status.shutdown_timeout.setDateTime(timer)
self.assertTrue(mode.server_status.shutdown_timeout.dateTime(), timer)
def timeout_widget_hidden(self, mode):
'''Test that the timeout widget is hidden when share has started'''
self.assertFalse(mode.server_status.shutdown_timeout_container.isVisible())
@ -306,6 +308,23 @@ class GuiBaseTest(object):
# We should have timed out now
self.assertEqual(mode.server_status.status, 0)
# Startup timer tests
def set_startup_timer(self, mode, timer):
'''Test that the timer can be set'''
schedule = QtCore.QDateTime.currentDateTime().addSecs(timer)
mode.server_status.startup_timer.setDateTime(schedule)
self.assertTrue(mode.server_status.startup_timer.dateTime(), schedule)
def startup_timer_widget_hidden(self, mode):
'''Test that the startup timer widget is hidden when share has started'''
self.assertFalse(mode.server_status.startup_timer_container.isVisible())
def scheduled_service_started(self, mode, wait):
'''Test that the server has timed out after the timer ran out'''
QtTest.QTest.qWait(wait)
# We should have started now
self.assertEqual(mode.server_status.status, 2)
# Hack to close an Alert dialog that would otherwise block tests
def accept_dialog(self):
window = self.gui.qtapp.activeWindow()

View File

@ -195,6 +195,15 @@ class GuiShareTest(GuiBaseTest):
self.server_timed_out(self.gui.share_mode, 10000)
self.web_server_is_stopped()
def run_all_share_mode_startup_timer_tests(self, public_mode):
"""Auto-stop timer tests in share mode"""
self.run_all_share_mode_setup_tests()
self.set_startup_timer(self.gui.share_mode, 5)
self.server_working_on_start_button_pressed(self.gui.share_mode)
self.startup_timer_widget_hidden(self.gui.share_mode)
self.server_status_indicator_says_scheduled(self.gui.share_mode)
self.scheduled_service_started(self.gui.share_mode, 7000)
self.web_server_is_running()
def run_all_share_mode_unreadable_file_tests(self):
'''Attempt to share an unreadable file'''

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
import pytest
import unittest
from .GuiShareTest import GuiShareTest
class LocalShareModeStartupTimerTest(unittest.TestCase, GuiShareTest):
@classmethod
def setUpClass(cls):
test_settings = {
"public_mode": False,
"startup_timer": True,
}
cls.gui = GuiShareTest.set_up(test_settings)
@classmethod
def tearDownClass(cls):
GuiShareTest.tear_down()
@pytest.mark.gui
def test_gui(self):
self.run_all_common_setup_tests()
self.run_all_share_mode_startup_timer_tests(False)
if __name__ == "__main__":
unittest.main()

View File

@ -30,9 +30,10 @@ class MyOnion:
self.auth_string = 'TestHidServAuth'
self.private_key = ''
self.stealth = stealth
self.scheduled_key = None
@staticmethod
def start_onion_service(_):
def start_onion_service(self, await_publication=True, save_scheduled_key=False):
return 'test_service_id.onion'

View File

@ -52,6 +52,7 @@ class TestSettings:
'auth_password': '',
'close_after_first_download': True,
'shutdown_timeout': False,
'startup_timer': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,