/** 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

/*
 * referrals.c - LDAP referral-related routines.
 *    a) manage in-memory copiedfrom and copyingfrom referrals.
 *    b) function to adjust smart referrals to match operation parameters.
 */
#include <stdio.h>
#include "slap.h"

/* Forward Decls */
static void adjust_referral_basedn(char **urlp, const Slapi_DN *refcontainerdn, char *opdn_norm, int isreference);
static int dn_is_below(const char *dn_norm, const char *ancestor_norm);
static Ref_Array *g_get_global_referrals(void);
static void ref_free(Ref **goner);
static Ref_Array global_referrals;

#define SLAPD_DEFAULT_REFARRAY_SIZE 10

/*
 *  Function: g_get_global_referrals
 *
 *  Returns: nothing
 *
 *  Description: Fills the global_referrals referral array.
 *               The size is set to "size". The mutex is created.
 *               The array is calloc'd.
 */
static Ref_Array *
g_get_global_referrals(void)
{
    if (global_referrals.ra_rwlock == NULL) {
        /* Make a new lock */
        global_referrals.ra_rwlock = slapi_new_rwlock();

        if (global_referrals.ra_rwlock == NULL) {
            slapi_log_err(SLAPI_LOG_ERR,
                          "g_get_global_referrals", "New lock creation failed\n");
            exit(-1);
        }

        /* Initialize all the fields. */
        global_referrals.ra_size = SLAPD_DEFAULT_REFARRAY_SIZE;
        global_referrals.ra_nextindex = 0;
        global_referrals.ra_readcount = 0;
        global_referrals.ra_refs = (Ref **)slapi_ch_calloc(SLAPD_DEFAULT_REFARRAY_SIZE, sizeof(Ref *));
    }
    return (&global_referrals);
}


/*
 * Function: ref_free
 *
 * Returns: nothing
 *
 * Description: frees up "goner"
 *
 * Author: RJP
 *
 */
static void
ref_free(Ref **goner)
{
    slapi_ch_free((void **)&((*goner)->ref_dn));
    ber_bvfree((*goner)->ref_referral);
    slapi_ch_free((void **)goner);
}


/*
 * Function: referrals_free
 *
 * Returns: nothing
 *
 * Description: frees up everything.
 *
 * Author: RJP
 *
 */
void
referrals_free(void)
{
    int walker;
    Ref_Array *grefs = NULL;

    grefs = g_get_global_referrals();

    GR_LOCK_WRITE();

    /* Walk down the array, deleting each referral */
    for (walker = 0; walker < grefs->ra_nextindex; walker++) {
        ref_free(&grefs->ra_refs[walker]);
    }

    slapi_ch_free((void **)&grefs->ra_refs);

    GR_UNLOCK_WRITE();

    slapi_destroy_rwlock(grefs->ra_rwlock);
}

/*
 * ref_adjust() -- adjust referrals based on operation-specific data.
 * The general idea is for us (the server) to be smart so LDAP clients
 * can be as dumb as possible.
 *
 * XXXmcs: We always duplicate the referrals even if no adjustments need to
 * be made.  It would be more efficient but more complicated to avoid making
 * a copy if we don't need to change anything.  If that were done, the
 * interface to this function would need to change since the caller would
 * need to know whether the returned memory needed to be freed or not.
 *
 * Parameters:
 *     pb is the pblock for the operation we are working on.
 *     urls is a list of referrals.
 *     refsdn is the Slapi_DN of the entry where the referral is stored.
 *     is_reference is true if a searchResultReference is being returned.
 *
 * Returns:
 *    a (possibly modified) copy of the urls.  It should be freed by
 *         calling ber_bvecfree().
 *
 *
 * First we strip off any labels from the referral URLs.  For example, a
 * referral like this:
 *
 *    ldap://directory.airius.com/ou=people,o=airius.com  Ref to people tree
 *
 * is changed to:
 *
 *    ldap://directory.airius.com/ou=people,o=airius.com
 *
 * Next, if the referral includes a baseDN we potentially modify it or strip
 * it off entirely. If the referral doesn't include a baseDN and we're
 * processing a continuation reference, we add a baseDN.
 *
 * Finally, if we are processing a continuation reference that resulted from
 * a one-level search, we append "??base" to the referral so the client will
 * use the correct scope when chasing the reference.
 */
struct berval **
ref_adjust(Slapi_PBlock *pb, struct berval **urls, const Slapi_DN *refsdn, int is_reference)
{
    int i, len, scope = 0;
    Slapi_DN *sdn = NULL;
    char *p, *opdn_norm;
    struct berval **urlscopy;
    Operation *op;

    if (NULL == urls || NULL == urls[0]) {
        return (NULL);
    }

    PR_ASSERT(pb != NULL);

    /*
     * grab the operation target DN and operation structure.
     * if the operation is a search, get the scope as well.
     */
    if (slapi_pblock_get(pb, SLAPI_TARGET_SDN, &sdn) != 0 || sdn == NULL ||
        slapi_pblock_get(pb, SLAPI_OPERATION, &op) != 0 || op == NULL ||
        (operation_get_type(op) == SLAPI_OPERATION_SEARCH && slapi_pblock_get(pb, SLAPI_SEARCH_SCOPE, &scope) != 0)) {
        slapi_log_err(SLAPI_LOG_ERR, "ref_adjust",
                      "Referrals suppressed (could not get target DN, operation, "
                      "or scope from pblock)\n");
        return (NULL);
    }

    /*
     * normalize the DNs we plan to compare with others later.
     */
    opdn_norm = slapi_ch_strdup(slapi_sdn_get_dn(sdn));


    /*
     * count referrals and duplicate the array
     */
    for (i = 0; urls[i] != NULL; ++i) {
        ;
    }
    urlscopy = (struct berval **)slapi_ch_malloc((i + 1) *
                                                 sizeof(struct berval *));

    for (i = 0; urls[i] != NULL; ++i) {
        /*
         * duplicate the URL, stripping off the label if there is one and
         * leaving extra room for "??base" in case we need to append that.
         */
        urlscopy[i] = (struct berval *)slapi_ch_malloc(
            sizeof(struct berval));
        if ((p = strchr(urls[i]->bv_val, ' ')) == NULL) {
            len = strlen(urls[i]->bv_val);
        } else {
            len = p - urls[i]->bv_val;
        }
        urlscopy[i]->bv_val = (char *)slapi_ch_malloc(len + 7);
        memcpy(urlscopy[i]->bv_val, urls[i]->bv_val, len);
        urlscopy[i]->bv_val[len] = '\0';

        /*
         * adjust the baseDN as needed and set the length
         */
        adjust_referral_basedn(&urlscopy[i]->bv_val, refsdn,
                               opdn_norm, is_reference);
        urlscopy[i]->bv_len = strlen(urlscopy[i]->bv_val);

        /*
         * if we are dealing with a continuation reference that resulted
         * from a one-level search, add a scope of base to the URL.
         */
        if (is_reference && operation_get_type(op) == SLAPI_OPERATION_SEARCH &&
            scope == LDAP_SCOPE_ONELEVEL) {
            strcat(urlscopy[i]->bv_val, "??base");
            urlscopy[i]->bv_len += 6;
        }
    }

    urlscopy[i] = NULL; /* NULL terminate the new referrals array */

    /*
     * log what we did (for debugging purposes)
     */
    if (loglevel_is_set(LDAP_DEBUG_ARGS)) {
        for (i = 0; urlscopy[i] != NULL; ++i) {
            slapi_log_err(SLAPI_LOG_ARGS, "ref_adjust", "\"%s\" -> \"%s\"\n",
                          urls[i]->bv_val, urlscopy[i]->bv_val);
        }
    }

    /*
     * done -- clean up and return the new referrals array
     */
    slapi_ch_free((void **)&opdn_norm);

    return (urlscopy);
}


/*
 * adjust_referral_basedn() -- pull the referral apart and modify the
 * baseDN contained in the referral if appropriate not.
 * If the referral does not have baseDN and we're not processing a
 * continuation reference, we do not need to do anything. If we're
 * processing a continuation reference, we need to add a baseDN.
 *
 * If it does have one, there are two special cases we deal with:
 *
 *  1) The referral's baseDN is an ancestor of the operation's baseDN,
 *     which means that the LDAP client already has a more specific
 *     baseDN than is contained in the referral.  If so, we strip off
 *     the baseDN so the client will re-use its operation DN when
 *     chasing the referral.
 *
 *  2) The referral's baseDN is not an ancestor of the operation's
 *     baseDN and the DN of the entry that contains the referral is
 *     an ancestor of the operation DN.  If so, we remove the portion
 *     of the operation DN that matches the DN of the entry containing
 *     the referral and prepend what's left to the referral base DN.
 *
 * We assume that the baseDN is the right-most piece of the URL, i.e., there
 * is no attribute list, scope, or options after the baseDN.
 *
 * The code also assumes that the baseDN doesn't contain any '/' character.
 * This later assumption NEED to be removed and code changed appropriately.
 */
static void
adjust_referral_basedn(char **urlp, const Slapi_DN *refsdn, char *opdn_norm, int isreference)
{
    LDAPURLDesc *ludp = NULL;
    char *p, *refdn_norm;
    int rc = 0;
    int secure = 0;

    PR_ASSERT(urlp != NULL);
    PR_ASSERT(*urlp != NULL);
    PR_ASSERT(refsdn != NULL);
    PR_ASSERT(opdn_norm != NULL);

    if (opdn_norm == NULL) {
        /* Be safe but this should never ever happen */
        return;
    }

    rc = slapi_ldap_url_parse(*urlp, &ludp, 0, &secure);

    if (rc != 0) {
        /* Nothing to do, just return */
        /* log bogus url? */
        return;
    }

    if (ludp && (ludp->lud_dn != NULL) && (ludp->lud_dn[0])) {

        refdn_norm = slapi_dn_normalize(slapi_ch_strdup(ludp->lud_dn));

        if (dn_is_below(opdn_norm, refdn_norm)) {
            /*
             * Strip off the baseDN.
             */
            if ((p = strrchr(*urlp, '/')) != NULL) {
                *p = '\0';
            }

        } else if (dn_is_below(opdn_norm, slapi_sdn_get_ndn(refsdn))) {
            int cur_len, add_len;

            /*
             * Prepend the portion of the operation DN that does not match
             * the ref container DN to the referral baseDN.
             */
            add_len = strlen(opdn_norm) - slapi_sdn_get_ndn_len(refsdn);
            cur_len = strlen(*urlp);
            /* + 7 because we keep extra space in case we add ??base */
            *urlp = slapi_ch_realloc(*urlp, cur_len + add_len + 7);

            if ((p = strrchr(*urlp, '/')) != NULL) {
                ++p;
                memmove(p + add_len, p, strlen(p) + 1);
                memmove(p, opdn_norm, add_len);
            }

        } /* else: leave the original referral baseDN intact. */

        slapi_ch_free((void **)&refdn_norm);
    } else if (isreference) {
        int cur_len, add_len;

        /* If ludp->lud_dn == NULL and isreference : Add the DN of the ref entry*/
        if (dn_is_below(opdn_norm, slapi_sdn_get_ndn(refsdn))) {
            add_len = strlen(opdn_norm);
            p = opdn_norm;
        } else {
            add_len = slapi_sdn_get_ndn_len(refsdn);
            p = (char *)slapi_sdn_get_ndn(refsdn);
        }

        cur_len = strlen(*urlp);
        /* + 8 because we keep extra space in case we add a / and/or ??base  */
        *urlp = slapi_ch_realloc(*urlp, cur_len + add_len + 8);
        if ((*urlp)[cur_len - 1] != '/') {
            /* The URL is not ending with a /, let's add one */
            strcat(*urlp, "/");
        }
        strcat(*urlp, p);
    }

    if (ludp != NULL) {
        ldap_free_urldesc(ludp);
    }
    return;
}


/*
 * dn_is_below(): return non-zero if "dn_norm" is below "ancestor_norm" in
 * the DIT and return zero if not.
 */
static int
dn_is_below(const char *dn_norm, const char *ancestor_norm)
{
    PR_ASSERT(dn_norm != NULL);
    PR_ASSERT(ancestor_norm != NULL);

    if (0 == strcasecmp(dn_norm, ancestor_norm)) {
        return (0); /* same DN --> not an ancestor relationship */
    }

    return (slapi_dn_issuffix(dn_norm, ancestor_norm));
}


/*
 * This function is useful to discover the source of data and provide
 * this as a referral. It is also useful if someone simply wants
 * to know if a dn is mastered somewhere else.
 *
 * For a given dn, traverse the referral list and look for the copiedFrom
 * attribute. If such an attribute is found get the server hostname
 * and port in the form "ldap://hostname:port".
 * Else return NULL.
 *
 * ORC extension: if orc == 1, this function will search for copyingFrom
 * which will refer searches and compares on trees that are half-baked.
 *
 * ORC extension: if cf_refs is NULL, then global_referrals is consulted,
 * otherwise, cf_refs is consulted.
 */
struct berval **
get_data_source(Slapi_PBlock *pb, const Slapi_DN *sdn, int orc, void *cfrp)
{
    int walker;
    struct berval **bvp;
    struct berval *bv;
    int found_it;
    Ref_Array *grefs = NULL;
    Ref_Array *the_refs = NULL;
    Ref_Array *cf_refs = (Ref_Array *)cfrp;

    /* If no Ref_Array is given, use global_referrals */
    if (cf_refs == NULL) {
        grefs = g_get_global_referrals();
        the_refs = grefs;
        GR_LOCK_READ();
    } else {
        the_refs = cf_refs;
    }

    /* optimization: if orc is 1 (a read), then check the readcount*/
    if (orc && the_refs->ra_readcount == 0) {
        if (cf_refs == NULL) {
            GR_UNLOCK_READ();
        }
        return (NULL);
    }


    bvp = NULL;
    bv = NULL;
    found_it = 0;

    /* Walk down the array, testing each dn to make see if it's a parent of "dn" */
    for (walker = 0; walker < the_refs->ra_nextindex; walker++) {
        if (slapi_dn_issuffix(slapi_sdn_get_ndn(sdn), the_refs->ra_refs[walker]->ref_dn)) {
            found_it = 1;
            break;
        }
    }
    /* no referral, so return NULL */
    if (!found_it) {
        if (cf_refs == NULL) {
            GR_UNLOCK_READ();
        }
        return (NULL);
    }

    /*
    * Gotta make sure we're returning the right one. If orc is 1, then
    * only return a read referral. if orc is 0, then only return a
    * write referral.
    */
    if (orc && the_refs->ra_refs[walker]->ref_reads != 1) {
        if (cf_refs == NULL) {
            GR_UNLOCK_READ();
        }
        return (NULL);
    }

    if (!orc && the_refs->ra_refs[walker]->ref_writes != 1) {
        if (cf_refs == NULL) {
            GR_UNLOCK_READ();
        }
        return (NULL);
    }

    Connection *pb_conn;
    slapi_pblock_get(pb, SLAPI_CONNECTION, &pb_conn);

    /* Fix for 310968 --- return an SSL referral to an SSL client */
    if (0 != (pb_conn->c_flags & CONN_FLAG_SSL)) {
        /* SSL connection */
        char *old_referral_string = NULL;
        char *new_referral_string = NULL;
        char *p = NULL;
        /* Get the basic referral */
        bv = slapi_ch_bvdup(the_refs->ra_refs[walker]->ref_referral);
        old_referral_string = bv->bv_val;
        /* Re-write it to replace ldap with ldaps, and remove the port information */
        /* The original string will look like this: ldap://host:port */
        /* We need to make it look like this: ldaps://host */
        /* Potentially the ":port" part might be missing from the original */
        new_referral_string = slapi_ch_smprintf("%s%s", LDAPS_URL_PREFIX, old_referral_string + strlen(LDAP_URL_PREFIX));
        /* Go looking for the port */
        p = new_referral_string + (strlen(LDAPS_URL_PREFIX) + 1);
        while (*p != '\0' && *p != ':')
            p++;
        if (':' == *p) {
            /* It had a port, zap it */
            *p = '\0';
        }
        /* Fix the bv to point to this new string */
        bv->bv_val = new_referral_string;
        /* Fix its length */
        bv->bv_len = strlen(bv->bv_val);
        /* Free the copy we made of the original */
        slapi_ch_free((void **)&old_referral_string);
    } else {
        /* regular connection */
        bv = (struct berval *)slapi_ch_bvdup(the_refs->ra_refs[walker]->ref_referral);
    }

    /* Package it up and send that puppy. */
    bvp = (struct berval **)slapi_ch_malloc(2 * sizeof(struct berval *));
    bvp[0] = bv;
    bvp[1] = NULL;

    if (cf_refs == NULL) {
        GR_UNLOCK_READ();
    }

    return (bvp);
}
