/*
 * Copyright 2013-2016 Haiku, Inc.
 * Copyright 2011-2015, Axel Dörfler, axeld@pinc-software.de.
 * Copyright 2016, Rene Gollent, rene@gollent.com.
 * Copyright 2010, Clemens Zeidler <haiku@clemens-zeidler.de>
 * Distributed under the terms of the MIT License.
 */


#include <SecureSocket.h>

#ifdef OPENSSL_ENABLED
#	include <openssl/ssl.h>
#	include <openssl/ssl3.h> // for TRACE_SESSION_KEY only
#	include <openssl/err.h>
#endif

#include <pthread.h>

#ifndef HoG_GENODE
#include <Certificate.h>
#endif
#include <FindDirectory.h>
#include <Path.h>

#include <AutoDeleter.h>

#ifndef HoG_GENODE
#include "CertificatePrivate.h"
#endif


//#define TRACE_SOCKET
#ifdef TRACE_SOCKET
#	define TRACE(x...) printf(x)
#else
#	define TRACE(x...) ;
#endif

//#define TRACE_SESSION_KEY


#ifdef OPENSSL_ENABLED

#ifdef TRACE_SESSION_KEY
#if OPENSSL_VERSION_NUMBER < 0x10100000L
/*
 * print session id and master key in NSS keylog format (RSA
 * Session-ID:<session id> Master-Key:<master key>)
 */
int SSL_SESSION_print_keylog(BIO *bp, const SSL_SESSION *x)
{
    size_t i;

    if (x == NULL)
        goto err;
    if (x->session_id_length == 0 || x->master_key_length == 0)
        goto err;

    /*
     * the RSA prefix is required by the format's definition although there's
     * nothing RSA-specific in the output, therefore, we don't have to check if
     * the cipher suite is based on RSA
     */
    if (BIO_puts(bp, "RSA ") <= 0)
        goto err;

    if (BIO_puts(bp, "Session-ID:") <= 0)
        goto err;
    for (i = 0; i < x->session_id_length; i++) {
        if (BIO_printf(bp, "%02X", x->session_id[i]) <= 0)
            goto err;
    }
    if (BIO_puts(bp, " Master-Key:") <= 0)
        goto err;
    for (i = 0; i < (size_t)x->master_key_length; i++) {
        if (BIO_printf(bp, "%02X", x->master_key[i]) <= 0)
            goto err;
    }
    if (BIO_puts(bp, "\n") <= 0)
        goto err;

    return (1);
 err:
    return (0);
}
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
/*
 * print client random id and master key in NSS keylog format
 * as session ID is not enough.
 */
int SSL_SESSION_print_client_random(BIO *bp, const SSL *ssl)
{
	const SSL_SESSION *x = SSL_get_session(ssl);
    size_t i;

    if (x == NULL)
        goto err;
    if (x->session_id_length == 0 || x->master_key_length == 0)
        goto err;

    /*
     * the RSA prefix is required by the format's definition although there's
     * nothing RSA-specific in the output, therefore, we don't have to check if
     * the cipher suite is based on RSA
     */
    if (BIO_puts(bp, "CLIENT_RANDOM ") <= 0)
        goto err;

    for (i = 0; i < sizeof(ssl->s3->client_random); i++) {
        if (BIO_printf(bp, "%02X", ssl->s3->client_random[i]) <= 0)
            goto err;
    }
    if (BIO_puts(bp, " ") <= 0)
        goto err;
    for (i = 0; i < (size_t)x->master_key_length; i++) {
        if (BIO_printf(bp, "%02X", x->master_key[i]) <= 0)
            goto err;
    }
    if (BIO_puts(bp, "\n") <= 0)
        goto err;

    return (1);
 err:
    return (0);
}
#endif /* TRACE_SESSION_KEY */

class BSecureSocket::Private {
public:
								Private();
								~Private();

			status_t			InitCheck();
			status_t			ErrorCode(int returnValue);

	static	SSL_CTX*			Context();
	static	int					VerifyCallback(int ok, X509_STORE_CTX* ctx);

private:
	static	void				_CreateContext();

public:
			SSL*				fSSL;
			BIO*				fBIO;
	static	int					sDataIndex;

private:
	static	SSL_CTX*			sContext;
		// FIXME When do we SSL_CTX_free it?
	static	pthread_once_t		sInitOnce;
#ifdef TRACE_SESSION_KEY
public:
	static	BIO*				sKeyLogBIO;
#endif

};


/* static */ SSL_CTX* BSecureSocket::Private::sContext = NULL;
/* static */ int BSecureSocket::Private::sDataIndex;
/* static */ pthread_once_t BSecureSocket::Private::sInitOnce
	= PTHREAD_ONCE_INIT;
#ifdef TRACE_SESSION_KEY
/* static */ BIO* BSecureSocket::Private::sKeyLogBIO = NULL;
#endif


BSecureSocket::Private::Private()
	:
	fSSL(NULL),
	fBIO(BIO_new(BIO_s_socket()))
{
}


BSecureSocket::Private::~Private()
{
	// SSL_free also frees the underlying BIO.
	if (fSSL != NULL)
		SSL_free(fSSL);
	else {
		// The SSL session was never created (Connect() was not called or
		// failed). We must free the BIO we created in the constructor.
		BIO_free(fBIO);
	}
}


status_t
BSecureSocket::Private::InitCheck()
{
	if (fBIO == NULL)
		return B_NO_MEMORY;
	return B_OK;
}


status_t
BSecureSocket::Private::ErrorCode(int returnValue)
{
	int error = SSL_get_error(fSSL, returnValue);
	switch (error) {
		case SSL_ERROR_NONE:
			// Shouldn't happen...
			return B_NO_ERROR;
		case SSL_ERROR_ZERO_RETURN:
			// Socket is closed
			return B_CANCELED;
		case SSL_ERROR_SSL:
			// Probably no certificate
			return B_NOT_ALLOWED;

		case SSL_ERROR_SYSCALL:
		{
			unsigned long error2;
			// Check for extra errors in the error stack...
			for(;;) {
				error2 = ERR_get_error();
				if (error2 == 0)
					break;
				fprintf(stderr, "SSL ERR %s\n", ERR_error_string(error2, NULL));
			}

			if (returnValue == 0)
			{
				// unexpected EOF, the remote host closed the socket without
				// telling us why.
				return ECONNREFUSED;
			}

			if (returnValue == -1)
			{
				fprintf(stderr, "SSL %s\n", ERR_error_string(error, NULL));
				return errno;
			}

			fprintf(stderr, "SSL %s\n", ERR_error_string(error, NULL));
			return B_ERROR;
		}

		case SSL_ERROR_WANT_READ:
		case SSL_ERROR_WANT_WRITE:
		case SSL_ERROR_WANT_CONNECT:
		case SSL_ERROR_WANT_ACCEPT:
		case SSL_ERROR_WANT_X509_LOOKUP:
		default:
			// TODO: translate SSL error codes!
			fprintf(stderr, "SSL %s\n", ERR_error_string(error, NULL));
			return B_ERROR;
	}
}


/* static */ SSL_CTX*
BSecureSocket::Private::Context()
{
	// We use lazy initialisation here, because reading certificates from disk
	// and parsing them is a relatively long operation and uses some memory.
	// We don't want programs that don't use SSL to waste resources with that.
	pthread_once(&sInitOnce, _CreateContext);

	return sContext;
}


/*!	This is called each time a certificate verification occurs. It allows us to
	catch failures and report them.
*/
/* static */ int
BSecureSocket::Private::VerifyCallback(int ok, X509_STORE_CTX* ctx)
{
	// OpenSSL already checked the certificate again the certificate store for
	// us, and tells the result of that in the ok parameter.

	// If the verification succeeded, no need for any further checks. Let's
	// proceed with the connection.
	if (ok)
		return ok;

	// The certificate verification failed. Signal this to the BSecureSocket.

	// First of all, get the affected BSecureSocket
	SSL* ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx,
		SSL_get_ex_data_X509_STORE_CTX_idx());
	BSecureSocket* socket = (BSecureSocket*)SSL_get_ex_data(ssl, sDataIndex);

	// Get the certificate that we could not validate (this may not be the one
	// we got from the server, but something higher up in the certificate
	// chain)
	X509* x509 = X509_STORE_CTX_get_current_cert(ctx);
	BCertificate::Private* certificate
		= new(std::nothrow) BCertificate::Private(x509);

	if (certificate == NULL)
		return 0;

	int error = X509_STORE_CTX_get_error(ctx);
	const char* message = X509_verify_cert_error_string(error);

	// Let the BSecureSocket (or subclass) decide if we should continue anyway.
	BCertificate failedCertificate(certificate);
	return socket->CertificateVerificationFailed(failedCertificate, message);
}


#if TRACE_SSL
static void apps_ssl_info_callback(const SSL *s, int where, int ret)
{
	const char *str;
	int w;

	w=where& ~SSL_ST_MASK;

	if (w & SSL_ST_CONNECT)
		str="SSL_connect";
	else if (w & SSL_ST_ACCEPT)
		str="SSL_accept";
	else
		str="undefined";

	if (where & SSL_CB_LOOP) {
		fprintf(stderr,"%s:%s\n", str, SSL_state_string_long(s));
	} else if (where & SSL_CB_ALERT) {
		str = (where & SSL_CB_READ) ? "read" : "write";
		fprintf(stderr,"SSL3 alert %s:%s:%s\n",
				str,
				SSL_alert_type_string_long(ret),
				SSL_alert_desc_string_long(ret));
	} else if (where & SSL_CB_EXIT) {
		if (ret == 0)
			fprintf(stderr,"%s:failed in %s\n",
					str,SSL_state_string_long(s));
		else if (ret < 0) {
			fprintf(stderr,"%s:error in %s\n",
					str,SSL_state_string_long(s));
		}
	}
}
#endif


/* static */ void
BSecureSocket::Private::_CreateContext()
{
	// We want SSL to report errors in human readable format.
	SSL_load_error_strings();

	sContext = SSL_CTX_new(SSLv23_method());

#if TRACE_SSL
	// For debugging purposes: get all SSL messages to the standard error.
	SSL_CTX_set_info_callback(sContext, apps_ssl_info_callback);
#endif

	// Disable legacy protocols. They have known vulnerabilities.
	SSL_CTX_set_options(sContext, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);

	// Disable SSL/TLS compression to prevent the CRIME attack.
	SSL_CTX_set_options(sContext, SSL_OP_NO_COMPRESSION);

	// Don't bother us with ERROR_WANT_READ.
	SSL_CTX_set_mode(sContext, SSL_MODE_AUTO_RETRY);

	// Setup cipher suites.
	// Only accept reasonably secure ones ("HIGH") and disable some known
	// broken stuff (https://wiki.openssl.org/index.php/SSL/TLS_Client)
	SSL_CTX_set_cipher_list(sContext, "HIGH:!aNULL:!PSK:!SRP:!MD5:!RC4");

	SSL_CTX_set_ecdh_auto(sContext, 1);

	// Setup certificate verification
	BPath certificateStore;
	find_directory(B_SYSTEM_DATA_DIRECTORY, &certificateStore);
	certificateStore.Append("ssl/CARootCertificates.pem");
	// TODO we may want to add a non-packaged certificate directory?
	// (would make it possible to store user-added certificate exceptions
	// there)
	SSL_CTX_load_verify_locations(sContext, certificateStore.Path(), NULL);
	SSL_CTX_set_verify(sContext, SSL_VERIFY_PEER, VerifyCallback);

	// OpenSSL 1.0.2 and later: use the alternate "trusted first" algorithm to validate certificate
	// chains. This makes the validation stop as soon as a recognized certificate is found in the
	// chain, instead of validating the whole chain, then seeing if the root certificate is known.
#ifdef X509_V_FLAG_TRUSTED_FIRST
	X509_VERIFY_PARAM* verifyParam = X509_VERIFY_PARAM_new();
	X509_VERIFY_PARAM_set_flags(verifyParam, X509_V_FLAG_TRUSTED_FIRST);
	SSL_CTX_set1_param(sContext, verifyParam);

	// TODO we need to free this after freeing the SSL context (which we currently never do)
	// X509_VERIFY_PARAM_free(verifyParam);
#endif

	// Get an unique index number for storing application data in SSL
	// structs. We will store a pointer to the BSecureSocket class there.
	sDataIndex = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);

#ifdef TRACE_SESSION_KEY
	FILE *keylog = NULL;
	const char *logpath = getenv("SSLKEYLOGFILE");
	if (logpath)
		keylog = fopen(logpath, "w+");
	if (keylog) {
		fprintf(keylog, "# Key Log File generated by Haiku Network Kit\n");
		sKeyLogBIO = BIO_new_fp(keylog, BIO_NOCLOSE);
	}
#endif
}


// # pragma mark - BSecureSocket


BSecureSocket::BSecureSocket()
	:
	fPrivate(new(std::nothrow) BSecureSocket::Private())
{
	fInitStatus = fPrivate != NULL ? fPrivate->InitCheck() : B_NO_MEMORY;
}


BSecureSocket::BSecureSocket(const BNetworkAddress& peer, bigtime_t timeout)
	:
	fPrivate(new(std::nothrow) BSecureSocket::Private())
{
	fInitStatus = fPrivate != NULL ? fPrivate->InitCheck() : B_NO_MEMORY;
	Connect(peer, timeout);
}


BSecureSocket::BSecureSocket(const BSecureSocket& other)
	:
	BSocket(other)
{
	fPrivate = new(std::nothrow) BSecureSocket::Private(*other.fPrivate);
		// TODO: this won't work this way! - write working copy constructor for
		// Private.

	if (fPrivate != NULL)
		SSL_set_ex_data(fPrivate->fSSL, Private::sDataIndex, this);
	else
		fInitStatus = B_NO_MEMORY;

}


BSecureSocket::~BSecureSocket()
{
	delete fPrivate;
}


status_t
BSecureSocket::Accept(BAbstractSocket*& _socket)
{
	int fd = -1;
	BNetworkAddress peer;
	status_t error = AcceptNext(fd, peer);
	if (error != B_OK)
		return error;
	BSecureSocket* socket = new(std::nothrow) BSecureSocket();
	ObjectDeleter<BSecureSocket> socketDeleter(socket);
	if (socket == NULL || socket->InitCheck() != B_OK) {
		close(fd);
		return B_NO_MEMORY;
	}

	socket->_SetTo(fd, fLocal, peer);
	error = socket->_SetupAccept();
	if (error != B_OK)
		return error;

	_socket = socket;
	socketDeleter.Detach();

	return B_OK;
}


status_t
BSecureSocket::Connect(const BNetworkAddress& peer, bigtime_t timeout)
{
	status_t status = InitCheck();
	if (status != B_OK)
		return status;

	status = BSocket::Connect(peer, timeout);
	if (status != B_OK)
		return status;

	return _SetupConnect(peer.HostName().String());
}


void
BSecureSocket::Disconnect()
{
	if (IsConnected()) {
		if (fPrivate->fSSL != NULL)
			SSL_shutdown(fPrivate->fSSL);

		BSocket::Disconnect();
	}
}


status_t
BSecureSocket::WaitForReadable(bigtime_t timeout) const
{
	if (fInitStatus != B_OK)
		return fInitStatus;
	if (!IsConnected())
		return B_ERROR;

	if (SSL_pending(fPrivate->fSSL) > 0)
		return B_OK;

	return BSocket::WaitForReadable(timeout);
}


status_t
BSecureSocket::InitCheck()
{
	if (fPrivate == NULL)
		return B_NO_MEMORY;

	status_t state = fPrivate->InitCheck();
	return state;
}


bool
BSecureSocket::CertificateVerificationFailed(BCertificate&, const char*)
{
	// Until apps actually make use of the certificate API, let's keep the old
	// behavior and accept all connections, even if the certificate validation
	// didn't work.
	return true;
}


//	#pragma mark - BDataIO implementation


ssize_t
BSecureSocket::Read(void* buffer, size_t size)
{
	if (!IsConnected())
		return B_ERROR;

	int bytesRead = SSL_read(fPrivate->fSSL, buffer, size);
	if (bytesRead >= 0)
		return bytesRead;

	return fPrivate->ErrorCode(bytesRead);
}


ssize_t
BSecureSocket::Write(const void* buffer, size_t size)
{
	if (!IsConnected())
		return B_ERROR;

	int bytesWritten = SSL_write(fPrivate->fSSL, buffer, size);
	if (bytesWritten >= 0)
		return bytesWritten;

	return fPrivate->ErrorCode(bytesWritten);
}


status_t
BSecureSocket::_SetupCommon(const char* host)
{
	// Do this only after BSocket::Connect has checked wether we're already
	// connected. We don't want to kill an existing SSL session, as that would
	// likely crash the protocol loop for it.
	if (fPrivate->fSSL != NULL) {
		SSL_free(fPrivate->fSSL);
	}

	fPrivate->fSSL = SSL_new(BSecureSocket::Private::Context());
	if (fPrivate->fSSL == NULL) {
		BSocket::Disconnect();
		return B_NO_MEMORY;
	}

	BIO_set_fd(fPrivate->fBIO, fSocket, BIO_NOCLOSE);
	SSL_set_bio(fPrivate->fSSL, fPrivate->fBIO, fPrivate->fBIO);
	SSL_set_ex_data(fPrivate->fSSL, Private::sDataIndex, this);
	if (host != NULL) {
		BString hostString = host;
		if (hostString != "")
			SSL_set_tlsext_host_name(fPrivate->fSSL, host);
	}


	return B_OK;
}


status_t
BSecureSocket::_SetupConnect(const char* host)
{
	status_t error = _SetupCommon(host);
	if (error != B_OK)
		return error;

	int returnValue = SSL_connect(fPrivate->fSSL);
	if (returnValue <= 0) {
		TRACE("SSLConnection can't connect\n");
		BSocket::Disconnect();
		return fPrivate->ErrorCode(returnValue);
	}

#ifdef TRACE_SESSION_KEY
	fprintf(stderr, "SSL SESSION INFO:\n");
	//SSL_SESSION_print_fp(stderr, SSL_get_session(fPrivate->fSSL));
	SSL_SESSION_print_keylog(fPrivate->sKeyLogBIO, SSL_get_session(fPrivate->fSSL));
	SSL_SESSION_print_client_random(fPrivate->sKeyLogBIO, fPrivate->fSSL);
	fprintf(stderr, "\n");
#endif

	return B_OK;
}


status_t
BSecureSocket::_SetupAccept()
{
	status_t error = _SetupCommon();
	if (error != B_OK)
		return error;

	int returnValue = SSL_accept(fPrivate->fSSL);
	if (returnValue <= 0) {
		TRACE("SSLConnection can't accept\n");
		BSocket::Disconnect();
		return fPrivate->ErrorCode(returnValue);
	}

	return B_OK;
}


#else	// OPENSSL_ENABLED


// #pragma mark - No-SSL stubs


BSecureSocket::BSecureSocket()
{
}


BSecureSocket::BSecureSocket(const BNetworkAddress& peer, bigtime_t timeout)
{
	fInitStatus = B_UNSUPPORTED;
}


BSecureSocket::BSecureSocket(const BSecureSocket& other)
	:
	BSocket(other)
{
}


BSecureSocket::~BSecureSocket()
{
}


bool
BSecureSocket::CertificateVerificationFailed(BCertificate& certificate, const char*)
{
	(void)certificate;
	return false;
}


status_t
BSecureSocket::Accept(BAbstractSocket*& _socket)
{
	return B_UNSUPPORTED;
}


status_t
BSecureSocket::Connect(const BNetworkAddress& peer, bigtime_t timeout)
{
	return fInitStatus = B_UNSUPPORTED;
}


void
BSecureSocket::Disconnect()
{
}


status_t
BSecureSocket::WaitForReadable(bigtime_t timeout) const
{
	return B_UNSUPPORTED;
}


//	#pragma mark - BDataIO implementation


ssize_t
BSecureSocket::Read(void* buffer, size_t size)
{
	return B_UNSUPPORTED;
}


ssize_t
BSecureSocket::Write(const void* buffer, size_t size)
{
	return B_UNSUPPORTED;
}


status_t
BSecureSocket::InitCheck()
{
	return B_UNSUPPORTED;
}


status_t
BSecureSocket::_SetupCommon(const char* host)
{
	return B_UNSUPPORTED;
}


status_t
BSecureSocket::_SetupConnect(const char* host)
{
	return B_UNSUPPORTED;
}


status_t
BSecureSocket::_SetupAccept()
{
	return B_UNSUPPORTED;
}


#endif	// !OPENSSL_ENABLED
