/*
 *
 *   mod_auth_mellon.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 "auth_mellon.h"

#include <curl/curl.h>

/* The size of the blocks we will allocate. */
#define AM_HC_BLOCK_SIZE 1000

#ifdef APLOG_USE_MODULE
APLOG_USE_MODULE(auth_mellon);
#endif

/* This structure describes a single-linked list of downloaded blocks. */
typedef struct am_hc_block_s {
    /* The next block we have allocated. */
    struct am_hc_block_s *next;

    /* The number of bytes written to this block. */
    apr_size_t used;

    /* The data stored in this block. */
    uint8_t data[AM_HC_BLOCK_SIZE];
} am_hc_block_t;


/* This structure describes a header for the block list. */
typedef struct {
    /* The pool we will allocate memory for new blocks from. */
    apr_pool_t *pool;

    /* The first block in the linked list of blocks. */
    am_hc_block_t *first;

    /* The last block in the linked list of blocks. */
    am_hc_block_t *last;
} am_hc_block_header_t;


/* This function allocates and initializes a block for data copying.
 *
 * Parameters:
 *  apr_pool_t *pool     The pool we should allocate the block from.
 *
 * Returns:
 *  The new block we allocated.
 */
static am_hc_block_t *am_hc_block_alloc(apr_pool_t *pool)
{
    am_hc_block_t *blk;

    blk = (am_hc_block_t *)apr_palloc(pool, sizeof(am_hc_block_t));

    blk->next = NULL;
    blk->used = 0;

    return blk;
}


/* This function adds data to the end of a block, and allocates new blocks
 * if the data doesn't fit in one block.
 *
 * Parameters:
 *  am_hc_block_t *block   The block we should begin by appending data to.
 *  apr_pool_t *pool       The pool we should allocate memory for new blocks
 *                         from.
 *  const uint8_t *data    The data we should append to the blocks.
 *  apr_size_t size        The length of the data we should append.
 *
 * Returns:
 *  The last block written to (i.e. the next block we should write to).
 */
static am_hc_block_t *am_hc_block_write(
    am_hc_block_t *block,
    apr_pool_t *pool,
    const uint8_t *data, apr_size_t size
    )
{
    apr_size_t num_cpy;

    while(size > 0) {
        /* Find the number of bytes we should write to this block. */
        num_cpy = AM_HC_BLOCK_SIZE - block->used;
        if(num_cpy == 0) {
            /* This block is full -- allocate a new block. */
            block->next = am_hc_block_alloc(pool);
            block = block->next;
            num_cpy = AM_HC_BLOCK_SIZE;
        }
        if(num_cpy > size) {
            num_cpy = size;
        }

        /* Copy data to this block. */
        memcpy(&block->data[block->used], data, num_cpy);
        block->used += num_cpy;

        size -= num_cpy;
        data += num_cpy;
    }

    /* The next write should be to this block. */
    return block;
}


/* This function initializes a am_hc_block_header_t structure, which
 * contains information about the linked list of data blocks.
 *
 * Parameters:
 *  am_hc_block_header_t *bh   Pointer to the data header whcih we
 *                             should initialize.
 *  apr_pool_t *pool           The pool we should allocate data from.
 *
 * Returns:
 *  Nothing.
 */
static void am_hc_block_header_init(am_hc_block_header_t *bh,
                                    apr_pool_t *pool)
{
    bh->pool = pool;

    bh->first = am_hc_block_alloc(pool);
    bh->last = bh->first;
}


/* This function writes data to the linked list of blocks identified by
 * the stream-parameter. It matches the prototype required by curl.
 *
 * Parameters:
 *  void *data           The data that should be written. It is size*nmemb
 *                       bytes long.
 *  size_t size          The size of each block of data that should
 *                       be written.
 *  size_t nmemb         The number of blocks of data that should be written.
 *  void *block_header   A pointer to a am_hc_block_header_t structure which
 *                       identifies the linked list we should store data in.
 *
 * Returns:
 *  The number of bytes that have been written.
 */
static size_t am_hc_data_write(void *data, size_t size, size_t nmemb,
                               void *data_header)
{
    am_hc_block_header_t *bh;

    bh = (am_hc_block_header_t *)data_header;

    bh->last = am_hc_block_write(bh->last, bh->pool, (const uint8_t *)data,
                                 size * nmemb);

    return size * nmemb;
}


/* This function fetches the data which was written to the databuffers
 * in the linked list which the am_hc_data_t structure keeps track of.
 *
 * Parameters:
 *  am_hc_block_header_t *bh   The header telling us which data buffers
 *                             we should extract data from.
 *  apr_pool_t *pool           The pool we should allocate the data
 *                             buffer from.
 *  void **buffer              A pointer to where we should store a pointer
 *                             to the data buffer we allocate. We will
 *                             always add a null-terminator to the end of
 *                             data buffer. This parameter can't be NULL.
 *  apr_size_t *size           This is a pointer to where we will store the
 *                             length of the data, not including the
 *                             null-terminator we add. This parameter can
 *                             be NULL.
 *
 * Returns:
 *  Nothing.
 */
static void am_hc_data_extract(am_hc_block_header_t *bh, apr_pool_t *pool,
                               void **buffer, apr_size_t *size)
{
    am_hc_block_t *blk;
    apr_size_t length;
    uint8_t *buf;
    apr_size_t pos;

    /* First we find the length of the data. */
    length = 0;
    for(blk = bh->first; blk != NULL; blk = blk->next) {
        length += blk->used;
    }

    /* Allocate memory for the data. Add one to the size in order to
     *  have space for the null-terminator.
     */
    buf = (uint8_t *)apr_palloc(pool, length + 1);

    /* Copy the data into the buffer. */
    pos = 0;
    for(blk = bh->first; blk != NULL; blk = blk->next) {
        memcpy(&buf[pos], blk->data, blk->used);
        pos += blk->used;
    }

    /* Add the null-terminator. */
    buf[length] = 0;

    /* Set up the return values. */
    *buffer = (void *)buf;
    if(size != NULL) {
        *size = length;
    }
}


/* This function creates a curl object and performs generic initialization
 * of it.
 *
 * Parameters:
 *  request_rec *r             The request we should log errors against.
 *  const char *uri            The URI we should request.
 *  am_hc_block_header_t *bh   The buffer curl will write response data to.
 *  char *curl_error           A buffer of size CURL_ERROR_SIZE where curl
 *                             will store error messages.
 *
 * Returns:
 *  A initialized curl object on succcess, or NULL on error.
 */
static CURL *am_httpclient_init_curl(request_rec *r, const char *uri,
                                     am_hc_block_header_t *bh,
                                     char *curl_error)
{
    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);
    CURL *curl;
    CURLcode res;

    /* Initialize the curl object. */
    curl = curl_easy_init();
    if(curl == NULL) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to initialize a curl object.");
        return NULL;
    }


    /* Set up error reporting. */
    res = curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_error);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set curl error buffer: [%u]\n", res);
        goto cleanup_fail;
    }

    /* Disable progress reporting. */
    res = curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to disable curl progress reporting: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Disable use of signals. */
    res = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to disable signals in curl: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Set the timeout of the transfer. It is currently set to two minutes. */
    res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set the timeout of the curl download:"
                      " [%u] %s", res, curl_error);
        goto cleanup_fail;
    }

    /* If we have a CA configured, try to use it */
    if (cfg->idp_ca_file != NULL) {
        res = curl_easy_setopt(curl, CURLOPT_CAINFO, cfg->idp_ca_file->path);
        if(res != CURLE_OK) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Failed to set SSL CA info %s:"
                          " [%u] %s", cfg->idp_ca_file->path, res, curl_error);
            goto cleanup_fail;
        }
    }

    /* Enable fail on http error. */
    res = curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to enable failure on http error: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Select which uri we should download. */
    res = curl_easy_setopt(curl, CURLOPT_URL, uri);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set curl download uri to \"%s\": [%u] %s",
                      uri, res, curl_error);
        goto cleanup_fail;
    }


    /* Set up data writing. */

    /* Set curl write function. */
    res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, am_hc_data_write);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set the curl write function: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Set the curl write function parameter. */
    res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, bh);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set the curl write function data: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    return curl;


 cleanup_fail:
    curl_easy_cleanup(curl);
    return NULL;
}


/* This function downloads data from a specified URI, with specified timeout
 *
 * Parameters:
 *  request_rec *r       The apache request this download is associated
 *                       with. It is used for memory allocation and logging.
 *  const char *uri      The URI we should download.
 *  void **buffer        A pointer to where we should store a pointer to the
 *                       downloaded data. We will always add a null-terminator
 *                       to the data. This parameter can't be NULL.
 *  apr_size_t *size     This is a pointer to where we will store the length
 *                       of the downloaded data, not including the
 *                       null-terminator we add. This parameter can be NULL.
 *  int timeout          Timeout in seconds, 0 for no timeout.
 *  long *status         Pointer to HTTP status code. 
 *
 * Returns:
 *  OK on success, or HTTP_INTERNAL_SERVER_ERROR on failure. On failure we
 *  will write a log message describing the error.
 */
int am_httpclient_get(request_rec *r, const char *uri,
                      void **buffer, apr_size_t *size,
                      int timeout, long *status)
{
    am_hc_block_header_t bh;
    CURL *curl;
    char curl_error[CURL_ERROR_SIZE];
    CURLcode res;

    /* Initialize the data storage. */
    am_hc_block_header_init(&bh, r->pool);

    /* Initialize the curl object. */
    curl = am_httpclient_init_curl(r, uri, &bh, curl_error);
    if(curl == NULL) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to download data from the uri \"%s\", "
                      "cannot set timeout to %ld: [%u] %s",
                      uri, (long)timeout, res, curl_error);
        goto cleanup_fail;
    }
    
    res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, (long)timeout);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to download data from the uri \"%s\", "
                      "cannot set connect timeout to %ld: [%u] %s",
                      uri, (long)timeout,  res, curl_error);
        goto cleanup_fail;
    }

    /* Do the download. */
    res = curl_easy_perform(curl);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to download data from the uri \"%s\", "
                      "transaction aborted: [%u] %s",
                      uri, res, curl_error);
        goto cleanup_fail;
    }

    if (status != NULL) {
        res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
        if(res != CURLE_OK) {
            AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                          "Failed to download data from the uri \"%s\", "
                          "no status report: [%u] %s",
                          uri, res, curl_error);
            goto cleanup_fail;
       }
    }
    
    /* Free the curl object. */
    curl_easy_cleanup(curl);

    /* Copy the data. */
    am_hc_data_extract(&bh, r->pool, buffer, size);

    return OK;


 cleanup_fail:
    curl_easy_cleanup(curl);
    return HTTP_INTERNAL_SERVER_ERROR;
}


/* This function downloads data from a specified URI by issuing a POST
 * request.
 *
 * Parameters:
 *  request_rec *r             The apache request this download is
 *                             associated with. It is used for memory
 *                             allocation and logging.
 *  const char *uri            The URI we should post data to.
 *  const void *post_data      The POST data we should send.
 *  apr_size_t post_length     The length of the POST data.
 *  const char *content_type   The content type of the POST data. This
 *                             parameter can be NULL, in which case the
 *                             content type will be
 *                             "application/x-www-form-urlencoded".
 *  void **buffer              A pointer to where we should store a pointer
 *                             to the downloaded data. We will always add a
 *                             null-terminator to the data. This parameter
 *                             can't be NULL.
 *  apr_size_t *size           This is a pointer to where we will store the
 *                             length of the downloaded data, not including
 *                             the null-terminator we add. This parameter
 *                             can be NULL.
 *
 * Returns:
 *  OK on success. On failure we will write a log message describing the
 *  error, and return HTTP_INTERNAL_SERVER_ERROR.
 */
int am_httpclient_post(request_rec *r, const char *uri,
                       const void *post_data, apr_size_t post_length,
                       const char *content_type,
                       void **buffer, apr_size_t *size)
{
    am_hc_block_header_t bh;
    CURL *curl;
    char curl_error[CURL_ERROR_SIZE];
    CURLcode res;
    struct curl_slist *ctheader;
    am_dir_cfg_rec *cfg = am_get_dir_cfg(r);

    /* Initialize the data storage. */
    am_hc_block_header_init(&bh, r->pool);

    /* Initialize the curl object. */
    curl = am_httpclient_init_curl(r, uri, &bh, curl_error);
    if(curl == NULL) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    /* Enable POST request. */
    res = curl_easy_setopt(curl, CURLOPT_POST, 1L);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to enable POST request: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Set POST data size. */
    res = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_length);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set the POST data length: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }

    /* Set POST data. */
    res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set the POST data: [%u] %s",
                      res, curl_error);
        goto cleanup_fail;
    }


    /* Set the content-type header. */

    /* Set default content type if content_type is NULL. */
    if(content_type == NULL) {
        content_type = "application/x-www-form-urlencoded";
    }

    /* Create header list. */
    ctheader = NULL;
    ctheader = curl_slist_append(ctheader, apr_pstrcat(
                                     r->pool,
                                     "Content-Type: ",
                                     content_type,
                                     NULL
                                     ));

    /* Check if the send expect header is "off". */
    if (cfg->send_expect_header == 0) {
        ctheader = curl_slist_append(ctheader, "Expect:");
    }

    /* Set headers. */
    res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, ctheader);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to set content-type header to \"%s\": [%u] %s",
                      content_type, res, curl_error);
        goto cleanup_fail;
    }


    /* Do the download. */
    res = curl_easy_perform(curl);
    if(res != CURLE_OK) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Failed to download data from the uri \"%s\": [%u] %s",
                      uri, res, curl_error);
        goto cleanup_fail;
    }

    /* Free the curl object. */
    curl_easy_cleanup(curl);

    /* Free the content-type header. */
    curl_slist_free_all(ctheader);

    /* Copy the data. */
    am_hc_data_extract(&bh, r->pool, buffer, size);

    return OK;


 cleanup_fail:
    curl_easy_cleanup(curl);
    return HTTP_INTERNAL_SERVER_ERROR;
}


/* This function downloads data from a specified URI by issuing a POST
 * request.
 *
 * Parameters:
 *  request_rec *r             The apache request this download is
 *                             associated with. It is used for memory
 *                             allocation and logging.
 *  const char *uri            The URI we should post data to.
 *  const char *post_data      The POST data we should send.
 *  const char *content_type   The content type of the POST data. This
 *                             parameter can be NULL, in which case the
 *                             content type will be
 *                             "application/x-www-form-urlencoded".
 *  void **buffer              A pointer to where we should store a pointer
 *                             to the downloaded data. We will always add a
 *                             null-terminator to the data. This parameter
 *                             can't be NULL.
 *  apr_size_t *size           This is a pointer to where we will store the
 *                             length of the downloaded data, not including
 *                             the null-terminator we add. This parameter
 *                             can be NULL.
 *
 * Returns:
 *  OK on success. On failure we will write a log message describing the
 *  error, and return HTTP_INTERNAL_SERVER_ERROR.
 */
int am_httpclient_post_str(request_rec *r, const char *uri,
                           const char *post_data,
                           const char *content_type,
                           void **buffer, apr_size_t *size)
{
    return am_httpclient_post(r, uri, post_data, strlen(post_data),
                              content_type, buffer, size);
}
