def create_app(config: 'SDConfig') -> Flask: app = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR, static_folder=path.join(config.SECUREDROP_ROOT, 'static')) app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS) app.session_interface = JournalistInterfaceSessionInterface() csrf = CSRFProtect(app) Environment(app) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI db.init_app(app) v2_enabled = path.exists( path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url')) v3_enabled = path.exists( path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url')) app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled) # TODO: Attaching a Storage dynamically like this disables all type checking (and # breaks code analysis tools) for code that uses current_app.storage; it should be refactored app.storage = Storage(config.STORE_DIR, config.TEMP_DIR, config.JOURNALIST_KEY) # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and # breaks code analysis tools) for code that uses current_app.storage; it should be refactored app.crypto_util = CryptoUtil( scrypt_params=config.SCRYPT_PARAMS, scrypt_id_pepper=config.SCRYPT_ID_PEPPER, scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER, securedrop_root=config.SECUREDROP_ROOT, word_list=config.WORD_LIST, nouns_file=config.NOUNS, adjectives_file=config.ADJECTIVES, gpg_key_dir=config.GPG_KEY_DIR, ) @app.errorhandler(CSRFError) def handle_csrf_error(e: CSRFError) -> 'Response': # render the message first to ensure it's localized. msg = gettext('You have been logged out due to inactivity.') session.clear() flash(msg, 'error') return redirect(url_for('main.login')) def _handle_http_exception( error: 'HTTPException' ) -> 'Tuple[Union[Response, str], Optional[int]]': # Workaround for no blueprint-level 404/5 error handlers, see: # https://github.com/pallets/flask/issues/503#issuecomment-71383286 handler = list(app.error_handler_spec['api'][error.code].values())[0] if request.path.startswith('/api/') and handler: return handler(error) return render_template('error.html', error=error), error.code for code in default_exceptions: app.errorhandler(code)(_handle_http_exception) i18n.setup_app(config, app) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.jinja_env.globals['version'] = version.__version__ app.jinja_env.filters['rel_datetime_format'] = \ template_filters.rel_datetime_format app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat @app.before_first_request def expire_blacklisted_tokens() -> None: cleanup_expired_revoked_tokens() @app.before_request def load_instance_config() -> None: app.instance_config = InstanceConfig.get_current() @app.before_request def setup_g() -> 'Optional[Response]': """Store commonly used values in Flask's special g object""" if 'expires' in session and datetime.utcnow() >= session['expires']: session.clear() flash(gettext('You have been logged out due to inactivity.'), 'error') uid = session.get('uid', None) if uid: user = Journalist.query.get(uid) if user and 'nonce' in session and \ session['nonce'] != user.session_nonce: session.clear() flash( gettext('You have been logged out due to password change'), 'error') session['expires'] = datetime.utcnow() + \ timedelta(minutes=getattr(config, 'SESSION_EXPIRATION_MINUTES', 120)) # Work around https://github.com/lepture/flask-wtf/issues/275 # -- after upgrading from Python 2 to Python 3, any existing # session's csrf_token value will be retrieved as bytes, # causing a TypeError. This simple fix, deleting the existing # token, was suggested in the issue comments. This code will # be safe to remove after Python 2 reaches EOL in 2020, and no # supported SecureDrop installations can still have this # problem. if sys.version_info.major > 2 and type( session.get('csrf_token')) is bytes: del session['csrf_token'] uid = session.get('uid', None) if uid: g.user = Journalist.query.get(uid) g.locale = i18n.get_locale(config) g.text_direction = i18n.get_text_direction(g.locale) g.html_lang = i18n.locale_to_rfc_5646(g.locale) g.locales = i18n.get_locale2name() if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']: g.show_v2_onion_eol_warning = True if request.path.split('/')[1] == 'api': pass # We use the @token_required decorator for the API endpoints else: # We are not using the API if request.endpoint not in _insecure_views and not logged_in(): return redirect(url_for('main.login')) if request.method == 'POST': filesystem_id = request.form.get('filesystem_id') if filesystem_id: g.filesystem_id = filesystem_id g.source = get_source(filesystem_id) return None app.register_blueprint(main.make_blueprint(config)) app.register_blueprint(account.make_blueprint(config), url_prefix='/account') app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin') app.register_blueprint(col.make_blueprint(config), url_prefix='/col') api_blueprint = api.make_blueprint(config) app.register_blueprint(api_blueprint, url_prefix='/api/v1') csrf.exempt(api_blueprint) return app
def create_app(config): app = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR, static_folder=path.join(config.SECUREDROP_ROOT, 'static')) app.config.from_object(config.JournalistInterfaceFlaskConfig) CSRFProtect(app) Environment(app) @app.errorhandler(CSRFError) def handle_csrf_error(e): # render the message first to ensure it's localized. msg = gettext('You have been logged out due to inactivity') session.clear() flash(msg, 'error') return redirect(url_for('main.login')) i18n.setup_app(config, app) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.jinja_env.globals['version'] = version.__version__ if hasattr(config, 'CUSTOM_HEADER_IMAGE'): app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE app.jinja_env.globals['use_custom_header_image'] = True else: app.jinja_env.globals['header_image'] = 'logo.png' app.jinja_env.globals['use_custom_header_image'] = False app.jinja_env.filters['rel_datetime_format'] = \ template_filters.rel_datetime_format app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat @app.template_filter('autoversion') def autoversion_filter(filename): """Use this template filter for cache busting""" absolute_filename = path.join(config.SECUREDROP_ROOT, filename[1:]) if path.exists(absolute_filename): timestamp = str(path.getmtime(absolute_filename)) else: return filename versioned_filename = "{0}?v={1}".format(filename, timestamp) return versioned_filename @app.teardown_appcontext def shutdown_session(exception=None): """Automatically remove database sessions at the end of the request, or when the application shuts down""" db_session.remove() @app.before_request def setup_g(): """Store commonly used values in Flask's special g object""" if 'expires' in session and datetime.utcnow() >= session['expires']: session.clear() flash(gettext('You have been logged out due to inactivity'), 'error') session['expires'] = datetime.utcnow() + \ timedelta(minutes=getattr(config, 'SESSION_EXPIRATION_MINUTES', 120)) uid = session.get('uid', None) if uid: g.user = Journalist.query.get(uid) g.locale = i18n.get_locale(config) g.text_direction = i18n.get_text_direction(g.locale) g.html_lang = i18n.locale_to_rfc_5646(g.locale) g.locales = i18n.get_locale2name() if request.endpoint not in _insecure_views and not logged_in(): return redirect(url_for('main.login')) if request.method == 'POST': filesystem_id = request.form.get('filesystem_id') if filesystem_id: g.filesystem_id = filesystem_id g.source = get_source(filesystem_id) app.register_blueprint(main.make_blueprint(config)) app.register_blueprint(account.make_blueprint(config), url_prefix='/account') app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin') app.register_blueprint(col.make_blueprint(config), url_prefix='/col') return app
def create_app(config): app = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR, static_folder=path.join(config.SECUREDROP_ROOT, 'static')) app.config.from_object(config.JournalistInterfaceFlaskConfig) CSRFProtect(app) Environment(app) @app.errorhandler(CSRFError) def handle_csrf_error(e): # render the message first to ensure it's localized. msg = gettext('You have been logged out due to inactivity') session.clear() flash(msg, 'error') return redirect(url_for('main.login')) i18n.setup_app(app) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.jinja_env.globals['version'] = version.__version__ if hasattr(config, 'CUSTOM_HEADER_IMAGE'): app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE app.jinja_env.globals['use_custom_header_image'] = True else: app.jinja_env.globals['header_image'] = 'logo.png' app.jinja_env.globals['use_custom_header_image'] = False app.jinja_env.filters['rel_datetime_format'] = \ template_filters.rel_datetime_format app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat @app.teardown_appcontext def shutdown_session(exception=None): """Automatically remove database sessions at the end of the request, or when the application shuts down""" db_session.remove() @app.before_request def setup_g(): """Store commonly used values in Flask's special g object""" if 'expires' in session and datetime.utcnow() >= session['expires']: session.clear() flash(gettext('You have been logged out due to inactivity'), 'error') session['expires'] = datetime.utcnow() + \ timedelta(minutes=getattr(config, 'SESSION_EXPIRATION_MINUTES', 120)) uid = session.get('uid', None) if uid: g.user = Journalist.query.get(uid) g.locale = i18n.get_locale() g.text_direction = i18n.get_text_direction(g.locale) g.html_lang = i18n.locale_to_rfc_5646(g.locale) g.locales = i18n.get_locale2name() if request.endpoint not in _insecure_views and not logged_in(): return redirect(url_for('main.login')) if request.method == 'POST': filesystem_id = request.form.get('filesystem_id') if filesystem_id: g.filesystem_id = filesystem_id g.source = get_source(filesystem_id) app.register_blueprint(main.make_blueprint(config)) app.register_blueprint(account.make_blueprint(config), url_prefix='/account') app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin') app.register_blueprint(col.make_blueprint(config), url_prefix='/col') return app
def create_app(config: "SDConfig") -> Flask: app = Flask( __name__, template_folder=config.JOURNALIST_TEMPLATES_DIR, static_folder=path.join(config.SECUREDROP_ROOT, "static"), ) app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS) app.session_interface = JournalistInterfaceSessionInterface() csrf = CSRFProtect(app) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SQLALCHEMY_DATABASE_URI"] = config.DATABASE_URI db.init_app(app) class JSONEncoder(json.JSONEncoder): """Custom JSON encoder to use our preferred timestamp format""" def default(self, obj: "Any") -> "Any": if isinstance(obj, datetime): return obj.strftime(API_DATETIME_FORMAT) super(JSONEncoder, self).default(obj) app.json_encoder = JSONEncoder # TODO: enable type checking once upstream Flask fix is available. See: # https://github.com/pallets/flask/issues/4295 @app.errorhandler(CSRFError) # type: ignore def handle_csrf_error(e: CSRFError) -> "Response": app.logger.error("The CSRF token is invalid.") session.clear() msg = gettext("You have been logged out due to inactivity.") flash(msg, "error") return redirect(url_for("main.login")) def _handle_http_exception( error: "HTTPException", ) -> "Tuple[Union[Response, str], Optional[int]]": # Workaround for no blueprint-level 404/5 error handlers, see: # https://github.com/pallets/flask/issues/503#issuecomment-71383286 # TODO: clean up API error handling such that all except 404/5s are # registered in the blueprint and 404/5s are handled at the application # level. handler = list(app.error_handler_spec["api"][error.code].values())[0] if request.path.startswith("/api/") and handler: return handler(error) # type: ignore return render_template("error.html", error=error), error.code for code in default_exceptions: app.errorhandler(code)(_handle_http_exception) i18n.configure(config, app) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.jinja_env.globals["version"] = version.__version__ app.jinja_env.filters["rel_datetime_format"] = template_filters.rel_datetime_format app.jinja_env.filters["filesizeformat"] = template_filters.filesizeformat app.jinja_env.filters["html_datetime_format"] = template_filters.html_datetime_format app.jinja_env.add_extension("jinja2.ext.do") @app.before_first_request def expire_blacklisted_tokens() -> None: cleanup_expired_revoked_tokens() @app.before_request def update_instance_config() -> None: InstanceConfig.get_default(refresh=True) @app.before_request def setup_g() -> "Optional[Response]": """Store commonly used values in Flask's special g object""" if "expires" in session and datetime.now(timezone.utc) >= session["expires"]: session.clear() flash(gettext("You have been logged out due to inactivity."), "error") uid = session.get("uid", None) if uid: user = Journalist.query.get(uid) if user and "nonce" in session and session["nonce"] != user.session_nonce: session.clear() flash(gettext("You have been logged out due to password change"), "error") session["expires"] = datetime.now(timezone.utc) + timedelta( minutes=getattr(config, "SESSION_EXPIRATION_MINUTES", 120) ) uid = session.get("uid", None) if uid: g.user = Journalist.query.get(uid) # pylint: disable=assigning-non-slot i18n.set_locale(config) if InstanceConfig.get_default().organization_name: g.organization_name = ( # pylint: disable=assigning-non-slot InstanceConfig.get_default().organization_name ) else: g.organization_name = gettext("SecureDrop") # pylint: disable=assigning-non-slot try: g.logo = get_logo_url(app) # pylint: disable=assigning-non-slot except FileNotFoundError: app.logger.error("Site logo not found.") if request.path.split("/")[1] == "api": pass # We use the @token_required decorator for the API endpoints else: # We are not using the API if request.endpoint not in _insecure_views and not logged_in(): return redirect(url_for("main.login")) if request.method == "POST": filesystem_id = request.form.get("filesystem_id") if filesystem_id: g.filesystem_id = filesystem_id # pylint: disable=assigning-non-slot g.source = get_source(filesystem_id) # pylint: disable=assigning-non-slot return None app.register_blueprint(main.make_blueprint(config)) app.register_blueprint(account.make_blueprint(config), url_prefix="/account") app.register_blueprint(admin.make_blueprint(config), url_prefix="/admin") app.register_blueprint(col.make_blueprint(config), url_prefix="/col") api_blueprint = api.make_blueprint(config) app.register_blueprint(api_blueprint, url_prefix="/api/v1") csrf.exempt(api_blueprint) return app