Blob Blame History Raw
/*
 *
 *   auth_mellon_util.c: an authentication apache module
 *   Copyright © 2003-2007 UNINETT (http://www.uninett.no/)
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#include <assert.h>

#include <openssl/err.h>
#include <openssl/rand.h>

#include "auth_mellon.h"

#ifdef APLOG_USE_MODULE
APLOG_USE_MODULE(auth_mellon);
#endif

/* This function is used to get the url of the current request.
 *
 * Parameters:
 *  request_rec *r       The current request.
 *
 * Returns:
 *  A string containing the full url of the current request.
 *  The string is allocated from r->pool.
 */
char *am_reconstruct_url(request_rec *r)
{
    char *url;

    /* This function will construct an full url for a given path relative to
     * the root of the web site. To configure what hostname and port this
     * function will use, see the UseCanonicalName configuration directive.
     */
    url = ap_construct_url(r->pool, r->unparsed_uri, r);

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "reconstruct_url: url==\"%s\", unparsed_uri==\"%s\"", url,
                  r->unparsed_uri);
    return url;
}

/* Get the hostname of the current request.
 *
 * Parameters:
 *  request_rec *r       The current request.
 *
 * Returns:
 *  The hostname of the current request.
 */
static const char *am_request_hostname(request_rec *r)
{
    const char *url;
    apr_uri_t uri;
    int ret;

    url = am_reconstruct_url(r);

    ret = apr_uri_parse(r->pool, url, &uri);
    if (ret != APR_SUCCESS) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to parse request URL: %s", url);
        return NULL;
    }

    if (uri.hostname == NULL) {
        /* This shouldn't happen, since the request URL is built with a hostname,
         * but log a message to make any debuggin around this code easier.
         */
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "No hostname in request URL: %s", url);
        return NULL;
    }

    return uri.hostname;
}

/* Validate the redirect URL.
 *
 * Checks that the redirect URL is to a trusted domain & scheme.
 *
 * Parameters:
 *  request_rec *r       The current request.
 *  const char *url      The redirect URL to validate.
 *
 * Returns:
 *  OK if the URL is valid, HTTP_BAD_REQUEST if not.
 */
int am_validate_redirect_url(request_rec *r, const char *url)
{
    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
    apr_uri_t uri;
    int ret;

    ret = apr_uri_parse(r->pool, url, &uri);
    if (ret != APR_SUCCESS) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Invalid redirect URL: %s", url);
        return HTTP_BAD_REQUEST;
    }

    /* Sanity check of the scheme of the domain. We only allow http and https. */
    if (uri.scheme) {
	/* http and https schemes without hostname are invalid. */
        if (!uri.hostname) {
            return HTTP_BAD_REQUEST;
	}
        if (strcasecmp(uri.scheme, "http")
            && strcasecmp(uri.scheme, "https")) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Only http or https scheme allowed in redirect URL: %s (%s)",
                          url, uri.scheme);
            return HTTP_BAD_REQUEST;
        }
    }

    if (!uri.hostname) {
        return OK; /* No hostname to check. */
    }

    for (int i = 0; cfg->redirect_domains[i] != NULL; i++) {
        const char *redirect_domain = cfg->redirect_domains[i];
        if (!strcasecmp(redirect_domain, "[self]")) {
            if (!strcasecmp(uri.hostname, am_request_hostname(r))) {
                return OK;
            }
        } else if (apr_fnmatch(redirect_domain, uri.hostname,
                               APR_FNM_PERIOD | APR_FNM_CASE_BLIND) ==
                   APR_SUCCESS) {
            return OK;
        }
    }
    AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                  "Untrusted hostname (%s) in redirect URL: %s",
                  uri.hostname, url);
    return HTTP_BAD_REQUEST;
}

/* This function builds an array of regexp backreferences
 *
 * Parameters:
 *  request_rec *r                 The current request.
 *  const am_cond_t *ce            The condition
 *  const char *value              Attribute value
 *  const ap_regmatch_t *regmatch  regmatch_t from ap_regexec()
 *
 * Returns:
 *  An array of collected backreference strings
 */
const apr_array_header_t *am_cond_backrefs(request_rec *r, 
                                           const am_cond_t *ce, 
                                           const char *value, 
                                           const ap_regmatch_t *regmatch)
{
    apr_array_header_t *backrefs;
    const char **ref;
    int nsub;
    int i;

    nsub = ce->regex->re_nsub + 1;     /* +1 for %0 */
    backrefs = apr_array_make(r->pool, nsub, sizeof(const char *));
    backrefs->nelts = nsub;

    ref = (const char **)(backrefs->elts);

    for (i = 0; i < nsub; i++) {
        if ((regmatch[i].rm_so == -1) || (regmatch[i].rm_eo == -1)) {
            ref[i] = "";
        } else {
            int len = regmatch[i].rm_eo - regmatch[i].rm_so;
            int off = regmatch[i].rm_so;

            ref[i] = apr_pstrndup(r->pool, value + off, len);
        }
    }

    return (const apr_array_header_t *)backrefs;
}

/* This function clones an am_cond_t and substitute value to 
 * match (both regexp and string) with backreferences from
 * a previous regex match.
 *
 * Parameters:
 *  request_rec *r                      The current request.
 *  const am_cond_t *cond               The am_cond_t to clone and substiture
 *  const apr_array_header_t *backrefs  Collected backreferences
 *
 * Returns:
 *  The cloned am_cond_t
 */
const am_cond_t *am_cond_substitue(request_rec *r, const am_cond_t *ce, 
                                   const apr_array_header_t *backrefs)
{
    am_cond_t *c;
    const char *instr = ce->str;
    apr_size_t inlen = strlen(instr);
    const char *outstr = "";
    size_t last;
    size_t i;

    c = (am_cond_t *)apr_pmemdup(r->pool, ce, sizeof(*ce));
    c->str = outstr;
    last = 0;
    
    for (i = strcspn(instr, "%"); i < inlen; i += strcspn(instr + i, "%")) {
        const char *fstr;
        const char *ns;
        const char *name;
        const char *value;
        apr_size_t flen;
        apr_size_t pad;
        apr_size_t nslen;

        /* 
         * Make sure we got a %
         */
	assert(instr[i] == '%');

        /*
         * Copy the format string in fstr. It can be a single 
         * digit (e.g.: %1) , or a curly-brace enclosed text
         * (e.g.: %{12})
         */
        fstr = instr + i + 1;
        if (*fstr == '{') {          /* Curly-brace enclosed text */
            pad = 3; /* 3 for %{} */
            fstr++;
            flen = strcspn(fstr, "}");

            /* If there is no closing }, we do not substitute  */
            if (fstr[flen] == '\0') {
                pad = 2; /* 2 for %{ */
                i += flen + pad;
                break;
            }

        } else if (*fstr == '\0') {  /* String ending by a % */
            break;

        } else {                     /* Single digit */
            pad = 1; /* 1 for % */
            flen = 1;
        }

        /*
         * Try to extract a namespace (ns) and a name, e.g: %{ENV:foo}
         */ 
        fstr = apr_pstrndup(r->pool, fstr, flen);
        if ((nslen = strcspn(fstr, ":")) != flen) {
            ns = apr_pstrndup(r->pool, fstr, nslen);
            name = fstr + nslen + 1; /* +1 for : */
        } else {
            nslen = 0;
            ns = "";
            name = fstr;
        }

        value = NULL;
        if ((*ns == '\0') && (strspn(fstr, "0123456789") == flen)) {
            /*
             * If fstr has only digits, this is a regexp backreference
             */
            int d = (int)apr_atoi64(fstr);

            if ((d >= 0) && (d < backrefs->nelts)) 
                value = ((const char **)(backrefs->elts))[d];

        } else if ((*ns == '\0') && (strcmp(fstr, "%") == 0)) {
            /*
             * %-escape
             */
            value = fstr;

        } else if (strcmp(ns, "ENV") == 0) {
            /*
             * ENV namespace. Get value from apache environment
             */
            value = getenv(name);
        }

        /*
         * If we did not find a value, substitue the
         * format string with an empty string.
         */
         if (value == NULL)
            value = "";

        /*
         * Concatenate the value with leading text, and * keep track 
         * of the last location we copied in source string
         */
        outstr = apr_pstrcat(r->pool, outstr,
                             apr_pstrndup(r->pool, instr + last, i - last), 
                             value, NULL);
        last = i + flen + pad;

        /*
         * Move index to the end of the format string
         */
        i += flen + pad;
    }

    /*
     * Copy text remaining after the last format string.
     */
    outstr = apr_pstrcat(r->pool, outstr,
                         apr_pstrndup(r->pool, instr + last, i - last), 
                         NULL);

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "Directive %s, \"%s\" substituted into \"%s\"",
                  ce->directive, instr, outstr);

    /*
     * If this was a regexp, recompile it.
     */
    if (ce->flags & AM_COND_FLAG_REG) {
        int regex_flags = AP_REG_EXTENDED|AP_REG_NOSUB;
 
        if (ce->flags & AM_COND_FLAG_NC)
            regex_flags |= AP_REG_ICASE;
 
        c->regex = ap_pregcomp(r->pool, outstr, regex_flags);
        if (c->regex == NULL) {
             AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r,
                           "Invalid regular expression \"%s\"", outstr);
             return ce;
        }
    }

    return (const am_cond_t *)c;
}

/* This function checks if the user has access according
 * to the MellonRequire and MellonCond directives.
 *
 * Parameters:
 *  request_rec *r              The current request.
 *  am_cache_entry_t *session   The current session.
 *
 * Returns:
 *  OK if the user has access and HTTP_FORBIDDEN if he doesn't.
 */
int am_check_permissions(request_rec *r, am_cache_entry_t *session)
{
    am_dir_cfg_rec *dir_cfg;
    int i, j;
    int skip_or = 0;
    const apr_array_header_t *backrefs = NULL;

    dir_cfg = am_get_dir_cfg(r);

    /* Iterate over all cond-directives */
    for (i = 0; i < dir_cfg->cond->nelts; i++) {
        const am_cond_t *ce;
        const char *value = NULL;
        int match = 0;

        ce = &((am_cond_t *)(dir_cfg->cond->elts))[i];

        am_diag_printf(r, "%s processing condition %d of %d: %s ",
                       __func__, i, dir_cfg->cond->nelts,
                       am_diag_cond_str(r, ce));

        /*
         * Rule with ignore flog?
         */
        if (ce->flags & AM_COND_FLAG_IGN)
            continue;

        /* 
         * We matched a [OR] rule, skip the next rules
         * until we have one without [OR]. 
         */
        if (skip_or) {
            if (!(ce->flags & AM_COND_FLAG_OR))
                skip_or = 0;

            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                          "Skip %s, [OR] rule matched previously",
                          ce->directive);

            am_diag_printf(r, "Skip, [OR] rule matched previously\n");
            continue;
        }
        
        /* 
         * look for a match on each value for this attribute, 
         * stop on first match.
         */
        for (j = 0; (j < session->size) && !match; j++) {
            const char *varname = NULL;
            am_envattr_conf_t *envattr_conf = NULL;

            /*
             * if MAP flag is set, check for remapped 
             * attribute name with mellonSetEnv
             */
            if (ce->flags & AM_COND_FLAG_MAP) {
                envattr_conf =  (am_envattr_conf_t *)apr_hash_get(dir_cfg->envattr, 
                                         am_cache_entry_get_string(session,&session->env[j].varname),
                                         APR_HASH_KEY_STRING);
                                                    
                if (envattr_conf != NULL)
                    varname = envattr_conf->name;
            }

            /*
             * Otherwise or if not found, use the attribute name
             * sent by the IdP.
             */
            if (varname == NULL)
                varname = am_cache_entry_get_string(session,
                                                    &session->env[j].varname);
                      
            if (strcmp(varname, ce->varname) != 0)
                    continue;

            value = am_cache_entry_get_string(session, &session->env[j].value);

            /*
             * Substiture backrefs if available
             */
            if ((ce->flags & AM_COND_FLAG_FSTR) && (backrefs != NULL))
                ce = am_cond_substitue(r, ce, backrefs);

            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                          "Evaluate %s vs \"%s\"", 
                          ce->directive, value);

            am_diag_printf(r, "evaluate value \"%s\" ", value);
    
            if (value == NULL) {
                 match = 0;          /* can not happen */

            } else if (ce->flags & (AM_COND_FLAG_REG|AM_COND_FLAG_REF)) {
                 int nsub = ce->regex->re_nsub + 1;
                 ap_regmatch_t *regmatch;

                 regmatch = (ap_regmatch_t *)apr_palloc(r->pool, 
                            nsub * sizeof(*regmatch));

                 match = !ap_regexec(ce->regex, value, nsub, regmatch, 0);
                 if (match)
                     backrefs = am_cond_backrefs(r, ce, value, regmatch);

            } else if (ce->flags & AM_COND_FLAG_REG) {
                 match = !ap_regexec(ce->regex, value, 0, NULL, 0);

            } else if (ce->flags & (AM_COND_FLAG_SUB|AM_COND_FLAG_NC)) {
                 match = (ap_strcasestr(ce->str, value) != NULL);

            } else if (ce->flags & AM_COND_FLAG_SUB) {
                 match = (strstr(ce->str, value) != NULL);

            } else if (ce->flags & AM_COND_FLAG_NC) {
                 match = !strcasecmp(ce->str, value);

            } else {
                 match = !strcmp(ce->str, value);
            }

        am_diag_printf(r, "match=%s, ", match ? "yes" : "no");
        }

        if (ce->flags & AM_COND_FLAG_NOT) {
            match = !match;

            am_diag_printf(r, "negating now match=%s ", match ? "yes" : "no");
        }

        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "%s: %smatch", ce->directive,
                      (match == 0) ? "no ": "");

        /*
         * If no match, we stop here, except if it is an [OR] condition
         */
        if (!match & !(ce->flags & AM_COND_FLAG_OR)) {
            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r,
                          "Client failed to match %s",
                          ce->directive);

            am_diag_printf(r, "failed (no OR condition)"
                           " returning HTTP_FORBIDDEN\n");
            return HTTP_FORBIDDEN;
        }

        /*
         * Match on [OR] condition means we skip until a rule
         * without [OR], 
         */
        if (match && (ce->flags & AM_COND_FLAG_OR))
            skip_or = 1;

        am_diag_printf(r, "\n");
    }

    am_diag_printf(r, "%s succeeds\n", __func__);

    return OK;
}

/* This function sets default Cache-Control headers.
 *
 * Parameters:
 *  request_rec *r       The request we are handling.
 *
 * Returns:
 *  Nothing.
 */
void am_set_cache_control_headers(request_rec *r)
{
    /* Send Cache-Control header to ensure that:
     * - no proxy in the path caches content inside this location (private),
     * - user agent have to revalidate content on server (must-revalidate).
     * - content is always stale as the session login status can change at any
     *   time synchronously (Redirect logout, session cookie is removed) or
     *   asynchronously (SOAP logout, session cookie still exists but is
     *   invalid),
     *
     * But never prohibit specifically any user agent to cache or store content
     *
     * Setting the headers in err_headers_out ensures that they will be
     * sent for all responses.
     */
    apr_table_setn(r->err_headers_out,
                   "Cache-Control", "private, max-age=0, must-revalidate");
}

/* This function reads the post data for a request.
 *
 * The data is stored in a buffer allocated from the request pool.
 * After successful operation *data contains a pointer to the data and
 * *length contains the length of the data. 
 * The data will always be null-terminated.
 *
 * Parameters:
 *  request_rec *r        The request we read the form data from.
 *  char **data           Pointer to where we will store the pointer
 *                        to the data we read.
 *  apr_size_t *length    Pointer to where we will store the length
 *                        of the data we read. Pass NULL if you don't
 *                        need to know the length of the data.
 *
 * Returns:
 *  OK if we successfully read the POST data.
 *  An error if we fail to read the data.
 */
int am_read_post_data(request_rec *r, char **data, apr_size_t *length)
{
    apr_size_t bytes_read;
    apr_size_t bytes_left;
    apr_size_t len;
    long read_length;
    int rc;

    /* Prepare to receive data from the client. We request that apache
     * dechunks data if it is chunked.
     */
    rc = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK);
    if (rc != OK) {
        return rc;
    }

    /* This function will send a 100 Continue response if the client is
     * waiting for that. If the client isn't going to send data, then this
     * function will return 0.
     */
    if (!ap_should_client_block(r)) {
        len = 0;
    } else {
        len = r->remaining;
    }

    if (len >= 1024*1024) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Too large POST data payload (%lu bytes).",
                      (unsigned long)len);
        return HTTP_BAD_REQUEST;
    }


    if (length != NULL) {
        *length = len;
    }

    *data = (char *)apr_palloc(r->pool, len + 1);
    if (*data == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to allocate memory for %lu bytes of POST data.",
                      (unsigned long)len);
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    /* Make sure that the data is null-terminated.  */
    (*data)[len] = '\0';

    bytes_read = 0;
    bytes_left = len;

    while (bytes_left > 0) {
        /* Read data from the client. Returns 0 on EOF and -1 on
         * error, the number of bytes otherwise.
         */
        read_length = ap_get_client_block(r, &(*data)[bytes_read],
                                          bytes_left);
        if (read_length == 0) {
            /* got the EOF */
            (*data)[bytes_read] = '\0';

            if (length != NULL) {
                *length = bytes_read;
            }
            break;
        }
        else if (read_length < 0) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Failed to read POST data from client.");
            return HTTP_INTERNAL_SERVER_ERROR;
        }

        bytes_read += read_length;
        bytes_left -= read_length;
    }

    am_diag_printf(r, "POST data: %s\n", *data);
    return OK;
}


/* extract_query_parameter is a function which extracts the value of
 * a given parameter in a query string. The query string can be the
 * query_string parameter of a GET request, or it can be the data
 * passed to the web server in a POST request.
 *
 * Parameters:
 *  apr_pool_t *pool           The memory pool which the memory for
 *                             the value will be allocated from.
 *  const char *query_string   Either the query_string from a GET
 *                             request, or the data from a POST
 *                             request.
 *  const char *name           The name of the parameter to extract.
 *                             Note that the search for this name is
 *                             case sensitive.
 *
 * Returns:
 *  The value of the parameter or NULL if we don't find the parameter.
 */
char *am_extract_query_parameter(apr_pool_t *pool,
                                 const char *query_string,
                                 const char *name)
{
    const char *ip;
    const char *value_end;
    apr_size_t namelen;

    if (query_string == NULL) {
        return NULL;
    }

    ip = query_string;
    namelen = strlen(name);

    /* Find parameter. Searches for /[^&]<name>[&=$]/.
     * Moves ip to the first character after the name (either '&', '='
     * or '\0').
     */
    for (;;) {
        /* First we find the name of the parameter. */
        ip = strstr(ip, name);
        if (ip == NULL) {
            /* Parameter not found. */
            return NULL;
        }

        /* Then we check what is before the parameter name. */
        if (ip != query_string && ip[-1] != '&') {
            /* Name not preceded by [^&]. */
            ip++;
            continue;
        }

        /* And last we check what follows the parameter name. */
        if (ip[namelen] != '=' && ip[namelen] != '&'
            && ip[namelen] != '\0') {
            /* Name not followed by [&=$]. */
            ip++;
            continue;
        }


        /* We have found the pattern. */
        ip += namelen;
        break;
    }

    /* Now ip points to the first character after the name. If this
     * character is '&' or '\0', then this field doesn't have a value.
     * If this character is '=', then this field has a value.
     */
    if (ip[0] == '=') {
        ip += 1;
    }

    /* The value is from ip to '&' or to the end of the string, whichever
     * comes first. */
    value_end = strchr(ip, '&');
    if (value_end != NULL) {
        /* '&' comes first. */
        return apr_pstrndup(pool, ip, value_end - ip);
    } else {
        /* Value continues until the end of the string. */
        return apr_pstrdup(pool, ip);
    }
}


/* Convert a hexadecimal digit to an integer.
 *
 * Parameters:
 *  char c           The digit we should convert.
 *
 * Returns:
 *  The digit as an integer, or -1 if it isn't a hex digit.
 */
static int am_unhex_digit(char c) {
    if (c >= '0' && c <= '9') {
        return c - '0';
    } else if (c >= 'a' && c <= 'f') {
        return c - 'a' + 0xa;
    } else if (c >= 'A' && c <= 'F') {
        return c - 'A' + 0xa;
    } else {
        return -1;
    }
}

/* This function urldecodes a string in-place.
 *
 * Parameters:
 *  char *data       The string to urldecode.
 *
 * Returns:
 *  OK if successful or HTTP_BAD_REQUEST if any escape sequence decodes to a
 *  null-byte ('\0'), or if an invalid escape sequence is found.
 */
int am_urldecode(char *data)
{
    char *ip;
    char *op;
    int c1, c2;

    if (data == NULL) {
        return HTTP_BAD_REQUEST;
    }

    ip = data;
    op = data;
    while (*ip) {
        switch (*ip) {
        case '+':
            *op = ' ';
            ip++;
            op++;
            break;
        case '%':
            /* Decode the hex digits. Note that we need to check the
             * result of the first conversion before attempting the
             * second conversion -- otherwise we may read past the end
             * of the string.
             */
            c1 = am_unhex_digit(ip[1]);
            if (c1 < 0) {
                return HTTP_BAD_REQUEST;
            }
            c2 = am_unhex_digit(ip[2]);
            if (c2 < 0) {
                return HTTP_BAD_REQUEST;
            }

            *op = (c1 << 4) | c2;
            if (*op == '\0') {
                /* null-byte. */
                return HTTP_BAD_REQUEST;
            }
            ip += 3;
            op++;
            break;
        default:
            *op = *ip;
            ip++;
            op++;
        }
    }
    *op = '\0';

    return OK;
}


/* This function urlencodes a string. It will escape all characters
 * except a-z, A-Z, 0-9, '_' and '.'.
 *
 * Parameters:
 *  apr_pool_t *pool   The pool we should allocate memory from.
 *  const char *str    The string we should urlencode.
 *
 * Returns:
 *  The urlencoded string, or NULL if str == NULL.
 */
char *am_urlencode(apr_pool_t *pool, const char *str)
{
    const char *ip;
    apr_size_t length;
    char *ret;
    char *op;
    int hi, low;
    /* Return NULL if str is NULL. */
    if(str == NULL) {
        return NULL;
    }


    /* Find the length of the output string. */
    length = 0;
    for(ip = str; *ip; ip++) {
        if(*ip >= 'a' && *ip <= 'z') {
            length++;
        } else if(*ip >= 'A' && *ip <= 'Z') {
            length++;
        } else if(*ip >= '0' && *ip <= '9') {
            length++;
        } else if(*ip == '_' || *ip == '.') {
            length++;
        } else {
            length += 3;
        }
    }

    /* Add space for null-terminator. */
    length++;

    /* Allocate memory for string. */
    ret = (char *)apr_palloc(pool, length);

    /* Encode string. */
    for(ip = str, op = ret; *ip; ip++, op++) {
        if(*ip >= 'a' && *ip <= 'z') {
            *op = *ip;
        } else if(*ip >= 'A' && *ip <= 'Z') {
            *op = *ip;
        } else if(*ip >= '0' && *ip <= '9') {
            *op = *ip;
        } else if(*ip == '_' || *ip == '.') {
            *op = *ip;
        } else {
            *op = '%';
            op++;

            hi = (*ip & 0xf0) >> 4;

            if(hi < 0xa) {
                *op = '0' + hi;
            } else {
                *op = 'A' + hi - 0xa;
            }
            op++;

            low = *ip & 0x0f;

            if(low < 0xa) {
                *op = '0' + low;
            } else {
                *op = 'A' + low - 0xa;
            }
        }
    }

    /* Make output string null-terminated. */
    *op = '\0';

    return ret;
}

/*
 * Check that a URL is safe for redirect.
 *
 * Parameters:
 *  request_rec *r       The request we are processing.
 *  const char *url      The URL we should check.
 *
 * Returns:
 *  OK on success, HTTP_BAD_REQUEST otherwise.
 */
int am_check_url(request_rec *r, const char *url)
{
    const char *i;

    for (i = url; *i; i++) {
        if (*i >= 0 && *i < ' ') {
            /* Deny all control-characters. */
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r,
                          "Control character detected in URL.");
            return HTTP_BAD_REQUEST;
        }
        if (*i == '\\') {
            /* Reject backslash character, as it can be used to bypass
             * redirect URL validation. */
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r,
                          "Backslash character detected in URL.");
            return HTTP_BAD_REQUEST;
        }
    }

    return OK;
}

/* This function generates a given number of (pseudo)random bytes.
 * The current implementation uses OpenSSL's RAND_*-functions.
 *
 * Parameters:
 *  request_rec *r       The request we are generating random bytes for.
 *                       The request is used for configuration and
 *                       error/warning reporting.
 *  void *dest           The address if the buffer we should fill with data.
 *  apr_size_t count     The number of random bytes to create.
 *
 * Returns:
 *  OK on success, or HTTP_INTERNAL_SERVER on failure.
 */
int am_generate_random_bytes(request_rec *r, void *dest, apr_size_t count)
{
    int rc;
    rc = RAND_bytes((unsigned char *)dest, (int)count);
    if(rc != 1) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Error generating random data: %lu",
                      ERR_get_error());
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    return OK;
}


/* This function generates an id which is AM_ID_LENGTH characters long.
 * The id will consist of hexadecimal characters.
 *
 * Parameters:
 *  request_rec *r       The request we associate allocated memory with.
 *
 * Returns:
 *  The session id, made up of AM_ID_LENGTH hexadecimal characters,
 *  terminated by a null-byte.
 */
char *am_generate_id(request_rec *r)
{
    int rc;
    char *ret;
    int rand_data_len;
    unsigned char *rand_data;
    int i;
    unsigned char b;
    int hi, low;

    ret = (char *)apr_palloc(r->pool, AM_ID_LENGTH + 1);

    /* We need to round the length of the random data _up_, in case the
     * length of the session id isn't even.
     */
    rand_data_len = (AM_ID_LENGTH + 1) / 2;

    /* Fill the last rand_data_len bytes of the string with
     * random bytes. This allows us to overwrite from the beginning of
     * the string.
     */
    rand_data = (unsigned char *)&ret[AM_ID_LENGTH - rand_data_len];

    /* Generate random numbers. */
    rc = am_generate_random_bytes(r, rand_data, rand_data_len);
    if(rc != OK) {
        return NULL;
    }

    /* Convert the random bytes to hexadecimal. Note that we will write
     * AM_ID_LENGTH+1 characters if we have a non-even length of the
     * session id. This is OK - we will simply overwrite the last character
     * with the null-terminator afterwards.
     */
    for(i = 0; i < AM_ID_LENGTH; i += 2) {
        b = rand_data[i / 2];
        hi = (b >> 4) & 0xf;
        low = b & 0xf;

        if(hi >= 0xa) {
            ret[i] = 'a' + hi - 0xa;
        } else {
            ret[i] = '0' + hi;
        }

        if(low >= 0xa) {
            ret[i+1] = 'a' + low - 0xa;
        } else {
            ret[i+1] = '0' + low;
        }
    }

    /* Add null-terminator- */
    ret[AM_ID_LENGTH] = '\0';

    return ret;
}

/* This returns the directroy part of a path, a la dirname(3)
 *
 * Parameters:
 *  apr_pool_t p         Pool to allocate memory from
 *  const char *path     Path to extract directory from
 *
 * Returns:
 *  The directory part of path
 */
const char *am_filepath_dirname(apr_pool_t *p, const char *path) 
{
    char *cp;

    /*
     * Try Unix and then Windows style. Borrowed from
     * apr_match_glob(), it seems it cannot be made more
     * portable.
     */
    if (((cp = strrchr(path, (int)'/')) == NULL) &&
        ((cp = strrchr(path, (int)'\\')) == NULL))
            return ".";
   
    return apr_pstrndup(p, path, cp - path);
}

/*
 * Allocate and initialize a am_file_data_t
 *
 * Parameters:
 *   apr_pool_t *pool  Allocation pool.
 *   const char *path  If non-NULL initialize file_data->path to copy of path
 *
 * Returns:
 *   Newly allocated & initialized file_data_t
 */
am_file_data_t *am_file_data_new(apr_pool_t *pool, const char *path)
{
    am_file_data_t *file_data = NULL;

    if ((file_data = apr_pcalloc(pool, sizeof(am_file_data_t))) == NULL) {
        return NULL;
    }

    file_data->pool = pool;
    file_data->rv = APR_EINIT;
    if (path) {
        file_data->path = apr_pstrdup(file_data->pool, path);
    }

    return file_data;
}

/*
 * Allocate a new am_file_data_t and copy
 *
 * Parameters:
 *   apr_pool_t *pool              Allocation pool.
 *   am_file_data_t *src_file_data The src being copied.
 *
 * Returns:
 *   Newly allocated & initialized from src_file_data
 */
am_file_data_t *am_file_data_copy(apr_pool_t *pool,
                                  am_file_data_t *src_file_data)
{
    am_file_data_t *dst_file_data = NULL;

    if ((dst_file_data = am_file_data_new(pool, src_file_data->path)) == NULL) {
        return NULL;
    }

    dst_file_data->path = apr_pstrdup(pool, src_file_data->path);
    dst_file_data->stat_time = src_file_data->stat_time;
    dst_file_data->finfo = src_file_data->finfo;
    dst_file_data->contents = apr_pstrdup(pool, src_file_data->contents);
    dst_file_data->read_time = src_file_data->read_time;
    dst_file_data->rv = src_file_data->rv;
    dst_file_data->strerror = apr_pstrdup(pool, src_file_data->strerror);
    dst_file_data->generated = src_file_data->generated;

    return dst_file_data;
}

/*
 * Peform a stat on a file to get it's properties
 *
 * A stat is performed on the file. If there was an error the
 * result value is left in file_data->rv and an error description
 * string is formatted and left in file_data->strerror and function
 * returns the rv value. If the stat was successful the stat
 * information is left in file_data->finfo and APR_SUCCESS
 * set set as file_data->rv and returned as the function result.
 * 
 * The file_data->stat_time indicates if and when the stat was
 * performed, a zero time value indicates the operation has not yet
 * been performed.
 *
 * Parameters:
 *   am_file_data_t *file_data   Struct containing file information
 *
 * Returns:
 *   APR status code, same value as file_data->rv
 */
apr_status_t am_file_stat(am_file_data_t *file_data)
{
    char buffer[512];

    if (file_data == NULL) {
        return APR_EINVAL;
    }

    file_data->strerror = NULL;

    file_data->stat_time = apr_time_now();
    file_data->rv = apr_stat(&file_data->finfo, file_data->path,
                             APR_FINFO_SIZE, file_data->pool);
    if (file_data->rv != APR_SUCCESS) {
        file_data->strerror =
            apr_psprintf(file_data->pool,
                         "apr_stat: Error opening \"%s\" [%d] \"%s\"",
                         file_data->path, file_data->rv,
                         apr_strerror(file_data->rv, buffer, sizeof(buffer)));
    }

    return file_data->rv;
}

/*
 * Read file into dynamically allocated buffer
 *
 * First a stat is performed on the file. If there was an error the
 * result value is left in file_data->rv and an error description
 * string is formatted and left in file_data->strerror and function
 * returns the rv value. If the stat was successful the stat
 * information is left in file_data->finfo.
 *
 * A buffer is dynamically allocated and the contents of the file is
 * read into file_data->contents. If there was an error the result
 * value is left in file_data->rv and an error description string is
 * formatted and left in file_data->strerror and the function returns
 * the rv value.
 *
 * The file_data->stat_time and file_data->read_time indicate if and
 * when those operations were performed, a zero time value indicates
 * the operation has not yet been performed.
 *
 * Parameters:
 *   am_file_data_t *file_data   Struct containing file information
 *
 * Returns:
 *   APR status code, same value as file_data->rv
 */
apr_status_t am_file_read(am_file_data_t *file_data)
{
    char buffer[512];
    apr_file_t *fd;
    apr_size_t nbytes;

    if (file_data == NULL) {
        return APR_EINVAL;
    }
    file_data->rv = APR_SUCCESS;
    file_data->strerror = NULL;

    am_file_stat(file_data);
    if (file_data->rv != APR_SUCCESS) {
        return file_data->rv;
    }

    if ((file_data->rv = apr_file_open(&fd, file_data->path,
                                       APR_READ, 0, file_data->pool)) != 0) {
        file_data->strerror =
            apr_psprintf(file_data->pool,
                         "apr_file_open: Error opening \"%s\" [%d] \"%s\"",
                         file_data->path, file_data->rv,
                         apr_strerror(file_data->rv, buffer, sizeof(buffer)));
        return file_data->rv;
    }

    file_data->read_time = apr_time_now();
    nbytes = file_data->finfo.size;
    file_data->contents = (char *)apr_palloc(file_data->pool, nbytes + 1);

    file_data->rv = apr_file_read_full(fd, file_data->contents, nbytes, NULL);
    if (file_data->rv != 0) {
        file_data->strerror =
            apr_psprintf(file_data->pool,
                         "apr_file_read_full: Error reading \"%s\" [%d] \"%s\"",
                         file_data->path, file_data->rv,
                         apr_strerror(file_data->rv, buffer, sizeof(buffer)));
        (void)apr_file_close(fd);
        return file_data->rv;

    }
    file_data->contents[nbytes] = '\0';

    (void)apr_file_close(fd);

    return file_data->rv;
}

/*
 * Purge outdated saved POST requests.
 *
 * Parameters:
 *   request_rec *r     The current request
 *
 * Returns:
 *  OK on success, or HTTP_INTERNAL_SERVER on failure.
 */
int am_postdir_cleanup(request_rec *r)
{
    am_mod_cfg_rec *mod_cfg;
    apr_dir_t *postdir;
    apr_status_t rv;
    char error_buffer[64];
    apr_finfo_t afi;
    char *fname;
    int count;
    apr_time_t expire_before;

    mod_cfg = am_get_mod_cfg(r->server);

    /* The oldes file we should keep. Delete files that are older. */
    expire_before = apr_time_now() - mod_cfg->post_ttl * APR_USEC_PER_SEC;

    /*
     * Open our POST directory or create it. 
     */
    rv = apr_dir_open(&postdir, mod_cfg->post_dir, r->pool);
    if (rv != 0) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Unable to open MellonPostDirectory \"%s\": %s",
                      mod_cfg->post_dir,
                      apr_strerror(rv, error_buffer, sizeof(error_buffer)));
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    /*
     * Purge outdated items
     */
    count = 0;
    do {
        rv = apr_dir_read(&afi, APR_FINFO_NAME|APR_FINFO_CTIME, postdir);
        if (rv != OK)
            break;

        /* Skip dot_files */
        if (afi.name[0] == '.')
             continue;

        if (afi.ctime < expire_before) {
            fname = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, afi.name);
            (void)apr_file_remove(fname , r->pool); 
        } else {
            count++;
        }
    } while (1 /* CONSTCOND */);

    (void)apr_dir_close(postdir);

    if (count >= mod_cfg->post_count) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Too many saved POST sessions. "
                      "Increase MellonPostCount directive.");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    return OK;
}

/* 
 * HTML-encode a string
 *
 * Parameters:
 *   request_rec *r     The current request
 *   const char *str    The string to encode
 *
 * Returns:
 *  The encoded string
 */
char *am_htmlencode(request_rec *r, const char *str)
{
    const char *cp;
    char *output;
    apr_size_t outputlen;
    int i;

    outputlen = 0;
    for (cp = str; *cp; cp++) {
        switch (*cp) {
        case '&':
            outputlen += 5;
            break;
        case '"':
            outputlen += 6;
            break;
        default:
            outputlen += 1;
            break;
        }
    }

    i = 0;
    output = apr_palloc(r->pool, outputlen + 1);
    for (cp = str; *cp; cp++) {
        switch (*cp) {
        case '&':
            (void)strcpy(&output[i], "&amp;");
            i += 5;
            break;
        case '"':
            (void)strcpy(&output[i], "&quot;");
            i += 6;
            break;
        default:
            output[i] = *cp;
            i += 1;
            break;
        }
    }
    output[i] = '\0';

    return output;
}

/* This function produces the endpoint URL
 *
 * Parameters:
 *  request_rec *r       The request we received.
 *
 * Returns:
 *  the endpoint URL
 */
char *am_get_endpoint_url(request_rec *r)
{
    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);

    return ap_construct_url(r->pool, cfg->endpoint_path, r);
}

/*
 * This function saves a POST request for later replay and updates
 * the return URL.
 *
 * Parameters:
 *  request_rec *r           The current request.
 *  const char **relay_state The returl URL
 *
 * Returns:
 *  OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise
 */
int am_save_post(request_rec *r, const char **relay_state)
{
    am_mod_cfg_rec *mod_cfg;
    const char *content_type;
    const char *charset;
    const char *psf_id;
    char *psf_name;
    char *post_data;
    apr_size_t post_data_len;
    apr_size_t written;
    apr_file_t *psf;

    mod_cfg = am_get_mod_cfg(r->server);
    if (mod_cfg->post_dir == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "MellonPostReplay enabled but MellonPostDirectory not set "
                      "-- cannot save post data");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    if (am_postdir_cleanup(r) != OK)
        return HTTP_INTERNAL_SERVER_ERROR;

    /* Check Content-Type */
    content_type = apr_table_get(r->headers_in, "Content-Type");
    if (content_type == NULL) {
        content_type = "urlencoded";
        charset = NULL; 
    } else {
        if (am_has_header(r, content_type, 
            "application/x-www-form-urlencoded")) {
            content_type = "urlencoded";

        } else if (am_has_header(r, content_type,
                   "multipart/form-data")) {
            content_type = "multipart";

        } else {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Unknown POST Content-Type \"%s\"", content_type);
            return HTTP_INTERNAL_SERVER_ERROR;
        }

        charset = am_get_header_attr(r, content_type, NULL, "charset");
    }     

    if ((psf_id = am_generate_id(r)) == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot generate id");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    psf_name = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, psf_id);

    if (apr_file_open(&psf, psf_name,
                      APR_WRITE|APR_CREATE|APR_BINARY, 
                      APR_FPROT_UREAD|APR_FPROT_UWRITE,
                      r->pool) != OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "cannot create POST session file");
        return HTTP_INTERNAL_SERVER_ERROR;
    } 

    if (am_read_post_data(r, &post_data, &post_data_len) != OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot read POST data");
        (void)apr_file_close(psf);
        return HTTP_INTERNAL_SERVER_ERROR;
    } 

    if (post_data_len > mod_cfg->post_size) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "POST data size %" APR_SIZE_T_FMT 
                      " exceeds maximum %" APR_SIZE_T_FMT ". "
                      "Increase MellonPostSize directive.",
                      post_data_len, mod_cfg->post_size);
        (void)apr_file_close(psf);
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    written = post_data_len;
    if ((apr_file_write(psf, post_data, &written) != OK) ||
        (written != post_data_len)) { 
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "cannot write to POST session file");
            (void)apr_file_close(psf);
            return HTTP_INTERNAL_SERVER_ERROR;
    } 
    
    if (apr_file_close(psf) != OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "cannot close POST session file");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    if (charset != NULL)
        charset = apr_psprintf(r->pool, "&charset=%s", 
                               am_urlencode(r->pool, charset));
    else 
        charset = "";

    *relay_state = apr_psprintf(r->pool, 
                                "%srepost?id=%s&ReturnTo=%s&enctype=%s%s",
                                am_get_endpoint_url(r), psf_id,
                                am_urlencode(r->pool, *relay_state), 
                                content_type, charset);

    return OK;
}

/*
 * This function replaces CRLF by LF in a string
 *
 * Parameters:
 *  request_rec *r  The current request
 *  const char *str The string
 *
 * Returns:
 *  Output string
 */
const char *am_strip_cr(request_rec *r, const char *str)
{
    char *output;
    const char *cp;
    apr_size_t i;

    output = apr_palloc(r->pool, strlen(str) + 1);
    i = 0;

    for (cp = str; *cp; cp++) {
        if ((*cp == '\r') && (*(cp + 1) == '\n'))
            continue;
        output[i++] = *cp;
    }

    output[i++] = '\0';
    
    return (const char *)output;
}

/*
 * This function replaces LF by CRLF in a string
 *
 * Parameters:
 *  request_rec *r  The current request
 *  const char *str The string
 *
 * Returns:
 *  Output string
 */
const char *am_add_cr(request_rec *r, const char *str)
{
    char *output;
    const char *cp;
    apr_size_t xlen;
    apr_size_t i;

    xlen = 0;

    for (cp = str; *cp; cp++)
        if (*cp == '\n')
            xlen++;

    output = apr_palloc(r->pool, strlen(str) + xlen + 1);
    i = 0;

    for (cp = str; *cp; cp++) {
        if (*cp == '\n')
            output[i++] = '\r';
        output[i++] = *cp;
    }

    output[i++] = '\0';
    
    return (const char *)output;
}

/*
 * This function tokenize a string, just like strtok_r, except that
 * the separator is a string instead of a character set.
 *
 * Parameters:
 *  const char *str The string to tokenize
 *  const char *sep The separator string
 *  char **last     Pointer to state (char *)
 *
 * Returns:
 *  OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise
 */
const char *am_xstrtok(request_rec *r, const char *str,
                       const char *sep, char **last)
{
    char *s;
    char *np;

    /* Resume */
    if (str != NULL)
        s = apr_pstrdup(r->pool, str);
    else
        s = *last;

    /* End of string */
    if (*s == '\0')
        return NULL;

    /* Next sep exists? */
    if ((np = strstr(s, sep)) == NULL) {
        *last = s + strlen(s);
    } else {
        *last = np + strlen(sep);
        memset(np, 0, strlen(sep));
    }

    return s;
}

/* This function strips leading spaces and tabs from a string
 *
 * Parameters:
 *  const char **s       Pointer to the string
 *
 */
void am_strip_blank(const char **s)
{
    while ((**s == ' ') || (**s == '\t'))
        (*s)++;
    return;
}

/* This function extracts a MIME header from a MIME section
 *
 * Parameters:
 *  request_rec *r        The request
 *  const char *m         The MIME section
 *  const char *h         The header to extract (case insensitive)
 *
 * Returns:
 *  The header value, or NULL on failure.
 */
const char *am_get_mime_header(request_rec *r, const char *m, const char *h) 
{
    const char *line;
    char *l1;
    const char *value;
    char *l2;

    for (line = am_xstrtok(r, m, "\n", &l1); line && *line; 
         line = am_xstrtok(r, NULL, "\n", &l1)) {

        am_strip_blank(&line);

        if (((value = am_xstrtok(r, line, ":", &l2)) != NULL) &&
            (strcasecmp(value, h) == 0)) {
            if ((value = am_xstrtok(r, NULL, ":", &l2)) != NULL)
                am_strip_blank(&value);
            return value;
        }
   }
   return NULL;
}

/* This function extracts an attribute from a header 
 *
 * Parameters:
 *  request_rec *r        The request
 *  const char *h         The header
 *  const char *v         Optional header value to check (case insensitive)
 *  const char *a         Optional attribute to extract (case insensitive)
 *
 * Returns:
 *   if i was provided, item value, or NULL on failure.
 *   if i is NULL, the whole header, or NULL on failure. This is
 *   useful for testing v.
 */
const char *am_get_header_attr(request_rec *r, const char *h,
                               const char *v, const char *a) 
{
    const char *value;
    const char *attr;
    char *l1;
    const char *attr_value = NULL;

    /* Looking for 
     * header-value; item_name="item_value"\n 
     */
    if ((value = am_xstrtok(r, h, ";", &l1)) == NULL)
        return NULL;
    am_strip_blank(&value);

    /* If a header value was provided, check it */ 
    if ((v != NULL) && (strcasecmp(value, v) != 0))
        return NULL;

    /* If no attribute name is provided, return everything */
    if (a == NULL)
        return h;

    while ((attr = am_xstrtok(r, NULL, ";", &l1)) != NULL) {
        const char *attr_name = NULL;
        char *l2;

        am_strip_blank(&attr);

        attr_name = am_xstrtok(r, attr, "=", &l2); 
        if ((attr_name != NULL) && (strcasecmp(attr_name, a) == 0)) {
            if ((attr_value = am_xstrtok(r, NULL, "=", &l2)) != NULL)
                am_strip_blank(&attr_value);
            break;
        }
    }
  
    /* Remove leading and trailing quotes */
    if (attr_value != NULL) {
        apr_size_t len; 

        len = strlen(attr_value);
        if ((len > 1) && (attr_value[len - 1] == '\"'))
            attr_value = apr_pstrndup(r->pool, attr_value, len - 1);
        if (attr_value[0] == '\"')
            attr_value++;
    }
    
    return attr_value;
}

/* This function checks for a header name/value existence
 *
 * Parameters:
 *  request_rec *r        The request
 *  const char *h         The header (case insensitive)
 *  const char *v         Optional header value to check (case insensitive)
 *
 * Returns:
 *   0 if header does not exists or does not has the value, 1 otherwise
 */
int am_has_header(request_rec *r, const char *h, const char *v)
{
    return (am_get_header_attr(r, h, v, NULL) != NULL);
}

/* This function extracts the body from a MIME section
 *
 * Parameters:
 *  request_rec *r        The request
 *  const char *mime      The MIME section
 *
 * Returns:
 *  The MIME section body, or NULL on failure.
 */
const char *am_get_mime_body(request_rec *r, const char *mime) 
{
    const char lflf[] = "\n\n";
    const char *body;
    apr_size_t body_len;

    if ((body = strstr(mime, lflf)) == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No MIME body");
        return NULL;
    }

    body += strlen(lflf);

    /* Strip tralling \n */
    if ((body_len = strlen(body)) >= 1) {
        if (body[body_len - 1] == '\n') 
            body = apr_pstrmemdup(r->pool, body, body_len - 1);
    }

    /* Turn back LF into CRLF */
    return am_add_cr(r, body);
}

/* This function returns the URL for a given provider service (type + method)
 *
 * Parameters:
 *  request_rec *r        The request
 *  LassoProfile *profile Login profile
 *  char *endpoint_name   Service and method as specified in metadata
 *                        e.g.: "SingleSignOnService HTTP-Redirect"
 * Returns:
 *  The endpoint URL that must be freed by caller, or NULL on failure.
 */
char *
am_get_service_url(request_rec *r, LassoProfile *profile, char *service_name)
{
    LassoProvider *provider;
    gchar *url;

    provider = lasso_server_get_provider(profile->server, 
                                         profile->remote_providerID);
    if (LASSO_IS_PROVIDER(provider) == FALSE) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r,
                      "Cannot find provider service %s, no provider.",
                      service_name);
	return NULL;
    }

    url = lasso_provider_get_metadata_one(provider, service_name);
    if (url == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r,
                      "Cannot find provider service %s from metadata.",
                      service_name);
	return NULL;
    }

    return url;
}

/*------------------------ Begin Token Parsing Code --------------------------*/

typedef enum {
    TOKEN_WHITESPACE = 1,
    TOKEN_SEMICOLON,
    TOKEN_COMMA,
    TOKEN_EQUAL,
    TOKEN_IDENTIFIER,
    TOKEN_DBL_QUOTE_STRING,
} TokenType;

typedef struct {
    TokenType type;             /* The type of this token */
    char *str;                  /* The string value of the token */
    apr_size_t len;             /* The number of characters in the token */
    apr_size_t offset;          /* The offset from the beginning of
                                   the string to the start of the token */
} Token;


#ifdef DEBUG
/* Return string representation of TokenType enumeration
 *
 * Parameters:
 *  token_type  A TokenType enumeration
 * Returns:     String name of token_type
 */
static const char *
token_type_str(TokenType token_type)
{
    switch(token_type) {
    case TOKEN_WHITESPACE:       return "WHITESPACE";
    case TOKEN_SEMICOLON:        return "SEMICOLON";
    case TOKEN_COMMA:            return "COMMA";
    case TOKEN_EQUAL:            return "EQUAL";
    case TOKEN_IDENTIFIER:       return "IDENTIFIER";
    case TOKEN_DBL_QUOTE_STRING: return "DBL_QUOTE_STRING";
    default:                     return "unknown";
    }
}

static void dump_tokens(request_rec *r, apr_array_header_t *tokens)
{
    apr_size_t i;
    
    for (i = 0; i < tokens->nelts; i++) {
        Token token = APR_ARRAY_IDX(tokens, i, Token);
        AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "token[%2zd] %s \"%s\" offset=%lu len=%lu ", i,
                      token_type_str(token.type), token.str,
                      token.offset, token.len);
    }
}
#endif


/* Initialize token and add to list of tokens
 *
 * Utility to assist tokenize function.
 *
 * A token object is created and added to the end of the list of
 * tokens. It is initialized with the type of token, a copy of the
 * string, it's length, and it's offset from the beginning of the
 * string where it was found.
 *
 * Tokens with special processing needs are also handled here.
 *
 * A double quoted string will:
 *
 * * Have it's delimiting quotes removed.
 * * Will unescape escaped characters.
 *
 * Parameters:
 *  tokens  Array of Token objects.
 *  type    The type of the token (e.g. TokenType).
 *  str     The string the token was parsed from, used to compute
 *          the position of the token in the original string.
 *  start   The first character in the token.
 *  end     the last character in the token.
 */
static inline void
push_token(apr_array_header_t *tokens, TokenType type, const char *str,
           const char *start, const char *end)
{
    apr_size_t offset = start - str;
    Token *token = apr_array_push(tokens);

    if (type == TOKEN_DBL_QUOTE_STRING) {
        /* do not include quotes in token value */
        start++; end--;
    }

    token->type = type;
    token->len = end - start;
    token->offset = offset;
    token->str = apr_pstrmemdup(tokens->pool, start, token->len);

    if (type == TOKEN_DBL_QUOTE_STRING) {
        /*
         * The original HTTP 1.1 spec was ambiguous with respect to
         * backslash quoting inside double quoted strings. This has since
         * been resolved in this errata:
         *
         * http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-16.html#rfc.section.3.2.3
         *
         * Which states:
         *
         * Recipients that process the value of the quoted-string MUST
         * handle a quoted-pair as if it were replaced by the octet
         * following the backslash.
         *
         * Senders SHOULD NOT escape octets in quoted-strings that do not
         * require escaping (i.e., other than DQUOTE and the backslash
         * octet).
         */
        char *p, *t;

        for (p = token->str; *p; p++) {
            if (p[0] == '\\' && p[1]) {
                /*
                 * Found backslash with following character.
                 * Move rest of string down 1 character.
                 */
                for (t = p; *t; t++) {
                    t[0] = t[1];
                }
                token->len--;
            }
        }
    }
}

/* Break a string into a series of tokens
 *
 * Given a string return an array of tokens. If the string cannot be
 * successfully parsed an error string is returned at the location
 * specified by the error parameter, if error is NULL then the parsing
 * was successful. If an error occured the returned array of tokens
 * will include all tokens parsed up until where the unrecognized
 * input occurred. The input str is never modified.
 *
 * Parameters:
 *  pool              memory allocation pool
 *  str               input string to be parsed.
 *  ignore_whitespace if True whitespace tokens are not returned
 *  error             location where error string is returned
 *                    if NULL no error occurred
 * Returns:           array of Token objects
 */
static apr_array_header_t *
tokenize(apr_pool_t *pool, const char *str, bool ignore_whitespace,
             char **error)
{
    apr_array_header_t *tokens = apr_array_make(pool, 10, sizeof(Token));
    const char *p, *start;

    *error = NULL;
    p = start = str;
    while(*p) {
        if (apr_isspace(*p)) {  /* whitespace */
            p++;
            while(*p && apr_isspace(*p)) p++;
            if (!ignore_whitespace) {
                push_token(tokens, TOKEN_WHITESPACE, str, start, p);
            }
            start = p;
        }
        else if (apr_isalpha(*p)) { /* identifier: must begin with
                                       alpha then any alphanumeric or
                                       underscore */
            p++;
            while(*p && (apr_isalnum(*p) || *p == '_')) p++;
            push_token(tokens, TOKEN_IDENTIFIER, str, start, p);
            start = p;
        }
        else if (*p == '"') {   /* double quoted string */
            p++;                /* step over double quote */
            while(*p) {
                if (*p == '\\') { /* backslash escape */
                    p++;          /* step over backslash */
                    if (*p) {
                        p++;      /* step over escaped character */
                    } else {
                        break;    /* backslash at end of string, stop */
                    }
                }
                if (*p == '\"') break; /* terminating quote delimiter */
                p++;                   /* keep scanning */
            }
            if (*p != '\"') {
                *error = apr_psprintf(pool,
                                      "unterminated string beginning at "
                                      "position %" APR_SIZE_T_FMT " in \"%s\"",
                                      start-str, str);
                break;
            }
            p++;
            push_token(tokens, TOKEN_DBL_QUOTE_STRING, str, start, p);
            start = p;
        }
        else if (*p == '=') {   /* equals */
            p++;
            push_token(tokens, TOKEN_EQUAL, str, start, p);
            start = p;
        }
        else if (*p == ',') {   /* comma */
            p++;
            push_token(tokens, TOKEN_COMMA, str, start, p);
            start = p;
        }
        else if (*p == ';') {   /* semicolon */
            p++;
            push_token(tokens, TOKEN_SEMICOLON, str, start, p);
            start = p;
        }
        else {                  /* unrecognized token */
            *error = apr_psprintf(pool,
                                  "unknown token at "
                                  "position %" APR_SIZE_T_FMT " in string \"%s\"",
                                  p-str, str);
            break;
        }
    }

    return tokens;
}

/* Test if the token is what we're looking for
 *
 * Given an index into the tokens array determine if the token type
 * matches. If the value parameter is non-NULL then the token's value
 * must also match. If the array index is beyond the last array item
 * false is returned.
 *
 * Parameters:
 *  tokens  Array of Token objects
 *  index   Index used to select the Token object from the Tokens array.
 *          If the index is beyond the last array item False is returned.
 *  type    The token type which must match
 *  value   If non-NULL then the token string value must be equal to this.
 * Returns: True if the token matches, False otherwise.
 */

static bool
is_token(apr_array_header_t *tokens, apr_size_t index, TokenType type, const char *value)
{
    if (index >= tokens->nelts) {
        return false;
    }

    Token token = APR_ARRAY_IDX(tokens, index, Token);

    if (token.type != type) {
        return false;
    }

    if (value) {
        if (!g_str_equal(token.str, value)) {
            return false;
        }
    }

    return true;
}

/*------------------------- End Token Parsing Code ---------------------------*/

/* Return message describing position an error when parsing.
 *
 * When parsing we expect tokens to appear in a certain sequence.  We
 * report the contents of the unexpected token and it's position in
 * the string. However if the parsing error is due to the fact we've
 * exhausted all tokens but are still expecting another token then our
 * error message indicates we reached the end of the string.
 *
 * Parameters:
 *  tokens  Array of Token objects.
 *  index   Index in tokens array where bad token was found
 */
static inline const char *
parse_error_msg(apr_array_header_t *tokens, apr_size_t index)
{
    if (index >= tokens->nelts) {
        return "end of string";
    }

    return apr_psprintf(tokens->pool, "\"%s\" at position %" APR_SIZE_T_FMT,
                        APR_ARRAY_IDX(tokens, index, Token).str,
                        APR_ARRAY_IDX(tokens, index, Token).offset);
}

/* This function checks if an HTTP PAOS header is valid and
 * returns any service options which may have been specified.
 *
 * A PAOS header is composed of a mandatory PAOS version and service
 * values. A semicolon separates the version from the service values.
 *
 * Service values are delimited by semicolons, and options are
 * comma-delimited from the service value and each other.
 *
 * The PAOS version must be in the form ver="xxx" (note the version
 * string must be in double quotes).
 *
 * The ECP service must be specified, it MAY be followed by optional
 * comma seperated options, all values must be in double quotes.
 *
 * ECP Service
 *   "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
 *
 * Recognized Options:
 *
 * Support for channel bindings
 *  urn:oasis:names:tc:SAML:protocol:ext:channel-binding
 *
 * Support for Holder-of-Key subject confirmation
 *   urn:oasis:names:tc:SAML:2.0:cm:holder-of-key
 *
 * Request for signed SAML request
 *   urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp:2.0:WantAuthnRequestsSigned
 *
 * Request to delegate credentials to the service provider
 *   urn:oasis:names:tc:SAML:2.0:conditions:delegation
 *
 *
 * Example PAOS HTTP header::
 *
 *   PAOS: ver="urn:liberty:paos:2003-08";
 *     "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp",
 *     "urn:oasis:names:tc:SAML:protocol:ext:channel-binding",
 *     "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key"
 *
 * Parameters:
 *  request_rec *r              The request
 *  const char *header          The PAOS header value
 *  ECPServiceOptions *options_return
 *                              Pointer to location to receive options,
 *                              may be NULL. Bitmask of option flags.
 *
 * Returns:
 *   true if the PAOS header is valid, false otherwise. If options is non-NULL
 *   then the set of option flags is returned there.
 *
 */
bool am_parse_paos_header(request_rec *r, const char *header,
                             ECPServiceOptions *options_return)
{
    bool result = false;
    ECPServiceOptions options = 0;
    apr_array_header_t *tokens;
    apr_size_t i;
    char *error;

    AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "PAOS header: \"%s\"", header);

    tokens = tokenize(r->pool, header, true, &error);

#ifdef DEBUG
    dump_tokens(r, tokens);
#endif

    if (error) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "%s", error);
        goto cleanup;
    }

    /* Header must begin with "ver=xxx" where xxx is paos version */
    if (!is_token(tokens, 0, TOKEN_IDENTIFIER, "ver") ||
        !is_token(tokens, 1, TOKEN_EQUAL, NULL) ||
        !is_token(tokens, 2, TOKEN_DBL_QUOTE_STRING, LASSO_PAOS_HREF)) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "invalid PAOS header, "
                      "expected header to begin with ver=\"%s\", "
                      "actual header=\"%s\"",
                      LASSO_PAOS_HREF, header);
        goto cleanup;
    }

    /* Next is the service value, separated from the version by a semicolon */
    if (!is_token(tokens, 3, TOKEN_SEMICOLON, NULL)) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                     "invalid PAOS header, "
                     "expected semicolon after PAOS version "
                     "but found %s in header=\"%s\"",
                      parse_error_msg(tokens, 3),
                      header);
        goto cleanup;
    }

    if (!is_token(tokens, 4, TOKEN_DBL_QUOTE_STRING, LASSO_ECP_HREF)) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "invalid PAOS header, "
                      "expected service token to be \"%s\", "
                      "but found %s in header=\"%s\"",
                      LASSO_ECP_HREF,
                      parse_error_msg(tokens, 4),
                      header);
        goto cleanup;
    }

    /* After the service value there may be optional flags separated by commas */

    if (tokens->nelts == 5) {    /* no options */
        result = true;
        goto cleanup;
    }

    /* More tokens after the service value, must be options, iterate over them */
    for (i = 5; i < tokens->nelts; i++) {
        if (!is_token(tokens, i, TOKEN_COMMA, NULL)) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "invalid PAOS header, "
                          "expected comma after PAOS service "
                          "but found %s in header=\"%s\"",
                          parse_error_msg(tokens, i),
                          header);
            goto cleanup;
        }

        if (++i > tokens->nelts) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "invalid PAOS header, "
                          "expected option after comma "
                          "in header=\"%s\"",
                          header);
            goto cleanup;
        }

        Token token = APR_ARRAY_IDX(tokens, i, Token);

        if (token.type != TOKEN_DBL_QUOTE_STRING) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "invalid PAOS header, "
                          "expected quoted string after comma "
                          "but found %s in header=\"%s\"",
                          parse_error_msg(tokens, i),
                          header);
            goto cleanup;
        }

        /* Have an option string, convert it to a bit flag */
        const char *value = token.str;

        if (g_str_equal(value, LASSO_SAML_EXT_CHANNEL_BINDING)) {
            options |= ECP_SERVICE_OPTION_CHANNEL_BINDING;
        } else if (g_str_equal(value, LASSO_SAML2_CONFIRMATION_METHOD_HOLDER_OF_KEY)) {
            options |= ECP_SERVICE_OPTION_HOLDER_OF_KEY;
        } else if (g_str_equal(value, LASSO_SAML2_ECP_PROFILE_WANT_AUTHN_SIGNED)) {
            options |= ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED;
        } else if (g_str_equal(value, LASSO_SAML2_CONDITIONS_DELEGATION)) {
            options |= ECP_SERVICE_OPTION_DELEGATION;
        } else {
            AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r,
                          "Unknown PAOS service option = \"%s\"",
                          value);
            goto cleanup;
        }
    }

    result = true;

 cleanup:
    if (options_return) {
        *options_return = options;
    }
    return result;

}

/* This function checks if Accept header has a media type
 *
 * Given an Accept header value like this:
 *
 * "text/html,application/xhtml+xml,application/xml;q=0.9"
 *
 * Parse the string and find name of each media type, ignore any parameters
 * bound to the name. Test to see if the name matches the input media_type.
 *
 * Parameters:
 *  request_rec *r         The request
 *  const char *header     The header value
 *  const char *media_type media type header value to check (case insensitive)
 *
 * Returns:
 *   true if media type is in header, false otherwise
 */
bool am_header_has_media_type(request_rec *r, const char *header, const char *media_type)
{
    bool result = false;
    char **comma_tokens = NULL;
    char **media_ranges = NULL;
    char *media_range = NULL;

    if (header == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                     "invalid Accept header, NULL");
        goto cleanup;
    }

    /*
     * Split the header into a list of media_range tokens separated by
     * a comma and iterate over the list.
     */
    comma_tokens = g_strsplit(header, ",", 0);
    for (media_ranges = comma_tokens, media_range = *media_ranges;
         media_range;
         media_range = *(++media_ranges)) {
        char **semicolon_tokens = NULL;
        char *name = NULL;

        /*
         * Split the media_range into a name and parameters, each
         * separated by a semicolon. The first element in the list is
         * the media_type name, subsequent params are optional and ignored.
         */
        media_range = g_strstrip(media_range);
        semicolon_tokens = g_strsplit(media_range, ";", 0);

        /*
         * Does the media_type match our required media_type?
         * If so clean up and return success.
         */
        name = g_strstrip(semicolon_tokens[0]);
        if (name && g_str_equal(name, media_type)) {
            result = true;
            g_strfreev(semicolon_tokens);
            goto cleanup;
        }
        g_strfreev(semicolon_tokens);
    }

 cleanup:
    g_strfreev(comma_tokens);
    return result;
}

/*
 * Lookup a config string in a specific language.  If lang is NULL and
 * the config string had been defined without a language qualifier
 * return the unqualified value.  If not found NULL is returned.
 */
const char *am_get_config_langstring(apr_hash_t *h, const char *lang)
{
    char *string;

    if (lang == NULL) {
        lang = "";
    }

    string = (char *)apr_hash_get(h, lang, APR_HASH_KEY_STRING);

    return string;
}

/*
 * Get the value of boolean query parameter.
 *
 * Parameters:
 *  request_rec *r         The request
 *  const char *name       The name of the query parameter
 *  int *return_value      The address of the variable to receive
 *                         the boolean value
 *  int default_value      The value returned if parameter is absent or
 *                          in event of an error
 *
 * Returns:
 *   OK on success, HTTP error otherwise
 *
 * Looks for the named parameter in the query parameters, if found
 * parses the value which must be one of:
 *
 *   * true
 *   * false
 *
 * If value cannot be parsed HTTP_BAD_REQUEST is returned.
 *
 * If not found, or if there is an error, the returned value is set to
 * default_value.
 */

int am_get_boolean_query_parameter(request_rec *r, const char *name,
                                   int *return_value, int default_value)
{
    char *value_str;
    int ret = OK;

    *return_value = default_value;

    value_str = am_extract_query_parameter(r->pool, r->args, name);
    if (value_str != NULL) {
        ret = am_urldecode(value_str);
        if (ret != OK) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Error urldecoding \"%s\" boolean query parameter, "
                          "value=\"%s\"", name, value_str);
            return ret;
        }
        if(!strcmp(value_str, "true")) {
            *return_value = TRUE;
        } else if(!strcmp(value_str, "false")) {
            *return_value = FALSE;
        } else {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Invalid value for \"%s\" boolean query parameter, "
                          "value=\"%s\"", name, value_str);
            ret = HTTP_BAD_REQUEST;
        }
    }

    return ret;
}

/*
 * Get the URL of the AssertionConsumerServer having specific protocol
 * binding.
 *
 * Parameters:
 *  LassoProvider *provider The provider whose endpoints will be scanned.
 *  const char *binding     The required binding short name.
 *
 * Returns:
 *   The endpoint URL or NULL if not found. Must be freed with g_free().
 *
 * Lasso does not provide a public API to select a provider endpoint
 * by binding. The best we can do is iterate over a list of endpoint
 * descriptors and select a matching descriptor.
 *
 * Lasso does not document the format of these descriptor names but
 * essentially a descriptor is a space separated concatenation of the
 * endpoint properties. For SAML2 one can assume it is the endpoint
 * type, optionally followed by the protocol binding name, optionally
 * followd by the index (if the endpoint type is indexed). If the
 * endpoint is a response location then "ResponseLocation" will be
 * appended as the final token. For example here is a list of
 * descriptors returned for a service provider (note they are
 * unordered).
 *
 *    "AssertionConsumerService HTTP-POST 0"
 *    "AuthnRequestsSigned"
 *    "AssertionConsumerService PAOS 2"
 *    "SingleLogoutService HTTP-Redirect"
 *    "SingleLogoutService SOAP"
 *    "AssertionConsumerService HTTP-Artifact 1"
 *    "NameIDFormat"
 *    "SingleLogoutService HTTP-POST ResponseLocation"
 *
 * The possible binding names are:
 *
 *    "SOAP"
 *    "HTTP-Redirect"
 *    "HTTP-POST"
 *    "HTTP-Artifact"
 *    "PAOS"
 *    "URI"
 *
 * We know the AssertionConsumerService is indexed. If there is more
 * than one endpoint with the required binding we select the one with
 * the lowest index assuming it is preferred.
 */

char *am_get_assertion_consumer_service_by_binding(LassoProvider *provider, const char *binding)
{
    GList *descriptors;
    char *url;
    char *selected_descriptor;
    char *descriptor;
    char **tokens;
    guint n_tokens;
    GList *i;
    char *endptr;
    long descriptor_index, min_index;

    url = NULL;
    selected_descriptor = NULL;
    min_index = LONG_MAX;

    /* The descriptor list is unordered */
    descriptors = lasso_provider_get_metadata_keys_for_role(provider,
                                                            LASSO_PROVIDER_ROLE_SP);

    for (i = g_list_first(descriptors), tokens=NULL;
         i;
         i = g_list_next(i), g_strfreev(tokens)) {

        descriptor = i->data;
        descriptor_index = LONG_MAX;

        /*
         * Split the descriptor into tokens, only consider descriptors
         * which have at least 3 tokens and whose first token is
         * AssertionConsumerService
         */

        tokens = g_strsplit(descriptor, " ", 0);
        n_tokens = g_strv_length(tokens);

        if (n_tokens < 3) continue;

        if (!g_str_equal(tokens[0], "AssertionConsumerService")) continue;
        if (!g_str_equal(tokens[1], binding)) continue;

        descriptor_index = strtol(tokens[2], &endptr, 10);
        if (tokens[2] == endptr) continue; /* could not parse int */

        if (descriptor_index < min_index) {
            selected_descriptor = descriptor;
            min_index = descriptor_index;
        }
    }

    if (selected_descriptor) {
        url = lasso_provider_get_metadata_one_for_role(provider,
                                                       LASSO_PROVIDER_ROLE_SP,
                                                       selected_descriptor);
    }

    lasso_release_list_of_strings(descriptors);

    return url;
}


#ifdef HAVE_ECP

/* String representation of ECPServiceOptions bitmask
 *
 * ECPServiceOptions is a bitmask of flags. Return a comma separated string
 * of all the flags. If any bit in the bitmask is unaccounted for an
 * extra string will be appended of the form "(unknown bits = x)".
 *
 * Parameters:
 *  pool    memory allocation pool
 *  options bitmask of PAOS options
 */
char *am_ecp_service_options_str(apr_pool_t *pool, ECPServiceOptions options)
{
    apr_array_header_t *names = apr_array_make(pool, 4, sizeof(const char *));

    if (options & ECP_SERVICE_OPTION_CHANNEL_BINDING) {
        APR_ARRAY_PUSH(names, const char *) = "channel-binding";
        options &= ~ECP_SERVICE_OPTION_CHANNEL_BINDING;
    }

    if (options & ECP_SERVICE_OPTION_HOLDER_OF_KEY) {
        APR_ARRAY_PUSH(names, const char *) = "holder-of-key";
        options &= ~ECP_SERVICE_OPTION_HOLDER_OF_KEY;
    }

    if (options & ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED) {
        APR_ARRAY_PUSH(names, const char *) = "want-authn-signed";
        options &= ~ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED;
    }

    if (options & ECP_SERVICE_OPTION_DELEGATION) {
        APR_ARRAY_PUSH(names, const char *) = "delegation";
        options &= ~ECP_SERVICE_OPTION_DELEGATION;
    }

    if (options) {
        APR_ARRAY_PUSH(names, const char *) =
            apr_psprintf(pool, "(unknown bits = %#x)", options);
    }

    return apr_array_pstrcat(pool, names, ',');
}

/* Determine if request is compatible with PAOS, decode headers
 *
 * To indicate support for the ECP profile, and the PAOS binding, the
 * request MUST include the following HTTP header fields:
 *
 * 1. An Accept header indicating acceptance of the MIME type
 *    "application/vnd.paos+xml"
 *
 * 2. A PAOS header specifying the PAOS version with a value, at minimum, of
 *    "urn:liberty:paos:2003-08" and a supported service value of
 *    "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp". The service value MAY
 *    contain option values.
 *
 * This function validates the Accept header the the PAOS header, if
 * all condidtions are met it returns true, false otherwise. If the
 * validation succeeds any ECP options specified along with the
 * ECP service are parsed and stored in req_cfg->ecp_service_options
 *
 * Any error discovered during processing are returned in the
 * error_code parameter, zero indicates success. This function never
 * returns true if an error occurred.
 *
 * Parameters:
 *  request_rec *r     The current request.
 *  int * error_code   Return error code here
 *
 */
bool am_is_paos_request(request_rec *r, int *error_code)
{
    const char *accept_header = NULL;
    const char *paos_header = NULL;
    bool have_paos_media_type = false;
    bool valid_paos_header = false;
    bool is_paos = false;
    ECPServiceOptions ecp_service_options = 0;

    *error_code = 0;
    accept_header = apr_table_get(r->headers_in, "Accept");
    paos_header = apr_table_get(r->headers_in, "PAOS");
    if (accept_header) {
        if (am_header_has_media_type(r, accept_header, MEDIA_TYPE_PAOS)) {
            have_paos_media_type = true;
        }
    }
    if (paos_header) {
        if (am_parse_paos_header(r, paos_header, &ecp_service_options)) {
            valid_paos_header = true;
        } else {
            if (*error_code == 0)
                *error_code = AM_ERROR_INVALID_PAOS_HEADER;
        }
    }
    if (have_paos_media_type) {
        if (valid_paos_header) {
            is_paos = true;
        } else {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "request supplied PAOS media type in Accept header "
                          "but omitted valid PAOS header");
            if (*error_code == 0)
                *error_code = AM_ERROR_MISSING_PAOS_HEADER;
        }
    } else {
        if (valid_paos_header) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "request supplied valid PAOS header "
                          "but omitted PAOS media type in Accept header");
            if (*error_code == 0)
                *error_code = AM_ERROR_MISSING_PAOS_MEDIA_TYPE;
        }
    }
    AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "have_paos_media_type=%s valid_paos_header=%s is_paos=%s "
                  "error_code=%d ecp options=[%s]",
                  have_paos_media_type ? "True" : "False",
                  valid_paos_header ? "True" : "False",
                  is_paos ? "True" : "False",
                  *error_code,
                  am_ecp_service_options_str(r->pool, ecp_service_options));

    if (is_paos) {
        am_req_cfg_rec *req_cfg;

        req_cfg = am_get_req_cfg(r);
        req_cfg->ecp_service_options = ecp_service_options;
    }

    return is_paos;
}
#endif /* HAVE_ECP */

char *
am_saml_response_status_str(request_rec *r, LassoNode *node)
{
    LassoSamlp2StatusResponse *response = (LassoSamlp2StatusResponse*)node;
    LassoSamlp2Status *status = NULL;
    const char *status_code1 = NULL;
    const char *status_code2 = NULL;

    if (!LASSO_IS_SAMLP2_STATUS_RESPONSE(response)) {
        return apr_psprintf(r->pool,
                            "error, expected LassoSamlp2StatusResponse "
                            "but got %s",
                            lasso_node_get_name((LassoNode*)response));
    }

    status = response->Status;
    if (status == NULL                  ||
        !LASSO_IS_SAMLP2_STATUS(status) ||
        status->StatusCode == NULL      ||
        status->StatusCode->Value == NULL) {
        return apr_psprintf(r->pool, "Status missing");

    }

    status_code1 = status->StatusCode->Value;
    if (status->StatusCode->StatusCode) {
        status_code2 = status->StatusCode->StatusCode->Value;
    }

    return apr_psprintf(r->pool,
                        "StatusCode1=\"%s\", StatusCode2=\"%s\", "
                        "StatusMessage=\"%s\"",
                        status_code1, status_code2, status->StatusMessage);
}