/*
  This file is part of libmicrohttpd
  Copyright (C) 2013, 2016 Christian Grothoff
  Copyright (C) 2016-2022 Evgeny Grin (Karlson2k)

  libmicrohttpd is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published
  by the Free Software Foundation; either version 3, or (at your
  option) any later version.

  libmicrohttpd is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with libmicrohttpd; see the file COPYING.  If not, write to the
  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  Boston, MA 02110-1301, USA.
*/

/**
 * @file test_https_sni.c
 * @brief  Testcase for libmicrohttpd HTTPS with SNI operations
 * @author Christian Grothoff
 * @author Karlson2k (Evgeny Grin)
 */
#include "platform.h"
#include "microhttpd.h"
#include <limits.h>
#include <sys/stat.h>
#include <curl/curl.h>
#ifdef MHD_HTTPS_REQUIRE_GCRYPT
#include <gcrypt.h>
#endif /* MHD_HTTPS_REQUIRE_GCRYPT */
#include "tls_test_common.h"
#include <gnutls/gnutls.h>

/* This test only works with GnuTLS >= 3.0 */
#if GNUTLS_VERSION_MAJOR >= 3

#include <gnutls/abstract.h>

/**
 * A hostname, server key and certificate.
 */
struct Hosts
{
  struct Hosts *next;
  const char *hostname;
  gnutls_pcert_st pcrt;
  gnutls_privkey_t key;
};


/**
 * Linked list of supported TLDs and respective certificates.
 */
static struct Hosts *hosts;

/* Load the certificate and the private key.
 * (This code is largely taken from GnuTLS).
 */
static void
load_keys (const char *hostname,
           const char *CERT_FILE,
           const char *KEY_FILE)
{
  int ret;
  gnutls_datum_t data;
  struct Hosts *host;

  host = malloc (sizeof (struct Hosts));
  if (NULL == host)
    abort ();
  host->hostname = hostname;
  host->next = hosts;
  hosts = host;

  ret = gnutls_load_file (CERT_FILE, &data);
  if (ret < 0)
  {
    fprintf (stderr,
             "*** Error loading certificate file %s.\n",
             CERT_FILE);
    exit (1);
  }
  ret =
    gnutls_pcert_import_x509_raw (&host->pcrt, &data, GNUTLS_X509_FMT_PEM,
                                  0);
  if (ret < 0)
  {
    fprintf (stderr,
             "*** Error loading certificate file: %s\n",
             gnutls_strerror (ret));
    exit (1);
  }
  gnutls_free (data.data);

  ret = gnutls_load_file (KEY_FILE, &data);
  if (ret < 0)
  {
    fprintf (stderr,
             "*** Error loading key file %s.\n",
             KEY_FILE);
    exit (1);
  }

  gnutls_privkey_init (&host->key);
  ret =
    gnutls_privkey_import_x509_raw (host->key,
                                    &data, GNUTLS_X509_FMT_PEM,
                                    NULL, 0);
  if (ret < 0)
  {
    fprintf (stderr,
             "*** Error loading key file: %s\n",
             gnutls_strerror (ret));
    exit (1);
  }
  gnutls_free (data.data);
}


/**
 * @param session the session we are giving a cert for
 * @param req_ca_dn NULL on server side
 * @param nreqs length of req_ca_dn, and thus 0 on server side
 * @param pk_algos NULL on server side
 * @param pk_algos_length 0 on server side
 * @param pcert list of certificates (to be set)
 * @param pcert_length length of pcert (to be set)
 * @param pkey the private key (to be set)
 */
static int
sni_callback (gnutls_session_t session,
              const gnutls_datum_t *req_ca_dn,
              int nreqs,
              const gnutls_pk_algorithm_t *pk_algos,
              int pk_algos_length,
              gnutls_pcert_st **pcert,
              unsigned int *pcert_length,
              gnutls_privkey_t *pkey)
{
  char name[256];
  size_t name_len;
  struct Hosts *host;
  unsigned int type;
  (void) req_ca_dn; (void) nreqs; (void) pk_algos; (void) pk_algos_length;   /* Unused. Silent compiler warning. */

  name_len = sizeof (name);
  if (GNUTLS_E_SUCCESS !=
      gnutls_server_name_get (session,
                              name,
                              &name_len,
                              &type,
                              0 /* index */))
    return -1;
  for (host = hosts; NULL != host; host = host->next)
    if (0 == strncmp (name, host->hostname, name_len))
      break;
  if (NULL == host)
  {
    fprintf (stderr,
             "Need certificate for %.*s\n",
             (int) name_len,
             name);
    return -1;
  }
#if 0
  fprintf (stderr,
           "Returning certificate for %.*s\n",
           (int) name_len,
           name);
#endif
  *pkey = host->key;
  *pcert_length = 1;
  *pcert = &host->pcrt;
  return 0;
}


/* perform a HTTP GET request via SSL/TLS */
static int
do_get (const char *url, uint16_t port)
{
  CURL *c;
  struct CBC cbc;
  CURLcode errornum;
  size_t len;
  struct curl_slist *dns_info;
  char buf[256];

  len = strlen (test_data);
  if (NULL == (cbc.buf = malloc (sizeof (char) * len)))
  {
    fprintf (stderr, MHD_E_MEM);
    return -1;
  }
  cbc.size = len;
  cbc.pos = 0;

  c = curl_easy_init ();
#ifdef _DEBUG
  curl_easy_setopt (c, CURLOPT_VERBOSE, 1L);
#endif
  curl_easy_setopt (c, CURLOPT_URL, url);
  curl_easy_setopt (c, CURLOPT_PORT, (long) port);
  curl_easy_setopt (c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  curl_easy_setopt (c, CURLOPT_TIMEOUT, 10L);
  curl_easy_setopt (c, CURLOPT_CONNECTTIMEOUT, 10L);
  curl_easy_setopt (c, CURLOPT_WRITEFUNCTION, &copyBuffer);
  curl_easy_setopt (c, CURLOPT_WRITEDATA, &cbc);
  curl_easy_setopt (c, CURLOPT_CAINFO, SRCDIR "/test-ca.crt");

  /* perform peer authentication */
  /* TODO merge into send_curl_req */
  curl_easy_setopt (c, CURLOPT_SSL_VERIFYPEER, 0L);
  curl_easy_setopt (c, CURLOPT_SSL_VERIFYHOST, 2L);
  sprintf (buf, "mhdhost1:%u:127.0.0.1", (unsigned int) port);
  dns_info = curl_slist_append (NULL, buf);
  sprintf (buf, "mhdhost2:%u:127.0.0.1", (unsigned int) port);
  dns_info = curl_slist_append (dns_info, buf);
  curl_easy_setopt (c, CURLOPT_RESOLVE, dns_info);
  curl_easy_setopt (c, CURLOPT_FAILONERROR, 1L);

  /* NOTE: use of CONNECTTIMEOUT without also
     setting NOSIGNAL results in really weird
     crashes on my system! */
  curl_easy_setopt (c, CURLOPT_NOSIGNAL, 1L);
  if (CURLE_OK != (errornum = curl_easy_perform (c)))
  {
    fprintf (stderr, "curl_easy_perform failed: `%s'\n",
             curl_easy_strerror (errornum));
    curl_easy_cleanup (c);
    free (cbc.buf);
    curl_slist_free_all (dns_info);
    return -1;
  }

  curl_easy_cleanup (c);
  curl_slist_free_all (dns_info);
  if (memcmp (cbc.buf, test_data, len) != 0)
  {
    fprintf (stderr, "Error: local file & received file differ.\n");
    free (cbc.buf);
    return -1;
  }

  free (cbc.buf);
  return 0;
}


int
main (int argc, char *const *argv)
{
  unsigned int error_count = 0;
  struct MHD_Daemon *d;
  uint16_t port;
  const char *tls_backend;
  (void) argc;   /* Unused. Silent compiler warning. */

  if (MHD_NO != MHD_is_feature_supported (MHD_FEATURE_AUTODETECT_BIND_PORT))
    port = 0;
  else
    port = 3065;

#ifdef MHD_HTTPS_REQUIRE_GCRYPT
  gcry_control (GCRYCTL_ENABLE_QUICK_RANDOM, 0);
#ifdef GCRYCTL_INITIALIZATION_FINISHED
  gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
#endif
#endif /* MHD_HTTPS_REQUIRE_GCRYPT */
  if (! testsuite_curl_global_init ())
    return 99;
  tls_backend = curl_version_info (CURLVERSION_NOW)->ssl_version;
  if (NULL == tls_backend)
  {
    fprintf (stderr, "Curl does not support SSL.  Cannot run the test.\n");
    curl_global_cleanup ();
    return 77;
  }
  if (! curl_tls_is_gnutls () && ! curl_tls_is_openssl ())
  {
    fprintf (stderr, "This test is reliable only with libcurl with GnuTLS or "
             "OpenSSL backends.\nSkipping the test as libcurl has '%s' "
             "backend.\n", tls_backend);
    curl_global_cleanup ();
    return 77;
  }

  load_keys ("mhdhost1", SRCDIR "/mhdhost1.crt",
             SRCDIR "/mhdhost1.key");
  load_keys ("mhdhost2", SRCDIR "/mhdhost2.crt",
             SRCDIR "/mhdhost2.key");
  d = MHD_start_daemon (MHD_USE_THREAD_PER_CONNECTION
                        | MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_TLS
                        | MHD_USE_ERROR_LOG,
                        port,
                        NULL, NULL,
                        &http_ahc, NULL,
                        MHD_OPTION_HTTPS_CERT_CALLBACK, &sni_callback,
                        MHD_OPTION_END);
  if (d == NULL)
  {
    fprintf (stderr, MHD_E_SERVER_INIT);
    return -1;
  }
  if (0 == port)
  {
    const union MHD_DaemonInfo *dinfo;
    dinfo = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT);
    if ((NULL == dinfo) || (0 == dinfo->port) )
    {
      MHD_stop_daemon (d); return -1;
    }
    port = dinfo->port;
  }
  if (0 != do_get ("https://mhdhost1/", port))
    error_count++;
  if (0 != do_get ("https://mhdhost2/", port))
    error_count++;

  MHD_stop_daemon (d);
  curl_global_cleanup ();
  if (error_count != 0)
    fprintf (stderr, "Failed test: %s, error: %u.\n", argv[0], error_count);
  return (0 != error_count) ? 1 : 0;
}


#else

int
main (void)
{
  fprintf (stderr,
           "SNI not supported by GnuTLS < 3.0\n");
  return 77;
}


#endif
