/* 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 "tcn.h"
#include "apr_file_io.h"
#include "apr_thread_mutex.h"
#include "apr_atomic.h"
#include "apr_poll.h"

#include "ssl_private.h"

static int ssl_initialized = 0;
static char *ssl_global_rand_file = NULL;
extern apr_pool_t *tcn_global_pool;

tcn_pass_cb_t tcn_password_callback;

static BIO *key_log_file = NULL;

static void ssl_keylog_callback(const SSL *ssl, const char *line)
{
    if (key_log_file && line && *line) {
        BIO_puts(key_log_file, line);
        BIO_puts(key_log_file, "\n");
    }
}

/* From netty-tcnative */
static jclass byteArrayClass;
static jclass stringClass;

void SSL_callback_add_keylog(SSL_CTX *ctx)
{
    if (key_log_file) {
        SSL_CTX_set_keylog_callback(ctx, ssl_keylog_callback);
    }
}

static void init_bio_methods(void);
static void free_bio_methods(void);

TCN_IMPLEMENT_CALL(jint, SSL, version)(TCN_STDARGS)
{
    UNREFERENCED_STDARGS;
    return OpenSSL_version_num();
}

TCN_IMPLEMENT_CALL(jstring, SSL, versionString)(TCN_STDARGS)
{
    UNREFERENCED(o);
    return AJP_TO_JSTRING(OpenSSL_version(OPENSSL_VERSION));
}

/*
 *  the various processing hooks
 */
static apr_status_t ssl_init_cleanup(void *data)
{
    UNREFERENCED(data);

    if (!ssl_initialized)
        return APR_SUCCESS;
    ssl_initialized = 0;

    free_bio_methods();

    /* Openssl v1.1+ handles all termination automatically. */

    if (key_log_file) {
        BIO_free(key_log_file);
        key_log_file = NULL;
    }

    /* Don't call ERR_free_strings here; ERR_load_*_strings only
     * actually load the error strings once per process due to static
     * variable abuse in OpenSSL. */

    /*
     * TODO: determine somewhere we can safely shove out diagnostics
     *       (when enabled) at this late stage in the game:
     * CRYPTO_mem_leaks_fp(stderr);
     */
    return APR_SUCCESS;
}

/*
 * To ensure thread-safetyness in LibreSSL
 */

static unsigned long ssl_thread_id(void)
{
    return (unsigned long)tcn_get_thread_id();
}

static int ssl_rand_choosenum(int l, int h)
{
    int i;
    char buf[50];

    apr_snprintf(buf, sizeof(buf), "%.0f",
                 (((double)(rand()%RAND_MAX)/RAND_MAX)*(h-l)));
    i = atoi(buf)+1;
    if (i < l) i = l;
    if (i > h) i = h;
    return i;
}

static int ssl_rand_load_file(const char *file)
{
    char buffer[APR_PATH_MAX];
    int n;

    if (file == NULL)
        file = ssl_global_rand_file;
    if (file && (strcmp(file, "builtin") == 0))
        return -1;
    if (file == NULL)
        file = RAND_file_name(buffer, sizeof(buffer));
    if (file) {
        if (strncmp(file, "egd:", 4) == 0) {
#ifndef OPENSSL_NO_EGD
            if ((n = RAND_egd(file + 4)) > 0)
                return n;
            else
#endif
                return -1;
        }
        if ((n = RAND_load_file(file, -1)) > 0)
            return n;
    }
    return -1;
}

int SSL_rand_seed(const char *file)
{
    unsigned char stackdata[256];
    static volatile apr_uint32_t counter = 0;

    if (ssl_rand_load_file(file) < 0) {
        int n;
        struct {
            apr_time_t    t;
            pid_t         p;
            unsigned long i;
            apr_uint32_t  u;
        } _ssl_seed;
        if (counter == 0) {
            apr_generate_random_bytes(stackdata, 256);
            RAND_seed(stackdata, 128);
        }
        _ssl_seed.t = apr_time_now();
        _ssl_seed.p = getpid();
        _ssl_seed.i = ssl_thread_id();
        apr_atomic_inc32(&counter);
        _ssl_seed.u = counter;
        RAND_seed((unsigned char *)&_ssl_seed, sizeof(_ssl_seed));
        /*
         * seed in some current state of the run-time stack (128 bytes)
         */
        n = ssl_rand_choosenum(0, sizeof(stackdata)-128-1);
        RAND_seed(stackdata + n, 128);
    }
    return RAND_status();
}

TCN_IMPLEMENT_CALL(jint, SSL, initialize)(TCN_STDARGS, jstring engine)
{
    jclass clazz;
    jclass sClazz;

    TCN_ALLOC_CSTRING(engine);

    UNREFERENCED(o);
    if (!tcn_global_pool) {
        TCN_FREE_CSTRING(engine);
        tcn_ThrowAPRException(e, APR_EINVAL);
        return (jint)APR_EINVAL;
    }
    /* Check if already initialized */
    if (ssl_initialized++) {
        TCN_FREE_CSTRING(engine);
        return (jint)APR_SUCCESS;
    }

    memset(&tcn_password_callback, 0, sizeof(tcn_pass_cb_t));
    /* Initialize PRNG
     * This will in most cases call the builtin
     * low entropy seed.
     */
    SSL_rand_seed(NULL);
    /* For SSL_get_app_data2(), SSL_get_app_data3() and SSL_get_app_data4() at request time */
    SSL_init_app_data_idx();

    init_bio_methods();

    /*
     * Let us cleanup the ssl library when the library is unloaded
     */
    apr_pool_cleanup_register(tcn_global_pool, NULL,
                              ssl_init_cleanup,
                              apr_pool_cleanup_null);
    TCN_FREE_CSTRING(engine);

    /* Cache the byte[].class for performance reasons */
    clazz = (*e)->FindClass(e, "[B");
    byteArrayClass = (jclass) (*e)->NewGlobalRef(e, clazz);

    /* Cache the String.class for performance reasons */
    sClazz = (*e)->FindClass(e, "java/lang/String");
    stringClass = (jclass) (*e)->NewGlobalRef(e, sClazz);

    if (!key_log_file) {
        char *key_log_file_name = getenv("SSLKEYLOGFILE");
        if (key_log_file_name) {
            FILE *file = fopen(key_log_file_name, "a");
            if (file) {
                if (setvbuf(file, NULL, _IONBF, 0)) {
                    fclose(file);
                } else {
                    key_log_file = BIO_new_fp(file, BIO_CLOSE);
                }
            }
        }
    }

    return (jint)APR_SUCCESS;
}

TCN_IMPLEMENT_CALL(void, SSL, randSet)(TCN_STDARGS, jstring file)
{
    TCN_ALLOC_CSTRING(file);
    UNREFERENCED(o);
    if (J2S(file)) {
        ssl_global_rand_file = apr_pstrdup(tcn_global_pool, J2S(file));
    }
    TCN_FREE_CSTRING(file);
}

TCN_IMPLEMENT_CALL(jint, SSL, fipsModeGet)(TCN_STDARGS)
{
#if defined(LIBRESSL_VERSION_NUMBER)
    UNREFERENCED(o);
    /* LibreSSL doesn't support FIPS */
    return 0;
#else
    EVP_MD              *md;
    const OSSL_PROVIDER *provider;
    const char          *name;
    UNREFERENCED(o);

    // Maps the OpenSSL 3.x onwards behaviour to the OpenSSL 1.x API

    // Checks that FIPS is the default provider
    md = EVP_MD_fetch(NULL, "SHA-512", NULL);
    provider = EVP_MD_get0_provider(md);
    name = OSSL_PROVIDER_get0_name(provider);
    // Clean up
    EVP_MD_free(md);

    if (strcmp("fips", name)) {
        return 0;
    } else {
        return 1;
    }
#endif
}

TCN_IMPLEMENT_CALL(jint, SSL, fipsModeSet)(TCN_STDARGS, jint mode)
{
    int r = 0;
    UNREFERENCED(o);

    /* This method should never be called when using Tomcat Native 2.x onwards */
    tcn_ThrowException(e, "fipsModeSet is not supported in Tomcat Native 2.x onwards.");

    return r;
}

/* OpenSSL Java Stream BIO */

typedef struct  {
    int            refcount;
    apr_pool_t     *pool;
    tcn_callback_t cb;
} BIO_JAVA;


static apr_status_t generic_bio_cleanup(void *data)
{
    BIO *b = (BIO *)data;

    if (b) {
        BIO_free(b);
    }
    return APR_SUCCESS;
}

void SSL_BIO_close(BIO *bi)
{
    BIO_JAVA *j;
    if (bi == NULL)
        return;
    j = (BIO_JAVA *)BIO_get_data(bi);
    if (j != NULL && BIO_test_flags(bi, SSL_BIO_FLAG_CALLBACK)) {
        j->refcount--;
        if (j->refcount == 0) {
            if (j->pool)
                apr_pool_cleanup_run(j->pool, bi, generic_bio_cleanup);
            else
                BIO_free(bi);
        }
    }
    else
        BIO_free(bi);
}

void SSL_BIO_doref(BIO *bi)
{
    BIO_JAVA *j;
    if (bi == NULL)
        return;
    j = (BIO_JAVA *)BIO_get_data(bi);
    if (j != NULL && BIO_test_flags(bi, SSL_BIO_FLAG_CALLBACK)) {
        j->refcount++;
    }
}


static int jbs_new(BIO *bi)
{
    BIO_JAVA *j;

    if ((j = OPENSSL_malloc(sizeof(BIO_JAVA))) == NULL)
        return 0;
    j->pool      = NULL;
    j->refcount  = 1;
    BIO_set_shutdown(bi, 1);
    BIO_set_init(bi, 0);
    BIO_set_data(bi, (void *)j);

    return 1;
}

static int jbs_free(BIO *bi)
{
    BIO_JAVA *j;
    if (bi == NULL)
        return 0;
    j = (BIO_JAVA *)BIO_get_data(bi);
    if (j != NULL) {
        if (BIO_get_init(bi)) {
            JNIEnv   *e = NULL;
            BIO_set_init(bi, 0);
            tcn_get_java_env(&e);
            TCN_UNLOAD_CLASS(e, j->cb.obj);
        }
        OPENSSL_free(j);
    }
    BIO_set_data(bi, NULL);
    return 1;
}

static int jbs_write(BIO *b, const char *in, int inl)
{
    jint ret = -1;
    if (BIO_get_init(b) && in != NULL) {
        BIO_JAVA *j = (BIO_JAVA *)BIO_get_data(b);
        JNIEnv   *e = NULL;
        jbyteArray jb;
        tcn_get_java_env(&e);
        jb = (*e)->NewByteArray(e, inl);
        if (!(*e)->ExceptionOccurred(e)) {
            BIO_clear_retry_flags(b);
            (*e)->SetByteArrayRegion(e, jb, 0, inl, (jbyte *)in);
            ret = (*e)->CallIntMethod(e, j->cb.obj,
                                      j->cb.mid[0], jb);
            (*e)->ReleaseByteArrayElements(e, jb, (jbyte *)in, JNI_ABORT);
            (*e)->DeleteLocalRef(e, jb);
        }
    }
    /* From netty-tc-native, in the AF we were returning 0 */
    if (ret == 0) {
        BIO_set_retry_write(b);
        ret = -1;
    }
    return ret;
}

static int jbs_read(BIO *b, char *out, int outl)
{
    jint ret = 0;
    if (BIO_get_init(b) && out != NULL) {
        BIO_JAVA *j = (BIO_JAVA *)BIO_get_data(b);
        JNIEnv   *e = NULL;
        jbyteArray jb;
        tcn_get_java_env(&e);
        jb = (*e)->NewByteArray(e, outl);
        if (!(*e)->ExceptionOccurred(e)) {
            BIO_clear_retry_flags(b);
            ret = (*e)->CallIntMethod(e, j->cb.obj,
                                      j->cb.mid[1], jb);
            if (ret > 0) {
                jbyte *jout = (*e)->GetPrimitiveArrayCritical(e, jb, NULL);
                memcpy(out, jout, ret);
                (*e)->ReleasePrimitiveArrayCritical(e, jb, jout, 0);
            } else if (outl != 0) {
                ret = -1;
                BIO_set_retry_read(b);
            }
            (*e)->DeleteLocalRef(e, jb);
        }
    }
    return ret;
}

static int jbs_puts(BIO *b, const char *in)
{
    int ret = 0;
    if (BIO_get_init(b) && in != NULL) {
        BIO_JAVA *j = (BIO_JAVA *)BIO_get_data(b);
        JNIEnv   *e = NULL;
        tcn_get_java_env(&e);
        ret = (*e)->CallIntMethod(e, j->cb.obj,
                                  j->cb.mid[2],
                                  tcn_new_string(e, in));
    }
    return ret;
}

static int jbs_gets(BIO *b, char *out, int outl)
{
    int ret = 0;
    if (BIO_get_init(b) && out != NULL) {
        BIO_JAVA *j = (BIO_JAVA *)BIO_get_data(b);
        JNIEnv   *e = NULL;
        jobject  o;
        tcn_get_java_env(&e);
        if ((o = (*e)->CallObjectMethod(e, j->cb.obj,
                            j->cb.mid[3], (jint)(outl - 1)))) {
            TCN_ALLOC_CSTRING(o);
            if (J2S(o)) {
                int l = (int)strlen(J2S(o));
                if (l < outl) {
                    strcpy(out, J2S(o));
                    ret = outl;
                }
            }
            TCN_FREE_CSTRING(o);
        }
    }
    return ret;
}

static long jbs_ctrl(BIO *b, int cmd, long num, void *ptr)
{
    int ret = 0;
    switch (cmd) {
        case BIO_CTRL_FLUSH:
            ret = 1;
            break;
        default:
            ret = 0;
            break;
    }
    return ret;
}

static BIO_METHOD *jbs_methods = NULL;

static void init_bio_methods(void)
{
    jbs_methods = BIO_meth_new(BIO_TYPE_FILE, "Java Callback");
    BIO_meth_set_write(jbs_methods, &jbs_write);
    BIO_meth_set_read(jbs_methods, &jbs_read);
    BIO_meth_set_puts(jbs_methods, &jbs_puts);
    BIO_meth_set_gets(jbs_methods, &jbs_gets);
    BIO_meth_set_ctrl(jbs_methods, &jbs_ctrl);
    BIO_meth_set_create(jbs_methods, &jbs_new);
    BIO_meth_set_destroy(jbs_methods, &jbs_free);
}

static void free_bio_methods(void)
{
    BIO_meth_free(jbs_methods);
}

/*** Begin Twitter 1:1 API addition ***/
TCN_IMPLEMENT_CALL(jint, SSL, getLastErrorNumber)(TCN_STDARGS) {
    UNREFERENCED_STDARGS;
    return SSL_ERR_get();
}

static void ssl_info_callback(const SSL *ssl, int where, int ret) {
    int *handshakeCount = NULL;
    if (0 != (where & SSL_CB_HANDSHAKE_DONE)) {
        handshakeCount = (int*) SSL_get_app_data3(ssl);
        if (handshakeCount != NULL) {
            ++(*handshakeCount);
        }
    }
}

static apr_status_t ssl_con_pool_cleanup(void *data)
{
    SSL *ssl = (SSL*) data;
    int *destroyCount;

    TCN_ASSERT(ssl != 0);

    destroyCount = SSL_get_app_data4(ssl);
    if (destroyCount != NULL) {
        ++(*destroyCount);
    }

    return APR_SUCCESS;
}

TCN_IMPLEMENT_CALL(jlong /* SSL * */, SSL, newSSL)(TCN_STDARGS,
                                                   jlong ctx /* tcn_ssl_ctxt_t * */,
                                                   jboolean server) {
    tcn_ssl_ctxt_t *c = J2P(ctx, tcn_ssl_ctxt_t *);
    int *handshakeCount = malloc(sizeof(int));
    int *destroyCount = malloc(sizeof(int));
    SSL *ssl;
    apr_pool_t *p = NULL;
    tcn_ssl_conn_t *con;

    UNREFERENCED_STDARGS;

    TCN_ASSERT(ctx != 0);

    ssl = SSL_new(c->ctx);
    if (ssl == NULL) {
        free(handshakeCount);
        free(destroyCount);
        tcn_ThrowException(e, "cannot create new ssl");
        return 0;
    }

    apr_pool_create(&p, c->pool);
    if (p == NULL) {
        free(handshakeCount);
        free(destroyCount);
        SSL_free(ssl);
        tcn_ThrowAPRException(e, apr_get_os_error());
        return 0;
    }

    if ((con = apr_pcalloc(p, sizeof(tcn_ssl_conn_t))) == NULL) {
        free(handshakeCount);
        free(destroyCount);
        SSL_free(ssl);
        apr_pool_destroy(p);
        tcn_ThrowAPRException(e, apr_get_os_error());
        return 0;
    }
    con->pool = p;
    con->ctx  = c;
    con->ssl  = ssl;
    con->shutdown_type = c->shutdown_type;

    /* Store the handshakeCount in the SSL instance. */
    *handshakeCount = 0;
    SSL_set_app_data3(ssl, handshakeCount);

    /* Store the destroyCount in the SSL instance. */
    *destroyCount = 0;
    SSL_set_app_data4(ssl, destroyCount);

    /* Add callback to keep track of handshakes. */
    SSL_CTX_set_info_callback(c->ctx, ssl_info_callback);

    if (server) {
        SSL_set_accept_state(ssl);
    } else {
        SSL_set_connect_state(ssl);
    }

    /* Setup verify and seed */
    SSL_set_verify_result(ssl, X509_V_OK);
    SSL_rand_seed(c->rand_file);

    /* Store for later usage in SSL_callback_SSL_verify */
    SSL_set_app_data2(ssl, c);
    SSL_set_app_data(ssl, con);
    /* Register cleanup that prevent double destruction */
    apr_pool_cleanup_register(con->pool, (const void *)ssl,
                              ssl_con_pool_cleanup,
                              apr_pool_cleanup_null);

    return P2J(ssl);
}

/* How much did SSL write into this BIO? */
TCN_IMPLEMENT_CALL(jint /* nbytes */, SSL, pendingWrittenBytesInBIO)(TCN_STDARGS,
                                                                     jlong bio /* BIO * */) {
    UNREFERENCED_STDARGS;

    return BIO_ctrl_pending(J2P(bio, BIO *));
}

/* How much is available for reading in the given SSL struct? */
TCN_IMPLEMENT_CALL(jint, SSL, pendingReadableBytesInSSL)(TCN_STDARGS, jlong ssl /* SSL * */) {
    UNREFERENCED_STDARGS;

    return SSL_pending(J2P(ssl, SSL *));
}

/* Write wlen bytes from wbuf into bio */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, writeToBIO)(TCN_STDARGS,
                                                       jlong bio /* BIO * */,
                                                       jlong wbuf /* char* */,
                                                       jint wlen /* sizeof(wbuf) */) {
    UNREFERENCED_STDARGS;

    return BIO_write(J2P(bio, BIO *), J2P(wbuf, void *), wlen);

}

/* Read up to rlen bytes from bio into rbuf */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, readFromBIO)(TCN_STDARGS,
                                                        jlong bio /* BIO * */,
                                                        jlong rbuf /* char * */,
                                                        jint rlen /* sizeof(rbuf) - 1 */) {
    UNREFERENCED_STDARGS;

    return BIO_read(J2P(bio, BIO *), J2P(rbuf, void *), rlen);
}

/* Write up to wlen bytes of application data to the ssl BIO (encrypt) */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, writeToSSL)(TCN_STDARGS,
                                                       jlong ssl /* SSL * */,
                                                       jlong wbuf /* char * */,
                                                       jint wlen /* sizeof(wbuf) */) {
    UNREFERENCED_STDARGS;

    return SSL_write(J2P(ssl, SSL *), J2P(wbuf, void *), wlen);
}

/* Read up to rlen bytes of application data from the given SSL BIO (decrypt) */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, readFromSSL)(TCN_STDARGS,
                                                        jlong ssl /* SSL * */,
                                                        jlong rbuf /* char * */,
                                                        jint rlen /* sizeof(rbuf) - 1 */) {
    UNREFERENCED_STDARGS;

    return SSL_read(J2P(ssl, SSL *), J2P(rbuf, void *), rlen);
}

/* Get the shutdown status of the engine */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, getShutdown)(TCN_STDARGS,
                                                        jlong ssl /* SSL * */) {
    UNREFERENCED_STDARGS;

    return SSL_get_shutdown(J2P(ssl, SSL *));
}

/* Free the SSL * and its associated internal BIO */
TCN_IMPLEMENT_CALL(void, SSL, freeSSL)(TCN_STDARGS,
                                       jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);
    int *handshakeCount = SSL_get_app_data3(ssl_);
    int *destroyCount = SSL_get_app_data4(ssl_);
    tcn_ssl_conn_t *con = SSL_get_app_data(ssl_);

    UNREFERENCED_STDARGS;

    if (destroyCount != NULL) {
        if (*destroyCount == 0) {
            apr_pool_destroy(con->pool);
        }
        free(destroyCount);
    }
    if (handshakeCount != NULL) {
        free(handshakeCount);
    }
    SSL_free(ssl_);
}

/* Make a BIO pair (network and internal) for the provided SSL * and return the network BIO */
TCN_IMPLEMENT_CALL(jlong, SSL, makeNetworkBIO)(TCN_STDARGS,
                                               jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);
    BIO *internal_bio;
    BIO *network_bio;

    UNREFERENCED(o);

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        goto fail;
    }

    if (BIO_new_bio_pair(&internal_bio, 0, &network_bio, 0) != 1) {
        tcn_ThrowException(e, "BIO_new_bio_pair failed");
        goto fail;
    }

    SSL_set_bio(ssl_, internal_bio, internal_bio);

    return P2J(network_bio);
 fail:
    return 0;
}

/* Free a BIO * (typically, the network BIO) */
TCN_IMPLEMENT_CALL(void, SSL, freeBIO)(TCN_STDARGS,
                                       jlong bio /* BIO * */) {
    BIO *bio_;
    UNREFERENCED_STDARGS;

    bio_ = J2P(bio, BIO *);
    BIO_free(bio_);
}

/* Send CLOSE_NOTIFY to peer */
TCN_IMPLEMENT_CALL(jint /* status */, SSL, shutdownSSL)(TCN_STDARGS,
                                                        jlong ssl /* SSL * */) {
    UNREFERENCED_STDARGS;

    return SSL_shutdown(J2P(ssl, SSL *));
}

/* Read which cipher was negotiated for the given SSL *. */
TCN_IMPLEMENT_CALL(jstring, SSL, getCipherForSSL)(TCN_STDARGS,
                                                  jlong ssl /* SSL * */)
{
    UNREFERENCED_STDARGS;

    return AJP_TO_JSTRING(SSL_get_cipher(J2P(ssl, SSL*)));
}

/* Read which protocol was negotiated for the given SSL *. */
TCN_IMPLEMENT_CALL(jstring, SSL, getVersion)(TCN_STDARGS,
                                                  jlong ssl /* SSL * */)
{
    UNREFERENCED_STDARGS;

    return AJP_TO_JSTRING(SSL_get_version(J2P(ssl, SSL*)));
}

/* Is the handshake over yet? */
TCN_IMPLEMENT_CALL(jint, SSL, isInInit)(TCN_STDARGS,
                                        jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);

    UNREFERENCED(o);

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    } else {
        return SSL_in_init(ssl_);
    }
}

TCN_IMPLEMENT_CALL(jint, SSL, doHandshake)(TCN_STDARGS,
                                           jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);
    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    return SSL_do_handshake(ssl_);
}

TCN_IMPLEMENT_CALL(jint, SSL, renegotiate)(TCN_STDARGS,
                                           jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);
    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    return SSL_renegotiate(ssl_);
}

TCN_IMPLEMENT_CALL(jint, SSL, renegotiatePending)(TCN_STDARGS,
                                                  jlong ssl /* SSL * */) {
    SSL *ssl_ = J2P(ssl, SSL *);
    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    return SSL_renegotiate_pending(ssl_);
}

TCN_IMPLEMENT_CALL(jint, SSL, verifyClientPostHandshake)(TCN_STDARGS,
                                                         jlong ssl /* SSL * */) {
#if defined(SSL_OP_NO_TLSv1_3)
    SSL *ssl_ = J2P(ssl, SSL *);
    tcn_ssl_conn_t *con;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    con = (tcn_ssl_conn_t *)SSL_get_app_data(ssl_);
    con->pha_state = PHA_STARTED;

    return SSL_verify_client_post_handshake(ssl_);
#else
    return 0;
#endif
}

TCN_IMPLEMENT_CALL(jint, SSL, getPostHandshakeAuthInProgress)(TCN_STDARGS,
                                                              jlong ssl /* SSL * */) {
#if defined(SSL_OP_NO_TLSv1_3)
    SSL *ssl_ = J2P(ssl, SSL *);
    tcn_ssl_conn_t *con;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    con = (tcn_ssl_conn_t *)SSL_get_app_data(ssl_);

    return (con->pha_state == PHA_STARTED);
#else
    return 0;
#endif
}

/*** End Twitter API Additions ***/

/*** Apple API Additions ***/

TCN_IMPLEMENT_CALL(jstring, SSL, getAlpnSelected)(TCN_STDARGS,
                                                         jlong ssl /* SSL * */) {
    /* Looks fishy we have the same in sslnetwork.c, it set by socket/connection */
    SSL *ssl_ = J2P(ssl, SSL *);
    const unsigned char *proto;
    unsigned int proto_len;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return NULL;
    }

    UNREFERENCED(o);

    SSL_get0_alpn_selected(ssl_, &proto, &proto_len);
    return tcn_new_stringn(e, (const char *) proto, (size_t) proto_len);
}

TCN_IMPLEMENT_CALL(jobjectArray, SSL, getPeerCertChain)(TCN_STDARGS,
                                                  jlong ssl /* SSL * */)
{
    STACK_OF(X509) *sk;
    int len;
    int i;
    X509 *cert;
    int length;
    unsigned char *buf;
    jobjectArray array;
    jbyteArray bArray;

    SSL *ssl_ = J2P(ssl, SSL *);

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return NULL;
    }

    UNREFERENCED(o);

    // Get a stack of all certs in the chain.
    sk = SSL_get_peer_cert_chain(ssl_);

    len = sk_X509_num(sk);
    if (len <= 0) {
        /* No peer certificate chain as no auth took place yet, or the auth was not successful. */
        return NULL;
    }
    /* Create the byte[][] array that holds all the certs */
    array = (*e)->NewObjectArray(e, len, byteArrayClass, NULL);

    for(i = 0; i < len; i++) {
        cert = (X509*) sk_X509_value(sk, i);

        buf = NULL;
        length = i2d_X509(cert, &buf);
        if (length < 0) {
            OPENSSL_free(buf);
            /* In case of error just return an empty byte[][] */
            return (*e)->NewObjectArray(e, 0, byteArrayClass, NULL);
        }
        bArray = (*e)->NewByteArray(e, length);
        (*e)->SetByteArrayRegion(e, bArray, 0, length, (jbyte*) buf);
        (*e)->SetObjectArrayElement(e, array, i, bArray);

        /*
         * Delete the local reference as we not know how long the chain is and local references are otherwise
         * only freed once jni method returns.
         */
        (*e)->DeleteLocalRef(e, bArray);

        OPENSSL_free(buf);
    }
    return array;
}

TCN_IMPLEMENT_CALL(jbyteArray, SSL, getPeerCertificate)(TCN_STDARGS,
                                                  jlong ssl /* SSL * */)
{
    X509 *cert;
    int length;
    unsigned char *buf = NULL;
    jbyteArray bArray;

    SSL *ssl_ = J2P(ssl, SSL *);

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return NULL;
    }

    UNREFERENCED(o);

    /* Get a stack of all certs in the chain */
    cert = SSL_get_peer_certificate(ssl_);
    if (cert == NULL) {
        return NULL;
    }

    length = i2d_X509(cert, &buf);

    bArray = (*e)->NewByteArray(e, length);
    (*e)->SetByteArrayRegion(e, bArray, 0, length, (jbyte*) buf);

    /*
     * We need to free the cert as the reference count is incremented by one and it is not destroyed when the
     * session is freed.
     * See https://www.openssl.org/docs/ssl/SSL_get_peer_certificate.html
     */
    X509_free(cert);

    OPENSSL_free(buf);

    return bArray;
}

TCN_IMPLEMENT_CALL(jstring, SSL, getErrorString)(TCN_STDARGS, jlong number)
{
    char buf[TCN_OPENSSL_ERROR_STRING_LENGTH];
    UNREFERENCED(o);
    ERR_error_string_n(number, buf, TCN_OPENSSL_ERROR_STRING_LENGTH);
    return tcn_new_string(e, buf);
}

TCN_IMPLEMENT_CALL(jlong, SSL, getTime)(TCN_STDARGS, jlong ssl)
{
    const SSL *ssl_ = J2P(ssl, SSL *);
    const SSL_SESSION *session;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    UNREFERENCED(o);

    session  = SSL_get_session(ssl_);
    if (session) {
#if (OPENSSL_VERSION_NUMBER > 0x302FFFFFL)
        return SSL_SESSION_get_time_ex(session);
#else
        return SSL_SESSION_get_time(session);
#endif
    } else {
        tcn_ThrowException(e, "ssl session is null");
        return 0;
    }
}

TCN_IMPLEMENT_CALL(void, SSL, setVerify)(TCN_STDARGS, jlong ssl,
                                                jint level, jint depth)
{
    tcn_ssl_ctxt_t *c;
    int verify;
    SSL *ssl_ = J2P(ssl, SSL *);

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return;
    }

    c = SSL_get_app_data2(ssl_);

    verify = SSL_VERIFY_NONE;

    UNREFERENCED(o);

    if (c == NULL) {
        tcn_ThrowException(e, "context is null");
        return;
    }
    c->verify_mode = level;

    if (c->verify_mode == SSL_CVERIFY_UNSET)
        c->verify_mode = SSL_CVERIFY_NONE;
    if (depth > 0)
        c->verify_depth = depth;
    /*
     *  Configure callbacks for SSL context
     */
    if (c->verify_mode == SSL_CVERIFY_REQUIRE)
        verify |= SSL_VERIFY_PEER_STRICT;
    if ((c->verify_mode == SSL_CVERIFY_OPTIONAL) ||
        (c->verify_mode == SSL_CVERIFY_OPTIONAL_NO_CA))
        verify |= SSL_VERIFY_PEER;
    if (!c->store)
        c->store = SSL_CTX_get_cert_store(c->ctx);

    SSL_set_verify(ssl_, verify, SSL_callback_SSL_verify);
}

TCN_IMPLEMENT_CALL(void, SSL, setOptions)(TCN_STDARGS, jlong ssl,
                                                 jint opt)
{
    SSL *ssl_ = J2P(ssl, SSL *);

    UNREFERENCED_STDARGS;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return;
    }

#ifndef SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
    /* Clear the flag if not supported */
    if (opt & 0x00040000) {
        opt &= ~0x00040000;
    }
#endif
    SSL_set_options(ssl_, opt);
}

TCN_IMPLEMENT_CALL(jint, SSL, getOptions)(TCN_STDARGS, jlong ssl)
{
    SSL *ssl_ = J2P(ssl, SSL *);

    UNREFERENCED_STDARGS;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return 0;
    }

    return SSL_get_options(ssl_);
}

TCN_IMPLEMENT_CALL(jobjectArray, SSL, getCiphers)(TCN_STDARGS, jlong ssl)
{
    STACK_OF(SSL_CIPHER) *sk;
    int len;
    jobjectArray array;
    SSL_CIPHER *cipher;
    const char *name;
    int i;
    jstring c_name;
    SSL *ssl_ = J2P(ssl, SSL *);

    UNREFERENCED_STDARGS;

    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return NULL;
    }

    sk = SSL_get_ciphers(ssl_);
    len = sk_SSL_CIPHER_num(sk);

    if (len <= 0) {
        /* No peer certificate chain as no auth took place yet, or the auth was not successful. */
        return NULL;
    }

    /* Ensure stringClass is initialized (lazy initialization) */
    if (stringClass == NULL) {
        jclass sClazz = (*e)->FindClass(e, "java/lang/String");
        stringClass = (jclass) (*e)->NewGlobalRef(e, sClazz);
    }

    /* Create the byte[][] array that holds all the certs */
    array = (*e)->NewObjectArray(e, len, stringClass, NULL);

    for (i = 0; i < len; i++) {
        cipher = (SSL_CIPHER*) sk_SSL_CIPHER_value(sk, i);
        name = SSL_CIPHER_get_name(cipher);

        c_name = (*e)->NewStringUTF(e, name);
        (*e)->SetObjectArrayElement(e, array, i, c_name);
    }
    return array;
}

TCN_IMPLEMENT_CALL(jboolean, SSL, setCipherSuites)(TCN_STDARGS, jlong ssl,
                                                         jstring cipherList)
{
    SSL *ssl_ = J2P(ssl, SSL *);
    TCN_ALLOC_CSTRING(cipherList);
    jboolean rv = JNI_TRUE;
    #ifndef HAVE_EXPORT_CIPHERS
        size_t len;
        char *buf;
    #endif
    UNREFERENCED(o);

    if (ssl_ == NULL) {
        TCN_FREE_CSTRING(cipherList);
        tcn_ThrowException(e, "ssl is null");
        return JNI_FALSE;
    }

    if (!J2S(cipherList)) {
        rv = JNI_FALSE;
        goto free_cipherList;
    }

#ifndef HAVE_EXPORT_CIPHERS
    /*
     *  Always disable NULL and export ciphers,
     *  no matter what was given in the config.
     */
    len = strlen(J2S(cipherList)) + strlen(SSL_CIPHERS_ALWAYS_DISABLED) + 1;
    buf = malloc(len * sizeof(char));
    if (buf == NULL) {
        rv = JNI_FALSE;
        goto free_cipherList;
    }
    memcpy(buf, SSL_CIPHERS_ALWAYS_DISABLED, strlen(SSL_CIPHERS_ALWAYS_DISABLED));
    memcpy(buf + strlen(SSL_CIPHERS_ALWAYS_DISABLED), J2S(cipherList), strlen(J2S(cipherList)));
    buf[len - 1] = '\0';
    if (!SSL_set_cipher_list(ssl_, buf)) {
#else
    if (!SSL_set_cipher_list(ssl_, J2S(cipherList))) {
#endif
        char err[TCN_OPENSSL_ERROR_STRING_LENGTH];
        ERR_error_string_n(SSL_ERR_get(), err, TCN_OPENSSL_ERROR_STRING_LENGTH);
        tcn_Throw(e, "Unable to configure permitted SSL ciphers (%s)", err);
        rv = JNI_FALSE;
    }
#ifndef HAVE_EXPORT_CIPHERS
    free(buf);
#endif
free_cipherList:
    TCN_FREE_CSTRING(cipherList);
    return rv;
}

TCN_IMPLEMENT_CALL(jboolean, SSL, setCipherSuitesEx)(TCN_STDARGS, jlong ssl,
                                                         jstring cipherSuites)
{
    SSL *ssl_ = J2P(ssl, SSL *);
    TCN_ALLOC_CSTRING(cipherSuites);
    jboolean rv = JNI_TRUE;
    UNREFERENCED(o);

    if (ssl_ == NULL) {
        TCN_FREE_CSTRING(cipherSuites);
        tcn_ThrowException(e, "ssl is null");
        return JNI_FALSE;
    }

    if (!J2S(cipherSuites)) {
        rv = JNI_FALSE;
        goto free_cipherSuites;
    }

    if (!SSL_set_ciphersuites(ssl_, J2S(cipherSuites))) {
        char err[TCN_OPENSSL_ERROR_STRING_LENGTH];
        ERR_error_string_n(SSL_ERR_get(), err, TCN_OPENSSL_ERROR_STRING_LENGTH);
        tcn_Throw(e, "Unable to configure permitted SSL cipher suites (%s)", err);
        rv = JNI_FALSE;
    }

free_cipherSuites:
    TCN_FREE_CSTRING(cipherSuites);
    return rv;
}

TCN_IMPLEMENT_CALL(jbyteArray, SSL, getSessionId)(TCN_STDARGS, jlong ssl)
{

    unsigned int len;
    const unsigned char *session_id;
    const SSL_SESSION *session;
    jbyteArray bArray;
    SSL *ssl_ = J2P(ssl, SSL *);
    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return NULL;
    }
    UNREFERENCED(o);
    session = SSL_get_session(ssl_);
    if (NULL == session) {
        return NULL;
    }

    session_id = SSL_SESSION_get_id(session, &len);

    if (len == 0 || session_id == NULL) {
        return NULL;
    }

    bArray = (*e)->NewByteArray(e, len);
    (*e)->SetByteArrayRegion(e, bArray, 0, len, (jbyte*) session_id);
    return bArray;
}

TCN_IMPLEMENT_CALL(jint, SSL, getHandshakeCount)(TCN_STDARGS, jlong ssl)
{
    int *handshakeCount = NULL;
    SSL *ssl_ = J2P(ssl, SSL *);
    if (ssl_ == NULL) {
        tcn_ThrowException(e, "ssl is null");
        return -1;
    }
    UNREFERENCED(o);

    handshakeCount = SSL_get_app_data3(ssl_);
    if (handshakeCount != NULL) {
        return *handshakeCount;
    }
    return 0;
}

/*** End Apple API Additions ***/
