From c8f0ccf706e303cf0c173d01e55207090dd69346 Mon Sep 17 00:00:00 2001 From: Terry Geng Date: Tue, 19 May 2020 09:45:11 +0800 Subject: [PATCH] feat: ban a user after too many failed attempts --- command.py | 29 +++++++++++++++++++--------- configuration.default.ini | 5 +++-- configuration.example.ini | 6 +++++- interface.py | 40 +++++++++++++++++++++++++++++++++------ static/js/custom.js | 8 +++++++- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/command.py b/command.py index ecc451f..1791d6f 100644 --- a/command.py +++ b/command.py @@ -6,6 +6,7 @@ import pymumble_py3 as pymumble import re import constants +import interface import media.system import util import variables as var @@ -1181,18 +1182,28 @@ def cmd_web_access(bot, user, text, command, parameter): import datetime import json - user_info = var.db.get("user", user, fallback=None) - if user_info is not None: - user_dict = json.loads(user_info) - token = user_dict['token'] + auth_method = var.config.get("webinterface", "auth_method") + + if auth_method == 'token': + interface.banned_ip = [] + interface.bad_access_count = {} + + user_info = var.db.get("user", user, fallback=None) + if user_info is not None: + user_dict = json.loads(user_info) + token = user_dict['token'] + else: + token = secrets.token_urlsafe(5) + var.db.set("web_token", token, user) + + var.db.set("user", user, json.dumps({'token': token, 'datetime': str(datetime.datetime.now()), 'IP': ''})) + + access_address = var.config.get("webinterface", "access_address") + "/?token=" + token else: - token = secrets.token_urlsafe(5) - var.db.set("web_token", token, user) + access_address = var.config.get("webinterface", "access_address") - var.db.set("user", user, json.dumps({'token': token, 'datetime': str(datetime.datetime.now()), 'IP': ''})) + bot.send_msg(constants.strings('webpage_address', address=access_address), text) - access_address = var.config.get("webinterface", "access_address") - bot.send_msg(constants.strings('webpage_token', address=access_address, token=token), text) # Just for debug use def cmd_real_time_rms(bot, user, text, command, parameter): diff --git a/configuration.default.ini b/configuration.default.ini index a1e7533..e0c2ac7 100644 --- a/configuration.default.ini +++ b/configuration.default.ini @@ -98,7 +98,8 @@ web_logfile = auth_method = 'none' user = -password = +password = +max_attempts = 10 access_address = http://127.0.0.1:8181 @@ -261,7 +262,7 @@ cleared_tags = Removed all tags from {song}. cleared_tags_from_all = Removed all tags from songs on the playlist. shortlist_instruction = Use !sl {indexes} to play the item you want. auto_paused = Use !play to resume music! -webpage_token= Your own address to access the web interface is {address}/?token={token} +webpage_address= Your own address to access the web interface is {address} help =

Commands

Control diff --git a/configuration.example.ini b/configuration.example.ini index e9b1635..27eaa22 100644 --- a/configuration.example.ini +++ b/configuration.example.ini @@ -128,8 +128,12 @@ port = 64738 #web_logfile = # 'auth_method': Method used to authenticate users accessing the web interface. -# Options are 'none', 'password' or 'token' (use unique token, see requests_webinterface_access command) +# Options are 'none', 'password' or 'token' (use unique token, see +# requests_webinterface_access command) +# 'max_attempts': Bad access attempts before being banned. Regenerating a token or +# rebooting the bot will reset this attempts tally. #auth_method = token +#max_attempts = 10 # 'user', 'password': If auth_method set to 'password', you need to set the username and # password. diff --git a/interface.py b/interface.py index c1645c2..90c59e4 100644 --- a/interface.py +++ b/interface.py @@ -93,10 +93,17 @@ def authenticate(): {'WWW-Authenticate': 'Basic realm="Login Required"'}) +bad_access_count = {} +banned_ip = [] + + def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): - global log, user + global log, user, bad_access_count, banned_ip + + if request.remote_addr in banned_ip: + abort(403) auth_method = var.config.get("webinterface", "auth_method") @@ -104,7 +111,17 @@ def requires_auth(f): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): if auth: - log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}.") + if request.remote_addr in bad_access_count: + bad_access_count[request.remote_addr] += 1 + log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}." + f"{bad_access_count[request.remote_addr]} attempts.") + if bad_access_count[request.remote_addr] > var.config.getint("webinterface", "max_attempts", + fallback=10): + banned_ip.append(request.remote_addr) + log.info(f"web: access banned for {request.remote_addr}") + else: + bad_access_count[request.remote_addr] = 1 + log.info(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}.") return authenticate() if auth_method == 'token': if 'user' in session and 'token' not in request.args: @@ -121,15 +138,26 @@ def requires_auth(f): user_dict['IP'] = request.remote_addr var.db.set("user", user, json.dumps(user_dict)) - log.debug(f"web: new user access, token validated for the user: {token_user}, from ip {request.remote_addr}.") + log.debug( + f"web: new user access, token validated for the user: {token_user}, from ip {request.remote_addr}.") session['token'] = token session['user'] = token_user return f(*args, **kwargs) - log.info(f"web: bad token from ip {request.remote_addr}.") + if request.remote_addr in bad_access_count: + bad_access_count[request.remote_addr] += 1 + log.info(f"web: bad token from ip {request.remote_addr}, " + f"{bad_access_count[request.remote_addr]} attempts.") + if bad_access_count[request.remote_addr] > var.config.getint("webinterface", "max_attempts", fallback=10): + banned_ip.append(request.remote_addr) + log.info(f"web: access banned for {request.remote_addr}") + else: + bad_access_count[request.remote_addr] = 1 + log.info(f"web: bad token from ip {request.remote_addr}.") + return render_template('need_token.html', - name=var.config.get('bot','username'), - command=f"{var.config.get('commands', 'command_symbol')[0]}{var.config.get('commands','requests_webinterface_access')}") + name=var.config.get('bot', 'username'), + command=f"{var.config.get('commands', 'command_symbol')[0]}{var.config.get('commands', 'requests_webinterface_access')}") return f(*args, **kwargs) diff --git a/static/js/custom.js b/static/js/custom.js index 8b5eec2..c619cb7 100644 --- a/static/js/custom.js +++ b/static/js/custom.js @@ -72,11 +72,14 @@ function request(_url, _data, refresh = false) { } updateControls(data.empty, data.play, data.mode, data.volume); updatePlayerPlayhead(data.playhead); + }, + 403: function (){ + location.reload(true); } }, }); if (refresh) { - location.reload() + location.reload(true) } } @@ -541,6 +544,9 @@ function updateResults(dest_page = 1) { lib_loading.hide(); lib_empty.show(); page_ul.empty(); + }, + 403: function () { + location.reload(true); } } });