/* ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

/*** Digest authentication ***/

#include <serf.h>
#include <serf_private.h>
#include <auth/auth.h>

#include <apr.h>
#include <apr_base64.h>
#include <apr_strings.h>
#include <apr_uuid.h>
#include <apr_md5.h>

/** Digest authentication, implements RFC 2617. **/

/* TODO: add support for the domain attribute. This defines the protection
   space, so that serf can decide per URI if it should reuse the cached
   credentials for the server, or not. */

/* Stores the context information related to Digest authentication.
   This information is stored in the per server cache in the serf context. */
typedef struct digest_authn_info_t {
    /* nonce-count for digest authentication */
    unsigned int digest_nc;

    const char *header;

    const char *ha1;

    const char *realm;
    const char *cnonce;
    const char *nonce;
    const char *opaque;
    const char *algorithm;
    const char *qop;
    const char *username;

    apr_pool_t *pool;
} digest_authn_info_t;

static char
int_to_hex(int v)
{
    return (v < 10) ? '0' + v : 'a' + (v - 10);
}

/**
 * Convert a string if ASCII characters HASHVAL to its hexadecimal
 * representation.
 *
 * The returned string will be allocated in the POOL and be null-terminated.
 */
static const char *
hex_encode(const unsigned char *hashval,
           apr_pool_t *pool)
{
    int i;
    char *hexval = apr_palloc(pool, (APR_MD5_DIGESTSIZE * 2) + 1);
    for (i = 0; i < APR_MD5_DIGESTSIZE; i++) {
        hexval[2 * i] = int_to_hex((hashval[i] >> 4) & 0xf);
        hexval[2 * i + 1] = int_to_hex(hashval[i] & 0xf);
    }
    hexval[APR_MD5_DIGESTSIZE * 2] = '\0';
    return hexval;
}

/**
 * Returns a 36-byte long string of random characters.
 * UUIDs are formatted as: 00112233-4455-6677-8899-AABBCCDDEEFF.
 *
 * The returned string will be allocated in the POOL and be null-terminated.
 */
static const char *
random_cnonce(apr_pool_t *pool)
{
    apr_uuid_t uuid;
    char *buf = apr_palloc(pool, APR_UUID_FORMATTED_LENGTH + 1);

    apr_uuid_get(&uuid);
    apr_uuid_format(buf, &uuid);

    return hex_encode((unsigned char*)buf, pool);
}

static apr_status_t
build_digest_ha1(const char **out_ha1,
                 const char *username,
                 const char *password,
                 const char *realm_name,
                 apr_pool_t *pool)
{
    const char *tmp;
    unsigned char ha1[APR_MD5_DIGESTSIZE];
    apr_status_t status;

    /* calculate ha1:
       MD5 hash of the combined user name, authentication realm and password */
    tmp = apr_psprintf(pool, "%s:%s:%s",
                       username,
                       realm_name,
                       password);
    status = apr_md5(ha1, tmp, strlen(tmp));
    if (status)
        return status;

    *out_ha1 = hex_encode(ha1, pool);

    return APR_SUCCESS;
}

static apr_status_t
build_digest_ha2(const char **out_ha2,
                 const char *uri,
                 const char *method,
                 const char *qop,
                 apr_pool_t *pool)
{
    if (!qop || strcmp(qop, "auth") == 0) {
        const char *tmp;
        unsigned char ha2[APR_MD5_DIGESTSIZE];
        apr_status_t status;

        /* calculate ha2:
           MD5 hash of the combined method and URI */
        tmp = apr_psprintf(pool, "%s:%s",
                           method,
                           uri);
        status = apr_md5(ha2, tmp, strlen(tmp));
        if (status)
            return status;

        *out_ha2 = hex_encode(ha2, pool);

        return APR_SUCCESS;
    } else {
        /* TODO: auth-int isn't supported! */
        return APR_ENOTIMPL;
    }
}

static apr_status_t
build_auth_header(const char **out_header,
                  digest_authn_info_t *digest_info,
                  const char *path,
                  const char *method,
                  apr_pool_t *pool)
{
    char *hdr;
    const char *ha2;
    const char *response;
    unsigned char response_hdr[APR_MD5_DIGESTSIZE];
    const char *response_hdr_hex;
    apr_status_t status;

    status = build_digest_ha2(&ha2, path, method, digest_info->qop, pool);
    if (status)
        return status;

    hdr = apr_psprintf(pool,
                       "Digest realm=\"%s\","
                       " username=\"%s\","
                       " nonce=\"%s\","
                       " uri=\"%s\"",
                       digest_info->realm, digest_info->username,
                       digest_info->nonce,
                       path);

    if (digest_info->qop) {
        if (! digest_info->cnonce)
            digest_info->cnonce = random_cnonce(digest_info->pool);

        hdr = apr_psprintf(pool, "%s, nc=%08x, cnonce=\"%s\", qop=\"%s\"",
                           hdr,
                           digest_info->digest_nc,
                           digest_info->cnonce,
                           digest_info->qop);

        /* Build the response header:
           MD5 hash of the combined HA1 result, server nonce (nonce),
           request counter (nc), client nonce (cnonce),
           quality of protection code (qop) and HA2 result. */
        response = apr_psprintf(pool, "%s:%s:%08x:%s:%s:%s",
                                digest_info->ha1, digest_info->nonce,
                                digest_info->digest_nc,
                                digest_info->cnonce, digest_info->qop, ha2);
    } else {
        /* Build the response header:
           MD5 hash of the combined HA1 result, server nonce (nonce)
           and HA2 result. */
        response = apr_psprintf(pool, "%s:%s:%s",
                                digest_info->ha1, digest_info->nonce, ha2);
    }

    status = apr_md5(response_hdr, response, strlen(response));
    if (status)
        return status;

    response_hdr_hex = hex_encode(response_hdr, pool);

    hdr = apr_psprintf(pool, "%s, response=\"%s\"", hdr, response_hdr_hex);

    if (digest_info->opaque) {
        hdr = apr_psprintf(pool, "%s, opaque=\"%s\"", hdr,
                           digest_info->opaque);
    }
    if (digest_info->algorithm) {
        hdr = apr_psprintf(pool, "%s, algorithm=\"%s\"", hdr,
                           digest_info->algorithm);
    }

    *out_header = hdr;

    return APR_SUCCESS;
}

/* Implements serf__auth_handler_func_t callback. */
static apr_status_t
serf__handle_digest_auth(const serf__authn_scheme_t *scheme,
                         int code,
                         serf_request_t *request,
                         serf_bucket_t *response,
                         const char *auth_hdr,
                         const char *auth_attr,
                         apr_pool_t *pool)
{
    char *attrs;
    char *nextkv;
    const char *realm, *realm_name = NULL;
    const char *nonce = NULL;
    const char *algorithm = NULL;
    const char *qop = NULL;
    const char *opaque = NULL;
    const char *key;
    serf_connection_t *conn = request->conn;
    serf_context_t *ctx = conn->ctx;
    serf__authn_info_t *authn_info;
    digest_authn_info_t *digest_info;
    apr_status_t status;
    apr_pool_t *cred_pool;
    char *username, *password;

    /* Can't do Digest authentication if there's no callback to get
       username & password. */
    if (!ctx->cred_cb) {
        return SERF_ERROR_AUTHN_FAILED;
    }

    if (code == 401) {
        authn_info = serf__get_authn_info_for_server(conn);
    } else {
        authn_info = &ctx->proxy_authn_info;
    }
    digest_info = authn_info->baton;

    /* Need a copy cuz we're going to write NUL characters into the string.  */
    attrs = apr_pstrdup(pool, auth_attr);

    /* We're expecting a list of key=value pairs, separated by a comma.
       Ex. realm="SVN Digest",
       nonce="f+zTl/leBAA=e371bd3070adfb47b21f5fc64ad8cc21adc371a5",
       algorithm=MD5, qop="auth" */
    for ( ; (key = apr_strtok(attrs, ",", &nextkv)) != NULL; attrs = NULL) {
        char *val;

        val = strchr(key, '=');
        if (val == NULL)
            continue;
        *val++ = '\0';

        /* skip leading spaces */
        while (*key == ' ')
            key++;

        /* If the value is quoted, then remove the quotes.  */
        if (*val == '"') {
            apr_size_t last = strlen(val) - 1;

            if (val[last] == '"') {
                val[last] = '\0';
                val++;
            }
        }

        if (strcmp(key, "realm") == 0)
            realm_name = val;
        else if (strcmp(key, "nonce") == 0)
            nonce = val;
        else if (strcmp(key, "algorithm") == 0)
            algorithm = val;
        else if (strcmp(key, "qop") == 0)
            qop = val;
        else if (strcmp(key, "opaque") == 0)
            opaque = val;

        /* Ignore all unsupported attributes. */
    }

    if (!realm_name) {
        return SERF_ERROR_AUTHN_MISSING_ATTRIBUTE;
    }

    realm = serf__construct_realm(code == 401 ? HOST : PROXY,
                                  conn, realm_name,
                                  pool);

    /* Ask the application for credentials */
    apr_pool_create(&cred_pool, pool);
    status = serf__provide_credentials(ctx,
                                       &username, &password,
                                       request,
                                       code, scheme->name,
                                       realm, cred_pool);
    if (status) {
        apr_pool_destroy(cred_pool);
        return status;
    }

    digest_info->header = (code == 401) ? "Authorization" :
                                          "Proxy-Authorization";

    /* Store the digest authentication parameters in the context cached for
       this server in the serf context, so we can use it to create the
       Authorization header when setting up requests on the same or different
       connections (e.g. in case of KeepAlive off on the server).
       TODO: we currently don't cache this info per realm, so each time a request
       'switches realms', we have to ask the application for new credentials. */
    digest_info->pool = conn->pool;
    digest_info->qop = apr_pstrdup(digest_info->pool, qop);
    digest_info->nonce = apr_pstrdup(digest_info->pool, nonce);
    digest_info->cnonce = NULL;
    digest_info->opaque = apr_pstrdup(digest_info->pool, opaque);
    digest_info->algorithm = apr_pstrdup(digest_info->pool, algorithm);
    digest_info->realm = apr_pstrdup(digest_info->pool, realm_name);
    digest_info->username = apr_pstrdup(digest_info->pool, username);
    digest_info->digest_nc++;

    status = build_digest_ha1(&digest_info->ha1, username, password,
                              digest_info->realm, digest_info->pool);

    apr_pool_destroy(cred_pool);

    /* If the handshake is finished tell serf it can send as much requests as it
       likes. */
    serf__connection_set_pipelining(conn, 1);

    return status;
}

/* Implements serf__init_conn_func_t callback. */
static apr_status_t
serf__init_digest_connection(const serf__authn_scheme_t *scheme,
                             int code,
                             serf_connection_t *conn,
                             apr_pool_t *pool)
{
    serf_context_t *ctx = conn->ctx;
    serf__authn_info_t *authn_info;

    if (code == 401) {
        authn_info = serf__get_authn_info_for_server(conn);
    } else {
        authn_info = &ctx->proxy_authn_info;
    }

    if (!authn_info->baton) {
        authn_info->baton = apr_pcalloc(pool, sizeof(digest_authn_info_t));
    }

    /* Make serf send the initial requests one by one */
    serf__connection_set_pipelining(conn, 0);

    return APR_SUCCESS;
}

/* Implements serf__setup_request_func_t callback. */
static apr_status_t
serf__setup_request_digest_auth(const serf__authn_scheme_t *scheme,
                                peer_t peer,
                                int code,
                                serf_connection_t *conn,
                                serf_request_t *request,
                                const char *method,
                                const char *uri,
                                serf_bucket_t *hdrs_bkt)
{
    serf_context_t *ctx = conn->ctx;
    serf__authn_info_t *authn_info;
    digest_authn_info_t *digest_info;
    apr_status_t status;

    if (peer == HOST) {
        authn_info = serf__get_authn_info_for_server(conn);
    } else {
        authn_info = &ctx->proxy_authn_info;
    }
    digest_info = authn_info->baton;

    if (digest_info && digest_info->realm) {
        const char *value;
        const char *path;

        /* TODO: per request pool? */

        /* for request 'CONNECT serf.googlecode.com:443', the uri also should be
           serf.googlecode.com:443. apr_uri_parse can't handle this, so special
           case. */
        if (strcmp(method, "CONNECT") == 0)
            path = uri;
        else {
            apr_uri_t parsed_uri;

            /* Extract path from uri. */
            status = apr_uri_parse(conn->pool, uri, &parsed_uri);
            if (status)
                return status;

            path = parsed_uri.path;
        }

        /* Build a new Authorization header. */
        digest_info->header = (peer == HOST) ? "Authorization" :
            "Proxy-Authorization";
        status = build_auth_header(&value, digest_info, path, method,
                                   conn->pool);
        if (status)
            return status;

        serf_bucket_headers_setn(hdrs_bkt, digest_info->header,
                                 value);
        digest_info->digest_nc++;

        /* Store the uri of this request on the serf_request_t object, to make
           it available when validating the Authentication-Info header of the
           matching response. */
        request->auth_baton = (void *)path;
    }

    return APR_SUCCESS;
}

/* Implements serf__validate_response_func_t callback. */
static apr_status_t
serf__validate_response_digest_auth(const serf__authn_scheme_t *scheme,
                                    peer_t peer,
                                    int code,
                                    serf_connection_t *conn,
                                    serf_request_t *request,
                                    serf_bucket_t *response,
                                    apr_pool_t *pool)
{
    const char *key;
    char *auth_attr;
    char *nextkv;
    const char *rspauth = NULL;
    const char *qop = NULL;
    const char *nc_str = NULL;
    serf_bucket_t *hdrs;
    serf_context_t *ctx = conn->ctx;
    apr_status_t status;

    hdrs = serf_bucket_response_get_headers(response);

    /* Need a copy cuz we're going to write NUL characters into the string.  */
    if (peer == HOST)
        auth_attr = apr_pstrdup(pool,
            serf_bucket_headers_get(hdrs, "Authentication-Info"));
    else
        auth_attr = apr_pstrdup(pool,
            serf_bucket_headers_get(hdrs, "Proxy-Authentication-Info"));

    /* If there's no Authentication-Info header there's nothing to validate. */
    if (! auth_attr)
        return APR_SUCCESS;

    /* We're expecting a list of key=value pairs, separated by a comma.
       Ex. rspauth="8a4b8451084b082be6b105e2b7975087",
       cnonce="346531653132652d303033392d3435", nc=00000007,
       qop=auth */
    for ( ; (key = apr_strtok(auth_attr, ",", &nextkv)) != NULL; auth_attr = NULL) {
        char *val;

        val = strchr(key, '=');
        if (val == NULL)
            continue;
        *val++ = '\0';

        /* skip leading spaces */
        while (*key == ' ')
            key++;

        /* If the value is quoted, then remove the quotes.  */
        if (*val == '"') {
            apr_size_t last = strlen(val) - 1;

            if (val[last] == '"') {
                val[last] = '\0';
                val++;
            }
        }

        if (strcmp(key, "rspauth") == 0)
            rspauth = val;
        else if (strcmp(key, "qop") == 0)
            qop = val;
        else if (strcmp(key, "nc") == 0)
            nc_str = val;
    }

    if (rspauth) {
        const char *ha2, *tmp, *resp_hdr_hex;
        unsigned char resp_hdr[APR_MD5_DIGESTSIZE];
        const char *req_uri = request->auth_baton;
        serf__authn_info_t *authn_info;
        digest_authn_info_t *digest_info;

        if (peer == HOST) {
            authn_info = serf__get_authn_info_for_server(conn);
        } else {
            authn_info = &ctx->proxy_authn_info;
        }
        digest_info = authn_info->baton;

        status = build_digest_ha2(&ha2, req_uri, "", qop, pool);
        if (status)
            return status;

        tmp = apr_psprintf(pool, "%s:%s:%s:%s:%s:%s",
                           digest_info->ha1, digest_info->nonce, nc_str,
                           digest_info->cnonce, digest_info->qop, ha2);
        apr_md5(resp_hdr, tmp, strlen(tmp));
        resp_hdr_hex =  hex_encode(resp_hdr, pool);

        /* Incorrect response-digest in Authentication-Info header. */
        if (strcmp(rspauth, resp_hdr_hex) != 0) {
            return SERF_ERROR_AUTHN_FAILED;
        }
    }

    return APR_SUCCESS;
}

const serf__authn_scheme_t serf__digest_authn_scheme = {
    "Digest",
    "digest",
    SERF_AUTHN_DIGEST,
    serf__init_digest_connection,
    serf__handle_digest_auth,
    serf__setup_request_digest_auth,
    serf__validate_response_digest_auth,
};
