/*
 * Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be 
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>

#define SOCKET           int
#define INVALID_SOCKET   (-1)
#endif

#include "brssl.h"

static void
dump_blob(const char *name, const void *data, size_t len)
{
	const unsigned char *buf;
	size_t u;

	buf = data;
	fprintf(stderr, "%s (len = %lu)", name, (unsigned long)len);
	for (u = 0; u < len; u ++) {
		if ((u & 15) == 0) {
			fprintf(stderr, "\n%08lX  ", (unsigned long)u);
		} else if ((u & 7) == 0) {
			fprintf(stderr, " ");
		}
		fprintf(stderr, " %02x", buf[u]);
	}
	fprintf(stderr, "\n");
}

/*
 * Inspect the provided data in case it is a "command" to trigger a
 * special behaviour. If the command is recognised, then it is executed
 * and this function returns 1. Otherwise, this function returns 0.
 */
static int
run_command(br_ssl_engine_context *cc, unsigned char *buf, size_t len)
{
	/*
	 * A single static slot for saving session parameters.
	 */
	static br_ssl_session_parameters slot;
	static int slot_used = 0;

	size_t u;

	if (len < 2 || len > 3) {
		return 0;
	}
	if (len == 3 && (buf[1] != '\r' || buf[2] != '\n')) {
		return 0;
	}
	if (len == 2 && buf[1] != '\n') {
		return 0;
	}
	switch (buf[0]) {
	case 'Q':
		fprintf(stderr, "closing...\n");
		br_ssl_engine_close(cc);
		return 1;
	case 'R':
		if (br_ssl_engine_renegotiate(cc)) {
			fprintf(stderr, "renegotiating...\n");
		} else {
			fprintf(stderr, "not renegotiating.\n");
		}
		return 1;
	case 'F':
		/*
		 * Session forget is nominally client-only. But the
		 * session parameters are in the engine structure, which
		 * is the first field of the client context, so the cast
		 * still works properly. On the server, this forgetting
		 * has no effect.
		 */
		fprintf(stderr, "forgetting session...\n");
		br_ssl_client_forget_session((br_ssl_client_context *)cc);
		return 1;
	case 'S':
		fprintf(stderr, "saving session parameters...\n");
		br_ssl_engine_get_session_parameters(cc, &slot);
		fprintf(stderr, "  id = ");
		for (u = 0; u < slot.session_id_len; u ++) {
			fprintf(stderr, "%02X", slot.session_id[u]);
		}
		fprintf(stderr, "\n");
		slot_used = 1;
		return 1;
	case 'P':
		if (slot_used) {
			fprintf(stderr, "restoring session parameters...\n");
			fprintf(stderr, "  id = ");
			for (u = 0; u < slot.session_id_len; u ++) {
				fprintf(stderr, "%02X", slot.session_id[u]);
			}
			fprintf(stderr, "\n");
			br_ssl_engine_set_session_parameters(cc, &slot);
			return 1;
		}
		return 0;
	default:
		return 0;
	}
}

#ifdef _WIN32

typedef struct {
	unsigned char buf[1024];
	size_t ptr, len;
} in_buffer;

static int
in_return_bytes(in_buffer *bb, unsigned char *buf, size_t len)
{
	if (bb->ptr < bb->len) {
		size_t clen;

		if (buf == NULL) {
			return 1;
		}
		clen = bb->len - bb->ptr;
		if (clen > len) {
			clen = len;
		}
		memcpy(buf, bb->buf + bb->ptr, clen);
		bb->ptr += clen;
		if (bb->ptr == bb->len) {
			bb->ptr = bb->len = 0;
		}
		return (int)clen;
	}
	return 0;
}

/*
 * A buffered version of in_read(), using a buffer to return only
 * full lines when feasible.
 */
static int
in_read_buffered(HANDLE h_in, in_buffer *bb, unsigned char *buf, size_t len)
{
	int n;

	if (len == 0) {
		return 0;
	}
	n = in_return_bytes(bb, buf, len);
	if (n != 0) {
		return n;
	}
	for (;;) {
		INPUT_RECORD inrec;
		DWORD v;

		if (!PeekConsoleInput(h_in, &inrec, 1, &v)) {
			fprintf(stderr, "ERROR: PeekConsoleInput()"
				" failed with 0x%08lX\n",
				(unsigned long)GetLastError());
			return -1;
		}
		if (v == 0) {
			return 0;
		}
		if (!ReadConsoleInput(h_in, &inrec, 1, &v)) {
			fprintf(stderr, "ERROR: ReadConsoleInput()"
				" failed with 0x%08lX\n",
				(unsigned long)GetLastError());
			return -1;
		}
		if (v == 0) {
			return 0;
		}
		if (inrec.EventType == KEY_EVENT
			&& inrec.Event.KeyEvent.bKeyDown)
		{
			int c;

			c = inrec.Event.KeyEvent.uChar.AsciiChar;
			if (c == '\n' || c == '\r' || c == '\t'
				|| (c >= 32 && c != 127))
			{
				if (c == '\r') {
					c = '\n';
				}
				bb->buf[bb->ptr ++] = (unsigned char)c;
				printf("%c", c);
				fflush(stdout);
				bb->len = bb->ptr;
				if (bb->len == sizeof bb->buf || c == '\n') {
					bb->ptr = 0;
					return in_return_bytes(bb, buf, len);
				}
			}
		}
	}
}

static int
in_avail_buffered(HANDLE h_in, in_buffer *bb)
{
	return in_read_buffered(h_in, bb, NULL, 1);
}

#endif

/* see brssl.h */
int
run_ssl_engine(br_ssl_engine_context *cc, unsigned long fd, unsigned flags)
{
	int hsdetails;
	int retcode;
	int verbose;
	int trace;
#ifdef _WIN32
	WSAEVENT fd_event;
	int can_send, can_recv;
	HANDLE h_in, h_out;
	in_buffer bb;
#endif

	hsdetails = 0;
	retcode = 0;
	verbose = (flags & RUN_ENGINE_VERBOSE) != 0;
	trace = (flags & RUN_ENGINE_TRACE) != 0;

	/*
	 * Print algorithm details.
	 */
	if (verbose) {
		const char *rngname;

		fprintf(stderr, "Algorithms:\n");
		br_prng_seeder_system(&rngname);
		fprintf(stderr, "   RNG:           %s\n", rngname);
		if (cc->iaes_cbcenc != 0) {
			fprintf(stderr, "   AES/CBC (enc): %s\n",
				get_algo_name(cc->iaes_cbcenc, 0));
		}
		if (cc->iaes_cbcdec != 0) {
			fprintf(stderr, "   AES/CBC (dec): %s\n",
				get_algo_name(cc->iaes_cbcdec, 0));
		}
		if (cc->iaes_ctr != 0) {
			fprintf(stderr, "   AES/CTR:       %s\n",
				get_algo_name(cc->iaes_ctr, 0));
		}
		if (cc->iaes_ctrcbc != 0) {
			fprintf(stderr, "   AES/CCM:       %s\n",
				get_algo_name(cc->iaes_ctrcbc, 0));
		}
		if (cc->ides_cbcenc != 0) {
			fprintf(stderr, "   DES/CBC (enc): %s\n",
				get_algo_name(cc->ides_cbcenc, 0));
		}
		if (cc->ides_cbcdec != 0) {
			fprintf(stderr, "   DES/CBC (dec): %s\n",
				get_algo_name(cc->ides_cbcdec, 0));
		}
		if (cc->ighash != 0) {
			fprintf(stderr, "   GHASH (GCM):   %s\n",
				get_algo_name(cc->ighash, 0));
		}
		if (cc->ichacha != 0) {
			fprintf(stderr, "   ChaCha20:      %s\n",
				get_algo_name(cc->ichacha, 0));
		}
		if (cc->ipoly != 0) {
			fprintf(stderr, "   Poly1305:      %s\n",
				get_algo_name(cc->ipoly, 0));
		}
		if (cc->iec != 0) {
			fprintf(stderr, "   EC:            %s\n",
				get_algo_name(cc->iec, 0));
		}
		if (cc->iecdsa != 0) {
			fprintf(stderr, "   ECDSA:         %s\n",
				get_algo_name(cc->iecdsa, 0));
		}
		if (cc->irsavrfy != 0) {
			fprintf(stderr, "   RSA (vrfy):    %s\n",
				get_algo_name(cc->irsavrfy, 0));
		}
	}

#ifdef _WIN32
	fd_event = WSA_INVALID_EVENT;
	can_send = 0;
	can_recv = 0;
	bb.ptr = bb.len = 0;
#endif

	/*
	 * On Unix systems, we need to follow three descriptors:
	 * standard input (0), standard output (1), and the socket
	 * itself (for both read and write). This is done with a poll()
	 * call.
	 *
	 * On Windows systems, we use WSAEventSelect() to associate
	 * an event handle with the network activity, and we use
	 * WaitForMultipleObjectsEx() on that handle and the standard
	 * input handle, when appropriate. Standard output is assumed
	 * to be always writeable, and standard input to be the console;
	 * this does not work well (or at all) with redirections (to
	 * pipes or files) but it should be enough for a debug tool
	 * (TODO: make something that handles redirections as well).
	 */

#ifdef _WIN32
	fd_event = WSACreateEvent();
	if (fd_event == WSA_INVALID_EVENT) {
		fprintf(stderr, "ERROR: WSACreateEvent() failed with %d\n",
			WSAGetLastError());
		retcode = -2;
		goto engine_exit;
	}
	WSAEventSelect(fd, fd_event, FD_READ | FD_WRITE | FD_CLOSE);
	h_in = GetStdHandle(STD_INPUT_HANDLE);
	h_out = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleMode(h_in, ENABLE_ECHO_INPUT
		| ENABLE_LINE_INPUT
		| ENABLE_PROCESSED_INPUT
		| ENABLE_PROCESSED_OUTPUT
		| ENABLE_WRAP_AT_EOL_OUTPUT);
#else
	/*
	 * Make sure that stdin and stdout are non-blocking.
	 */
	fcntl(0, F_SETFL, O_NONBLOCK);
	fcntl(1, F_SETFL, O_NONBLOCK);
#endif

	/*
	 * Perform the loop.
	 */
	for (;;) {
		unsigned st;
		int sendrec, recvrec, sendapp, recvapp;
#ifdef _WIN32
		HANDLE pfd[2];
		DWORD wt;
#else
		struct pollfd pfd[3];
		int n;
#endif
		size_t u, k_fd, k_in, k_out;
		int sendrec_ok, recvrec_ok, sendapp_ok, recvapp_ok;

		/*
		 * Get current engine state.
		 */
		st = br_ssl_engine_current_state(cc);
		if (st == BR_SSL_CLOSED) {
			int err;

			err = br_ssl_engine_last_error(cc);
			if (err == BR_ERR_OK) {
				if (verbose) {
					fprintf(stderr,
						"SSL closed normally\n");
				}
				retcode = 0;
				goto engine_exit;
			} else {
				fprintf(stderr, "ERROR: SSL error %d", err);
				retcode = err;
				if (err >= BR_ERR_SEND_FATAL_ALERT) {
					err -= BR_ERR_SEND_FATAL_ALERT;
					fprintf(stderr,
						" (sent alert %d)\n", err);
				} else if (err >= BR_ERR_RECV_FATAL_ALERT) {
					err -= BR_ERR_RECV_FATAL_ALERT;
					fprintf(stderr,
						" (received alert %d)\n", err);
				} else {
					const char *ename;

					ename = find_error_name(err, NULL);
					if (ename == NULL) {
						ename = "unknown";
					}
					fprintf(stderr, " (%s)\n", ename);
				}
				goto engine_exit;
			}
		}

		/*
		 * Compute descriptors that must be polled, depending
		 * on engine state.
		 */
		sendrec = ((st & BR_SSL_SENDREC) != 0);
		recvrec = ((st & BR_SSL_RECVREC) != 0);
		sendapp = ((st & BR_SSL_SENDAPP) != 0);
		recvapp = ((st & BR_SSL_RECVAPP) != 0);
		if (verbose && sendapp && !hsdetails) {
			char csn[80];
			const char *pname;

			fprintf(stderr, "Handshake completed\n");
			fprintf(stderr, "   version:               ");
			switch (cc->session.version) {
			case BR_SSL30:
				fprintf(stderr, "SSL 3.0");
				break;
			case BR_TLS10:
				fprintf(stderr, "TLS 1.0");
				break;
			case BR_TLS11:
				fprintf(stderr, "TLS 1.1");
				break;
			case BR_TLS12:
				fprintf(stderr, "TLS 1.2");
				break;
			default:
				fprintf(stderr, "unknown (0x%04X)",
					(unsigned)cc->session.version);
				break;
			}
			fprintf(stderr, "\n");
			get_suite_name_ext(
				cc->session.cipher_suite, csn, sizeof csn);
			fprintf(stderr, "   cipher suite:          %s\n", csn);
			if (uses_ecdhe(cc->session.cipher_suite)) {
				get_curve_name_ext(
					br_ssl_engine_get_ecdhe_curve(cc),
					csn, sizeof csn);
				fprintf(stderr,
					"   ECDHE curve:           %s\n", csn);
			}
			fprintf(stderr, "   secure renegotiation:  %s\n",
				cc->reneg == 1 ? "no" : "yes");
			pname = br_ssl_engine_get_selected_protocol(cc);
			if (pname != NULL) {
				fprintf(stderr,
					"   protocol name (ALPN):  %s\n",
					pname);
			}
			hsdetails = 1;
		}

		k_fd = (size_t)-1;
		k_in = (size_t)-1;
		k_out = (size_t)-1;

		u = 0;
#ifdef _WIN32
		/*
		 * If we recorded that we can send or receive data, and we
		 * want to do exactly that, then we don't wait; we just do
		 * it.
		 */
		recvapp_ok = 0;
		sendrec_ok = 0;
		recvrec_ok = 0;
		sendapp_ok = 0;

		if (sendrec && can_send) {
			sendrec_ok = 1;
		} else if (recvrec && can_recv) {
			recvrec_ok = 1;
		} else if (recvapp) {
			recvapp_ok = 1;
		} else if (sendapp && in_avail_buffered(h_in, &bb)) {
			sendapp_ok = 1;
		} else {
			/*
			 * If we cannot do I/O right away, then we must
			 * wait for some event, and try again.
			 */
			pfd[u] = (HANDLE)fd_event;
			k_fd = u;
			u ++;
			if (sendapp) {
				pfd[u] = h_in;
				k_in = u;
				u ++;
			}
			wt = WaitForMultipleObjectsEx(u, pfd,
				FALSE, INFINITE, FALSE);
			if (wt == WAIT_FAILED) {
				fprintf(stderr, "ERROR:"
					" WaitForMultipleObjectsEx()"
					" failed with 0x%08lX",
					(unsigned long)GetLastError());
				retcode = -2;
				goto engine_exit;
			}
			if (wt == k_fd) {
				WSANETWORKEVENTS e;

				if (WSAEnumNetworkEvents(fd, fd_event, &e)) {
					fprintf(stderr, "ERROR:"
						" WSAEnumNetworkEvents()"
						" failed with %d\n",
						WSAGetLastError());
					retcode = -2;
					goto engine_exit;
				}
				if (e.lNetworkEvents & (FD_WRITE | FD_CLOSE)) {
					can_send = 1;
				}
				if (e.lNetworkEvents & (FD_READ | FD_CLOSE)) {
					can_recv = 1;
				}
			}
			continue;
		}
#else
		if (sendrec || recvrec) {
			pfd[u].fd = fd;
			pfd[u].revents = 0;
			pfd[u].events = 0;
			if (sendrec) {
				pfd[u].events |= POLLOUT;
			}
			if (recvrec) {
				pfd[u].events |= POLLIN;
			}
			k_fd = u;
			u ++;
		}
		if (sendapp) {
			pfd[u].fd = 0;
			pfd[u].revents = 0;
			pfd[u].events = POLLIN;
			k_in = u;
			u ++;
		}
		if (recvapp) {
			pfd[u].fd = 1;
			pfd[u].revents = 0;
			pfd[u].events = POLLOUT;
			k_out = u;
			u ++;
		}
		n = poll(pfd, u, -1);
		if (n < 0) {
			if (errno == EINTR) {
				continue;
			}
			perror("ERROR: poll()");
			retcode = -2;
			goto engine_exit;
		}
		if (n == 0) {
			continue;
		}

		/*
		 * We transform closures/errors into read+write accesses
		 * so as to force the read() or write() call that will
		 * detect the situation.
		 */
		while (u -- > 0) {
			if (pfd[u].revents & (POLLERR | POLLHUP)) {
				pfd[u].revents |= POLLIN | POLLOUT;
			}
		}

		recvapp_ok = recvapp && (pfd[k_out].revents & POLLOUT) != 0;
		sendrec_ok = sendrec && (pfd[k_fd].revents & POLLOUT) != 0;
		recvrec_ok = recvrec && (pfd[k_fd].revents & POLLIN) != 0;
		sendapp_ok = sendapp && (pfd[k_in].revents & POLLIN) != 0;
#endif

		/*
		 * We give preference to outgoing data, on stdout and on
		 * the socket.
		 */
		if (recvapp_ok) {
			unsigned char *buf;
			size_t len;
#ifdef _WIN32
			DWORD wlen;
#else
			ssize_t wlen;
#endif

			buf = br_ssl_engine_recvapp_buf(cc, &len);
#ifdef _WIN32
			if (!WriteFile(h_out, buf, len, &wlen, NULL)) {
				if (verbose) {
					fprintf(stderr, "stdout closed...\n");
				}
				retcode = -2;
				goto engine_exit;
			}
#else
			wlen = write(1, buf, len);
			if (wlen <= 0) {
				if (verbose) {
					fprintf(stderr, "stdout closed...\n");
				}
				retcode = -2;
				goto engine_exit;
			}
#endif
			br_ssl_engine_recvapp_ack(cc, wlen);
			continue;
		}
		if (sendrec_ok) {
			unsigned char *buf;
			size_t len;
			int wlen;

			buf = br_ssl_engine_sendrec_buf(cc, &len);
			wlen = send(fd, buf, len, 0);
			if (wlen <= 0) {
#ifdef _WIN32
				int err;

				err = WSAGetLastError();
				if (err == EWOULDBLOCK
					|| err == WSAEWOULDBLOCK)
				{
					can_send = 0;
					continue;
				}
#else
				if (errno == EINTR || errno == EWOULDBLOCK) {
					continue;
				}
#endif
				if (verbose) {
					fprintf(stderr, "socket closed...\n");
				}
				retcode = -1;
				goto engine_exit;
			}
			if (trace) {
				dump_blob("Outgoing bytes", buf, wlen);
			}
			br_ssl_engine_sendrec_ack(cc, wlen);
			continue;
		}
		if (recvrec_ok) {
			unsigned char *buf;
			size_t len;
			int rlen;

			buf = br_ssl_engine_recvrec_buf(cc, &len);
			rlen = recv(fd, buf, len, 0);
			if (rlen == 0) {
				if (verbose) {
					fprintf(stderr, "socket closed...\n");
				}
				retcode = -1;
				goto engine_exit;
			}
			if (rlen < 0) {
#ifdef _WIN32
				int err;

				err = WSAGetLastError();
				if (err == EWOULDBLOCK
					|| err == WSAEWOULDBLOCK)
				{
					can_recv = 0;
					continue;
				}
#else
				if (errno == EINTR || errno == EWOULDBLOCK) {
					continue;
				}
#endif
				if (verbose) {
					fprintf(stderr, "socket broke...\n");
				}
				retcode = -1;
				goto engine_exit;
			}
			if (trace) {
				dump_blob("Incoming bytes", buf, rlen);
			}
			br_ssl_engine_recvrec_ack(cc, rlen);
			continue;
		}
		if (sendapp_ok) {
			unsigned char *buf;
			size_t len;
#ifdef _WIN32
			int rlen;
#else
			ssize_t rlen;
#endif

			buf = br_ssl_engine_sendapp_buf(cc, &len);
#ifdef _WIN32
			rlen = in_read_buffered(h_in, &bb, buf, len);
#else
			rlen = read(0, buf, len);
#endif
			if (rlen <= 0) {
				if (verbose) {
					fprintf(stderr, "stdin closed...\n");
				}
				br_ssl_engine_close(cc);
			} else if (!run_command(cc, buf, rlen)) {
				br_ssl_engine_sendapp_ack(cc, rlen);
			}
			br_ssl_engine_flush(cc, 0);
			continue;
		}

		/* We should never reach that point. */
		fprintf(stderr, "ERROR: poll() misbehaves\n");
		retcode = -2;
		goto engine_exit;
	}

	/*
	 * Release allocated structures.
	 */
engine_exit:
#ifdef _WIN32
	if (fd_event != WSA_INVALID_EVENT) {
		WSACloseEvent(fd_event);
	}
#endif
	return retcode;
}
