/*
 * Copyright (c) 2012-2015 Tim Ruehsen
 * Copyright (c) 2015-2026 Free Software Foundation, Inc.
 *
 * This file is part of libwget.
 *
 * Libwget is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Libwget 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
 *
 *
 * gnutls SSL/TLS routines
 * - some parts have been copied from GnuTLS example code
 * - OCSP code has been copied from gnutls-cli/ocsptool code
 *
 * Changelog
 * 03.08.2012  Tim Ruehsen  created inspired from gnutls client example
 * 26.08.2012               wget compatibility regarding config options
 * 15.01.2015               added OCSP fix from https://gitorious.org/gnutls/gnutls/commit/11eebe14b232ec198d1446a3720e6ed78d118c4b
 *
 * Resources:
 * RFC6066 Transport Layer Security (TLS) Extensions: Extension Definitions (defines OCSP stapling)
 * RFC6960 Online Certificate Status Protocol - OCSP
 * RFC6961 TLS Multiple Certificate Status Request Extension
 *
 * Testing revocation:
 * https://revoked.grc.com/
 * https://test-sspev.verisign.com:2443/test-SSPEV-revoked-verisign.html
 *
 */

#include <config.h>

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>

#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#ifdef WITH_OCSP
#	include <gnutls/ocsp.h>
#endif
#ifdef WITH_LIBDANE
#	include <gnutls/dane.h>
#endif
#include <gnutls/crypto.h>
#include <gnutls/abstract.h>

#include <wget.h>
#include "private.h"
#include "net.h"

/**
 * \file
 * \brief Functions for establishing and managing SSL/TLS connections
 * \defgroup libwget-ssl SSL/TLS engine
 *
 * @{
 */

static wget_tls_stats_callback
	*tls_stats_callback;
static void
	*tls_stats_ctx;

static wget_ocsp_stats_callback
	*ocsp_stats_callback;
static void
	*ocsp_stats_ctx;

static struct config {
	const char
		*secure_protocol,
		*ca_directory,
		*ca_file,
		*cert_file,
		*key_file,
		*crl_file,
		*ocsp_server,
		*alpn;
	wget_ocsp_db
		*ocsp_cert_cache,
		*ocsp_host_cache;
	wget_tls_session_db
		*tls_session_cache;
	wget_hpkp_db
		*hpkp_cache;
	char
		ca_type,
		cert_type,
		key_type;
	bool
		check_certificate : 1,
		report_invalid_cert : 1,
		check_hostname : 1,
		print_info : 1,
		ocsp : 1,
		ocsp_date : 1,
		ocsp_stapling : 1,
		ocsp_nonce : 1,
		dane : 1;
} config = {
	.check_certificate = true,
	.report_invalid_cert = true,
	.check_hostname = true,
#ifdef WITH_OCSP
	.ocsp = false,
	.ocsp_stapling = true,
#endif
	.ca_type = WGET_SSL_X509_FMT_PEM,
	.cert_type = WGET_SSL_X509_FMT_PEM,
	.key_type = WGET_SSL_X509_FMT_PEM,
	.secure_protocol = "AUTO",
	.ca_directory = "system",
	.ca_file = "system",
#ifdef WITH_LIBNGHTTP2
	.alpn = "h2,http/1.1",
#endif
};

struct session_context {
	const char *
		hostname;
	wget_hpkp_stats_result
		stats_hpkp;
	uint16_t
		port;
	bool
		ocsp_stapling : 1,
		valid : 1,
		delayed_session_data : 1;
};

static gnutls_certificate_credentials_t
	credentials;
static gnutls_priority_t
	priority_cache;

#define error_printf_check(...) if (config.report_invalid_cert) wget_error_printf(__VA_ARGS__)

/**
 * \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`) to set
 * \param[in] value The value for the config parameter (a NULL-terminated string)
 *
 * Set a configuration parameter, as a string.
 *
 * The following parameters accept a string as their value (\p key can have any of those values):
 *
 *  - WGET_SSL_SECURE_PROTOCOL: A string describing which SSL/TLS version should be used. It can have either
 *  an arbitrary value, or one of the following fixed values (case does not matter):
 *      - "SSL": SSLv3 will be used. Warning: this protocol is insecure and should be avoided.
 *      - "TLSv1": TLS 1.0 will be used.
 *      - "TLSv1_1": TLS 1.1 will be used.
 *      - "TLSv1_2": TLS 1.2 will be used.
 *      - "TLSv1_3": TLS 1.3 will be used.
 *      - "AUTO": Let the TLS library decide.
 *      - "PFS": Let the TLS library decide, but make sure only forward-secret ciphers are used.
 *
 *  An arbitrary string can also be supplied (an string that's different from any of the previous ones). If that's the case
 *  the string will be directly taken as the priority string and sent to the library. Priority strings provide the greatest flexibility,
 *  but have a library-specific syntax. A GnuTLS priority string will not work if your libwget has been compiled with OpenSSL, for instance.
 *  - WGET_SSL_CA_DIRECTORY: A path to the directory where the root certificates will be taken from
 *  for server cert validation. Every file of that directory is expected to contain an X.509 certificate,
 *  encoded in PEM format. If the string "system" is specified, the system's default directory will be used.
 *  The default value is "system". Certificates get loaded in wget_ssl_init().
 *  - WGET_SSL_CA_FILE: A path to a file containing a single root certificate. This will be used to validate
 *  the server's certificate chain. This option can be used together with `WGET_SSL_CA_DIRECTORY`. The certificate
 *  can be in either PEM or DER format. The format is specified in the `WGET_SSL_CA_TYPE` option (see
 *  wget_ssl_set_config_int()).
 *  - WGET_SSL_CERT_FILE: Set the client certificate. It will be used for client authentication if the server requests it.
 *  It can be in either PEM or DER format. The format is specified in the `WGET_SSL_CERT_TYPE` option (see
 *  wget_ssl_set_config_int()). The `WGET_SSL_KEY_FILE` option specifies the private key corresponding to the cert's
 *  public key. If `WGET_SSL_KEY_FILE` is not set, then the private key is expected to be in the same file as the certificate.
 *  - WGET_SSL_KEY_FILE: Set the private key corresponding to the client certificate specified in `WGET_SSL_CERT_FILE`.
 *  It can be in either PEM or DER format. The format is specified in the `WGET_SSL_KEY_TYPE` option (see
 *  wget_ssl_set_config_int()). IF `WGET_SSL_CERT_FILE` is not set, then the certificate is expected to be in the same file
 *  as the private key.
 *  - WGET_SSL_CRL_FILE: Sets a CRL (Certificate Revocation List) file which will be used to verify client and server certificates.
 *  A CRL file is a black list that contains the serial numbers of the certificates that should not be treated as valid. Whenever
 *  a client or a server presents a certificate in the TLS handshake whose serial number is contained in the CRL, the handshake
 *  will be immediately aborted. The CRL file must be in PEM format.
 *  - WGET_SSL_OCSP_SERVER: Set the URL of the OCSP server that will be used to validate certificates.
 *  OCSP is a protocol by which a server is queried to tell whether a given certificate is valid or not. It's an approach contrary
 *  to that used by CRLs. While CRLs are black lists, OCSP takes a white list approach where a certificate can be checked for validity.
 *  Whenever a client or server presents a certificate in a TLS handshake, the provided URL will be queried (using OCSP) to check whether
 *  that certificate is valid or not. If the server responds the certificate is not valid, the handshake will be immediately aborted.
 *  - WGET_SSL_ALPN: Sets the ALPN string to be sent to the remote host. ALPN is a TLS extension
 *  ([RFC 7301](https://tools.ietf.org/html/rfc7301))
 *  that allows both the server and the client to signal which application-layer protocols they support (HTTP/2, QUIC, etc.).
 *  That information can then be used for the server to ultimately decide which protocol will be used on top of TLS.
 *
 *  An invalid value for \p key will not harm the operation of TLS, but will cause
 *  a complain message to be printed to the error log stream.
 */
void wget_ssl_set_config_string(int key, const char *value)
{
	switch (key) {
	case WGET_SSL_SECURE_PROTOCOL: config.secure_protocol = value; break;
	case WGET_SSL_CA_DIRECTORY: config.ca_directory = value; break;
	case WGET_SSL_CA_FILE: config.ca_file = value; break;
	case WGET_SSL_CERT_FILE: config.cert_file = value; break;
	case WGET_SSL_KEY_FILE: config.key_file = value; break;
	case WGET_SSL_CRL_FILE: config.crl_file = value; break;
	case WGET_SSL_OCSP_SERVER: config.ocsp_server = value; break;
	case WGET_SSL_ALPN: config.alpn = value; break;
	default: error_printf(_("Unknown config key %d (or value must not be a string)\n"), key);
	}
}

/**
 * \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`) to set
 * \param[in] value The value for the config parameter (a pointer)
 *
 * Set a configuration parameter, as a libwget object.
 *
 * The following parameters expect an already initialized libwget object as their value.
 *
 *  - WGET_SSL_OCSP_CACHE: This option takes a pointer to a \ref wget_ocsp_db
 *  structure as an argument. Such a pointer is returned when initializing the OCSP cache with wget_ocsp_db_init().
 *  The cache is used to store OCSP responses locally and avoid querying the OCSP server repeatedly for the same certificate.
 *  - WGET_SSL_SESSION_CACHE: This option takes a pointer to a \ref wget_tls_session_db structure.
 *  Such a pointer is returned when initializing the TLS session cache with wget_tls_session_db_init().
 *  This option thus sets the handle to the TLS session cache that will be used to store TLS sessions.
 *  The TLS session cache is used to support TLS session resumption. It stores the TLS session parameters derived from a previous TLS handshake
 *  (most importantly the session identifier and the master secret) so that there's no need to run the handshake again
 *  the next time we connect to the same host. This is useful as the handshake is an expensive process.
 *  - WGET_SSL_HPKP_CACHE: Set the HPKP cache to be used to verify known HPKP pinned hosts. This option takes a pointer
 *  to a \ref wget_hpkp_db structure. Such a pointer is returned when initializing the HPKP cache
 *  with wget_hpkp_db_init(). HPKP is a HTTP-level protocol that allows the server to "pin" its present and future X.509
 *  certificate fingerprints, to support rapid certificate change in the event that the higher level root CA
 *  gets compromised ([RFC 7469](https://tools.ietf.org/html/rfc7469)).
 */

void wget_ssl_set_config_object(int key, void *value)
{
	switch (key) {
	case WGET_SSL_OCSP_CACHE: config.ocsp_cert_cache = (wget_ocsp_db *)value; break;
	case WGET_SSL_SESSION_CACHE: config.tls_session_cache = (wget_tls_session_db *)value; break;
	case WGET_SSL_HPKP_CACHE: config.hpkp_cache = (wget_hpkp_db *)value; break;
	default: error_printf(_("Unknown config key %d (or value must not be an object)\n"), key);
	}
}

/**
 * \param[in] key An identifier for the config parameter (starting with `WGET_SSL_`)
 * \param[in] value The value for the config parameter
 *
 * Set a configuration parameter, as an integer.
 *
 * These are the parameters that can be set (\p key can have any of these values):
 *
 *  - WGET_SSL_CHECK_CERTIFICATE: whether certificates should be verified (1) or not (0)
 *  - WGET_SSL_REPORT_INVALID_CERT: whether to print (1) errors/warnings regarding certificate verification or not (0)
 *  - WGET_SSL_CHECK_HOSTNAME: whether or not to check if the certificate's subject field
 *  matches the peer's hostname. This check is done according to the rules in [RFC 6125](https://tools.ietf.org/html/rfc6125)
 *  and typically involves checking whether the hostname and the common name (CN) field of the subject match.
 *  - WGET_SSL_PRINT_INFO: whether or not information should be printed about the established SSL/TLS handshake (negotiated
 *  ciphersuites, certificates, etc.). The default is no (0).
 *
 * The following three options all can take either `WGET_SSL_X509_FMT_PEM` (to specify the PEM format) or `WGET_SSL_X509_FMT_DER`
 * (for the DER format). The default in for all of them is `WGET_SSL_X509_FMT_PEM`.
 *
 *  - WGET_SSL_CA_TYPE: Specifies what's the format of the root CA certificate(s) supplied with either `WGET_SSL_CA_DIRECTORY`
 *  or `WGET_SSL_CA_FILE`.
 *  - WGET_SSL_CERT_TYPE: Specifies what's the format of the certificate file supplied with `WGET_SSL_CERT_FILE`. **The certificate
 *  and the private key supplied must both be of the same format.**
 *  - WGET_SSL_KEY_TYPE: Specifies what's the format of the private key file supplied with `WGET_SSL_KEY_FILE`. **The private key
 *  and the certificate supplied must both be of the same format.**
 *
 * The following two options control OCSP queries. These don't affect the CRL set with `WGET_SSL_CRL_FILE`, if any.
 * If both CRLs and OCSP are enabled, both will be used.
 *
 *  - WGET_SSL_OCSP: whether or not OCSP should be used. The default is yes (1).
 *  - WGET_SSL_OCSP_STAPLING: whether or not OCSP stapling should be used. The default is yes (1).
 *  - WGET_SSL_OCSP_NONCE: whether or not an OCSP nonce should be sent in the request. The default is yes (1).
 *  If a nonce was sent in the request, the OCSP verification will fail if the response nonce doesn't match.
 *  However if the response does not include a nonce extension, verification will be allowed to continue.
 *  The OCSP nonce extension is not a critical one.
 *  - WGET_SSL_OCSP_DATE: Reject the OCSP response if it's older than 3 days.
 */
void wget_ssl_set_config_int(int key, int value)
{
	switch (key) {
	case WGET_SSL_CHECK_CERTIFICATE: config.check_certificate = !!value; break;
	case WGET_SSL_REPORT_INVALID_CERT: config.report_invalid_cert = !!value; break;
	case WGET_SSL_CHECK_HOSTNAME: config.check_hostname = !!value; break;
	case WGET_SSL_CA_TYPE: config.ca_type = (char)value; break;
	case WGET_SSL_CERT_TYPE: config.cert_type = (char)value; break;
	case WGET_SSL_DANE: config.dane = !!value; break;
	case WGET_SSL_KEY_TYPE: config.key_type = (char)value; break;
	case WGET_SSL_PRINT_INFO: config.print_info = !!value; break;
	case WGET_SSL_OCSP: config.ocsp = !!value; break;
	case WGET_SSL_OCSP_DATE: config.ocsp_date = !!value; break;
	case WGET_SSL_OCSP_STAPLING: config.ocsp_stapling = !!value; break;
	case WGET_SSL_OCSP_NONCE: config.ocsp_nonce = !!value; break;
	default: error_printf(_("Unknown config key %d (or value must not be an integer)\n"), key);
	}
}

static const char *safe_ctime(time_t t, char *buf, size_t size)
{
	struct tm tm;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-y2k"
	if (localtime_r(&t, &tm) && strftime(buf, size, "%c", &tm))
		return buf;
#pragma GCC diagnostic pop

	return "[error]";
}

static void print_x509_certificate_info(gnutls_session_t session)
{
	const char *name;
	char dn[128], timebuf[64];
	unsigned char digest[64];
	unsigned char serial[40];
	size_t dn_size = sizeof(dn);
	size_t digest_size = sizeof (digest);
	size_t serial_size = sizeof(serial);
	time_t expired, activated;
	unsigned int bits;
	int algo;
	unsigned int cert_list_size = 0, ncert;
	const gnutls_datum_t *cert_list;
	gnutls_x509_crt_t cert;
	gnutls_certificate_type_t cert_type;

	cert_list = gnutls_certificate_get_peers(session, &cert_list_size);

	for (ncert = 0; ncert < cert_list_size; ncert++) {
		if ((cert_type = gnutls_certificate_type_get(session)) == GNUTLS_CRT_X509) {

			if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS)
				continue;

			if (gnutls_x509_crt_import(cert, &cert_list[ncert], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) {
				gnutls_x509_crt_deinit(cert);
				continue;
			}

			info_printf(_("Certificate info [%u]:\n"), ncert);

			activated = gnutls_x509_crt_get_activation_time(cert);
			info_printf(_("  Valid since: %s"), safe_ctime(activated, timebuf, sizeof(timebuf)));

			expired = gnutls_x509_crt_get_expiration_time(cert);
			info_printf(_("  Expires: %s"), safe_ctime(expired, timebuf, sizeof(timebuf)));

			if (!gnutls_fingerprint(GNUTLS_DIG_SHA256, &cert_list[ncert], digest, &digest_size)) {
				char digest_hex[sizeof(digest) * 2 + 1];

				wget_memtohex(digest, digest_size, digest_hex, sizeof(digest_hex));

				info_printf(_("  Fingerprint: %s\n"), digest_hex);
			}

			if (!gnutls_x509_crt_get_serial(cert, serial, &serial_size)) {
				char serial_hex[sizeof(serial) * 2 + 1];

				wget_memtohex(serial, serial_size, serial_hex, sizeof(serial_hex));

				info_printf(_("  Serial number: %s\n"), serial_hex);
			}

			algo = gnutls_x509_crt_get_pk_algorithm(cert, &bits);
			name = gnutls_pk_algorithm_get_name(algo);
			info_printf(_("  Public key: %s, %s (%u bits)\n"),
				name ? name : "Unknown",
				gnutls_sec_param_get_name(gnutls_pk_bits_to_sec_param(algo, bits)),
				bits);

			info_printf(_("  Version: #%d\n"), gnutls_x509_crt_get_version(cert));

			dn_size = sizeof(dn);
			gnutls_x509_crt_get_dn(cert, dn, &dn_size);
			info_printf(_("  DN: %s\n"), dn);

			dn_size = sizeof(dn);
			gnutls_x509_crt_get_issuer_dn(cert, dn, &dn_size);
			info_printf(_("  Issuer's DN: %s\n"), dn);

			dn_size = sizeof(dn);
			gnutls_x509_crt_get_issuer_dn_oid(cert, 0, dn, &dn_size);
			info_printf(_("  Issuer's OID: %s\n"), dn);

			dn_size = sizeof(dn);
			gnutls_x509_crt_get_issuer_unique_id(cert, dn, &dn_size);
			info_printf(_("  Issuer's UID: %s\n"), dn);
/*
			dn_size = sizeof(dn);
			gnutls_x509_crt_get_subject_key_id(cert, dn, &dn_size, NULL);
			info_printf(_("  Certificate Subject ID: %s\n"), dn);

			dn_size = sizeof(dn);
			gnutls_x509_crt_get_subject_unique_id(cert, dn, &dn_size);
			info_printf(_("  Certificate Subject UID: %s\n"), dn);
*/
			gnutls_x509_crt_deinit(cert);
		} else {
			info_printf(_("  Unknown certificate type %d\n"), (int) cert_type);
		}
	}
}

static int print_info(gnutls_session_t session)
{
	const char *tmp;
	gnutls_credentials_type_t cred;
	gnutls_kx_algorithm_t kx;
	int dhe = 0;
	int ecdh = 0;

	kx = gnutls_kx_get(session);

	info_printf(_("----\n"));

	/* Check the authentication type used and switch
	 * to the appropriate.
	 */
	cred = gnutls_auth_get_type(session);
	switch (cred) {
	case GNUTLS_CRD_IA:
		info_printf(_("TLS/IA session\n"));
		break;

	case GNUTLS_CRD_SRP:
#ifdef HAVE_GNUTLS_SRP_SERVER_GET_USERNAME
		info_printf(_("SRP session with username %s\n"), gnutls_srp_server_get_username(session));
#endif
		break;

	case GNUTLS_CRD_PSK:
		/* This returns NULL in server side.
		 */
		if (gnutls_psk_client_get_hint(session) != NULL)
			info_printf(_("PSK authentication. PSK hint '%s'\n"), gnutls_psk_client_get_hint(session));

		/* This returns NULL in client side.
		 */
		if (gnutls_psk_server_get_username(session) != NULL)
			info_printf(_("PSK authentication. Connected as '%s'\n"), gnutls_psk_server_get_username(session));

		if (kx == GNUTLS_KX_DHE_PSK)
			dhe = 1;
		else if (kx == GNUTLS_KX_ECDHE_PSK)
			ecdh = 1;
		break;

	case GNUTLS_CRD_ANON: /* anonymous authentication */

		info_printf(_("Anonymous authentication.\n"));
		if (kx == GNUTLS_KX_ANON_DH)
			dhe = 1;
		else if (kx == GNUTLS_KX_ANON_ECDH)
			ecdh = 1;
		break;

	case GNUTLS_CRD_CERTIFICATE: /* certificate authentication */

		/* Check if we have been using ephemeral Diffie-Hellman.
		 */
		if (kx == GNUTLS_KX_DHE_RSA || kx == GNUTLS_KX_DHE_DSS)
			dhe = 1;
		else if (kx == GNUTLS_KX_ECDHE_RSA || kx == GNUTLS_KX_ECDHE_ECDSA)
			ecdh = 1;

		/* if the certificate list is available, then
		 * print some information about it.
		 */
		print_x509_certificate_info(session);
		break;

	default:
		if ((int) cred == -1)
			info_printf(_("Transport authentication failure\n"));
		else
			info_printf(_("Unsupported credential type %d.\n"), (int) cred);
		break;
	} /* switch */

	info_printf(_("----\n"));

	if (dhe != 0)
		info_printf(_("Ephemeral DH using prime of %d bits\n"), gnutls_dh_get_prime_bits(session));
	else if (ecdh != 0)
		info_printf(_("Ephemeral ECDH using curve %s\n"), gnutls_ecc_curve_get_name(gnutls_ecc_curve_get(session)));

	/* print the key exchange's algorithm name */
	tmp = gnutls_kx_get_name(kx);
	info_printf(_("Key Exchange: %s\n"), tmp);

	/* print the protocol's name (ie TLS 1.0) */
	tmp = gnutls_protocol_get_name(gnutls_protocol_get_version(session));
	info_printf(_("Protocol: %s\n"), tmp);

	/* print the certificate type of the peer, ie X.509 */
	tmp = gnutls_certificate_type_get_name(gnutls_certificate_type_get(session));
	info_printf(_("Certificate Type: %s\n"), tmp);

	/* print the name of the cipher used, ie 3DES. */
	tmp = gnutls_cipher_get_name(gnutls_cipher_get(session));
	info_printf(_("Cipher: %s\n"), tmp);

	/* Print the MAC algorithms name, ie SHA1 */
	tmp = gnutls_mac_get_name(gnutls_mac_get(session));
	info_printf(_("MAC: %s\n"), tmp);

	info_printf(_("----\n"));

	return 0;
}

#ifdef WITH_OCSP
static int
_generate_ocsp_data(gnutls_x509_crt_t cert, gnutls_x509_crt_t issuer,
		  gnutls_datum_t * rdata, gnutls_datum_t *nonce)
{
	gnutls_ocsp_req_t req;
	int ret = gnutls_ocsp_req_init(&req);

	if (ret < 0) {
		debug_printf("ocsp_req_init: %s", gnutls_strerror(ret));
		return -1;
	}

	ret = gnutls_ocsp_req_add_cert(req, GNUTLS_DIG_SHA1, issuer, cert);
	if (ret < 0) {
		debug_printf("ocsp_req_add_cert: %s", gnutls_strerror(ret));
		goto error;
	}

	if (nonce) {
		ret = gnutls_ocsp_req_set_nonce(req, 0, nonce);
		if (ret < 0) {
			debug_printf("ocsp_req_set_nonce: %s", gnutls_strerror(ret));
			goto error;
		}
	}

	ret = gnutls_ocsp_req_export(req, rdata);
	if (ret) {
		debug_printf("ocsp_req_export: %s", gnutls_strerror(ret));
		goto error;
	}

	ret = 0;
error:
	gnutls_ocsp_req_deinit(req);
	return ret;
}

/* Returns 0 on ok, and -1 on error */
static int send_ocsp_request(const char *server,
		      gnutls_x509_crt_t cert, gnutls_x509_crt_t issuer,
		      wget_buffer **ocsp_data, gnutls_datum_t *nonce)
{
	int ret = -1;
	int server_allocated = 0;
	gnutls_datum_t body;
	wget_iri *iri;
	wget_http_request *req = NULL;

	if (!server) {
		/* try to read URL from issuer certificate */
		gnutls_datum_t data;
		unsigned i = 0;
		int rc;

		do {
			rc = gnutls_x509_crt_get_authority_info_access(cert, i++, GNUTLS_IA_OCSP_URI, &data, NULL);
		} while(rc < 0 && rc != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE);

		if (rc < 0) {
			i = 0;
			do {
				rc = gnutls_x509_crt_get_authority_info_access(issuer, i++, GNUTLS_IA_OCSP_URI, &data, NULL);
			} while(rc < 0 && rc != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE);
		}

		if (rc < 0) {
			debug_printf("Cannot find URL from issuer: %s\n", gnutls_strerror(rc));
			return -1;
		}

		server = wget_strmemdup((char *)data.data, data.size);
		server_allocated = 1;

		gnutls_free(data.data);
	}

	iri = wget_iri_parse(server, NULL);

	if (server_allocated)
		xfree(server);

	if (!iri)
		return -1;

	if (_generate_ocsp_data(cert, issuer, &body, nonce))
		goto out;

	if (!(req = wget_http_create_request(iri, "POST")))
		goto out;

	wget_http_add_header(req, "Accept-Encoding", "identity");
	wget_http_add_header(req, "Accept", "*/*");
	wget_http_add_header(req, "Connection", "close");

	wget_http_connection *conn;
	if (wget_http_open(&conn, iri) == WGET_E_SUCCESS) {
		wget_http_request_set_body(req, "application/ocsp-request", wget_memdup(body.data, body.size), body.size);
		req->debug_skip_body = 1;
		if (wget_http_send_request(conn, &req) == WGET_E_SUCCESS) {
			wget_http_response *resp;

			if ((resp = wget_http_get_response(conn))) {
				*ocsp_data = resp->body;
				resp->body = NULL;
				wget_http_free_response(&resp);
				ret = 0;
			}
		}
		wget_http_close(&conn);
	}

	xfree(body.data);

out:
	wget_http_free_request(&req);
	wget_iri_free(&iri);
	return ret;
}

static void print_ocsp_verify_res(unsigned int status)
{
	debug_printf("*** Verifying OCSP Response: ");

	if (status) {
		debug_printf("Failure");

		if (status & GNUTLS_OCSP_VERIFY_SIGNER_NOT_FOUND)
			debug_printf(", Signer cert not found");

		if (status & GNUTLS_OCSP_VERIFY_SIGNER_KEYUSAGE_ERROR)
			debug_printf(", Signer cert keyusage error");

		if (status & GNUTLS_OCSP_VERIFY_UNTRUSTED_SIGNER)
			debug_printf(", Signer cert is not trusted");

		if (status & GNUTLS_OCSP_VERIFY_INSECURE_ALGORITHM)
			debug_printf(", Insecure algorithm");

		if (status & GNUTLS_OCSP_VERIFY_SIGNATURE_FAILURE)
			debug_printf(", Signature failure");

		if (status & GNUTLS_OCSP_VERIFY_CERT_NOT_ACTIVATED)
			debug_printf(", Signer cert not yet activated");

		if (status & GNUTLS_OCSP_VERIFY_CERT_EXPIRED)
			debug_printf(", Signer cert expired");

		debug_printf("\n");
	} else
		debug_printf("Success\n");
}

/* three days */
#define OCSP_VALIDITY_SECS (3*60*60*24)

/* Returns:
 *  0: certificate is revoked
 *  1: certificate is ok
 *  -1: dunno
 */
static int check_ocsp_response(gnutls_x509_crt_t cert,
	gnutls_x509_crt_t issuer, wget_buffer *data,
	gnutls_datum_t *nonce)
{
	gnutls_ocsp_resp_t resp;
	int ret = -1, rc;
	unsigned int status, cert_status;
	time_t rtime = 0, vtime = 0, ntime = 0, now;
	char timebuf[64];

	now = time(NULL);

	if ((rc = gnutls_ocsp_resp_init(&resp)) < 0) {
		debug_printf("ocsp_resp_init: %s", gnutls_strerror(rc));
		return -1;
	}

	rc = gnutls_ocsp_resp_import(resp, &(gnutls_datum_t){ .data = (unsigned char *) data->data, .size = (unsigned) data->length });
	if (rc < 0) {
		debug_printf("importing response: %s", gnutls_strerror(rc));
		goto cleanup;
	}

	if ((rc = gnutls_ocsp_resp_check_crt(resp, 0, cert)) < 0) {
		if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
			debug_printf("got OCSP response with no data (ignoring)\n");
		} else {
			debug_printf("got OCSP response on an unrelated certificate (ignoring)\n");
		}
		goto cleanup;
	}

	if ((rc = gnutls_ocsp_resp_verify_direct(resp, issuer, &status, 0)) < 0) {
		debug_printf("gnutls_ocsp_resp_verify_direct: %s", gnutls_strerror(rc));
		goto cleanup;
	}

	if (status) {
		print_ocsp_verify_res(status);
		goto cleanup;
	}

	rc = gnutls_ocsp_resp_get_single(resp, 0, NULL, NULL, NULL, NULL,
					  &cert_status, &vtime, &ntime, &rtime, NULL);
	if (rc < 0) {
		debug_printf("reading response: %s", gnutls_strerror(rc));
		goto cleanup;
	}

	if (cert_status == GNUTLS_OCSP_CERT_REVOKED) {
		debug_printf("*** Certificate was revoked at %s", safe_ctime(rtime, timebuf, sizeof(timebuf)));
		ret = 0;
		goto cleanup;
	}

	debug_printf("*** OCSP issued time: %s\n", safe_ctime(vtime, timebuf, sizeof(timebuf)));
	debug_printf("*** OCSP update time  : %s\n", safe_ctime(ntime, timebuf, sizeof(timebuf)));

	if (ntime == -1) {
		if (config.ocsp_date && now - vtime > OCSP_VALIDITY_SECS) {
			debug_printf("*** The OCSP response is old (was issued at: %s) ignoring", safe_ctime(vtime, timebuf, sizeof(timebuf)));
			goto cleanup;
		}
	} else {
		/* there is a newer OCSP answer, don't trust this one */
		if (ntime < now) {
			debug_printf("*** The OCSP response was issued at: %s", safe_ctime(vtime, timebuf, sizeof(timebuf)));
			debug_printf("    but there is a newer issue at %s", safe_ctime(ntime, timebuf, sizeof(timebuf)));
			goto cleanup;
		}
	}

	if (nonce) {
		gnutls_datum_t rnonce;

		rc = gnutls_ocsp_resp_get_nonce(resp, NULL, &rnonce);
		if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
			debug_printf("*** The OCSP reply did not include the requested nonce.\n");
			goto finish_ok;
		}

		if (rc < 0) {
			debug_printf("could not read response's nonce: %s\n", gnutls_strerror(rc));
			goto cleanup;
		}

		if (config.ocsp_nonce && (rnonce.size != nonce->size || memcmp(nonce->data, rnonce.data, nonce->size) != 0)) {
			debug_printf("nonce in the response doesn't match\n");
			xfree(rnonce.data);
			goto cleanup;
		}

		xfree(rnonce.data);
	}

 finish_ok:
	debug_printf("OCSP server flags certificate not revoked as of %s", safe_ctime(vtime, timebuf, sizeof(timebuf)));
	ret = 1;

cleanup:
	gnutls_ocsp_resp_deinit(resp);
	return ret;
}

/*
 * Calculate fingerprint from certificate
 */
static char *_get_cert_fingerprint(gnutls_x509_crt_t cert, char *fingerprint_hex, size_t length)
{
	unsigned char fingerprint[64];
	size_t fingerprint_size = sizeof(fingerprint);
	int err;

	if ((err = gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA256, fingerprint, &fingerprint_size)) < 0) {
		debug_printf("Failed to get fingerprint: %s\n", gnutls_strerror(err));
		wget_strscpy(fingerprint_hex, "00", length);
	} else {
		wget_memtohex(fingerprint, fingerprint_size, fingerprint_hex, length);
	}

	return fingerprint_hex;
}

/*
 * Add cert to OCSP cache, being either valid or revoked (valid==0)
 */
static void add_cert_to_ocsp_cache(gnutls_x509_crt_t cert, bool valid)
{
	if (config.ocsp_cert_cache) {
		char fingerprint_hex[64 * 2 +1];

		_get_cert_fingerprint(cert, fingerprint_hex, sizeof(fingerprint_hex));
		wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache, fingerprint_hex, time(NULL) + 3600, valid); // 1h valid
	}
}

/* OCSP check for the peer's certificate. Should be called
 * only after the certificate list verification is complete.
 * Returns:
 * 0: certificate is revoked
 * 1: certificate is ok
 * -1: dunno
 */
//static int cert_verify_ocsp(gnutls_session_t session)
static int cert_verify_ocsp(gnutls_x509_crt_t cert, gnutls_x509_crt_t issuer)
{
	wget_buffer *resp = NULL;
	unsigned char noncebuf[23];
	gnutls_datum_t nonce = { noncebuf, sizeof(noncebuf) };
	int ret;

	ret = gnutls_rnd(GNUTLS_RND_NONCE, nonce.data, nonce.size);
	if (ret < 0) {
		debug_printf("gnutls_rnd: %s", gnutls_strerror(ret));
		return -1;
	}

	if (send_ocsp_request(config.ocsp_server, cert, issuer, &resp, &nonce) < 0) {
		debug_printf("Cannot contact OCSP server\n");
		return -1;
	}

	if (!resp) {
		debug_printf("Missing response from OCSP server\n");
		return -1;
	}

	/* verify and check the response for revoked cert */
	ret = check_ocsp_response(cert, issuer, resp, &nonce);
	wget_buffer_free(&resp);

	return ret;
}
#endif // WITH_OCSP

static int cert_verify_hpkp(gnutls_x509_crt_t cert, const char *hostname, gnutls_session_t session)
{
	gnutls_pubkey_t key = NULL;
	int rc, ret = -1;
	struct session_context *ctx = gnutls_session_get_ptr(session);

	if (!config.hpkp_cache)
		return 0;

	gnutls_pubkey_init(&key);

	if ((rc = gnutls_pubkey_import_x509(key, cert, 0)) != GNUTLS_E_SUCCESS) {
		error_printf(_("Failed to import pubkey: %s\n"), gnutls_strerror(rc));
		return 0;
	}

	gnutls_datum_t pubkey;

	if ((rc = gnutls_pubkey_export2(key, GNUTLS_X509_FMT_DER, &pubkey)) != GNUTLS_E_SUCCESS) {
		error_printf(_("Failed to export pubkey: %s\n"), gnutls_strerror(rc));
		ret = 0;
		goto out;
	}

	rc = wget_hpkp_db_check_pubkey(config.hpkp_cache, hostname, pubkey.data, pubkey.size);
	xfree(pubkey.data);

	if (rc != -2) {
		if (rc == 0) {
			debug_printf("host has no pubkey pinnings stored in hpkp db\n");
			ctx->stats_hpkp = WGET_STATS_HPKP_NO;
		} else if (rc == 1) {
			debug_printf("pubkey is matching a pinning\n");
			ctx->stats_hpkp = WGET_STATS_HPKP_MATCH;
		} else if (rc == -1) {
			debug_printf("Error while checking pubkey pinning\n");
			ctx->stats_hpkp = WGET_STATS_HPKP_ERROR;
		}
		ret = 0;
	} else
		ctx->stats_hpkp = WGET_STATS_HPKP_NOMATCH;

out:
	gnutls_pubkey_deinit(key);
	return ret; // Pubkey not found
}

static void print_verification_status(gnutls_session_t session, const char *tag, int status) {
	gnutls_datum_t out;

	if (gnutls_certificate_verification_status_print(
		status, gnutls_certificate_type_get(session), &out, 0) == GNUTLS_E_SUCCESS)
	{
		error_printf_check("%s: %s\n", tag, out.data); // no translation
		xfree(out.data);
	}
}

/* This function will verify the peer's certificate, and check
 * if the hostname matches, as well as the activation, expiration dates.
 */
static int verify_certificate_callback(gnutls_session_t session)
{
	unsigned int status, deinit_cert = 0, deinit_issuer = 0;
	const gnutls_datum_t *cert_list = NULL;
	unsigned int cert_list_size;
	int ret = -1, err, ocsp_ok = 0;
	gnutls_x509_crt_t cert = NULL, issuer = NULL;
	const char *tag = config.check_certificate ? _("ERROR") : _("WARNING");
#ifdef WITH_OCSP
	bool skip_server_cert_check = false;
	unsigned nvalid = 0, nrevoked = 0, nignored = 0;
#endif

	// read hostname
	struct session_context *ctx = gnutls_session_get_ptr(session);
	const char *hostname = ctx->hostname;

	/* This verification function uses the trusted CAs in the credentials
	 * structure. So you must have installed one or more CA certificates.
	 */
	gnutls_typed_vdata_st data[] = {
		{
			.type = GNUTLS_DT_DNS_HOSTNAME,
			.data = (void *) hostname,
		},
		{
			.type = GNUTLS_DT_KEY_PURPOSE_OID,
			.data = (void *) GNUTLS_KP_TLS_WWW_SERVER,
		}
	};
	unsigned elements = config.check_certificate ? countof(data) : 1;

	if (gnutls_certificate_verify_peers(session, data, elements, &status) != GNUTLS_E_SUCCESS) {
		error_printf_check(_("%s: Certificate verification error\n"), tag);
		goto out;
	}

#ifdef WITH_OCSP
	if (status & GNUTLS_CERT_REVOKED) {
		if (config.ocsp_cert_cache)
			wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, 0); // remove entry from cache
		if (ctx->ocsp_stapling) {
			if (gnutls_x509_crt_init(&cert) == GNUTLS_E_SUCCESS) {
				if ((cert_list = gnutls_certificate_get_peers(session, &cert_list_size))) {
					if (gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER) == GNUTLS_E_SUCCESS) {
						add_cert_to_ocsp_cache(cert, false);
					}
				}
				gnutls_x509_crt_deinit(cert);
			}
		}
	}
#endif

#ifdef WITH_LIBDANE
	// If CA cert verification failed due to missing certificates, we try DANE verification (if requested by the user).
	if (status) {
		if (!config.dane) {
			print_verification_status(session, tag, status);
			goto out;
		}
		if (status != (GNUTLS_CERT_INVALID | GNUTLS_CERT_SIGNER_NOT_FOUND)) {
			print_verification_status(session, tag, status);
			goto out;
		}

		// GNUTLS_CERT_SIGNER_NOT_FOUND indicates that no matching CA cert exists.

		unsigned verify = 0;

		int rc = dane_verify_session_crt(NULL, session, hostname, "tcp", ctx->port, 0,
			DANE_VFLAG_FAIL_IF_NOT_CHECKED,
			&verify);

		if (rc < 0) {
			debug_printf("DANE verification error for %s: %s\n", hostname, dane_strerror(rc));
			goto out;
		} else if (verify) {
			gnutls_datum_t out;
			rc = dane_verification_status_print(verify, &out, 0);
			if (rc < 0) {
				error_printf(_("DANE verification print error for %s: %s\n"), hostname, dane_strerror(rc));
			} else {
				error_printf(_("DANE verification failed for %s: %s\n"), hostname, out.data);
			}
			gnutls_free(out.data);
			goto out;
		} else {
			debug_printf("DANE verification: %s\n", dane_strerror(rc));
		}
	}
#else
	if (status) {
		print_verification_status(session, tag, status);
		goto out;
	}
#endif

	/* Up to here the process is the same for X.509 certificates and
	 * OpenPGP keys. From now on X.509 certificates are assumed. This can
	 * be easily extended to work with openpgp keys as well.
	 */
	if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) {
		error_printf_check(_("%s: Certificate must be X.509\n"), tag);
		goto out;
	}

	if (gnutls_x509_crt_init(&cert) != GNUTLS_E_SUCCESS) {
		error_printf_check(_("%s: Error initializing X.509 certificate\n"), tag);
		goto out;
	}
	deinit_cert = 1;

	if (!(cert_list = gnutls_certificate_get_peers(session, &cert_list_size))) {
		error_printf_check(_("%s: No certificate was found!\n"), tag);
		goto out;
	}

	if ((err = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) {
		error_printf_check(_("%s: Failed to parse certificate: %s\n"), tag, gnutls_strerror (err));
		goto out;
	}

	if (config.check_hostname) {
		if (!hostname || !gnutls_x509_crt_check_hostname(cert, hostname)) {
			error_printf_check(_("%s: Hostname mismatch or missing\n"), tag);
			goto out; // Fail immediately
		}
	}

	// At this point, the cert chain has been found valid regarding the locally available CA certificates and CRLs.
	// Now, we are going to check the revocation status via OCSP
#ifdef WITH_OCSP
	if (config.ocsp_stapling) {
		if (!ctx->valid && ctx->ocsp_stapling) {
			if (gnutls_ocsp_status_request_is_checked(session, 0)) {
				debug_printf("Server certificate is valid regarding OCSP stapling\n");
//				_get_cert_fingerprint(cert, fingerprint, sizeof(fingerprint)); // calc hexadecimal fingerprint string
				add_cert_to_ocsp_cache(cert, true);
				nvalid = 1;
				skip_server_cert_check = true;
			}
			else if (gnutls_ocsp_status_request_is_checked(session, GNUTLS_OCSP_SR_IS_AVAIL)) {
				error_printf_check(_("WARNING: The certificate's (stapled) OCSP status is invalid\n"));
				skip_server_cert_check = true;
			}
			else if (!config.ocsp) {
				debug_printf("OCSP stapling is not supported by '%s'\n", hostname);
			} else {
				error_printf_check(_("WARNING: OCSP stapling is not supported by '%s', but OCSP validation has been requested.\n"), hostname);
				error_printf_check(_("WARNING: This implies a privacy leak: the client sends the certificate serial ID over HTTP to the CA.\n"));
			}
		} else if (ctx->valid)
			debug_printf("OCSP: Host '%s' is valid (from cache)\n", hostname);
	}
#endif

	for (unsigned it = 0; it < cert_list_size; it++) {
		gnutls_x509_crt_deinit(cert);
		gnutls_x509_crt_init(&cert);

		if ((err = gnutls_x509_crt_import(cert, &cert_list[it], GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) {
			error_printf_check(_("%s: Failed to parse certificate[%u]: %s\n"), tag, it, gnutls_strerror (err));
			goto out;
		}

		if (cert_verify_hpkp(cert, hostname, session) != 0) {
			error_printf_check(_("%s: Pubkey pinning mismatch!\n"), tag);
			goto out;
		}

#ifdef WITH_OCSP
		if (!config.ocsp || (skip_server_cert_check && it == 0))
			continue;

		char fingerprint[64 * 2 +1];
		_get_cert_fingerprint(cert, fingerprint, sizeof(fingerprint)); // calc hexadecimal fingerprint string

		int revoked;
		if (wget_ocsp_fingerprint_in_cache(config.ocsp_cert_cache, fingerprint, &revoked)) {
			// found cert's fingerprint in cache
			if (revoked) {
				debug_printf("Certificate[%u] of '%s' has been revoked (cached)\n", it, hostname);
				nrevoked++;
			} else {
				debug_printf("Certificate[%u] of '%s' is valid (cached)\n", it, hostname);
				nvalid++;
			}
			continue;
		}

		if (deinit_issuer) {
			gnutls_x509_crt_deinit(issuer);
			deinit_issuer = 0;
		}
		if ((err = gnutls_certificate_get_issuer(credentials, cert, &issuer, 0)) != GNUTLS_E_SUCCESS && it < cert_list_size - 1) {
			gnutls_x509_crt_init(&issuer);
			deinit_issuer = 1;
			if ((err = gnutls_x509_crt_import(issuer, &cert_list[it + 1], GNUTLS_X509_FMT_DER))  != GNUTLS_E_SUCCESS) {
				debug_printf("Decoding error: %s\n", gnutls_strerror(err));
				goto out;
			}
		} else if (err  != GNUTLS_E_SUCCESS) {
			debug_printf("Cannot find issuer: %s\n", gnutls_strerror(err));
			goto out;
		}

		ocsp_ok = cert_verify_ocsp(cert, issuer);
		debug_printf("check_ocsp_response() returned %d\n", ocsp_ok);

		if (ocsp_ok == 1) {
			debug_printf("Certificate[%u] of '%s' is valid (via OCSP)\n", it, hostname);
			wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache, fingerprint, time(NULL) + 3600, true); // 1h valid
			nvalid++;
		} else if (ocsp_ok == 0) {
			debug_printf("%s: Certificate[%u] of '%s' has been revoked (via OCSP)\n", tag, it, hostname);
			wget_ocsp_db_add_fingerprint(config.ocsp_cert_cache, fingerprint, time(NULL) + 3600, false);  // cert has been revoked
			nrevoked++;
		} else {
			debug_printf("WARNING: OCSP response not available or ignored\n");
			nignored++;
		}
#endif
	}

#ifdef WITH_OCSP
	if (config.ocsp && ocsp_stats_callback) {
		wget_ocsp_stats_data stats;
		stats.hostname = hostname;
		stats.nvalid = nvalid;
		stats.nrevoked = nrevoked;
		stats.nignored = nignored;
		stats.stapling = ctx->ocsp_stapling;

		ocsp_stats_callback(&stats, ocsp_stats_ctx);
	}

	if (config.ocsp_stapling || config.ocsp) {
		if (nvalid == cert_list_size) {
			wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, time(NULL) + 3600); // 1h valid
		} else if (nrevoked) {
			wget_ocsp_db_add_host(config.ocsp_cert_cache, hostname, 0); // remove entry from cache
			goto out;
		}
	}
#endif

	ret = 0;

	// 0: continue handshake
	// else: stop handshake
out:
	if (deinit_cert)
		gnutls_x509_crt_deinit(cert);
	if (deinit_issuer)
		gnutls_x509_crt_deinit(issuer);

	return config.check_certificate ? ret : 0;
}

static int init;
static wget_thread_mutex mutex;

static void tls_exit(void)
{
	if (mutex)
		wget_thread_mutex_destroy(&mutex);
}

INITIALIZER(tls_init)
{
	if (!mutex) {
		wget_thread_mutex_init(&mutex);

		// Initialize paths while in a thread-safe environment (mostly for _WIN32).
		wget_ssl_default_cert_dir();
		wget_ssl_default_ca_bundle_path();

		atexit(tls_exit);
	}
}

static int key_type(int type)
{
	if (type == WGET_SSL_X509_FMT_DER)
		return GNUTLS_X509_FMT_DER;

	return GNUTLS_X509_FMT_PEM;
}

// ssl_init() is thread safe

static void set_credentials(gnutls_certificate_credentials_t creds)
{
	if (config.cert_file && !config.key_file) {
		// Use the private key from the cert file unless otherwise specified.
		config.key_file = config.cert_file;
		config.key_type = config.cert_type;
	}
	else if (!config.cert_file && config.key_file) {
		// Use the cert from the private key file unless otherwise specified.
		config.cert_file = config.key_file;
		config.cert_type = config.key_type;
	}

	if (config.cert_file && config.key_file) {
		if (config.key_type != config.cert_type) {
			// GnuTLS can't handle this
			error_printf(_("GnuTLS requires the key and the cert to be of the same type.\n"));
		}

		if (gnutls_certificate_set_x509_key_file(creds, config.cert_file, config.key_file, key_type(config.key_type)) != GNUTLS_E_SUCCESS)
			error_printf(_("No certificates or keys were found\n"));
	}

	if (config.ca_file && !wget_strcmp(config.ca_file, "system"))
		config.ca_file = wget_ssl_default_ca_bundle_path();
	if (config.ca_file) {
		if (gnutls_certificate_set_x509_trust_file(creds, config.ca_file, key_type(config.ca_type)) <= 0)
			error_printf(_("No CAs were found in '%s'\n"), config.ca_file);
	}
}

/**
 * Initialize the SSL/TLS engine as a client.
 *
 * This function assumes the caller is an SSL client connecting to a server.
 * The functions wget_ssl_open(), wget_ssl_close() and wget_ssl_deinit() can be called
 * after this.
 *
 * This is where the root certificates get loaded from the folder specified in the
 * `WGET_SSL_CA_DIRECTORY` parameter. If any of the files in that folder cannot be loaded
 * for whatever reason, that file will be silently skipped without harm (a message will be
 * printed to the debug log stream).
 *
 * CLRs and private keys and their certificates are also loaded here.
 *
 * On systems with automatic library constructors/destructors, this function
 * is thread-safe. On other systems it is not thread-safe.
 *
 * This function may be called several times. Only the first call really
 * takes action.
 */
void wget_ssl_init(void)
{
	tls_init();

	wget_thread_mutex_lock(mutex);

	if (!init) {
		int rc, ncerts = -1;

		debug_printf("GnuTLS init\n");
		gnutls_global_init();
		gnutls_certificate_allocate_credentials(&credentials);
		gnutls_certificate_set_verify_function(credentials, verify_certificate_callback);

		if (config.ca_directory && *config.ca_directory && config.check_certificate) {
			if (!strcmp(config.ca_directory, "system")) {
				ncerts = gnutls_certificate_set_x509_system_trust(credentials);
				if (ncerts < 0)
					debug_printf("GnuTLS system certificate store error %d\n", ncerts);
				else
					debug_printf("GnuTLS system certificate store is empty\n");
			}

			if (ncerts < 0) {
				DIR *dir;

				ncerts = 0;

				if (!strcmp(config.ca_directory, "system"))
					config.ca_directory = wget_ssl_default_cert_dir();

				if ((dir = opendir(config.ca_directory))) {
					struct dirent *dp;

					while ((dp = readdir(dir))) {
						size_t len = strlen(dp->d_name);

						if (len >= 4 && !wget_strncasecmp_ascii(dp->d_name + len - 4, ".pem", 4)) {
							char *fname = wget_aprintf("%s/%s", config.ca_directory, dp->d_name);

							if (!fname) {
								error_printf(_("Failed to allocate file name for cert '%s/%s'\n"), config.ca_directory, dp->d_name);
								continue;
							}

							struct stat st;
							if (stat(fname, &st) == 0 && S_ISREG(st.st_mode)) {
								debug_printf("GnuTLS loading %s\n", fname);
								if ((rc = gnutls_certificate_set_x509_trust_file(credentials, fname, GNUTLS_X509_FMT_PEM)) <= 0)
									debug_printf("Failed to load cert '%s': (%d)\n", fname, rc);
								else
									ncerts += rc;
							}

							xfree(fname);
						}
					}

					closedir(dir);
				} else {
					error_printf(_("Failed to opendir %s\n"), config.ca_directory);
				}
			}
		}

		if (config.crl_file) {
			if ((rc = gnutls_certificate_set_x509_crl_file(credentials, config.crl_file, GNUTLS_X509_FMT_PEM)) <= 0)
				error_printf(_("Failed to load CRL '%s': (%d)\n"), config.crl_file, rc);
		}

		set_credentials(credentials);

		debug_printf("Certificates loaded: %d\n", ncerts);

		if (!config.secure_protocol || !wget_strcasecmp_ascii(config.secure_protocol, "auto")) {
			// use GnuTLS defaults, which might hold insecure ciphers
			if ((rc = gnutls_priority_init(&priority_cache, NULL, NULL)))
				error_printf(_("GnuTLS: Unsupported default priority 'NULL': %s\n"), gnutls_strerror(rc));
		} else {
			const char *priorities = NULL;

			if (!wget_strcasecmp_ascii(config.secure_protocol, "PFS"))
				priorities = "PFS:-RSA:-VERS-SSL3.0";
			else if (!wget_strncasecmp_ascii(config.secure_protocol, "SSL", 3))
				priorities = "-VERS-TLS-ALL:+VERS-SSL3.0"; // SSLv3 only
			else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1"))
				priorities = "-VERS-SSL3.0";
			else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_1"))
				priorities = "-VERS-SSL3.0:-VERS-TLS1.0";
			else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_2"))
				priorities = "-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1";
			else if (!wget_strcasecmp_ascii(config.secure_protocol, "TLSv1_3"))
				priorities = "-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2:+VERS-TLS1.3";
			else if (*config.secure_protocol)
				priorities = config.secure_protocol; // user-defined priorities

			rc = gnutls_priority_init2(&priority_cache, priorities, NULL, GNUTLS_PRIORITY_INIT_DEF_APPEND);
			if (rc != GNUTLS_E_SUCCESS)
				error_printf(_("GnuTLS: Unsupported priority string '%s': %s\n"), priorities ? priorities : "(null)", gnutls_strerror(rc));
		}

		init++;

		debug_printf("GnuTLS init done\n");
	}

	wget_thread_mutex_unlock(mutex);
}

/**
 * Deinitialize the SSL/TLS engine, after it has been initialized
 * with wget_ssl_init().
 *
 * This function unloads everything that was loaded in wget_ssl_init().
 *
 * On systems with automatic library constructors/destructors, this function
 * is thread-safe. On other systems it is not thread-safe.
 *
 * This function may be called several times. Only the last deinit really
 * takes action.
 */
void wget_ssl_deinit(void)
{
	wget_thread_mutex_lock(mutex);

	if (init == 1) {
		gnutls_certificate_free_credentials(credentials);
		gnutls_priority_deinit(priority_cache);
		gnutls_global_deinit();
	}

	if (init > 0) init--;

	wget_thread_mutex_unlock(mutex);
}

static int do_handshake(gnutls_session_t session, int sockfd, int timeout)
{
	int ret;

	// Wait for socket being ready before we call gnutls_handshake().
	// I had problems on a KVM Win7 + CygWin (gnutls 3.2.4-1).
	int rc = wget_ready_2_write(sockfd, timeout);

	if (rc == 0)
		ret = WGET_E_TIMEOUT;
	else
		ret = WGET_E_HANDSHAKE;

	// Perform the TLS handshake
	while (rc > 0) {
		rc = gnutls_handshake(session);

		if (rc == GNUTLS_E_SUCCESS) {
			ret = WGET_E_SUCCESS;
			break;
		}

		if (gnutls_error_is_fatal(rc)) {
			debug_printf("gnutls_handshake: (%d) %s (errno=%d)\n", rc, gnutls_strerror(rc), errno);

			if (rc == GNUTLS_E_CERTIFICATE_ERROR) {
				ret = WGET_E_CERTIFICATE;
			} else if (rc == GNUTLS_E_PUSH_ERROR && (errno == ECONNREFUSED || errno == ENOTCONN)) {
				/*
				 * ECONNREFUSED: on Linux
				 * ENOTCONN: MinGW (in out Gitlab CI runner)
				 */
				ret = WGET_E_CONNECT;
			} else if (rc == GNUTLS_E_PULL_ERROR && errno == 61 /* ENODATA, but not on OSX/Travis ? */) {
				// We see this with older versions of GnuTLS, e.g. on TravisCI. (Tim, 11.4.2018)
				// It happens when trying to connect to a port without a listener
				ret = WGET_E_CONNECT;
			} else if (rc == GNUTLS_E_PREMATURE_TERMINATION && errno == EAGAIN) {
				// It happens when trying to connect to a closed port
				ret = WGET_E_CONNECT;
			} else if (rc == GNUTLS_E_UNEXPECTED_PACKET_LENGTH && errno == EAGAIN) {
				// We see this with older versions of GnuTLS, e.g. on TravisCI. (Tim, 11.4.2018)
				// It happens when trying to connect to a port without a listener
				ret = WGET_E_CONNECT;
			} else
				ret = WGET_E_HANDSHAKE;

			break;
		}

		if (gnutls_record_get_direction(session)) {
			// wait for writeability
			rc = wget_ready_2_write(sockfd, timeout);
		} else {
			// wait for readability
			rc = wget_ready_2_read(sockfd, timeout);
		}
	}

	if (ret == WGET_E_SUCCESS)
		debug_printf("TLS False Start: %s\n",
			(gnutls_session_get_flags(session) & GNUTLS_SFLAGS_FALSE_START) ? "on" : "off");

	return ret;
}

#ifdef MSG_FASTOPEN
#include <sys/socket.h>
#include <sys/uio.h> // writev
#include <netdb.h>
#include <errno.h>
static ssize_t ssl_writev(gnutls_transport_ptr_t *p, const giovec_t *iov, int iovcnt)
{
	wget_tcp *tcp = (wget_tcp *) p;
	ssize_t ret;

	// info_printf("%s: %d %zu\n", __func__, iovcnt, iov[0].iov_len);
	if (tcp->first_send) {
		struct msghdr hdr = {
			.msg_name = tcp->connect_addrinfo->ai_addr,
			.msg_namelen = tcp->connect_addrinfo->ai_addrlen,
			.msg_iov = (struct iovec *) iov,
			.msg_iovlen = iovcnt,
		};

//		ret = sendto(tcp->sockfd, iov[0].iov_base, iov[0].iov_len, MSG_FASTOPEN,
//				tcp->connect_addrinfo->ai_addr, tcp->connect_addrinfo->ai_addrlen);
		ret = sendmsg(tcp->sockfd, &hdr, MSG_FASTOPEN);
		if (ret < 0) {
			if (errno == EINPROGRESS) {
				errno = EAGAIN; // GnuTLS does not handle EINPROGRESS
			} else if (errno == EOPNOTSUPP) {
				// fallback from fastopen, e.g. when fastopen is disabled in system
				debug_printf("Fallback from TCP Fast Open... TFO is disabled at system level\n");
				tcp->tcp_fastopen = 0;
				set_socket_timeout(tcp->sockfd, tcp->connect_timeout);
				ret = connect(tcp->sockfd, tcp->connect_addrinfo->ai_addr, tcp->connect_addrinfo->ai_addrlen);
				set_socket_timeout(tcp->sockfd, tcp->timeout);
				if (ret < 0 && (errno == ENOTCONN || errno == EINPROGRESS))
					errno = EAGAIN;
			}
		}

		tcp->first_send = 0;
	} else {
		ret = writev(tcp->sockfd, (struct iovec *) iov, iovcnt);
	}
	// info_printf("errno=%d ret=%d\n", errno, ret);

	// after the first write we set back the transport push function and the transport pointer to standard functions
	gnutls_transport_set_int(tcp->ssl_session, tcp->sockfd);

#if defined __clang__
  #pragma clang diagnostic push
  #pragma clang diagnostic ignored "-Wcast-function-type"
#endif
	gnutls_transport_set_vec_push_function(tcp->ssl_session, (ssize_t (*) (gnutls_transport_ptr_t, const giovec_t * iov, int iovcnt)) writev);
#if defined __clang__
  #pragma clang diagnostic pop
#endif

	return ret;
}
#endif

#ifdef _WIN32
static ssize_t win32_send(gnutls_transport_ptr_t p, const void *buf, size_t size)
{
	int sockfd = (int) (ptrdiff_t) p;

	return send(sockfd, buf, size, 0);
}
static ssize_t win32_recv(gnutls_transport_ptr_t p, void *buf, size_t size)
{
	int sockfd = (int) (ptrdiff_t) p;

	return recv(sockfd, buf, size, 0);
}
#endif

/**
 * \param[in] tcp A TCP connection (see wget_tcp_init())
 * \return `WGET_E_SUCCESS` on success or an error code (`WGET_E_*`) on failure
 *
 * Run an SSL/TLS handshake.
 *
 * This functions establishes an SSL/TLS tunnel (performs an SSL/TLS handshake)
 * over an active TCP connection. A pointer to the (internal) SSL/TLS session context
 * can be found in `tcp->ssl_session` after successful execution of this function. This pointer
 * has to be passed to wget_ssl_close() to close the SSL/TLS tunnel.
 *
 * If the handshake cannot be completed in the specified timeout for the provided TCP connection
 * this function fails and returns `WGET_E_TIMEOUT`. You can set the timeout with wget_tcp_set_timeout().
 */
int wget_ssl_open(wget_tcp *tcp)
{
	gnutls_session_t session;
	wget_tls_stats_data stats = {
			.alpn_protocol = NULL,
			.version = -1,
			.false_start = -1,
			.tfo = -1,
			.resumed = 0,
			.http_protocol = WGET_PROTOCOL_HTTP_1_1,
			.cert_chain_size = 0
	};

	int ret = WGET_E_UNKNOWN;
	int rc, sockfd, tls_connect_timeout;
	const char *hostname;
	long long before_millisecs = 0;

	if (!tcp)
		return WGET_E_INVALID;

	struct session_context *ctx = wget_calloc(1, sizeof(struct session_context));
	if (!ctx)
		return WGET_E_MEMORY;

	if (!init)
		wget_ssl_init();

	hostname = tcp->ssl_hostname;
	sockfd= tcp->sockfd;
	tls_connect_timeout = tcp->timeout;

	unsigned int flags = GNUTLS_CLIENT | GNUTLS_AUTO_REAUTH | GNUTLS_POST_HANDSHAKE_AUTH;

	if (tcp->tls_false_start) {
		debug_printf("TLS False Start requested\n");

		flags |= GNUTLS_NONBLOCK | GNUTLS_ENABLE_FALSE_START;

		gnutls_init(&session, flags);
	} else {
		flags |= GNUTLS_NONBLOCK;

		gnutls_init(&session, flags);
	}

	if ((rc = gnutls_priority_set(session, priority_cache)) != GNUTLS_E_SUCCESS)
		error_printf(_("GnuTLS: Failed to set priorities: %s\n"), gnutls_strerror(rc));

	if (!config.check_certificate && !wget_strcasecmp_ascii(config.secure_protocol, "auto"))
		gnutls_session_enable_compatibility_mode(session);

	// RFC 6066 SNI Server Name Indication
	if (hostname) {
		gnutls_server_name_set(session, GNUTLS_NAME_DNS, hostname, strlen(hostname));
		debug_printf("SNI %s\n", hostname);
	}
	gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, credentials);

	ctx->hostname = wget_strdup(hostname);
	ctx->port = tcp->remote_port;

#ifdef WITH_OCSP
	// If we know the cert chain for the hostname being valid at the moment,
	// we don't ask for OCSP stapling to avoid unneeded IP traffic.
	// In the unlikely case that the server's certificate chain changed right now,
	// we fallback to OCSP responder request later (if enabled).
	if (hostname) {
		if (!(ctx->valid = wget_ocsp_hostname_is_valid(config.ocsp_host_cache, hostname))) {
			if ((rc = gnutls_ocsp_status_request_enable_client(session, NULL, 0, NULL)) == GNUTLS_E_SUCCESS) {
				debug_printf("OCSP stapling requested for %s\n", hostname);
				ctx->ocsp_stapling = 1;
			} else
				error_printf("GnuTLS: %s\n", gnutls_strerror(rc)); // no translation
		}
	}
#else
	if (config.ocsp || config.ocsp_stapling)
		error_printf(_("WARNING: OCSP is not available in this version of GnuTLS.\n"));
#endif

	if (config.alpn) {
		unsigned nprot;
		const char *e, *s;

		for (nprot = 0, s = e = config.alpn; *e; s = e + 1)
			if ((e = strchrnul(s, ',')) != s)
				nprot++;

		if (nprot) {
			gnutls_datum_t data[16];

			for (nprot = 0, s = e = config.alpn; *e && nprot < countof(data); s = e + 1) {
				if ((e = strchrnul(s, ',')) != s) {
					data[nprot].data = (unsigned char *) s;
					data[nprot].size = (unsigned) (e - s);
					debug_printf("ALPN offering %.*s\n", (int) data[nprot].size, data[nprot].data);
					nprot++;
				}
			}

			if ((rc = gnutls_alpn_set_protocols(session, data, nprot, 0)))
				debug_printf("GnuTLS: Set ALPN: %s\n", gnutls_strerror(rc));
		}
	}

	tcp->ssl_session = session;
	gnutls_session_set_ptr(session, ctx);

#ifdef MSG_FASTOPEN
	if ((rc = wget_tcp_get_tcp_fastopen(tcp))) {
		if (tls_stats_callback)
			stats.tfo = (char)rc;

		// prepare for TCP FASTOPEN... sendmsg() instead of connect/write on first write
		gnutls_transport_set_vec_push_function(session, (ssize_t (*)(gnutls_transport_ptr_t, const giovec_t *iov, int iovcnt)) ssl_writev);
		gnutls_transport_set_ptr(session, tcp);
	} else {
#endif

#ifdef _WIN32
	gnutls_transport_set_push_function(session, (gnutls_push_func) win32_send);
	gnutls_transport_set_pull_function(session, (gnutls_pull_func) win32_recv);
#endif

	gnutls_transport_set_int(session, sockfd);

#ifdef MSG_FASTOPEN
	}
#endif

	{
		void *data;
		size_t size;

		if (wget_tls_session_get(config.tls_session_cache, ctx->hostname, &data, &size) == 0) {
			debug_printf("found cached session data for %s\n", ctx->hostname);
			if ((rc = gnutls_session_set_data(session, data, size)) != GNUTLS_E_SUCCESS)
				error_printf(_("GnuTLS: Failed to set session data: %s\n"), gnutls_strerror(rc));
			xfree(data);
		}
	}

	if (tls_stats_callback)
		before_millisecs = wget_get_timemillis();

	ret = do_handshake(session, sockfd, tls_connect_timeout);

	if (tls_stats_callback) {
		long long after_millisecs = wget_get_timemillis();
		stats.tls_secs = after_millisecs - before_millisecs;
		stats.tls_con = 1;
		stats.false_start = (gnutls_session_get_flags(session) & GNUTLS_SFLAGS_FALSE_START) != 0;
	}

	if (config.alpn) {
		gnutls_datum_t protocol;
		if ((rc = gnutls_alpn_get_selected_protocol(session, &protocol))) {
			debug_printf("GnuTLS: Get ALPN: %s\n", gnutls_strerror(rc));
			if (!strstr(config.alpn,"http/1.1"))
				ret = WGET_E_CONNECT;
		} else {
			debug_printf("ALPN: Server accepted protocol '%.*s'\n", (int) protocol.size, protocol.data);
			if (tls_stats_callback)
				stats.alpn_protocol = wget_strmemdup(protocol.data, protocol.size);

			if (!memcmp(protocol.data, "h2", 2)) {
				tcp->protocol = WGET_PROTOCOL_HTTP_2_0;
				if (tls_stats_callback)
					stats.http_protocol = WGET_PROTOCOL_HTTP_2_0;
			}
		}
	}

	if (config.print_info)
		print_info(session);

	if (ret == WGET_E_SUCCESS) {
		int resumed = gnutls_session_is_resumed(session);

		if (tls_stats_callback) {
			stats.resumed = resumed;
			stats.version = gnutls_protocol_get_version(session);
			gnutls_certificate_get_peers(session, (unsigned int *)&(stats.cert_chain_size));
		}

		debug_printf("Handshake completed%s\n", resumed ? " (resumed session)" : "");

		if (!resumed && config.tls_session_cache) {
			if (tcp->tls_false_start) {
				ctx->delayed_session_data = 1;
			} else {
				gnutls_datum_t session_data;

				if ((rc = gnutls_session_get_data2(session, &session_data)) == GNUTLS_E_SUCCESS) {
					wget_tls_session_db_add(config.tls_session_cache,
						wget_tls_session_new(ctx->hostname, 18 * 3600, session_data.data, session_data.size)); // 18h valid
					xfree(session_data.data);
				} else
					debug_printf("Failed to get session data: %s", gnutls_strerror(rc));
			}
		}
	}

	if (tls_stats_callback) {
		stats.hostname = hostname;
		tls_stats_callback(&stats, tls_stats_ctx);
		xfree(stats.alpn_protocol);
	}

	tcp->hpkp = ctx->stats_hpkp;

	if (ret != WGET_E_SUCCESS) {
		if (ret == WGET_E_TIMEOUT)
			debug_printf("Handshake timed out\n");
		xfree(ctx->hostname);
		xfree(ctx);
		gnutls_deinit(session);
		tcp->ssl_session = NULL;
	}

	return ret;
}

/**
 * \param[in] session The SSL/TLS session (a pointer to it), which is located at the `ssl_session` field
 * of the TCP connection (see wget_ssl_open()).
 *
 * Close an active SSL/TLS tunnel, which was opened with wget_ssl_open().
 *
 * The underlying TCP connection is kept open.
 */
void wget_ssl_close(void **session)
{
	if (session && *session) {
		gnutls_session_t s = *session;
		struct session_context *ctx = gnutls_session_get_ptr(s);
		int ret;

		do
			ret = gnutls_bye(s, GNUTLS_SHUT_WR);
		while (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN);

		if (ret < 0)
			debug_printf("TLS shutdown failed: %s\n", gnutls_strerror(ret));

		gnutls_deinit(s);
		*session = NULL;

		xfree(ctx->hostname);
		xfree(ctx);
	}
}

/**
 * \param[in] session An opaque pointer to the SSL/TLS session (obtained with wget_ssl_open() or wget_ssl_server_open())
 * \param[in] buf Destination buffer where the read data will be placed
 * \param[in] count Length of the buffer \p buf
 * \param[in] timeout The amount of time to wait until data becomes available (in milliseconds)
 * \return The number of bytes read, or a negative value on error.
 *
 * Read data from the SSL/TLS tunnel.
 *
 * This function will read at most \p count bytes, which will be stored
 * in the buffer \p buf.
 *
 * The \p timeout parameter tells how long to wait until some data becomes
 * available to read. A \p timeout value of zero causes this function to return
 * immediately, whereas a negative value will cause it to wait indefinitely.
 * This function returns the number of bytes read, which may be zero if the timeout elapses
 * without any data having become available.
 *
 * If a rehandshake is needed, this function does it automatically and tries
 * to read again.
 */
ssize_t wget_ssl_read_timeout(void *session, char *buf, size_t count, int timeout)
{
	int sockfd = gnutls_transport_get_int(session);

	ssize_t nbytes;

	for (;;) {
		int rc;

		if (gnutls_record_check_pending(session) <= 0 && (rc = wget_ready_2_read(sockfd, timeout)) <= 0)
			return rc;

		nbytes = gnutls_record_recv(session, buf, count);

		// If False Start + Session Resumption are enabled, we get the session data after the first read()
		struct session_context *ctx = gnutls_session_get_ptr(session);
		if (ctx && ctx->delayed_session_data) {
			gnutls_datum_t session_data;

			if ((rc = gnutls_session_get_data2(session, &session_data)) == GNUTLS_E_SUCCESS) {
				debug_printf("Got delayed session data\n");
				ctx->delayed_session_data = 0;
				wget_tls_session_db_add(config.tls_session_cache,
					wget_tls_session_new(ctx->hostname, 18 * 3600, session_data.data, session_data.size)); // 18h valid
				xfree(session_data.data);
			} else
				debug_printf("No delayed session data%s\n", gnutls_strerror(rc));
		}

		if (nbytes == GNUTLS_E_REHANDSHAKE) {
			debug_printf("*** REHANDSHAKE while reading\n");
			if ((nbytes = do_handshake(session, sockfd, timeout)) == 0)
				nbytes = GNUTLS_E_AGAIN; /* restart reading */
		}
		if (nbytes >= 0 || nbytes != GNUTLS_E_AGAIN)
			break;
	}

	return nbytes < -1 ? -1 : nbytes;
}

/**
 * \param[in] session An opaque pointer to the SSL/TLS session (obtained with wget_ssl_open() or wget_ssl_server_open())
 * \param[in] buf Buffer with the data to be sent
 * \param[in] count Length of the buffer \p buf
 * \param[in] timeout The amount of time to wait until data can be sent to the wire (in milliseconds)
 * \return The number of bytes written, or a negative value on error.
 *
 * Send data through the SSL/TLS tunnel.
 *
 * This function will write \p count bytes from \p buf.
 *
 * The \p timeout parameter tells how long to wait until data can be finally sent
 * over the SSL/TLS tunnel. A \p timeout value of zero causes this function to return
 * immediately, whereas a negative value will cause it to wait indefinitely.
 * This function returns the number of bytes sent, which may be zero if the timeout elapses
 * before any data could be sent.
 *
 * If a rehandshake is needed, this function does it automatically and tries
 * to write again.
 */
ssize_t wget_ssl_write_timeout(void *session, const char *buf, size_t count, int timeout)
{
	int sockfd = gnutls_transport_get_int(session);

	for (;;) {
		ssize_t nbytes;
		int rc;

		if ((rc = wget_ready_2_write(sockfd, timeout)) <= 0)
			return rc;

		if ((nbytes = gnutls_record_send(session, buf, count)) >= 0)
			return nbytes;

		if (nbytes == GNUTLS_E_REHANDSHAKE) {
			debug_printf("*** REHANDSHAKE while writing\n");
			if ((nbytes = do_handshake(session, sockfd, timeout)) == 0)
				continue; /* restart writing */
		}
		if (nbytes == GNUTLS_E_AGAIN)
			return 0; // indicate timeout

		return -1;
	}
}

/**
 * \param[in] fn A `wget_ssl_stats_callback_tls_t` callback function to receive TLS statistics data
 * \param[in] ctx Context data given to \p fn
 *
 * Set callback function to be called when TLS statistics are available
 */
void wget_ssl_set_stats_callback_tls(wget_tls_stats_callback *fn, void *ctx)
{
	tls_stats_callback = fn;
	tls_stats_ctx = ctx;
}

/**
 * \param[in] fn A `wget_ssl_stats_callback_ocsp_t` callback function to receive OCSP statistics data
 * \param[in] ctx Context data given to \p fn
 *
 * Set callback function to be called when OCSP statistics are available
 */
void wget_ssl_set_stats_callback_ocsp(wget_ocsp_stats_callback *fn, void *ctx)
{
	ocsp_stats_callback = fn;
	ocsp_stats_ctx = ctx;
}

/** @} */
