1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-25 17:40:31 +00:00

Rate limited login

This commit is contained in:
Ozzie Isaacs 2022-07-02 19:12:18 +02:00
parent 3bde8a5d95
commit 7344ef353c
3 changed files with 39 additions and 18 deletions

View File

@ -97,7 +97,7 @@ web_server = WebServer()
updater_thread = Updater() updater_thread = Updater()
limiter = Limiter(key_func=True, headers_enabled=True) limiter = Limiter(key_func=True, headers_enabled=True, auto_check=False, swallow_errors=True)
def create_app(): def create_app():
if csrf: if csrf:

View File

@ -28,6 +28,7 @@ from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session from flask import session as flask_session
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale from flask_babel import get_locale
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
@ -54,6 +55,8 @@ from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import remove_synced_book from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template from .render_template import render_title_template
from .kobo_sync_status import change_archived_books from .kobo_sync_status import change_archived_books
from . import limiter
from flask_limiter import RateLimitExceeded
feature_support = { feature_support = {
@ -1266,8 +1269,20 @@ def register():
register_user_with_oauth() register_user_with_oauth()
return render_title_template('register.html', config=config, title=_("Register"), page="register") return render_title_template('register.html', config=config, title=_("Register"), page="register")
def handle_login_user(user, remember, message, category):
login_user(user, remember=remember)
ub.store_user_session()
flash(message, category=category)
try:
limiter.check()
except RateLimitExceeded:
[limiter.limiter.storage.clear(k.key) for k in limiter.current_limits]
return redirect_back(url_for("web.index"))
@web.route('/login', methods=['GET', 'POST']) @web.route('/login', methods=['GET', 'POST'])
@limiter.limit("40/day", key_func=lambda: request.form.get('username'), per_method=["POST"])
@limiter.limit("2/minute", key_func=lambda: request.form.get('username'), per_method=["POST"])
def login(): def login():
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'))
@ -1276,26 +1291,25 @@ def login():
flash(_(u"Cannot activate LDAP authentication"), category="error") flash(_(u"Cannot activate LDAP authentication"), category="error")
if request.method == "POST": if request.method == "POST":
form = request.form.to_dict() form = request.form.to_dict()
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == form['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'))
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "":
login_result, error = services.ldap.bind_user(form['username'], form['password']) login_result, error = services.ldap.bind_user(form['username'], form['password'])
if login_result: if login_result:
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
log.debug(u"You are now logged in as: '{}'".format(user.name)) log.debug(u"You are now logged in as: '{}'".format(user.name))
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), return handle_login_user(user,
category="success") remember_me,
return redirect_back(url_for("web.index")) _(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
"success")
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \ elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
and user.name != "Guest": and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
log.info("Local Fallback Login as: '{}'".format(user.name)) log.info("Local Fallback Login as: '{}'".format(user.name))
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known", return handle_login_user(user,
nickname=user.name), remember_me,
category="warning") _(u"Fallback Login as: '%(nickname)s', "
return redirect_back(url_for("web.index")) u"LDAP Server not reachable, or user not known", nickname=user.name),
"warning")
elif login_result is None: elif login_result is None:
log.info(error) log.info(error)
flash(_(u"Could not login: %(message)s", message=error), category="error") flash(_(u"Could not login: %(message)s", message=error), category="error")
@ -1319,12 +1333,12 @@ def login():
log.warning('Username missing for password reset IP-address: %s', ip_address) log.warning('Username missing for password reset IP-address: %s', ip_address)
else: else:
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest": if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
log.debug(u"You are now logged in as: '%s'", user.name)
flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
config.config_is_initial = False config.config_is_initial = False
return redirect_back(url_for("web.index")) log.debug(u"You are now logged in as: '{}'".format(user.name))
return handle_login_user(user,
remember_me,
_(u"You are now logged in as: '%(nickname)s'", nickname=user.name),
"success")
else: else:
log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address)) log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address))
flash(_(u"Wrong Username or Password"), category="error") flash(_(u"Wrong Username or Password"), category="error")
@ -1332,6 +1346,12 @@ def login():
next_url = request.args.get('next', default=url_for("web.index"), type=str) next_url = request.args.get('next', default=url_for("web.index"), type=str)
if url_for("web.logout") == next_url: if url_for("web.logout") == next_url:
next_url = url_for("web.index") next_url = url_for("web.index")
# Check rate limit and prevent displaying remaining flash messages from last attempt
try:
limiter.check()
except RateLimitExceeded:
flask_session['_flashes'].clear()
raise
return render_title_template('login.html', return render_title_template('login.html',
title=_(u"Login"), title=_(u"Login"),
next_url=next_url, next_url=next_url,

View File

@ -18,3 +18,4 @@ lxml>=3.8.0,<4.9.0
flask-wtf>=0.14.2,<1.1.0 flask-wtf>=0.14.2,<1.1.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<2.5.0