
#include "openssl_filter.h"

#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/rand.h>


struct liOpenSSLFilter {
	int refcount;
	const liOpenSSLFilterCallbacks *callbacks;
	gpointer callback_data;

	liServer *srv;
	liWorker *wrk;
	liLogContext *log_context; /* TODO: support setting this to VR context */

	SSL *ssl;
	BIO *bio;
	liStream crypt_source;
	liStream crypt_drain;
	liStream plain_source;
	liStream plain_drain;

	liBuffer *raw_in_buffer; /* for SSL_read */

	unsigned int initial_handshaked_finished:1;
	unsigned int client_initiated_renegotiation:1;
	unsigned int closing:1, aborted:1;
	unsigned int write_wants_read:1;
};

#define BIO_TYPE_LI_STREAM (127|BIO_TYPE_SOURCE_SINK)

static int stream_bio_write(BIO *bio, const char *buf, int len) {
	liOpenSSLFilter *f = BIO_get_data(bio);
	liChunkQueue *cq;

	errno = ECONNRESET;

	if (NULL == f || NULL == f->crypt_source.out) return -1;
	cq = f->crypt_source.out;
	if (cq->is_closed) return -1;

	li_chunkqueue_append_mem(cq, buf, len);
	li_stream_notify_later(&f->crypt_source);

	errno = 0;
	return len;
}
static int stream_bio_read(BIO *bio, char *buf, int len) {
	liOpenSSLFilter *f = BIO_get_data(bio);
	liChunkQueue *cq;

	errno = ECONNRESET;
	BIO_clear_retry_flags(bio);

	if (NULL == f || NULL == f->crypt_drain.out) return -1;

	cq = f->crypt_drain.out;

	if (0 == cq->length) {
		if (cq->is_closed) {
			errno = 0;
			return 0;
		} else {
			errno = EAGAIN;
			BIO_set_retry_read(bio);
			return -1;
		}
	}

	if (len > cq->length) len = cq->length;
	if (!li_chunkqueue_extract_to_memory(cq, len, buf, NULL)) return -1;
	li_chunkqueue_skip(cq, len);

	errno = 0;
	return len;
}
static int stream_bio_puts(BIO *bio, const char *str) {
	return stream_bio_write(bio, str, strlen(str));
}
static int stream_bio_gets(BIO *bio, char *str, int len) {
	UNUSED(bio); UNUSED(str); UNUSED(len);
	return -1;
}
static long stream_bio_ctrl(BIO *bio, int cmd, long num, void *ptr) {
	liOpenSSLFilter *f = BIO_get_data(bio);
	UNUSED(num); UNUSED(ptr);

	switch (cmd) {
	case BIO_CTRL_FLUSH:
		return 1;
	case BIO_CTRL_PENDING:
		if (NULL == f || NULL == f->crypt_drain.out) return 0;
		return f->crypt_drain.out->length;
	default:
		return 0;
	}
}
static int stream_bio_destroy(BIO *bio) {
	liOpenSSLFilter *f = BIO_get_data(bio);
	BIO_set_data(bio, NULL);
	if (NULL != f) f->bio = NULL;
	BIO_set_init(bio, 0);
	return 1;
}

static BIO_METHOD* get_cq_bio_method(void) {
	static gint once = 0;
	static BIO_METHOD *m = NULL;

	/* initialize once */
	if (g_atomic_int_compare_and_exchange(&once, 0, 1)) {
		m = BIO_meth_new(BIO_TYPE_LI_STREAM, "lighttpd stream glue");
		LI_FORCE_ASSERT(NULL != m);

		BIO_meth_set_write(m, stream_bio_write);
		BIO_meth_set_read(m, stream_bio_read);
		BIO_meth_set_puts(m, stream_bio_puts);
		BIO_meth_set_gets(m, stream_bio_gets);
		BIO_meth_set_ctrl(m, stream_bio_ctrl);
		/* BIO_meth_set_create(m, stream_bio_create); */
		BIO_meth_set_destroy(m, stream_bio_destroy);

		g_atomic_int_set(&once, 2);
	} else {
		while (g_atomic_int_get(&once) != 2) { }
	}

	return m;
}

static BIO* new_cq_bio(liOpenSSLFilter *f) {
	BIO *bio;
	BIO_METHOD *m = get_cq_bio_method();

	if (NULL == m) return NULL;

	bio = BIO_new(m);
	BIO_set_data(bio, f);
	BIO_set_init(bio, 1);
	return bio;
}

static void f_close_ssl(liOpenSSLFilter *f) {
	if (NULL != f->ssl && !f->closing) {
		SSL *ssl;
		liCQLimit *limit;

		f->closing = TRUE;

		LI_FORCE_ASSERT(NULL != f->crypt_source.out);
		LI_FORCE_ASSERT(NULL != f->crypt_source.out->limit);
		limit = f->crypt_source.out->limit;
		limit->notify = NULL;
		limit->context = NULL;

		li_stream_disconnect(&f->plain_source); /* crypt in -> plain out */

		li_stream_disconnect(&f->plain_drain); /* app -> plain in */
		li_stream_disconnect_dest(&f->plain_source); /* plain out -> app */

		f->log_context = NULL;
		if (NULL != f->callbacks && NULL != f->callbacks->closed_cb) {
			f->callbacks->closed_cb(f, f->callback_data);
		}
		ssl = f->ssl;
		f->ssl = NULL;
		if (NULL != ssl) SSL_free(ssl);
	}
}
static void f_acquire(liOpenSSLFilter *f) {
	LI_FORCE_ASSERT(f->refcount > 0);
	++f->refcount;
}
static void f_release(liOpenSSLFilter *f) {
	LI_FORCE_ASSERT(f->refcount > 0);
	if (0 == --f->refcount) {
		f->refcount = 1;
		f_close_ssl(f);
		if (NULL != f->bio) {
			BIO_free(f->bio);
			f->bio = NULL;
		}
		if (NULL != f->raw_in_buffer) {
			li_buffer_release(f->raw_in_buffer);
			f->raw_in_buffer = NULL;
		}

		g_slice_free(liOpenSSLFilter, f);
	}
}
static void f_abort_ssl(liOpenSSLFilter *f) {
	if (f->aborted) return;
	f->aborted = TRUE;
	f_acquire(f);
	f_close_ssl(f);
	li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */
	li_stream_disconnect(&f->crypt_drain); /* io -> crypt in */
	li_stream_disconnect_dest(&f->crypt_source); /* crypt out -> io */
	f_release(f);
}

static void do_handle_error(liOpenSSLFilter *f, const char *sslfunc, int r, gboolean writing) {
	int oerrno = errno, err;
	gboolean was_fatal;

	err = SSL_get_error(f->ssl, r);

	switch (err) {
	case SSL_ERROR_WANT_READ:
		if (writing) f->write_wants_read = TRUE;
		break;
	/*
	case SSL_ERROR_WANT_WRITE:
		we buffer all writes, can't happen! - handle as fatal error below
		break;
	*/
	case SSL_ERROR_SYSCALL:
		/**
			* man SSL_get_error()
			*
			* SSL_ERROR_SYSCALL
			*   Some I/O error occurred.  The OpenSSL error queue may contain more
			*   information on the error.  If the error queue is empty (i.e.
			*   ERR_get_error() returns 0), ret can be used to find out more about
			*   the error: If ret == 0, an EOF was observed that violates the
			*   protocol.  If ret == -1, the underlying BIO reported an I/O error
			*   (for socket I/O on Unix systems, consult errno for details).
			*
			*/
		while (0 != (err = ERR_get_error())) {
			_ERROR(f->srv, f->wrk, f->log_context, "%s: %s", sslfunc,
				ERR_error_string(err, NULL));
		}

		if (0 != r || (0 != oerrno && ECONNRESET != oerrno)) {
			_ERROR(f->srv, f->wrk, f->log_context, "%s returned %i: %s", sslfunc,
				r,
				g_strerror(oerrno));
		}
		f_abort_ssl(f);

		break;
	case SSL_ERROR_ZERO_RETURN:
		/* clean shutdown on the remote side */
		f->plain_source.out->is_closed = TRUE;
		li_stream_notify(&f->plain_source);
		li_stream_disconnect(&f->crypt_drain);
		li_stream_disconnect_dest(&f->crypt_source);
		break;
	default:
		was_fatal = FALSE;

		/* get all errors from the error-queue */
		while((err = ERR_get_error())) {
			switch (ERR_GET_REASON(err)) {
			case SSL_R_SSL_HANDSHAKE_FAILURE:
			case SSL_R_TLSV1_ALERT_UNKNOWN_CA:
			case SSL_R_SSLV3_ALERT_CERTIFICATE_UNKNOWN:
			case SSL_R_SSLV3_ALERT_BAD_CERTIFICATE:
			case SSL_R_NO_SHARED_CIPHER:
			case SSL_R_UNKNOWN_PROTOCOL:
				_DEBUG(f->srv, f->wrk, f->log_context, "%s: %s", sslfunc,
					ERR_error_string(err, NULL));
				continue;
			default:
				was_fatal = TRUE;
				_ERROR(f->srv, f->wrk, f->log_context, "%s: %s", sslfunc,
					ERR_error_string(err, NULL));
				break;
			}
		}
		if (was_fatal) f_abort_ssl(f);
	}

}

static gboolean do_ssl_handshake(liOpenSSLFilter *f, gboolean writing) {
	int r = SSL_do_handshake(f->ssl);
	if (1 == r) {
		f->initial_handshaked_finished = 1;
		li_stream_acquire(&f->plain_source);
		li_stream_acquire(&f->plain_drain);
		f->callbacks->handshake_cb(f, f->callback_data, &f->plain_source, &f->plain_drain);
		li_stream_release(&f->plain_source);
		li_stream_release(&f->plain_drain);
		return TRUE;
	} else {
		do_handle_error(f, "SSL_do_handshake", r, writing);
		return FALSE;
	}
}

static void do_ssl_read(liOpenSSLFilter *f) {
	const ssize_t blocksize = 16*1024; /* 16k */
	off_t max_read = 4 * blocksize; /* 64k */
	ssize_t r;
	off_t len = 0;
	liChunkQueue *cq = f->plain_source.out;

	f_acquire(f);

	if (NULL != f->ssl && !f->initial_handshaked_finished && !do_ssl_handshake(f, FALSE)) goto out;
	if (NULL == f->ssl) {
		f_abort_ssl(f);
		goto out;
	}

	do {
		liBuffer *buf;
		gboolean cq_buf_append;

		ERR_clear_error();

		buf = li_chunkqueue_get_last_buffer(cq, 1024);
		cq_buf_append = (buf != NULL);

		if (buf != NULL) {
			/* use last buffer as raw_in_buffer; they should be the same anyway */
			if (HEDLEY_UNLIKELY(buf != f->raw_in_buffer)) {
				li_buffer_acquire(buf);
				li_buffer_release(f->raw_in_buffer);
				f->raw_in_buffer = buf;
			}
		} else {
			buf = f->raw_in_buffer;
			if (buf != NULL && buf->alloc_size - buf->used < 1024) {
				/* release *buffer */
				li_buffer_release(buf);
				f->raw_in_buffer = buf = NULL;
			}
			if (buf == NULL) {
				f->raw_in_buffer = buf = li_buffer_new(blocksize);
			}
		}
		LI_FORCE_ASSERT(f->raw_in_buffer == buf);

		r = SSL_read(f->ssl, buf->addr + buf->used, buf->alloc_size - buf->used);
		if (f->client_initiated_renegotiation) {
			_ERROR(f->srv, f->wrk, f->log_context, "%s", "SSL: client initiated renegotiation, closing connection");
			f_abort_ssl(f);
			goto out;
		}
		if (r < 0) {
			do_handle_error(f, "SSL_read", r, FALSE);
			goto out;
		} else if (r == 0) {
			/* clean shutdown? */
			r = SSL_shutdown(f->ssl);
			switch (r) {
			case 0: /* don't care about bidirectional shutdown */
			case 1: /* bidirectional shutdown finished */
				f->plain_source.out->is_closed = TRUE;
				f->plain_drain.out->is_closed = TRUE;
				f->crypt_source.out->is_closed = TRUE;
				f->crypt_drain.out->is_closed = TRUE;
				li_stream_disconnect(&f->crypt_drain); /* io -> crypt in */
				li_stream_disconnect_dest(&f->crypt_source); /* crypt out -> io */
				li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */
				f_close_ssl(f);
				break;
			default:
				do_handle_error(f, "SSL_shutdown", r, TRUE);
				f_abort_ssl(f);
				break;
			}
			goto out;
		}

		if (cq_buf_append) {
			li_chunkqueue_update_last_buffer_size(cq, r);
		} else {
			gsize offset;

			li_buffer_acquire(buf);

			offset = buf->used;
			buf->used += r;
			li_chunkqueue_append_buffer2(cq, buf, offset, r);
		}
		if (buf->alloc_size - buf->used < 1024) {
			/* release *buffer */
			li_buffer_release(buf);
			f->raw_in_buffer = buf = NULL;
		}
		len += r;
	} while (len < max_read);

out:
	f_release(f);
}

static void do_ssl_write(liOpenSSLFilter *f) {
	const ssize_t blocksize = 16*1024; /* 16k */
	char *block_data;
	off_t block_len;
	ssize_t r;
	off_t write_max = 64*1024;
	liChunkQueue *cq = f->plain_drain.out;

	f_acquire(f);

	f->write_wants_read = FALSE;

	/* use space in (encrypted) outgoing buffer as amounts of bytes we try to write from (plain) output
	 * don't care if we write a little bit more than the limit allowed */
	write_max = li_chunkqueue_limit_available(f->crypt_source.out);
	LI_FORCE_ASSERT(write_max >= 0); /* we set a limit! */
	/* if we start writing, try to write at least blocksize bytes */
	if (write_max < blocksize) write_max = blocksize;

	if (NULL != f->ssl && !f->initial_handshaked_finished && !do_ssl_handshake(f, TRUE)) goto out;
	if (NULL == f->ssl) {
		f_abort_ssl(f);
		goto out;
	}

	do {
		GError *err = NULL;
		liChunkIter ci;

		if (0 == cq->length) break;

		ci = li_chunkqueue_iter(cq);
		switch (li_chunkiter_read(ci, 0, blocksize, &block_data, &block_len, &err)) {
		case LI_HANDLER_GO_ON:
			break;
		case LI_HANDLER_ERROR:
			if (NULL != err) {
				_ERROR(f->srv, f->wrk, f->log_context, "Couldn't read data from chunkqueue: %s", err->message);
				g_error_free(err);
			}
			HEDLEY_FALL_THROUGH;
		default:
			f_abort_ssl(f);
			goto out;
		}

		/**
		 * SSL_write man-page
		 *
		 * WARNING
		 *        When an SSL_write() operation has to be repeated because of
		 *        SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE, it must be
		 *        repeated with the same arguments.
		 *
		 */

		ERR_clear_error();
		r = SSL_write(f->ssl, block_data, block_len);
		if (f->client_initiated_renegotiation) {
			_ERROR(f->srv, f->wrk, f->log_context, "%s", "SSL: client initiated renegotiation, closing connection");
			f_abort_ssl(f);
			goto out;
		}
		if (r <= 0) {
			do_handle_error(f, "SSL_write", r, TRUE);
			goto out;
		}

		li_chunkqueue_skip(cq, r);
		write_max -= r;
	} while (r == block_len && write_max > 0);

	if (cq->is_closed && 0 == cq->length) {
		r = SSL_shutdown(f->ssl);
		switch (r) {
		case 0: /* don't care about bidirectional shutdown */
		case 1: /* bidirectional shutdown finished */
			f->plain_source.out->is_closed = TRUE;
			f->crypt_source.out->is_closed = TRUE;
			f->crypt_drain.out->is_closed = TRUE;
			li_stream_disconnect(&f->crypt_source); /* plain in -> crypt out */
			f_close_ssl(f);
			break;
		default:
			do_handle_error(f, "SSL_shutdown", r, TRUE);
			f_abort_ssl(f);
			break;
		}
	} else if (0 < cq->length && 0 != li_chunkqueue_limit_available(f->crypt_source.out)) {
		li_stream_again_later(&f->plain_drain);
	}

out:
	f_release(f);
}

/* ssl crypted out -> io */
static void stream_crypt_source_cb(liStream *stream, liStreamEvent event) {
	liOpenSSLFilter *f = LI_CONTAINER_OF(stream, liOpenSSLFilter, crypt_source);
	switch (event) {
	case LI_STREAM_NEW_DATA:
		/* data comes through SSL */
		break;
	case LI_STREAM_NEW_CQLIMIT:
		break;
	case LI_STREAM_CONNECTED_DEST: /* io out */
		break;
	case LI_STREAM_CONNECTED_SOURCE: /* plain_drain */
		break;
	case LI_STREAM_DISCONNECTED_DEST: /* io out disconnect */
		if (!stream->out->is_closed || 0 != stream->out->length) {
			f_abort_ssl(f); /* didn't read everything */
		}
		break;
	case LI_STREAM_DISCONNECTED_SOURCE: /* plain_drain */
		if (!stream->out->is_closed) { /* f_close_ssl before we were ready */
			f_abort_ssl(f);
		}
		break;
	case LI_STREAM_DESTROY:
		f_release(f);
		break;
	}
}
/* io -> ssl crypted in */
static void stream_crypt_drain_cb(liStream *stream, liStreamEvent event) {
	liOpenSSLFilter *f = LI_CONTAINER_OF(stream, liOpenSSLFilter, crypt_drain);
	switch (event) {
	case LI_STREAM_NEW_DATA:
		if (!stream->out->is_closed && NULL != stream->source) {
			li_chunkqueue_steal_all(stream->out, stream->source->out);
			stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed;
			li_stream_notify(stream); /* tell plain_source to do SSL_read */
		}
		if (stream->out->is_closed) {
			li_stream_disconnect(stream);
		}
		break;
	case LI_STREAM_NEW_CQLIMIT:
		break;
	case LI_STREAM_CONNECTED_DEST: /* plain_source */
		break;
	case LI_STREAM_CONNECTED_SOURCE: /* io in */
		break;
	case LI_STREAM_DISCONNECTED_DEST: /* plain_source */
		if (!stream->out->is_closed || 0 != stream->out->length) {
			f_abort_ssl(f); /* didn't read everything */
		}
		break;
	case LI_STREAM_DISCONNECTED_SOURCE: /* io in disconnect */
		if (!stream->out->is_closed) {
			f_abort_ssl(f); /* conn aborted */
		}
		break;
	case LI_STREAM_DESTROY:
		f_release(f);
		break;
	}
}
/* ssl (plain) -> app */
static void stream_plain_source_cb(liStream *stream, liStreamEvent event) {
	liOpenSSLFilter *f = LI_CONTAINER_OF(stream, liOpenSSLFilter, plain_source);
	switch (event) {
	case LI_STREAM_NEW_DATA:
		do_ssl_read(f);
		if (f->write_wants_read) do_ssl_write(f);
		li_stream_notify(stream);
		break;
	case LI_STREAM_NEW_CQLIMIT:
		break;
	case LI_STREAM_CONNECTED_DEST: /* app */
		break;
	case LI_STREAM_CONNECTED_SOURCE: /* crypt_drain */
		break;
	case LI_STREAM_DISCONNECTED_DEST: /* app */
		if (!stream->out->is_closed || 0 != stream->out->length) {
			f_abort_ssl(f); /* didn't read everything */
		}
		break;
	case LI_STREAM_DISCONNECTED_SOURCE: /* crypt_drain */
		if (!stream->out->is_closed) {
			f_abort_ssl(f); /* didn't get everything */
		}
		break;
	case LI_STREAM_DESTROY:
		f_release(f);
		break;
	}
}
/* app -> ssl (plain) */
static void stream_plain_drain_cb(liStream *stream, liStreamEvent event) {
	liOpenSSLFilter *f = LI_CONTAINER_OF(stream, liOpenSSLFilter, plain_drain);
	switch (event) {
	case LI_STREAM_NEW_DATA:
		if (!stream->out->is_closed && NULL != stream->source) {
			li_chunkqueue_steal_all(stream->out, stream->source->out);
			stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed;
		}
		do_ssl_write(f);
		if (stream->out->is_closed) {
			li_stream_disconnect(stream);
		}
		break;
	case LI_STREAM_NEW_CQLIMIT:
		break;
	case LI_STREAM_CONNECTED_DEST: /* crypt_source */
		break;
	case LI_STREAM_CONNECTED_SOURCE: /* app */
		break;
	case LI_STREAM_DISCONNECTED_DEST:
		if (!stream->out->is_closed || 0 != stream->out->length) {
			f_abort_ssl(f); /* didn't read everything */
		}
		break;
	case LI_STREAM_DISCONNECTED_SOURCE:
		if (!stream->out->is_closed) {
			f_abort_ssl(f); /* didn't get everything */
		}
		break;
	case LI_STREAM_DESTROY:
		f_release(f);
		break;
	}
}
static void stream_crypt_source_limit_notify_cb(gpointer context, gboolean locked) {
	liOpenSSLFilter *f = context;
	if (!locked && !f->closing) li_stream_again_later(&f->plain_drain);
}

static void openssl_info_callback(const SSL *ssl, int where, int ret) {
	UNUSED(ret);

	if (0 != (where & SSL_CB_HANDSHAKE_START)) {
		liOpenSSLFilter *f = SSL_get_app_data(ssl);
		if (f->initial_handshaked_finished) {
			f->client_initiated_renegotiation = TRUE;
		}
	}
}

liOpenSSLFilter* li_openssl_filter_new(
	liServer *srv, liWorker *wrk,
	const liOpenSSLFilterCallbacks *callbacks, gpointer data,
	SSL_CTX *ssl_ctx, liStream *crypt_source, liStream *crypt_drain
) {
	liEventLoop *loop = crypt_source->loop;
	liOpenSSLFilter *f;
	SSL *ssl;
	liCQLimit *out_limit;

	ssl = SSL_new(ssl_ctx);
	if (NULL == ssl) return NULL;

	f = g_slice_new0(liOpenSSLFilter);
	f->refcount = 5; /* 1 + 4 streams */
	f->callbacks = callbacks;
	f->callback_data = data;
	f->srv = srv;
	f->wrk = wrk;

	f->ssl = ssl;
	SSL_set_app_data(f->ssl, f);
	SSL_set_info_callback(f->ssl, openssl_info_callback);
	f->bio = new_cq_bio(f);
	SSL_set_bio(f->ssl, f->bio, f->bio);
	/* BIO_set_callback(f->bio, BIO_debug_callback); */

	f->initial_handshaked_finished = 0;
	f->client_initiated_renegotiation = 0;
	f->closing = f->aborted = 0;
	f->write_wants_read = 0;

	li_stream_init(&f->crypt_source, loop, stream_crypt_source_cb);
	li_stream_init(&f->crypt_drain, loop, stream_crypt_drain_cb);
	li_stream_init(&f->plain_source, loop, stream_plain_source_cb);
	li_stream_init(&f->plain_drain, loop, stream_plain_drain_cb);

	/* "virtual" connections - the content goes through SSL. needed for liCQLimit. */
	li_stream_connect(&f->plain_drain, &f->crypt_source);
	li_stream_connect(&f->crypt_drain, &f->plain_source);

	li_stream_connect(crypt_source, &f->crypt_drain);
	li_stream_connect(&f->crypt_source, crypt_drain);

	/* separate limit for buffer of encrypted data
	 *
	 * f->plain_drain is already connected to f->crypt_source,
	 *   so they won't share the same limit */
	out_limit = li_cqlimit_new();
	out_limit->notify = stream_crypt_source_limit_notify_cb;
	out_limit->context = f;
	li_cqlimit_set_limit(out_limit, 32*1024);
	li_chunkqueue_set_limit(crypt_drain->out, out_limit);
	li_chunkqueue_set_limit(f->crypt_source.out, out_limit);
	li_cqlimit_release(out_limit);

	SSL_set_accept_state(f->ssl);

	return f;
}

void li_openssl_filter_free(liOpenSSLFilter *f) {
	LI_FORCE_ASSERT(NULL != f->callbacks);
	f->callbacks = NULL;
	f->callback_data = NULL;

	f_close_ssl(f);

	li_stream_release(&f->crypt_source);
	li_stream_release(&f->crypt_drain);
	li_stream_release(&f->plain_source);
	li_stream_release(&f->plain_drain);
	f_release(f);
}

SSL* li_openssl_filter_ssl(liOpenSSLFilter *f) {
	return f->ssl;
}
