/*
 * Copyright (c) 2009, Steve Bennett
 * 
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, 
 *   this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice, 
 *   this list of conditions and the following disclaimer in the documentation 
 *   and/or other materials provided with the distribution.
 * * Neither the name of the axTLS project nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * 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.
 */

/*
 * sslwrap re-implemented with axTLS - a way to wrap an existing webserver
 * with axTLS.
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/poll.h>
#include "os_port.h"
#include "ssl.h"

/* If nothing is received or sent in this many seconds, give up */
static int opt_timeout = 60;

static int opt_verbose = 0;

int main(int argc, char *argv[])
{
	int log_opts = LOG_PERROR;
	int fd[2]; /* output from child */
	int df[2]; /* input to child */
	int pid;
	int readlen;
	unsigned char *readpt;
	static unsigned char *readbuf = NULL;
	int readbuf_len = 0;

	SSL_CTX *ssl_ctx;
	SSL *ssl;

	/* This relies on stdin and stdout being one and the same */
	int sslfd = fileno(stdin);

	while (argc > 2 && argv[1][0] == '-') 
    {
		if (argc > 3 && strcmp(argv[1], "-t") == 0) 
        {
			opt_timeout = atoi(argv[2]);
			argv += 2;
			argc -= 2;
			continue;
		}

		if (strcmp(argv[1], "-q") == 0) 
        {
			log_opts = 0;
			argv++;
			argc--;
			continue;
		}

		if (strcmp(argv[1], "-v") == 0) 
        {
			opt_verbose++;
			argv++;
			argc--;
			continue;
		}
	}

	if (argc < 2) 
    {
		fprintf(stderr, "Usage: axtlswrap [-v] [-q] "
                "[-t timeout] command ...\n");
		return 1;
	}

	if (access(argv[1], X_OK) != 0) 
    {
		fprintf(stderr, "Not an executabled: %s\n", argv[1]);
		return 1;
	}

	openlog("axtlswrap", LOG_PID | log_opts, LOG_DAEMON);

	/* Create an SSL context with the required options */
	ssl_ctx = ssl_ctx_new(opt_verbose > 1 ? 
                    SSL_DISPLAY_STATES | SSL_DISPLAY_CERTS : 0, 1);

	if (ssl_ctx == NULL) 
    {
		syslog(LOG_ERR, "Failed to create SSL ctx");
		return 1;
	}

	/* And create an ssl session attached to sslfd */
	ssl = ssl_server_new(ssl_ctx, sslfd);
	if (ssl == NULL) 
    {
		syslog(LOG_ERR, "Failed to create SSL connection");
		return 1;
	}

	/* Get past the handshaking */
	while ((readlen = ssl_read(ssl, &readpt)) == SSL_OK) 
    {
		/* Still handshaking */
	}

	if (readlen < 0) 
    {
		syslog(LOG_ERR, "SSL handshake failed: %d", readlen);
		return 1;
	}

	if (opt_verbose) 
    {
		syslog(LOG_INFO, "SSL handshake OK");
	}

	/* Looks OK, we have data, so fork the child and start */
	if (pipe(fd) < 0 || pipe(df) < 0) 
    {
		syslog(LOG_ERR, "pipe failed: %m");
		return 1;
	}

	/* Give some indication to the child that we are running SSL
	 * It would be possible to provide other details
	 * too. Perhaps as in: http://httpd.apache.org/docs/2.0/mod/mod_ssl.html
	 */
	setenv("SSL_PROTOCOL", "TLSv1", 1);

#ifndef NOMMU
	if (opt_verbose) 
    {
		pid = fork();
	}
	else
#endif
	pid = vfork();
	if (pid < 0) 
    {
		syslog(LOG_ERR, "vfork failed: %m");
		return 1;
	}

	if (pid > 0) 
    {
		/* This is the parent */
		static unsigned char writebuf[4096];
		int writelen = 0;
		struct pollfd pfd[3];
		int timeout_count = 0;

		int cwfd = df[1];	/* write to child */
		int crfd = fd[0];	/* read from child */

		int child_alive = 1;

		/* Don't die on SIGPIPE */
		signal(SIGPIPE, SIG_IGN);

		close(df[0]);
		close(fd[1]);

		pfd[0].fd = sslfd;
		pfd[1].fd = cwfd;
		pfd[2].fd = crfd;

		/* Need to copy any remaining read data into readbuf */
		if (readlen > readbuf_len) {
			readbuf = realloc(readbuf, readlen);
			readbuf_len = readlen;
		}
		memcpy(readbuf, readpt, readlen);


		/* While the child is alive or there is something to return...  */
		while (child_alive || writelen > 0) 
        {
			/* Work out what to read and what to write */
			int ret;

			pfd[0].events = 0;
			pfd[0].revents = 0;

			/* Only want to read ssl data if there is nothing else to do */
			if (readlen == 0) 
            {
				/* can read ssl data */
				pfd[0].events |= POLLIN;
			}

			if (writelen > 0) 
            {
				/* can write ssl data - will block to do this */
				pfd[0].events |= POLLOUT;
			}

			pfd[1].events = 0;
			pfd[1].revents = 0;

			if (child_alive && readlen > 0) 
            {
				pfd[1].events |= POLLOUT;
			}

			pfd[2].events = 0;
			pfd[2].revents = 0;

			if (child_alive && writelen == 0) 
            {
				pfd[2].events |= POLLIN;
			}

			/* Timeout after 1 second so we can increment timeout_count */
			ret = poll(pfd, 3, 1000);

			if (ret < 0) 
            {
				if (errno != EAGAIN) 
                {
					/* Kill off the child */
					kill(pid, SIGTERM);
					break;
				}

				continue;
			}

			if (ret == 0) 
            {
				if (++timeout_count >= opt_timeout) 
                {
					/* Kill off the child */
					kill(pid, SIGTERM);
					break;
				}

				continue;
			}

			timeout_count = 0;

			if (pfd[2].revents & POLLNVAL) 
            {
				/* REVISIT: This can probably be removed */
				syslog(LOG_ERR, "Child closed output pipe");
				child_alive = 0;
			}
			else if (pfd[2].revents & POLLIN) 
            {
				/* Can read from (3) */
				writelen = read(crfd, writebuf, sizeof(writebuf));
				if (writelen <= 0) 
                {
					if (writelen < 0) 
                    {
						syslog(LOG_WARNING, "Failed to read from child: len=%d",
                                writelen);
					}
					break;
				}
			}
			else if ((pfd[2].revents & POLLHUP) && kill(pid, 0) == 0) 
            {
				if (opt_verbose) 
                {
					syslog(LOG_INFO, "Child died and pipe gave POLLHUP");
				}

				child_alive = 0;
			}

			if (writelen > 0) 
            {
				const unsigned char *pt = writebuf;
				while (writelen > 0) 
                {
					ret = ssl_write(ssl, pt, writelen);
					if (ret <= 0) 
                    {
						syslog(LOG_WARNING, "Failed to write ssl: ret=%d", ret);
						/* Kill off the child now */
						kill(pid, SIGTERM);
						writelen = -1;
						break;
					}
					else 
                    {
						pt += ret;
						writelen -= ret;
					}
				}
				if (writelen < 0) 
                {
					break;
				}
			}
			else if (pfd[0].revents & POLLIN) 
            {
				readlen = ssl_read(ssl, &readpt);
				if (readlen <= 0 && opt_verbose) 
                {
					syslog(LOG_INFO, "ssl_read() returned %d", readlen);
				}

				if (readlen < 0) 
                {
					/* Kill off the child */
					kill(pid, SIGTERM);
					break;
				}
			}

			/* Copy read data into readbuf since ssl_write() shares a buffer with ssl_read() */
			if (readlen > readbuf_len) {
				readbuf = realloc(readbuf, readlen);
				readbuf_len = readlen;
			}
			memcpy(readbuf, readpt, readlen);

			if (pfd[1].revents & POLLNVAL) 
            {
				/* REVISIT: This can probably be removed */
				syslog(LOG_ERR, "Child closed input pipe");
				readlen = -1;
				child_alive = 0;
			}
			else if (pfd[1].revents & POLLOUT) 
            {
				const unsigned char *pt = readbuf;
				while (readlen > 0) 
                {
					int len = write(cwfd, pt, readlen);
					if (len <= 0) 
                    {
						syslog(LOG_WARNING, "Failed to write to child: len=%d", 
                                len);
						break;
					}

					readlen -= len;
					pt += len;
				}
			}

		}

		ssl_free(ssl);
#if 0
		fprintf(stderr, "[%d] SSL done: timeout_count=%d, readlen=%d, writelen=%d, child_alive=%d\n",
			getpid(), timeout_count, readlen, writelen, child_alive);
#endif
		return 0;
	}

	/* Child */
	close(df[1]);
	close(fd[0]);

	dup2(df[0],0);
	dup2(fd[1],1);

	close(df[0]);
	close(fd[1]);

	execv(argv[1], argv + 1);
	_exit(1);
}
