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

#include "serf.h"
#include "test_server.h"

#include "serf_private.h"

#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10100000L
#define USE_OPENSSL_1_1_API
#endif

static int init_done = 0;

typedef struct ssl_context_t {
    int handshake_done;

    SSL_CTX* ctx;
    SSL* ssl;
    BIO *bio;
    BIO_METHOD *biom;

} ssl_context_t;

static int pem_passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
    strncpy(buf, "serftest", size);
    buf[size - 1] = '\0';
    return strlen(buf);
}

static void bio_set_data(BIO *bio, void *data)
{
#ifdef USE_OPENSSL_1_1_API
    BIO_set_data(bio, data);
#else
    bio->ptr = data;
#endif
}

static void *bio_get_data(BIO *bio)
{
#ifdef USE_OPENSSL_1_1_API
    return BIO_get_data(bio);
#else
    return bio->ptr;
#endif
}

static int bio_apr_socket_create(BIO *bio)
{
#ifdef USE_OPENSSL_1_1_API
    BIO_set_shutdown(bio, 1);
    BIO_set_init(bio, 1);
    BIO_set_data(bio, NULL);
#else
    bio->shutdown = 1;
    bio->init = 1;
    bio->num = -1;
    bio->ptr = NULL;
#endif

    return 1;
}

static int bio_apr_socket_destroy(BIO *bio)
{
    /* Did we already free this? */
    if (bio == NULL) {
        return 0;
    }

    return 1;
}

static long bio_apr_socket_ctrl(BIO *bio, int cmd, long num, void *ptr)
{
    long ret = 1;

    switch (cmd) {
        default:
            /* abort(); */
            break;
        case BIO_CTRL_FLUSH:
            /* At this point we can't force a flush. */
            break;
        case BIO_CTRL_PUSH:
        case BIO_CTRL_POP:
            ret = 0;
            break;
    }
    return ret;
}

/* Returns the amount read. */
static int bio_apr_socket_read(BIO *bio, char *in, int inlen)
{
    apr_size_t len = inlen;
    serv_ctx_t *serv_ctx = bio_get_data(bio);
    apr_status_t status;

    BIO_clear_retry_flags(bio);

    status = apr_socket_recv(serv_ctx->client_sock, in, &len);
    serv_ctx->bio_read_status = status;
    serf__log_skt(TEST_VERBOSE, __FILE__, serv_ctx->client_sock,
                  "Read %d bytes from socket with status %d.\n", len, status);

    if (status == APR_EAGAIN) {
        BIO_set_retry_read(bio);
        if (len == 0)
            return -1;
    }

    if (SERF_BUCKET_READ_ERROR(status))
        return -1;

    return len;
}

/* Returns the amount written. */
static int bio_apr_socket_write(BIO *bio, const char *in, int inlen)
{
    apr_size_t len = inlen;
    serv_ctx_t *serv_ctx = bio_get_data(bio);

    apr_status_t status = apr_socket_send(serv_ctx->client_sock, in, &len);

    serf__log_skt(TEST_VERBOSE, __FILE__, serv_ctx->client_sock,
                  "Wrote %d of %d bytes to socket with status %d.\n",
                  len, inlen, status);

    if (SERF_BUCKET_READ_ERROR(status))
        return -1;

    return len;
}


#ifndef USE_OPENSSL_1_1_API
static BIO_METHOD bio_apr_socket_method = {
    BIO_TYPE_SOCKET,
    "APR sockets",
    bio_apr_socket_write,
    bio_apr_socket_read,
    NULL,                        /* Is this called? */
    NULL,                        /* Is this called? */
    bio_apr_socket_ctrl,
    bio_apr_socket_create,
    bio_apr_socket_destroy,
#ifdef OPENSSL_VERSION_NUMBER
    NULL /* sslc does not have the callback_ctrl field */
#endif
};
#endif

static BIO_METHOD *bio_meth_apr_socket_new(void)
{
    BIO_METHOD *biom = NULL;

#ifdef USE_OPENSSL_1_1_API
    biom = BIO_meth_new(BIO_TYPE_SOCKET, "APR sockets");
    if (biom) {
        BIO_meth_set_write(biom, bio_apr_socket_write);
        BIO_meth_set_read(biom, bio_apr_socket_read);
        BIO_meth_set_ctrl(biom, bio_apr_socket_ctrl);
        BIO_meth_set_create(biom, bio_apr_socket_create);
        BIO_meth_set_destroy(biom, bio_apr_socket_destroy);
    }
#else
    biom = &bio_apr_socket_method;
#endif

    return biom;
}

static int validate_client_certificate(int preverify_ok, X509_STORE_CTX *ctx)
{
    serf__log(TEST_VERBOSE, __FILE__, "validate_client_certificate called, "
              "preverify code: %d.\n", preverify_ok);

    return preverify_ok;
}

static apr_status_t init_ssl(serv_ctx_t *serv_ctx)
{
    ssl_context_t *ssl_ctx = serv_ctx->ssl_ctx;

    ssl_ctx->ssl = SSL_new(ssl_ctx->ctx);
    SSL_set_cipher_list(ssl_ctx->ssl, "ALL");
    SSL_set_bio(ssl_ctx->ssl, ssl_ctx->bio, ssl_ctx->bio);

    return APR_SUCCESS;
}

static apr_status_t
init_ssl_context(serv_ctx_t *serv_ctx,
                 const char *keyfile,
                 const char **certfiles,
                 const char *client_cn)
{
    ssl_context_t *ssl_ctx = apr_pcalloc(serv_ctx->pool, sizeof(*ssl_ctx));
    serv_ctx->ssl_ctx = ssl_ctx;
    serv_ctx->client_cn = client_cn;
    serv_ctx->bio_read_status = APR_SUCCESS;

    /* Init OpenSSL globally */
    if (!init_done)
    {
#ifdef USE_OPENSSL_1_1_API
        OPENSSL_malloc_init();
#else
        CRYPTO_malloc_init();
#endif
        ERR_load_crypto_strings();
        SSL_load_error_strings();
        SSL_library_init();
        OpenSSL_add_all_algorithms();
        init_done = 1;
    }

    /* Init this connection */
    if (!ssl_ctx->ctx) {
        X509_STORE *store;
        const char *certfile;
        int i;
        int rv;

        ssl_ctx->ctx = SSL_CTX_new(SSLv23_server_method());
        SSL_CTX_set_default_passwd_cb(ssl_ctx->ctx, pem_passwd_cb);
        rv = SSL_CTX_use_PrivateKey_file(ssl_ctx->ctx, keyfile,
                                         SSL_FILETYPE_PEM);
        if (rv != 1) {
            fprintf(stderr, "Cannot load private key from file '%s'\n", keyfile);
            exit(1);
        }

        /* Set server certificate, add ca certificates if provided. */
        certfile = certfiles[0];
        rv = SSL_CTX_use_certificate_file(ssl_ctx->ctx, certfile, SSL_FILETYPE_PEM);
        if (rv != 1) {
            fprintf(stderr, "Cannot load certficate from file '%s'\n", certfile);
            exit(1);
        }

        i = 1;
        certfile = certfiles[i++];
        store = SSL_CTX_get_cert_store(ssl_ctx->ctx);

        while(certfile) {
            FILE *fp = fopen(certfile, "r");
            if (fp) {
                X509 *ssl_cert = PEM_read_X509(fp, NULL, NULL, NULL);
                fclose(fp);

                SSL_CTX_add_extra_chain_cert(ssl_ctx->ctx, ssl_cert);

                X509_STORE_add_cert(store, ssl_cert);
            }
            certfile = certfiles[i++];
        }

        /* This makes the server send a client certificate request during
           handshake. The client certificate is optional (most tests don't
           send one) by default, but mandatory if client_cn was specified. */
        SSL_CTX_set_verify(ssl_ctx->ctx, SSL_VERIFY_PEER,
                           validate_client_certificate);

        SSL_CTX_set_mode(ssl_ctx->ctx, SSL_MODE_ENABLE_PARTIAL_WRITE);

        ssl_ctx->biom = bio_meth_apr_socket_new();
        ssl_ctx->bio = BIO_new(ssl_ctx->biom);
        bio_set_data(ssl_ctx->bio, serv_ctx);
        init_ssl(serv_ctx);
    }

    return APR_SUCCESS;
}

static apr_status_t ssl_reset(serv_ctx_t *serv_ctx)
{
    ssl_context_t *ssl_ctx = serv_ctx->ssl_ctx;

    serf__log(TEST_VERBOSE, __FILE__, "Reset ssl context.\n");

    ssl_ctx->handshake_done = 0;
    if (ssl_ctx)
        SSL_clear(ssl_ctx->ssl);
    init_ssl(serv_ctx);

    return APR_SUCCESS;
}

static apr_status_t ssl_handshake(serv_ctx_t *serv_ctx)
{
    ssl_context_t *ssl_ctx = serv_ctx->ssl_ctx;
    int result;

    if (ssl_ctx->handshake_done)
        return APR_SUCCESS;

    /* SSL handshake */
    result = SSL_accept(ssl_ctx->ssl);
    if (result == 1) {
        X509 *peer;

        serf__log(TEST_VERBOSE, __FILE__, "Handshake successful.\n");

        /* Check client certificate */
        peer = SSL_get_peer_certificate(ssl_ctx->ssl);
        if (peer)
        {
            serf__log(TEST_VERBOSE, __FILE__, "Peer cert received.\n");
            if (SSL_get_verify_result(ssl_ctx->ssl) == X509_V_OK)
            {
                /* The client sent a certificate which verified OK */
                char buf[1024];
                int ret;
                X509_NAME *subject = X509_get_subject_name(peer);

                ret = X509_NAME_get_text_by_NID(subject,
                                                NID_commonName,
                                                buf, 1024);
                if (ret != -1 && strcmp(serv_ctx->client_cn, buf) != 0) {
                    serf__log(TEST_VERBOSE, __FILE__, "Client cert common name "
                              "\"%s\" doesn't match expected \"%s\".\n", buf,
                              serv_ctx->client_cn);
                    return SERF_ERROR_ISSUE_IN_TESTSUITE;

                }
            }
        } else {
            if (serv_ctx->client_cn) {
                serf__log(TEST_VERBOSE, __FILE__, "Client cert expected but not"
                          " received.\n");
                return SERF_ERROR_ISSUE_IN_TESTSUITE;
            }
        }

        ssl_ctx->handshake_done = 1;
    }
    else {
        int ssl_err;

        ssl_err = SSL_get_error(ssl_ctx->ssl, result);
        switch (ssl_err) {
            case SSL_ERROR_WANT_READ:
            case SSL_ERROR_WANT_WRITE:
                return APR_EAGAIN;
            case SSL_ERROR_SYSCALL:
                return serv_ctx->bio_read_status; /* Usually APR_EAGAIN */
            default:
                serf__log(TEST_VERBOSE, __FILE__, "SSL Error %d: ", ssl_err);
                ERR_print_errors_fp(stderr);
                serf__log_nopref(TEST_VERBOSE, "\n");
                return SERF_ERROR_ISSUE_IN_TESTSUITE;
        }
    }

    return APR_EAGAIN;
}

static apr_status_t
ssl_socket_write(serv_ctx_t *serv_ctx, const char *data,
                 apr_size_t *len)
{
    ssl_context_t *ssl_ctx = serv_ctx->ssl_ctx;

    int result = SSL_write(ssl_ctx->ssl, data, *len);
    if (result > 0) {
        *len = result;
        return APR_SUCCESS;
    }

    if (result == 0)
        return APR_EAGAIN;

    serf__log(TEST_VERBOSE, __FILE__, "ssl_socket_write: ssl error?\n");

    return SERF_ERROR_ISSUE_IN_TESTSUITE;
}

static apr_status_t
ssl_socket_read(serv_ctx_t *serv_ctx, char *data,
                apr_size_t *len)
{
    ssl_context_t *ssl_ctx = serv_ctx->ssl_ctx;

    int result = SSL_read(ssl_ctx->ssl, data, *len);
    if (result > 0) {
        *len = result;
        return APR_SUCCESS;
    } else {
        int ssl_err;

        ssl_err = SSL_get_error(ssl_ctx->ssl, result);
        switch (ssl_err) {
            case SSL_ERROR_SYSCALL:
                /* error in bio_bucket_read, probably APR_EAGAIN or APR_EOF */
                *len = 0;
                return serv_ctx->bio_read_status;
            case SSL_ERROR_WANT_READ:
                *len = 0;
                return APR_EAGAIN;
            case SSL_ERROR_SSL:
            default:
                *len = 0;
                serf__log(TEST_VERBOSE, __FILE__,
                          "ssl_socket_read SSL Error %d: ", ssl_err);
                ERR_print_errors_fp(stderr);
                serf__log_nopref(TEST_VERBOSE, "\n");
                return SERF_ERROR_ISSUE_IN_TESTSUITE;
        }
    }

    /* not reachable */
    return SERF_ERROR_ISSUE_IN_TESTSUITE;
}

static apr_status_t cleanup_https_server(void *baton)
{
    serv_ctx_t *servctx = baton;
    ssl_context_t *ssl_ctx = servctx->ssl_ctx;

    if (ssl_ctx) {
        if (ssl_ctx->ssl) {
          SSL_clear(ssl_ctx->ssl);
#ifdef USE_OPENSSL_1_1_API
          BIO_meth_free(ssl_ctx->biom);
#endif
        }
        SSL_CTX_free(ssl_ctx->ctx);
    }

    return APR_SUCCESS;
}

void setup_https_test_server(serv_ctx_t **servctx_p,
                             apr_sockaddr_t *address,
                             test_server_message_t *message_list,
                             apr_size_t message_count,
                             test_server_action_t *action_list,
                             apr_size_t action_count,
                             apr_int32_t options,
                             const char *keyfile,
                             const char **certfiles,
                             const char *client_cn,
                             apr_pool_t *pool)
{
    serv_ctx_t *servctx;

    setup_test_server(servctx_p, address, message_list,
                      message_count, action_list, action_count,
                      options, pool);
    servctx = *servctx_p;
    apr_pool_cleanup_register(pool, servctx,
                              cleanup_https_server,
                              apr_pool_cleanup_null);

    servctx->handshake = ssl_handshake;
    servctx->reset = ssl_reset;

    /* Override with SSL encrypt/decrypt functions */
    servctx->read = ssl_socket_read;
    servctx->send = ssl_socket_write;

    init_ssl_context(servctx, keyfile, certfiles, client_cn);
}
