class UserSecurity(object): _model = User _share = False def _verify_password(self, mobile_or_token, password): if request.method == 'OPTIONS': token = request.values.get('token', False) if not token: return True token = request.values.get('token', None) if (not token) and mobile_or_token and (not password): token = mobile_or_token if token: user = get_user_from_auth_token(token, self._model) if user and user.active: g.current_user = user g.token_used = True return True if mobile_or_token and password: user = None if hasattr(self._model, 'mobile') and hasattr( self._model, 'email'): user = self._model.query.filter( or_(self._model.mobile == str(mobile_or_token), self._model.email == str(mobile_or_token))).first() elif not hasattr(self._model, 'mobile') and hasattr( self._model, 'email'): user = self._model.query.filter( self._model.email == str(mobile_or_token)).first() if user and user.active and user.check_password(password): # set token user.generate_auth_token() g.current_user = user g.token_used = False return True if self._share: return True raise_401_response(message=u'用户名或者密码错误') def __init__(self, app=None, db=None): self.app = app self.db = db self.auth = HTTPBasicAuth() self.auth.error_handler(unauthorized) self.auth.verify_password(self._verify_password) if app is not None and db is not None: self.init_app(app, db) def init_app(self, app, db): if app is None: raise () self.app = app self.db = db
class Auth: def __init__(self): """Creates access control class for authentication and authorization.""" self._basic_auth = HTTPBasicAuth() self._token_auth = HTTPTokenAuth() self._auth = MultiAuth(self._basic_auth, self._token_auth) self._resources = {} def error_handler(self, f: typing.Callable) -> typing.NoReturn: """Set error handler for Authentication Errors. :param f: error handler. :return: NoReturn """ self._token_auth.error_handler(f) self._basic_auth.error_handler(f) def verify_password(self, f: typing.Callable) -> typing.Any: """ Verifies basic password. :param f: function defining verification process. :return: Any """ return self._basic_auth.verify_password(f) def verify_token(self, f: typing.Callable) -> typing.Any: """ Verifies token. :param f: function defining verification process. :return: Any """ return self._token_auth.verify_token(f) def login_required(self, f: typing.Callable = None, role: typing.Text = None) -> typing.Any: """ Identifies as login required for provided function {f}. :param f: input function. :param role: user role :return: func """ return self._auth.login_required(f, role)
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="share"): self.common = common 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"), static_url_path="/static_".format( 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 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) 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 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 = "/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, ) 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.common.settings.get("public_mode"): @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("{}/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"]) 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, "{}".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) return self.add_security_headers(r) def error405(self): 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.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")) 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, persistent_password=None): 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), ) else: self.password = self.common.build_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") 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, stay_open=False, public_mode=False, password=None): """ Start the flask web server. """ self.common.log( "Web", "start", "port={}, stay_open={}, public_mode={}, password={}".format( port, stay_open, public_mode, password), ) self.stay_open = stay_open # 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 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: 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
class BaseSecurity(object): _model = None _auth_model = None _ignore_auth = False def _get_user_from_auth_token(self, token): s = Serializer(current_app.config['SECRET_KEY'], salt=current_app.config['SECRET_SALT']) try: data = s.loads(token) except SignatureExpired: return None except BadSignature: return None return self._model.get_by_id(data['id']) def _verify_password(self, access_id_or_token, password): token = request.values.get('token', None) if request.method == 'OPTIONS' and not token: return True # 是否忽略验证 if self._ignore_auth: return True # 登录名、密码登录 if access_id_or_token and password: user_auth = self._auth_model.query.filter_by( access_id=access_id_or_token).first() if not user_auth or not user_auth.user: raise_401_response(message=u'该用户未注册') user = user_auth.user # FIXME: 0需要与数据库常量同步 if (not user) or user.status != 0: raise_401_response(message=u'该用户未注册') if bcrypt.check_password_hash(user_auth.access_token, password): g.current_user = user g.token_used = False return True else: raise_401_response(message=u'用户名或密码错误') # 验证token if access_id_or_token: user = self._get_user_from_auth_token(access_id_or_token) if not user: raise_401_response(message=u'请重新登录') # FIXME: 0需要与数据库常量同步 if (not user) or user.status != 0: raise_401_response(message=u'请重新登录') g.current_user = user g.token_used = True return True raise_401_response(message=u'请登录') def __init__(self, app=None, db=None): self.app = app self.db = db self.auth = HTTPBasicAuth() self.auth.error_handler(_unauthorized) self.auth.verify_password(self._verify_password) if app is not None and db is not None: self.init_app(app, db) def init_app(self, app, db): if app is None: raise self.app = app self.db = db
db = client['run'] foo_collection = db['foo'] foo_collection.insert_one({'message': 'db created'}) mongo = PyMongo(app, uri=f'{mongodb_url}/run') app.config['SECRET_KEY'] = 'mql' app.config['mongo'] = mongo if not User.get_user('mql', app.config['mongo']): common_user = User(user_name='mql', passwd_hash='python', mongo=app.config['mongo']) common_user.save() CORS(app, supports_credentials=True) auth = HTTPBasicAuth() # 此处修改了httpauth包中的error_handler,直接返回json信息 auth.error_handler(my_auth_error) @app.route('/api/treemap', methods=['POST']) # @app.route('/api/treemap', methods=['GET']) def query_treemap(): project_name = request.json.get('project_name') version_name = request.json.get('version_name') # project_name = 'alibaba/cooma' # version_name = '0.4.0' version = app.config['mongo'].db.versions.find_one({ 'project_name': project_name, 'version_name': version_name })