# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# Copyright (C) 2012-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
# Copyright (C) 2012-2020 by Josh Lukens <jlukens@botch.com>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

"""\
:class:`x2gobroker.brokers.base_broker.X2GoBroker` class - base skeleton for X2GoBroker implementations

"""
__NAME__ = 'x2gobroker-pylib'

# modules
import copy
import socket
import uuid
import netaddr
import random
import time
import os.path

# X2Go Broker modules
import x2gobroker.config
import x2gobroker.defaults
import x2gobroker.agent
import x2gobroker.x2gobroker_exceptions
import x2gobroker.loadchecker

from x2gobroker.loggers import logger_broker, logger_error

from x2gobroker.defaults import X2GOBROKER_USER as _X2GOBROKER_USER
from x2gobroker.defaults import X2GOBROKER_DAEMON_USER as _X2GOBROKER_DAEMON_USER

class X2GoBroker(object):
    """\
    :class:`x2gobroker.brokers.base_broker.X2GoBroker` is an abstract class for X2Go broker implementations.

    This class needs to be inherited from a concrete broker class.

    Currently available broker classes are::
        :class:`zeroconf.X2GoBroker <x2gobroker.brokers.zeroconf.X2GoBroker>` (working)
        :class:`inifile.X2GoBroker <x2gobroker.brokers.inifile.X2GoBroker>` (working)
        :class:`ldap.X2GoBroker <x2gobroker.brokers.ldap.X2GoBroker>` (in prep)

    """

    backend_name = 'base'
    nameservice_module = None
    authmech_module = None

    def __init__(self, config_file=None, config_defaults=None):
        """\
        Initialize a new X2GoBroker instance to control X2Go session through an
        X2Go Client with an intermediate session broker.

        :param config_file: path to the X2Go Session Broker configuration file (x2gobroker.conf)
        :type config_file: ``str``
        :param config_defaults: Default settings for the broker's global configuration parameters.
        :type config_defaults: ``dict``

        """
        self.config_file = config_file
        if self.config_file is None: self.config_file = x2gobroker.defaults.X2GOBROKER_CONFIG
        if config_defaults is None: config_defaults = x2gobroker.defaults.X2GOBROKER_CONFIG_DEFAULTS
        self.config = x2gobroker.config.X2GoBrokerConfigFile(config_files=self.config_file, defaults=config_defaults)
        self.enabled =  self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'enable')

        self._dynamic_cookie_map = {}
        self._client_address = None
        self._cookie = None

    def __del__(self):
        """\
        Cleanup on destruction of an :class:`X2GoBroker <x2gobroker.brokers.base_broker.X2GoBroker>` instance.

        """
        pass

    def is_enabled(self):
        """\
        Check if this backend has been enabled in the configuration file.

        """
        return self.enabled

    def get_name(self):
        """\
        Accessor for self.backend_name property.

        :returns: the backend name
        :rtype: ``str``

        """
        return self.backend_name

    def enable(self):
        """\
        Enable this broker backend.

        """
        self.enabled = True

    def disable(self):
        """\
        Disable this broker backend.

        """
        self.enabled = False

    def set_client_address(self, address):
        """\
        Set the client IP address.

        :param address: the client IP
        :type address: ``str``

        """
        if netaddr.valid_ipv6(address):
            pass
        elif netaddr.valid_ipv4(address):
            pass
        else:
            self._client_address = None
            raise ValueError('address {address} is neither a valid IPv6 nor a valid IPv4 address'.format(address=address))
        self._client_address = netaddr.IPAddress(address)

    def get_client_address(self):
        """\
        Get the client IP address (if set).

        :returns: the client IP (either IPv4 or IPv6)
        :rtype: ``str``

        """
        if self._client_address:
            return str(self._client_address)
        else:
            return None

    def get_client_address_type(self):
        """\
        Get the client IP address type of the client address (if set).

        :returns: the client address type (4: IPv4, 6: IPv6)
        :rtype: ``int``

        """
        return self._client_address.version

    def get_global_config(self):
        """\
        Get the global section of the configuration file.

        :returns: all global configuration parameters
        :rtype: ``dict``

        """
        return self.config.get_section('global')

    def get_global_value(self, option):
        """\
        Get the configuration setting for an option in the global section of the
        configuration file.

        :param option: option name in the global configuration section
        :type option: ``str``

        :returns: the value for the given global ``option``
        :rtype: ``bool``, ``str``, ``int`` or ``list``

        """
        return self.config.get_value('global', option)

    def get_my_cookie(self):
        """\
        Get the pre-set authentication cookie UUID hash that clients
        have to use on their first connection attempt (if the global
        config option "require-cookie" has been set).

        :returns: the pre-set authentication cookie UUID hash
        :rtype: ``str``

        """
        unconfigured_my_cookie = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
        my_cookie = unconfigured_my_cookie

        deprecated_my_cookie = self.config.get_value('global', 'my-cookie')
        if deprecated_my_cookie is None:
            deprecated_my_cookie = uuid.uuid4()
        my_cookie_file = self.config.get_value('global', 'my-cookie-file')

        if os.path.isfile(my_cookie_file):
            fh = open(my_cookie_file, "r")
            lines = [ line for line in fh.read().split('\n') if line and not line.startswith('#') ]
            my_cookie = lines[0].split(' ')[0]

        if my_cookie != unconfigured_my_cookie:
            return my_cookie
        elif deprecated_my_cookie != unconfigured_my_cookie:
            return deprecated_my_cookie

        # instead of returning None here, we invent a cookie and return that
        return str(uuid.uuid4())

    def get_backend_config(self):
        """\
        Get the configuration section of a specific backend.

        :returns: all backend configuration parameters
        :rtype: ``dict``

        """
        return self.config.get_section('broker_{backend}'.format(backend=self.backend_name))

    def get_backend_value(self, backend='zeroconf', option='enable'):
        """\
        Get the configuration setting for backend ``backend`` and option
        ``option``.

        :param backend: the name of the backend
        :type backend: ``str``
        :param option: option name of the backend's configuration section
        :type option: ``str``

        :returns: the value for the given ``backend`` ``option``
        :rtype: ``bool``, ``str``, ``int`` or ``list``

        """
        return self.config.get_value(backend, option)

    def get_profile_ids(self):
        """\
        Retrieve the complete list of session profile IDs.

        :returns: list of profile IDs
        :rtype: ``list``

        """
        return []

    def get_profile_ids_for_user(self, username):
        """\
        Retrieve the list of session profile IDs for a given user.

        :param username: query profile id list for this user
        :type username: ``str``

        :returns: list of profile IDs
        :rtype: ``list``

        """
        return [ id for id in self.get_profile_ids() if self.check_profile_acls(username, self.get_profile_acls(id)) ]

    def get_profile_defaults(self):
        """\
        Get the session profile defaults, i.e. profile options that all
        configured session profiles have in common.

        The defaults are hard-coded in :mod:`x2gobroker.defaults` for class
        :class:`x2gobroker.brokers.base_broker.X2GoBroker`.

        :returns: a dictionary containing the session profile defaults
        :rtype: ``dict``

        """
        profile_defaults = copy.deepcopy(x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS['DEFAULT'])
        for key in copy.deepcopy(profile_defaults):
            if key.startswith('acl-'):
                del profile_defaults[key]
        return profile_defaults

    def get_acl_defaults(self):
        """\
        Get the ACL defaults for session profiles. The defaults are hard-coded
        in :mod:`x2gobroker.defaults` for class :class:`x2gobroker.brokers.base_broker.X2GoBroker`.

        :returns: a dictionary containing the ACL defaults for all session profiles
        :rtype: ``dict``

        """
        acl_defaults = copy.deepcopy(x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS['DEFAULT'])
        for key in copy.deepcopy(acl_defaults):
            if not key.startswith('acl-'):
                del acl_defaults[key]
        return acl_defaults

    def get_profile(self, profile_id):
        """\
        Get the session profile for profile ID <profile_id>.

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the session profile for ID <profile_id>
        :rtype: ``dict``

        """
        return {}

    def get_profile_broker(self, profile_id):
        """\
        Get broker-specific session profile options from the session profile with profile ID <profile_id>.

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the session profile for ID <profile_id>
        :rtype: ``dict``

        """
        return {}

    def get_profile_acls(self, profile_id):
        """\
        Get the ACLs for session profile with profile ID <profile_id>.

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the ACLs for session profile with ID <profile_id>
        :rtype: ``dict``

        """
        return {}

    def check_profile_acls(self, username, acls):
        """\
        Test if a given user can get through an ACL check using <acls> as a list
        of allow and deny rules.

        :param username: the username of interest
        :type username: ``str``
        :param acls: a dictionary data structure containing ACL information (see :envvar:`x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS`)
        :type acls: ``dict``

        """
        ### extract ACLs evaluation orders

        _acls = self.get_acl_defaults()
        _acls.update(acls)
        _order = {}
        _order['users'] = _order['groups'] = _order['clients'] = _acls['acl-any-order']

        try: _order['users'] = _acls['acl-users-order']
        except KeyError: pass
        try: _order['groups'] = _acls['acl-groups-order']
        except KeyError: pass
        try: _order['clients'] = _acls['acl-clients-order']
        except KeyError: pass

        # to pass an ACL test, all three keys in the dict below have to be set to True
        # if one stays False, the related session profile will not be returned to the querying
        # X2Go client...
        _grant_availability = {
            'by_user': None,
            'by_group': None,
            'by_client': None,
        }

        ### CHECKING on a per-client basis...

        ### clients access is granted first, if that fails then we return False here...
        if len( _acls['acl-clients-allow'] + _acls['acl-clients-deny'] ) > 0:

            _acls_clients_allow = copy.deepcopy(_acls['acl-clients-allow'])
            _acls_clients_deny = copy.deepcopy(_acls['acl-clients-deny'])

            _allow_client = False
            _deny_client = False

            for idx, item in enumerate(_acls_clients_allow):
                if item == 'ALL':
                    _acls_clients_allow[idx] = '0.0.0.0/0'
                    _acls_clients_allow.insert(idx, '::/0')
            for idx, item in enumerate(_acls_clients_deny):
                if item == 'ALL':
                    _acls_clients_deny[idx] = '0.0.0.0/0'
                    _acls_clients_deny.insert(idx, '::/0')

            _allow_address_set = []
            _deny_address_set = []
            try:
                _allow_address_set = netaddr.IPSet(_acls_clients_allow)
                _deny_address_set = netaddr.IPSet(_acls_clients_deny)
            except netaddr.core.AddrFormatError as e:
                logger_error.error('base_broker.X2GoBroker.check_acls(): netaddr.core.AddrFormatError - {why}'.format(why=str(e)))
            except ValueError as e:
                logger_error.error('base_broker.X2GoBroker.check_acls(): ValueError - {why}'.format(why=str(e)))

            _allow_client = self._client_address in _allow_address_set
            _deny_client = self._client_address in _deny_address_set

            if _order['clients'] == 'allow-deny':
                if   _allow_client: _grant_availability['by_client'] = True
                elif _deny_client : _grant_availability['by_client'] = False
            else:
                if   _deny_client : _grant_availability['by_client'] = False
                elif _allow_client: _grant_availability['by_client'] = True

            if _grant_availability['by_client'] is not True:
                return False

        ### no user/group ACLs are in use, allow access then...

        if len(_acls['acl-users-allow'] + _acls['acl-users-deny'] + _acls['acl-groups-allow'] + _acls['acl-groups-deny']) == 0:
            return True

        ### CHECKING on a per-user basis...

        if len( _acls['acl-users-allow'] + _acls['acl-users-deny'] ) > 0:

            _allow_user = False
            _deny_user = False

            if username in _acls['acl-users-allow'] or 'ALL' in _acls['acl-users-allow']:
                _allow_user = True

            if username in _acls['acl-users-deny'] or 'ALL' in _acls['acl-users-deny']:
                _deny_user = True

            if _order['users'] == 'allow-deny':
                if   _allow_user: _grant_availability['by_user'] = True
                elif _deny_user : _grant_availability['by_user'] = False
            else:
                if   _deny_user : _grant_availability['by_user'] = False
                elif _allow_user: _grant_availability['by_user'] = True

            # if a user has been granted access directly, then the corresponding session profile(s)
            # will be provided to him/her, it does not matter what the group acl will have to say to this...
            if _grant_availability['by_user']:
                return True

        ### CHECKING on a per-group basis...

        if len(_acls['acl-groups-allow'] + _acls['acl-groups-deny']) > 0:
            _allow_group = False
            _deny_group = False

            _user_groups = ['ALL'] + self.get_user_groups(username, primary_groups=not self.get_global_value('ignore-primary-group-memberships'))

            _allow_group = bool(len(set(_user_groups).intersection( set(_acls['acl-groups-allow']) )))
            _deny_group = bool(len(set(_user_groups).intersection( set(_acls['acl-groups-deny']) )))

            if _order['groups'] == 'allow-deny':
                if   _allow_group: _grant_availability['by_group'] = True
                elif _deny_group : _grant_availability['by_group'] = False
            else:
                if   _deny_group : _grant_availability['by_group'] = False
                elif _allow_group: _grant_availability['by_group'] = True

            if _grant_availability['by_group'] and _grant_availability['by_user'] is not False:
                return True

        return False

    def test_connection(self):
        return 'OK'

    def _import_authmech_module(self, mech='pam'):
        try:
            if self.authmech_module is None:
                _authmech_module = None
                namespace = {}
                exec("import x2gobroker.authmechs.{mech}_authmech as _authmech_module".format(mech=mech), namespace)
                self.authmech_module = namespace['_authmech_module']
            return True
        except ImportError:
            return False

    def _do_authenticate(self, username='', password=''):

        mech = self.get_authentication_mechanism()
        logger_broker.debug('base_broker.X2GoBroker._do_authenticate(): attempting authentication, will try "{mech}" mechanism for authenticating the user.'.format(mech=mech))
        if self._import_authmech_module(mech=mech):
            logger_broker.debug('base_broker.X2GoBroker._do_authenticate(): authenticating user={username} with password=<hidden> against mechanism "{mech}".'.format(username=username, mech=mech))
            return self.authmech_module.X2GoBrokerAuthMech().authenticate(username, password, config=self.config)
        else:
            return False

    def get_authentication_mechanism(self):
        """\
        Get the name of the authentication mechanism that is configured for this
        X2Go Session Broker instance.

        :returns: auth-mech name
        :rtype: ``str``

        """
        _default_auth_mech = "pam"
        _auth_mech = ""

        if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech'):
            _auth_mech = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'auth-mech').lower()
            logger_broker.debug('base_broker.X2GoBroker.get_authentication_mechanism(): found auth-mech in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_auth_mech))

        elif self.config.has_value('global', 'default-auth-mech'):
            _default_auth_mech = self.config.get_value('global', 'default-auth-mech').lower()
            logger_broker.debug('base_broker.X2GoBroker.get_authentication_mechanism(): found default-auth-mech in global config section: {value}'.format(value=_default_auth_mech))

        return _auth_mech or _default_auth_mech

    def _enforce_agent_query_mode(self, mode='LOCAL'):
        """\
        Allow frontends to enforce a certain broker agent backend.

        :param mode: what agent query mode demanded
        :type mode: ``str``
        :returns: the agent query mode we force the broker to
        :rtype: ``str``

        """
        return None

    def get_agent_query_mode(self, profile_id):
        """\
        Get the agent query mode (LOCAL or SSH, normally) that is configured for this
        X2Go Session Broker instance.

        :returns: agent query mode
        :rtype: ``str``

        """
        _default_agent_query_mode = "LOCAL"
        _backend_agent_query_mode = ""
        _agent_query_mode = ""

        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-agent-query-mode' in _profile and _profile['broker-agent-query-mode']:
            _agent_query_mode = _profile['broker-agent-query-mode']
            logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found broker-agent-query-mode in session profile with ID {id}: {value}. This one has precendence over the default and the backend value.'.format(id=profile_id, value=_agent_query_mode))

        elif self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode'):
            _backend_agent_query_mode = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-query-mode').lower()
            logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found agent-query-mode in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_agent_query_mode))

        elif self.config.has_value('global', 'default-agent-query-mode') and self.config.get_value('global', 'default-agent-query-mode'):
            _default_agent_query_mode = self.config.get_value('global', 'default-agent-query-mode').lower()
            logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): found default-agent-query-mode in global config section: {value}'.format(value=_default_agent_query_mode))

        _mode = _agent_query_mode or _backend_agent_query_mode or _default_agent_query_mode

        # if the frontend overrides the agent query mode, immediately return it here...
        if self._enforce_agent_query_mode(mode=_mode):
            _new_mode = self._enforce_agent_query_mode(mode=_mode)
            logger_broker.debug('base_broker.X2GoBroker.get_agent_query_mode(): broker frontend overrides configured agent query mode ("{mode}"), using mode agent query mode: "{new_mode}".'.format(mode=_mode, new_mode=_new_mode))
            return _new_mode
        else:
            return _mode

    def get_agent_hostkey_policy(self, profile_id):
        """\
        Get the agent hostkey policy (either of 'RejectPolicy',
        'AutoAddPolicy' or 'WarningPolicy') that is configured for this
        X2Go Session Broker instance.

        The returned policy names match the MissingHostkeyPolicy class
        names as found in Python Paramiko.

        :returns: agent hostkey policy
        :rtype: ``str``

        """
        _default_agent_hostkey_policy = "RejectPolicy"
        _backend_agent_hostkey_policy = ""
        _agent_hostkey_policy = ""

        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-agent-hostkey-policy' in _profile and _profile['broker-agent-hostkey-policy']:
            _agent_hostkey_policy = _profile['broker-agent-hostkey-policy']
            logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found broker-agent-hostkey-policy in session profile with ID {id}: {value}. This one has precendence over the default and the backend value.'.format(id=profile_id, value=_agent_hostkey_policy))

        elif self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy') and self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy'):
            _backend_agent_hostkey_policy = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'agent-hostkey-policy')
            logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found agent-hostkey-policy in backend config section »{backend}«: {value}. This one has precendence over the default value.'.format(backend=self.backend_name, value=_agent_hostkey_policy))

        elif self.config.has_value('global', 'default-agent-hostkey-policy') and self.config.get_value('global', 'default-agent-hostkey-policy'):
            _default_agent_hostkey_policy = self.config.get_value('global', 'default-agent-hostkey-policy')
            logger_broker.debug('base_broker.X2GoBroker.get_agent_hostkey_policy(): found default-agent-hostkey-policy in global config section: {value}'.format(value=_default_agent_hostkey_policy))

        _policy = _agent_hostkey_policy or _backend_agent_hostkey_policy or _default_agent_hostkey_policy

        if _policy not in ('AutoAddPolicy', 'RejectPolicy', 'WarningPolicy'):
            logger_broker.warn('base_broker.X2GoBroker.get_agent_hostkey_policy(): given hostkey policy ({policy}) is invalid/unknown, falling back to default hostkey policy ({default_policy}).'.format(policy=_policy, default_policy=_default_agent_hostkey_policy))
            _policy = _default_agent_hostkey_policy

        return _policy

    def get_session_autologin(self, profile_id):
        """\
        Detect if the given profile is configured to try automatic session
        logons.

        :returns: ``True`` to denote that automatic session login should be attempted
        :rtype: ``bool``

        """
        _default_session_autologin = False
        _session_autologin = False

        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-session-autologin' in _profile and _profile['broker-session-autologin']:
            _session_autologin = _profile['broker-session-autologin']
            if type(_session_autologin) == str:
                _session_autologin = _session_autologin.lower() in ('1', 'true')
            logger_broker.debug('base_broker.X2GoBroker.get_session_autologin(): found broker-session-autologin in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_session_autologin))

        elif self.config.has_value('global', 'default-session-autologin'):
            _default_session_autologin = self.config.get_value('global', 'default-session-autologin')
            logger_broker.debug('base_broker.X2GoBroker.get_session_autologin(): found default-session-autologin in global config section: {value}'.format(value=_default_session_autologin))

        return _session_autologin or _default_session_autologin
    # API compat name:
    use_session_autologin = get_session_autologin

    def get_portscan_x2goservers(self, profile_id):
        """\
        Detect if the given profile is configured to try portscanning on X2Go Servers
        before offering an X2Go Server hostname to the client.

        :returns: ``True`` if X2Go Servers shall be probed before offering it to clients
        :rtype: ``bool``

        """
        _default_portscan_x2goservers = False
        _portscan_x2goservers = False

        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-portscan-x2goservers' in _profile and _profile['broker-portscan-x2goservers']:
            _portscan_x2goservers = _profile['broker-portscan-x2goservers']
            if type(_portscan_x2goservers) == str:
                _portscan_x2goservers = _portscan_x2goservers.lower() in ('1', 'true')
            logger_broker.debug('base_broker.X2GoBroker.get_portscan_x2goservers(): found broker-portscan-x2goservers in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_portscan_x2goservers))

        elif self.config.has_value('global', 'default-portscan-x2goservers'):
            _default_portscan_x2goservers = self.config.get_value('global', 'default-portscan-x2goservers')
            logger_broker.debug('base_broker.X2GoBroker.get_portscan_x2goservers(): found default-portscan-x2goservers in global config section: {value}'.format(value=_default_portscan_x2goservers))

        return _portscan_x2goservers or _default_portscan_x2goservers
    # API compat name:
    use_portscan_x2goservers = get_portscan_x2goservers

    def get_authorized_keys_file(self, profile_id):
        """\
        Get the default location of server-side authorized_keys files used with
        the X2Go Session Broker.

        The file location can be configured broker-wide. It is also possible to
        provide a broker-authorized-keys file in session profiles. The latter
        will override the broker-wide conigured file location.

        :returns: authorized_keys location on the remote server
        :rtype: ``str``

        """
        _default_authorized_keys_file = "%h/.x2go/authorized_keys"
        _authorized_keys_file = ""
        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-authorized-keys' in _profile and _profile['broker-authorized-keys']:
            _authorized_keys_file = _profile['broker-authorized-keys']
            logger_broker.debug('base_broker.X2GoBroker.get_authorized_keys_file(): found broker-authorized-keys in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_authorized_keys_file))

        elif self.config.has_value('global', 'default-authorized-keys'):
            _default_authorized_keys_file = self.config.get_value('global', 'default-authorized-keys')
            logger_broker.debug('base_broker.X2GoBroker.get_authorized_keys_file(): found default-authorized-keys in global config section: {value}'.format(value=_default_authorized_keys_file))

        return _authorized_keys_file or _default_authorized_keys_file

    def get_sshproxy_authorized_keys_file(self, profile_id):
        """\
        Get the default location of SSH proxy server-side authorized_keys files used with
        the X2Go Session Broker.

        The file location can be configured broker-wide. It is also possible to
        provide a broker-authorized-keys file in session profiles. The latter
        will override the broker-wide conigured file location.

        :returns: authorized_keys location on the remote SSH proxy server
        :rtype: ``str``

        """
        _default_authorized_keys_file = "%h/.x2go/authorized_keys"
        _authorized_keys_file = ""
        _profile = self.get_profile_broker(profile_id)
        if _profile and 'broker-sshproxy-authorized-keys' in _profile and _profile['broker-sshproxy-authorized-keys']:
            _authorized_keys_file = _profile['broker-sshproxy-authorized-keys']
            logger_broker.debug('base_broker.X2GoBroker.get_sshproxy_authorized_keys_file(): found broker-sshproxy-authorized-keys in session profile with ID {id}: {value}. This one has precendence over the default value.'.format(id=profile_id, value=_authorized_keys_file))

        elif self.config.has_value('global', 'default-sshproxy-authorized-keys'):
            _default_authorized_keys_file = self.config.get_value('global', 'default-sshproxy-authorized-keys')
            logger_broker.debug('base_broker.X2GoBroker.get_sshproxy_authorized_keys_file(): found default-sshproxy-authorized-keys in global config section: {value}'.format(value=_default_authorized_keys_file))

        return _authorized_keys_file or _default_authorized_keys_file

    def get_userdb_service(self):
        """\
        Get the name of the backend being used for retrieving user information from the
        system.

        :returns: user service name
        :rtype: ``str``

        """
        _user_db = "libnss"
        if self.config.has_value('global', 'default-user-db'):
            _user_db = self.config.get_value('global', 'default-user-db').lower() or _user_db

        if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'user-db'):
            _user_db = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'user-db').lower() or _user_db

        return _user_db

    def get_groupdb_service(self):
        """\
        Get the name of the backend being used for retrieving group information from the
        system.

        :returns: group service name
        :rtype: ``str``

        """
        _group_db = "libnss"
        if self.config.has_value('global', 'default-group-db'):
            _group_db = self.config.get_value('global', 'default-group-db').lower() or _group_db

        if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'group-db'):
            _group_db = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'group-db').lower() or _group_db

        return _group_db

    def get_use_load_checker(self):
        """\
        Is this broker backend configured to access an X2Go Broker LoadChecker daemon.

        :returns: ``True`` if there should a load checker daemon running.
        :rtype: ``bool``

        """
        _use_load_checker = False
        if self.config.has_value('global', 'default-use-load-checker'):
            _use_load_checker = self.config.get_value('global', 'default-use-load-checker') or _use_load_checker

        if self.config.has_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker'):
            _use_load_checker = self.config.get_value('broker_{backend}'.format(backend=self.backend_name), 'use-load-checker') or _use_load_checker

        return _use_load_checker

    def use_load_checker(self, profile_id):
        """\
        Actually query the load checker daemon for the given session profile ID.
        This method will check:

          - broker backend configured per backend or globally
            to use load checker daemon?
          - or on a per session profile basis?
          - plus: more than one host configured for the given session profile?

        :param profile_id: choose remote agent for this profile ID
        :type profile_id: ``str``

        :returns: ``True`` if there is a load checker daemon running.
        :rtype: ``bool``

        """
        _profile_broker  = self.get_profile_broker(profile_id)
        if not _profile_broker:
           return False
        _profile_session = self.get_profile(profile_id)

        # only use load checker if...

        # more than one host is defined in the session profile
        if len(_profile_session['host']) < 2:
            return False

        # if not blocked on a per session profile basis
        if 'broker-use-load-checker' in _profile_broker and _profile_broker['broker-use-load-checker'] not in ('1', 'true', 'TRUE', 'True'):
            return False

        # if load checking is enabled globally, for the broker backend,
        # or for the given session profile...
        if self.get_use_load_checker() or ('broker-use-load-checker' in _profile_broker and _profile_broker['broker-use-load-checker'] in ('1', 'true', 'TRUE', 'True')):
            return True

        return False

    def _import_nameservice_module(self, service='libnss'):
        try:
            if self.nameservice_module is None:
                _nameservice_module = None
                namespace = {}
                exec("import x2gobroker.nameservices.{service}_nameservice as _nameservice_module".format(service=service), namespace)
                self.nameservice_module = namespace['_nameservice_module']
            return True
        except ImportError:
            return False

    def has_user(self, username):
        """\
        Test if the broker knows user ``<username>``.

        :param username: test for existence of this user
        :type username: ``str``

        :returns: returns ``True`` if a user exists
        :rtype: ``bool``

        """
        if self._import_nameservice_module(service=self.get_userdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().has_user(username=username)
        else:
            return False

    def get_users(self):
        """\
        Get list of known users.

        :returns: returns list of known users
        :rtype: ``list``

        """
        if self._import_nameservice_module(service=self.get_userdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().get_users()
        else:
            return False

    def has_group(self, group):
        """\
        Test if the broker knows group ``<group>``.

        :param group: test for existence of this group
        :type group: ``str``

        :returns: returns ``True`` if a group exists
        :rtype: ``bool``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().has_group(group=group)
        else:
            return False

    def get_groups(self):
        """\
        Get list of known groups.

        :returns: returns list of known groups
        :rtype: ``list``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().get_groups()
        else:
            return False

    def get_primary_group(self, username):
        """\
        Get the primary group of a given user.

        :param username: get primary group for this username
        :type username: ``str``

        :returns: returns the name of the primary group
        :rtype: ``str``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().get_primary_group(username)
        else:
            return False

    def is_group_member(self, username, group, primary_groups=False):
        """\
        Check if a user is member of a given group.

        :param username: check group membership of this user
        :type username: ``str``
        :param group: test if user is member of this group
        :type group: ``str``
        :param primary_groups:  if ``True``, test for primary group membership, as well
        :type primary_groups: ``bool``

        :returns: returns ``True`` if the user is member of the given group
        :rtype: ``bool``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().is_group_member(username=username, group=group, primary_groups=primary_groups)
        else:
            return []

    def get_group_members(self, group, primary_groups=False):
        """\
        Get the list of members in group ``<group>``.

        :param group: valid group name
        :type group: ``str``
        :param primary_groups: include primary groups found with the user db service
        :type primary_groups: ``bool``

        :returns: list of users belonging to the given group
        :rtype: ``list``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().get_group_members(group=group, primary_groups=primary_groups)
        else:
            return []

    def get_user_groups(self, username, primary_groups=False):
        """\
        Get all groups a given user is member of.

        :param username: get groups for this user
        :type username: ``str``
        :param primary_groups:  if ``True``, include the user's primary group in the group list
        :type primary_groups: ``bool``

        :returns: list of groups the given user is member of
        :rtype: ``list``

        """
        if self._import_nameservice_module(service=self.get_groupdb_service()):
            return self.nameservice_module.X2GoBrokerNameService().get_user_groups(username=username, primary_groups=primary_groups)
        else:
            return []

    def check_access(self, username='', password='', ip='', cookie=None, override_password_auth=False):
        """\
        Check if a given user with a given password may gain access to the
        X2Go session broker.

        :param username: a username known to the session broker
        :type username: ``str``
        :param password: a password that authenticates the user against the X2Go session broker
        :type password: ``str``
        :param ip: the ip address of the client
        :type ip: ``str``
        :param cookie: an extra (static or dynamic) authentication token
        :type cookie: ``str``
        :param override_password_auth: let password auth always succeed, needed for SSH broker (where SSH
            handled the password (or key) based authentication
        :type override_password_auth: ``bool``

        :returns: returns ``True`` if the authentication has been successful
        :rtype: ``bool``,``str``

        """
        require_password = self.config.get_value('global', 'require-password')
        require_cookie = self.config.get_value('global', 'require-cookie')

        # LEGACY support for X2Go Session Broker (<< 0.0.3.0) configuration files
        if not self.config.get_value('global', 'check-credentials'):
            logger_broker.warning('base_broker.X2GoBroker.check_access(): deprecated parameter \'check-credentials\' used in x2gobroker.conf (use \'require-password\' and \'require-cookie\' instead)!!!'.format(configfile=self.config_file))
            require_password = False
            require_cookie = False

        ### FOR INTRANET LOAD BALANCER WE MAY JUST ALLOW ACCESS TO EVERYONE
        ### This is handled through the config file, normally /etc/x2go/x2gobroker.conf
        if not require_password and not require_cookie:
            logger_broker.debug('base_broker.X2GoBroker.check_access(): access is granted without checking credentials, prevent this in {configfile}'.format(configfile=self.config_file))
            return True, None
        elif username == 'check-credentials' and password == 'FALSE':
            # this catches a validation check from the UCCS web frontend...
            return False, None

        ### IMPLEMENT YOUR AUTHENTICATION LOGIC IN THE self._do_authenticate(**kwargs) METHOD
        ### when inheriting from the x2gobroker.brokers.base_broker.X2GoBroker class.
        if type(cookie) is bytes:
            cookie = cookie

        if (((cookie == None) or (cookie == "")) and require_cookie):
            #cookie required but we did not get one - catch wrong cookie case later
            logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie required but none given.')
            return False, None

        # check if cookie sent was our preset cookie from config file
        next_cookie = self.get_my_cookie()
        access = (cookie == next_cookie )
        logger_broker.debug('base_broker.X2GoBroker.check_access(): checking if our configured cookie was submitted: {access}'.format(access=access))

        # the require cookie but not password case falls through to returning value of access
        if require_password:

            # using files to store persistant cookie information because global variables do not work across threads in WSGI
            if _X2GOBROKER_USER == _X2GOBROKER_DAEMON_USER:
                cookie_directory = self.config.get_value('global', 'cookie-directory')
                cookie_directory = os.path.normpath(cookie_directory)
            else:
                cookie_directory=os.path.normpath(os.path.expanduser('~/.x2go/broker-cookies/'))

            if (not os.path.isdir(cookie_directory)):
                logger_broker.debug('base_broker.X2GoBroker.check_access(): cookie-directory {cookie_directory} does not exist trying to create it'.format(cookie_directory=cookie_directory))
                try:
                    os.makedirs(cookie_directory);
                except:
                    logger_broker.warning('base_broker.X2GoBroker.check_access(): could not create cookie-directory {cookie_directory} failing to authenticate'.format(cookie_directory=cookie_directory))
                    return False, None

            if access or cookie == None or cookie == "":
                # this should be the first time we have seen this user or they are using old client so verify their passwrd

                ### IMPLEMENT YOUR AUTHENTICATION LOGIC IN THE self._do_authenticate(**kwargs) METHOD
                ### when inheriting from the x2gobroker.brokers.base_brokers.X2GoBroker class.

                access = self._do_authenticate(username=username, password=password) or override_password_auth

                ###
                ###

                if access:
                    logger_broker.warning('base_broker.X2GoBroker.check_access(): authentication succeeded for user {username} at ip {ip}'.format(username=username, ip=ip))
                    #create new cookie for this user
                    #each user gets one or more tuples of IP, time stored as username_UUID files so they can connect from multiple sessions
                    next_cookie = str(uuid.uuid4())
                    if cookie_directory and username and next_cookie:
                        fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w")
                        fh.write('{ip} {time}'.format(ip=ip, time=time.time()))
                        fh.close()
                    if cookie_directory and username and cookie:
                        os.remove(cookie_directory+"/"+username+"_"+cookie)
                    logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving new cookie: {cookie} to user {username} at ip {ip}'.format(cookie=next_cookie,username=username,ip=ip))
                else:
                    logger_broker.warning('base_broker.X2GoBroker.check_access(): authentication failed for user {username} at ip {ip}'.format(username=username, ip=ip))

            else:
                # there is a cookie but its not ours so its either wrong or subsequent password auth
                if os.path.isfile(cookie_directory+"/"+username+"_"+cookie):

                    logger_broker.debug('base_broker.X2GoBroker.check_access(): found valid auth key for user cookie: {usercookie}'.format(usercookie=username+"_"+cookie))
                    fh=open(cookie_directory+"/"+username+"_"+cookie,"r")
                    origip,origtime= fh.read().split()
                    fh.close()
                    os.unlink(cookie_directory+"/"+username+"_"+cookie)

                    # found cookie - make sure IP and time are good
                    if self.config.get_value('global', 'verify-ip') and (ip != origip):
                        logger_broker.debug('base_broker.X2GoBroker.check_access(): IPs differ (new: {ip} old: {origip}) - rejecting user'.format(ip=ip,origip=origip))
                        return False, None
                    if (time.time() - float(origtime)) > self.config.get_value('global', 'auth-timeout'):
                        logger_broker.debug('base_broker.X2GoBroker.check_access(): Too much time elapsed since origional auth - rejecting user')
                        return False, None
                    if self.config.get_value('global', 'use-static-cookie'):
                        #if using static cookies keep same cookie as user presented
                        next_cookie = cookie
                    else:
                        #otherwise give them new random cookie
                        next_cookie = str(uuid.uuid4())

                    logger_broker.debug('base_broker.X2GoBroker.check_access(): Giving cookie: {cookie} to ip {ip}'.format(cookie=next_cookie, ip=ip))
                    fh = open(cookie_directory+"/"+username+"_"+next_cookie,"w")
                    fh.write('{ip} {time}'.format(ip=ip, time=origtime))
                    fh.close()
                    access = True
                else:

                    # FIXME: here we need some magic to remove deprecated cookie files (by their timestamp)!!!

                    # client sent us an unknown cookie so failing auth
                    logger_broker.debug('base_broker.X2GoBroker.check_access(): User {username} from {ip} presented cookie {cookie} which is not recognized - rejecting user'.format(username=username, cookie=cookie, ip=ip))
                    return False, None

        return access, next_cookie

    def get_remote_agent(self, profile_id, exclude_agents=[], ):
        """\
        Randomly choose a remote agent for agent query.

        :param profile_id: choose remote agent for this profile ID
        :type profile_id: ``str``
        :param exclude_agents: a list of remote agent dict objects to be exclude
            from the random choice
        :type exclude_agents: ``list``

        :returns: remote agent to use for queries for profile ID
        :rtype: ``dict``

        """
        remote_agent = None

        # no remote agent needed for shadow sessions
        if self.is_shadow_profile(profile_id):
            return remote_agent

        agent_query_mode = self.get_agent_query_mode(profile_id).upper()
        if agent_query_mode == 'SSH' and x2gobroker.agent.has_remote_broker_agent_setup():

            profile = self.get_profile(profile_id)
            server_list = profile['host']
            random.shuffle(server_list)

            # if the load checker is in use for this profile, let's retrieve the available server loads here
            # because:
            #    - it is fast...
            #    - if hosts are marked as "HOST-UNREACHABLE", we don't have to attempt
            #      using them as a remote agent (reduce delays at session
            #      startup/resumption)
            #    - the retrieved load factors can be re-used in X2GoBroker.select_session().
            load_factors = {}
            if self.use_load_checker(profile_id):
                load_factors = x2gobroker.loadchecker.check_load(self.backend_name, profile_id)
                for h in [ _h for _h in list(load_factors.keys()) if type(load_factors[_h]) != int ]:
                    if h in server_list:
                        server_list.remove(h)

            for agent in exclude_agents:
                if agent['hostname'] in server_list:
                    server_list.remove(agent['hostname'])

            while server_list:

                remote_agent_hostname = server_list[-1]
                remote_agent_hostaddr = remote_agent_hostname
                remote_agent_port = profile['sshport']
                if 'sshport={hostname}'.format(hostname=remote_agent_hostname) in profile:
                    remote_agent_port = profile["sshport={hostname}".format(hostname=remote_agent_hostname)]
                if 'host={hostname}'.format(hostname=remote_agent_hostname) in profile:
                    remote_agent_hostaddr = profile["host={hostname}".format(hostname=remote_agent_hostname)]

                remote_agent = {
                    'hostname': remote_agent_hostname,
                    'hostaddr': remote_agent_hostaddr,
                    'port': remote_agent_port,
                    'host_key_policy': self.get_agent_hostkey_policy(profile_id),
                }

                try:
                    if x2gobroker.agent.ping(remote_agent=remote_agent):
                        break
                except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException:
                    # at the end of this loop, an empty dict means: no X2Go Server could be contacted!!!
                    remote_agent = {}

                server_list = server_list[0:-1]

            if not remote_agent:
                logger_broker.warning('base_broker.X2GoBroker.get_remote_agent(): failed to allocate any broker agent (query-mode: {query_mode}, remote_agent: {remote_agent})'.format(query_mode=agent_query_mode, remote_agent=remote_agent))
            else:
                # ship the load_factors retrieved from the load checker service in the remote_agent dict
                remote_agent['load_factors'] = load_factors

        elif agent_query_mode == 'LOCAL':
            # use a non-False value here, not used anywhere else...
            remote_agent = 'LOCAL'

        return remote_agent

    def get_all_remote_agents(self, profile_id):
        """\
        Get all remote agents.

        :param profile_id: choose remote agent for this profile ID
        :type profile_id: ``str``

        :returns: ``list`` of remote agents for the given profile ID
        :rtype: ``list``

        """
        remote_agents = []

        # no remote agent needed for shadow sessions
        if self.is_shadow_profile(profile_id):
            return remote_agents

        agent_query_mode = self.get_agent_query_mode(profile_id).upper()
        if agent_query_mode == 'SSH' and x2gobroker.agent.has_remote_broker_agent_setup():

            profile = self.get_profile(profile_id)
            server_list = profile['host']

            while server_list:

                remote_agent_hostname = server_list[-1]
                remote_agent_hostaddr = remote_agent_hostname
                remote_agent_port = profile['sshport']
                if 'sshport={hostname}'.format(hostname=remote_agent_hostname) in profile:
                    remote_agent_port = profile["sshport={hostname}".format(hostname=remote_agent_hostname)]
                if 'host={hostname}'.format(hostname=remote_agent_hostname) in profile:
                    remote_agent_hostaddr = profile["host={hostname}".format(hostname=remote_agent_hostname)]

                remote_agents.append({
                    'hostname': remote_agent_hostname,
                    'hostaddr': remote_agent_hostaddr,
                    'port': remote_agent_port, }
                )

                server_list = server_list[0:-1]

        return remote_agents

    def is_shadow_profile(self, profile_id):
        """\
        Detect from the session profile, if it defines a desktop sharing (shadow)
        session.

        :param profile_id: ID of a valid session profile
        :type profile_id: ``str``

        :returns: ``True`` if the session profile defines a desktop sharing (shadow) session
        :rtype: ``bool``

        """
        profile = self.get_profile(profile_id)
        return profile['command'] == "SHADOW"

    def check_for_sessions(self, profile_id):
        """\
        Detect from the session profile, if we should query the remote broker
        agent for running or suspended sessions.

        :param profile_id: ID of a valid session profile
        :type profile_id: ``str``

        :returns: ``True`` if the remote broker agent should be queried for running/suspended sessions
        :rtype: ``bool``

        """
        do_check = True

        # do check, for all commands except the "SHADOW" command
        do_check = do_check and not self.is_shadow_profile(profile_id)

        return do_check

    def get_profile_for_user(self, profile_id, username, broker_frontend=None):
        """\
        Expect a profile id and perform some checks and preparations to
        make it ready for exporting to a broker client:

          - drop internal host=<hostname> and sshport=<port> keys from the
            profile, broker clients cannot handle those
          - drop keys with value "not-set"
          - replace BROKER_USER by the name of the authenticated user
          - test if autologin is possible
          - fix rootless session profile option for non-desktop sessions
          - perform an ACL check (return ``None`` if it fails)
          - query a remote agent (if configured) to check if we have
            running / suspended sessions on the remote X2Go Server

        :param profile_id: ID of a valid session profile
        :type profile_id: ``str``
        :param username: prepare session profile for this (authenticated) user
        :type username: ``str``
        :param broker_frontend: some broker frontend (e.g. UCCS) require special treatment
            by this method
        :type broker_frontend: ``str``

        :returns: session profile as a dictionary (ready for sending out to a broker client)
        :rtype: ``dict``

        """
        profile = self.get_profile(profile_id)

        acls = self.get_profile_acls(profile_id)
        if self.check_profile_acls(username, acls):

            for key in list(copy.deepcopy(profile).keys()):

                if profile[key] == "not-set":
                    del profile[key]
                    continue

                if key.startswith('host=') and broker_frontend != 'uccs':
                    del profile[key]
                if key.startswith('sshport=') and broker_frontend != 'uccs':
                    del profile[key]
                if key == 'user' and profile[key] == 'BROKER_USER':
                    profile[key] = username

            if self.get_session_autologin(profile_id):
                profile['autologin'] = True
                profile['key'] = '<will-be-exchanged-during-session-selection>'

            # make sure that desktop sessions (that we know by name) do run with rootless=false
            # and that the command string is always upper case (otherwise x2goruncommand might
            # stumble over it...)
            if profile['command'].upper() in x2gobroker.defaults.X2GO_DESKTOP_SESSIONS:
                profile['rootless'] = False
                profile['command'] = profile['command'].upper()

            remote_agent = self.get_remote_agent(profile_id)
            if self.check_for_sessions(profile_id):
                if remote_agent:
                    try:
                        success, running_sessions, suspended_sessions = x2gobroker.agent.has_sessions(username, remote_agent=remote_agent)
                        if running_sessions:
                            logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): found running sessions on host(s): {hosts}'.format(hosts=', '.join(running_sessions)))
                        if suspended_sessions:
                            logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): found running sessions on host(s): {hosts}'.format(hosts=', '.join(suspended_sessions)))
                        suspended_matching_hostnames = x2gobroker.utils.matching_hostnames(profile['host'], suspended_sessions)
                        running_matching_hostnames = x2gobroker.utils.matching_hostnames(profile['host'], running_sessions)
                        if suspended_matching_hostnames:
                            profile['status'] = 'S'
                            profile['host'] = [suspended_matching_hostnames[0]]
                        elif running_matching_hostnames:
                            profile['status'] = 'R'
                            profile['host'] = [running_matching_hostnames[0]]
                        else:
                            profile['host'] = [profile['host'][0]]

                        if 'status' in profile and profile['status']:
                            logger_broker.debug('base_broker.X2GoBroker.get_profile_for_user(): marking session profile {name} as {status}'.format(name=profile['name'], status=profile['status']))

                    except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException as e:
                        logger_broker.warning('base_broker.X2GoBroker.get_profile_for_user(): broker agent call failed. Error message is: {errmsg}'.format(errmsg=str(e)))
            else:
                profile['host'] = [profile['host'][0]]

            return profile
        else:
            return None

    def list_profiles(self, username):
        """\
        Retrieve a list of available session profiles for the
        authenticated user.

        :param username: query session profile list for this user
        :type username: ``str``

        :returns: list of profile dictionaries
        :rtype: ``dict``

        """
        list_of_profiles = {}
        for profile_id in self.get_profile_ids_for_user(username):
            profile = self.get_profile_for_user(profile_id, username)
            if profile:
                list_of_profiles.update({profile_id: profile, })

        return list_of_profiles

    def select_session(self, profile_id, username=None, pubkey=None):
        """\
        Start/resume a session by selecting a profile name offered by the
        X2Go client.

        The X2Go server that the session is launched on is selected
        automatically by the X2Go session broker.

        :param profile_id: the selected profile ID. This matches one of the dictionary
            keys offered by the ``list_profiles`` method
        :type profile_id: ``str``
        :param username: specify X2Go Server username that this operation runs for
        :type username: ``str``
        :param pubkey: The broker clients may send us a public key that we may
            temporarily install into a remote X2Go Server for non-interactive login
        :type pubkey: ``str``

        :returns: the seclected session (X2Go session ID)
        :rtype: ``str``

        """
        try:
            profile = self.get_profile(profile_id)
        except x2gobroker.x2gobroker_exceptions.X2GoBrokerProfileException:
            return { 'server': 'no-server-available', 'port': 22, }

        # if we have more than one server, pick one server randomly for X2Go Broker Agent queries
        server_list = profile['host']
        if len(server_list) == 0:
            return { 'server': 'no-server-available', 'port': profile['sshport'], }

        # if everything below fails, this will be the X2Go Server's hostname that
        # we will connect to...
        server_name = server_list[0]
        server_port = profile['sshport']

        # try to retrieve a remote broker agent
        remote_agent = self.get_remote_agent(profile_id)

        # check for already running sessions for the given user (if any is given)
        session_list = []
        if remote_agent and username:
            try:
                success, session_list = x2gobroker.agent.list_sessions(username=username, remote_agent=remote_agent)
            except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException:
                session_list = []

        session_info = None

        selected_session = {}
        busy_servers = None
        _save_server_list = None
        _save_busy_servers = None
        initial_server_list = copy.deepcopy(server_list)
        agent_query_mode_is_SSH = self.get_agent_query_mode(profile_id).upper() == 'SSH'

        while not selected_session and server_list:

            # X2Go Server uses the system's hostname, so let's replace
            # that here automatically, if we tested things via localhost...
            for h in server_list:
                if h == 'localhost':
                      server_list.remove(h)
                      server_list.append(socket.gethostname())

            matching_server_names = None
            if session_list:
                matching_server_names = x2gobroker.utils.matching_hostnames(server_list, [ si.split('|')[3] for si in session_list ])

            if remote_agent == {}:

                # we failed to contact any remote agent, so it is very likely, that all servers are down...
                server_list = []

            elif session_list and matching_server_names:

                # Obviously a remote broker agent reported an already running session
                # on the / on one the available X2Go Server host(s)

                # When resuming, always select the first session in the list,
                # there should only be one running/suspended session by design
                # of X2Go brokerage (this may change in the future)
                try:
                    running_sessions = []
                    suspended_sessions = []

                    for session_info in session_list:
                        if session_info.split('|')[3] in matching_server_names:
                            if session_info.split('|')[4] == 'R':
                                running_sessions.append(session_info)
                            if session_info.split('|')[4] == 'S':
                                suspended_sessions.append(session_info)

                    if suspended_sessions or running_sessions:
                        # we prefer suspended sessions over resuming sessions if we find sessions with both
                        # states of activity
                        if suspended_sessions:
                            session_info = suspended_sessions[0]
                        elif running_sessions:
                            session_info = running_sessions[0]
                            x2gobroker.agent.suspend_session(username=username, session_name=session_info.split('|')[1], remote_agent=remote_agent)
                            # this is the turn-around in x2gocleansessions, so waiting as along as the daemon
                            # that will suspend the session
                            time.sleep(2)
                            session_info = session_info.replace('|R|', '|S|')

                        # only use the server's official hostname (as set on the server)
                        # if we have been provided with a physical server address.
                        # If no physical server address has been provided, we have to use
                        # the host address as found in server_list (and hope we can connect
                        # to that address.
                        _session_server_name = session_info.split('|')[3]
                        if 'host={server_name}'.format(server_name=_session_server_name) in profile:
                            server_name = _session_server_name
                        elif _session_server_name in server_list:
                            server_name = _session_server_name
                        elif x2gobroker.utils.matching_hostnames(server_list, [_session_server_name]):
                            for _server_name in server_list:
                                if _server_name.startswith(_session_server_name):
                                    server_name = _server_name
                                    break
                        else:
                            logger_broker.error('base_broker.X2GoBroker.select_session(): configuration error. Hostnames in session profile and actual server names do not match, we won\'t be able to resume/take-over a session this time')
                            # choosing a random server from the server list, to end up anywhere at least...
                            server_name = random.choice(server_list)

                except IndexError:
                    # FIXME: if we get here, we have to deal with a broken session info
                    # entry in the X2Go session database. -> AWFUL!!!
                    pass

            # detect best X2Go server for this user if load balancing is configured
            elif remote_agent and len(server_list) >= 2 and username:

                # No running / suspended session was found on any of the available
                # X2Go Servers. Thus, we will try to detect the best server for this
                # load balanced X2Go Server farm.

                # query remote agent on how busy our servers are... (if a selected server is down
                # and we come through here again, don't query business state again, use the remembered
                # status)
                if busy_servers is None:
                    try:
                        if agent_query_mode_is_SSH and remote_agent['load_factors']:
                            success, busy_servers = x2gobroker.agent.get_servers(username=username, remote_agent=remote_agent)
                        else:
                            success, busy_servers = x2gobroker.agent.find_busy_servers(username=username, remote_agent=remote_agent)
                    except x2gobroker.x2gobroker_exceptions.X2GoBrokerAgentException:
                        pass

                if busy_servers is not None:

                    # if we do not get here, we failed to query a valid agent...

                    # when detecting the server load we have to support handling of differing subdomains (config
                    # file vs. server load returned by x2gobroker agent). Best approach: all members of a multi-node
                    # server farm either
                    #
                    #  (a) do not have a subdomain in their hostname or
                    #  (b) have an identical subdomain in their hostnames

                    # Example:
                    #
                    #    ts01, ts02 - hostnames as returned by agent
                    #    ts01.intern, ts02.intern - hostnames configured in session profile option ,,host''
                    #    -> this will result in the subdomain .intern being stripped off from the hostnames before
                    #       detecting the best server for this user

                    ### NORMALIZE (=reduce to hostname only) X2Go server names (as found in config) if possible
                    server_list_normalized, subdomains_config = x2gobroker.utils.normalize_hostnames(server_list)

                    ### NORMALIZE X2Go server names (as returned by broker agent)--only if the hostnames in
                    # the config share the same subdomain
                    if len(subdomains_config) == 1:

                        busy_servers_normalized, subdomains_agent = x2gobroker.utils.normalize_hostnames(busy_servers)
                        if len(subdomains_agent) <= 1:
                            # all X2Go servers in the multi-node server farm are in the same DNS subdomain
                            # we can operate on hostname-only hostnames
                            _save_server_list = copy.deepcopy(server_list)
                            _save_busy_servers = copy.deepcopy(busy_servers)
                            server_list = server_list_normalized
                            busy_servers = busy_servers_normalized

                    # the list of busy_servers only shows servers with sessions, but not those servers that are entirely idle...

                    for server in server_list:
                        if server not in list(busy_servers.keys()):
                            busy_servers[server] = 0

                    # we will only contact servers that are (still) in server_list
                    for busy_server in list(busy_servers.keys()):
                        if busy_server not in server_list:
                            del busy_servers[busy_server]

                    # dynamic load-balancing via load checker service
                    if agent_query_mode_is_SSH and remote_agent['load_factors']:

                        load_factors = remote_agent['load_factors']
                        busy_servers_temp = copy.deepcopy(busy_servers)

                        for busy_server in list(busy_servers_temp.keys()):
                            if busy_server in list(load_factors.keys()) and type(load_factors[busy_server]) is not int:
                                # if a host cannot report its load, let's ignore it...
                                del busy_servers_temp[busy_server]

                            elif busy_server in list(load_factors.keys()) and  ( type(load_factors[busy_server]) is int or busy_servers[busy_server] == 0):

                                # when using the load checker service, then busy_servers contains the number of sessions per host

                                # do the load-factor / numSessions calculation here... (avoid divison-by-zero by adding +1 to
                                # the number of sessions here)
                                busy_servers_temp[busy_server] = 1.0 / (load_factors[busy_server] / ( busy_servers[busy_server] +1))

                            else:
                                # ignore the load checker, results are garbage...
                                busy_servers_temp = None
                                break

                        if busy_servers_temp is not None:
                            busy_servers = copy.deepcopy(busy_servers_temp)

                    busy_server_list = [ (load, server) for server, load in list(busy_servers.items()) ]
                    busy_server_list.sort()

                    logger_broker.debug('base_broker.X2GoBroker.select_session(): load balancer analysis: {server_load}'.format(server_load=busy_server_list))

                    server_name = busy_server_list[0][1]

                    # this makes sure we allow back-translation of hostname to host address
                    # when the format "<hostname> (<ip-address>)" ist used in the hosts field...
                    if len(subdomains_config) == 1:
                        server_name += '.{domain}'.format(domain=subdomains_config[0])

                    if _save_server_list:
                        server_list = copy.deepcopy(_save_server_list)
                        _save_server_list = None
                    if _save_busy_servers:
                        busy_servers = copy.deepcopy(_save_busy_servers)
                        _save_busy_servers = None

                else:
                    logger_broker.warning('base_broker.X2GoBroker.select_session(): no broker agent could be contacted, this does not look good. We tried these agent hosts: {agent_hosts}'.format(agent_hosts=initial_server_list))

            # detect best X2Go server for this user if load balancing is configured
            elif len(server_list) >= 2:

                if self.is_shadow_profile(profile_id):
                    # we will ignore load-balancing for desktop sharing profiles
                    server_list = [server_list[0]]
                    server_name = server_list[0]
                else:
                    # no remote broker agent or no username? Let's play roulette then...
                    server_name = random.choice(server_list)

            ###
            ### by now we should know the proper host to connect to...
            ###

            server_addr = server_name
            # if we have an explicit TCP/IP port server_name, let's use that instead...
            try:
                server_port = profile['sshport={hostname}'.format(hostname=server_name)]
                logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server port: {port}'.format(port=server_port))
            except KeyError:
                pass

            # if we have an explicit TCP/IP address for server_name, let's use that instead...
            try:
                server_addr = profile['host={hostname}'.format(hostname=server_name)]
                logger_broker.debug('base_broker.X2GoBroker.select_session(): use physical server address: {address}'.format(address=server_addr))
            except KeyError:
                pass

            if server_list:
                if not self.get_portscan_x2goservers(profile_id) or x2gobroker.utils.portscan(addr=server_name, port=server_port) or x2gobroker.utils.portscan(addr=server_addr, port=server_port):
                    selected_session = {
                        'server': server_addr,
                        'port': server_port,
                    }
                else:
                    server_list.remove(server_name)

                    # pick remaining server from server list (if any)
                    if server_list:
                        logger_broker.warning('base_broker.X2GoBroker.select_session(): failed to contact host \'{down_server}\', trying next server \'{next_server}\''.format(down_server=server_name, next_server=server_list[0]))
                        server_name = server_list[0]
                    else:
                        logger_broker.error('base_broker.X2GoBroker.select_session(): no X2Go Server could be contacted, session startup will fail, tried these hosts: {server_list}'.format(server_list=initial_server_list))

                    # If we arrive here and session_list carries an entry for this user, then the session DB probably still
                    # carries a zombie session entry (that will disappear when the down X2Go Server comes up again (cleanup
                    # via x2gocleansessions).
                    #
                    # Thus, let's ignore this session and check if there is another appropriate session in session_list
                    if session_info is not None:
                        session_list.remove(session_info)
                        session_info = None

        if not selected_session and not server_list:
            if len(initial_server_list) > 1:
                selected_session = {
                    'server': 'no-X2Go-Server-available',
                    'port': server_port,
                }
            else:
                # hand-over the original hostname for non-load-balanced session profiles
                failed_server_port = server_port
                failed_server_name = initial_server_list[0]
                try: failed_server_port = profile['port={hostname}'.format(hostname=failed_server_name)]
                except KeyError: pass
                try: failed_server_name = profile['host={hostname}'.format(hostname=failed_server_name)]
                except KeyError: pass
                selected_session = {
                    'server': failed_server_name,
                    'port': failed_server_port,
                }

        # are we resuming a running/suspended session?
        if session_info is not None:
            selected_session['session_info'] = session_info

        # define a remote SSH proxy agent if an SSH proxy host is used with this session profile
        if 'sshproxyhost' in profile and profile['sshproxyhost']:
            remote_sshproxy_agent = {
                'hostname': profile['sshproxyhost'],
                'hostaddr': profile['sshproxyhost'],
                'port': "22"
            }
            if 'sshproxyport' in profile and profile['sshproxyport']:
                remote_sshproxy_agent['port'] = profile['sshproxyport']
        else:
            remote_sshproxy_agent = None

        # session autologin feature
        if remote_agent and self.get_session_autologin(profile_id) and username:

            # let's use the chosen server_name if remote_agent is reachable via SSH
            if type(remote_agent) is dict:
                remote_agent = {
                    'hostname': server_name,
                    'hostaddr': server_addr,
                    'port': selected_session['port'],
                }

            if not pubkey:
                # if the broker client has not provided a public SSH key, we will generate one
                # this is the OLD style of the auto login feature

                # FIXME: we somehow have to find out about the username of the person at the broker client-side...
                # using the username used for server login for now...
                pubkey, privkey = x2gobroker.agent.genkeypair(local_username=username, client_address=self.get_client_address())

                if remote_sshproxy_agent is not None:
                    x2gobroker.agent.add_authorized_key(username=username,
                                                        pubkey_hash=pubkey,
                                                        authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id),
                                                        remote_agent=remote_sshproxy_agent,
                    ),
                x2gobroker.agent.add_authorized_key(username=username,
                                                    pubkey_hash=pubkey,
                                                    authorized_keys_file=self.get_authorized_keys_file(profile_id),
                                                    remote_agent=remote_agent,
                ),
                selected_session.update({
                    'authentication_privkey': privkey,
                })
                if remote_sshproxy_agent is not None:
                    x2gobroker.agent.delete_authorized_key(username=username,
                                                           pubkey_hash=pubkey,
                                                           authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id),
                                                           remote_agent=remote_sshproxy_agent,
                                                           delay_deletion=20,
                    )
                x2gobroker.agent.delete_authorized_key(username=username,
                                                       pubkey_hash=pubkey,
                                                       authorized_keys_file=self.get_authorized_keys_file(profile_id),
                                                       remote_agent=remote_agent,
                                                       delay_deletion=20,
                )
            else:
                logger_broker.info('base_broker.X2GoBroker.select_session(): accepting public SSH key from broker client')
                if remote_sshproxy_agent is not None:
                    x2gobroker.agent.add_authorized_key(username=username,
                                                        pubkey_hash=pubkey,
                                                        authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id),
                                                        remote_agent=remote_sshproxy_agent,
                    ),
                x2gobroker.agent.add_authorized_key(username=username,
                                                    pubkey_hash=pubkey,
                                                    authorized_keys_file=self.get_authorized_keys_file(profile_id),
                                                    remote_agent=remote_agent,
                ),
                selected_session.update({
                    'authentication_pubkey': 'ACCEPTED',
                })
                if remote_sshproxy_agent is not None:
                    x2gobroker.agent.delete_authorized_key(username=username,
                                                           pubkey_hash=pubkey,
                                                           authorized_keys_file=self.get_sshproxy_authorized_keys_file(profile_id),
                                                           remote_agent=remote_sshproxy_agent,
                                                           delay_deletion=20,
                    )
                x2gobroker.agent.delete_authorized_key(username=username,
                                                       pubkey_hash=pubkey,
                                                       authorized_keys_file=self.get_authorized_keys_file(profile_id),
                                                       remote_agent=remote_agent,
                                                       delay_deletion=20,
                )

        return selected_session

    def change_password(self, new='', old=''):
        """\
        Modify the authenticated user's password on the X2Go
        infrastructure (normally, one user in one X2Go site setup should
        have the same password on all machines).

        This function is a dummy function and needs to be overridden in
        specific broker backend implementations

        :param new: the new password that is to be set
        :type new: ``str``
        :param old: the currently set password
        :type old: ``str``

        :returns: whether the password change has been successful
        :rtype: ``bool``

        """
        return False

    def run_optional_script(self, script_type, username, password, task, profile_id, ip, cookie, authed=None, server=None):
        """\
        Run all optional scripts of type script_type.  Called with 3
        different script types:

            - pre_auth_scripts - before authentication happens
            - post_auth_scripts - after authentication but before
              anything else occurs
            - select_session_scripts - after load balancing before a
              specific server is sent to the client

        These scripts allow for both addional actions to be performed as
        well as the mangling of any relevant fields.

        :param script_type: name of the script type to be executed (``pre_auth_scripts``, ``post_auth_scripts``, ``select_session_scripts``)
        :type script_type: ``str``
        :param username: name of the X2Go session user a script will run for
        :type username: ``str``
        :param password: password for the X2Go session
        :type password: ``str``
        :param task: the broker task that currently being processed
        :type task: ``str``
        :param profile_id: the session profile ID that is being operated upon
        :type profile_id: ``str``
        :param ip: the client machine's IP address
        :type ip: ``str``
        :param cookie: the currently valid authentication cookie
        :type cookie: ``str``
        :param authed: authentication status (already authenticated or not)
        :type authed: ``bool``
        :param server: hostname or IP address of the X2Go server being operated upon
        :type server: ``str``

        :returns: Pass-through of the return value returned by the to-be-run optional script (i.e., success or failure)
        :rtype: ``bool``

        """

        global_config = self.get_global_config()
        if len(global_config[script_type]) != 0:
            for script in global_config[script_type]:
                try:
                    if script:
                        my_script=None
                        namespace = {}
                        exec("import x2gobroker.optional_scripts.{script}_script\nmy_script = x2gobroker.optional_scripts.{script}_script.X2GoBrokerOptionalScript()".format(script=script), namespace)
                        my_script = namespace['my_script']
                        logger_broker.debug ('Calling {script_type} {script} with username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, ip: {ip}, cookie: {cookie}, authed: {authed}, server: {server}'.format(script_type=script_type,script=script,username=username, password='XXXXX', task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server))
                        username, password, task, profile_id, ip, cookie, authed, server = my_script.run_me(username=username, password=password, task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server)
                        logger_broker.debug ('Finished {script_type} {script} with username: {username}, password: {password}, task: {task}, profile_id: {profile_id}, ip: {ip}, cookie: {cookie}, authed: {authed}, server: {server}'.format(script_type=script_type,script=script,username=username, password='XXXXX', task=task, profile_id=profile_id, ip=ip, cookie=cookie, authed=authed, server=server))
                except ImportError:
                    logger_error.error('No such optional script \'{script}\''.format(script=script))
        return username, password, task, profile_id, ip, cookie, authed, server
