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