/* Copyright 2008 Justin Erenkrantz and Greg Stein
 *
 * Licensed 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.
 */

#include <apr.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_env.h>

#include "serf.h"
#include "serf_bucket_types.h"

/* Access to internal functions, for DER decoding. */
#include "buckets/bucket_private.h"

#include "test_serf.h"

#if defined(WIN32) && defined(_DEBUG)
/* Include this file to allow running a Debug build of serf with a Release
   build of OpenSSL. */
#include <openssl/applink.c>
#endif

/* Test setting up the openssl library. */
static void test_ssl_init(CuTest *tc)
{
    serf_bucket_t *bkt, *stream;
    serf_ssl_context_t *ssl_context;
    apr_status_t status;

    apr_pool_t *test_pool = tc->testBaton;
    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(test_pool, NULL,
                                                              NULL);

    stream = SERF_BUCKET_SIMPLE_STRING("", alloc);

    bkt = serf_bucket_ssl_decrypt_create(stream, NULL,
                                         alloc);
    CuAssertTrue(tc, SERF_BUCKET_IS_SSL_DECRYPT(bkt));

    ssl_context = serf_bucket_ssl_decrypt_context_get(bkt);

    bkt = serf_bucket_ssl_encrypt_create(stream, ssl_context,
                                         alloc);
    CuAssertTrue(tc, SERF_BUCKET_IS_SSL_ENCRYPT(bkt));

    status = serf_ssl_use_default_certificates(ssl_context);

    CuAssertIntEquals(tc, APR_SUCCESS, status);
}


static const char * get_ca_file(apr_pool_t *pool, const char * file)
{
    char *srcdir = "";

    if (apr_env_get(&srcdir, "srcdir", pool) == APR_SUCCESS) {
        return apr_pstrcat(pool, srcdir, "/", file, NULL);
    }
    else {
        return file;
    }
}


/* Test that loading a custom CA certificate file works. */
static void test_ssl_load_cert_file(CuTest *tc)
{
    serf_ssl_certificate_t *cert = NULL;

    apr_pool_t *test_pool = tc->testBaton;
    apr_status_t status = serf_ssl_load_cert_file(
        &cert, get_ca_file(test_pool, "test/serftestca.pem"), test_pool);

    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);
}

typedef struct
{
    const char *cn;
    const char *o;
    const char *ou;
    const char *l;
    const char *st;
    const char *c;
    const char *e;
} test_dn_t;

typedef struct {
    test_dn_t issuer;
    test_dn_t subject;
    const char *sha1;
    const char *notBefore;
    const char *notAfter;
    /* serial number, subjectAltNames */
} test_cert_t;

static void
validate_dn(CuTest *tc, const test_dn_t *expected, const apr_hash_t *actual)
{
    apr_hash_t *dn = (apr_hash_t *)actual;

    CuAssertStrEquals(tc, expected->cn,
                      apr_hash_get(dn, "CN", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->ou,
                      apr_hash_get(dn, "OU", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->o,
                      apr_hash_get(dn, "O", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->l,
                      apr_hash_get(dn, "L", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->st,
                      apr_hash_get(dn, "ST", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->c,
                      apr_hash_get(dn, "C", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->e,
                      apr_hash_get(dn, "E", APR_HASH_KEY_STRING));
}

static void
validate_cert(CuTest *tc, const test_cert_t *expected, const apr_hash_t *actual)
{
    const apr_hash_t *subject, *issuer;
    apr_hash_t *cert = (apr_hash_t *)actual;

    subject = apr_hash_get(cert, "subject", APR_HASH_KEY_STRING);
    CuAssertPtrNotNullMsg(tc, "Expected subject", subject);

    validate_dn(tc, &expected->subject, subject);

    issuer = apr_hash_get(cert, "issuer", APR_HASH_KEY_STRING);
    CuAssertPtrNotNullMsg(tc, "Expected issuer", issuer);

    validate_dn(tc, &expected->issuer, issuer);
    /*
     TODO: sha1sum of DER encoded cert for signature.
     CuAssertStrEquals(tc, expected->sha1,
     apr_hash_get(cert, "sha1", APR_HASH_KEY_STRING));
     */
    CuAssertStrEquals(tc, expected->notBefore,
                      apr_hash_get(cert, "notBefore", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, expected->notAfter,
                      apr_hash_get(cert, "notAfter", APR_HASH_KEY_STRING));
    /* TODO: subjectAltNames */
}

/* Test that reading the subject from a custom CA certificate file works. */
static void test_ssl_cert_subject(CuTest *tc)
{
    apr_hash_t *subject;
    serf_ssl_certificate_t *cert = NULL;
    apr_status_t status;
    const test_dn_t exp_subject = { "Serf", "In Serf we trust, Inc.",
        "Test Suite", "Mechelen", "Antwerp", "BE", "serf@example.com" };

    apr_pool_t *test_pool = tc->testBaton;

    status = serf_ssl_load_cert_file(&cert, get_ca_file(test_pool,
                                                        "test/serftestca.pem"),
                                     test_pool);
    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);

    subject = serf_ssl_cert_subject(cert, test_pool);
    CuAssertPtrNotNull(tc, subject);

    validate_dn(tc, &exp_subject, subject);
}

/* Test that reading the issuer from a custom CA certificate file works. */
static void test_ssl_cert_issuer(CuTest *tc)
{
    apr_hash_t *issuer;
    serf_ssl_certificate_t *cert = NULL;
    apr_status_t status;
    const test_dn_t exp_issuer = { "Serf", "In Serf we trust, Inc.",
        "Test Suite", "Mechelen", "Antwerp", "BE", "serf@example.com" };

    apr_pool_t *test_pool = tc->testBaton;

    status = serf_ssl_load_cert_file(&cert, get_ca_file(test_pool,
                                                        "test/serftestca.pem"),
                                     test_pool);

    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);

    issuer = serf_ssl_cert_issuer(cert, test_pool);
    CuAssertPtrNotNull(tc, issuer);

    validate_dn(tc, &exp_issuer, issuer);
}

/* Test that reading the notBefore,notAfter,sha1 fingerprint and subjectAltNames
   from a custom CA certificate file works. */
static void test_ssl_cert_certificate(CuTest *tc)
{
    apr_hash_t *kv;
    serf_ssl_certificate_t *cert = NULL;
    apr_array_header_t *san_arr;
    apr_status_t status;

    apr_pool_t *test_pool = tc->testBaton;

    status = serf_ssl_load_cert_file(&cert, get_ca_file(test_pool,
                                                        "test/serftestca.pem"),
                                     test_pool);
    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);

    kv = serf_ssl_cert_certificate(cert, test_pool);
    CuAssertPtrNotNull(tc, kv);

    CuAssertStrEquals(tc, "8A:4C:19:D5:F2:52:4E:35:49:5E:7A:14:80:B2:02:BD:B4:4D:22:18",
                      apr_hash_get(kv, "sha1", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, "Mar 21 13:18:17 2008 GMT",
                      apr_hash_get(kv, "notBefore", APR_HASH_KEY_STRING));
    CuAssertStrEquals(tc, "Mar 21 13:18:17 2011 GMT",
                      apr_hash_get(kv, "notAfter", APR_HASH_KEY_STRING));

    /* TODO: create a new test certificate with a/some sAN's. */
    san_arr = apr_hash_get(kv, "subjectAltName", APR_HASH_KEY_STRING);
    CuAssertTrue(tc, san_arr == NULL);
}

static void test_ssl_load_CA_cert_from_file(CuTest *tc)
{
    serf_ssl_certificate_t *cert = NULL;
    serf_bucket_t *bkt, *stream;
    serf_ssl_context_t *ssl_context;
    apr_status_t status;

    apr_pool_t *test_pool = tc->testBaton;
    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(test_pool, NULL,
                                                              NULL);

    stream = SERF_BUCKET_SIMPLE_STRING("", alloc);
    bkt = serf_bucket_ssl_decrypt_create(stream, NULL, alloc);
    ssl_context = serf_bucket_ssl_decrypt_context_get(bkt);

    status = serf_ssl_load_CA_cert_from_file(ssl_context,
                                             &cert,
                                             get_ca_file(test_pool, "test/serftestca.pem"),
                                             test_pool);

    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);
}

static const char *extract_cert_from_pem(const char *pemdata,
                                         apr_size_t *pemlen,
                                         int copy_delimiters,
                                         apr_pool_t *pool)
{
    enum { INIT, CERT_BEGIN, CERT_FOUND } state;
    serf_bucket_t *pembkt;
    const char *begincert = "-----BEGIN CERTIFICATE-----";
    const char *endcert = "-----END CERTIFICATE-----";
    char *certdata = "";
    apr_size_t certlen = 0;
    apr_status_t status = APR_SUCCESS;

    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(pool,
                                                              NULL, NULL);

    /* Extract the certificate from the .pem file, also remove newlines. */
    pembkt = SERF_BUCKET_SIMPLE_STRING(pemdata, alloc);
    state = INIT;
    while (state != CERT_FOUND && status != APR_EOF) {
        const char *data;
        apr_size_t len;
        int found;
        int delimiter = FALSE;

        status = serf_bucket_readline(pembkt, SERF_NEWLINE_ANY, &found,
                                      &data, &len);
        if (SERF_BUCKET_READ_ERROR(status))
            return NULL;

        if (state == INIT) {
            if (strncmp(begincert, data, strlen(begincert)) != 0)
                continue;

            state = CERT_BEGIN;
            delimiter = TRUE;
            if (!copy_delimiters)
                continue;
        } else if (state == CERT_BEGIN) {
            if (strncmp(endcert, data, strlen(endcert)) == 0)
            {
                state = CERT_FOUND;
                delimiter = TRUE;
                if (!copy_delimiters)
                    break;
                certdata = apr_pstrcat(pool, certdata, CRLF, NULL);
                certlen += 2;
            }
        }
        /* Copy the line to the output buffer. Remove linefeeds from the
         base64 DER encoded certificate. */
        certdata = apr_pstrcat(pool, certdata, data, NULL);
        certlen += len;
        switch (found && !delimiter)
        {
            case SERF_NEWLINE_CR:
            case SERF_NEWLINE_LF:
                certdata[certlen-1] = '\0';
                certlen--;
                break;
            case SERF_NEWLINE_CRLF:
                certdata[certlen-2] = '\0';
                certlen-=2;
                break;
        }
    }

    if (state == CERT_FOUND) {
        *pemlen = certlen;
        return certdata;
    }
    else
        return NULL;
}

static void test_ssl_cert_export(CuTest *tc)
{
    serf_ssl_certificate_t *cert = NULL;
    apr_file_t *fp;
    apr_finfo_t file_info;
    const char *base64derbuf;
    char *pembuf;
    apr_size_t pemlen;
    apr_status_t status;

    apr_pool_t *test_pool = tc->testBaton;

    status = serf_ssl_load_cert_file(&cert, get_ca_file(test_pool,
                                                        "test/serftestca.pem"),
                                     test_pool);
    CuAssertIntEquals(tc, APR_SUCCESS, status);
    CuAssertPtrNotNull(tc, cert);

    /* A .pem file contains a Base64 encoded DER certificate, which is exactly
       what serf_ssl_cert_export is supposed to be returning. */
    status = apr_file_open(&fp, "test/serftestca.pem",
                           APR_FOPEN_READ | APR_FOPEN_BINARY,
                           APR_FPROT_OS_DEFAULT, test_pool);
    CuAssertIntEquals(tc, APR_SUCCESS, status);

    apr_file_info_get(&file_info, APR_FINFO_SIZE, fp);
    pembuf = apr_palloc(test_pool, file_info.size);

    status = apr_file_read_full(fp, pembuf, file_info.size, &pemlen);
    CuAssertIntEquals(tc, APR_SUCCESS, status);

    base64derbuf = serf_ssl_cert_export(cert, test_pool);

    CuAssertStrEquals(tc,
                      extract_cert_from_pem(pembuf, &pemlen, 0, test_pool),
                      base64derbuf);
}

/****************************************************************************/
/* Test the DER decoding with certificates collected in file
   test/certificates.pem.
 */

static apr_status_t read_certificate(serf_bucket_t *bkt,
                                     char *buf,
                                     apr_size_t buf_len,
                                     apr_size_t *read_len)
{
    apr_size_t total_read;
    apr_status_t status;
    const char *delimiter = "=========================";
    int acceptable = SERF_NEWLINE_CRLF | SERF_NEWLINE_LF | SERF_NEWLINE_CR;

    total_read = 0;
    do
    {
        const char *data;
        apr_size_t len;
        int found;

        status = serf_bucket_readline(bkt, acceptable, &found,
                                      &data, &len);
        if (!SERF_BUCKET_READ_ERROR(status))
        {
            if (len >= sizeof(delimiter) &&
                strncmp(delimiter, data, sizeof(delimiter)) == 0)
            {
                /* end of certificate found, return current data. */
                status = APR_SUCCESS;
                break;
            }
            if (total_read + len > buf_len)
            {
                /* Buffer is not large enough to read all data */
                status = SERF_ERROR_ISSUE_IN_TESTSUITE;
            }
            memcpy(buf + total_read, data, len);
            total_read += len;
        }
    } while(status == APR_SUCCESS);

    *read_len = total_read;
    return status;
}

const test_cert_t test_certs[] =
{
    { { "Serf CA", "In Serf we trust, Inc.", "Test Suite CA",
        "Mechelen", "Antwerp", "BE", "serfca@example.com"},
      { "Serf Server", "In Serf we trust, Inc.", "Test Suite Server",
        "Mechelen", "Antwerp", "BE", "serfserver@example.com"},
      "", "Apr 18 19:50:11 2013 GMT", "Apr 18 19:50:11 2014 GMT",
    },
    { { "Serf Root CA", "In Serf we trust, Inc.", "Test Suite Root CA",
        "Mechelen", "Antwerp", "BE", "serfrootca@example.com"},
      { "Serf Root CA", "In Serf we trust, Inc.", "Test Suite Root CA",
        "Mechelen", "Antwerp", "BE", "serfrootca@example.com"},
      "", "Apr 13 11:19:14 2013 GMT", "Apr 11 11:19:14 2023 GMT",
    },
    { { "Belgium Root CA", NULL, NULL, NULL, NULL, "BE", NULL},
      { "Belgium Root CA", NULL, NULL, NULL, NULL, "BE", NULL},
        "", "Jan 26 23:00:00 2003 GMT", "Jan 26 23:00:00 2014 GMT",
    },
    { { "Belgium Root CA2", NULL, NULL, NULL, NULL, "BE", NULL},
      { "Belgium Root CA2", NULL, NULL, NULL, NULL, "BE", NULL},
      "", "Oct 04 10:00:00 2007 GMT", "Dec 15 08:00:00 2021 GMT",
    },
};

static void test_macosxssl_DER_decoding(CuTest *tc)
{
#ifdef SERF_HAVE_MACOSXSSL
    serf_bucket_t *filebkt;
    apr_file_t *fp;
    char buf[16384];
    apr_size_t len;
    apr_status_t status;
    int current_cert;

    apr_pool_t *test_pool = tc->testBaton;
    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(test_pool,
                                                              NULL, NULL);

    status = apr_file_open(&fp, "test/certificates.pem",
                           APR_FOPEN_READ | APR_FOPEN_BINARY,
                           APR_FPROT_OS_DEFAULT, test_pool);
    CuAssertIntEquals(tc, APR_SUCCESS, status);

    filebkt = serf_bucket_file_create(fp, alloc);

    current_cert = 0;
    do
    {
        status = read_certificate(filebkt, buf, sizeof(buf), &len);
        CuAssertIntEquals(tc, SERF_BUCKET_READ_ERROR(status), 0);

        /* A certificate was read from test/certificates.pem */
        if (status == APR_SUCCESS)
        {
            apr_size_t pemlen;
            const char *pemdata = extract_cert_from_pem(buf, &pemlen, 1, test_pool);
            serf_ssl_certificate_t *cert;
            apr_hash_t *actual;
            
            test_cert_t thiscert = test_certs[current_cert];

/*            printf("Certificate found: %.*s\n\n", pemlen, pemdata);**/
            status = load_CA_cert_from_buffer(&cert, pemdata, pemlen, test_pool);
            CuAssertIntEquals(tc, APR_SUCCESS, status);

            status = serf__macosxssl_read_X509_DER_certificate(&actual,
                                                               cert->impl_cert,
                                                               test_pool);
            CuAssertIntEquals(tc, APR_SUCCESS, status);

            validate_cert(tc, &thiscert, actual);

            current_cert++;
        }
    } while (status == APR_SUCCESS &&
             current_cert < sizeof(test_certs) / sizeof(test_certs[0]));

#endif
}

/* Disable all available SSL implementations, test for no crash. */
static void test_ssl_no_implementations(CuTest *tc)
{
    serf_bucket_t *bkt, *stream;

    apr_pool_t *test_pool = tc->testBaton;
    serf_bucket_alloc_t *alloc = serf_bucket_allocator_create(test_pool, NULL,
                                                              NULL);
    stream = SERF_BUCKET_SIMPLE_STRING("", alloc);

    serf_config_disable_bucket_impls(SERF_IMPL_SSL_ALL);

    bkt = serf_bucket_ssl_decrypt_create(stream, NULL, alloc);
    CuAssertPtrEquals(tc, NULL, bkt);

    bkt = serf_bucket_ssl_encrypt_create(stream, NULL, alloc);
    CuAssertPtrEquals(tc, NULL, bkt);

    serf_config_enable_bucket_impls(SERF_IMPL_SSL_ALL);
}

CuSuite *test_ssl(void)
{
    CuSuite *suite = CuSuiteNew();
    CuSuite *openssl_suite, *macosxssl_suite;

    CuSuiteSetSetupTeardownCallbacks(suite, test_setup, test_teardown);

#ifdef SERF_HAVE_OPENSSL
    openssl_suite = CuSuiteNew();

    CuSuiteSetSetupTeardownCallbacks(openssl_suite, test_openssl_setup,
                                     test_openssl_teardown);

    SUITE_ADD_TEST(openssl_suite, test_ssl_init);
    SUITE_ADD_TEST(openssl_suite, test_ssl_load_cert_file);
    SUITE_ADD_TEST(openssl_suite, test_ssl_cert_subject);
    SUITE_ADD_TEST(openssl_suite, test_ssl_cert_issuer);
    SUITE_ADD_TEST(openssl_suite, test_ssl_cert_certificate);
    SUITE_ADD_TEST(openssl_suite, test_ssl_load_CA_cert_from_file);
    SUITE_ADD_TEST(openssl_suite, test_ssl_cert_export);
    SUITE_ADD_TEST(openssl_suite, test_macosxssl_DER_decoding);

    CuSuiteAddSuite(suite, openssl_suite);
#endif
#ifdef SERF_HAVE_MACOSXSSL
    macosxssl_suite = CuSuiteNew();

    CuSuiteSetSetupTeardownCallbacks(macosxssl_suite, test_macosxssl_setup,
                                     test_macosxssl_teardown);

    SUITE_ADD_TEST(macosxssl_suite, test_ssl_init);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_load_cert_file);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_cert_subject);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_cert_issuer);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_cert_certificate);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_load_CA_cert_from_file);
    SUITE_ADD_TEST(macosxssl_suite, test_ssl_cert_export);
    SUITE_ADD_TEST(macosxssl_suite, test_macosxssl_DER_decoding);

    CuSuiteAddSuite(suite, macosxssl_suite);
#endif

    SUITE_ADD_TEST(suite, test_ssl_no_implementations);

    return suite;
}
