mirror of
https://github.com/onionshare/onionshare.git
synced 2025-02-11 20:29:15 -05:00
Add a Startup Timer feature (scheduled start / dead man's switch)
This commit is contained in:
parent
d86b13d91c
commit
26d262ccfc
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: {}"
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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'''
|
||||
|
26
tests/local_onionshare_share_mode_startup_timer_test.py
Normal file
26
tests/local_onionshare_share_mode_startup_timer_test.py
Normal 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()
|
@ -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'
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user