/*
 * Copyright (c) 2011, 2012, 2013, Jonathan Schleifer <js@webkeks.org>
 * Copyright (c) 2011, Florian Zeitz <florob@babelmonkeys.de>
 * Copyright (c) 2011, Jos Kuijpers <jos@kuijpersvof.nl>
 *
 * https://webkeks.org/git/?p=objopenssl.git
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice is present in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <unistd.h>
#include <errno.h>
#include <assert.h>

#include <openssl/crypto.h>
#include <openssl/err.h>

#import <ObjFW/OFThread.h>
#import <ObjFW/OFHTTPRequest.h>
#import <ObjFW/OFDataArray.h>

#import <ObjFW/OFAcceptFailedException.h>
#import <ObjFW/OFConnectionFailedException.h>
#import <ObjFW/OFInitializationFailedException.h>
#import <ObjFW/OFInvalidArgumentException.h>
#import <ObjFW/OFNotConnectedException.h>
#import <ObjFW/OFOutOfRangeException.h>
#import <ObjFW/OFReadFailedException.h>
#import <ObjFW/OFWriteFailedException.h>
#import <ObjFW/macros.h>
#import <ObjFW/threading.h>

#import "SSLSocket.h"
#import "SSLInvalidCertificateException.h"
#import "X509Certificate.h"

#ifndef INVALID_SOCKET
# define INVALID_SOCKET -1
#endif

static SSL_CTX *ctx;
static of_mutex_t *ssl_mutexes;

static unsigned long
get_thread_id(void)
{
	return (unsigned long)(uintptr_t)[OFThread currentThread];
}

static void
locking_callback(int mode, int n, const char *file, int line)
{
	/*
	 * This function must handle up to CRYPTO_num_locks() mutexes.
	 * It must set the n-th lock if mode & CRYPTO_LOCK,
	 * release it otherwise.
	 */
	if (mode & CRYPTO_LOCK)
		of_mutex_lock(&ssl_mutexes[n]);
	else
		of_mutex_unlock(&ssl_mutexes[n]);
}

@implementation SSLSocket
+ (void)load
{
	of_tls_socket_class = self;
}

+ (void)initialize
{
	int m;

	if (self != [SSLSocket class])
		return;

	CRYPTO_set_id_callback(&get_thread_id);

	/* Generate number of mutexes needed */
	m = CRYPTO_num_locks();
	ssl_mutexes = malloc(m * sizeof(of_mutex_t));
	for (m--; m >= 0; m--)
		of_mutex_new(&ssl_mutexes[m]);

	CRYPTO_set_locking_callback(&locking_callback);

	SSL_library_init();

	if ((ctx = SSL_CTX_new(SSLv23_method())) == NULL)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];

	if ((SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2) & SSL_OP_NO_SSLv2) == 0)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];

	if (SSL_CTX_set_default_verify_paths(ctx) == 0)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
}

- initWithSocket: (OFTCPSocket*)socket
{
	self = [self init];

	_socket = dup(socket->_socket);

	return self;
}

- (void)dealloc
{
	SSL *SSL_ = _SSL;

	[_privateKeyFile release];
	[_certificateFile release];

	[super dealloc];

	if (SSL_ != NULL)
		SSL_free(SSL_);
}

- (void)startTLS
{
	of_string_encoding_t encoding = [OFString nativeOSEncoding];
	if ((_SSL = SSL_new(ctx)) == NULL || !SSL_set_fd(_SSL, _socket)) {
		[super close];
		@throw [OFConnectionFailedException
		    exceptionWithHost: nil
				 port: 0
			       socket: self];
	}

	SSL_set_connect_state(_SSL);

	if ((_privateKeyFile != nil && !SSL_use_PrivateKey_file(_SSL,
	    [_privateKeyFile cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM)) || (_certificateFile != nil &&
	    !SSL_use_certificate_file(_SSL, [_certificateFile
	    cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM)) || SSL_connect(_SSL) != 1) {
		[super close];
		@throw [OFConnectionFailedException
		    exceptionWithHost: nil
				 port: 0
			       socket: self];
	}
}

- (void)connectToHost: (OFString*)host
		 port: (uint16_t)port
{
	[super connectToHost: host
			port: port];

	[self startTLS];
}

- (instancetype)accept
{
	of_string_encoding_t encoding = [OFString nativeOSEncoding];
	SSLSocket *client = (SSLSocket*)[super accept];

	if ((client->_SSL = SSL_new(ctx)) == NULL ||
	    !SSL_set_fd(client->_SSL, client->_socket)) {
		[client SSL_super_close];
		@throw [OFAcceptFailedException exceptionWithSocket: self];
	}

	if (_requestsClientCertificates)
		SSL_set_verify(client->_SSL, SSL_VERIFY_PEER, NULL);

	SSL_set_accept_state(client->_SSL);

	if (!SSL_use_PrivateKey_file(client->_SSL, [_privateKeyFile
	    cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM) || !SSL_use_certificate_file(client->_SSL,
	    [_certificateFile cStringWithEncoding: encoding],
	    SSL_FILETYPE_PEM) || SSL_accept(client->_SSL) != 1) {
		[client SSL_super_close];
		@throw [OFAcceptFailedException exceptionWithSocket: self];
	}

	return client;
}

- (void)close
{
	if (_SSL != NULL)
		SSL_shutdown(_SSL);

	[super close];
}

- (void)SSL_super_close
{
	[super close];
}

- (size_t)lowlevelReadIntoBuffer: (void*)buffer
			  length: (size_t)length
{
	ssize_t ret;

	if (length > INT_MAX)
		@throw [OFOutOfRangeException exception];

	if (_socket == INVALID_SOCKET)
		@throw [OFNotConnectedException exceptionWithSocket: self];

	if (_atEndOfStream) {
		OFReadFailedException *e;

		e = [OFReadFailedException exceptionWithStream: self
					       requestedLength: length];
#ifndef _WIN32
		e->_errNo = ENOTCONN;
#else
		e->_errNo = WSAENOTCONN;
#endif

		@throw e;
	}

	if ((ret = SSL_read(_SSL, buffer, (int)length)) < 0) {
		if (SSL_get_error(_SSL, ret) ==  SSL_ERROR_WANT_READ)
			return 0;

		@throw [OFReadFailedException exceptionWithStream: self
						  requestedLength: length];
	}

	if (ret == 0)
		_atEndOfStream = true;

	return ret;
}

- (void)lowlevelWriteBuffer: (const void*)buffer
		     length: (size_t)length
{
	if (length > INT_MAX)
		@throw [OFOutOfRangeException exception];

	if (_socket == INVALID_SOCKET)
		@throw [OFNotConnectedException exceptionWithSocket: self];

	if (_atEndOfStream) {
		OFWriteFailedException *e;

		e = [OFWriteFailedException exceptionWithStream: self
						requestedLength: length];

#ifndef _WIN32
		e->_errNo = ENOTCONN;
#else
		e->_errNo = WSAENOTCONN;
#endif

		@throw e;
	}

	if (SSL_write(_SSL, buffer, (int)length) < length)
		@throw [OFWriteFailedException exceptionWithStream: self
						   requestedLength: length];
}

- (size_t)numberOfBytesInReadBuffer
{
	if (_SSL == NULL)
		return [super numberOfBytesInReadBuffer];

	return [super numberOfBytesInReadBuffer] + SSL_pending(_SSL);
}

- (void)setDelegate: (id <OFTLSSocketDelegate>)delegate
{
	/* FIXME */
	[self doesNotRecognizeSelector: _cmd];
	abort();
}

- (id <OFTLSSocketDelegate>)delegate
{
	/* FIXME */
	return nil;
}

- (void)setCertificateFile: (OFString*)certificateFile
{
	OF_SETTER(_certificateFile, certificateFile, true, 1)
}

- (OFString*)certificateFile
{
	OF_GETTER(_certificateFile, true)
}

- (void)setPrivateKeyFile: (OFString*)privateKeyFile
{
	OF_SETTER(_privateKeyFile, privateKeyFile, true, 1)
}

- (OFString*)privateKeyFile
{
	OF_GETTER(_privateKeyFile, true)
}

- (void)setPrivateKeyPassphrase: (const char*)privateKeyPassphrase
{
	/* FIXME */
}

- (const char*)privateKeyPassphrase
{
	/* FIXME */
	return NULL;
}

- (void)setRequestsClientCertificates: (bool)enabled
{
	_requestsClientCertificates = enabled;
}

- (bool)requestsClientCertificates
{
	return _requestsClientCertificates;
}

- (OFDataArray*)channelBindingDataWithType: (OFString*)type
{
	size_t length;
	char buffer[64];
	OFDataArray *data;

	if (![type isEqual: @"tls-unique"])
		@throw [OFInvalidArgumentException exception];

	if (SSL_session_reused(_SSL) ^ !_listening) {
		/*
		 * We are either client or the session has been resumed
		 * => we have sent the finished message
		 */
		length = SSL_get_finished(_SSL, buffer, 64);
	} else {
		/* peer sent the finished message */
		length = SSL_get_peer_finished(_SSL, buffer, 64);
	}

	data = [OFDataArray dataArray];
	[data addItems: buffer
		 count: length];

	return data;
}

- (X509Certificate*)peerCertificate
{
	X509 *certificate = SSL_get_peer_certificate(_SSL);

	if (!certificate)
		return nil;

	return [[[X509Certificate alloc]
	    initWithX509Struct: certificate] autorelease];
}

- (void)verifyPeerCertificate
{
	unsigned long ret;

	if (SSL_get_peer_certificate(_SSL) != NULL) {
		if ((ret = SSL_get_verify_result(_SSL)) != X509_V_OK) {
			const char *tmp = X509_verify_cert_error_string(ret);
			OFString *reason = [OFString stringWithUTF8String: tmp];
			@throw [SSLInvalidCertificateException
			    exceptionWithReason: reason];
		}
	} else
		@throw [SSLInvalidCertificateException
		    exceptionWithReason: @"No certificate"];
}
@end
