mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-30 03:28:31 -07:00
Add rate limiting to login page
This commit is contained in:
parent
199119cafb
commit
3e0b240154
4 changed files with 44 additions and 7 deletions
|
@ -204,7 +204,7 @@ ${next.modalIncludes()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
|
<span id="sign-in-alert" style="padding-right: 25px; display: none;"></span>
|
||||||
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
||||||
|
@ -446,12 +446,16 @@ ${next.modalIncludes()}
|
||||||
data: $(this).serialize(),
|
data: $(this).serialize(),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
statusCode: {
|
statusCode: {
|
||||||
200: function() {
|
200: function(xhr, status) {
|
||||||
window.location = "${http_root}";
|
window.location = "${http_root}";
|
||||||
},
|
},
|
||||||
401: function() {
|
401: function(xhr, status) {
|
||||||
$('#incorrect-login').show();
|
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||||
$('#username').focus();
|
$('#username').focus();
|
||||||
|
},
|
||||||
|
429: function(xhr, status) {
|
||||||
|
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||||
|
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
|
|
|
@ -159,16 +159,20 @@
|
||||||
data: data,
|
data: data,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
statusCode: {
|
statusCode: {
|
||||||
200: function() {
|
200: function(xhr, status) {
|
||||||
window.location = "${redirect_uri or http_root}";
|
window.location = "${redirect_uri or http_root}";
|
||||||
},
|
},
|
||||||
401: function() {
|
401: function(xhr, status) {
|
||||||
if (plex) {
|
if (plex) {
|
||||||
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
||||||
} else {
|
} else {
|
||||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||||
$('#username').focus();
|
$('#username').focus();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
429: function(xhr, status) {
|
||||||
|
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||||
|
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
|
|
|
@ -134,6 +134,9 @@ _CONFIG_DEFINITIONS = {
|
||||||
'HTTP_USERNAME': (str, 'General', ''),
|
'HTTP_USERNAME': (str, 'General', ''),
|
||||||
'HTTP_PLEX_ADMIN': (int, 'General', 0),
|
'HTTP_PLEX_ADMIN': (int, 'General', 0),
|
||||||
'HTTP_BASE_URL': (str, 'General', ''),
|
'HTTP_BASE_URL': (str, 'General', ''),
|
||||||
|
'HTTP_RATE_LIMIT_ATTEMPTS': (int, 'General', 10),
|
||||||
|
'HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL': (int, 'General', 300),
|
||||||
|
'HTTP_RATE_LIMIT_LOCKOUT_TIME': (int, 'General', 300),
|
||||||
'INTERFACE': (str, 'General', 'default'),
|
'INTERFACE': (str, 'General', 'default'),
|
||||||
'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
|
'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
|
||||||
'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
|
'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
|
||||||
|
|
|
@ -33,11 +33,13 @@ import plexpy
|
||||||
if plexpy.PYTHON2:
|
if plexpy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
from database import MonitorDatabase
|
from database import MonitorDatabase
|
||||||
|
from helpers import timestamp
|
||||||
from users import Users, refresh_users
|
from users import Users, refresh_users
|
||||||
from plextv import PlexTV
|
from plextv import PlexTV
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from plexpy import logger
|
||||||
from plexpy.database import MonitorDatabase
|
from plexpy.database import MonitorDatabase
|
||||||
|
from plexpy.helpers import timestamp
|
||||||
from plexpy.users import Users, refresh_users
|
from plexpy.users import Users, refresh_users
|
||||||
from plexpy.plextv import PlexTV
|
from plexpy.plextv import PlexTV
|
||||||
|
|
||||||
|
@ -246,6 +248,20 @@ def all_of(*conditions):
|
||||||
return check
|
return check
|
||||||
|
|
||||||
|
|
||||||
|
def check_rate_limit(ip_address):
|
||||||
|
monitor_db = MonitorDatabase()
|
||||||
|
result = monitor_db.select('SELECT timestamp FROM user_login '
|
||||||
|
'WHERE ip_address = ? AND success = 0 '
|
||||||
|
'AND timestamp >= (SELECT MAX(timestamp) FROM user_login WHERE success = 1) '
|
||||||
|
'AND timestamp > (SELECT MAX(timestamp) - ? FROM user_login) '
|
||||||
|
'ORDER BY timestamp DESC',
|
||||||
|
[ip_address, plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL])
|
||||||
|
|
||||||
|
if len(result) >= plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS:
|
||||||
|
last_timestamp = result[0]['timestamp']
|
||||||
|
return max(last_timestamp - (timestamp() - plexpy.CONFIG.HTTP_RATE_LIMIT_LOCKOUT_TIME), 0)
|
||||||
|
|
||||||
|
|
||||||
# Controller to provide login and logout actions
|
# Controller to provide login and logout actions
|
||||||
|
|
||||||
class AuthController(object):
|
class AuthController(object):
|
||||||
|
@ -325,6 +341,16 @@ class AuthController(object):
|
||||||
cherrypy.response.status = 405
|
cherrypy.response.status = 405
|
||||||
return {'status': 'error', 'message': 'Sign in using POST.'}
|
return {'status': 'error', 'message': 'Sign in using POST.'}
|
||||||
|
|
||||||
|
ip_address = cherrypy.request.remote.ip
|
||||||
|
rate_limit = check_rate_limit(ip_address)
|
||||||
|
|
||||||
|
if rate_limit:
|
||||||
|
logger.debug("Tautulli WebAuth :: Too many incorrect login attempts from '%s'." % ip_address)
|
||||||
|
error_message = {'status': 'error', 'message': 'Too many login attempts.'}
|
||||||
|
cherrypy.response.status = 429
|
||||||
|
cherrypy.response.headers['Retry-After'] = rate_limit
|
||||||
|
return error_message
|
||||||
|
|
||||||
error_message = {'status': 'error', 'message': 'Invalid credentials.'}
|
error_message = {'status': 'error', 'message': 'Invalid credentials.'}
|
||||||
|
|
||||||
valid_login, user_details, user_group = check_credentials(username=username,
|
valid_login, user_details, user_group = check_credentials(username=username,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue