#include "mkcert.hpp"
#include <openssl/x509v3.h>
#include <openssl/opensslv.h>


typedef struct ssl_cert_s {
    X509* cert;
    EVP_PKEY* key;
    ssl_cert_s(): cert(NULL), key(NULL) {}
    ~ssl_cert_s() {
        if (cert) X509_free(cert);
        if (key) EVP_PKEY_free(key);
    }
} ssl_cert_t;


static bool ca_config(void*& ctx, char* value) {
    char* p = strchr(value, ':');
    if (!p) {
        log(error, "syntax: certfile:keyfile");
        return false;
    }
    *p = '\0'; ++p;

    ssl_cert_t* ca = new ssl_cert_t();
    if (!mkcert(value, p, ca->cert, ca->key)) {
        delete ca;
        return false;
    }
    ctx = (void*)ca;
    return true;
}
static void ca_unconfig(void*& ctx) {
    if (ctx) {
        delete (ssl_cert_t*)ctx;
        ctx = NULL;
    }
}
CONF_DEF(config) {
    ConfigKey ca;
};
CONF_INIT(config) {
    CONF_KEY_INIT(ca, false, false, &ca_config, &ca_unconfig); // not reconfigurable atm as it is blocking
}


//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//\\//


static bool add_ext(X509* cert, int nid, const char* value) {
    // This sets the 'context' of the extensions; No configuration database.
    X509V3_CTX ctx;
    X509V3_set_ctx_nodb(&ctx);

    // Issuer and subject certs: both the target since it is self signed, no request and no CRL.
    X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0);
    X509_EXTENSION* ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, (char*)value); // XXX: this const cast ok?
    unless (ex) {
        ssl_log_err(notice);
        return false;
    }
    X509_add_ext(cert, ex, -1);
    X509_EXTENSION_free(ex);
    return true;
}


// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-22/openssl/demos/x509/mkcert.c
// http://fm4dd.com/openssl/
static bool mkcert(const char* commonname, int bits, int days, X509*& x, EVP_PKEY*& k) {
    k = NULL;
    x = NULL;
    RSA* rsa = NULL;
    BIGNUM* bne = NULL;
    time_t epoch = 0;
    ssl_cert_t* ca = config? config->ca.ctx_as<ssl_cert_t>(): NULL;

    k = EVP_PKEY_new();
    unless (k) goto err;

    x = X509_new();
    unless (x) goto err;

    rsa = RSA_new();
    unless (rsa) goto err;
    bne = BN_new();
    unless (BN_set_word(bne, RSA_F4) == 1) goto err;
    if (RSA_generate_key_ex(rsa, bits, bne, NULL) != 1) { // The pseudo-random number generator must be seeded prior to calling RSA_generate_key()
        goto err;
    }
    unless (EVP_PKEY_assign_RSA(k, rsa)) goto err;

    X509_set_version(x, 2); // v3 m)
    X509_set_pubkey(x, k);

    // set the commonname (only, as this could be stated in some CA nameConstraint)
    unless (X509_NAME_add_entry_by_txt(X509_get_subject_name(x), "CN", MBSTRING_ASC, (const unsigned char*)commonname, -1, -1, 0)) goto err;
    unless (add_ext(x, NID_netscape_ssl_server_name, commonname)) goto err;

    // set the serial number for the cert
    unless (ASN1_INTEGER_set(X509_get_serialNumber(x), (long)hash(commonname)+(long)NOW)) goto err; // The CA can choose the serial number in any way as it sees fit, not necessarily randomly (and it has to fit in 20 bytes). A CA is supposed to choose unique serial numbers, that is, unique for the CA;

    // set notBefore/notAfter
    unless (X509_time_adj(X509_get_notBefore(x), 0, &epoch)) goto err;
    unless (X509_time_adj(X509_get_notAfter(x), 60*60*24*days, &NOW)) goto err;

    // Add various extensions: standard extensions and some Netscape specific extensions
    if (!add_ext(x, NID_basic_constraints, "CA:FALSE") || // is no CA/root cert
        !add_ext(x, NID_key_usage, "keyEncipherment,dataEncipherment,keyAgreement") ||
        !add_ext(x, NID_ext_key_usage, "serverAuth") ||
        !add_ext(x, NID_subject_key_identifier, "hash") ||
        !add_ext(x, NID_netscape_cert_type, "server")) goto err;

    // (self-)signing & done
    if (ca) {
        unless (X509_set_issuer_name(x, X509_get_subject_name(ca->cert))) goto err;
        unless (X509_sign(x, ca->key, EVP_sha256())) goto err; // browsers might reject MD5
    } else {
        log(debug, "self-signing for '%s'", commonname);
        unless (X509_set_issuer_name(x, X509_get_subject_name(x))) goto err; // Its self signed so set the issuer name to be the same as the subject.
        unless (X509_sign(x, k, EVP_sha256())) goto err;
    }

    // TODO: somehow set preferred/allowed cipher suites
    return true;

    // rollback
    err:;
    ssl_log_err(notice);
    if (rsa) RSA_free(rsa);
    if (bne) BN_free(bne);
    if (x) X509_free(x);
    if (k) EVP_PKEY_free(k);
    x = NULL;
    k = NULL;
    return false;
}


bool mkcert(const char* cn, X509*& x, EVP_PKEY*& k) {
    return mkcert(cn, 1024, 365, x, k);
}


bool mkcert(const char* cert, const char* key, X509*& x, EVP_PKEY*& k) {
    x = NULL;
    k = NULL;
    FILE* xfp = NULL;
    FILE* kfp = NULL;
    if ((xfp = fopen(cert, "r")) == NULL) {
        log_errno(error, "fopen(%s)", cert);
        goto err;
    }
    if ((kfp = fopen(key, "r")) == NULL) {
        log_errno(error, "fopen(%s)", key);
        goto err;
    }
    if ((x = PEM_read_X509(xfp, &x, NULL, NULL)) == NULL) {
        ssl_log_err(notice);
        goto err;
    }
    if ((k = PEM_read_PrivateKey(kfp, &k, NULL, NULL)) == NULL) {
        ssl_log_err(notice);
        goto err;
    }
    fclose(xfp);
    fclose(kfp);
    return true;
    err:;
    if (xfp) fclose(xfp);
    if (kfp) fclose(kfp);
    if (x) X509_free(x);
    if (k) EVP_PKEY_free(k);
    x = NULL;
    k = NULL;
    return false;
}


bool mkcert(const char* cn, char*& cert, size_t& certlen, char*& key, size_t& keylen) {
    char* tmp;
    cert = NULL;
    key = NULL;

    X509* x = NULL;
    EVP_PKEY* k = NULL;
    if (!mkcert(cn, x, k)) {
        return false;
    }

    BIO* out = BIO_new(BIO_s_mem());
    unless (PEM_write_bio_X509(out, x)) {
        ssl_log_err(notice);
        goto err;
    }
    certlen = BIO_get_mem_data(out, &tmp);
    cert = strndup(tmp, certlen);

    BIO_vfree(out);
    out = BIO_new(BIO_s_mem());
    unless (PEM_write_bio_PrivateKey(out, k, NULL, NULL, 0, NULL, NULL)) {
        ssl_log_err(notice);
        goto err;
    }
    keylen = BIO_get_mem_data(out, &tmp);
    key = strndup(tmp, keylen);

    BIO_vfree(out);
    X509_free(x);
    EVP_PKEY_free(k);
    return true;

    err:;
    BIO_vfree(out);
    X509_free(x);
    EVP_PKEY_free(k);
    if (cert) free(cert);
    if (key) free(key);
    return false;
}


bool mkcert(const char* certbuf, size_t certlen, const char* keybuf, size_t keylen, X509*& cert, EVP_PKEY*& key) {
    BIO* out;

    out = BIO_new_mem_buf((void*)certbuf, certlen);
    cert = PEM_read_bio_X509(out, NULL, NULL, NULL);
    unless (cert) {
        ssl_log_err(notice);
        BIO_vfree(out);
        cert = NULL;
        key = NULL;
        return false;
    }
    BIO_vfree(out);

    out = BIO_new_mem_buf((void*)keybuf, keylen);
    key = PEM_read_bio_PrivateKey(out, NULL, NULL, NULL);
    unless (key) {
        ssl_log_err(notice);
        BIO_vfree(out);
        X509_free(cert);
        cert = NULL;
        key = NULL;
        return false;
    }
    BIO_vfree(out);

    return true;
}


SSL_CTX* ssl_ctx_get(bool server) {
    SSL_CTX* rv = SSL_CTX_new(server? SSLv23_server_method(): SSLv23_method());
    if (!rv) {
        ssl_log_err(notice);
        return NULL;
    }
    unless (SSL_CTX_set_cipher_list(rv, "ALL:!ADH:!EXP:!LOW:!RC2:!3DES:!SEED:!eNULL:!aNULL:!SRP:!CAMELLIA:!DSS:+HIGH:+ECDH:+MEDIUM")) die("no given cipher supported"); // TODO:
    SSL_CTX_set_verify(rv, SSL_VERIFY_NONE, NULL); // TODO: should we abort upon some errors, as we cannot pass these along?
    SSL_CTX_clear_options(rv, SSL_OP_SINGLE_DH_USE); // TODO: check whether this weakening has i.e. performance benefits
    SSL_CTX_set_options(rv, SSL_OP_ALL|SSL_OP_NO_SSLv2|SSL_OP_NO_TICKET|SSL_OP_LEGACY_SERVER_CONNECT); // SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION?
    if (server) SSL_CTX_set_options(rv, SSL_OP_CIPHER_SERVER_PREFERENCE); // honor upstream?
    if (!server) SSL_CTX_set_quiet_shutdown(rv, 1); // don't cause client warnings but violate the standard for upstream
    SSL_CTX_set_mode(rv, SSL_MODE_ENABLE_PARTIAL_WRITE);
    SSL_CTX_set_session_cache_mode(rv, SSL_SESS_CACHE_OFF); // no cache/tickets/sessions
    SSL_CTX_set_tmp_dh_callback(rv, ssl_dh_callback);
    SSL_CTX_set_tmp_rsa_callback(rv, &ssl_rsa_callback);
    return rv;
}


ssl_ctx_t* ssl_ctx_get(const char* certbuf, size_t certlen, const char* keybuf, size_t keylen) {
    SSL_CTX* ctx = NULL;
    X509* cert = NULL;
    EVP_PKEY* key = NULL;

    unless (mkcert(certbuf, certlen, keybuf, keylen, cert, key)) {
        return NULL;
    }
    assert(cert && key);

    ctx = ssl_ctx_get(true);
    unless (ctx) {
        goto err;
    }
    if (SSL_CTX_use_certificate(ctx, cert) != 1) {
        ssl_log_err(notice);
        goto err;
    }
    if (SSL_CTX_use_PrivateKey(ctx, key) != 1) {
        ssl_log_err(notice);
        goto err;
    }
    if (SSL_CTX_check_private_key(ctx) != 1) {
        ssl_log_err(notice);
        goto err;
    }
    X509_free(cert); // apparently not needed anymore and must be free'd??
    EVP_PKEY_free(key);

    return ctx;
    err:;
    if (ctx) SSL_CTX_free(ctx);
    if (cert) X509_free(cert);
    if (key) EVP_PKEY_free(key);
    return NULL;
}


void ssl_ctx_del(ssl_ctx_t* ctx) {
    SSL_CTX_free(ctx); // also calls the free()ing procedures for indirectly affected items, if applicable: the session cache, the list of ciphers, the list of Client CAs, the certificates and keys.
}


void ssl_ctx_up_ref(ssl_ctx_t* ctx) {
    // http://openssl.6102.n7.nabble.com/OpenSSL-1-1-SSL-CTX-issues-td62592.html
    #if 1
        CRYPTO_add(&ctx->references, 1, CRYPTO_LOCK_SSL_CTX);
    #else
        SSL_CTX_up_ref(ctx);
    #endif
}