/* Released under AGPL v3 with exception for the OpenSSL library. See license.txt */
#include "config.h"
#ifndef NO_SSL
#include <errno.h>
#include "gen.h"
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <openssl/bio.h>
#include <openssl/conf.h>
#include <openssl/engine.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/md5.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>

#include "error.h"
#include "gen.h"
#include "mssl.h"
#include "tcp.h"
#include "io.h"
#include "http.h"
#include "utils.h"
#include "main.h"

BIO *bio_err = NULL;

void shutdown_ssl(void)
{
	BIO_free(bio_err);

	ERR_free_strings();

	CONF_modules_free();
	EVP_cleanup();
	CRYPTO_cleanup_all_ex_data();
}

int close_ssl_connection(SSL *const ssl_h)
{
	int rc = SSL_shutdown(ssl_h);

	if (!rc)
		rc = SSL_shutdown(ssl_h);

/*
ignoring failures: the socket will be closed
anyway later on so any openssl failures won't
do any harm
	if (rc == -1)
	{
		fprintf(stderr, "SSL_shutdown failed: %s\n", strerror(errno));
		return -1;
	}
*/

	return 0;
}

int READ_SSL(SSL *const ssl_h, char *whereto, int len, const double timeout)
{
	const int cnt = len;
	const int fd = SSL_get_rfd(ssl_h);
	double end = get_ts() + timeout;

	while(len > 0 && !got_sigquit)
	{
		int rc = -1;
		fd_set rfds, wfds;
		struct timeval tv;
		double now = get_ts(), left = end - now;

		if (left <= 0.0)
		{
			set_error(gettext("Time-out on SSL connection"));
			return -1;
		}

		FD_ZERO(&rfds);
		FD_SET(fd, &rfds);
		FD_ZERO(&wfds); /* yes, see openssl */
		FD_SET(fd, &wfds);

		tv.tv_sec = left;
		tv.tv_usec = (left - tv.tv_sec) * 1000000;

		rc = select(fd + 1, &rfds, &wfds, NULL, &tv);

		if (rc == -1)
		{
			if (errno != EINTR && errno != EAGAIN)
				set_error(gettext("READ_SSL: io-error: %s"), strerror(errno));

			return -1;
		}

		if (rc == 0)
		{
			set_error(gettext("Time-out on SSL connection (READ)"));
			return -1;
		}

		rc = SSL_read(ssl_h, whereto, len);
		if (rc == -1)
		{
			if (errno != EINTR && errno != EAGAIN)
				set_error(gettext("READ_SSL: io-error: %s"), strerror(errno));

			return -1;
		}

		if (rc == 0)
			return 0;

		whereto += rc;
		len -= rc;
	}

	return cnt;
}

int WRITE_SSL(SSL *const ssl_h, const char *wherefrom, int len, const double timeout)
{
	const int cnt = len;
	const int fd = SSL_get_wfd(ssl_h);
	double end = get_ts() + timeout;

	while(len > 0 && !got_sigquit)
	{
		int rc = -1;
		fd_set rfds, wfds;
		struct timeval tv;
		double now = get_ts(), left = end - now;

		if (left <= 0.0)
		{
			set_error(gettext("Time-out on SSL connection"));
			return -1;
		}

		FD_ZERO(&rfds); /* yes, that's correct */
		FD_SET(fd, &rfds);
		FD_ZERO(&wfds);
		FD_SET(fd, &wfds);

		tv.tv_sec = left;
		tv.tv_usec = (left - tv.tv_sec) * 1000000;

		rc = select(fd + 1, &rfds, &wfds, NULL, &tv);

		if (rc == -1)
		{
			if (errno != EINTR && errno != EAGAIN)
				set_error(gettext("WRITE_SSL: io-error: %s"), strerror(errno));

			return -1;
		}

		if (rc == 0)
		{
			set_error(gettext("Time-out on SSL connection (write)"));
			return -1;
		}

		rc = SSL_write(ssl_h, wherefrom, len);
		if (rc == -1)
		{
			if (errno != EINTR && errno != EAGAIN)
				set_error(gettext("WRITE_SSL: io-error: %s"), strerror(errno));
			return -1;
		}

		if (rc == 0)
			return 0;

		wherefrom += rc;
		len -= rc;
	}

	return cnt;
}

int connect_ssl(const int fd, SSL_CTX *const client_ctx, SSL **const ssl_h, BIO **const s_bio, const double timeout, double *const ssl_handshake, char *const hostname, const char ignore_ssl_errors)
{
	double dstart = get_ts();
	double end = get_ts() + timeout;

	struct timeval tv;
	tv.tv_sec  = (long)(timeout / 1000.0);
	tv.tv_usec = (long)(timeout * 1000.0) % 1000000;

	*ssl_handshake = -1.0;

	if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv) == -1)
	{
		set_error(gettext("problem setting receive timeout (%s)"), strerror(errno));
		return -1;
	}

	if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv) == -1)
	{
		set_error(gettext("problem setting transmit timeout (%s)"), strerror(errno));
		return -1;
	}

	*ssl_h = SSL_new(client_ctx);
	SSL_set_tlsext_host_name(*ssl_h, hostname);

	X509_VERIFY_PARAM *param = SSL_get0_param(*ssl_h);
	X509_VERIFY_PARAM_set1_host(param, hostname, 0);

	*s_bio = BIO_new_socket(fd, BIO_NOCLOSE);
	SSL_set_bio(*ssl_h, *s_bio, *s_bio);

	if (set_fd_nonblocking(fd) == -1)
		return RC_INVAL;

	do
	{
		int rc = SSL_connect(*ssl_h);

		if (rc <= 0)
		{
			int err = SSL_get_error(*ssl_h, rc);

			if (err == SSL_ERROR_WANT_CONNECT || err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
			{
				struct timeval tv;
				fd_set fds;
				double left = end - get_ts();

				if (left <= 0)
				{
					set_error(gettext("Time-out during SSL handshake"));
					return -1;
				}

				tv.tv_sec = left;
				tv.tv_usec = (left - tv.tv_sec) * 1000000;

				FD_ZERO(&fds);
				FD_SET(fd, &fds);

				if (err == SSL_ERROR_WANT_READ)
					rc = select(fd + 1, &fds, NULL, NULL, &tv);
				else
					rc = select(fd + 1, NULL, &fds, NULL, &tv);
			}
			else
			{
				set_error(gettext("SSL handshake error: %s"), ERR_reason_error_string(ERR_get_error()));
				return -1;
			}
		}
	}
	while (!SSL_is_init_finished(*ssl_h) && !got_sigquit);

	X509 *cert = SSL_get_peer_certificate(*ssl_h);
	if (cert)
		X509_free(cert);
	else
		set_error(gettext("SSL no peer certificate"));

	long v = SSL_get_verify_result(*ssl_h);
	if (v != X509_V_OK && ignore_ssl_errors == 0)
		set_error(gettext("SSL certificate validation failed: %s"), X509_verify_cert_error_string(v));

	if (got_sigquit)
		return -1;

	*ssl_handshake = get_ts() - dstart;

	if (set_fd_blocking(fd) == -1)
		return -1;

	return 0;
}

SSL_CTX * initialize_ctx(const char ask_compression, const char *ca_path)
{
	const SSL_METHOD *meth = NULL;
	SSL_CTX *ctx = NULL;

	if (!bio_err)
	{
		SSL_library_init();
		SSL_load_error_strings();
		ERR_load_crypto_strings();

		/* error write context */
		bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
	}

	/* create context */
	meth = SSLv23_method();
	ctx = SSL_CTX_new(meth);

	if (ca_path == NULL)
#if defined(__NetBSD__)
		ca_path = "/etc/openssl/certs";
#else
		ca_path = "/etc/ssl/certs";
#endif

	SSL_CTX_load_verify_locations(ctx, NULL, ca_path);

#ifdef SSL_OP_NO_COMPRESSION
	if (!ask_compression)
		SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION);
#endif

	return ctx;
}

char * get_fingerprint(SSL *const ssl_h)
{
	char *string = NULL;

	unsigned char fp_digest[EVP_MAX_MD_SIZE];
	X509 *x509_data = SSL_get_peer_certificate(ssl_h);

	if (x509_data)
	{
		unsigned int fp_digest_size = sizeof fp_digest;

		memset(fp_digest, 0x00, fp_digest_size);

		if (X509_digest(x509_data, EVP_md5(), fp_digest, &fp_digest_size))
		{
			string = (char *)malloc(MD5_DIGEST_LENGTH * 3 + 1);
			if (string)
			{
				int loop, pos =0;

				for(loop=0; loop<MD5_DIGEST_LENGTH; loop++)
				{
					if (loop)
						pos += sprintf(&string[pos], ":%02x", fp_digest[loop]);
					else
						pos = sprintf(&string[pos], "%02x", fp_digest[loop]);
				}
			}
		}

		X509_free(x509_data);
	}

	return string;
}

int connect_ssl_proxy(const int fd, struct addrinfo *const ai, const double timeout, const char *const proxy_user, const char *const proxy_password, const char *const hostname, const int portnr, char *const tfo)
{
	int rc = -1;
	char request_headers[4096] = { 0 };
	int request_headers_len = -1;
	char rh_sent = 0;
	char *response_headers = NULL, *code = NULL, *term = NULL;

	request_headers_len = snprintf(request_headers, sizeof request_headers, "CONNECT %s:%d HTTP/1.1\r\nUser-Agent: HTTPing v" VERSION \
			"\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: %s\r\n", hostname, portnr, hostname);

	if (proxy_user)
	{
		char ppa_string[256] = { 0 };
		char b64_ppa_string[512] = { 0 };

		sprintf(ppa_string, "%s:%s", proxy_user, proxy_password);
		enc_b64(ppa_string, strlen(ppa_string), b64_ppa_string);
		request_headers_len += snprintf(&request_headers[request_headers_len], sizeof request_headers - request_headers_len, "Proxy-Authorization: Basic %s\r\n", b64_ppa_string);
	}

	request_headers_len += snprintf(&request_headers[request_headers_len], sizeof request_headers - request_headers_len, "\r\n");

	if ((rc = connect_to(fd, ai, timeout, tfo, request_headers, request_headers_len, &rh_sent)) == -1)
		return rc;

	if (!rh_sent)
	{
		if ((rc = mywrite(fd, request_headers, request_headers_len, timeout)) < RC_OK)
		{
			set_error(gettext("Problem sending request to proxy"));
			return rc;
		}
	}

	rc = dumb_get_HTTP_headers(fd, &response_headers, timeout);
	if (rc != RC_OK)
	{
		free(response_headers);
		set_error(gettext("Problem retrieving proxy response"));
		return rc;
	}

	term = strchr(response_headers, '\r');
	if (!term)
		term = strchr(response_headers, '\n');
	if (term)
		*term = 0x00;

	code = strchr(response_headers, ' ');
	if (!code)
	{
		free(response_headers);
		set_error(gettext("Invalid proxy response headers"));
		return RC_INVAL;
	}

	if (atoi(code + 1) != 200)
	{
		free(response_headers);
		set_error(gettext("Proxy indicated error: %s"), code + 1);
		return RC_INVAL;
	}

	free(response_headers);

	return RC_OK;
}
#endif
