From e89a74729b2baa15c25ba6837b3fc4a273095aea Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 21 Jul 2018 17:06:11 +1000 Subject: [PATCH 1/5] Expand 'public mode' (optional slugs) to be possible for sharing too, not just receiving, with no rate-limiting/self-destruct on invalid routes. --- onionshare/__init__.py | 4 +- onionshare/settings.py | 4 +- onionshare/web.py | 68 +++++++++++++++++++++---------- onionshare_gui/mode.py | 7 ++-- onionshare_gui/server_status.py | 2 +- onionshare_gui/settings_dialog.py | 42 +++++++++++-------- share/locale/en.json | 3 +- share/templates/send.html | 4 ++ 8 files changed, 87 insertions(+), 47 deletions(-) diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 1cebc4e3..becca93f 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -128,7 +128,7 @@ def main(cwd=None): print('') # Start OnionShare http service in new thread - t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('slug'))) + t = threading.Thread(target=web.start, args=(app.port, stay_open, common.settings.get('public_mode'), common.settings.get('slug'))) t.daemon = True t.start() @@ -147,7 +147,7 @@ def main(cwd=None): common.settings.save() # Build the URL - if receive and common.settings.get('receive_public_mode'): + if common.settings.get('public_mode'): url = 'http://{0:s}'.format(app.onion_host) else: url = 'http://{0:s}/{1:s}'.format(app.onion_host, web.slug) diff --git a/onionshare/settings.py b/onionshare/settings.py index 6d551ca0..c0e0e30c 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -70,11 +70,11 @@ class Settings(object): 'tor_bridges_use_custom_bridges': '', 'save_private_key': False, 'private_key': '', + 'public_mode': False, 'slug': '', 'hidservauth_string': '', 'downloads_dir': self.build_default_downloads_dir(), - 'receive_allow_receiver_shutdown': True, - 'receive_public_mode': False + 'receive_allow_receiver_shutdown': True } self._settings = {} self.fill_in_defaults() diff --git a/onionshare/web.py b/onionshare/web.py index 94fc5396..60f1d22d 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -143,11 +143,19 @@ class Web(object): """ @self.app.route("/") def index(slug_candidate): + self.check_slug_candidate(slug_candidate) + return index_logic() + + @self.app.route("/") + def index_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return index_logic() + + def index_logic(slug_candidate=''): """ Render the template for the onionshare landing page. """ - self.check_slug_candidate(slug_candidate) - self.add_request(Web.REQUEST_LOAD, request.path) # Deny new downloads if "Stop After First Download" is checked and there is @@ -158,22 +166,39 @@ class Web(object): return self.add_security_headers(r) # If download is allowed to continue, serve download page - r = make_response(render_template( - 'send.html', - slug=self.slug, - file_info=self.file_info, - filename=os.path.basename(self.zip_filename), - filesize=self.zip_filesize, - filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + if self.slug: + r = make_response(render_template( + 'send.html', + slug=self.slug, + file_info=self.file_info, + filename=os.path.basename(self.zip_filename), + filesize=self.zip_filesize, + filesize_human=self.common.human_readable_filesize(self.zip_filesize))) + else: + # If download is allowed to continue, serve download page + r = make_response(render_template( + 'send.html', + file_info=self.file_info, + filename=os.path.basename(self.zip_filename), + filesize=self.zip_filesize, + filesize_human=self.common.human_readable_filesize(self.zip_filesize))) return self.add_security_headers(r) @self.app.route("//download") def download(slug_candidate): + self.check_slug_candidate(slug_candidate) + return download_logic() + + @self.app.route("/download") + def download_public(): + if not self.common.settings.get('public_mode'): + return self.error404() + return download_logic() + + def download_logic(slug_candidate=''): """ Download the zip file. """ - self.check_slug_candidate(slug_candidate) - # Deny new downloads if "Stop After First Download" is checked and there is # currently a download deny_download = not self.stay_open and self.download_in_progress @@ -288,7 +313,7 @@ class Web(object): def index_logic(): self.add_request(Web.REQUEST_LOAD, request.path) - if self.common.settings.get('receive_public_mode'): + if self.common.settings.get('public_mode'): upload_action = '/upload' close_action = '/close' else: @@ -309,7 +334,7 @@ class Web(object): @self.app.route("/") def index_public(): - if not self.common.settings.get('receive_public_mode'): + if not self.common.settings.get('public_mode'): return self.error404() return index_logic() @@ -332,7 +357,7 @@ class Web(object): valid = False if not valid: flash('Error uploading, please inform the OnionShare user') - if self.common.settings.get('receive_public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -395,7 +420,7 @@ class Web(object): for filename in filenames: flash('Uploaded {}'.format(filename)) - if self.common.settings.get('receive_public_mode'): + if self.common.settings.get('public_mode'): return redirect('/') else: return redirect('/{}'.format(slug_candidate)) @@ -407,7 +432,7 @@ class Web(object): @self.app.route("/upload", methods=['POST']) def upload_public(): - if not self.common.settings.get('receive_public_mode'): + if not self.common.settings.get('public_mode'): return self.error404() return upload_logic() @@ -428,7 +453,7 @@ class Web(object): @self.app.route("/close", methods=['POST']) def close_public(): - if not self.common.settings.get('receive_public_mode'): + if not self.common.settings.get('public_mode'): return self.error404() return close_logic() @@ -458,7 +483,7 @@ class Web(object): self.error404_count += 1 # In receive mode, with public mode enabled, skip rate limiting 404s - if not (self.receive_mode and self.common.settings.get('receive_public_mode')): + if not self.common.settings.get('public_mode'): if self.error404_count == 20: self.add_request(Web.REQUEST_RATE_LIMIT, request.path) self.force_shutdown() @@ -563,12 +588,13 @@ class Web(object): pass self.running = False - def start(self, port, stay_open=False, persistent_slug=None): + def start(self, port, stay_open=False, public_mode=False, persistent_slug=None): """ Start the flask web server. """ self.common.log('Web', 'start', 'port={}, stay_open={}, persistent_slug={}'.format(port, stay_open, persistent_slug)) - self.generate_slug(persistent_slug) + if not public_mode: + self.generate_slug(persistent_slug) self.stay_open = stay_open @@ -719,7 +745,7 @@ class ReceiveModeRequest(Request): if self.path == '/{}/upload'.format(self.web.slug): self.upload_request = True else: - if self.web.common.settings.get('receive_public_mode'): + if self.web.common.settings.get('public_mode'): if self.path == '/upload': self.upload_request = True diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode.py index d2579d2c..418afffd 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode.py @@ -144,13 +144,14 @@ class Mode(QtWidgets.QWidget): self.app.choose_port() # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('slug'))) + t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) t.daemon = True t.start() # Wait for the web app slug to generate before continuing - while self.web.slug == None: - time.sleep(0.1) + if not self.common.settings.get('public_mode'): + while self.web.slug == None: + time.sleep(0.1) # Now start the onion service try: diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 1562ee10..e016e8f9 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -314,7 +314,7 @@ class ServerStatus(QtWidgets.QWidget): """ Returns the OnionShare URL. """ - if self.mode == ServerStatus.MODE_RECEIVE and self.common.settings.get('receive_public_mode'): + if self.common.settings.get('public_mode'): url = 'http://{0:s}'.format(self.app.onion_host) else: url = 'http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 94480205..057c7e53 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -52,6 +52,25 @@ class SettingsDialog(QtWidgets.QDialog): self.system = platform.system() + # General options + + # Whether or not to save the Onion private key for reuse (persistent URL mode) + 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)) + + # Use a slug + self.public_mode_checkbox = QtWidgets.QCheckBox() + self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox", True)) + + # General options layout + general_group_layout = QtWidgets.QVBoxLayout() + general_group_layout.addWidget(self.save_private_key_checkbox) + general_group_layout.addWidget(self.public_mode_checkbox) + general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label", True)) + general_group.setLayout(general_group_layout) + # Sharing options # Close after first download @@ -64,16 +83,10 @@ class SettingsDialog(QtWidgets.QDialog): self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", 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.shutdown_timeout_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) @@ -93,16 +106,10 @@ class SettingsDialog(QtWidgets.QDialog): self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Checked) self.receive_allow_receiver_shutdown_checkbox.setText(strings._("gui_settings_receive_allow_receiver_shutdown_checkbox", True)) - # Use a slug - self.receive_public_mode_checkbox = QtWidgets.QCheckBox() - self.receive_public_mode_checkbox.setCheckState(QtCore.Qt.Checked) - self.receive_public_mode_checkbox.setText(strings._("gui_settings_receive_public_mode_checkbox", True)) - # Receiving options layout receiving_group_layout = QtWidgets.QVBoxLayout() receiving_group_layout.addLayout(downloads_layout) receiving_group_layout.addWidget(self.receive_allow_receiver_shutdown_checkbox) - receiving_group_layout.addWidget(self.receive_public_mode_checkbox) receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label", True)) receiving_group.setLayout(receiving_group_layout) @@ -377,6 +384,7 @@ class SettingsDialog(QtWidgets.QDialog): # Layout left_col_layout = QtWidgets.QVBoxLayout() + left_col_layout.addWidget(general_group) left_col_layout.addWidget(sharing_group) left_col_layout.addWidget(receiving_group) left_col_layout.addWidget(stealth_group) @@ -431,11 +439,11 @@ class SettingsDialog(QtWidgets.QDialog): else: self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Unchecked) - receive_public_mode = self.old_settings.get('receive_public_mode') - if receive_public_mode: - self.receive_public_mode_checkbox.setCheckState(QtCore.Qt.Checked) + public_mode = self.old_settings.get('public_mode') + if public_mode: + self.public_mode_checkbox.setCheckState(QtCore.Qt.Checked) else: - self.receive_public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) use_stealth = self.old_settings.get('use_stealth') if use_stealth: @@ -819,7 +827,7 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('hidservauth_string', '') settings.set('downloads_dir', self.downloads_dir_lineedit.text()) settings.set('receive_allow_receiver_shutdown', self.receive_allow_receiver_shutdown_checkbox.isChecked()) - settings.set('receive_public_mode', self.receive_public_mode_checkbox.isChecked()) + settings.set('public_mode', self.public_mode_checkbox.isChecked()) settings.set('use_stealth', self.stealth_checkbox.isChecked()) # Always unset the HidServAuth if Stealth mode is unset if not self.stealth_checkbox.isChecked(): diff --git a/share/locale/en.json b/share/locale/en.json index b1d247d9..4624fd14 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -91,6 +91,7 @@ "gui_settings_autoupdate_timestamp": "Last checked: {}", "gui_settings_autoupdate_timestamp_never": "Never", "gui_settings_autoupdate_check_button": "Check For Upgrades", + "gui_settings_general_label": "General options", "gui_settings_sharing_label": "Sharing options", "gui_settings_close_after_first_download_option": "Stop sharing after first download", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", @@ -183,7 +184,7 @@ "gui_settings_downloads_label": "Save files to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", - "gui_settings_receive_public_mode_checkbox": "Receive mode is open to the public\n(don't prevent people from guessing the OnionShare address)", + "gui_settings_public_mode_checkbox": "OnionShare is open to the public\n(don't prevent people from guessing the OnionShare address)", "systray_close_server_title": "OnionShare Server Closed", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", diff --git a/share/templates/send.html b/share/templates/send.html index ba43f306..df1d3563 100644 --- a/share/templates/send.html +++ b/share/templates/send.html @@ -13,7 +13,11 @@
From 6de110bb22134b8bc7097aa2a9862c7a26b082a0 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 22 Jul 2018 14:58:14 +1000 Subject: [PATCH 2/5] Fix tests for public_mode --- test/test_onionshare_settings.py | 2 +- test/test_onionshare_web.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 3942ab8c..0c248341 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -66,7 +66,7 @@ class TestSettings: 'hidservauth_string': '', 'downloads_dir': os.path.expanduser('~/OnionShare'), 'receive_allow_receiver_shutdown': True, - 'receive_public_mode': False + 'public_mode': False } def test_fill_in_defaults(self, settings_obj): diff --git a/test/test_onionshare_web.py b/test/test_onionshare_web.py index 0b96359b..dbd026f4 100644 --- a/test/test_onionshare_web.py +++ b/test/test_onionshare_web.py @@ -168,9 +168,9 @@ class TestWeb: assert res.status_code == 302 assert web.running == True - def test_receive_mode_receive_public_mode_on(self, common_obj): + def test_public_mode_on(self, common_obj): web = web_obj(common_obj, True) - common_obj.settings.set('receive_public_mode', True) + common_obj.settings.set('public_mode', True) with web.app.test_client() as c: # Upload page should be accessible from both / and /[slug] @@ -182,9 +182,9 @@ class TestWeb: data2 = res.get_data() assert res.status_code == 200 - def test_receive_mode_receive_public_mode_off(self, common_obj): + def test_public_mode_off(self, common_obj): web = web_obj(common_obj, True) - common_obj.settings.set('receive_public_mode', False) + common_obj.settings.set('public_mode', False) with web.app.test_client() as c: # / should be a 404 From 6586cf6df9632e6565b8355a47886b2eecc4f7f8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 15 Sep 2018 11:36:34 +1000 Subject: [PATCH 3/5] Don't check slug candidate in public mode --- onionshare/web.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index 8044dbaf..bc06ca8c 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -143,7 +143,8 @@ class Web(object): """ @self.app.route("/") def index(slug_candidate): - self.check_slug_candidate(slug_candidate) + if not self.common.settings.get('public_mode'): + self.check_slug_candidate(slug_candidate) return index_logic() @self.app.route("/") @@ -186,7 +187,8 @@ class Web(object): @self.app.route("//download") def download(slug_candidate): - self.check_slug_candidate(slug_candidate) + if not self.common.settings.get('public_mode'): + self.check_slug_candidate(slug_candidate) return download_logic() @self.app.route("/download") @@ -329,7 +331,8 @@ class Web(object): @self.app.route("/") def index(slug_candidate): - self.check_slug_candidate(slug_candidate) + if not self.common.settings.get('public_mode'): + self.check_slug_candidate(slug_candidate) return index_logic() @self.app.route("/") @@ -427,7 +430,8 @@ class Web(object): @self.app.route("//upload", methods=['POST']) def upload(slug_candidate): - self.check_slug_candidate(slug_candidate) + if not self.common.settings.get('public_mode'): + self.check_slug_candidate(slug_candidate) return upload_logic(slug_candidate) @self.app.route("/upload", methods=['POST']) @@ -448,7 +452,8 @@ class Web(object): @self.app.route("//close", methods=['POST']) def close(slug_candidate): - self.check_slug_candidate(slug_candidate) + if not self.common.settings.get('public_mode'): + self.check_slug_candidate(slug_candidate) return close_logic(slug_candidate) @self.app.route("/close", methods=['POST']) From 8769cf7c97569bf8c7f30b572344147639ab9dda Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 15 Sep 2018 19:47:42 -0700 Subject: [PATCH 4/5] Check for public_mode in the check_slug_candidate function, to make 404 errors work again during public mode --- onionshare/web.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/onionshare/web.py b/onionshare/web.py index bc06ca8c..221c2c53 100644 --- a/onionshare/web.py +++ b/onionshare/web.py @@ -143,8 +143,7 @@ class Web(object): """ @self.app.route("/") def index(slug_candidate): - if not self.common.settings.get('public_mode'): - self.check_slug_candidate(slug_candidate) + self.check_slug_candidate(slug_candidate) return index_logic() @self.app.route("/") @@ -187,8 +186,7 @@ class Web(object): @self.app.route("//download") def download(slug_candidate): - if not self.common.settings.get('public_mode'): - self.check_slug_candidate(slug_candidate) + self.check_slug_candidate(slug_candidate) return download_logic() @self.app.route("/download") @@ -331,8 +329,7 @@ class Web(object): @self.app.route("/") def index(slug_candidate): - if not self.common.settings.get('public_mode'): - self.check_slug_candidate(slug_candidate) + self.check_slug_candidate(slug_candidate) return index_logic() @self.app.route("/") @@ -430,8 +427,7 @@ class Web(object): @self.app.route("//upload", methods=['POST']) def upload(slug_candidate): - if not self.common.settings.get('public_mode'): - self.check_slug_candidate(slug_candidate) + self.check_slug_candidate(slug_candidate) return upload_logic(slug_candidate) @self.app.route("/upload", methods=['POST']) @@ -452,8 +448,7 @@ class Web(object): @self.app.route("//close", methods=['POST']) def close(slug_candidate): - if not self.common.settings.get('public_mode'): - self.check_slug_candidate(slug_candidate) + self.check_slug_candidate(slug_candidate) return close_logic(slug_candidate) @self.app.route("/close", methods=['POST']) @@ -574,10 +569,14 @@ class Web(object): self.app.logger.addHandler(log_handler) def check_slug_candidate(self, slug_candidate, slug_compare=None): - if not slug_compare: - slug_compare = self.slug - if not hmac.compare_digest(slug_compare, slug_candidate): + self.common.log('Web', 'check_slug_candidate: slug_candidate={}, slug_compare={}'.format(slug_candidate, slug_compare)) + if self.common.settings.get('public_mode'): abort(404) + else: + if not slug_compare: + slug_compare = self.slug + if not hmac.compare_digest(slug_compare, slug_candidate): + abort(404) def force_shutdown(self): """ From 4bf69445a0f8d3dad83ddbdb3e6c0b7aacd74ac5 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sat, 15 Sep 2018 19:52:53 -0700 Subject: [PATCH 5/5] Make 404 error page look better, and remove the text that it's probably a typo, because in public mode that isn't necessarily true --- share/static/css/style.css | 8 ++++---- share/templates/404.html | 20 +++++++++++++------- share/templates/closed.html | 8 ++++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/share/static/css/style.css b/share/static/css/style.css index 7f5f4310..fd10ecdf 100644 --- a/share/static/css/style.css +++ b/share/static/css/style.css @@ -172,23 +172,23 @@ li.info { min-height: 400px; } -.closed { +.info { text-align: center; } -.closed img { +.info img { width: 120px; height: 120px; } -.closed .closed-header { +.info .info-header { font-size: 30px; font-weight: normal; color: #666666; margin: 0 0 10px 0; } -.closed .closed-description { +.info .info-description { color: #666666; margin: 0 0 20px 0; } diff --git a/share/templates/404.html b/share/templates/404.html index b704f9f2..264ca517 100644 --- a/share/templates/404.html +++ b/share/templates/404.html @@ -1,10 +1,16 @@ - - OnionShare: Error 404 - - - -

Error 404: You probably typed the OnionShare address wrong

- + + OnionShare: 404 Not Found + + + + +
+
+

+

404 Not Found

+
+
+ diff --git a/share/templates/closed.html b/share/templates/closed.html index c34e0ee4..64c8b369 100644 --- a/share/templates/closed.html +++ b/share/templates/closed.html @@ -11,11 +11,11 @@

OnionShare

-
-
+
+

-

Thank you for using OnionShare

-

You may now close this window.

+

Thank you for using OnionShare

+

You may now close this window.