diff options
Diffstat (limited to 'desktop/onionshare/web/web.py')
-rw-r--r-- | desktop/onionshare/web/web.py | 426 |
1 files changed, 0 insertions, 426 deletions
diff --git a/desktop/onionshare/web/web.py b/desktop/onionshare/web/web.py deleted file mode 100644 index 117ea83a..00000000 --- a/desktop/onionshare/web/web.py +++ /dev/null @@ -1,426 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2020 Micah Lee, et al. <micah@micahflee.com> - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see <http://www.gnu.org/licenses/>. -""" - -import hmac -import logging -import os -import queue -import socket -import sys -import tempfile -import requests -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_httpauth import HTTPBasicAuth -from flask_socketio import SocketIO - -from .. import strings - -from .share_mode import ShareModeWeb -from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest -from .website_mode import WebsiteModeWeb -from .chat_mode import ChatModeWeb - -# Stub out flask's show_server_banner function, to avoiding showing warnings that -# are not applicable to OnionShare -def stubbed_show_server_banner(env, debug, app_import_path, eager_loading): - pass - - -try: - flask.cli.show_server_banner = stubbed_show_server_banner -except: - pass - - -class Web: - """ - The Web object is the OnionShare web server, powered by flask - """ - - REQUEST_LOAD = 0 - REQUEST_STARTED = 1 - REQUEST_PROGRESS = 2 - REQUEST_CANCELED = 3 - REQUEST_RATE_LIMIT = 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 - REQUEST_INVALID_PASSWORD = 14 - - def __init__(self, common, is_gui, mode_settings, mode="share"): - self.common = common - self.common.log("Web", "__init__", f"is_gui={is_gui}, mode={mode}") - - self.settings = mode_settings - - # The flask app - self.app = Flask( - __name__, - static_folder=self.common.get_resource_path("static"), - static_url_path=f"/static_{self.common.random_string(16)}", # randomize static_url_path to avoid making /static unusable - 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() - self.auth.error_handler(self.error401) - - # Verbose mode? - if self.common.verbose: - self.verbose_mode() - - # Are we running in GUI mode? - self.is_gui = is_gui - - # If the user stops the server while a transfer is in progress, it should - # immediately stop the transfer. In order to make it thread-safe, stop_q - # is a queue. If anything is in it, then the user stopped the server - self.stop_q = queue.Queue() - - # Are we using receive mode? - self.mode = mode - 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 - self.app.request_class = ReceiveModeRequest - - # Starting in Flask 0.11, render_template_string autoescapes template variables - # 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"): - # 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"), - ] - - self.q = queue.Queue() - self.password = None - - self.reset_invalid_passwords() - - self.done = False - - # shutting down the server only works within the context of flask, so the easiest way to do it is over http - self.shutdown_password = self.common.random_string(16) - - # Keep track if the server is running - self.running = False - - # Define the web app routes - self.define_common_routes() - - # Create the mode web object, which defines its own routes - self.share_mode = None - self.receive_mode = None - self.website_mode = None - self.chat_mode = None - if self.mode == "share": - self.share_mode = ShareModeWeb(self.common, self) - elif self.mode == "receive": - self.receive_mode = ReceiveModeWeb(self.common, self) - elif self.mode == "website": - self.website_mode = WebsiteModeWeb(self.common, self) - elif self.mode == "chat": - self.socketio = SocketIO() - self.socketio.init_app(self.app) - self.chat_mode = ChatModeWeb(self.common, self) - - def get_mode(self): - if self.mode == "share": - return self.share_mode - elif self.mode == "receive": - return self.receive_mode - elif self.mode == "website": - return self.website_mode - elif self.mode == "chat": - return self.chat_mode - else: - return None - - 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 = f"/static_{self.common.random_string(16)}" - self.common.log( - "Web", - "generate_static_url_path", - f"new static_url_path is {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, - ) - - def define_common_routes(self): - """ - 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.errorhandler(404) - def not_found(e): - mode = self.get_mode() - history_id = mode.cur_history_id - mode.cur_history_id += 1 - return self.error404(history_id) - - @self.app.route("/<password_candidate>/shutdown") - def shutdown(password_candidate): - """ - Stop the flask web server, from the context of an http request. - """ - if password_candidate == self.shutdown_password: - self.force_shutdown() - return "" - abort(404) - - if self.mode != "website": - - @self.app.route("/favicon.ico") - def favicon(): - return send_file( - 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) - - def error404(self, history_id): - self.add_request( - self.REQUEST_INDIVIDUAL_FILE_STARTED, - 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 - ) - return self.add_security_headers(r) - - def error405(self, history_id): - self.add_request( - self.REQUEST_INDIVIDUAL_FILE_STARTED, - request.path, - {"id": history_id, "status_code": 405}, - ) - - 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) - - 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'; 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")) - - 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}) - - def generate_password(self, saved_password=None): - self.common.log("Web", "generate_password", f"saved_password={saved_password}") - if saved_password != 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. - """ - 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) - - 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. - """ - # Shutdown the flask service - try: - func = request.environ.get("werkzeug.server.shutdown") - if func is None: - raise RuntimeError("Not running with the Werkzeug Server") - func() - except: - pass - self.running = False - - def start(self, port): - """ - Start the flask web server. - """ - self.common.log("Web", "start", f"port={port}") - - # Make sure the stop_q is empty when starting a new server - while not self.stop_q.empty(): - try: - self.stop_q.get(block=False) - except queue.Empty: - 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" - else: - host = "127.0.0.1" - - self.running = True - if self.mode == "chat": - self.socketio.run(self.app, host=host, port=port) - else: - self.app.run(host=host, port=port, threaded=True) - - def stop(self, port): - """ - Stop the flask web server by loading /shutdown. - """ - self.common.log("Web", "stop", "stopping server") - - # Let the mode know that the user stopped the server - self.stop_q.put(True) - - # 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: - requests.get( - f"http://127.0.0.1:{port}/{self.shutdown_password}/shutdown" - ) - - # Reset any password that was in use - self.password = None |