Make it possible to disable ratelimiter
Update APScheduler Error message on missing flask-limiter
This commit is contained in:
		
							parent
							
								
									4b7a0f3662
								
							
						
					
					
						commit
						fb42f6bfff
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -364,6 +364,10 @@
 | 
			
		|||
    </div>
 | 
			
		||||
    <div id="collapsesix" class="panel-collapse collapse">
 | 
			
		||||
      <div class="panel-body">
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
              <input type="checkbox" id="config_ratelimiter" name="config_ratelimiter" {% if config.config_ratelimiter %}checked{% endif %}>
 | 
			
		||||
              <label for="config_ratelimiter">{{_('Limit failed login attempts')}}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
              <label for="config_session">{{_('Session protection')}}</label>
 | 
			
		||||
                <select name="config_session" id="config_session" class="form-control">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ from werkzeug.security import check_password_hash
 | 
			
		|||
from flask_login import login_required, login_user
 | 
			
		||||
from flask import request, Response
 | 
			
		||||
 | 
			
		||||
from . import lm, ub, config, constants, services, logger
 | 
			
		||||
from . import lm, ub, config, constants, services, logger, limiter
 | 
			
		||||
 | 
			
		||||
log = logger.create()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +65,9 @@ def requires_basic_auth_if_no_ano(f):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def _load_user_from_auth_header(username, password):
 | 
			
		||||
    limiter.check()
 | 
			
		||||
    user = _fetch_user_by_name(username)
 | 
			
		||||
    if bool(user and check_password_hash(str(user.password), password)):
 | 
			
		||||
    if bool(user and check_password_hash(str(user.password), password)) and user.name != "Guest":
 | 
			
		||||
        login_user(user)
 | 
			
		||||
        return user
 | 
			
		||||
    else:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1315,17 +1315,17 @@ def login():
 | 
			
		|||
@limiter.limit("40/day", key_func=lambda: request.form.get('username', "").strip().lower())
 | 
			
		||||
@limiter.limit("3/minute", key_func=lambda: request.form.get('username', "").strip().lower())
 | 
			
		||||
def login_post():
 | 
			
		||||
    form = request.form.to_dict()
 | 
			
		||||
    try:
 | 
			
		||||
        limiter.check()
 | 
			
		||||
    except RateLimitExceeded:
 | 
			
		||||
        flash(_(u"Wait one minute"), category="error")
 | 
			
		||||
        return render_login()
 | 
			
		||||
        flash(_(u"Please wait one minute before next login"), category="error")
 | 
			
		||||
        return render_login(form.get("username", ""), form.get("password", ""))
 | 
			
		||||
    if current_user is not None and current_user.is_authenticated:
 | 
			
		||||
        return redirect(url_for('web.index'))
 | 
			
		||||
    if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
 | 
			
		||||
        log.error(u"Cannot activate LDAP authentication")
 | 
			
		||||
        flash(_(u"Cannot activate LDAP authentication"), category="error")
 | 
			
		||||
    form = request.form.to_dict()
 | 
			
		||||
    user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form.get('username', "").strip().lower()) \
 | 
			
		||||
        .first()
 | 
			
		||||
    remember_me = bool(form.get('remember_me'))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
APScheduler>=3.6.3,<3.10.0
 | 
			
		||||
APScheduler>=3.6.3,<3.11.0
 | 
			
		||||
werkzeug<2.1.0
 | 
			
		||||
Babel>=1.3,<3.0
 | 
			
		||||
Flask-Babel>=0.11.1,<3.1.0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user