Merge pull request #156 from TerryGeng/token

Several improvements to azlux's token auth scheme #154
This commit is contained in:
azlux 2020-05-18 16:37:38 +02:00 committed by GitHub
commit 390c0034f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 70 deletions

View File

@ -1183,9 +1183,19 @@ def cmd_web_access(bot, user, text, command, parameter):
import secrets import secrets
import datetime import datetime
import json import json
token = secrets.token_urlsafe(5)
var.db.set("user", user, json.dumps({'token': token, 'datetime': str(datetime.datetime.now()), 'IP':''})) user_info = var.db.get("user", user, fallback=None)
bot.send_msg(constants.strings('webpage_token',token=token), text) 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")
bot.send_msg(constants.strings('webpage_token', address=access_address, token=token), text)
# Just for debug use # Just for debug use
def cmd_real_time_rms(bot, user, text, command, parameter): def cmd_real_time_rms(bot, user, text, command, parameter):

View File

@ -96,14 +96,13 @@ listening_addr = 127.0.0.1
listening_port = 8181 listening_port = 8181
web_logfile = web_logfile =
# Set this option to True to enable password protection for the web interface auth_method = password
require_auth = False user = botamusique
user = password = mumble
password =
access_address = http://127.0.0.1:8181
# Set this option to match mumble user with token on flask and add a password to encrypt/sign cookies
flask_secret = ChangeThisPassword flask_secret = ChangeThisPassword
match_mumble_user = False
[debug] [debug]
# Set ffmpeg to True if you want to display DEBUG level log of ffmpeg. # Set ffmpeg to True if you want to display DEBUG level log of ffmpeg.
@ -259,7 +258,7 @@ cleared_tags = Removed all tags from <b>{song}</b>.
cleared_tags_from_all = Removed all tags from songs on the playlist. cleared_tags_from_all = Removed all tags from songs on the playlist.
shortlist_instruction = Use <i>!sl {indexes}</i> to play the item you want. shortlist_instruction = Use <i>!sl {indexes}</i> to play the item you want.
auto_paused = Use <i>!play</i> to resume music! auto_paused = Use <i>!play</i> to resume music!
webpage_token= Your token to access the Bot webpage is {token}, short <a href="YOUR_URL_HERE?token={token}">URL</a> webpage_token= Your own address to access the web interface is <a href="{address}/?token={token}">{address}/?token={token}</a>
help = <h3>Commands</h3> help = <h3>Commands</h3>
<b>Control</b> <b>Control</b>

View File

@ -108,33 +108,35 @@ port = 64738
# - "pause", # - "pause",
# - "pause_resume" (pause and resume once somebody re-enters the channel) # - "pause_resume" (pause and resume once somebody re-enters the channel)
# - "stop" (also clears playlist) # - "stop" (also clears playlist)
# - "nothing" or leave empty (do nothing) # - leave empty (do nothing)
#when_nobody_in_channel = nothing #when_nobody_in_channel =
# [webinterface] stores settings related to the web interface. # [webinterface] stores settings related to the web interface.
[webinterface] [webinterface]
# 'enable': Set 'enabled' to True if you'd like to use the web interface to manage # 'enable': Set 'enabled' to True if you'd like to use the web interface to manage
# your playlist, upload files, etc. # your playlist, upload files, etc.
# The web interface is disable by default for security and performance reason. # The web interface is disable by default for security and performance reason.
# 'access_address': Used when user are questing the address to access the web interface.
#enabled = False #enabled = False
#listening_addr = 127.0.0.1 #listening_addr = 127.0.0.1
#listening_port = 8181 #listening_port = 8181
#is_web_proxified = True #is_web_proxified = True
#access_address = http://127.0.0.1:8181
# 'web_logfile': write access logs of the web server into this file. # 'web_logfile': write access logs of the web server into this file.
#web_logfile = #web_logfile =
# 'required_auth': Set this to True to enable password protection for the web interface. # 'auth_method': Method used to authenticate users accessing the web interface.
#require_auth = False # Options are 'password', 'token', 'none'
#user = #auth_method = password
#password =
# Set this option to match mumble user with user on the webinterface # 'user', 'password': If auth_method set to 'password', you need to set the username and
# It's working with an unique token an user can ask to the bot with token and to add music to the bot. # password.
# It's also allow users to know who have add a music from the webinterface #user = botamusique
# match_mumble_user = True #password = mumble
# To use token (need session) flask need a password to encrypt/sign cookies used. !! YOU NEED TO CHANGE IT IF PREVIOUS OPTION IS TRUE!! # 'flask_secret': To use token, flask need a password to encrypt/sign cookies.
# !! YOU NEED TO CHANGE IT IF auth_method IS 'token'!!
# flask_secret = ChangeThisPassword # flask_secret = ChangeThisPassword
# [debug] stores some debug settings. # [debug] stores some debug settings.

View File

@ -94,53 +94,47 @@ def authenticate():
def requires_auth(f): def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
global log
auth = request.authorization
if var.config.getboolean("webinterface", "require_auth") and (
not auth or not check_auth(auth.username, auth.password)):
if auth:
log.info("web: Failed login attempt, user: %s" % auth.username)
return authenticate()
return f(*args, **kwargs)
return decorated
def set_cookie_token(f):
@wraps(f)
def decorated(*args, **kwargs):
global log
if var.config.getboolean("webinterface", "match_mumble_user"):
users = var.db.items('user')
if users:
for user in users:
tp = json.loads(user[1])
log.info(tp)
if tp['token'] == request.args.get('token'):
t_user = user[0]
log.info(f"web: token validated for the user: {t_user}")
session['user']=t_user
return f(*args, **kwargs)
log.info("web: Bad token used")
abort(403)
return f(*args, **kwargs)
return decorated
def requires_token(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
global log, user global log, user
if var.config.getboolean("webinterface", "match_mumble_user"):
if 'user' in session: auth_method = var.config.get("webinterface", "auth_method")
log.debug(f"Request done by {session['user']}")
user = session['user'] if auth_method == 'password':
else: auth = request.authorization
abort(403) if var.config.getboolean("webinterface", "require_auth") and (
not auth or not check_auth(auth.username, auth.password)):
if auth:
log.warning(f"web: failed login attempt, user: {auth.username}, from ip {request.remote_addr}.")
return authenticate()
if auth_method == 'token':
if 'token' in session and 'token' not in request.args:
token = session['token']
token_user = var.db.get("web_token", token, fallback=None)
if token_user is not None:
user = token_user
log.debug(f"web: token validated for the user: {token_user}, from ip {request.remote_addr}.")
return f(*args, **kwargs)
elif 'token' in request.args:
token = request.args.get('token')
token_user = var.db.get("web_token", token, fallback=None)
if token_user is not None:
user = token_user
user_info = var.db.get("user", user, fallback=None)
user_dict = json.loads(user_info)
user_dict['IP'] = request.remote_addr
var.db.set("user", user, json.dumps(user_dict))
log.info(f"web: new user access, token validated for the user: {token_user}, from ip {request.remote_addr}.")
session['token'] = token
return f(*args, **kwargs)
log.info(f"web: bad token from ip {request.remote_addr}.")
abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
@ -190,7 +184,6 @@ def get_all_dirs():
@web.route("/", methods=['GET']) @web.route("/", methods=['GET'])
@requires_auth @requires_auth
@set_cookie_token
def index(): def index():
while var.cache.dir_lock.locked(): while var.cache.dir_lock.locked():
time.sleep(0.1) time.sleep(0.1)
@ -205,7 +198,6 @@ def index():
@web.route("/playlist", methods=['GET']) @web.route("/playlist", methods=['GET'])
@requires_auth @requires_auth
@requires_token
def playlist(): def playlist():
if len(var.playlist) == 0: if len(var.playlist) == 0:
return ('', 204) return ('', 204)
@ -301,7 +293,6 @@ def status():
@web.route("/post", methods=['POST']) @web.route("/post", methods=['POST'])
@requires_auth @requires_auth
@requires_token
def post(): def post():
global log global log
@ -520,7 +511,6 @@ def build_library_query_condition(form):
@web.route("/library", methods=['POST']) @web.route("/library", methods=['POST'])
@requires_auth @requires_auth
@requires_token
def library(): def library():
global log global log
ITEM_PER_PAGE = 10 ITEM_PER_PAGE = 10
@ -622,8 +612,7 @@ def library():
@web.route('/upload', methods=["POST"]) @web.route('/upload', methods=["POST"])
#@requires_auth missing here ? @requires_auth
@requires_token
def upload(): def upload():
global log global log
@ -672,6 +661,7 @@ def upload():
@web.route('/download', methods=["GET"]) @web.route('/download', methods=["GET"])
@requires_auth
def download(): def download():
global log global log

View File

@ -928,6 +928,7 @@ function uploadNextFile(){
form.append('targetdir', uploadTargetDir.value); form.append('targetdir', uploadTargetDir.value);
req.open('POST', 'upload'); req.open('POST', 'upload');
req.withCredentials = true;
req.send(form); req.send(form);
file_progress_item.progress.classList.add("progress-bar-striped"); file_progress_item.progress.classList.add("progress-bar-striped");