#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2011 Benjamin Jemlich # Copyright (C) 2011 Nathaniel Kofalt # Copyright (C) 2013 Stefan Hacker # Copyright (C) 2014 Dominik George # Copyright (C) 2020 Andreas Valder # # All rights reserved. # # 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. # This script will let you authenticate Murmur against an LDAP tree. # Note that you should have a reasonable understanding of LDAP before trying to use this script. # # Unfortunately, LDAP is a rather complex concept / protocol / software suite. # So if you're not already experienced with LDAP, the Mumble team may be unable to assist you. # Unless you already have an existing LDAP tree, you may want to authenticate your users another way. # However, LDAP has the advantage of being extremely scalable, flexible, and resilient. # This is probably a decent choice for larger-scale deployments (code review this script first!) # # There are some excellent resources to get you started: # Wikipedia article: http://en.wikipedia.org/wiki/LDAP # OpenLDAP intro: http://www.openldap.org/doc/admin24/intro.html # LDAP on Debian: http://techpubs.spinlocksolutions.com/dklar/ldap.html # IRC Chat room: Channel #ldap on irc.freenode.net # # Configuring this to hit LDAP correctly can be a little tricky. # This is largely due to the numerous ways you can store user information in LDAP. # The example configuration is probably not the best way to do things; it's just a simple setup. # # The group-membership code will have to be expanded if you want multiple groups allowed, etc. # This is just a simple example. # # In this configuration, I use a really simple groupOfUniqueNames and OU of inetOrgPersons. # The tree already uses the "uid" attribute for usernames, so roomNumber was used to store UID. # Note that mumble needs a username, password, and unique UID for each user. # You can definitely set things up differently; this is a bit of a kludge. # # Here is the tree layout used in the example config: # dc=example,dc=com (organization) # ou=Groups (organizationalUnit) # cn=mumble (groupOfUniqueNames) # "uniqueMember: uid=user1,dc=example,dc=com" # "uniqueMember: uid=user2,dc=example,dc=com" # ou=Users (organizationalUnit) # uid=user1 (inetOrgPerson) # "userPassword: {SHA}password-hash" # "displayName: User One" # "roomNumber: 1" # uid=user2 (inetOrgPerson) # "userPassword: {SHA}password-hash" # "displayName: User Two" # "roomNumber: 2" # uid=user3 (inetOrgPerson) # "userPassword: {SHA}password-hash" # "displayName: User Three" # "roomNumber: 3" # # How the script operates: # First, the script will attempt to "bind" with the user's credentials. # If the bind fails, the username/password combination is rejected. # Second, it optionally checks for a group membership. # With groups off, all three users are let in; with groups on, only user1 & user2 are allowed. # Finally, it optionally logs in the user with a separate "display_attr" name. # This allows user1 to log in with the USERNAME "user1" but is displayed in mumble as "User One". # # If you use the bind_dn option, the script will bind with the specified DN # and check for the existence of user and (optionally) the group membership # before it binds with the username/password. This allows you to use a server # which only allows authentication by end users without any search # permissions. It also allows you to set the reject_on_miss option to false # and let login IDs not found in LDAP fall-through to an alternate # authentication scheme. # # Requirements: # * python >=3.8 (maybe 3.6 is enough but it wasn't tested) and the following python modules: # * ice-python # * python-ldap # * daemon (when run as a daemon) # If you are using Ubuntu/Debian (only Ubuntu 20.04 was tested) the following packages provide these: # * python3 # * python3-zeroc-ice # * python3-ldap # * python3-daemon # * zeroc-ice-slice import sys import ldap import Ice import _thread import urllib.request, urllib.error, urllib.parse import logging import configparser from threading import Timer from optparse import OptionParser from logging import (debug, info, warning, error, critical, exception, getLogger) def x2bool(s): """Helper function to convert strings from the config to bool""" if isinstance(s, bool): return s elif isinstance(s, str): return s.lower() in ['1', 'true'] raise ValueError() # #--- Default configuration values # cfgfile = 'LDAPauth.ini' default = { 'ldap':(('ldap_uri', str, 'ldap://127.0.0.1'), ('bind_dn', str, ''), ('bind_pass', str, ''), ('users_dn', str, 'ou=Users,dc=example,dc=org'), ('discover_dn', x2bool, True), ('username_attr', str, 'uid'), ('number_attr', str, 'RoomNumber'), ('display_attr', str, 'displayName'), ('group_dn', str, ''), ('group_attr', str, ''), ('provide_info', x2bool, False), ('mail_attr', str, 'mail'), ('provide_users', x2bool, False), ('use_start_tls', x2bool, False)), 'user':(('id_offset', int, 1000000000), ('reject_on_error', x2bool, True), ('reject_on_miss', 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:list(map(int, x.split(','))), []),), 'glacier':(('enabled', x2bool, False), ('user', str, 'ldapauth'), ('password', str, 'secret'), ('host', str, 'localhost'), ('port', int, '4063')), 'log':(('level', int, logging.DEBUG), ('file', str, 'LDAPauth.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.items(): 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 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 LDAPAuthenticatorApp(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') 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(LDAPAuthenticator()) 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) as 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 as 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 as 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) as 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 LDAPAuthenticator(Murmur.ServerUpdatingAuthenticator): def __init__(self): Murmur.ServerUpdatingAuthenticator.__init__(self) self.name_uid_cache = dict() @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 # SuperUser is a special login. if name == 'SuperUser': debug('Forced fall through for SuperUser') return (FALL_THROUGH, None, None) # Otherwise, let's check the LDAP server. uid = None if cfg.ldap.use_start_tls: # try StartTLS: global options debug('use_start_tls is set, setting global option TLS_REQCERT = never') ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap_trace = 0 # Change to 1 for more verbose trace ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, ldap_trace) if cfg.ldap.use_start_tls: # try StartTLS: connection specific options debug('use_start_tls is set, setting connection options X_TLS_*') ldap_conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) ldap_conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) ldap_conn.set_option(ldap.OPT_X_TLS_DEMAND, True) try: ldap_conn.start_tls_s() except Exception as e: warning('could not initiate StartTLS, e = ' + str(e)) return (AUTH_REFUSED, None, None) if cfg.ldap.bind_dn: # Bind the functional account to search the directory. bind_dn = cfg.ldap.bind_dn bind_pass = cfg.ldap.bind_pass try: debug('try to connect to ldap (bind_dn will be used)') ldap_conn.bind_s(bind_dn, bind_pass) except ldap.INVALID_CREDENTIALS: ldap_conn.unbind() warning('Invalid credentials for bind_dn=' + bind_dn) return (AUTH_REFUSED, None, None) elif cfg.ldap.discover_dn: # Use anonymous bind to discover the DN try: ldap_conn.bind_s() except ldap.INVALID_CREDENTIALS: ldap_conn.unbind() warning('Failed anomymous bind for discovering DN') return (AUTH_REFUSED, None, None) else: # Prevent anonymous authentication. if not pw: warning("No password supplied for user " + name) return (AUTH_REFUSED, None, None) # Bind the user account to search the directory. bind_dn = "%s=%s,%s" % (cfg.ldap.username_attr, name, cfg.ldap.users_dn) bind_pass = pw try: ldap_conn.bind_s(bind_dn, bind_pass) except ldap.INVALID_CREDENTIALS: ldap_conn.unbind() warning('User ' + name + ' failed with invalid credentials') return (AUTH_REFUSED, None, None) # Search for the user. name_split = name.split(".") username_to_try = name_split[0] if "." in name else name device = name_split[1] if "." in name else "" res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.username_attr, username_to_try), [cfg.ldap.number_attr, cfg.ldap.display_attr]) if len(res) == 0: warning("User " + username_to_try + " not found, input was " + name) if cfg.user.reject_on_miss: return (AUTH_REFUSED, None, None) else: return (FALL_THROUGH, None, None) match = res[0] #Only interested in the first result, as there should only be one match # Parse the user information. uid = int(match[1][cfg.ldap.number_attr][0]) displayName = match[1][cfg.ldap.display_attr][0].decode() user_dn = match[0] debug('User match found, display "' + displayName + '" with UID ' + repr(uid)) # Optionally check groups. if cfg.ldap.group_dn != "" : debug('Checking group membership for ' + name) #Search for user in group res = ldap_conn.search_s(cfg.ldap.group_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.group_attr, user_dn), [cfg.ldap.number_attr, cfg.ldap.display_attr]) # Check if the user is a member of the group if len(res) < 1: debug('User ' + name + ' failed with no group membership') return (AUTH_REFUSED, None, None) # Second bind to test user credentials if using bind_dn or discover_dn. if cfg.ldap.bind_dn or cfg.ldap.discover_dn: # Prevent anonymous authentication. if not pw: warning("No password supplied for user " + name) return (AUTH_REFUSED, None, None) bind_dn = user_dn bind_pass = pw try: ldap_conn.bind_s(bind_dn, bind_pass) except ldap.INVALID_CREDENTIALS: ldap_conn.unbind() warning('User ' + name + ' failed with wrong password') return (AUTH_REFUSED, None, None) # Unbind and close connection. ldap_conn.unbind() # If we get here, the login is correct. # Add the user/id combo to cache, then accept: self.name_uid_cache[displayName] = uid debug("Login accepted for " + name) if device != "": displayName = f"{displayName} ({device})" return (uid + cfg.user.id_offset, displayName, []) @fortifyIceFu((False, None)) @checkSecret def getInfo(self, id, current = None): """ Gets called to fetch user specific information """ if not cfg.ldap.provide_info: # We do not expose any additional information so always fall through debug('getInfo for %d -> denied', id) return (False, None) ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, 0) # Bind if configured, else do explicit anonymous bind if cfg.ldap.bind_dn and cfg.ldap.bind_pass: ldap_conn.simple_bind_s(cfg.ldap.bind_dn, cfg.ldap.bind_pass) else: ldap_conn.simple_bind_s() name = self.idToName(id, current) res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.display_attr, name), [cfg.ldap.display_attr, cfg.ldap.mail_attr ]) #If user found, return info if len(res) == 1: info = {} if cfg.ldap.mail_attr in res[0][1]: info[Murmur.UserInfo.UserEmail] = res[0][1][cfg.ldap.mail_attr][0].decode() debug('getInfo %s -> %s', name, repr(info)) return (True, info) else: debug('getInfo %s -> ?', name) 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 if name in self.name_uid_cache: uid = self.name_uid_cache[name] + cfg.user.id_offset debug("nameToId %s (cache) -> %d", name, uid) return uid ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, 0) # Bind if configured, else do explicit anonymous bind if cfg.ldap.bind_dn and cfg.ldap.bind_pass: ldap_conn.simple_bind_s(cfg.ldap.bind_dn, cfg.ldap.bind_pass) else: ldap_conn.simple_bind_s() res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (cfg.ldap.display_attr, name), [cfg.ldap.number_attr]) #If user found, return the ID if len(res) == 1: uid = int(res[0][1][cfg.ldap.number_attr][0]) + cfg.user.id_offset debug('nameToId %s -> %d', name, uid) else: debug('nameToId %s -> ?', name) return FALL_THROUGH return uid @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 LDAP user id if id < cfg.user.id_offset: debug('idToName %d -> fall through', id) return FALL_THROUGH ldapid = id - cfg.user.id_offset for name, uid in self.name_uid_cache.items(): if uid == ldapid: if name == 'SuperUser': debug('idToName %d -> "SuperUser" catched', id) return FALL_THROUGH debug('idToName %d -> "%s"', id, name) return name 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 %d -> 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 LDAP directory # 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 LDAP directory which contain filter as a substring. """ FALL_THROUGH = {} if not cfg.ldap.provide_users: # Fall through if not configured to provide user list debug('getRegisteredUsers -> fall through') return FALL_THROUGH ldap_conn = ldap.initialize(cfg.ldap.ldap_uri, 0) # Bind if configured, else do explicit anonymous bind if cfg.ldap.bind_dn and cfg.ldap.bind_pass: ldap_conn.simple_bind_s(cfg.ldap.bind_dn, cfg.ldap.bind_pass) else: ldap_conn.simple_bind_s() if filter: res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(&(uid=*)(%s=*%s*))' % (cfg.ldap.display_attr, filter), [cfg.ldap.number_attr, cfg.ldap.display_attr]) else: res = ldap_conn.search_s(cfg.ldap.users_dn, ldap.SCOPE_SUBTREE, '(uid=*)', [cfg.ldap.number_attr, cfg.ldap.display_attr]) # Build result dict users = {} for dn, attrs in res: if cfg.ldap.number_attr in attrs and cfg.ldap.display_attr in attrs: uid = int(attrs[cfg.ldap.number_attr][0]) + cfg.user.id_offset name = attrs[cfg.ldap.display_attr][0] users[uid] = name debug('getRegisteredUsers %s -> %s', filter, repr(users)) return users @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 do not store # any information in LDAP 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 # We do not store textures in LDAP debug('setTexture %d -> fall through', id) return FALL_THROUGH class CustomLogger(Ice.Logger): """ Logger implementation to pipe Ice log messages into out 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 LDAP 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 = LDAPAuthenticatorApp() state = app.main(sys.argv[:1], initData = initdata) info('Shutdown complete') # #--- 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 as e: print('Fatal error, could not load config file from "%s"' % cfgfile, file=sys.stderr) sys.exit(1) # Initialize logger if cfg.log.file: try: logfile = open(cfg.log.file, 'a') except IOError as e: #print>>sys.stderr, str(e) print('Fatal error, could not open logfile "%s"' % cfg.log.file, file=sys.stderr) 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('Fatal error, could not daemonize process due to missing "daemon" library, ' \ 'please install the missing dependency and restart the authenticator', file=sys.stderr) 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)