From 289dbad71f0d045dc0bf0d12c3f71d5f6d35cef1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 7 Dec 2017 12:45:29 +1100 Subject: [PATCH 01/22] Optionally save the private key of a running share to settings for reuse --- onionshare/onion.py | 13 ++++++++++--- onionshare/onionshare.py | 2 +- onionshare_gui/onionshare_gui.py | 2 +- onionshare_gui/server_status.py | 21 ++++++++++++++++++++- onionshare_gui/settings_dialog.py | 17 +++++++++++++++++ share/locale/en.json | 4 +++- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 2f79719b..b95d975d 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -396,24 +396,31 @@ class Onion(object): else: basic_auth = None + if self.settings.get('private_key'): + key_type = "RSA1024" + key_content = self.settings.get('private_key') + else: + key_type = "NEW" + key_content = "RSA1024" try: if basic_auth != None : - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth) + 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')) self.service_id = res.content()[0][2].split('=')[1] onion_host = self.service_id + '.onion' + private_key = res.private_key if self.stealth: auth_cookie = res.content()[2][2].split('=')[1].split(':')[1] self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) - return onion_host + return (onion_host, private_key) def cleanup(self): """ diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 85bfaf22..128e71af 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -76,7 +76,7 @@ class OnionShare(object): if self.shutdown_timeout > 0: self.shutdown_timer = common.close_after_seconds(self.shutdown_timeout) - self.onion_host = self.onion.start_onion_service(self.port) + self.onion_host, self.private_key = self.onion.start_onion_service(self.port) if self.stealth: self.auth_string = self.onion.auth_string diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 3ed30db7..dd5c00bf 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) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 48bffdca..c2cb8f47 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -22,6 +22,7 @@ from .alert import Alert from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common +from onionshare.settings import Settings class ServerStatus(QtWidgets.QVBoxLayout): """ @@ -31,12 +32,13 @@ class ServerStatus(QtWidgets.QVBoxLayout): server_stopped = QtCore.pyqtSignal() url_copied = QtCore.pyqtSignal() hidservauth_copied = QtCore.pyqtSignal() + private_key_saved = QtCore.pyqtSignal() STATUS_STOPPED = 0 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 @@ -44,6 +46,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.app = app 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 @@ -87,10 +90,13 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.copy_url_button.clicked.connect(self.copy_url) self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) + self.save_private_key_button = QtWidgets.QPushButton(strings._('gui_save_private_key', True)) + self.save_private_key_button.clicked.connect(self.save_private_key) url_layout = QtWidgets.QHBoxLayout() url_layout.addWidget(self.url_label) url_layout.addWidget(self.copy_url_button) url_layout.addWidget(self.copy_hidservauth_button) + url_layout.addWidget(self.save_private_key_button) # add the widgets self.addLayout(shutdown_timeout_layout_group) @@ -140,6 +146,8 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)) self.url_label.show() self.copy_url_button.show() + if not self.settings.get('private_key'): + self.save_private_key_button.show() if self.app.stealth: self.copy_hidservauth_button.show() @@ -153,6 +161,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.hide() self.copy_url_button.hide() self.copy_hidservauth_button.hide() + self.save_private_key_button.hide() # button if self.file_selection.get_num_files() == 0: @@ -249,3 +258,13 @@ class ServerStatus(QtWidgets.QVBoxLayout): clipboard.setText(self.app.auth_string) self.hidservauth_copied.emit() + + def save_private_key(self): + """ + Save the Onion private key to settings, so the Onion URL can be re-used. + """ + self.save_private_key_button.setEnabled(False) + self.settings.set('private_key', self.app.private_key) + self.settings.save() + + self.private_key_saved.emit() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index df806a06..3421255d 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) @@ -275,6 +281,13 @@ class SettingsDialog(QtWidgets.QDialog): else: self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked) + save_private_key = self.old_settings.get('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) + self.save_private_key_checkbox.hide() + use_stealth = self.old_settings.get('use_stealth') if use_stealth: self.stealth_checkbox.setCheckState(QtCore.Qt.Checked) @@ -529,6 +542,10 @@ 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('private_key', settings.get('private_key')) + else: + settings.set('private_key', '') settings.set('use_stealth', self.stealth_checkbox.isChecked()) if self.connection_type_bundled_radio.isChecked(): diff --git a/share/locale/en.json b/share/locale/en.json index 0756843e..5ec68229 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -121,5 +121,7 @@ "gui_tor_connection_canceled": "OnionShare cannot connect to Tor.\n\nMake sure you're connected to the internet, then re-open OnionShare to configure the Tor connection.", "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": "Save private key?", + "gui_save_private_key_checkbox": "Should the private key be saved for re-use?\nThis makes the Onion share URL persistent.\nUnchecking will remove the private key from settings." } From dfac27fe84e7dfb3a2e7561c90e0b9e5613509a6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 7 Dec 2017 13:02:56 +1100 Subject: [PATCH 02/22] emit to the status bar when the private key is saved to disk --- onionshare_gui/onionshare_gui.py | 8 ++++++++ share/locale/en.json | 1 + 2 files changed, 9 insertions(+) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index dd5c00bf..a5fb0edb 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -80,6 +80,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.file_selection.file_list.files_updated.connect(self.server_status.update) self.server_status.url_copied.connect(self.copy_url) self.server_status.hidservauth_copied.connect(self.copy_hidservauth) + self.server_status.private_key_saved.connect(self.private_key_saved) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) @@ -470,6 +471,13 @@ class OnionShareGui(QtWidgets.QMainWindow): common.log('OnionShareGui', 'copy_hidservauth') self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) + def private_key_saved(self): + """ + When the private key gets saved, display this in the status bar. + """ + common.log('OnionShareGui', 'private_key_saved') + self.status_bar.showMessage(strings._('gui_private_key_saved', True), 2000) + def clear_message(self): """ Clear messages from the status bar. diff --git a/share/locale/en.json b/share/locale/en.json index 5ec68229..b239433e 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -46,6 +46,7 @@ "gui_canceled": "Canceled", "gui_copied_url": "Copied URL to clipboard", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", + "gui_private_key_saved": "Private key saved to settings", "gui_starting_server1": "Starting Tor onion service...", "gui_starting_server2": "Crunching files...", "gui_please_wait": "Please wait...", From 90807727c80cb83a14ada335145ce70b79f1c312 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 7 Dec 2017 15:33:12 +1100 Subject: [PATCH 03/22] try and fix tests --- onionshare/onion.py | 4 ++-- onionshare/onionshare.py | 4 +++- test/test_onionshare.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index b95d975d..218dafd5 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -414,13 +414,13 @@ class Onion(object): self.service_id = res.content()[0][2].split('=')[1] onion_host = self.service_id + '.onion' - private_key = res.private_key + self.private_key = res.private_key if self.stealth: auth_cookie = res.content()[2][2].split('=')[1].split(':')[1] self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) - return (onion_host, private_key) + return onion_host def cleanup(self): """ diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 128e71af..7466a163 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -76,7 +76,9 @@ class OnionShare(object): if self.shutdown_timeout > 0: self.shutdown_timer = common.close_after_seconds(self.shutdown_timeout) - self.onion_host, self.private_key = self.onion.start_onion_service(self.port) + self.onion_host = self.onion.start_onion_service(self.port) + + self.private_key = self.onion.private_key if self.stealth: self.auth_string = self.onion.auth_string 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 From 3a4b56b7b4eef084ca96ee1a453fefcadb16c676 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 7 Dec 2017 16:08:03 +1100 Subject: [PATCH 04/22] cleanup the ephemeral hidden service when GUI server is stopped, but don't disconnect from Tor --- onionshare/onion.py | 39 +++++++++++++++++--------------- onionshare_gui/onionshare_gui.py | 2 ++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 218dafd5..00b7dd51 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -422,7 +422,7 @@ class Onion(object): return onion_host - def cleanup(self): + def cleanup(self, stop_tor=True): """ Stop onion services that were created earlier. If there's a tor subprocess running, kill it. """ @@ -436,25 +436,28 @@ class Onion(object): pass self.service_id = None - # Stop tor process - if self.tor_proc: - self.tor_proc.terminate() - time.sleep(0.2) - if not self.tor_proc.poll(): - self.tor_proc.kill() - self.tor_proc = None + if stop_tor: + # Stop tor process + if self.tor_proc: + self.tor_proc.terminate() + time.sleep(0.2) + if not self.tor_proc.poll(): + try: + self.tor_proc.kill() + except: + pass + self.tor_proc = None - # Reset other Onion settings - self.connected_to_tor = False - self.stealth = False - self.service_id = None + # Reset other Onion settings + self.connected_to_tor = False + self.stealth = False - try: - # Delete the temporary tor data directory - self.tor_data_directory.cleanup() - except AttributeError: - # Skip if cleanup was somehow run before connect - pass + try: + # Delete the temporary tor data directory + self.tor_data_directory.cleanup() + except AttributeError: + # Skip if cleanup was somehow run before connect + pass def get_tor_socks_port(self): """ diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index a5fb0edb..17b4cfc1 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -355,6 +355,8 @@ class OnionShareGui(QtWidgets.QMainWindow): # Probably we had no port to begin with (Onion service didn't start) pass self.app.cleanup() + # Remove ephemeral service, but don't disconnect from Tor + self.onion.cleanup(stop_tor=False) self.filesize_warning.hide() self.stop_server_finished.emit() From 75d7d3dfd7d397dbd8f1bb25071381070530abed Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 8 Dec 2017 11:15:12 +1100 Subject: [PATCH 05/22] remove question mark from QPushButton --- share/locale/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/locale/en.json b/share/locale/en.json index b239433e..88113fba 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -123,6 +123,6 @@ "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", - "gui_save_private_key": "Save private key?", + "gui_save_private_key": "Save private key", "gui_save_private_key_checkbox": "Should the private key be saved for re-use?\nThis makes the Onion share URL persistent.\nUnchecking will remove the private key from settings." } From ebf0d694f3d9ce88a3b8c225b4f74dcff8e683b9 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 9 Dec 2017 06:49:34 +1100 Subject: [PATCH 06/22] Save the HidServAuth string to settings when private key is also saved. Allow to copy it to clipboard from the SettingsDialog too. --- onionshare/onion.py | 32 ++++++++++++++++++++++++------- onionshare_gui/server_status.py | 3 +++ onionshare_gui/settings_dialog.py | 27 ++++++++++++++++++++++++++ share/locale/en.json | 1 + 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 00b7dd51..409c9ad8 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: @@ -392,20 +392,27 @@ 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 : + 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 : + 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, key_type = key_type, key_content=key_content) @@ -414,11 +421,22 @@ class Onion(object): self.service_id = res.content()[0][2].split('=')[1] onion_host = self.service_id + '.onion' - self.private_key = res.private_key + + # A new private key was generated and is in the Control port response. + if not self.settings.get('private_key'): + self.private_key = res.private_key if self.stealth: - auth_cookie = res.content()[2][2].split('=')[1].split(':')[1] - self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) + # 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('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) return onion_host diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index c2cb8f47..77964677 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -265,6 +265,9 @@ class ServerStatus(QtWidgets.QVBoxLayout): """ self.save_private_key_button.setEnabled(False) self.settings.set('private_key', self.app.private_key) + if self.app.stealth: + self.settings.set('hidservauth_string', self.app.auth_string) self.settings.save() + self.settings.load() self.private_key_saved.emit() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 3421255d..22b24fc5 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -82,10 +82,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) @@ -291,6 +301,9 @@ class SettingsDialog(QtWidgets.QDialog): 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) @@ -390,6 +403,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 @@ -546,7 +568,12 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('private_key', settings.get('private_key')) else: settings.set('private_key', '') + # 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 88113fba..2ec21580 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -68,6 +68,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: {}", From de36fea474c161f61272301343aef8fbedad06e8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 9 Dec 2017 08:11:04 +1100 Subject: [PATCH 07/22] properly set saved private key/hidservauth string in settings_from_fields, so those values are populated when the SettingsDialog is saved --- onionshare_gui/settings_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 22b24fc5..3de3ef85 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -565,7 +565,8 @@ 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('private_key', settings.get('private_key')) + settings.set('private_key', self.old_settings.get('private_key')) + settings.set('hidservauth_string', self.old_settings.get('hidservauth_string')) else: settings.set('private_key', '') # Also unset the HidServAuth if we are removing our reusable private key From d5140be0465256b623c53622b4ff3a7c76ff06d8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 9 Dec 2017 08:18:06 +1100 Subject: [PATCH 08/22] only set self.private_key after the Onion service has started, if we received a new one --- onionshare/onion.py | 2 ++ onionshare/onionshare.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 409c9ad8..862266e7 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -425,6 +425,8 @@ class Onion(object): # A new private key was generated and is in the Control port response. if not self.settings.get('private_key'): self.private_key = res.private_key + else: + self.private_key = '' if self.stealth: # Similar to the PrivateKey, the Control port only returns the ClientAuth diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index 7466a163..ff5f33fb 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -78,7 +78,8 @@ class OnionShare(object): self.onion_host = self.onion.start_onion_service(self.port) - self.private_key = self.onion.private_key + if self.onion.private_key: + self.private_key = self.onion.private_key if self.stealth: self.auth_string = self.onion.auth_string From 7a973c83edc1e417dafef7519621361f1a1840d5 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 9 Dec 2017 08:51:30 +1100 Subject: [PATCH 09/22] set the 'Save Private Key' button to enabled when showing it, but when it's not already been pressed (in case private key is removed later via SettingsDialog) --- onionshare_gui/server_status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 77964677..1a5d0807 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -148,6 +148,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.copy_url_button.show() if not self.settings.get('private_key'): self.save_private_key_button.show() + self.save_private_key_button.setEnabled(True) if self.app.stealth: self.copy_hidservauth_button.show() From 38eb14d0e3b82777a9c6f2bbf1e48348fba45650 Mon Sep 17 00:00:00 2001 From: Baccount Date: Tue, 26 Dec 2017 22:57:47 -0800 Subject: [PATCH 10/22] Update get-tor-windows.py Fixes 2 onion service exploits --- install/get-tor-windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-windows.py b/install/get-tor-windows.py index d6cbfed5..83b8f2b3 100644 --- a/install/get-tor-windows.py +++ b/install/get-tor-windows.py @@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, zipfile, io, shutil import urllib.request def main(): - zip_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.10/tor-win32-0.3.1.8.zip' - zip_filename = 'tor-win32-0.3.1.8.zip' - expected_zip_sha256 = '101defd239cda42f364815e91809fad16b17f03843a169ffbeb8cb91183b6ba8' + zip_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/tor-win32-0.3.1.9.zip' + zip_filename = 'tor-win32-0.3.1.9.zip' + expected_zip_sha256 = 'faf28efb606455842bda66ca369287a116b6d6e5ad3720ebed9337da0717f1b4' # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) From 9c9f968ab51bdb3868ca5343bde9da393cf701c0 Mon Sep 17 00:00:00 2001 From: Baccount Date: Tue, 26 Dec 2017 23:00:34 -0800 Subject: [PATCH 11/22] Update get-tor-osx.py Fixes 5 exploits 2 of the exploits effect onion services https://blog.torproject.org/new-stable-tor-releases-security-fixes-0319-03013-02914-02817-02516 --- install/get-tor-osx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/get-tor-osx.py b/install/get-tor-osx.py index b8c17fba..76d8cb4f 100644 --- a/install/get-tor-osx.py +++ b/install/get-tor-osx.py @@ -28,9 +28,9 @@ import inspect, os, sys, hashlib, zipfile, io, shutil, subprocess import urllib.request def main(): - dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.10/TorBrowser-7.0.10-osx64_en-US.dmg' - dmg_filename = 'TorBrowser-7.0.10-osx64_en-US.dmg' - expected_dmg_sha256 = '2de32de962d14ecb55f8008078875f3d0b0875dba4a140726714e67ce329fe9a' + dmg_url = 'https://archive.torproject.org/tor-package-archive/torbrowser/7.0.11/TorBrowser-7.0.11-osx64_en-US.dmg' + dmg_filename = 'TorBrowser-7.0.11-osx64_en-US.dmg' + expected_dmg_sha256 = '5143e4a2141a69f66869be13eef4bcaac4e6c27c78383fc8a4c38b334759f3a2' # Build paths root_path = os.path.dirname(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) From 4079704be51f0f0d29719f296a9872f0569f5c61 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 2 Jan 2018 09:36:57 +1100 Subject: [PATCH 12/22] Check the file is a valid file or dir (not just that it exists) --- onionshare/__init__.py | 5 ++++- onionshare_gui/__init__.py | 5 ++++- share/locale/en.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 99beb0e0..4b64d420 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -68,12 +68,15 @@ def main(cwd=None): # Validation valid = True for filename in filenames: - if not os.path.exists(filename): + if not os.path.isfile(filename) and not os.path.isdir(filename): print(strings._("not_a_file").format(filename)) valid = False if not os.access(filename, os.R_OK): print(strings._("not_a_readable_file").format(filename)) valid = False + if os.path.isdir(filename) and os.path.getsize(filename) < 4096: + print(strings._("not_a_file").format(filename)) + valid = False if not valid: sys.exit() diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 83d03cc0..9d105f6a 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -91,12 +91,15 @@ def main(): if filenames: valid = True for filename in filenames: - if not os.path.exists(filename): + if not os.path.isfile(filename) and not os.path.isdir(filename): Alert(strings._("not_a_file", True).format(filename)) valid = False if not os.access(filename, os.R_OK): Alert(strings._("not_a_readable_file", True).format(filename)) valid = False + if os.path.isdir(filename) and os.path.getsize(filename) < 4096: + Alert(strings._("not_a_file", True).format(filename)) + valid = False if not valid: sys.exit() diff --git a/share/locale/en.json b/share/locale/en.json index 0756843e..3fc0c313 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -8,7 +8,7 @@ "give_this_url": "Give this URL to the person you're sending the file to:", "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:", "ctrlc_to_stop": "Press Ctrl-C to stop server", - "not_a_file": "{0:s} is not a file.", + "not_a_file": "{0:s} is not a valid file.", "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not start the Onion service as there was no available port.", "download_page_loaded": "Download page loaded", From d6677060af114f54902c9db182f6c941b2da631e Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 2 Jan 2018 09:45:56 +1100 Subject: [PATCH 13/22] 4096 byte dir check is not consistent across platforms, remove it for now --- onionshare/__init__.py | 3 --- onionshare_gui/__init__.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 4b64d420..705ecc79 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -74,9 +74,6 @@ def main(cwd=None): if not os.access(filename, os.R_OK): print(strings._("not_a_readable_file").format(filename)) valid = False - if os.path.isdir(filename) and os.path.getsize(filename) < 4096: - print(strings._("not_a_file").format(filename)) - valid = False if not valid: sys.exit() diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 9d105f6a..14c76617 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -97,9 +97,6 @@ def main(): if not os.access(filename, os.R_OK): Alert(strings._("not_a_readable_file", True).format(filename)) valid = False - if os.path.isdir(filename) and os.path.getsize(filename) < 4096: - Alert(strings._("not_a_file", True).format(filename)) - valid = False if not valid: sys.exit() From 5a08810f2386310bc9c87339df16867c258885ea Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 3 Jan 2018 11:16:50 +1100 Subject: [PATCH 14/22] #528 disable the 'Check for Updates' button in the SettingsDialog if Tor is not connected --- onionshare_gui/settings_dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index df806a06..105b1e3d 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -96,6 +96,9 @@ class SettingsDialog(QtWidgets.QDialog): # Check for updates button self.check_for_updates_button = QtWidgets.QPushButton(strings._('gui_settings_autoupdate_check_button', True)) self.check_for_updates_button.clicked.connect(self.check_for_updates) + # We can't check for updates if not connected to Tor + if not self.onion.connected_to_tor: + self.check_for_updates_button.setEnabled(False) # Autoupdate options layout autoupdate_group_layout = QtWidgets.QVBoxLayout() From 2d606d5e7fd8c2a4ea6b7651da3e1b92d98eb05a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 4 Jan 2018 08:43:43 +1100 Subject: [PATCH 15/22] Don't re-enable the Check for Updates button after testing Tor connection, if the main Tor connection is still not active --- onionshare_gui/settings_dialog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 105b1e3d..38cf36d3 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -596,8 +596,11 @@ class SettingsDialog(QtWidgets.QDialog): def _enable_buttons(self): common.log('SettingsDialog', '_enable_buttons') - - self.check_for_updates_button.setEnabled(True) + # We can't check for updates if we're still not connected to Tor + if not self.onion.connected_to_tor: + self.check_for_updates_button.setEnabled(False) + else: + self.check_for_updates_button.setEnabled(True) self.connection_type_test_button.setEnabled(True) self.save_button.setEnabled(True) self.cancel_button.setEnabled(True) From eff94d7bfdee5d3968f6dba8dd9885fb29af0e11 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 13 Jan 2018 20:58:24 +1100 Subject: [PATCH 16/22] Move the saving of private key into the SettingsDialog and Onion objects entirely (no QPushButton to save it) --- onionshare/onion.py | 17 +++++++++++------ onionshare/onionshare.py | 3 --- onionshare/settings.py | 5 ++++- onionshare_gui/onionshare_gui.py | 8 -------- onionshare_gui/server_status.py | 23 ++--------------------- onionshare_gui/settings_dialog.py | 5 +++-- share/locale/en.json | 4 +--- 7 files changed, 21 insertions(+), 44 deletions(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 862266e7..3e30761a 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -423,10 +423,9 @@ class Onion(object): onion_host = self.service_id + '.onion' # A new private key was generated and is in the Control port response. - if not self.settings.get('private_key'): - self.private_key = res.private_key - else: - self.private_key = '' + 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 @@ -434,12 +433,18 @@ class Onion(object): # 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('hidservauth_string'): - self.auth_string = self.settings.get('hidservauth_string') + 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() return onion_host def cleanup(self, stop_tor=True): diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py index ff5f33fb..85bfaf22 100644 --- a/onionshare/onionshare.py +++ b/onionshare/onionshare.py @@ -78,9 +78,6 @@ class OnionShare(object): self.onion_host = self.onion.start_onion_service(self.port) - if self.onion.private_key: - self.private_key = self.onion.private_key - if self.stealth: self.auth_string = self.onion.auth_string diff --git a/onionshare/settings.py b/onionshare/settings.py index 408c8bdc..e87638f1 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -60,7 +60,10 @@ class Settings(object): 'systray_notifications': True, 'use_stealth': False, 'use_autoupdate': True, - 'autoupdate_timestamp': None + 'autoupdate_timestamp': None, + 'save_private_key': False, + 'private_key': '', + 'hidservauth_string': '' } self._settings = {} self.fill_in_defaults() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 17b4cfc1..18c0f642 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -80,7 +80,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.file_selection.file_list.files_updated.connect(self.server_status.update) self.server_status.url_copied.connect(self.copy_url) self.server_status.hidservauth_copied.connect(self.copy_hidservauth) - self.server_status.private_key_saved.connect(self.private_key_saved) self.starting_server_step2.connect(self.start_server_step2) self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) @@ -473,13 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow): common.log('OnionShareGui', 'copy_hidservauth') self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000) - def private_key_saved(self): - """ - When the private key gets saved, display this in the status bar. - """ - common.log('OnionShareGui', 'private_key_saved') - self.status_bar.showMessage(strings._('gui_private_key_saved', True), 2000) - def clear_message(self): """ Clear messages from the status bar. diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 1a5d0807..7e8706b9 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -32,7 +32,6 @@ class ServerStatus(QtWidgets.QVBoxLayout): server_stopped = QtCore.pyqtSignal() url_copied = QtCore.pyqtSignal() hidservauth_copied = QtCore.pyqtSignal() - private_key_saved = QtCore.pyqtSignal() STATUS_STOPPED = 0 STATUS_WORKING = 1 @@ -90,13 +89,10 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.copy_url_button.clicked.connect(self.copy_url) self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) - self.save_private_key_button = QtWidgets.QPushButton(strings._('gui_save_private_key', True)) - self.save_private_key_button.clicked.connect(self.save_private_key) url_layout = QtWidgets.QHBoxLayout() url_layout.addWidget(self.url_label) url_layout.addWidget(self.copy_url_button) url_layout.addWidget(self.copy_hidservauth_button) - url_layout.addWidget(self.save_private_key_button) # add the widgets self.addLayout(shutdown_timeout_layout_group) @@ -146,9 +142,8 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)) self.url_label.show() self.copy_url_button.show() - if not self.settings.get('private_key'): - self.save_private_key_button.show() - self.save_private_key_button.setEnabled(True) + if self.settings.get('save_private_key'): + common.log('ServerStatus', 'update', '@TODO need to show a "persistent URL in use" label') if self.app.stealth: self.copy_hidservauth_button.show() @@ -162,7 +157,6 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.hide() self.copy_url_button.hide() self.copy_hidservauth_button.hide() - self.save_private_key_button.hide() # button if self.file_selection.get_num_files() == 0: @@ -259,16 +253,3 @@ class ServerStatus(QtWidgets.QVBoxLayout): clipboard.setText(self.app.auth_string) self.hidservauth_copied.emit() - - def save_private_key(self): - """ - Save the Onion private key to settings, so the Onion URL can be re-used. - """ - self.save_private_key_button.setEnabled(False) - self.settings.set('private_key', self.app.private_key) - if self.app.stealth: - self.settings.set('hidservauth_string', self.app.auth_string) - self.settings.save() - self.settings.load() - - self.private_key_saved.emit() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 3de3ef85..2aaaab47 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -291,12 +291,11 @@ class SettingsDialog(QtWidgets.QDialog): else: self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked) - save_private_key = self.old_settings.get('private_key') + 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) - self.save_private_key_checkbox.hide() use_stealth = self.old_settings.get('use_stealth') if use_stealth: @@ -565,9 +564,11 @@ 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('hidservauth_string', self.old_settings.get('hidservauth_string')) else: + settings.set('save_private_key', False) settings.set('private_key', '') # Also unset the HidServAuth if we are removing our reusable private key settings.set('hidservauth_string', '') diff --git a/share/locale/en.json b/share/locale/en.json index 2ec21580..ae9da805 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -46,7 +46,6 @@ "gui_canceled": "Canceled", "gui_copied_url": "Copied URL to clipboard", "gui_copied_hidservauth": "Copied HidServAuth line to clipboard", - "gui_private_key_saved": "Private key saved to settings", "gui_starting_server1": "Starting Tor onion service...", "gui_starting_server2": "Crunching files...", "gui_please_wait": "Please wait...", @@ -124,6 +123,5 @@ "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", - "gui_save_private_key": "Save private key", - "gui_save_private_key_checkbox": "Should the private key be saved for re-use?\nThis makes the Onion share URL persistent.\nUnchecking will remove the private key from settings." + "gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)" } From efb9ba2e268cd6d3b2d21ef4e84214710e079f3b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 13 Jan 2018 21:03:44 +1100 Subject: [PATCH 17/22] Attempt to fix tests --- test/test_onionshare_settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 1489c348..0e03080f 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -57,7 +57,10 @@ class TestSettings: 'systray_notifications': True, 'use_stealth': False, 'use_autoupdate': True, - 'autoupdate_timestamp': None + 'autoupdate_timestamp': None, + 'save_private_key': False, + 'private_key': '', + 'hidservauth_string': '' } def test_fill_in_defaults(self, settings_obj): From 339eb09b57de12b36477862c3a509e1dc528e417 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 14 Jan 2018 18:41:54 +1100 Subject: [PATCH 18/22] Add a 'persistent URL' label when re-using a private key --- onionshare_gui/onionshare_gui.py | 12 +++++++++++- onionshare_gui/server_status.py | 6 +----- share/locale/en.json | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 18c0f642..ca332f74 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.settings) + self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection) 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) @@ -329,6 +335,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 @@ -357,6 +366,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 7e8706b9..48bffdca 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -22,7 +22,6 @@ from .alert import Alert from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings, common -from onionshare.settings import Settings class ServerStatus(QtWidgets.QVBoxLayout): """ @@ -37,7 +36,7 @@ class ServerStatus(QtWidgets.QVBoxLayout): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, qtapp, app, web, file_selection, settings): + def __init__(self, qtapp, app, web, file_selection): super(ServerStatus, self).__init__() self.status = self.STATUS_STOPPED @@ -45,7 +44,6 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.app = app 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 @@ -142,8 +140,6 @@ class ServerStatus(QtWidgets.QVBoxLayout): self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug)) self.url_label.show() self.copy_url_button.show() - if self.settings.get('save_private_key'): - common.log('ServerStatus', 'update', '@TODO need to show a "persistent URL in use" label') if self.app.stealth: self.copy_hidservauth_button.show() diff --git a/share/locale/en.json b/share/locale/en.json index ae9da805..d52e0434 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -123,5 +123,6 @@ "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", - "gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)" + "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" } From 3d6ccac376629504c32743b2e9824c3733667bc3 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 14 Jan 2018 18:45:10 +1100 Subject: [PATCH 19/22] Resolve conflict with upstream's onion.py --- onionshare/onion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/onionshare/onion.py b/onionshare/onion.py index 3e30761a..cf761bb9 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -445,7 +445,10 @@ class Onion(object): self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie) self.settings.save() - return onion_host + if onion_host is not None: + return onion_host + else: + raise TorErrorProtocolError(strings._('error_tor_protocol_error')) def cleanup(self, stop_tor=True): """ From 2c22fcc2e09a55274a78dd9198732aec36b8c05a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 14 Jan 2018 20:36:43 +1100 Subject: [PATCH 20/22] Catch the OSError in CLI mode when a file/folder can't be zipped up, and error out more gracefully --- onionshare/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 705ecc79..3f8d8cad 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -98,8 +98,12 @@ def main(cwd=None): # Prepare files to share print(strings._("preparing_files")) - web.set_file_info(filenames) - app.cleanup_filenames.append(web.zip_filename) + try: + web.set_file_info(filenames) + app.cleanup_filenames.append(web.zip_filename) + except OSError as e: + print(e.strerror) + sys.exit(1) # Warn about sending large files over Tor if web.zip_filesize >= 157286400: # 150mb From e8a4d513b4bc7bf1b743ad20e8cf989172236a4b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 15 Jan 2018 10:01:34 +1100 Subject: [PATCH 21/22] Save the slug when using a persistent private key --- onionshare/__init__.py | 14 ++++++++++++-- onionshare/settings.py | 1 + onionshare/web.py | 11 +++++++---- onionshare_gui/onionshare_gui.py | 4 ++-- onionshare_gui/server_status.py | 11 +++++++++-- onionshare_gui/settings_dialog.py | 2 ++ test/test_onionshare_settings.py | 1 + 7 files changed, 34 insertions(+), 10 deletions(-) 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/settings.py b/onionshare/settings.py index e87638f1..e8103757 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -63,6 +63,7 @@ class Settings(object): 'autoupdate_timestamp': None, 'save_private_key': False, 'private_key': '', + 'slug': '', 'hidservauth_string': '' } self._settings = {} 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 452e58db..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) @@ -278,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 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 a978c403..ca2cc4c7 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -570,10 +570,12 @@ class SettingsDialog(QtWidgets.QDialog): 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()) diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 0e03080f..ba45ef5e 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -60,6 +60,7 @@ class TestSettings: 'autoupdate_timestamp': None, 'save_private_key': False, 'private_key': '', + 'slug': '', 'hidservauth_string': '' } From b61b3c7b2374a0691c25c768fd57b81817145fa0 Mon Sep 17 00:00:00 2001 From: Freddy Martinez Date: Sun, 14 Jan 2018 18:45:18 -0800 Subject: [PATCH 22/22] this commit cleans up the README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ad1263eb..c83636b0 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,21 @@ [![Build Status](https://travis-ci.org/micahflee/onionshare.png)](https://travis-ci.org/micahflee/onionshare) -OnionShare lets you securely and anonymously share files of any size. It works by starting a web server, making it accessible as a Tor onion service, and generating an unguessable URL to access and download the files. It doesn't require setting up a server on the internet somewhere or using a third party file-sharing service. You host the file on your own computer and use a Tor onion service to make it temporarily accessible over the internet. The other user just needs to use Tor Browser to download the file from you. +[OnionShare](https://onionshare.org) lets you securely and anonymously share files of any size. It works by starting a web server, making it accessible as a Tor Onion Service, and generating an unguessable URL to access and download the files. It does _not_ require setting up a separate server or using a third party file-sharing service. You host the files on your own computer and use a Tor Onion Service to make it temporarily accessible over the internet. The receiving user just needs to open the URL in Tor Browser to download the file. -**To learn how OnionShare works, what its security properties are, and how to use it, check out the [wiki](https://github.com/micahflee/onionshare/wiki).** +## Documentation -**You can download OnionShare for Windows and macOS from . It should be available in your package manager for Linux, and it's included by default in Tails.** +To learn how OnionShare works, what its security properties are, and how to use it, check out the [wiki](https://github.com/micahflee/onionshare/wiki). + +## Downloading Onionshare + +You can download OnionShare for Windows and macOS from the [OnionShare website](https://onionshare.org). It should be available in your package manager for Linux, and it's included by default in [Tails](https://tails.boum.org). + +## Developing OnionShare You can set up your development environment to build OnionShare yourself by following [these instructions](/BUILD.md). +# Screenshots + ![Server Screenshot](/screenshots/server.png) ![Client Screenshot](/screenshots/client.png)