summaryrefslogtreecommitdiff
path: root/onionshare/web/web.py
diff options
context:
space:
mode:
Diffstat (limited to 'onionshare/web/web.py')
-rw-r--r--onionshare/web/web.py186
1 files changed, 122 insertions, 64 deletions
diff --git a/onionshare/web/web.py b/onionshare/web/web.py
index f3e1e07a..4c4207e6 100644
--- a/onionshare/web/web.py
+++ b/onionshare/web/web.py
@@ -10,7 +10,15 @@ from distutils.version import LooseVersion as Version
from urllib.request import urlopen
import flask
-from flask import Flask, request, render_template, abort, make_response, send_file, __version__ as flask_version
+from flask import (
+ Flask,
+ request,
+ render_template,
+ abort,
+ make_response,
+ send_file,
+ __version__ as flask_version,
+)
from flask_httpauth import HTTPBasicAuth
from .. import strings
@@ -24,6 +32,7 @@ from .website_mode import WebsiteModeWeb
def stubbed_show_server_banner(env, debug, app_import_path, eager_loading):
pass
+
try:
flask.cli.show_server_banner = stubbed_show_server_banner
except:
@@ -34,6 +43,7 @@ class Web:
"""
The Web object is the OnionShare web server, powered by flask
"""
+
REQUEST_LOAD = 0
REQUEST_STARTED = 1
REQUEST_PROGRESS = 2
@@ -50,14 +60,16 @@ class Web:
REQUEST_OTHER = 13
REQUEST_INVALID_PASSWORD = 14
- def __init__(self, common, is_gui, mode='share'):
+ def __init__(self, common, is_gui, mode="share"):
self.common = common
- self.common.log('Web', '__init__', 'is_gui={}, mode={}'.format(is_gui, mode))
+ self.common.log("Web", "__init__", "is_gui={}, mode={}".format(is_gui, mode))
# The flask app
- self.app = Flask(__name__,
- static_folder=self.common.get_resource_path('static'),
- template_folder=self.common.get_resource_path('templates'))
+ self.app = Flask(
+ __name__,
+ static_folder=self.common.get_resource_path("static"),
+ template_folder=self.common.get_resource_path("templates"),
+ )
self.app.secret_key = self.common.random_string(8)
self.generate_static_url_path()
self.auth = HTTPBasicAuth()
@@ -77,7 +89,7 @@ class Web:
# Are we using receive mode?
self.mode = mode
- if self.mode == 'receive':
+ if self.mode == "receive":
# Use custom WSGI middleware, to modify environ
self.app.wsgi_app = ReceiveModeWSGIMiddleware(self.app.wsgi_app, self)
# Use a custom Request class to track upload progess
@@ -87,16 +99,16 @@ class Web:
# by default. To prevent content injection through template variables in
# earlier versions of Flask, we force autoescaping in the Jinja2 template
# engine if we detect a Flask version with insecure default behavior.
- if Version(flask_version) < Version('0.11'):
+ if Version(flask_version) < Version("0.11"):
# Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc
Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape
self.security_headers = [
- ('X-Frame-Options', 'DENY'),
- ('X-Xss-Protection', '1; mode=block'),
- ('X-Content-Type-Options', 'nosniff'),
- ('Referrer-Policy', 'no-referrer'),
- ('Server', 'OnionShare')
+ ("X-Frame-Options", "DENY"),
+ ("X-Xss-Protection", "1; mode=block"),
+ ("X-Content-Type-Options", "nosniff"),
+ ("Referrer-Policy", "no-referrer"),
+ ("Server", "OnionShare"),
]
self.q = queue.Queue()
@@ -119,19 +131,19 @@ class Web:
self.share_mode = None
self.receive_mode = None
self.website_mode = None
- if self.mode == 'share':
+ if self.mode == "share":
self.share_mode = ShareModeWeb(self.common, self)
- elif self.mode == 'receive':
+ elif self.mode == "receive":
self.receive_mode = ReceiveModeWeb(self.common, self)
- elif self.mode == 'website':
+ elif self.mode == "website":
self.website_mode = WebsiteModeWeb(self.common, self)
def get_mode(self):
- if self.mode == 'share':
+ if self.mode == "share":
return self.share_mode
- elif self.mode == 'receive':
+ elif self.mode == "receive":
return self.receive_mode
- elif self.mode == 'website':
+ elif self.mode == "website":
return self.website_mode
else:
return None
@@ -139,14 +151,20 @@ class Web:
def generate_static_url_path(self):
# The static URL path has a 128-bit random number in it to avoid having name
# collisions with files that might be getting shared
- self.static_url_path = '/static_{}'.format(self.common.random_string(16))
- self.common.log('Web', 'generate_static_url_path', 'new static_url_path is {}'.format(self.static_url_path))
+ self.static_url_path = "/static_{}".format(self.common.random_string(16))
+ self.common.log(
+ "Web",
+ "generate_static_url_path",
+ "new static_url_path is {}".format(self.static_url_path),
+ )
# Update the flask route to handle the new static URL path
self.app.static_url_path = self.static_url_path
self.app.add_url_rule(
- self.static_url_path + '/<path:filename>',
- endpoint='static', view_func=self.app.send_static_file)
+ self.static_url_path + "/<path:filename>",
+ endpoint="static",
+ view_func=self.app.send_static_file,
+ )
def define_common_routes(self):
"""
@@ -155,7 +173,7 @@ class Web:
@self.auth.get_password
def get_pw(username):
- if username == 'onionshare':
+ if username == "onionshare":
return self.password
else:
return None
@@ -163,11 +181,12 @@ class Web:
@self.app.before_request
def conditional_auth_check():
# Allow static files without basic authentication
- if(request.path.startswith(self.static_url_path + '/')):
+ if request.path.startswith(self.static_url_path + "/"):
return None
# If public mode is disabled, require authentication
- if not self.common.settings.get('public_mode'):
+ if not self.common.settings.get("public_mode"):
+
@self.auth.login_required
def _check_login():
return None
@@ -191,46 +210,63 @@ class Web:
return ""
abort(404)
- if self.mode != 'website':
+ if self.mode != "website":
+
@self.app.route("/favicon.ico")
def favicon():
- return send_file('{}/img/favicon.ico'.format(self.common.get_resource_path('static')))
+ return send_file(
+ "{}/img/favicon.ico".format(self.common.get_resource_path("static"))
+ )
def error401(self):
auth = request.authorization
if auth:
- if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_passwords:
- print('Invalid password guess: {}'.format(auth['password']))
- self.add_request(Web.REQUEST_INVALID_PASSWORD, data=auth['password'])
-
- self.invalid_passwords.append(auth['password'])
+ if (
+ auth["username"] == "onionshare"
+ and auth["password"] not in self.invalid_passwords
+ ):
+ print("Invalid password guess: {}".format(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.")
+ 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)
+ 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)
+ r = make_response(
+ render_template("403.html", static_url_path=self.static_url_path), 403
+ )
return self.add_security_headers(r)
def error404(self, history_id):
- self.add_request(self.REQUEST_INDIVIDUAL_FILE_STARTED, '{}'.format(request.path), {
- 'id': history_id,
- 'status_code': 404
- })
+ self.add_request(
+ self.REQUEST_INDIVIDUAL_FILE_STARTED,
+ "{}".format(request.path),
+ {"id": history_id, "status_code": 404},
+ )
self.add_request(Web.REQUEST_OTHER, request.path)
- r = make_response(render_template('404.html', static_url_path=self.static_url_path), 404)
+ r = make_response(
+ render_template("404.html", static_url_path=self.static_url_path), 404
+ )
return self.add_security_headers(r)
def error405(self):
- r = make_response(render_template('405.html', static_url_path=self.static_url_path), 405)
+ r = make_response(
+ render_template("405.html", static_url_path=self.static_url_path), 405
+ )
return self.add_security_headers(r)
def add_security_headers(self, r):
@@ -240,39 +276,53 @@ class Web:
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.common.settings.get('csp_header_disabled') or self.mode != 'website':
- r.headers.set('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;')
+ if (
+ not self.common.settings.get("csp_header_disabled")
+ or self.mode != "website"
+ ):
+ r.headers.set(
+ "Content-Security-Policy",
+ "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:;",
+ )
return r
def _safe_select_jinja_autoescape(self, filename):
if filename is None:
return True
- return filename.endswith(('.html', '.htm', '.xml', '.xhtml'))
+ return filename.endswith((".html", ".htm", ".xml", ".xhtml"))
def add_request(self, request_type, path=None, data=None):
"""
Add a request to the queue, to communicate with the GUI.
"""
- self.q.put({
- 'type': request_type,
- 'path': path,
- 'data': data
- })
+ self.q.put({"type": request_type, "path": path, "data": data})
def generate_password(self, persistent_password=None):
- self.common.log('Web', 'generate_password', 'persistent_password={}'.format(persistent_password))
- if persistent_password != None and persistent_password != '':
+ self.common.log(
+ "Web",
+ "generate_password",
+ "persistent_password={}".format(persistent_password),
+ )
+ if persistent_password != None and persistent_password != "":
self.password = persistent_password
- self.common.log('Web', 'generate_password', 'persistent_password sent, so password is: "{}"'.format(self.password))
+ self.common.log(
+ "Web",
+ "generate_password",
+ 'persistent_password sent, so password is: "{}"'.format(self.password),
+ )
else:
self.password = self.common.build_password()
- self.common.log('Web', 'generate_password', 'built random password: "{}"'.format(self.password))
+ self.common.log(
+ "Web",
+ "generate_password",
+ 'built random password: "{}"'.format(self.password),
+ )
def verbose_mode(self):
"""
Turn on verbose mode, which will log flask errors to a file.
"""
- flask_log_filename = os.path.join(self.common.build_data_dir(), 'flask.log')
+ flask_log_filename = os.path.join(self.common.build_data_dir(), "flask.log")
log_handler = logging.FileHandler(flask_log_filename)
log_handler.setLevel(logging.WARNING)
self.app.logger.addHandler(log_handler)
@@ -287,9 +337,9 @@ class Web:
"""
# Shutdown the flask service
try:
- func = request.environ.get('werkzeug.server.shutdown')
+ func = request.environ.get("werkzeug.server.shutdown")
if func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
+ raise RuntimeError("Not running with the Werkzeug Server")
func()
except:
pass
@@ -299,7 +349,13 @@ class Web:
"""
Start the flask web server.
"""
- self.common.log('Web', 'start', 'port={}, stay_open={}, public_mode={}, password={}'.format(port, stay_open, public_mode, password))
+ self.common.log(
+ "Web",
+ "start",
+ "port={}, stay_open={}, public_mode={}, password={}".format(
+ port, stay_open, public_mode, password
+ ),
+ )
self.stay_open = stay_open
@@ -311,10 +367,10 @@ class Web:
pass
# In Whonix, listen on 0.0.0.0 instead of 127.0.0.1 (#220)
- if os.path.exists('/usr/share/anon-ws-base-files/workstation'):
- host = '0.0.0.0'
+ if os.path.exists("/usr/share/anon-ws-base-files/workstation"):
+ host = "0.0.0.0"
else:
- host = '127.0.0.1'
+ host = "127.0.0.1"
self.running = True
self.app.run(host=host, port=port, threaded=True)
@@ -323,7 +379,7 @@ class Web:
"""
Stop the flask web server by loading /shutdown.
"""
- self.common.log('Web', 'stop', 'stopping server')
+ self.common.log("Web", "stop", "stopping server")
# Let the mode know that the user stopped the server
self.stop_q.put(True)
@@ -331,8 +387,10 @@ 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('http://127.0.0.1:{}/{}/shutdown'.format(port, self.shutdown_password),
- auth=requests.auth.HTTPBasicAuth('onionshare', self.password))
+ requests.get(
+ "http://127.0.0.1:{}/{}/shutdown".format(port, self.shutdown_password),
+ auth=requests.auth.HTTPBasicAuth("onionshare", self.password),
+ )
# Reset any password that was in use
self.password = None