summaryrefslogtreecommitdiff
path: root/cli/onionshare_cli/web
diff options
context:
space:
mode:
Diffstat (limited to 'cli/onionshare_cli/web')
-rw-r--r--cli/onionshare_cli/web/chat_mode.py7
-rw-r--r--cli/onionshare_cli/web/receive_mode.py18
-rw-r--r--cli/onionshare_cli/web/send_base_mode.py4
-rw-r--r--cli/onionshare_cli/web/share_mode.py18
-rw-r--r--cli/onionshare_cli/web/web.py159
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):
"""