/*
 * Copyright (c) 2003, 2004 Niels Provos <provos@citi.umich.edu>
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <sys/param.h>
#include <sys/types.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#include <sys/tree.h>
#include <sys/queue.h>

#include <unistd.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <dnet.h>
#include <assert.h>

#undef timeout_pending
#undef timeout_initialized

#include <event.h>

#include "honeyd.h"
#include "template.h"
#include "subsystem.h"
#include "util.h"
#include "fdpass.h"

ssize_t atomicio(ssize_t (*)(), int, void *, size_t);


int
templ_container_compare(struct template_container *a,
    struct template_container *b)
{
	return (strcmp(a->tmpl->name, b->tmpl->name));
}

/* Store referencing templates in tree */
SPLAY_GENERATE(subtmpltree, template_container, node, templ_container_compare);

void subsystem_read(int, short, void *);
void subsystem_write(int, short, void *);

struct callback subsystem_cb = {
	subsystem_read, subsystem_write, NULL, NULL
};

/* Determine if the socket information is valid */

#define SOCKET_REMOTE		0
#define SOCKET_LOCAL		1
#define SOCKET_MAYBELOCAL	2

int
subsystem_socket(struct subsystem_command *cmd, int local,
    char *ip, size_t iplen, u_short *port, int *proto)
{
	struct sockaddr_in *si;
	struct addr src;
	socklen_t len;

	si = (struct sockaddr_in *)(local ? &cmd->sockaddr : &cmd->rsockaddr);
	len = local ? cmd->len : cmd->rlen;

	/* Only IPv4 TCP or UDP is allowed.  No raw sockets or such */
	if (si->sin_family != AF_INET || cmd->domain != AF_INET ||
	    !(cmd->type == SOCK_DGRAM || cmd->type == SOCK_STREAM) ||
	    len != sizeof(struct sockaddr_in)) {
		if (local == SOCKET_LOCAL)
			return (-1);
		memset(&cmd->sockaddr, 0, sizeof(cmd->sockaddr));
	}

	addr_pack(&src, ADDR_TYPE_IP, IP_ADDR_BITS, &si->sin_addr.s_addr,
	    IP_ADDR_LEN);
	addr_ntop(&src, ip, iplen);

	*port = ntohs(si->sin_port);
	*proto = cmd->type == SOCK_DGRAM ? IP_PROTO_UDP : IP_PROTO_TCP;

	return (0);
}

struct template *
subsystem_template_find(struct subsystem *sub, char *name)
{
	struct template tmp;
	struct template_container cont, *find;

	tmp.name = name;
	cont.tmpl = &tmp;

	find = SPLAY_FIND(subtmpltree, &sub->root, &cont);
	
	return (find != NULL ? find->tmpl : NULL);
}

void
subsystem_insert_template(struct subsystem *sub, struct template *tmpl)
{
	struct template_container *cont;
	struct addr addr;
	int isipaddr = addr_aton(tmpl->name, &addr) != -1;

	if ((cont = calloc(1, sizeof(struct template_container))) == NULL)
		err(1, "%s: calloc");
	cont->tmpl = template_ref(tmpl);

	TAILQ_INSERT_TAIL(&sub->templates, cont, next);
	if (isipaddr)
		SPLAY_INSERT(subtmpltree, &sub->root, cont);
}

void
subsystem_restart(int fd, short what, void *arg)
{
	struct subsystem *sub = arg;
	struct template_container *cont = TAILQ_FIRST(&sub->templates);
	struct template *tmpl = cont->tmpl;
	template_subsystem_free_ports(sub);
	cmd_free(&sub->cmd);
	syslog(LOG_INFO, "Restarting subsystem \"%s\"", sub->cmdstring);
	template_subsystem_start(tmpl, sub);
}

void
subsystem_cleanup(struct subsystem *sub)
{
	syslog(LOG_INFO, "Subsystem \"%s\" died", sub->cmdstring);

	if (sub->flags & SUBSYSTEM_RESTART) {
		struct timeval tmp;
		gettimeofday(&tmp, NULL);
		timersub(&tmp, &sub->tv_restart, &tmp);

		/*
		 * Restart the subsystem immediately if we did not restart
		 * it recently.  Otherwise, delay the restart.
		 */
		if (tmp.tv_sec >= SUBSYSTEM_RESTART_INTERVAL) {
			subsystem_restart(-1, EV_TIMEOUT, sub);
		} else {
			struct timeval tv;
			
			timerclear(&tv);
			tv.tv_sec = 2 * SUBSYSTEM_RESTART_INTERVAL;
			event_once(-1, EV_TIMEOUT,
			    subsystem_restart, sub, &tv);
		}
		return;
	}

	/* XXX - do proper cleanup here */
	template_subsystem_free(sub);
}

void
subsystem_readyport(struct port *port, struct subsystem *sub,
    struct template *tmpl)
{
	assert(port->sub == NULL);
	assert(port->subtmpl == NULL);
	port->sub = sub;
	port->subtmpl = tmpl;
	port->sub_fd = -1;

	TAILQ_INSERT_TAIL(&sub->ports, port, next);
}

/*
 * Tries to find an unallocated port in all of the templates that
 * are being shared or just in the single template if no sharing
 * is going on.
 */

int
subsystem_findport(struct subsystem *sub, char *name, int proto)
{
	struct template_container *cont;
	struct template *tmpl;
	struct port *sub_port = NULL;
	u_short port;
	int done = 0;

	while (!done) {
		struct action action;
		memset(&action, 0, sizeof(action));
		action.status = PORT_RESERVED;

		/* 
		 * Try to find the right template or default to the root,
		 * if we need to deal with multiple templates at the same
		 * time.
		 */
		cont = SPLAY_ROOT(&sub->root);
		tmpl = cont->tmpl;
		if (!strcmp(name, "0.0.0.0")) {
			tmpl = subsystem_template_find(sub, name);
			if (tmpl == NULL) {
				cont = SPLAY_ROOT(&sub->root);
				tmpl = cont->tmpl;
			}
		}

		sub_port = port_random(tmpl, proto, &action, 1024, 49151);
		if (sub_port == NULL)
			return (0);

		port = sub_port->number;
		port_free(tmpl, sub_port);

		/* Fast path for single template */
		if (!strcmp(name, "0.0.0.0")) {
			break;
		}

		/* Assume that we succeed */
		done = 1;

		/* Now test this port number for all templates */
		SPLAY_FOREACH(cont, subtmpltree, &sub->root) {
			tmpl = cont->tmpl;
			sub_port = port_insert(tmpl, proto, port, &action);
			if (sub_port == NULL) {
				/* This port is in use already */
				done = 0;
				break;
			}

			/* Free the port and continue with the next template */
			port_free(tmpl, sub_port);
		}
	}

	syslog(LOG_DEBUG, "Subsytem \"%s\" binds %s to port %d",
	    sub->cmdstring, name, port);

	return (port);
}

int
subsystem_bind(int fd, struct template *tmpl, struct subsystem *sub,
    int proto, u_short port)
{
	struct port *sub_port;
	struct action action;

	/* Setup port type */
	memset(&action, 0, sizeof(action));
	action.status = PORT_RESERVED;

	sub_port = port_insert(tmpl, proto, port, &action);
	if (sub_port == NULL)
		return (-1);

	/* Set up necessary port information */
	subsystem_readyport(sub_port, sub, tmpl);

	syslog(LOG_DEBUG, "Subsytem \"%s\" binds %s:%d",
	    sub->cmdstring, tmpl->name, port);

	return (0);
}

int
subsystem_listen(struct port *sub_port, char *ip, int nfd)
{
	syslog(LOG_DEBUG, "Listen: %s:%d -> fd %d", 
	    ip, sub_port->number, nfd);

	/* We use this fd to notify the other side */
	TRACE(nfd, sub_port->sub_fd = fdshare_dup(nfd));
	if (sub_port->sub_fd == -1)
		return (-1);
	sub_port->sub_islisten = 1;
			
	/* Enable this port */
	sub_port->action.status = PORT_SUBSYSTEM;

	return (0);
}

int
subsystem_cmd_listen(int fd,
    struct subsystem *sub, struct subsystem_command *cmd)
{
	struct template_container *cont;
	struct template *tmpl;
	struct port *sub_port = NULL;
	char asrc[24];
	u_short port;
	int proto;
	int nfd;
	int res = -1;

	/* Check address family */
	if (subsystem_socket(cmd, SOCKET_LOCAL, asrc, sizeof(asrc),
		&port, &proto) == -1) {
		syslog(LOG_WARNING, "%s: listen bad socket", __func__);
		return (-1);
	}

	if (strcmp(asrc, "0.0.0.0") != 0) {
		tmpl = subsystem_template_find(sub, asrc);
	} else {
		cont = SPLAY_ROOT(&sub->root);
		tmpl = cont->tmpl;
	}
	if (tmpl != NULL)
		sub_port = port_find(tmpl, proto, port);
	if (sub_port == NULL) {
		syslog(LOG_WARNING, "%s: proto %d port %d not bound",
		    __func__, proto, port);
		return (-1);
	}

	res = 0;
	TRACE(fd, atomicio(write, fd, &res, 1));
	res = -1;

	/* Repeat until we get a result */
	while ((nfd = receive_fd(fd, NULL, NULL)) == -1) {
		if (errno != EAGAIN)
			break;
	}

	if (nfd == -1) {
		syslog(LOG_WARNING, "%s: no file descriptor",__func__);
		return (-1);
	}

	TRACE(nfd, res = fdshare_dup(nfd));
	if (res == -1) {
		syslog(LOG_WARNING, "%s: out of memory", __func__);
		TRACE_RESET(nfd, close(nfd));
		return (-1);
	}

	if (strcmp(asrc, "0.0.0.0") != 0) {
		TRACE(nfd, subsystem_listen(sub_port, asrc, nfd));
	} else {
		/* 
		 * Subsystem sharing means that we need to
		 * listen to all templates
		 */
		int success = 0;
		SPLAY_FOREACH(cont, subtmpltree, &sub->root) {
			tmpl = cont->tmpl;
			sub_port = port_find(tmpl, proto, port);
			if (sub_port == NULL)
				errx(1, "%s: no proto %d port %d",
				    __func__, proto, port);
			if (sub_port->sub == NULL) {
				syslog(LOG_DEBUG,
				    "Subsystem %s fails to listen on %s:%d",
				    sub->cmdstring, tmpl->name, port);
			} else {
				int ok;
				TRACE(nfd, ok = subsystem_listen(sub_port,
					  asrc, nfd));
				if (ok != -1)
					success = 1;
			}
		}
		if (!success)
			res = -1;
	}

	/* Close this file descriptor */
	TRACE(nfd, fdshare_close(nfd));
		
	return (res);
}

void
subsystem_read(int fd, short what, void *arg)
{
	struct subsystem *sub = arg;
	struct subsystem_command cmd;
	struct sockaddr_in *si = (struct sockaddr_in *)&cmd.sockaddr;
	char asrc[24], adst[24];
	u_short port, local_port;
	int proto, n;
	char res = -1;

	TRACE(fd, n = atomicio(read, fd, &cmd, sizeof(cmd)));
	if (n != sizeof(cmd)) {
		subsystem_cleanup(sub);
		return;
	}

	switch (cmd.command) {
	case SUB_BIND: {
		struct template_container *cont;
		struct template *tmpl;
	
		/* Check address family */
		if (subsystem_socket(&cmd, SOCKET_LOCAL, asrc, sizeof(asrc),
			&port, &proto) == -1)
			goto out;

		if (port == 0) {
			/* Try to find a port that is free everywhere */
			port = subsystem_findport(sub, asrc, proto);
			if (port == 0)
				goto out;
		}

		/* See if it tries to bind an address that we know */
		if (si->sin_addr.s_addr == IP_ADDR_ANY) {
			int ret;
			res = -1;
			/* Bind to all associated templates */
			SPLAY_FOREACH(cont, subtmpltree, &sub->root) {
				tmpl = cont->tmpl;
				TRACE(fd, ret = subsystem_bind(fd, tmpl,
					  sub,proto,port));
				/* One success is good enough for us */
				if (ret != -1) {
					res = 0;
				} else {
					syslog(LOG_DEBUG,
					    "Subsystem %s fails to bind to "
					    "port %d on %s",
					    sub->cmdstring, port, tmpl->name);
				}
			}
		} else {
			/* See if we can find a good template */
			tmpl = subsystem_template_find(sub, asrc);
			if (tmpl == NULL) {
				cont = SPLAY_ROOT(&sub->root);
				tmpl = cont->tmpl;
				syslog(LOG_WARNING,
				    "Subsystem %s on %s attempts "
				    "illegal bind %s:%d",
				    sub->cmdstring, tmpl->name, asrc, port);
				goto out;
			}

			TRACE(fd,
			    res = subsystem_bind(fd, tmpl, sub, proto, port));
		}

		/* Confirm success or failure of this phase */
		TRACE(fd, atomicio(write, fd, &res, 1));
			
		/* On success, we also communicate the port back */
		if (res != -1) {
			TRACE(fd, atomicio(write, fd, &port, sizeof(port)));
		}
		goto reschedule;
	}

	case SUB_LISTEN:
		TRACE(fd, res = subsystem_cmd_listen(fd, sub, &cmd));
		break;

	case SUB_CLOSE: {
		struct template_container *cont;
		struct template *tmpl = NULL;
		struct port *sub_port;

		/* Check address family */
		if (subsystem_socket(&cmd, SOCKET_LOCAL, asrc, sizeof(asrc),
			&port, &proto) == -1)
			goto out;

		syslog(LOG_DEBUG, "Close: %s:%d", asrc, port);
		if (strcmp(asrc, "0.0.0.0") != 0) {
			tmpl = subsystem_template_find(sub, asrc);
			if (tmpl == NULL)
				goto out;
			sub_port = port_find(tmpl, proto, port);
			if (sub_port == NULL || sub_port->sub != sub)
				goto out;
			
			port_free(tmpl, sub_port);
		} else {
			SPLAY_FOREACH(cont, subtmpltree, &sub->root) {
				tmpl = cont->tmpl;
				/* XXX - only bound port */
				sub_port = port_find(tmpl, proto, port);
				if (sub_port == NULL || sub_port->sub != sub)
					continue;
				
				port_free(tmpl, sub_port);
			}
		}
		break;
	}

	case SUB_CONNECT: {
		struct template_container *cont;
		struct template *tmpl;
		struct port *sub_port;
		struct action action;
		struct addr src, dst;
		struct ip_hdr ip;

		/* Check remote address family */
		if (subsystem_socket(&cmd, SOCKET_MAYBELOCAL,
			asrc, sizeof(asrc), &local_port, &proto) == -1)
			goto out;
		if (subsystem_socket(&cmd, SOCKET_REMOTE, adst, sizeof(adst),
			&port, &proto) == -1)
			goto out;
		
		/* Find appropriate template */
		if (strcmp(asrc, "0.0.0.0") != 0) {
			tmpl = subsystem_template_find(sub, asrc);
			if (tmpl == NULL)
				errx(1, "%s: source address %s not found",
				    __func__, asrc);
		} else {
			cont = SPLAY_ROOT(&sub->root);
			tmpl = cont->tmpl;
		}

		syslog(LOG_DEBUG, "Connect: %s %s:%d -> %s:%d",
		    proto == IP_PROTO_UDP ? "udp" : "tcp",
		    tmpl->name, local_port, adst, port);

		if (addr_aton(tmpl->name, &src) == -1)
			goto out;
		if (addr_aton(adst, &dst) == -1)
			goto out;

		memset(&action, 0, sizeof(action));
		action.status = PORT_RESERVED;

		if (local_port == 0) {
			sub_port = port_random(tmpl, proto, &action,
			    1024, 49151);
			if (sub_port != NULL)
				subsystem_readyport(sub_port, sub, tmpl);
		} else {
			/*
			 * If the port is bound already, then we need to use
			 * that port number.
			 */
			sub_port = port_find(tmpl, proto, local_port);
		}
		if (sub_port == NULL)
			goto out;
		
		/* Verify that we have the correct binding */
		assert(sub_port->sub == sub);

		syslog(LOG_DEBUG, "Connect: allocated port %d",
		    sub_port->number);

		/* The remote side is the source */
		ip.ip_src = dst.addr_ip;
		ip.ip_dst = src.addr_ip;

		/* Try to setup a TCP connection */
		if (proto == IP_PROTO_TCP) {
			struct tcp_con *con;
			struct tcp_hdr tcp;
			int nfd;

			tcp.th_sport = htons(port);
			tcp.th_dport = htons(sub_port->number);

			if ((con = tcp_new(&ip, &tcp, 1)) == NULL)
				goto out;
			con->tmpl = template_ref(tmpl);

			/* Cross notify */
			con->port = sub_port;
			sub_port->sub_conport = &con->port;

			/* Confirm success of this phase */
			res = 0;
			TRACE(fd, atomicio(write, fd, &res, 1));
			
			/* Now get the control fd */
			while ((nfd = receive_fd(fd, NULL, NULL)) == -1) {
				if (errno != EAGAIN) {
					tcp_free(con);
					goto out;
				}
			}
			TRACE(nfd, sub_port->sub_fd = fdshare_dup(nfd));

			/* Confirm success again */
			res = 0;
			TRACE(nfd, atomicio(write, nfd, &res, 1));
			
			/* Send out the SYN packet */
			con->state = TCP_STATE_SYN_SENT;
			tcp_send(con, TH_SYN, NULL, 0);
			con->snd_una++;

			con->retrans_time = 1;
			generic_timeout(&con->retrans_timeout, con->retrans_time);
			goto reschedule;
		} else if (proto == IP_PROTO_UDP) {
			struct udp_con *con;
			struct udp_hdr udp;
			int nfd;

			/* The remote side is the source */
			udp.uh_sport = htons(port);
			udp.uh_dport = htons(sub_port->number);

			if ((con = udp_new(&ip, &udp, 1)) == NULL)
				goto out;
			con->tmpl = template_ref(tmpl);
			
			/* Cross notify */
			con->port = sub_port;
			sub_port->sub_conport = &con->port;

			/* Confirm success of this phase */
			res = 0;
			TRACE(fd, atomicio(write, fd, &res, 1));

			/* Now get the control fd */
			while ((nfd = receive_fd(fd, NULL, NULL)) == -1) {
				if (errno != EAGAIN) {
					udp_free(con);
					goto out;
				}
			}
			TRACE(nfd, sub_port->sub_fd = fdshare_dup(nfd));

			/* Confirm success again */
			res = 0;
			TRACE(nfd, atomicio(write, nfd, &res, 1));
                       
			/* Connect our system to the subsystem */
			cmd_subsystem_localconnect(&con->conhdr, &con->cmd,
			    sub_port, con);
			goto reschedule;
		}
	}
	default:
		break;
	}

 out:
	TRACE(fd, atomicio(write, fd, &res, 1));
 reschedule:
	/* Reschedule read */
	TRACE(sub->cmd.pread.ev_fd, event_add(&sub->cmd.pread, NULL));
}

void
subsystem_write(int fd, short what, void *arg)
{
	/* Nothing */
}

void
subsystem_print(struct evbuffer *buffer, struct subsystem *sub)
{
	time_t restart_secs = sub->tv_restart.tv_sec;

	evbuffer_add_printf(buffer, "subsystem %s:\n", sub->cmdstring);
	evbuffer_add_printf(buffer, "  pid: %d %s%s\n",
	    sub->cmd.pid,
	    sub->flags & SUBSYSTEM_SHARED ? "shared " : "",
	    sub->flags & SUBSYSTEM_RESTART ? "restart " : "");
	evbuffer_add_printf(buffer, "  running since: %s",
	    ctime(&restart_secs));
	    
}
