diff --git a/alliance_auth/settings.py.example b/alliance_auth/settings.py.example index e068b5a1..e2289117 100755 --- a/alliance_auth/settings.py.example +++ b/alliance_auth/settings.py.example @@ -97,15 +97,6 @@ DATABASES = { 'PASSWORD': os.environ.get('AA_DB_PHPBB3_PASSWORD', 'password'), 'HOST': os.environ.get('AA_DB_PHPBB3_HOST', '127.0.0.1'), 'PORT': os.environ.get('AA_DB_PHPBB3_PORT', '3306'), - }, - - 'mumble': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'alliance_mumble', - 'USER': os.environ.get('AA_DB_MUMBLE_USER', 'allianceserver'), - 'PASSWORD': os.environ.get('AA_DB_MUMBLE_PASSWORD', 'password'), - 'HOST': os.environ.get('AA_DB_MUMBLE_HOST', '127.0.0.1'), - 'PORT': os.environ.get('AA_DB_MUMBLE_PORT', '3306'), } } diff --git a/services/managers/mumble_manager.py b/services/managers/mumble_manager.py index c312e00e..bce04b4c 100755 --- a/services/managers/mumble_manager.py +++ b/services/managers/mumble_manager.py @@ -6,46 +6,13 @@ import django from django.db import connections from django.conf import settings +from services.models import MumbleUser + import logging logger = logging.getLogger(__name__) class MumbleManager: - SQL_SELECT_USER_MAX_ID = r"SELECT max(user_id)+1 as next_id from murmur_users" - - SQL_SELECT_GROUP_MAX_ID = r"SELECT MAX(group_id)+1 FROM murmur_groups" - - SQL_CREATE_USER = r"INSERT INTO murmur_users (server_id, user_id, name, pw) VALUES (%s, %s, %s, %s)" - - SQL_SELECT_GET_USER_ID_BY_NAME = r"SELECT user_id from murmur_users WHERE name = %s AND server_id = %s" - - SQL_CHECK_USER_EXIST = r"SELECT name from murmur_users WHERE name = %s AND server_id = %s" - - SQL_DELETE_USER = r"DELETE FROM murmur_users WHERE name = %s AND server_id = %s" - - SQL_UPDATE_USER_PASSWORD = r"UPDATE murmur_users SET pw = %s WHERE name = %s AND server_id = %s" - - SQL_GET_GROUPS = r"SELECT group_id, name FROM murmur_groups WHERE server_id = %s AND channel_id = 0" - - SQL_GET_GROUP_FROM_NAME = r"SELECT group_id, name FROM murmur_groups " \ - r"WHERE server_id = %s AND channel_id = 0 AND name = %s" - - SQL_GET_USER_GROUPS = r"SELECT murmur_groups.name FROM murmur_groups, murmur_group_members WHERE " \ - r"murmur_group_members.group_id = murmur_groups.group_id AND " \ - r"murmur_group_members.server_id = murmur_groups.server_id AND " \ - r"murmur_group_members.user_id = %s" - - SQL_ADD_GROUP = r"INSERT INTO murmur_groups (group_id, server_id, name, channel_id, inherit, inheritable) " \ - r"VALUES (%s, %s, %s, 0, 1, 1)" - - SQL_ADD_USER_TO_GROUP = r"INSERT INTO murmur_group_members (group_id, server_id, user_id, addit) " \ - r"VALUES (%s, %s, %s, 1)" - - SQL_DELETE_USER_FROM_GROUP = r"DELETE FROM murmur_group_members WHERE group_id = %s " \ - r"AND server_id = %s AND user_id = %s" - - def __init__(self): - pass @staticmethod def __santatize_username(username): @@ -69,167 +36,66 @@ class MumbleManager: def _gen_pwhash(password): return hashlib.sha1(password).hexdigest() - @staticmethod - def _get_groups(): - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_GET_GROUPS, [settings.MUMBLE_SERVER_ID]) - rows = dbcursor.fetchall() - - out = {} - for row in rows: - out[row[1]] = row[0] - - logger.debug("Got mumble groups %s" % out) - return out - - @staticmethod - def _get_group(name): - logger.debug("Looking for group name %s in mumble." % name) - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_GET_GROUP_FROM_NAME, [settings.MUMBLE_SERVER_ID, name]) - row = dbcursor.fetchone() - if row: - logger.debug("Found group %s in mumble - %s" % (name, row[0])) - return row[0] - - @staticmethod - def _get_user_groups(name): - logger.debug("Getting mumble groups for username %s" % name) - dbcursor = connections['mumble'].cursor() - user_id = MumbleManager.get_user_id_by_name(name) - dbcursor.execute(MumbleManager.SQL_GET_USER_GROUPS, [user_id]) - out = [row[0] for row in dbcursor.fetchall()] - logger.debug("Got user %s mumble groups %s" % (name, out)) - return out - - @staticmethod - def _add_group(name): - logger.debug("Adding group %s to mumble server." % name) - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_SELECT_GROUP_MAX_ID) - row = dbcursor.fetchone() - groupid = row[0] - dbcursor.execute(MumbleManager.SQL_ADD_GROUP, [groupid, settings.MUMBLE_SERVER_ID, name]) - logger.info("Created group %s on mumble server with id %s" % (name, groupid)) - return groupid - - @staticmethod - def _add_user_to_group(userid, groupid): - if userid != None: - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_ADD_USER_TO_GROUP, [groupid, settings.MUMBLE_SERVER_ID, userid]) - logger.info("Added user id %s to mumble group id %s" % (userid, groupid)) - - @staticmethod - def _del_user_from_group(userid, groupid): - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_DELETE_USER_FROM_GROUP, [groupid, settings.MUMBLE_SERVER_ID, userid]) - logger.info("Removed user id %s from mumble group id %s" % (userid, groupid)) - - @staticmethod - def get_user_id_by_name(name): - logger.debug("Getting mumble user id for user with name %s" % name) - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_SELECT_GET_USER_ID_BY_NAME, [name, settings.MUMBLE_SERVER_ID]) - row = dbcursor.fetchone() - if row: - logger.debug("Got mumble user id %s for name %s" % (row[0], name)) - return row[0] - @staticmethod def create_user(corp_ticker, username): logger.debug("Creating mumble user with username %s and ticker %s" % (username, corp_ticker)) - dbcursor = connections['mumble'].cursor() username_clean = MumbleManager.__santatize_username(MumbleManager.__generate_username(username, corp_ticker)) password = MumbleManager.__generate_random_pass() pwhash = MumbleManager._gen_pwhash(password) logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % (username_clean, pwhash[0:5])) - try: - dbcursor.execute(MumbleManager.SQL_SELECT_USER_MAX_ID) - user_id = dbcursor.fetchone()[0] - - dbcursor.execute(MumbleManager.SQL_CREATE_USER, - [settings.MUMBLE_SERVER_ID, user_id, username_clean, pwhash]) - logger.info("Added user to mumble with username %s" % username_clean) + if MumbleUser.objects.filter(username=username_clean).exists() is False: + logger.info("Creating mumble user %s" % username_clean) + model = MumbleUser.objects.create(username=username_clean, pwhash=pwhash) + return username_clean, password + else: + logger.warn("Mumble user %s already exists. Updating password") + model = MumbleUser.objects.get(username=username_clean) + model.pwhash = pwhash + model.save() + logger.info("Updated mumble user %s" % username_clean) return username_clean, password - except django.db.utils.IntegrityError as error: - logger.exception("IntegrityError during mumble create_user occured.") - except: - logger.exception("Unhandled exception occured.") - logger.error("Exception prevented creation of mumble user. Returning blank for username, password.") - return "", "" @staticmethod def create_blue_user(corp_ticker, username): logger.debug("Creating mumble blue user with username %s and ticker %s" % (username, corp_ticker)) - dbcursor = connections['mumble'].cursor() - username_clean = MumbleManager.__santatize_username(MumbleManager.__generate_username_blue(username, - corp_ticker)) + username_clean = MumbleManager.__santatize_username(MumbleManager.__generate_username_blue(username, corp_ticker)) password = MumbleManager.__generate_random_pass() pwhash = MumbleManager._gen_pwhash(password) logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % (username_clean, pwhash[0:5])) - try: - dbcursor.execute(MumbleManager.SQL_SELECT_USER_MAX_ID) - user_id = dbcursor.fetchone()[0] - - dbcursor.execute(MumbleManager.SQL_CREATE_USER, - [settings.MUMBLE_SERVER_ID, user_id, username_clean, pwhash]) - logger.info("Added blue user to mumble with username %s" % username_clean) + if MumbleUser.objects.filter(username=username_clean).exists() is False: + logger.info("Creating mumble user %s" % username_clean) + model = MumbleUser.objects.create(username=username_clean, pwhash=pwhash) + return username_clean, password + else: + logger.warn("Mumble user %s already exists. Updating password") + model = MumbleUser.objects.get(username=username_clean) + model.pwhash = pwhash + model.save() + logger.info("Updated mumble user %s" % username_clean) return username_clean, password - except: - logger.exception("Unhandled exception occured.") - logger.error("Exception prevented creation of mumble blue user. Returning blank for username, password.") - return "", "" - - @staticmethod - def check_user_exist(username): - logger.debug("Checking if username %s exists on mumble." % username) - dbcursor = connections['mumble'].cursor() - dbcursor.execute(MumbleManager.SQL_CHECK_USER_EXIST, - [username, settings.MUMBLE_SERVER_ID]) - - row = dbcursor.fetchone() - if row and row[0].lower() == username.lower(): - logger.debug("Found username %s on mumble." % username) - return True - logger.debug("Unable to find username %s on mumble." % username) - return False @staticmethod def delete_user(username): logger.debug("Deleting user %s from mumble." % username) - dbcursor = connections['mumble'].cursor() - if MumbleManager.check_user_exist(username): - try: - - dbcursor.execute(MumbleManager.SQL_DELETE_USER, - [username, settings.MUMBLE_SERVER_ID]) - logger.info("Deleted user %s from mumble." % username) - return True - except: - logger.exception("Exception prevented deletion of user %s from mumble." % username) - return False - logger.error("User %s not found on mumble. Unable to delete." % username) + if MumbleUser.objects.filter(username=username).exists(): + MumbleUser.objects.filter(username=username).delete() + logger.info("Deleted user %s from mumble" % username) + return True + logger.error("Unable to delete user %s from mumble: MumbleUser model not found" % username) return False @staticmethod def update_user_password(username, password=None): logger.debug("Updating mumble user %s password." % username) - dbcursor = connections['mumble'].cursor() if not password: password = MumbleManager.__generate_random_pass() pwhash = MumbleManager._gen_pwhash(password) logger.debug("Proceeding with mumble user %s password update - pwhash starts with %s" % (username, pwhash[0:5])) - if MumbleManager.check_user_exist(username): - try: - - dbcursor.execute(MumbleManager.SQL_UPDATE_USER_PASSWORD, - [pwhash, username, settings.MUMBLE_SERVER_ID]) - logger.info("Updated mumble user %s password." % username) - return password - except: - logger.exception("Exception prevented updating of mumble user %s password." % username) - return "" + if MumbleUser.objects.filter(username=username).exists(): + model = MumbleUser.objects.get(username=username) + model.pwhash = pwhash + model.save() + return password logger.error("User %s not found on mumble. Unable to update password." % username) return "" @@ -237,24 +103,11 @@ class MumbleManager: def update_groups(username, groups): logger.debug("Updating mumble user %s groups %s" % (username, groups)) userid = MumbleManager.get_user_id_by_name(username) - mumble_groups = MumbleManager._get_groups() - user_groups = set(MumbleManager._get_user_groups(username)) - act_groups = set([g.replace(' ', '-') for g in groups]) - addgroups = act_groups - user_groups - remgroups = user_groups - act_groups - logger.info("Updating mumble user %s groups - adding %s, removing %s" % (username, addgroups, remgroups)) - for g in addgroups: - if not g in mumble_groups: - mumble_groups[g] = MumbleManager._add_group(g) - try: - logger.debug("Adding mumble user %s to group %s" % (userid, mumble_groups[g])) - MumbleManager._add_user_to_group(userid, mumble_groups[g]) - except: - logger.exception("Exception occured while adding mumble user %s with id %s to group %s with id %s" % (username, userid, g, mumble_groups[g])) - - for g in remgroups: - try: - logger.debug("Deleting mumble user %s from group %s" % (userid, mumble_groups[g])) - MumbleManager._del_user_from_group(userid, mumble_groups[g]) - except: - logger.exception("Exception occured while removing mumble user %s with id %s from group %s with id %s" % (username, userid, g, mumble_groups[g])) + safe_groups = set([g.replace(' ', '-') for g in groups]) + if MumbleUser.objects.filter(username=username).exists(): + logger.info("Updating mumble user %s groups to" % (username, safe_groups)) + model = MumbleUser.objects.get(username=username) + model.groups = safe_groups + model.save() + else: + logger.error("User %s not found on mumble. Unable to update groups." % username) diff --git a/services/models.py b/services/models.py index 2cc7f017..4e6379d9 100644 --- a/services/models.py +++ b/services/models.py @@ -38,3 +38,11 @@ class DiscordAuthToken(models.Model): def __str__(self): output = "Discord Token for email %s user %s" % (self.email, self.user) return output.encode('utf-8') + +class MumbleUser(models.Model): + username = models.CharField(max_length=254, unique=True) + pwhash = models.CharField(max_length=40) + groups = models.TextField(blank=True, null=True) + + def __str__(self): + return self.username + ' - ' + str(self.user) diff --git a/thirdparty/Mumble/authenticator.ini b/thirdparty/Mumble/authenticator.ini new file mode 100644 index 00000000..9b1df99a --- /dev/null +++ b/thirdparty/Mumble/authenticator.ini @@ -0,0 +1,40 @@ +;Database configuration +[database] +;Only tested with MySQL at the moment +lib = MySQLdb +name = alliance_auth +user = allianceserver +password = password +prefix = +host = 127.0.0.1 +port = 3306 + +;Player configuration +[user] +;If you do not already know what it is just leave it as it is +id_offset = 1000000000 +avatar_enable = False +;Reject users if the authenticator experiences an internal error during authentication +reject_on_error = True + +;Ice configuration +[ice] +host = 127.0.0.1 +port = 6502 +slice = Murmur.ice +secret = +watchdog = 30 + +;Murmur configuration +[murmur] +;List of virtual server IDs, empty = all +servers = + +;Logging configuration +[log] +; Available loglevels: 10 = DEBUG (default) | 20 = INFO | 30 = WARNING | 40 = ERROR +level = +file = allianceauth.log + +[iceraw] +Ice.ThreadPool.Server.Size = 5 diff --git a/thirdparty/Mumble/authenticator.py b/thirdparty/Mumble/authenticator.py new file mode 100644 index 00000000..62720858 --- /dev/null +++ b/thirdparty/Mumble/authenticator.py @@ -0,0 +1,796 @@ +#!/usr/bin/env python +# -*- coding: utf-8 + +# Copyright (C) 2010 Stefan Hacker +# All rights reserved. +# Adapted by Adarnof for AllianceAuth +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# - Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the Mumble Developers nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# +# allianceauth.py - Authenticator implementation for password authenticating +# a Murmur server against an AllianceAuth database +# +# Requirements: +# * python >=2.4 and the following python modules: +# * ice-python +# * MySQLdb +# * daemon (when run as a daemon) +# + +import sys +import Ice +import thread +import urllib2 +import logging +import ConfigParser + +from threading import Timer +from optparse import OptionParser +from logging import (debug, + info, + warning, + error, + critical, + exception, + getLogger) + +try: + from hashlib import sha1 +except ImportError: # python 2.4 compat + from sha import sha as sha1 + +def x2bool(s): + """Helper function to convert strings from the config to bool""" + if isinstance(s, bool): + return s + elif isinstance(s, basestring): + return s.lower() in ['1', 'true'] + raise ValueError() + +# +#--- Default configuration values +# +cfgfile = 'allianceauth.ini' +default = {'database':(('lib', str, 'MySQLdb'), + ('name', str, 'alliance_auth'), + ('user', str, 'allianceserver'), + ('password', str, 'password'), + ('prefix', str, ''), + ('host', str, '127.0.0.1'), + ('port', int, 3306)), + + 'user':(('id_offset', int, 1000000000), + ('reject_on_error', x2bool, True)), + + 'ice':(('host', str, '127.0.0.1'), + ('port', int, 6502), + ('slice', str, 'Murmur.ice'), + ('secret', str, ''), + ('watchdog', int, 30)), + + 'iceraw':None, + + 'murmur':(('servers', lambda x:map(int, x.split(',')), []),), + 'glacier':(('enabled', x2bool, False), + ('user', str, 'smf'), + ('password', str, 'secret'), + ('host', str, 'localhost'), + ('port', int, '4063')), + + 'log':(('level', int, logging.DEBUG), + ('file', str, 'allianceauth.log'))} + +# +#--- Helper classes +# +class config(object): + """ + Small abstraction for config loading + """ + + def __init__(self, filename = None, default = None): + if not filename or not default: return + cfg = ConfigParser.ConfigParser() + cfg.optionxform = str + cfg.read(filename) + + for h,v in default.iteritems(): + if not v: + # Output this whole section as a list of raw key/value tuples + try: + self.__dict__[h] = cfg.items(h) + except ConfigParser.NoSectionError: + self.__dict__[h] = [] + else: + self.__dict__[h] = config() + for name, conv, vdefault in v: + try: + self.__dict__[h].__dict__[name] = conv(cfg.get(h, name)) + except (ValueError, ConfigParser.NoSectionError, ConfigParser.NoOptionError): + self.__dict__[h].__dict__[name] = vdefault + +def entity_decode(string): + """ + Python reverse implementation of php htmlspecialchars + """ + htmlspecialchars = (('"', '"'), + ("'", '''), + ('<', '<'), + ('>', '>'), + ('&', '&')) + ret = string + for (s,t) in htmlspecialchars: + ret = ret.replace(t, s) + return ret + +def entity_encode(string): + """ + Python implementation of htmlspecialchars + """ + htmlspecialchars = (('&', '&'), + ('"', '"'), + ("'", '''), + ('<', '<'), + ('>', '>')) + ret = string + for (s,t) in htmlspecialchars: + ret = ret.replace(s, t) + return ret + +class threadDbException(Exception): pass +class threadDB(object): + """ + Small abstraction to handle database connections for multiple + threads + """ + + db_connections = {} + + def connection(cls): + tid = thread.get_ident() + try: + con = cls.db_connections[tid] + except: + info('Connecting to database server (%s %s:%d %s) for thread %d', + cfg.database.lib, cfg.database.host, cfg.database.port, cfg.database.name, tid) + + try: + con = db.connect(host = cfg.database.host, + port = cfg.database.port, + user = cfg.database.user, + passwd = cfg.database.password, + db = cfg.database.name, + charset = 'utf8') + # Transactional engines like InnoDB initiate a transaction even + # on SELECTs-only. Thus, we auto-commit so smfauth gets recent data. + con.autocommit(True) + except db.Error, e: + error('Could not connect to database: %s', str(e)) + raise threadDbException() + cls.db_connections[tid] = con + return con + connection = classmethod(connection) + + def cursor(cls): + return cls.connection().cursor() + cursor = classmethod(cursor) + + def execute(cls, *args, **kwargs): + if "threadDB__retry_execution__" in kwargs: + # Have a magic keyword so we can call ourselves while preventing + # an infinite loop + del kwargs["threadDB__retry_execution__"] + retry = False + else: + retry = True + + c = cls.cursor() + try: + c.execute(*args, **kwargs) + except db.OperationalError, e: + error('Database operational error %d: %s', e.args[0], e.args[1]) + c.close() + cls.invalidate_connection() + if retry: + # Make sure we only retry once + info('Retrying database operation') + kwargs["threadDB__retry_execution__"] = True + c = cls.execute(*args, **kwargs) + else: + error('Database operation failed ultimately') + raise threadDbException() + return c + execute = classmethod(execute) + + def invalidate_connection(cls): + tid = thread.get_ident() + con = cls.db_connections.pop(tid, None) + if con: + debug('Invalidate connection to database for thread %d', tid) + con.close() + + invalidate_connection = classmethod(invalidate_connection) + + def disconnect(cls): + while cls.db_connections: + tid, con = cls.db_connections.popitem() + debug('Close database connection for thread %d', tid) + con.close() + disconnect = classmethod(disconnect) + +def do_main_program(): + # + #--- Authenticator implementation + # All of this has to go in here so we can correctly daemonize the tool + # without loosing the file descriptors opened by the Ice module + slicedir = Ice.getSliceDir() + if not slicedir: + slicedir = ["-I/usr/share/Ice/slice", "-I/usr/share/slice"] + else: + slicedir = ['-I' + slicedir] + Ice.loadSlice('', slicedir + [cfg.ice.slice]) + import Murmur + + class allianceauthauthenticatorApp(Ice.Application): + def run(self, args): + self.shutdownOnInterrupt() + + if not self.initializeIceConnection(): + return 1 + + if cfg.ice.watchdog > 0: + self.failedWatch = True + self.checkConnection() + + # Serve till we are stopped + self.communicator().waitForShutdown() + self.watchdog.cancel() + + if self.interrupted(): + warning('Caught interrupt, shutting down') + + threadDB.disconnect() + return 0 + + def initializeIceConnection(self): + """ + Establishes the two-way Ice connection and adds the authenticator to the + configured servers + """ + ice = self.communicator() + + if cfg.ice.secret: + debug('Using shared ice secret') + ice.getImplicitContext().put("secret", cfg.ice.secret) + elif not cfg.glacier.enabled: + warning('Consider using an ice secret to improve security') + + if cfg.glacier.enabled: + #info('Connecting to Glacier2 server (%s:%d)', glacier_host, glacier_port) + error('Glacier support not implemented yet') + #TODO: Implement this + + info('Connecting to Ice server (%s:%d)', cfg.ice.host, cfg.ice.port) + base = ice.stringToProxy('Meta:tcp -h %s -p %d' % (cfg.ice.host, cfg.ice.port)) + self.meta = Murmur.MetaPrx.uncheckedCast(base) + + adapter = ice.createObjectAdapterWithEndpoints('Callback.Client', 'tcp -h %s' % cfg.ice.host) + adapter.activate() + + metacbprx = adapter.addWithUUID(metaCallback(self)) + self.metacb = Murmur.MetaCallbackPrx.uncheckedCast(metacbprx) + + authprx = adapter.addWithUUID(smfauthenticator()) + self.auth = Murmur.ServerUpdatingAuthenticatorPrx.uncheckedCast(authprx) + + return self.attachCallbacks() + + def attachCallbacks(self, quiet = False): + """ + Attaches all callbacks for meta and authenticators + """ + + # Ice.ConnectionRefusedException + #debug('Attaching callbacks') + try: + if not quiet: info('Attaching meta callback') + + self.meta.addCallback(self.metacb) + + for server in self.meta.getBootedServers(): + if not cfg.murmur.servers or server.id() in cfg.murmur.servers: + if not quiet: info('Setting authenticator for virtual server %d', server.id()) + server.setAuthenticator(self.auth) + + except (Murmur.InvalidSecretException, Ice.UnknownUserException, Ice.ConnectionRefusedException), e: + if isinstance(e, Ice.ConnectionRefusedException): + error('Server refused connection') + elif isinstance(e, Murmur.InvalidSecretException) or \ + isinstance(e, Ice.UnknownUserException) and (e.unknown == 'Murmur::InvalidSecretException'): + error('Invalid ice secret') + else: + # We do not actually want to handle this one, re-raise it + raise e + + self.connected = False + return False + + self.connected = True + return True + + def checkConnection(self): + """ + Tries reapplies all callbacks to make sure the authenticator + survives server restarts and disconnects. + """ + #debug('Watchdog run') + + try: + if not self.attachCallbacks(quiet = not self.failedWatch): + self.failedWatch = True + else: + self.failedWatch = False + except Ice.Exception, e: + error('Failed connection check, will retry in next watchdog run (%ds)', cfg.ice.watchdog) + debug(str(e)) + self.failedWatch = True + + # Renew the timer + self.watchdog = Timer(cfg.ice.watchdog, self.checkConnection) + self.watchdog.start() + + def checkSecret(func): + """ + Decorator that checks whether the server transmitted the right secret + if a secret is supposed to be used. + """ + if not cfg.ice.secret: + return func + + def newfunc(*args, **kws): + if 'current' in kws: + current = kws["current"] + else: + current = args[-1] + + if not current or 'secret' not in current.ctx or current.ctx['secret'] != cfg.ice.secret: + error('Server transmitted invalid secret. Possible injection attempt.') + raise Murmur.InvalidSecretException() + + return func(*args, **kws) + + return newfunc + + def fortifyIceFu(retval = None, exceptions = (Ice.Exception,)): + """ + Decorator that catches exceptions,logs them and returns a safe retval + value. This helps preventing the authenticator getting stuck in + critical code paths. Only exceptions that are instances of classes + given in the exceptions list are not caught. + + The default is to catch all non-Ice exceptions. + """ + def newdec(func): + def newfunc(*args, **kws): + try: + return func(*args, **kws) + except Exception, e: + catch = True + for ex in exceptions: + if isinstance(e, ex): + catch = False + break + + if catch: + critical('Unexpected exception caught') + exception(e) + return retval + raise + + return newfunc + return newdec + + class metaCallback(Murmur.MetaCallback): + def __init__(self, app): + Murmur.MetaCallback.__init__(self) + self.app = app + + @fortifyIceFu() + @checkSecret + def started(self, server, current = None): + """ + This function is called when a virtual server is started + and makes sure an authenticator gets attached if needed. + """ + if not cfg.murmur.servers or server.id() in cfg.murmur.servers: + info('Setting authenticator for virtual server %d', server.id()) + try: + server.setAuthenticator(app.auth) + # Apparently this server was restarted without us noticing + except (Murmur.InvalidSecretException, Ice.UnknownUserException), e: + if hasattr(e, "unknown") and e.unknown != "Murmur::InvalidSecretException": + # Special handling for Murmur 1.2.2 servers with invalid slice files + raise e + + error('Invalid ice secret') + return + else: + debug('Virtual server %d got started', server.id()) + + @fortifyIceFu() + @checkSecret + def stopped(self, server, current = None): + """ + This function is called when a virtual server is stopped + """ + if self.app.connected: + # Only try to output the server id if we think we are still connected to prevent + # flooding of our thread pool + try: + if not cfg.murmur.servers or server.id() in cfg.murmur.servers: + info('Authenticated virtual server %d got stopped', server.id()) + else: + debug('Virtual server %d got stopped', server.id()) + return + except Ice.ConnectionRefusedException: + self.app.connected = False + + debug('Server shutdown stopped a virtual server') + + if cfg.user.reject_on_error: # Python 2.4 compat + authenticateFortifyResult = (-1, None, None) + else: + authenticateFortifyResult = (-2, None, None) + + class allianceauthauthenticator(Murmur.ServerUpdatingAuthenticator): + texture_cache = {} + def __init__(self): + Murmur.ServerUpdatingAuthenticator.__init__(self) + + @fortifyIceFu(authenticateFortifyResult) + @checkSecret + def authenticate(self, name, pw, certlist, certhash, strong, current = None): + """ + This function is called to authenticate a user + """ + + # Search for the user in the database + FALL_THROUGH = -2 + AUTH_REFUSED = -1 + + if name == 'SuperUser': + debug('Forced fall through for SuperUser') + return (FALL_THROUGH, None, None) + + try: + sql = 'SELECT id, pwhash, groups FROM %sservices_mumbleuser WHERE username = %%s' % cfg.database.prefix + cur = threadDB.execute(sql, [name]) + except threadDbException: + return (FALL_THROUGH, None, None) + + res = cur.fetchone() + cur.close() + if not res: + info('Fall through for unknown user "%s"', name) + return (FALL_THROUGH, None, None) + + uid, upwhash, ugroups = res + + if allianceauth_check_hash(pw, upwhash): + info('User authenticated: "%s" (%d)', name, uid + cfg.user.id_offset) + debug('Group memberships: %s', str(ugroups)) + return (uid + cfg.user.id_offset, entity_decode(name), ugroups) + + info('Failed authentication attempt for user: "%s" (%d)', name, uid + cfg.user.id_offset) + return (AUTH_REFUSED, None, None) + + @fortifyIceFu((False, None)) + @checkSecret + def getInfo(self, id, current = None): + """ + Gets called to fetch user specific information + """ + + # We do not expose any additional information so always fall through + debug('getInfo for %d -> denied', id) + return (False, None) + + @fortifyIceFu(-2) + @checkSecret + def nameToId(self, name, current = None): + """ + Gets called to get the id for a given username + """ + + FALL_THROUGH = -2 + if name == 'SuperUser': + debug('nameToId SuperUser -> forced fall through') + return FALL_THROUGH + + try: + sql = 'SELECT id FROM %sservices_mumbleuser WHERE username = %%s' % cfg.database.prefix + cur = threadDB.execute(sql, [name]) + except threadDbException: + return FALL_THROUGH + + res = cur.fetchone() + cur.close() + if not res: + debug('nameToId %s -> ?', name) + return FALL_THROUGH + + debug('nameToId %s -> %d', name, (res[0] + cfg.user.id_offset)) + return res[0] + cfg.user.id_offset + + @fortifyIceFu("") + @checkSecret + def idToName(self, id, current = None): + """ + Gets called to get the username for a given id + """ + + FALL_THROUGH = "" + # Make sure the ID is in our range and transform it to the actual smf user id + if id < cfg.user.id_offset: + return FALL_THROUGH + bbid = id - cfg.user.id_offset + + # Fetch the user from the database + try: + sql = 'SELECT username FROM %sservices_mumbleuser WHERE id = %%s' % cfg.database.prefix + cur = threadDB.execute(sql, [bbid]) + except threadDbException: + return FALL_THROUGH + + res = cur.fetchone() + cur.close() + if res: + if res[0] == 'SuperUser': + debug('idToName %d -> "SuperUser" catched') + return FALL_THROUGH + + debug('idToName %d -> "%s"', id, res[0]) + return res[0] + + debug('idToName %d -> ?', id) + return FALL_THROUGH + + @fortifyIceFu("") + @checkSecret + def idToTexture(self, id, current = None): + """ + Gets called to get the corresponding texture for a user + """ + + FALL_THROUGH = "" + + debug('idToTexture "%s" -> fall through', id) + return FALL_THROUGH + + @fortifyIceFu(-2) + @checkSecret + def registerUser(self, name, current = None): + """ + Gets called when the server is asked to register a user. + """ + + FALL_THROUGH = -2 + debug('registerUser "%s" -> fall through', name) + return FALL_THROUGH + + @fortifyIceFu(-1) + @checkSecret + def unregisterUser(self, id, current = None): + """ + Gets called when the server is asked to unregister a user. + """ + + FALL_THROUGH = -1 + # Return -1 to fall through to internal server database, we will not modify the smf database + # but we can make murmur delete all additional information it got this way. + debug('unregisterUser %d -> fall through', id) + return FALL_THROUGH + + @fortifyIceFu({}) + @checkSecret + def getRegisteredUsers(self, filter, current = None): + """ + Returns a list of usernames in the AllianceAuth database which contain + filter as a substring. + """ + + if not filter: + filter = '%' + + try: + sql = 'SELECT id, username FROM %sservices_mumbleuser WHERE username LIKE %%s' % cfg.database.prefix + cur = threadDB.execute(sql, [filter]) + except threadDbException: + return {} + + res = cur.fetchall() + cur.close() + if not res: + debug('getRegisteredUsers -> empty list for filter "%s"', filter) + return {} + debug ('getRegisteredUsers -> %d results for filter "%s"', len(res), filter) + return dict([(a + cfg.user.id_offset, b) for a,b in res]) + + @fortifyIceFu(-1) + @checkSecret + def setInfo(self, id, info, current = None): + """ + Gets called when the server is supposed to save additional information + about a user to his database + """ + + FALL_THROUGH = -1 + # Return -1 to fall through to the internal server handler. We must not modify + # the smf database so the additional information is stored in murmurs database + debug('setInfo %d -> fall through', id) + return FALL_THROUGH + + @fortifyIceFu(-1) + @checkSecret + def setTexture(self, id, texture, current = None): + """ + Gets called when the server is asked to update the user texture of a user + """ + + FALL_THROUGH = -1 + + debug('setTexture %d -> fall through', id) + return FALL_THROUGH + + class CustomLogger(Ice.Logger): + """ + Logger implementation to pipe Ice log messages into + our own log + """ + + def __init__(self): + Ice.Logger.__init__(self) + self._log = getLogger('Ice') + + def _print(self, message): + self._log.info(message) + + def trace(self, category, message): + self._log.debug('Trace %s: %s', category, message) + + def warning(self, message): + self._log.warning(message) + + def error(self, message): + self._log.error(message) + + # + #--- Start of authenticator + # + info('Starting AllianceAuth mumble authenticator') + initdata = Ice.InitializationData() + initdata.properties = Ice.createProperties([], initdata.properties) + for prop, val in cfg.iceraw: + initdata.properties.setProperty(prop, val) + + initdata.properties.setProperty('Ice.ImplicitContext', 'Shared') + initdata.properties.setProperty('Ice.Default.EncodingVersion', '1.0') + initdata.logger = CustomLogger() + + app = allianceauthauthenticatorApp() + state = app.main(sys.argv[:1], initData = initdata) + info('Shutdown complete') + + + +# +#--- Python implementation of the AllianceAuth MumbleUser hash function +# +def allianceauth_check_hash(password, hash): + """ + Python implementation of the smf check hash function + """ + return sha1(password).hexdigest() == hash + +# +#--- Start of program +# +if __name__ == '__main__': + # Parse commandline options + parser = OptionParser() + parser.add_option('-i', '--ini', + help = 'load configuration from INI', default = cfgfile) + parser.add_option('-v', '--verbose', action='store_true', dest = 'verbose', + help = 'verbose output [default]', default = True) + parser.add_option('-q', '--quiet', action='store_false', dest = 'verbose', + help = 'only error output') + parser.add_option('-d', '--daemon', action='store_true', dest = 'force_daemon', + help = 'run as daemon', default = False) + parser.add_option('-a', '--app', action='store_true', dest = 'force_app', + help = 'do not run as daemon', default = False) + (option, args) = parser.parse_args() + + if option.force_daemon and option.force_app: + parser.print_help() + sys.exit(1) + + # Load configuration + try: + cfg = config(option.ini, default) + except Exception, e: + print>>sys.stderr, 'Fatal error, could not load config file from "%s"' % cfgfile + sys.exit(1) + + try: + db = __import__(cfg.database.lib) + except ImportError, e: + print>>sys.stderr, 'Fatal error, could not import database library "%s", '\ + 'please install the missing dependency and restart the authenticator' % cfg.database.lib + sys.exit(1) + + + # Initialize logger + if cfg.log.file: + try: + logfile = open(cfg.log.file, 'a') + except IOError, e: + #print>>sys.stderr, str(e) + print>>sys.stderr, 'Fatal error, could not open logfile "%s"' % cfg.log.file + sys.exit(1) + else: + logfile = logging.sys.stderr + + + if option.verbose: + level = cfg.log.level + else: + level = logging.ERROR + + logging.basicConfig(level = level, + format='%(asctime)s %(levelname)s %(message)s', + stream = logfile) + + # As the default try to run as daemon. Silently degrade to running as a normal application if this fails + # unless the user explicitly defined what he expected with the -a / -d parameter. + try: + if option.force_app: + raise ImportError # Pretend that we couldn't import the daemon lib + import daemon + except ImportError: + if option.force_daemon: + print>>sys.stderr, 'Fatal error, could not daemonize process due to missing "daemon" library, ' \ + 'please install the missing dependency and restart the authenticator' + sys.exit(1) + do_main_program() + else: + context = daemon.DaemonContext(working_directory = sys.path[0], + stderr = logfile) + context.__enter__() + try: + do_main_program() + finally: + context.__exit__(None, None, None)