mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-25 02:57:22 +00:00
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 flask import Flask
|
||||||
from .MyLoginManager import MyLoginManager
|
from .MyLoginManager import MyLoginManager
|
||||||
from flask_principal import Principal
|
from flask_principal import Principal
|
||||||
from flask_limiter import Limiter
|
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .cli import CliParameter
|
from .cli import CliParameter
|
||||||
@ -42,6 +41,11 @@ from . import config_sql
|
|||||||
from . import cache_buster
|
from . import cache_buster
|
||||||
from . import ub, db
|
from . import ub, db
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_limiter import Limiter
|
||||||
|
limiter_present = True
|
||||||
|
except ImportError:
|
||||||
|
limiter_present = False
|
||||||
try:
|
try:
|
||||||
from flask_wtf.csrf import CSRFProtect
|
from flask_wtf.csrf import CSRFProtect
|
||||||
wtf_present = True
|
wtf_present = True
|
||||||
@ -97,7 +101,10 @@ web_server = WebServer()
|
|||||||
|
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
|
|
||||||
|
if limiter_present:
|
||||||
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
|
limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
|
||||||
|
else:
|
||||||
|
limiter = None
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
if csrf:
|
if csrf:
|
||||||
@ -115,21 +122,13 @@ def create_app():
|
|||||||
if error:
|
if error:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
|
|
||||||
lm.login_view = 'web.login'
|
if not limiter:
|
||||||
lm.anonymous_user = ub.Anonymous
|
log.info('*** "flask-limiter" is needed for calibre-web to run. '
|
||||||
lm.session_protection = 'strong' if config.config_session == 1 else "basic"
|
'Please install it using pip: "pip install flask-limiter" ***')
|
||||||
|
print('*** "flask-limiter" is needed for calibre-web to run. '
|
||||||
db.CalibreDB.update_config(config)
|
'Please install it using pip: "pip install flask-limiter" ***')
|
||||||
db.CalibreDB.setup_db(config.config_calibre_dir, cli_param.settings_path)
|
web_server.stop(True)
|
||||||
calibre_db.init_db()
|
sys.exit(8)
|
||||||
|
|
||||||
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 sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
log.info(
|
log.info(
|
||||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
|
'*** 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" ***')
|
'Please install it using pip: "pip install flask-WTF" ***')
|
||||||
web_server.stop(True)
|
web_server.stop(True)
|
||||||
sys.exit(7)
|
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):
|
for res in dependency_check() + dependency_check(True):
|
||||||
log.info('*** "{}" version does not meet the requirements. '
|
log.info('*** "{}" version does not meet the requirements. '
|
||||||
'Should: {}, Found: {}, please consider installing required version ***'
|
'Should: {}, Found: {}, please consider installing required version ***'
|
||||||
@ -157,8 +172,6 @@ def create_app():
|
|||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
cache_buster.init_cache_busting(app)
|
cache_buster.init_cache_busting(app)
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
limiter.init_app(app)
|
|
||||||
# limiter.limit("2/minute")(parent)
|
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
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_goodreads_api_secret_e,
|
||||||
config.config_use_goodreads)
|
config.config_use_goodreads)
|
||||||
config.store_calibre_uuid(calibre_db, db.Library_Id)
|
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
|
# Register scheduled tasks
|
||||||
from .schedule import register_scheduled_tasks, register_startup_tasks
|
from .schedule import register_scheduled_tasks, register_startup_tasks
|
||||||
register_scheduled_tasks(config.schedule_reconnect)
|
register_scheduled_tasks(config.schedule_reconnect)
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import operator
|
import operator
|
||||||
import time
|
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:
|
if not ub.check_user_session(current_user.id, flask_session.get('_id')) and 'opds' not in request.path:
|
||||||
logout_user()
|
logout_user()
|
||||||
g.constants = constants
|
g.constants = constants
|
||||||
# g.user = current_user
|
|
||||||
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
|
g.google_site_verification = os.getenv('GOOGLE_SITE_VERIFICATION','')
|
||||||
g.allow_registration = config.config_public_reg
|
g.allow_registration = config.config_public_reg
|
||||||
g.allow_anonymous = config.config_anonbrowse
|
g.allow_anonymous = config.config_anonbrowse
|
||||||
@ -1802,6 +1800,7 @@ def _configuration_update_helper():
|
|||||||
_config_checkbox(to_save, "config_password_special")
|
_config_checkbox(to_save, "config_password_special")
|
||||||
_config_int(to_save, "config_password_min_length")
|
_config_int(to_save, "config_password_min_length")
|
||||||
reboot_required |= _config_int(to_save, "config_session")
|
reboot_required |= _config_int(to_save, "config_session")
|
||||||
|
reboot_required |= _config_checkbox(to_save, "config_ratelimiter")
|
||||||
|
|
||||||
# Rarfile Content configuration
|
# Rarfile Content configuration
|
||||||
_config_string(to_save, "config_rarfile_location")
|
_config_string(to_save, "config_rarfile_location")
|
||||||
|
@ -161,108 +161,12 @@ class _Settings(_Base):
|
|||||||
config_password_upper = Column(Boolean, default=True)
|
config_password_upper = Column(Boolean, default=True)
|
||||||
config_password_special = Column(Boolean, default=True)
|
config_password_special = Column(Boolean, default=True)
|
||||||
config_session = Column(Integer, default=1)
|
config_session = Column(Integer, default=1)
|
||||||
|
config_ratelimiter = Column(Boolean, default=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.__class__.__name__
|
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 holds all application specific settings in calibre-web
|
||||||
class ConfigSQL(object):
|
class ConfigSQL(object):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
@ -62,7 +62,7 @@ def main():
|
|||||||
app.register_blueprint(tasks)
|
app.register_blueprint(tasks)
|
||||||
app.register_blueprint(web)
|
app.register_blueprint(web)
|
||||||
app.register_blueprint(opds)
|
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(jinjia)
|
||||||
app.register_blueprint(about)
|
app.register_blueprint(about)
|
||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
@ -74,7 +74,7 @@ def main():
|
|||||||
if kobo_available:
|
if kobo_available:
|
||||||
app.register_blueprint(kobo)
|
app.register_blueprint(kobo)
|
||||||
app.register_blueprint(kobo_auth)
|
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:
|
if oauth_available:
|
||||||
app.register_blueprint(oauth)
|
app.register_blueprint(oauth)
|
||||||
success = web_server.start()
|
success = web_server.start()
|
||||||
|
@ -364,6 +364,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="collapsesix" class="panel-collapse collapse">
|
<div id="collapsesix" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<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">
|
<div class="form-group">
|
||||||
<label for="config_session">{{_('Session protection')}}</label>
|
<label for="config_session">{{_('Session protection')}}</label>
|
||||||
<select name="config_session" id="config_session" class="form-control">
|
<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_login import login_required, login_user
|
||||||
from flask import request, Response
|
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()
|
log = logger.create()
|
||||||
|
|
||||||
@ -65,8 +65,9 @@ def requires_basic_auth_if_no_ano(f):
|
|||||||
|
|
||||||
|
|
||||||
def _load_user_from_auth_header(username, password):
|
def _load_user_from_auth_header(username, password):
|
||||||
|
limiter.check()
|
||||||
user = _fetch_user_by_name(username)
|
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)
|
login_user(user)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
|
@ -1315,17 +1315,17 @@ def login():
|
|||||||
@limiter.limit("40/day", key_func=lambda: request.form.get('username', "").strip().lower())
|
@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())
|
@limiter.limit("3/minute", key_func=lambda: request.form.get('username', "").strip().lower())
|
||||||
def login_post():
|
def login_post():
|
||||||
|
form = request.form.to_dict()
|
||||||
try:
|
try:
|
||||||
limiter.check()
|
limiter.check()
|
||||||
except RateLimitExceeded:
|
except RateLimitExceeded:
|
||||||
flash(_(u"Wait one minute"), category="error")
|
flash(_(u"Please wait one minute before next login"), category="error")
|
||||||
return render_login()
|
return render_login(form.get("username", ""), form.get("password", ""))
|
||||||
if current_user is not None and current_user.is_authenticated:
|
if current_user is not None and current_user.is_authenticated:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:
|
||||||
log.error(u"Cannot activate LDAP authentication")
|
log.error(u"Cannot activate LDAP authentication")
|
||||||
flash(_(u"Cannot activate LDAP authentication"), category="error")
|
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()) \
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form.get('username', "").strip().lower()) \
|
||||||
.first()
|
.first()
|
||||||
remember_me = bool(form.get('remember_me'))
|
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
|
werkzeug<2.1.0
|
||||||
Babel>=1.3,<3.0
|
Babel>=1.3,<3.0
|
||||||
Flask-Babel>=0.11.1,<3.1.0
|
Flask-Babel>=0.11.1,<3.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user