diff options
Diffstat (limited to 'onionshare/web/web.py')
-rw-r--r-- | onionshare/web/web.py | 186 |
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 |