diff --git a/command.py b/command.py index 1791d6f..f049b61 100644 --- a/command.py +++ b/command.py @@ -1,9 +1,10 @@ # coding=utf-8 import logging -import math - -import pymumble_py3 as pymumble +import secrets +import datetime +import json import re +import pymumble_py3 as pymumble import constants import interface @@ -21,12 +22,12 @@ log = logging.getLogger("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('user_ban'), cmd_user_ban, no_partial_match=True) - bot.register_command(constants.commands('user_unban'), cmd_user_unban, no_partial_match=True) - bot.register_command(constants.commands('url_ban_list'), cmd_url_ban_list, no_partial_match=True) - bot.register_command(constants.commands('url_ban'), cmd_url_ban, no_partial_match=True) - bot.register_command(constants.commands('url_unban'), cmd_url_unban, no_partial_match=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, admin=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, admin=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, admin=True) bot.register_command(constants.commands('play'), cmd_play) bot.register_command(constants.commands('pause'), cmd_pause) 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('stop'), cmd_stop) bot.register_command(constants.commands('clear'), cmd_clear) - bot.register_command(constants.commands('kill'), cmd_kill) - bot.register_command(constants.commands('update'), cmd_update, no_partial_match=True) + bot.register_command(constants.commands('kill'), cmd_kill, admin=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('volume'), cmd_volume) 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('add_from_shortlist'), cmd_shortlist) 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('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 bot.register_command('rtrms', cmd_real_time_rms, True) #bot.register_command('loop', cmd_loop_state, True) @@ -126,67 +131,47 @@ def cmd_joinme(bot, user, text, command, parameter): def cmd_user_ban(bot, user, text, command, parameter): global log - if bot.is_admin(user): - if parameter: - bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter)) - else: - bot.mumble.users[text.actor].send_text_message(util.get_user_ban()) + if parameter: + bot.mumble.users[text.actor].send_text_message(util.user_ban(parameter)) else: - bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) - return + bot.mumble.users[text.actor].send_text_message(util.get_user_ban()) def cmd_user_unban(bot, user, text, command, parameter): global log - if bot.is_admin(user): - if 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 + if parameter: + bot.mumble.users[text.actor].send_text_message(util.user_unban(parameter)) def cmd_url_ban(bot, user, text, command, parameter): global log - if bot.is_admin(user): - if parameter: - bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter))) + if parameter: + bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(parameter))) - id = item_id_generators['url'](url=parameter) - var.cache.free_and_delete(id) - var.playlist.remove_by_id(id) - else: - if var.playlist.current_item() and var.playlist.current_item().type == 'url': - item = var.playlist.current_item().item() - bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(item.url))) - var.cache.free_and_delete(item.id) - var.playlist.remove_by_id(item.id) - else: - bot.send_msg(constants.strings('bad_parameter', command=command), text) + id = item_id_generators['url'](url=parameter) + var.cache.free_and_delete(id) + var.playlist.remove_by_id(id) else: - bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) - return + if var.playlist.current_item() and var.playlist.current_item().type == 'url': + item = var.playlist.current_item().item() + bot.mumble.users[text.actor].send_text_message(util.url_ban(util.get_url_from_input(item.url))) + var.cache.free_and_delete(item.id) + var.playlist.remove_by_id(item.id) + else: + bot.send_msg(constants.strings('bad_parameter', command=command), text) 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()) - else: - bot.mumble.users[text.actor].send_text_message(constants.strings('not_admin')) - return + bot.mumble.users[text.actor].send_text_message(util.get_url_ban()) def cmd_url_unban(bot, user, text, command, parameter): global log - if bot.is_admin(user): - if 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 + if parameter: + bot.mumble.users[text.actor].send_text_message(util.url_unban(util.get_url_from_input(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): global log - if bot.is_admin(user): - bot.pause() - bot.exit = True - else: - bot.mumble.users[text.actor].send_text_message( - constants.strings('not_admin')) + bot.pause() + bot.exit = True 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): - import secrets - import datetime - import json - 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) + user_info = var.db.get("user", user, fallback='{}') + user_dict = json.loads(user_info) + if 'token' in user_dict: + var.db.remove_option("web_token", user_dict['token']) - 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 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) +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 def cmd_real_time_rms(bot, user, text, command, parameter): bot._display_rms = not bot._display_rms diff --git a/configuration.default.ini b/configuration.default.ini index 8efa24c..7d134e3 100644 --- a/configuration.default.ini +++ b/configuration.default.ini @@ -96,7 +96,7 @@ listening_addr = 127.0.0.1 listening_port = 8181 web_logfile = -auth_method = 'none' +auth_method = none user = password = max_attempts = 10 @@ -190,6 +190,10 @@ drop_database = dropdatabase rescan = rescan requests_webinterface_access = web +list_webinterface_user = webuserlist +add_webinterface_user = webuseradd +remove_webinterface_user = webuserdel +change_user_password = password [strings] current_volume = Current volume: {volume}. @@ -266,6 +270,9 @@ 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_address= Your own address to access the web interface is {address} +web_user_list = Following users has the privilege to access the web interface:
{users} +user_password_set = Your password has been updated. +command_disabled = {command}: command disabled! help =

Commands

Control diff --git a/configuration.example.ini b/configuration.example.ini index 27eaa22..43176fc 100644 --- a/configuration.example.ini +++ b/configuration.example.ini @@ -135,8 +135,8 @@ port = 64738 #auth_method = token #max_attempts = 10 -# 'user', 'password': If auth_method set to 'password', you need to set the username and -# password. +# 'user', 'password': If auth_method set to 'password', you need to set the default +# username and password. You can add more users by '!webadduser' #user = botamusique #password = mumble diff --git a/interface.py b/interface.py index 90c59e4..e58d7a9 100644 --- a/interface.py +++ b/interface.py @@ -66,7 +66,7 @@ class ReverseProxied(object): web = Flask(__name__) web.config['TEMPLATES_AUTO_RELOAD'] = True log = logging.getLogger("bot") -user = 'webuser' +user = 'Remote Control' def init_proxy(): @@ -82,7 +82,18 @@ def check_auth(username, password): """This function is called to check if a username / 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(): @@ -109,6 +120,7 @@ def requires_auth(f): if auth_method == 'password': auth = request.authorization + user = auth.username if not auth or not check_auth(auth.username, auth.password): if auth: if request.remote_addr in bad_access_count: diff --git a/mumbleBot.py b/mumbleBot.py index 280948f..70df04d 100644 --- a/mumbleBot.py +++ b/mumbleBot.py @@ -192,14 +192,15 @@ class MumbleBot: else: 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(",") for command in cmds: command = command.strip() if command: self.cmd_handle[command] = {'handle': handle, '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) def set_comment(self): @@ -287,6 +288,10 @@ class MumbleBot: constants.strings('bad_command', command=command)) 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'] \ and not self.is_admin(user) \ and not var.config.getboolean('bot', 'allow_other_channel_message') \ diff --git a/util.py b/util.py index 1486e75..44333a5 100644 --- a/util.py +++ b/util.py @@ -386,6 +386,20 @@ def parse_file_size(human): 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): def __init__(self, logger: logging.Logger, logging_level, fallback_io_buffer): super().__init__(fallback_io_buffer, write_through=True)