#include <mongoc/mongoc.h>

#include <bson/memory.h>

#include <errno.h>
#include <stddef.h>
#include <stdint.h>


#ifdef MONGOC_ENABLE_SSL_OPENSSL
#include <openssl/err.h>
#endif

#include <TestSuite.h>
#include <ssl-test.h>
#include <test-conveniences.h>
#include <test-libmongoc.h>

#if !defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)

static void
test_mongoc_tls_no_certs(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   capture_logs(true);
   /* No server cert is not valid TLS at all */
   ssl_test(&copt, &sopt, "doesnt_matter", &cr, &sr);

   ASSERT_CMPINT(cr.result, !=, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, !=, SSL_TEST_SUCCESS);
}


#ifdef MONGOC_ENABLE_SSL_OPENSSL
static void
test_mongoc_tls_password(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_PASSWORD_PROTECTED;
   copt.pem_pwd = CERT_PASSWORD;

   /* Password protected key */
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);
   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}

static void
test_mongoc_tls_bad_password(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_PASSWORD_PROTECTED;
   copt.pem_pwd = "incorrect password";


   capture_logs(true);
   /* Incorrect password cannot unlock the key */
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SSL_HANDSHAKE);
   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SSL_INIT);

   /* Change it to the right password */
   copt.pem_pwd = CERT_PASSWORD;
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}
#endif


static void
test_mongoc_tls_no_verify(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;
   /* Weak cert validation allows never fails */
   copt.weak_cert_validation = 1;

   ssl_test(&copt, &sopt, "bad_domain.com", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


static void
test_mongoc_tls_allow_invalid_hostname(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;
   copt.allow_invalid_hostname = 1;

   /* Connect to a domain not listed in the cert */
   ssl_test(&copt, &sopt, "bad_domain.com", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


static void
test_mongoc_tls_bad_verify(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   capture_logs(true);
   ssl_test(&copt, &sopt, "bad_domain.com", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SSL_HANDSHAKE);
   ASSERT_CMPINT(sr.result, !=, SSL_TEST_SUCCESS);


   /* weak_cert_validation allows bad domains */
   copt.weak_cert_validation = 1;
   ssl_test(&copt, &sopt, "bad_domain.com", &cr, &sr);
   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


static void
test_mongoc_tls_basic(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


#ifdef MONGOC_ENABLE_SSL_OPENSSL
static void
test_mongoc_tls_weak_cert_validation(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.crl_file = CERT_CRL;
   copt.pem_file = CERT_CLIENT;

   capture_logs(true);
   /* Certificate has has been revoked, this should fail */
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SSL_HANDSHAKE);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SSL_HANDSHAKE);


   /* weak_cert_validation allows revoked certs */
   copt.weak_cert_validation = 1;
   ssl_test(&copt, &sopt, "bad_domain.com", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


static void
test_mongoc_tls_crl(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);

   copt.crl_file = CERT_CRL;
   capture_logs(true);
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SSL_HANDSHAKE);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SSL_HANDSHAKE);

   /* Weak cert validation allows revoked */
   copt.weak_cert_validation = true;
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);
   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}
#endif


static void
test_mongoc_tls_expired(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_EXPIRED;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   capture_logs(true);
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SSL_HANDSHAKE);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SSL_HANDSHAKE);

   /* Weak cert validation allows expired certs */
   copt.weak_cert_validation = true;
   ssl_test(&copt, &sopt, "localhost", &cr, &sr);
   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


#if !defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
static void
test_mongoc_tls_common_name(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_COMMONNAME;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   /* Match against commonName */
   ssl_test(&copt, &sopt, "commonName.mongodb.org", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}
#endif // !defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)


static void
test_mongoc_tls_altname(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_ALTNAME;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   /* Match against secondary Subject Alternative Name (SAN) */
   ssl_test(&copt, &sopt, "alternative.mongodb.com", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


static void
test_mongoc_tls_wild(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_WILD;

   copt.ca_file = CERT_CA;
   copt.pem_file = CERT_CLIENT;

   ssl_test(&copt, &sopt, "anything.mongodb.org", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}


#ifdef MONGOC_ENABLE_SSL_OPENSSL
static void
test_mongoc_tls_ip(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.ca_file = CERT_CA;
   sopt.pem_file = CERT_SERVER;

   copt.ca_file = CERT_CA;

   ssl_test(&copt, &sopt, "127.0.0.1", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}
#endif


#if !defined(__APPLE__) && !defined(_WIN32) && defined(MONGOC_ENABLE_SSL_OPENSSL) && \
   OPENSSL_VERSION_NUMBER >= 0x10000000L
static void
test_mongoc_tls_trust_dir(void)
{
   mongoc_ssl_opt_t sopt = {0};
   mongoc_ssl_opt_t copt = {0};
   ssl_test_result_t sr;
   ssl_test_result_t cr;

   sopt.pem_file = CERT_SERVER;
   sopt.ca_file = CERT_CA;

   copt.ca_dir = CERT_TEST_DIR;

   ssl_test(&copt, &sopt, "localhost", &cr, &sr);

   ASSERT_CMPINT(cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT(sr.result, ==, SSL_TEST_SUCCESS);
}
#endif

#if defined(MONGOC_ENABLE_SSL_OPENSSL)
typedef struct {
   mongoc_stream_t base;
   mongoc_stream_t *wrapped;
   int eagain_remaining;
} _eagain_stream_t;

static ssize_t
_eagain_readv(mongoc_stream_t *s, mongoc_iovec_t *iov, size_t iovcnt, size_t min_bytes, int32_t timeout_msec)
{
   _eagain_stream_t *const es = (_eagain_stream_t *)s;
   if (es->eagain_remaining > 0) {
      --es->eagain_remaining;
      errno = EAGAIN;
      return 0;
   }
   return mongoc_stream_readv(es->wrapped, iov, iovcnt, min_bytes, timeout_msec);
}

static ssize_t
_eagain_writev(mongoc_stream_t *s, mongoc_iovec_t *iov, size_t iovcnt, int32_t timeout_msec)
{
   return mongoc_stream_writev(((_eagain_stream_t *)s)->wrapped, iov, iovcnt, timeout_msec);
}

static int
_eagain_close(mongoc_stream_t *s)
{
   return mongoc_stream_close(((_eagain_stream_t *)s)->wrapped);
}

static int
_eagain_flush(mongoc_stream_t *s)
{
   return mongoc_stream_flush(((_eagain_stream_t *)s)->wrapped);
}

static void
_eagain_destroy(mongoc_stream_t *s)
{
   _eagain_stream_t *const es = (_eagain_stream_t *)s;
   mongoc_stream_destroy(es->wrapped);
   bson_free(es);
}

static bool
_eagain_check_closed(mongoc_stream_t *s)
{
   return mongoc_stream_check_closed(((_eagain_stream_t *)s)->wrapped);
}

static mongoc_stream_t *
_eagain_get_base_stream(mongoc_stream_t *s)
{
   return ((_eagain_stream_t *)s)->wrapped;
}

static mongoc_stream_t *
_eagain_stream_new(mongoc_stream_t *base)
{
   _eagain_stream_t *const es = bson_malloc0(sizeof(*es));
   es->base.type = base->type;
   es->base.destroy = _eagain_destroy;
   es->base.close = _eagain_close;
   es->base.flush = _eagain_flush;
   es->base.writev = _eagain_writev;
   es->base.readv = _eagain_readv;
   es->base.get_base_stream = _eagain_get_base_stream;
   es->base.check_closed = _eagain_check_closed;
   es->wrapped = base;
   es->eagain_remaining = 3; // Return EAGAIN three times, then succeed.
   return (mongoc_stream_t *)es;
}

static mongoc_stream_t *
_eagain_stream_wrapper(mongoc_stream_t *s)
{
   return _eagain_stream_new(s);
}

static void
test_mongoc_tls_eagain(void)
{
   mongoc_ssl_opt_t sopt = {.ca_file = CERT_CA, .pem_file = CERT_SERVER};
   mongoc_ssl_opt_t copt = {.ca_file = CERT_CA, .pem_file = CERT_CLIENT};
   ssl_test_result_t sr;
   ssl_test_result_t cr;
   ssl_test_with_client_stream_wrapper(&copt, &sopt, "localhost", &cr, &sr, _eagain_stream_wrapper);
   ASSERT_CMPINT((int)cr.result, ==, SSL_TEST_SUCCESS);
   ASSERT_CMPINT((int)sr.result, ==, SSL_TEST_SUCCESS);
}
#endif // defined(MONGOC_ENABLE_SSL_OPENSSL)

#endif /* !MONGOC_ENABLE_SSL_SECURE_CHANNEL */

void
test_mongoc_tls_insecure_nowarning(void)
{
   mongoc_uri_t *uri;
   mongoc_client_t *client;

   if (!test_framework_get_ssl()) {
      return;
   }
   uri = test_framework_get_uri();
   mongoc_uri_set_option_as_bool(uri, MONGOC_URI_TLSINSECURE, true);
   client = test_framework_client_new_from_uri(uri, NULL);

   capture_logs(true);
   mongoc_client_command_simple(
      client, "admin", tmp_bson("{'ping': 1}"), NULL /* read prefs */, NULL /* reply */, NULL /* error */);
   ASSERT_NO_CAPTURED_LOGS("has no effect");
   mongoc_client_destroy(client);
   mongoc_uri_destroy(uri);
}

void
test_stream_tls_install(TestSuite *suite)
{
   BSON_UNUSED(suite);

#if !defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)

   /* Disable /TLS/commonName on macOS due to CDRIVER-4256. */
#if !defined(MONGOC_ENABLE_SSL_SECURE_TRANSPORT)
   TestSuite_Add(suite, "/TLS/commonName", test_mongoc_tls_common_name);
#endif
   TestSuite_Add(suite, "/TLS/altname", test_mongoc_tls_altname);
   TestSuite_Add(suite, "/TLS/basic", test_mongoc_tls_basic);
   TestSuite_Add(suite, "/TLS/allow_invalid_hostname", test_mongoc_tls_allow_invalid_hostname);
   TestSuite_Add(suite, "/TLS/wild", test_mongoc_tls_wild);
   TestSuite_Add(suite, "/TLS/no_verify", test_mongoc_tls_no_verify);
   TestSuite_Add(suite, "/TLS/bad_verify", test_mongoc_tls_bad_verify);
   TestSuite_Add(suite, "/TLS/no_certs", test_mongoc_tls_no_certs);

   TestSuite_Add(suite, "/TLS/expired", test_mongoc_tls_expired);

#ifdef MONGOC_ENABLE_SSL_OPENSSL
   TestSuite_Add(suite, "/TLS/ip", test_mongoc_tls_ip);
   TestSuite_Add(suite, "/TLS/password", test_mongoc_tls_password);
   TestSuite_Add(suite, "/TLS/bad_password", test_mongoc_tls_bad_password);
   TestSuite_Add(suite, "/TLS/weak_cert_validation", test_mongoc_tls_weak_cert_validation);
   TestSuite_Add(suite, "/TLS/crl", test_mongoc_tls_crl);
   TestSuite_Add(suite, "/TLS/eagain", test_mongoc_tls_eagain);
#endif

#if !defined(__APPLE__) && !defined(_WIN32) && defined(MONGOC_ENABLE_SSL_OPENSSL) && \
   OPENSSL_VERSION_NUMBER >= 0x10000000L
   TestSuite_Add(suite, "/TLS/trust_dir", test_mongoc_tls_trust_dir);
#endif

   TestSuite_AddLive(suite, "/TLS/insecure_nowarning", test_mongoc_tls_insecure_nowarning);

#endif // !defined(MONGOC_ENABLE_SSL_SECURE_CHANNEL)
}
