/* Icecast
 *
 * This program is distributed under the GNU General Public License, version 2.
 * A copy of this license is included with this source.
 *
 * Copyright 2016-2018, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
 */

/**
 * TLS support functions
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_OPENSSL
#include <openssl/ssl.h>
#include <openssl/err.h>
#endif

#include <stdlib.h>
#include <strings.h>

#include "tls.h"

#include "logging.h"
#define CATMODULE "tls"

/* Check for a specific implementation. Returns 0 if supported, 1 if unsupported and -1 on error. */
int        tls_check_impl(const char *impl)
{
#ifdef HAVE_OPENSSL
    if (!strcasecmp(impl, "openssl"))
        return 0;
#endif
#ifdef ICECAST_CAP_TLS
    if (!strcasecmp(impl, "generic"))
        return 0;
#endif

    return 1;
}

#ifdef HAVE_OPENSSL
struct tls_ctx_tag {
    size_t refc;
    SSL_CTX *ctx;
};

struct tls_tag {
    size_t refc;
    SSL *ssl;
    tls_ctx_t *ctx;
    bool error;
    bool no_shutdown;
};

void       tls_initialize(void)
{
}

void       tls_shutdown(void)
{
}

tls_ctx_t *tls_ctx_new(const char *cert_file, const char *key_file, const char *cipher_list)
{
    tls_ctx_t *ctx;
    long ssl_opts = 0;

    if (!cert_file || !key_file || !cipher_list)
        return NULL;

    ctx = calloc(1, sizeof(*ctx));
    if (!ctx)
        return NULL;

    ctx->refc = 1;

    ctx->ctx = SSL_CTX_new(TLS_server_method());
    SSL_CTX_set_min_proto_version(ctx->ctx, TLS1_2_VERSION);

#ifdef SSL_OP_NO_COMPRESSION
    ssl_opts |= SSL_OP_NO_COMPRESSION;             // Never use compression
#endif

    /* Even though this function is called set, it adds the
     * flags to the already existing flags (possibly default
     * flags already set by OpenSSL)!
     * Calling SSL_CTX_get_options is not needed here, therefore.
     */
    SSL_CTX_set_options(ctx->ctx, ssl_opts);
    do {
        if (SSL_CTX_use_certificate_chain_file(ctx->ctx, cert_file) <= 0) {
            ICECAST_LOG_WARN("Invalid cert file %s", cert_file);
            break;
        }
        if (SSL_CTX_use_PrivateKey_file(ctx->ctx, key_file, SSL_FILETYPE_PEM) <= 0) {
            ICECAST_LOG_WARN("Invalid private key file %s", key_file);
            break;
        }
        if (!SSL_CTX_check_private_key(ctx->ctx)) {
            ICECAST_LOG_ERROR("Invalid %s - Private key does not match cert public key", key_file);
            break;
        }
        if (SSL_CTX_set_cipher_list(ctx->ctx, cipher_list) <= 0) {
            ICECAST_LOG_WARN("Invalid cipher list: %s", cipher_list);
        }
        ICECAST_LOG_INFO("Certificate found at %s", cert_file);
        ICECAST_LOG_INFO("Using ciphers %s", cipher_list);
        return ctx;
    } while (0);

    ICECAST_LOG_INFO("Can not setup TLS.");
    tls_ctx_unref(ctx);
    return NULL;
}

void       tls_ctx_ref(tls_ctx_t *ctx)
{
    if (!ctx)
        return;

    ctx->refc++;
}

void       tls_ctx_unref(tls_ctx_t *ctx)
{
    if (!ctx)
        return;

    ctx->refc--;

    if (ctx->refc)
        return;

    if (ctx->ctx)
        SSL_CTX_free(ctx->ctx);

    free(ctx);
}

tls_t     *tls_new(tls_ctx_t *ctx)
{
    tls_t *tls;
    SSL *ssl;

    if (!ctx)
        return NULL;

    ssl = SSL_new(ctx->ctx);
    if (!ssl)
        return NULL;

    tls = calloc(1, sizeof(*tls));
    if (!tls) {
        SSL_free(ssl);
        return NULL;
    }

    tls_ctx_ref(ctx);

    tls->refc = 1;
    tls->ssl  = ssl;
    tls->ctx  = ctx;

    ICECAST_LOG_DEBUG("tls_new(ctx=%p) = %p", ctx, tls);

    return tls;
}
void       tls_ref(tls_t *tls)
{
    if (!tls)
        return;
    
    tls->refc++;
}
void       tls_unref(tls_t *tls)
{
    if (!tls)
        return;

    tls->refc--;

    if (tls->refc)
        return;

    if (!tls->no_shutdown) {
        int ret = SSL_shutdown(tls->ssl);
        if (ret < 0) {
            int error = SSL_get_error(tls->ssl, ret);
            ICECAST_LOG_DEBUG("Shutdown unsuccessful: tls=%p, ret=%i, error=%i", tls, ret, error);
        }
    }

    SSL_free(tls->ssl);

    if (tls->ctx)
        tls_ctx_unref(tls->ctx);

    free(tls);
}

void       tls_set_incoming(tls_t *tls)
{
    if (!tls)
        return;

    SSL_set_accept_state(tls->ssl);
}
void       tls_set_socket(tls_t *tls, sock_t sock)
{
    if (!tls)
        return;

    SSL_set_fd(tls->ssl, sock);
}

int        tls_want_io(tls_t *tls)
{
    int what;

    if (!tls)
        return -1;

    what = SSL_want(tls->ssl);

    switch (what) {
        case SSL_WRITING:
        case SSL_READING:
            return 1;
        break;
        case SSL_NOTHING:
        default:
            return 0;
        break;
    }
}

int        tls_got_shutdown(tls_t *tls)
{
    if (!tls)
        return -1;

    if (SSL_get_shutdown(tls->ssl) & SSL_RECEIVED_SHUTDOWN) {
        return 1;
    } else {
        return 0;
    }
}

ssize_t    tls_read(tls_t *tls, void *buffer, size_t len)
{
    int ret;

    if (!tls)
        return -1;

    ret = SSL_read(tls->ssl, buffer, len);
    ICECAST_LOG_DDEBUG("Read on TLS (tls=%o, ret=%i)", tls, ret);

    if (ret <= 0 && !tls->error) {
        int error = SSL_get_error(tls->ssl, ret);
        ICECAST_LOG_DEBUG("Zero read on TLS (tls=%p, ret=%i, error=%i)", tls, ret, error);
        if (error == SSL_ERROR_SYSCALL || error == SSL_ERROR_SSL)
            tls->error = true;
        if (error == SSL_ERROR_SSL)
            tls->no_shutdown = true;
    }

    return ret;
}
ssize_t    tls_write(tls_t *tls, const void *buffer, size_t len)
{
    int ret;

    if (!tls)
        return -1;

    ret = SSL_write(tls->ssl, buffer, len);

    if (ret <= 0) {
        switch (SSL_get_error(tls->ssl, ret)) {
            case SSL_ERROR_SSL:
                tls->no_shutdown = true;
                /* fall thru */
            case SSL_ERROR_SYSCALL:
                return -1;
                break;
            default:
                return 0;
                break;
        }
    }

    return ret;
}

bool       tls_error(tls_t *tls) {
    if (!tls)
        return true;

    return tls->error;
}
#else
void       tls_initialize(void)
{
}
void       tls_shutdown(void)
{
}

tls_ctx_t *tls_ctx_new(const char *cert_file, const char *key_file, const char *cipher_list)
{
    return NULL;
}
void       tls_ctx_ref(tls_ctx_t *ctx)
{
}
void       tls_ctx_unref(tls_ctx_t *ctx)
{
}

tls_t     *tls_new(tls_ctx_t *ctx)
{
    return NULL;
}
void       tls_ref(tls_t *tls)
{
}
void       tls_unref(tls_t *tls)
{
}

void       tls_set_incoming(tls_t *tls)
{
}
void       tls_set_socket(tls_t *tls, sock_t sock)
{
}

int        tls_want_io(tls_t *tls)
{
    return -1;
}

int        tls_got_shutdown(tls_t *tls)
{
    return -1;
}

ssize_t    tls_read(tls_t *tls, void *buffer, size_t len)
{
    return -1;
}
ssize_t    tls_write(tls_t *tls, const void *buffer, size_t len)
{
    return -1;
}

bool       tls_error(tls_t *tls) {
    return true;
}

#endif
