feat: 'password' auth method: support user addition

This commit is contained in:
Terry Geng 2020-05-23 14:46:21 +08:00
parent 19d868d352
commit e61f791c82
No known key found for this signature in database
GPG Key ID: F982F8EA1DF720E7
6 changed files with 152 additions and 77 deletions

View File

@ -1,9 +1,10 @@
# coding=utf-8 # coding=utf-8
import logging import logging
import math import secrets
import datetime
import pymumble_py3 as pymumble import json
import re import re
import pymumble_py3 as pymumble
import constants import constants
import interface import interface
@ -21,12 +22,12 @@ log = logging.getLogger("bot")
def register_all_commands(bot): def register_all_commands(bot):
bot.register_command(constants.commands('joinme'), cmd_joinme, no_partial_match=False, access_outside_channel=True) bot.register_command(constants.commands('joinme'), cmd_joinme, access_outside_channel=True)
bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True) bot.register_command(constants.commands('user_ban'), cmd_user_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True) bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True) bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True) bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True) bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=True, admin=True)
bot.register_command(constants.commands('play'), cmd_play) bot.register_command(constants.commands('play'), cmd_play)
bot.register_command(constants.commands('pause'), cmd_pause) bot.register_command(constants.commands('pause'), cmd_pause)
bot.register_command(constants.commands('play_file'), cmd_play_file) bot.register_command(constants.commands('play_file'), cmd_play_file)
@ -42,8 +43,8 @@ def register_all_commands(bot):
bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True) bot.register_command(constants.commands('help'), cmd_help, no_partial_match=False, access_outside_channel=True)
bot.register_command(constants.commands('stop'), cmd_stop) bot.register_command(constants.commands('stop'), cmd_stop)
bot.register_command(constants.commands('clear'), cmd_clear) bot.register_command(constants.commands('clear'), cmd_clear)
bot.register_command(constants.commands('kill'), cmd_kill) bot.register_command(constants.commands('kill'), cmd_kill, admin=True)
bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True) bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True, admin=True)
bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout) bot.register_command(constants.commands('stop_and_getout'), cmd_stop_and_getout)
bot.register_command(constants.commands('volume'), cmd_volume) bot.register_command(constants.commands('volume'), cmd_volume)
bot.register_command(constants.commands('ducking'), cmd_ducking) bot.register_command(constants.commands('ducking'), cmd_ducking)
@ -64,9 +65,13 @@ def register_all_commands(bot):
bot.register_command(constants.commands('search'), cmd_search_library) bot.register_command(constants.commands('search'), cmd_search_library)
bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist) bot.register_command(constants.commands('add_from_shortlist'), cmd_shortlist)
bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library) bot.register_command(constants.commands('delete_from_library'), cmd_delete_from_library)
bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True) bot.register_command(constants.commands('drop_database'), cmd_drop_database, no_partial_match=True, admin=True)
bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True) bot.register_command(constants.commands('rescan'), cmd_refresh_cache, no_partial_match=True)
bot.register_command(constants.commands('requests_webinterface_access'), cmd_web_access) bot.register_command(constants.commands('requests_webinterface_access'), cmd_web_access)
bot.register_command(constants.commands('add_webinterface_user'), cmd_web_user_add, admin=True)
bot.register_command(constants.commands('remove_webinterface_user'), cmd_web_user_remove, admin=True)
bot.register_command(constants.commands('list_webinterface_user'), cmd_web_user_list, admin=True)
bot.register_command(constants.commands('change_user_password'), cmd_user_password)
# Just for debug use # Just for debug use
bot.register_command('rtrms', cmd_real_time_rms, True) bot.register_command('rtrms', cmd_real_time_rms, True)
#bot.register_command('loop', cmd_loop_state, True) #bot.register_command('loop', cmd_loop_state, True)
@ -126,31 +131,22 @@ def cmd_joinme(bot, user, text, command, parameter):
def cmd_user_ban(bot, user, text, command, parameter): def cmd_user_ban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user):
if parameter: if parameter:
bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter)) bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter))
else: else:
bot.mumble.users[text.actor].send_text_message(util.get_user_ban()) bot.mumble.users[text.actor].send_text_message(util.get_user_ban())
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_user_unban(bot, user, text, command, parameter): def cmd_user_unban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user):
if parameter: if parameter:
bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter)) bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_ban(bot, user, text, command, parameter): def cmd_url_ban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user):
if parameter: if parameter:
bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter))) bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter)))
@ -165,28 +161,17 @@ def cmd_url_ban(bot, user, text, command, parameter):
var.playlist.remove_by_id(item.id) var.playlist.remove_by_id(item.id)
else: else:
bot.send_msg(constants.strings('bad_parameter', command=command), text) bot.send_msg(constants.strings('bad_parameter', command=command), text)
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_ban_list(bot, user, text, command, parameter): def cmd_url_ban_list(bot, user, text, command, parameter):
if bot.is_admin(user):
bot.mumble.users[text.actor].send_text_message(util.get_url_ban()) bot.mumble.users[text.actor].send_text_message(util.get_url_ban())
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_url_unban(bot, user, text, command, parameter): def cmd_url_unban(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user):
if parameter: if parameter:
bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter))) bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(parameter)))
else:
bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
def cmd_play(bot, user, text, command, parameter): def cmd_play(bot, user, text, command, parameter):
@ -584,12 +569,8 @@ def cmd_clear(bot, user, text, command, parameter):
def cmd_kill(bot, user, text, command, parameter): def cmd_kill(bot, user, text, command, parameter):
global log global log
if bot.is_admin(user):
bot.pause() bot.pause()
bot.exit = True bot.exit = True
else:
bot.mumble.users[text.actor].send_text_message(
constants.strings('not_admin'))
def cmd_update(bot, user, text, command, parameter): def cmd_update(bot, user, text, command, parameter):
@ -1178,25 +1159,23 @@ def cmd_refresh_cache(bot, user, text, command, parameter):
def cmd_web_access(bot, user, text, command, parameter): def cmd_web_access(bot, user, text, command, parameter):
import secrets
import datetime
import json
auth_method = var.config.get("webinterface", "auth_method") auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'token': if auth_method == 'token':
interface.banned_ip = [] interface.banned_ip = []
interface.bad_access_count = {} interface.bad_access_count = {}
user_info = var.db.get("user", user, fallback=None) user_info = var.db.get("user", user, fallback='{}')
if user_info is not None:
user_dict = json.loads(user_info) user_dict = json.loads(user_info)
token = user_dict['token'] if 'token' in user_dict:
else: var.db.remove_option("web_token", user_dict['token'])
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': ''})) token = secrets.token_urlsafe(5)
user_dict['token'] = token
user_dict['token_created'] = str(datetime.datetime.now())
user_dict['last_ip'] = ''
var.db.set("web_token", token, user)
var.db.set("user", user, json.dumps(user_dict))
access_address = var.config.get("webinterface", "access_address") + "/?token=" + token access_address = var.config.get("webinterface", "access_address") + "/?token=" + token
else: else:
@ -1205,6 +1184,64 @@ def cmd_web_access(bot, user, text, command, parameter):
bot.send_msg(constants.strings('webpage_address', address=access_address), text) bot.send_msg(constants.strings('webpage_address', address=access_address), text)
def cmd_user_password(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
user_info = var.db.get("user", user, fallback='{}')
user_dict = json.loads(user_info)
user_dict['password'], user_dict['salt'] = util.get_salted_password_hash(parameter)
var.db.set("user", user, json.dumps(user_dict))
bot.send_msg(constants.strings('user_password_set'), text)
def cmd_web_user_add(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if parameter not in web_users:
web_users.append(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
def cmd_web_user_remove(bot, user, text, command, parameter):
if not parameter:
bot.send_msg(constants.strings('bad_parameter', command=command), text)
return
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if parameter in web_users:
web_users.remove(parameter)
var.db.set("privilege", "web_access", json.dumps(web_users))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), text)
def cmd_web_user_list(bot, user, text, command, parameter):
auth_method = var.config.get("webinterface", "auth_method")
if auth_method == 'password':
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
bot.send_msg(constants.strings('web_user_list', users=", ". join(web_users)), text)
else:
bot.send_msg(constants.strings('command_disabled', command=command), 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):
bot._display_rms = not bot._display_rms bot._display_rms = not bot._display_rms

View File

@ -96,7 +96,7 @@ listening_addr = 127.0.0.1
listening_port = 8181 listening_port = 8181
web_logfile = web_logfile =
auth_method = 'none' auth_method = none
user = user =
password = password =
max_attempts = 10 max_attempts = 10
@ -190,6 +190,10 @@ drop_database = dropdatabase
rescan = rescan rescan = rescan
requests_webinterface_access = web requests_webinterface_access = web
list_webinterface_user = webuserlist
add_webinterface_user = webuseradd
remove_webinterface_user = webuserdel
change_user_password = password
[strings] [strings]
current_volume = Current volume: {volume}. current_volume = Current volume: {volume}.
@ -266,6 +270,9 @@ 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_address= Your own address to access the web interface is <a href="{address}">{address}</a> webpage_address= Your own address to access the web interface is <a href="{address}">{address}</a>
web_user_list = Following users has the privilege to access the web interface: <br /> {users}
user_password_set = Your password has been updated.
command_disabled = {command}: command disabled!
help = <h3>Commands</h3> help = <h3>Commands</h3>
<b>Control</b> <b>Control</b>

View File

@ -135,8 +135,8 @@ port = 64738
#auth_method = token #auth_method = token
#max_attempts = 10 #max_attempts = 10
# 'user', 'password': If auth_method set to 'password', you need to set the username and # 'user', 'password': If auth_method set to 'password', you need to set the default
# password. # username and password. You can add more users by '!webadduser'
#user = botamusique #user = botamusique
#password = mumble #password = mumble

View File

@ -66,7 +66,7 @@ class ReverseProxied(object):
web = Flask(__name__) web = Flask(__name__)
web.config['TEMPLATES_AUTO_RELOAD'] = True web.config['TEMPLATES_AUTO_RELOAD'] = True
log = logging.getLogger("bot") log = logging.getLogger("bot")
user = 'webuser' user = 'Remote Control'
def init_proxy(): def init_proxy():
@ -82,7 +82,18 @@ def check_auth(username, password):
"""This function is called to check if a username / """This function is called to check if a username /
password combination is valid. password combination is valid.
""" """
return username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password")
if username == var.config.get("webinterface", "user") and password == var.config.get("webinterface", "password"):
return True
web_users = json.loads(var.db.get("privilege", "web_access", fallback='[]'))
if username in web_users:
user_dict = json.loads(var.db.get("user", username, fallback='{}'))
if 'password' in user_dict and 'salt' in user_dict and \
util.verify_password(password, user_dict['password'], user_dict['salt']):
return True
return False
def authenticate(): def authenticate():
@ -109,6 +120,7 @@ def requires_auth(f):
if auth_method == 'password': if auth_method == 'password':
auth = request.authorization auth = request.authorization
user = auth.username
if not auth or not check_auth(auth.username, auth.password): if not auth or not check_auth(auth.username, auth.password):
if auth: if auth:
if request.remote_addr in bad_access_count: if request.remote_addr in bad_access_count:

View File

@ -192,14 +192,15 @@ class MumbleBot:
else: else:
self.log.debug("update: no new version found.") self.log.debug("update: no new version found.")
def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False): def register_command(self, cmd, handle, no_partial_match=False, access_outside_channel=False, admin=False):
cmds = cmd.split(",") cmds = cmd.split(",")
for command in cmds: for command in cmds:
command = command.strip() command = command.strip()
if command: if command:
self.cmd_handle[command] = {'handle': handle, self.cmd_handle[command] = {'handle': handle,
'partial_match': not no_partial_match, 'partial_match': not no_partial_match,
'access_outside_channel': access_outside_channel} 'access_outside_channel': access_outside_channel,
'admin': admin}
self.log.debug("bot: command added: " + command) self.log.debug("bot: command added: " + command)
def set_comment(self): def set_comment(self):
@ -287,6 +288,10 @@ class MumbleBot:
constants.strings('bad_command', command=command)) constants.strings('bad_command', command=command))
return return
if self.cmd_handle[command_exc]['admin'] and not self.is_admin(user):
self.mumble.users[text.actor].send_text_message(constants.strings('not_admin'))
return
if not self.cmd_handle[command_exc]['access_outside_channel'] \ if not self.cmd_handle[command_exc]['access_outside_channel'] \
and not self.is_admin(user) \ and not self.is_admin(user) \
and not var.config.getboolean('bot', 'allow_other_channel_message') \ and not var.config.getboolean('bot', 'allow_other_channel_message') \

14
util.py
View File

@ -386,6 +386,20 @@ def parse_file_size(human):
raise ValueError("Invalid file size given.") raise ValueError("Invalid file size given.")
def get_salted_password_hash(password):
salt = os.urandom(10)
hashed = hashlib.pbkdf2_hmac('sha1', password.encode("utf-8"), salt, 100000)
return hashed.hex(), salt.hex()
def verify_password(password, salted_hash, salt):
hashed = hashlib.pbkdf2_hmac('sha1', password.encode("utf-8"), bytearray.fromhex(salt), 100000)
if hashed.hex() == salted_hash:
return True
return False
class LoggerIOWrapper(io.TextIOWrapper): class LoggerIOWrapper(io.TextIOWrapper):
def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer): def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer):
super().__init__(fallback_io_buffer, write_through=True) super().__init__(fallback_io_buffer, write_through=True)