/** BEGIN COPYRIGHT BLOCK
 * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
 * Copyright (C) 2005 Red Hat, Inc.
 * All rights reserved.
 *
 * License: GPL (version 3 or any later version).
 * See LICENSE for details.
 * END COPYRIGHT BLOCK **/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*
 * ptconn.c - LDAP connection-related code for Pass Through Authentication
 *
 */

#include "passthru.h"

/*
 * function prototypes
 */
static int dn_is_underneath_suffix(PassThruSuffix *suffix, const char *normdn, int dnlen);
static void close_and_dispose_connection(PassThruConnection *conn);
static void check_for_stale_connections(PassThruServer *srvr);


/*
 * Most of the complicated connection-related code lives in this file.  Some
 * general notes about how we manage our connections to "remote" LDAP servers:
 *
 * 1) Each server we have a relationship with is managed independently.
 *
 * 2) We may simultaneously issue multiple bind requests on a single LDAP
 *    connection.  Each server has a "maxconcurrency" configuration
 *    parameter associated with it that caps the number of outstanding
 *    binds per connection.  For each connection we maintain a "usecount"
 *    which is used to track the number of threads using the connection.
 *
 * 3) We may open more than one connection to a server.  This is only done
 *    when "maxconcurrency" is exceeded for all the connections we already
 *    have open.  Each server has a "maxconnections" configuration
 *    parameter associated with it that caps the number of connections.
 *    We also maintain a "connlist_count" for each server so we know when
 *    we have reached the maximum number of open connections allowed.
 *
 * 4) If no connection is available to service a request (and we have
 *    reached the limit of how many we are supposed to open), threads
 *    go to sleep on a condition variable and one is woken up each time
 *    a connection's "usecount" is decremented.
 *
 * 5) If we see an LDAP_CONNECT_ERROR or LDAP_SERVER_DOWN error on a
 *    session handle, we mark its status as PASSTHRU_CONNSTATUS_DOWN and
 *    close it as soon as all threads using it release it.  Connections
 *    marked as "down" are not counted against the "maxconnections" limit.
 *
 * 6) We close and reopen connections that have been open for more than
 *    the server's configured connection lifetime.  This is done to ensure
 *    that we reconnect to a primary server after failover occurs.  If no
 *    lifetime is configured or it is set to 0, we never close and reopen
 *    connections.
 */


/*
 * Given a normalized target dn, see if it we should "pass through"
 * authentication to another LDAP server.  The answer is "yes" if the
 * target dn resides under one of the suffixes we have that is associated
 * with an LDAP server we know about.
 *
 * This function assumes that normdn is normalized and the the suffixes in the
 * cfg structure have also been normalized.
 *
 * Returns an LDAP error code, typically:
 *    LDAP_SUCCESS        should pass though; *srvrp set.
 *    LDAP_NO_SUCH_OBJECT    let this server handle the bind.
 */
int
passthru_dn2server(PassThruConfig *cfg, const char *normdn, PassThruServer **srvrp)
{
    PassThruServer *ptsrvr;
    PassThruSuffix *ptsuffix;
    int dnlen;

    PASSTHRU_ASSERT(cfg != NULL);
    PASSTHRU_ASSERT(normdn != NULL);
    PASSTHRU_ASSERT(srvrp != NULL);

    dnlen = strlen(normdn);

    for (ptsrvr = cfg->ptconfig_serverlist; ptsrvr != NULL;
         ptsrvr = ptsrvr->ptsrvr_next) {
        for (ptsuffix = ptsrvr->ptsrvr_suffixes; ptsuffix != NULL;
             ptsuffix = ptsuffix->ptsuffix_next) {
            if (dn_is_underneath_suffix(ptsuffix, normdn, dnlen)) {
                *srvrp = ptsrvr;
                return (LDAP_SUCCESS); /* got it */
            }
        }
    }

    *srvrp = NULL;
    return (LDAP_NO_SUCH_OBJECT); /* no match */
}


/*
 * Get an LDAP session handle for communicating with srvr.
 *
 * Returns an LDAP eror code, typically:
 *    LDAP_SUCCESS
 *    other
 */
int
passthru_get_connection(PassThruServer *srvr, LDAP **ldp)
{
    int rc = LDAP_SUCCESS; /* optimistic */
    PassThruConnection *conn, *connprev;
    LDAP *ld;

    PASSTHRU_ASSERT(srvr != NULL);
    PASSTHRU_ASSERT(ldp != NULL);

    check_for_stale_connections(srvr);

    slapi_lock_mutex(srvr->ptsrvr_connlist_mutex);

    slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                  "=> passthru_get_connection server %s:%d conns: %d maxconns: %d\n",
                  srvr->ptsrvr_hostname, srvr->ptsrvr_port, srvr->ptsrvr_connlist_count,
                  srvr->ptsrvr_maxconnections);

    for (;;) {
        /*
         * look for an available, already open connection
         */
        connprev = NULL;
        for (conn = srvr->ptsrvr_connlist; conn != NULL;
             conn = conn->ptconn_next) {
            if (conn->ptconn_status == PASSTHRU_CONNSTATUS_OK && conn->ptconn_usecount < srvr->ptsrvr_maxconcurrency) {
#ifdef PASSTHRU_VERBOSE_LOGGING
                slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                              "<= passthru_get_connection server found "
                              "conn 0x%x to use)\n",
                              conn->ptconn_ld);
#endif
                goto unlock_and_return; /* found one */
            }
            connprev = conn;
        }

        if (srvr->ptsrvr_connlist_count < srvr->ptsrvr_maxconnections) {
            /*
             * we have not exceeded the maximum number of connections allowed,
             * so we initialize a new one and add it to the end of our list.
             */
            if ((ld = slapi_ldap_init(srvr->ptsrvr_hostname,
                                      srvr->ptsrvr_port, srvr->ptsrvr_secure, 1)) == NULL) {
#ifdef PASSTHRU_VERBOSE_LOGGING
                slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                              "<= passthru_get_connection slapi_ldap_init failed\n");
#endif
                rc = LDAP_LOCAL_ERROR;
                goto unlock_and_return;
            }

            if (srvr->ptsrvr_secure == SLAPI_LDAP_INIT_FLAG_startTLS) {
                if (srvr->ptsrvr_ldapversion == LDAP_VERSION3 ) {
                    rc = ldap_start_tls_s(ld, NULL, NULL);
                    if (LDAP_SUCCESS != rc) {
                        if (errno != 0) {
                            /* Log the system errno */
                            slapi_log_err(SLAPI_LOG_ERR, PASSTHRU_PLUGIN_SUBSYSTEM, "passthru_get_connection - "
                                          "Error: could not send startTLS request: error %d (%s) errno %d (%s)\n",
                                          rc, ldap_err2string(rc), errno,
                                          slapd_system_strerror(errno));
                        } else {
                            /* Only LDAP error, no system error */
                            slapi_log_err(SLAPI_LOG_ERR, PASSTHRU_PLUGIN_SUBSYSTEM, "passthru_get_connection - "
                                          "Error: could not send startTLS request: error %d (%s)\n",
                                          rc, ldap_err2string(rc));
                        }
                        goto unlock_and_return;
                    }
                } else {
                    /* We only support StartTLS on LDAPv3  */
                    slapi_log_err(SLAPI_LOG_ERR, PASSTHRU_PLUGIN_SUBSYSTEM, "passthru_get_connection - "
                                  "Error: configured to use StartTLS but ldap version (v%d) is not supported "
                                  "(version 3 is required).  Aborting connection...\n",srvr->ptsrvr_ldapversion);
                    rc = LDAP_UNWILLING_TO_PERFORM;
                    goto unlock_and_return;
                }
            }

            /*
             * set protocol version to correct value for this server
             */
            if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION,
                                &srvr->ptsrvr_ldapversion) != 0) {
                slapi_ldap_unbind(ld);
            }

            conn = (PassThruConnection *)slapi_ch_malloc(
                sizeof(PassThruConnection));
            conn->ptconn_ld = ld;
            conn->ptconn_status = PASSTHRU_CONNSTATUS_OK;
            time(&conn->ptconn_opentime);
            conn->ptconn_ldapversion = srvr->ptsrvr_ldapversion;
            conn->ptconn_usecount = 0;
            conn->ptconn_next = NULL;
            conn->ptconn_prev = connprev;
            if (connprev == NULL) {
                srvr->ptsrvr_connlist = conn;
                conn->ptconn_prev = NULL;
            } else {
                connprev->ptconn_next = conn;
                conn->ptconn_prev = connprev;
            }

            ++srvr->ptsrvr_connlist_count;

#ifdef PASSTHRU_VERBOSE_LOGGING
            slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                          "<= passthru_get_connection added new conn 0x%x, "
                          "conn count now %d\n",
                          ld, srvr->ptsrvr_connlist_count);
#endif
            goto unlock_and_return; /* got a new one */
        }

#ifdef PASSTHRU_VERBOSE_LOGGING
        slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                      "... passthru_get_connection waiting for conn to free up\n");
#endif
        slapi_wait_condvar_pt(srvr->ptsrvr_connlist_cv, srvr->ptsrvr_connlist_mutex, NULL);

#ifdef PASSTHRU_VERBOSE_LOGGING
        slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                      "... passthru_get_connection awake again\n");
#endif
    }

unlock_and_return:
    if (conn != NULL) {
        ++conn->ptconn_usecount;
        *ldp = conn->ptconn_ld;
        slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                      "<= passthru_get_connection ld=0x%p (concurrency now %d)\n",
                      *ldp, conn->ptconn_usecount);
    } else {
        slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                      "<= passthru_get_connection error %d\n", rc);
    }

    slapi_unlock_mutex(srvr->ptsrvr_connlist_mutex);
    return (rc);
}


/*
 * Mark the connection ld is associated with as free to be used again.
 * If dispose is non-zero, we mark the connection as "bad" and dispose
 *    of it and its ld once the use count becomes zero.
 */
void
passthru_release_connection(PassThruServer *srvr, LDAP *ld, int dispose)
{
    PassThruConnection *conn, *connprev;

    PASSTHRU_ASSERT(srvr != NULL);
    PASSTHRU_ASSERT(ld != NULL);

#ifdef PASSTHRU_VERBOSE_LOGGING
    slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                  "=> passthru_release_connection ld=0x%x%s\n", ld,
                  dispose ? " (disposing)" : "");
#endif

    slapi_lock_mutex(srvr->ptsrvr_connlist_mutex);

    /*
     * find the connection structure this ld is part of
     */
    connprev = NULL;
    for (conn = srvr->ptsrvr_connlist; conn != NULL;
         conn = conn->ptconn_next) {
        if (ld == conn->ptconn_ld) {
            break;
        }
        connprev = conn;
    }

    if (conn == NULL) { /* ld not found -- unexpected */
        slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                      "=> passthru_release_connection ld=0x%p not found\n", ld);
    } else {
        PASSTHRU_ASSERT(conn->ptconn_usecount > 0);
        --conn->ptconn_usecount;
        if (dispose) {
            conn->ptconn_status = PASSTHRU_CONNSTATUS_DOWN;
        }

        if (conn->ptconn_status != PASSTHRU_CONNSTATUS_OK && conn->ptconn_usecount == 0) {
            /*
         * remove from server's connection list
         */
            if (connprev == NULL) {
                srvr->ptsrvr_connlist = conn->ptconn_next;
            } else {
                connprev->ptconn_next = conn->ptconn_next;
            }
            --srvr->ptsrvr_connlist_count;

            /*
         * close connection and free memory
         */
            close_and_dispose_connection(conn);
        }
    }

    /*
     * wake up a thread that is waiting for a connection (there may not be
     * any but the slapi_notify_condvar() call should be cheap in any event).
     */
    slapi_notify_condvar(srvr->ptsrvr_connlist_cv, 0);

    /*
     * unlock and return
     */
    slapi_unlock_mutex(srvr->ptsrvr_connlist_mutex);
}


/*
 * close all open connections in preparation for server shutdown, etc.
 */
void
passthru_close_all_connections(PassThruConfig *cfg)
{
    PassThruServer *srvr;
    PassThruConnection *conn, *nextconn;

    PASSTHRU_ASSERT(cfg != NULL);

    for (srvr = cfg->ptconfig_serverlist; srvr != NULL;
         srvr = srvr->ptsrvr_next) {
        for (conn = srvr->ptsrvr_connlist; conn != NULL; conn = nextconn) {
            nextconn = conn->ptconn_next;
            close_and_dispose_connection(conn);
        }
    }
}


/*
 * return non-zero value if normdn falls underneath a suffix
 */
static int
dn_is_underneath_suffix(PassThruSuffix *suffix, const char *normdn, int dnlen)
{
    PASSTHRU_ASSERT(suffix != NULL);
    PASSTHRU_ASSERT(normdn != NULL);
    PASSTHRU_ASSERT(dnlen >= 0);

    return (suffix->ptsuffix_len <= dnlen &&
            slapi_UTF8CASECMP(suffix->ptsuffix_normsuffix,
                              (char *)normdn + (dnlen - suffix->ptsuffix_len)) == 0);
}


/*
 * Unbind from server and dispose of a connection.
 */
static void
close_and_dispose_connection(PassThruConnection *conn)
{
    PASSTHRU_ASSERT(conn != NULL);
    PASSTHRU_ASSERT(conn->ptconn_ld != NULL);

    slapi_ldap_unbind(conn->ptconn_ld);
    conn->ptconn_ld = NULL;
    slapi_ch_free((void **)&conn);
}


/*
 * Close (or mark to be closed) any connections for this srvr that have
 * exceeded the maximum connection lifetime.
 */
static void
check_for_stale_connections(PassThruServer *srvr)
{
    PassThruConnection *conn, *prevconn, *nextconn;
    time_t curtime;

    PASSTHRU_ASSERT(srvr != NULL);

#ifdef PASSTHRU_VERBOSE_LOGGING
    slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                  "check_for_stale_connections: server %s (lifetime %d secs)\n",
                  srvr->ptsrvr_url, srvr->ptsrvr_connlifetime);
#endif


    if (srvr->ptsrvr_connlifetime <= 0) {
        return;
    }

    time(&curtime);

    slapi_lock_mutex(srvr->ptsrvr_connlist_mutex);

    prevconn = NULL;
    for (conn = srvr->ptsrvr_connlist; conn != NULL; conn = nextconn) {
        nextconn = conn->ptconn_next;

        if (curtime - conn->ptconn_opentime > srvr->ptsrvr_connlifetime) {
            if (conn->ptconn_usecount == 0) {
/*
         * connection is idle and stale -- remove from server's list
         */
#ifdef PASSTHRU_VERBOSE_LOGGING
                slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                              "check_for_stale_connections: discarding idle, "
                              "stale connection 0x%x\n",
                              conn->ptconn_ld);
#endif
                if (prevconn == NULL) {
                    srvr->ptsrvr_connlist = nextconn;
                } else {
                    prevconn->ptconn_next = nextconn;
                }
                --srvr->ptsrvr_connlist_count;
                close_and_dispose_connection(conn);
            } else {
/*
         * connection is stale but in use -- mark to be disposed later
         */
#ifdef PASSTHRU_VERBOSE_LOGGING
                slapi_log_err(SLAPI_LOG_PLUGIN, PASSTHRU_PLUGIN_SUBSYSTEM,
                              "check_for_stale_connections: marking connection 0x%x "
                              "stale (use count %d)\n",
                              conn->ptconn_ld,
                              conn->ptconn_usecount);
#endif
                conn->ptconn_status = PASSTHRU_CONNSTATUS_STALE;
                prevconn = conn;
            }
        } else {
            prevconn = conn;
        }
    }

    slapi_unlock_mutex(srvr->ptsrvr_connlist_mutex);
}
