From fb42f6bfff07c9d9dabac6e370f7b8954d355c7d Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 5 Feb 2023 13:43:35 +0100 Subject: [PATCH] Make it possible to disable ratelimiter Update APScheduler Error message on missing flask-limiter --- cps/__init__.py | 55 ++++++++++++------- cps/admin.py | 3 +- cps/config_sql.py | 98 +--------------------------------- cps/main.py | 4 +- cps/templates/config_edit.html | 4 ++ cps/usermanagement.py | 5 +- cps/web.py | 6 +-- requirements.txt | 2 +- 8 files changed, 51 insertions(+), 126 deletions(-) diff --git a/cps/__init__.py b/cps/__init__.py index c3d0cbf7..9bf03761 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -28,7 +28,6 @@ import mimetypes from flask import Flask from .MyLoginManager import MyLoginManager from flask_principal import Principal -from flask_limiter import Limiter from . import logger from .cli import CliParameter @@ -42,6 +41,11 @@ from . import config_sql from . import cache_buster from . import ub, db +try: + from flask_limiter import Limiter + limiter_present = True +except ImportError: + limiter_present = False try: from flask_wtf.csrf import CSRFProtect wtf_present = True @@ -97,7 +101,10 @@ web_server = WebServer() updater_thread = Updater() -limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True) +if limiter_present: + limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True) +else: + limiter = None def create_app(): if csrf: @@ -115,21 +122,13 @@ def create_app(): if error: log.error(error) - lm.login_view = 'web.login' - lm.anonymous_user = ub.Anonymous - lm.session_protection = 'strong' if config.config_session == 1 else "basic" - - db.CalibreDB.update_config(config) - db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path) - calibre_db.init_db() - - updater_thread.init_updater(config, web_server) - # Perform dry run of updater and exit afterwards - if cli_param.dry_run: - updater_thread.dry_run() - sys.exit(0) - updater_thread.start() - + if not limiter: + log.info('*** "flask-limiter" is needed for calibre-web to run. ' + 'Please install it using pip: "pip install flask-limiter" ***') + print('*** "flask-limiter" is needed for calibre-web to run. ' + 'Please install it using pip: "pip install flask-limiter" ***') + web_server.stop(True) + sys.exit(8) if sys.version_info < (3, 0): log.info( '*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, ' @@ -146,6 +145,22 @@ def create_app(): 'Please install it using pip: "pip install flask-WTF" ***') web_server.stop(True) sys.exit(7) + + lm.login_view = 'web.login' + lm.anonymous_user = ub.Anonymous + lm.session_protection = 'strong' if config.config_session == 1 else "basic" + + db.CalibreDB.update_config(config) + db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path) + calibre_db.init_db() + + updater_thread.init_updater(config, web_server) + # Perform dry run of updater and exit afterwards + if cli_param.dry_run: + updater_thread.dry_run() + sys.exit(0) + updater_thread.start() + for res in dependency_check() + dependency_check(True): log.info('*** "{}" version does not meet the requirements. ' 'Should: {}, Found: {}, please consider installing required version ***' @@ -157,8 +172,6 @@ def create_app(): if os.environ.get('FLASK_DEBUG'): cache_buster.init_cache_busting(app) log.info('Starting Calibre Web...') - limiter.init_app(app) - # limiter.limit("2/minute")(parent) Principal(app) lm.init_app(app) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) @@ -179,6 +192,10 @@ def create_app(): config.config_goodreads_api_secret_e, config.config_use_goodreads) config.store_calibre_uuid(calibre_db, db.Library_Id) + # Configure rate limiter + app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter) + limiter.init_app(app) + # Register scheduled tasks from .schedule import register_scheduled_tasks, register_startup_tasks register_scheduled_tasks(config.schedule_reconnect) diff --git a/cps/admin.py b/cps/admin.py index 2b8606c0..f4e5fc20 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -22,7 +22,6 @@ import os import re -import base64 import json import operator import time @@ -104,7 +103,6 @@ def before_request(): if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path: logout_user() g.constants = constants - # g.user = current_user g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','') g.allow_registration = config.config_public_reg g.allow_anonymous = config.config_anonbrowse @@ -1802,6 +1800,7 @@ def _configuration_update_helper(): _config_checkbox(to_save, "config_password_special") _config_int(to_save, "config_password_min_length") reboot_required |= _config_int(to_save, "config_session") + reboot_required |= _config_checkbox(to_save, "config_ratelimiter") # Rarfile Content configuration _config_string(to_save, "config_rarfile_location") diff --git a/cps/config_sql.py b/cps/config_sql.py index d537dbbb..771b353c 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -161,108 +161,12 @@ class _Settings(_Base): config_password_upper = Column(Boolean, default=True) config_password_special = Column(Boolean, default=True) config_session = Column(Integer, default=1) + config_ratelimiter = Column(Boolean, default=True) def __repr__(self): return self.__class__.__name__ -class MailConfigSQL(object): - - def __init__(self): - self.__dict__["dirty"] = list() - - def init_config(self, session, secret_key): - self._session = session - self._settings = None - self._fernet = Fernet(secret_key) - self.load() - - def _read_from_storage(self): - if self._settings is None: - log.debug("_MailConfigSQL._read_from_storage") - self._settings = self._session.query(_Mail_Settings).first() - return self._settings - - def to_dict(self): - storage = {} - for k, v in self.__dict__.items(): - if k[0] != '_' and not k.endswith("password") and not k.endswith("secret") and not k == "cli": - storage[k] = v - return storage - - def load(self): - """Load all configuration values from the underlying storage.""" - s = self._read_from_storage() # type: _Settings - for k, v in s.__dict__.items(): - if k[0] != '_': - if v is None: - # if the storage column has no value, apply the (possible) default - column = s.__class__.__dict__.get(k) - if column.default is not None: - v = column.default.arg - if k.endswith("enc") and v is not None: - try: - setattr(s, k, self._fernet.decrypt(v).decode()) - except cryptography.exceptions.InvalidKey: - setattr(s, k, None) - else: - setattr(self, k, v) - self.__dict__["dirty"] = list() - - def save(self): - """Apply all configuration values to the underlying storage.""" - s = self._read_from_storage() # type: _Settings - for k in self.dirty: - if k[0] == '_': - continue - if hasattr(s, k): - if k.endswith("enc"): - setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode())) - else: - setattr(s, k, self.__dict__[k]) - - log.debug("_MailConfigSQL updating storage") - self._session.merge(s) - try: - self._session.commit() - except OperationalError as e: - log.error('Database error: %s', e) - self._session.rollback() - self.load() - - - def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None): - """Possibly updates a field of this object. - The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. - - :returns: `True` if the field has changed value - """ - new_value = dictionary.get(field, default) - if new_value is None: - return False - - if field not in self.__dict__: - log.warning("_ConfigSQL trying to set unknown field '%s' = %r", field, new_value) - return False - - if convertor is not None: - if encode: - new_value = convertor(new_value.encode(encode)) - else: - new_value = convertor(new_value) - - current_value = self.__dict__.get(field) - if current_value == new_value: - return False - - setattr(self, field, new_value) - return True - - def __setattr__(self, attr_name, attr_value): - super().__setattr__(attr_name, attr_value) - self.__dict__["dirty"].append(attr_name) - - # Class holds all application specific settings in calibre-web class ConfigSQL(object): # pylint: disable=no-member diff --git a/cps/main.py b/cps/main.py index 99f7391c..286b2b27 100644 --- a/cps/main.py +++ b/cps/main.py @@ -62,7 +62,7 @@ def main(): app.register_blueprint(tasks) app.register_blueprint(web) app.register_blueprint(opds) - limiter.limit("10/minute",key_func=request_username)(opds) + limiter.limit("3/minute",key_func=request_username)(opds) app.register_blueprint(jinjia) app.register_blueprint(about) app.register_blueprint(shelf) @@ -74,7 +74,7 @@ def main(): if kobo_available: app.register_blueprint(kobo) app.register_blueprint(kobo_auth) - limiter.limit("10/minute", key_func=get_remote_address)(kobo) + limiter.limit("3/minute", key_func=get_remote_address)(kobo) if oauth_available: app.register_blueprint(oauth) success = web_server.start() diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 557345b6..eec4b616 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -364,6 +364,10 @@
+
+ + +