diff options
Diffstat (limited to 'cli/onionshare_cli/web')
-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 | 18 | ||||
-rw-r--r-- | cli/onionshare_cli/web/web.py | 159 |
5 files changed, 54 insertions, 152 deletions
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..92a4c9af 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: @@ -426,10 +425,7 @@ class ShareModeWeb(SendBaseModeWeb): # Render directory listing filenames = [] for filename in os.listdir(filesystem_path): - if os.path.isdir(os.path.join(filesystem_path, filename)): - filenames.append(filename + "/") - else: - filenames.append(filename) + filenames.append(filename) filenames.sort() return self.directory_listing(filenames, path, filesystem_path) diff --git a/cli/onionshare_cli/web/web.py b/cli/onionshare_cli/web/web.py index 04919185..e12fccc7 100644 --- a/cli/onionshare_cli/web/web.py +++ b/cli/onionshare_cli/web/web.py @@ -34,7 +34,6 @@ from flask import ( send_file, __version__ as flask_version, ) -from flask_httpauth import HTTPBasicAuth from flask_socketio import SocketIO from .share_mode import ShareModeWeb @@ -64,18 +63,16 @@ class Web: REQUEST_STARTED = 1 REQUEST_PROGRESS = 2 REQUEST_CANCELED = 3 - REQUEST_RATE_LIMIT = 4 - REQUEST_UPLOAD_INCLUDES_MESSAGE = 5 - REQUEST_UPLOAD_FILE_RENAMED = 6 - REQUEST_UPLOAD_SET_DIR = 7 - REQUEST_UPLOAD_FINISHED = 8 - REQUEST_UPLOAD_CANCELED = 9 - REQUEST_INDIVIDUAL_FILE_STARTED = 10 - REQUEST_INDIVIDUAL_FILE_PROGRESS = 11 - REQUEST_INDIVIDUAL_FILE_CANCELED = 12 - REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 13 - REQUEST_OTHER = 14 - REQUEST_INVALID_PASSWORD = 15 + REQUEST_UPLOAD_INCLUDES_MESSAGE = 4 + REQUEST_UPLOAD_FILE_RENAMED = 5 + REQUEST_UPLOAD_SET_DIR = 6 + REQUEST_UPLOAD_FINISHED = 7 + REQUEST_UPLOAD_CANCELED = 8 + REQUEST_INDIVIDUAL_FILE_STARTED = 9 + REQUEST_INDIVIDUAL_FILE_PROGRESS = 10 + REQUEST_INDIVIDUAL_FILE_CANCELED = 11 + REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 12 + REQUEST_OTHER = 13 def __init__(self, common, is_gui, mode_settings, mode="share"): self.common = common @@ -92,8 +89,6 @@ class Web: ) self.app.secret_key = self.common.random_string(8) self.generate_static_url_path() - self.auth = HTTPBasicAuth() - self.auth.error_handler(self.error401) # Verbose mode? if self.common.verbose: @@ -132,9 +127,6 @@ class Web: ] self.q = queue.Queue() - self.password = None - - self.reset_invalid_passwords() self.done = False @@ -199,27 +191,20 @@ class Web: Common web app routes between all modes. """ - @self.auth.get_password - def get_pw(username): - if username == "onionshare": - return self.password - else: - return None - - @self.app.before_request - def conditional_auth_check(): - # Allow static files without basic authentication - if request.path.startswith(self.static_url_path + "/"): - return None - - # If public mode is disabled, require authentication - if not self.settings.get("general", "public"): - - @self.auth.login_required - def _check_login(): - return None - - return _check_login() + @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): @@ -260,37 +245,9 @@ class Web: f"{self.common.get_resource_path('static')}/img/favicon.ico" ) - def error401(self): - auth = request.authorization - if auth: - if ( - auth["username"] == "onionshare" - and auth["password"] not in self.invalid_passwords - ): - print(f"Invalid password guess: {auth['password']}") - self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth["password"]) - - self.invalid_passwords.append(auth["password"]) - self.invalid_passwords_count += 1 - - if self.invalid_passwords_count == 20: - self.add_request(Web.REQUEST_RATE_LIMIT) - self.force_shutdown() - print( - "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share." - ) - - r = make_response( - render_template("401.html", static_url_path=self.static_url_path), 401 - ) - return self.add_security_headers(r) - 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() @@ -302,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() @@ -317,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() @@ -332,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: @@ -362,21 +296,6 @@ class Web: """ self.q.put({"type": request_type, "path": path, "data": data}) - def generate_password(self, saved_password=None): - self.common.log("Web", "generate_password", f"saved_password={saved_password}") - if saved_password is not None and saved_password != "": - self.password = saved_password - self.common.log( - "Web", - "generate_password", - f'saved_password sent, so password is: "{self.password}"', - ) - else: - self.password = self.common.build_password() - self.common.log( - "Web", "generate_password", f'built random password: "{self.password}"' - ) - def verbose_mode(self): """ Turn on verbose mode, which will log flask errors to a file. @@ -386,10 +305,6 @@ class Web: log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - def reset_invalid_passwords(self): - self.invalid_passwords_count = 0 - self.invalid_passwords = [] - def force_shutdown(self): """ Stop the flask web server, from the context of the flask app. @@ -446,18 +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: - if self.password: - requests.get( - f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown", - auth=requests.auth.HTTPBasicAuth("onionshare", self.password), - ) - else: + try: requests.get( f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown" ) - - # Reset any password that was in use - self.password = None + 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): """ |