diff options
-rw-r--r-- | .circleci/config.yml | 5 | ||||
-rw-r--r-- | cli/onionshare_cli/__init__.py | 21 | ||||
-rw-r--r-- | cli/onionshare_cli/web/chat_mode.py | 7 | ||||
-rw-r--r-- | cli/onionshare_cli/web/receive_mode.py | 18 | ||||
-rw-r--r-- | cli/onionshare_cli/web/send_base_mode.py | 4 | ||||
-rw-r--r-- | cli/onionshare_cli/web/share_mode.py | 13 | ||||
-rw-r--r-- | cli/onionshare_cli/web/web.py | 64 | ||||
-rw-r--r-- | cli/tests/test_cli_common.py | 2 | ||||
-rw-r--r-- | cli/tests/test_cli_settings.py | 2 | ||||
-rw-r--r-- | cli/tests/test_cli_web.py | 4 |
10 files changed, 62 insertions, 78 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 3be131b3..175595f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,3 @@ -# To run the tests, CircleCI needs these environment variables: -# QT_EMAIL - email address for a Qt account -# QT_PASSWORD - password for a Qt account -# (Unfortunately you can't install Qt without logging in.) - version: 2 workflows: version: 2 diff --git a/cli/onionshare_cli/__init__.py b/cli/onionshare_cli/__init__.py index a359f770..4bc00929 100644 --- a/cli/onionshare_cli/__init__.py +++ b/cli/onionshare_cli/__init__.py @@ -201,15 +201,6 @@ def main(cwd=None): disable_csp = bool(args.disable_csp) verbose = bool(args.verbose) - if receive: - mode = "receive" - elif website: - mode = "website" - elif chat: - mode = "chat" - else: - mode = "share" - # Verbose mode? common.verbose = verbose @@ -223,16 +214,26 @@ def main(cwd=None): if persistent_filename: mode_settings = ModeSettings(common, persistent_filename) mode_settings.set("persistent", "enabled", True) - mode_settings.set("persistent", "mode", mode) else: mode_settings = ModeSettings(common) + if receive: + mode = "receive" + elif website: + mode = "website" + elif chat: + mode = "chat" + else: + mode = "share" + if mode_settings.just_created: # This means the mode settings were just created, not loaded from disk mode_settings.set("general", "title", title) mode_settings.set("general", "public", public) mode_settings.set("general", "autostart_timer", autostart_timer) mode_settings.set("general", "autostop_timer", autostop_timer) + if persistent_filename: + mode_settings.set("persistent", "mode", mode) if mode == "share": mode_settings.set("share", "autostop_sharing", autostop_sharing) if mode == "receive": diff --git a/cli/onionshare_cli/web/chat_mode.py b/cli/onionshare_cli/web/chat_mode.py index f6dc2d1a..e92ce385 100644 --- a/cli/onionshare_cli/web/chat_mode.py +++ b/cli/onionshare_cli/web/chat_mode.py @@ -68,15 +68,12 @@ class ChatModeWeb: ) self.web.add_request(self.web.REQUEST_LOAD, request.path) - r = make_response( - render_template( + return render_template( "chat.html", static_url_path=self.web.static_url_path, username=session.get("name"), title=self.web.settings.get("general", "title"), - ) ) - return self.web.add_security_headers(r) @self.web.app.route("/update-session-username", methods=["POST"], provide_automatic_options=False) def update_session_username(): @@ -112,7 +109,7 @@ class ChatModeWeb: success=False, ) ) - return self.web.add_security_headers(r) + return r @self.web.socketio.on("joined", namespace="/chat") def joined(message): diff --git a/cli/onionshare_cli/web/receive_mode.py b/cli/onionshare_cli/web/receive_mode.py index 76abb0a8..6b106d37 100644 --- a/cli/onionshare_cli/web/receive_mode.py +++ b/cli/onionshare_cli/web/receive_mode.py @@ -86,16 +86,13 @@ class ReceiveModeWeb: ) self.web.add_request(self.web.REQUEST_LOAD, request.path) - r = make_response( - render_template( - "receive.html", - static_url_path=self.web.static_url_path, - disable_text=self.web.settings.get("receive", "disable_text"), - disable_files=self.web.settings.get("receive", "disable_files"), - title=self.web.settings.get("general", "title"), - ) + return render_template( + "receive.html", + static_url_path=self.web.static_url_path, + disable_text=self.web.settings.get("receive", "disable_text"), + disable_files=self.web.settings.get("receive", "disable_files"), + title=self.web.settings.get("general", "title") ) - return self.web.add_security_headers(r) @self.web.app.route("/upload", methods=["POST"], provide_automatic_options=False) def upload(ajax=False): @@ -222,12 +219,11 @@ class ReceiveModeWeb: ) else: # It was the last upload and the timer ran out - r = make_response( + return make_response( render_template("thankyou.html"), static_url_path=self.web.static_url_path, title=self.web.settings.get("general", "title"), ) - return self.web.add_security_headers(r) @self.web.app.route("/upload-ajax", methods=["POST"], provide_automatic_options=False) def upload_ajax_public(): diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py index e448d2dd..27de598a 100644 --- a/cli/onionshare_cli/web/send_base_mode.py +++ b/cli/onionshare_cli/web/send_base_mode.py @@ -149,10 +149,9 @@ class SendBaseModeWeb: # If filesystem_path is None, this is the root directory listing files, dirs = self.build_directory_listing(path, filenames, filesystem_path) - r = self.directory_listing_template( + return self.directory_listing_template( path, files, dirs, breadcrumbs, breadcrumbs_leaf ) - return self.web.add_security_headers(r) def build_directory_listing(self, path, filenames, filesystem_path): files = [] @@ -286,7 +285,6 @@ class SendBaseModeWeb: "filename*": "UTF-8''%s" % url_quote(basename), } r.headers.set("Content-Disposition", "inline", **filename_dict) - r = self.web.add_security_headers(r) (content_type, _) = mimetypes.guess_type(basename, strict=False) if content_type is not None: r.headers.set("Content-Type", content_type) diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py index 51ddd674..8ac4055e 100644 --- a/cli/onionshare_cli/web/share_mode.py +++ b/cli/onionshare_cli/web/share_mode.py @@ -25,7 +25,7 @@ import sys import tempfile import zipfile import mimetypes -from datetime import datetime +from datetime import datetime, timezone from flask import Response, request, render_template, make_response, abort from unidecode import unidecode from werkzeug.http import parse_date, http_date @@ -127,7 +127,7 @@ class ShareModeWeb(SendBaseModeWeb): self.download_etag = None self.gzip_etag = None - self.last_modified = datetime.utcnow() + self.last_modified = datetime.now(tz=timezone.utc) def define_routes(self): """ @@ -149,8 +149,7 @@ class ShareModeWeb(SendBaseModeWeb): and self.download_in_progress ) if deny_download: - r = make_response(render_template("denied.html")) - return self.web.add_security_headers(r) + return render_template("denied.html") # If download is allowed to continue, serve download page if self.should_use_gzip(): @@ -172,8 +171,7 @@ class ShareModeWeb(SendBaseModeWeb): and self.download_in_progress ) if deny_download: - r = make_response(render_template("denied.html")) - return self.web.add_security_headers(r) + return render_template("denied.html") # Prepare some variables to use inside generate() function below # which is outside of the request context @@ -232,7 +230,6 @@ class ShareModeWeb(SendBaseModeWeb): "filename*": "UTF-8''%s" % url_quote(basename), } r.headers.set("Content-Disposition", "attachment", **filename_dict) - r = self.web.add_security_headers(r) # guess content type (content_type, _) = mimetypes.guess_type(basename, strict=False) if content_type is not None: @@ -288,6 +285,8 @@ class ShareModeWeb(SendBaseModeWeb): if_unmod = request.headers.get("If-Unmodified-Since") if if_unmod: if_date = parse_date(if_unmod) + if if_date and not if_date.tzinfo: + if_date = if_date.replace(tzinfo=timezone.utc) # Compatible with Flask < 2.0.0 if if_date and if_date > last_modified: abort(412) elif range_header is None: diff --git a/cli/onionshare_cli/web/web.py b/cli/onionshare_cli/web/web.py index 0f2dfe7e..e12fccc7 100644 --- a/cli/onionshare_cli/web/web.py +++ b/cli/onionshare_cli/web/web.py @@ -191,6 +191,21 @@ class Web: Common web app routes between all modes. """ + @self.app.after_request + def add_security_headers(r): + """ + Add security headers to a response + """ + for header, value in self.security_headers: + r.headers.set(header, value) + # Set a CSP header unless in website mode and the user has disabled it + if not self.settings.get("website", "disable_csp") or self.mode != "website": + r.headers.set( + "Content-Security-Policy", + "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; img-src 'self' data:;", + ) + return r + @self.app.errorhandler(404) def not_found(e): mode = self.get_mode() @@ -232,10 +247,7 @@ class Web: def error403(self): self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response( - render_template("403.html", static_url_path=self.static_url_path), 403 - ) - return self.add_security_headers(r) + return render_template("403.html", static_url_path=self.static_url_path), 403 def error404(self, history_id): mode = self.get_mode() @@ -247,10 +259,7 @@ class Web: ) self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response( - render_template("404.html", static_url_path=self.static_url_path), 404 - ) - return self.add_security_headers(r) + return render_template("404.html", static_url_path=self.static_url_path), 404 def error405(self, history_id): mode = self.get_mode() @@ -262,10 +271,7 @@ class Web: ) self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response( - render_template("405.html", static_url_path=self.static_url_path), 405 - ) - return self.add_security_headers(r) + return render_template("405.html", static_url_path=self.static_url_path), 405 def error500(self, history_id): mode = self.get_mode() @@ -277,24 +283,7 @@ class Web: ) self.add_request(Web.REQUEST_OTHER, request.path) - r = make_response( - render_template("500.html", static_url_path=self.static_url_path), 500 - ) - return self.add_security_headers(r) - - def add_security_headers(self, r): - """ - Add security headers to a request - """ - for header, value in self.security_headers: - r.headers.set(header, value) - # Set a CSP header unless in website mode and the user has disabled it - if not self.settings.get("website", "disable_csp") or self.mode != "website": - r.headers.set( - "Content-Security-Policy", - "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; img-src 'self' data:;", - ) - return r + return render_template("500.html", static_url_path=self.static_url_path), 500 def _safe_select_jinja_autoescape(self, filename): if filename is None: @@ -372,9 +361,18 @@ class Web: # To stop flask, load http://shutdown:[shutdown_password]@127.0.0.1/[shutdown_password]/shutdown # (We're putting the shutdown_password in the path as well to make routing simpler) if self.running: - requests.get( - f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown" - ) + try: + requests.get( + f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown" + ) + except requests.exceptions.ConnectionError as e: + # The way flask-socketio stops a connection when running using + # eventlet is by raising SystemExit to abort all the processes. + # Hence the connections are closed and no response is returned + # to the above request. So I am just catching the ConnectionError + # to check if it was chat mode, in which case it's okay + if self.mode != "chat": + raise e def cleanup(self): """ diff --git a/cli/tests/test_cli_common.py b/cli/tests/test_cli_common.py index 3288e52b..9f113a84 100644 --- a/cli/tests/test_cli_common.py +++ b/cli/tests/test_cli_common.py @@ -169,7 +169,7 @@ class TestGetTorPaths: obfs4proxy_file_path, ) - @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux") + @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") def test_get_tor_paths_linux(self, platform_linux, common_obj): ( tor_path, diff --git a/cli/tests/test_cli_settings.py b/cli/tests/test_cli_settings.py index 4c012901..ed8d5bb9 100644 --- a/cli/tests/test_cli_settings.py +++ b/cli/tests/test_cli_settings.py @@ -123,7 +123,7 @@ class TestSettings: "~/Library/Application Support/OnionShare-testdata/onionshare.json" ) - @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux") + @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") def test_filename_linux(self, monkeypatch, platform_linux): obj = settings.Settings(common.Common()) assert obj.filename == os.path.expanduser( diff --git a/cli/tests/test_cli_web.py b/cli/tests/test_cli_web.py index f2b1af62..71bfeeeb 100644 --- a/cli/tests/test_cli_web.py +++ b/cli/tests/test_cli_web.py @@ -569,7 +569,7 @@ class TestRangeRequests: assert resp.status_code == 206 - @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux") + @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") @check_unsupported("curl", ["--version"]) def test_curl(self, temp_dir, tmpdir, common_obj): web = web_obj(temp_dir, common_obj, "share", 3) @@ -591,7 +591,7 @@ class TestRangeRequests: ] ) - @pytest.mark.skipif(sys.platform != "Linux", reason="requires Linux") + @pytest.mark.skipif(sys.platform != "linux", reason="requires Linux") @check_unsupported("wget", ["--version"]) def test_wget(self, temp_dir, tmpdir, common_obj): web = web_obj(temp_dir, common_obj, "share", 3) |