diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 99beb0e0..a3474da9 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -23,7 +23,7 @@ import os, sys, time, argparse, threading from . import strings, common, web from .onion import * from .onionshare import OnionShare - +from .settings import Settings def main(cwd=None): """ @@ -77,6 +77,10 @@ def main(cwd=None): if not valid: sys.exit() + + settings = Settings(config) + settings.load() + # Start the Onion object onion = Onion() try: @@ -108,7 +112,7 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, app.stay_open)) + t = threading.Thread(target=web.start, args=(app.port, app.stay_open, settings.get('slug'))) t.daemon = True t.start() @@ -120,6 +124,12 @@ def main(cwd=None): if app.shutdown_timeout > 0: app.shutdown_timer.start() + # Save the web slug if we are using a persistent private key + if settings.get('save_private_key'): + if not settings.get('slug'): + settings.set('slug', web.slug) + settings.save() + if(stealth): print(strings._("give_this_url_stealth")) print('http://{0:s}/{1:s}'.format(app.onion_host, web.slug)) diff --git a/onionshare/onion.py b/onionshare/onion.py index a3aee7a5..db1dfeaf 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -368,7 +368,7 @@ class Onion(object): # Do the versions of stem and tor that I'm using support stealth onion services? try: res = self.c.create_ephemeral_hidden_service({1:1}, basic_auth={'onionshare':None}, await_publication=False) - tmp_service_id = res.content()[0][2].split('=')[1] + tmp_service_id = res.service_id self.c.remove_ephemeral_hidden_service(tmp_service_id) self.supports_stealth = True except: @@ -403,16 +403,29 @@ class Onion(object): print(strings._('using_ephemeral')) if self.stealth: - basic_auth = {'onionshare':None} + if self.settings.get('hidservauth_string'): + hidservauth_string = self.settings.get('hidservauth_string').split()[2] + basic_auth = {'onionshare':hidservauth_string} + else: + basic_auth = {'onionshare':None} else: basic_auth = None + if self.settings.get('private_key'): + key_type = "RSA1024" + key_content = self.settings.get('private_key') + common.log('Onion', 'Starting a hidden service with a saved private key') + else: + key_type = "NEW" + key_content = "RSA1024" + common.log('Onion', 'Starting a hidden service with a new private key') + try: - if basic_auth != None : - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth) - else : + if basic_auth != None: + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth, key_type = key_type, key_content=key_content) + else: # if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True) + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, key_type = key_type, key_content=key_content) except ProtocolError: raise TorErrorProtocolError(strings._('error_tor_protocol_error')) @@ -420,10 +433,29 @@ class Onion(object): self.service_id = res.service_id onion_host = self.service_id + '.onion' - if self.stealth: - auth_cookie = res.content()[2][2].split('=')[1].split(':')[1] - self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) + # A new private key was generated and is in the Control port response. + if self.settings.get('save_private_key'): + if not self.settings.get('private_key'): + self.settings.set('private_key', res.private_key) + 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 + # in the first place. + # If we sent the basic_auth (due to a saved hidservauth_string in the settings), + # there is no response here, so use the saved value from settings. + if self.settings.get('save_private_key'): + if self.settings.get('hidservauth_string'): + self.auth_string = self.settings.get('hidservauth_string') + else: + auth_cookie = list(res.client_auth.values())[0] + self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) + self.settings.set('hidservauth_string', self.auth_string) + else: + auth_cookie = list(res.client_auth.values())[0] + self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) + + self.settings.save() if onion_host is not None: return onion_host else: diff --git a/onionshare/settings.py b/onionshare/settings.py index 408c8bdc..e8103757 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -60,7 +60,11 @@ class Settings(object): 'systray_notifications': True, 'use_stealth': False, 'use_autoupdate': True, - 'autoupdate_timestamp': None + 'autoupdate_timestamp': None, + 'save_private_key': False, + 'private_key': '', + 'slug': '', + 'hidservauth_string': '' } self._settings = {} self.fill_in_defaults() diff --git a/onionshare/web.py b/onionshare/web.py index 03da8fd7..3eef67c7 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -128,9 +128,12 @@ def add_request(request_type, path, data=None): slug = None -def generate_slug(): +def generate_slug(persistent_slug=''): global slug - slug = common.build_slug() + if persistent_slug: + slug = persistent_slug + else: + slug = common.build_slug() download_count = 0 error404_count = 0 @@ -383,11 +386,11 @@ def force_shutdown(): func() -def start(port, stay_open=False): +def start(port, stay_open=False, persistent_slug=''): """ Start the flask web server. """ - generate_slug() + generate_slug(persistent_slug) set_stay_open(stay_open) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index c056a32d..582ebdb3 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -69,7 +69,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.file_selection.file_list.add_file(filename) # Server status - self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection) + self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings) self.server_status.server_started.connect(self.file_selection.server_started) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.file_selection.server_stopped) @@ -119,11 +119,17 @@ class OnionShareGui(QtWidgets.QMainWindow): # Status bar, zip progress bar self._zip_progress_bar = None + # Persistent URL notification + self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True)) + self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;') + self.persistent_url_label.hide() + # Main layout self.layout = QtWidgets.QVBoxLayout() self.layout.addLayout(self.file_selection) self.layout.addLayout(self.server_status) self.layout.addWidget(self.filesize_warning) + self.layout.addWidget(self.persistent_url_label) self.layout.addWidget(self.downloads_container) central_widget = QtWidgets.QWidget() central_widget.setLayout(self.layout) @@ -272,7 +278,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.app.stay_open = not self.settings.get('close_after_first_download') # start onionshare http service in new thread - t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open)) + t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open, self.settings.get('slug'))) t.daemon = True t.start() # wait for modules in thread to load, preventing a thread-related cx_Freeze crash @@ -346,6 +352,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.stop_server() self.start_server_error(strings._('gui_server_started_after_timeout')) + if self.settings.get('save_private_key'): + self.persistent_url_label.show() + def start_server_error(self, error): """ If there's an error when trying to start the onion service @@ -377,6 +386,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Remove ephemeral service, but don't disconnect from Tor self.onion.cleanup(stop_tor=False) self.filesize_warning.hide() + self.persistent_url_label.hide() self.stop_server_finished.emit() self.set_server_active(False) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 48bffdca..442ae440 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -21,7 +21,7 @@ import platform from .alert import Alert from PyQt5 import QtCore, QtWidgets, QtGui -from onionshare import strings, common +from onionshare import strings, common, settings class ServerStatus(QtWidgets.QVBoxLayout): """ @@ -36,7 +36,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, qtapp, app, web, file_selection): + def __init__(self, qtapp, app, web, file_selection, settings): super(ServerStatus, self).__init__() self.status = self.STATUS_STOPPED @@ -45,6 +45,8 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.web = web self.file_selection = file_selection + self.settings = settings + # Helper boolean as this is used in a few places self.timer_enabled = False # Shutdown timeout layout @@ -141,6 +143,11 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.show() self.copy_url_button.show() + if self.settings.get('save_private_key'): + if not self.settings.get('slug'): + self.settings.set('slug', self.web.slug) + self.settings.save() + if self.app.stealth: self.copy_hidservauth_button.show() else: diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 6542a8d5..ca2cc4c7 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -60,10 +60,16 @@ class SettingsDialog(QtWidgets.QDialog): self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked) self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True)) + # Whether or not to save the Onion private key for reuse + self.save_private_key_checkbox = QtWidgets.QCheckBox() + self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True)) + # Sharing options layout sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout.addWidget(self.close_after_first_download_checkbox) sharing_group_layout.addWidget(self.systray_notifications_checkbox) + sharing_group_layout.addWidget(self.save_private_key_checkbox) sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True)) sharing_group.setLayout(sharing_group_layout) @@ -78,10 +84,20 @@ class SettingsDialog(QtWidgets.QDialog): self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True)) + hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True)) + hidservauth_details.setWordWrap(True) + hidservauth_details.hide() + + self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked) + self.hidservauth_copy_button.hide() + # Stealth options layout stealth_group_layout = QtWidgets.QVBoxLayout() stealth_group_layout.addWidget(stealth_details) stealth_group_layout.addWidget(self.stealth_checkbox) + stealth_group_layout.addWidget(hidservauth_details) + stealth_group_layout.addWidget(self.hidservauth_copy_button) stealth_group = QtWidgets.QGroupBox(strings._("gui_settings_stealth_label", True)) stealth_group.setLayout(stealth_group_layout) @@ -277,9 +293,18 @@ class SettingsDialog(QtWidgets.QDialog): else: self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked) + save_private_key = self.old_settings.get('save_private_key') + if save_private_key: + self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) + use_stealth = self.old_settings.get('use_stealth') if use_stealth: self.stealth_checkbox.setCheckState(QtCore.Qt.Checked) + if save_private_key: + hidservauth_details.show() + self.hidservauth_copy_button.show() else: self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) @@ -379,6 +404,15 @@ class SettingsDialog(QtWidgets.QDialog): else: self.authenticate_password_extras.hide() + def hidservauth_copy_button_clicked(self): + """ + Toggle the 'Copy HidServAuth' button + to copy the saved HidServAuth to clipboard. + """ + common.log('SettingsDialog', 'hidservauth_copy_button_clicked', 'HidServAuth was copied to clipboard') + clipboard = self.qtapp.clipboard() + clipboard.setText(self.old_settings.get('hidservauth_string')) + def test_tor_clicked(self): """ Test Tor Settings button clicked. With the given settings, see if we can @@ -533,7 +567,21 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked()) + if self.save_private_key_checkbox.isChecked(): + settings.set('save_private_key', True) + settings.set('private_key', self.old_settings.get('private_key')) + settings.set('slug', self.old_settings.get('slug')) + settings.set('hidservauth_string', self.old_settings.get('hidservauth_string')) + else: + settings.set('save_private_key', False) + settings.set('private_key', '') + settings.set('slug', '') + # Also unset the HidServAuth if we are removing our reusable private key + settings.set('hidservauth_string', '') settings.set('use_stealth', self.stealth_checkbox.isChecked()) + # Always unset the HidServAuth if Stealth mode is unset + if not self.stealth_checkbox.isChecked(): + settings.set('hidservauth_string', '') if self.connection_type_bundled_radio.isChecked(): settings.set('connection_type', 'bundled') diff --git a/share/locale/en.json b/share/locale/en.json index abd734c9..30e0c196 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -67,6 +67,7 @@ "gui_settings_stealth_label": "Stealth (advanced)", "gui_settings_stealth_option": "Create stealth onion services", "gui_settings_stealth_option_details": "This makes OnionShare more secure, but also more difficult for the recipient to connect to it.
More information.", + "gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.", "gui_settings_autoupdate_label": "Check for updates", "gui_settings_autoupdate_option": "Notify me when updates are available", "gui_settings_autoupdate_timestamp": "Last checked: {}", @@ -122,5 +123,7 @@ "gui_tor_connection_lost": "Disconnected from Tor.", "gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.", "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.", - "share_via_onionshare": "Share via OnionShare" + "share_via_onionshare": "Share via OnionShare", + "gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)", + "persistent_url_in_use": "This share is using a persistent URL" } diff --git a/test/test_onionshare.py b/test/test_onionshare.py index f3e35dfc..76e471bd 100644 --- a/test/test_onionshare.py +++ b/test/test_onionshare.py @@ -27,6 +27,7 @@ from onionshare import OnionShare class MyOnion: def __init__(self, stealth=False): self.auth_string = 'TestHidServAuth' + self.private_key = '' self.stealth = stealth @staticmethod diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 1489c348..ba45ef5e 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -57,7 +57,11 @@ class TestSettings: 'systray_notifications': True, 'use_stealth': False, 'use_autoupdate': True, - 'autoupdate_timestamp': None + 'autoupdate_timestamp': None, + 'save_private_key': False, + 'private_key': '', + 'slug': '', + 'hidservauth_string': '' } def test_fill_in_defaults(self, settings_obj):