/* $Id: dcc.c 389 2006-09-12 14:51:32Z tsaviran $
 * -------------------------------------------------------
 * Copyright (C) 2003-2005 Tommi Saviranta <wnd@iki.fi>
 *	(C) 1998-2002 Sebastian Kienzl <zap@riot.org>
 * -------------------------------------------------------
 * 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.
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */

#ifdef DCCBOUNCE

#include "table.h"
#include "irc.h"
#include "error.h"
#include "messages.h"
#include "tools.h"
#include "miau.h"
#include "common.h"

#include <errno.h>
#include <netdb.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>

#if HAVE_STRINGS_H
#include <strings.h>
#endif



static int dcc_bouncedata(int a, int b, int *wrflag);
static int dcc_addbounce();
static void dcc_killbounce(int dccindex);

/* #define DEBUG */

typedef struct {
	/* Sockets. */
	int	src;
	int	dest;

	/* Writeable ? */
	int	src_wa;
	int	dest_wa;

	int	connected;
	time_t	created;

	unsigned int		srcport;
	struct sockaddr_in	destaddr;
} dccbounce;


typedef struct {
	dccbounce	**data;
	int		amount;
} dccbounce_type;


dccbounce_type dccs;


typedef struct {
	char	type[16];
	int	argc;
	char	*arg1;
	int	args[3];
	int	fromclient;	/* Sent by client or by server ? */
} dcccommand;


static int dcc_realinitiate(char *dest, size_t dsize, dcccommand *dcc);
static int dcc_resume(char *dest, size_t dsize, dcccommand *dcc);


typedef struct {
	char	*cmd;
	int	(*func) (char *dest, size_t dsize, dcccommand *dcc);
} dcccall;


dcccall dcccalls[] = {
	{"CHAT", dcc_realinitiate},
	{"SEND", dcc_realinitiate},
	{"RESUME", dcc_resume},
	{"ACCEPT", dcc_resume},
	{NULL, NULL}
};


static int
dcc_bouncedata(int a, int b, int *wrflag)
{
	static char	buffer[2048];
	int		rret, wret;
	
	rret = (int) recv(a, buffer, 2048, MSG_PEEK);
	if (rret <= 0) {
		if (errno == EAGAIN) {
			return 1;
		}
		return 0;
	}

	wret = send(b, buffer, rret, 0);
	if (wret <= 0) {
		if (errno == EAGAIN) {
			*wrflag = 0;
		}
		return 0;
	}

	/* Remove from queue. */
	recv(a, buffer, wret, 0);

	if (wret != rret) {
		*wrflag = 0;
	}
	
	return 1;
} /* int dcc_bouncedata(int a, int b, int *wrflag) */



static int
dcc_addbounce(void)
{
	int	dccindex;
	dccs.data = (dccbounce **) table_add_item((void **) dccs.data,
			sizeof(dccbounce), &dccs.amount, &dccindex);
	memset((void *) dccs.data[dccindex], 0, sizeof(dccbounce));
	dccs.data[dccindex]->created = time(NULL);
#ifdef DEBUG
	printf("added bounce %d\n", dccindex);
#endif
	return dccindex;
} /* int dcc_addbounce(void) */



static void
dcc_killbounce(int dccindex)
{
	if (dccs.data[dccindex]->src) {
		rawsock_close(dccs.data[dccindex]->src);
	}
	if (dccs.data[dccindex]->dest) {
		rawsock_close(dccs.data[dccindex]->dest);
	}
	dccs.data = (dccbounce **) table_rem_item((void **) dccs.data, dccindex,
			&dccs.amount);
#ifdef DEBUG
	printf("killed bounce %d\n", dccindex);
#endif
} /* void dcc_killbounce(int dccindex) */



void
dcc_timer(void)
{
	int	i;
	time_t	t;

	t = time(NULL);

	for (i = 0; i < dccs.amount; i++) {
		if (dccs.data[i] == NULL) {
			continue;
		}
		if (dccs.data[i]->dest == 0
				&& dccs.data[i]->created + 80 < t) {
			error(DCC_TIMEOUT, i);
			dcc_killbounce(i);
			continue;
		}
	}
} /* void dcc_timer(void) */



void
dcc_socketsubscribe(fd_set *readset, fd_set *writeset)
{
	int	i;
	
	for (i = 0; i < dccs.amount; i++) {
		if (dccs.data[i] == NULL) {
			continue;
		}

		if (! dccs.data[i]->connected || dccs.data[i]->dest_wa) {
			FD_SET(dccs.data[i]->src, readset);
		}

		if (dccs.data[i]->connected && dccs.data[i]->src_wa) {
			FD_SET(dccs.data[i]->dest, readset);
		}

		if (dccs.data[i]->dest && ! dccs.data[i]->dest_wa) {
			FD_SET(dccs.data[i]->dest, writeset);
		}
		
		if (dccs.data[i]->connected && ! dccs.data[i]->src_wa) {
			FD_SET(dccs.data[i]->src, writeset);
		}
	}
} /* void dcc_socketsubscribe(fd_set *readset, fd_set *writeset) */



void
dcc_socketcheck(fd_set *readset, fd_set *writeset)
{
	int		i;
	int		sockopt;
	socklen_t	len;
	int		socksave;
	char		*host;

	for (i = 0; i < dccs.amount; i++) {
		if (dccs.data[i] == NULL) {
			continue;
		}

		/* writesets */
		if (dccs.data[i]->connected &&
				FD_ISSET(dccs.data[i]->src, writeset)) {
			dccs.data[i]->src_wa = 1;
#ifdef DEBUG
			printf("src is now writeable\n");
#endif
		}

		if (dccs.data[i]->dest &&
				FD_ISSET(dccs.data[i]->dest, writeset)) {
			if (! dccs.data[i]->connected) {
				len = sizeof(int);
				getsockopt(dccs.data[i]->dest, SOL_SOCKET,
						SO_ERROR, &sockopt, &len);
				if (sockopt) {
					error(DCC_ERRCONNECT, strerror(sockopt),
							i);
					dcc_killbounce(i);
					continue;
				}
				socksave = dccs.data[i]->src;

				dccs.data[i]->src = sock_accept(socksave,
						&host, 0);
				if (dccs.data[i]->src <= 0) {
					error(DCC_ERRACCEPT, net_errstr, i);
					dcc_killbounce(i);
					continue;
				}
				rawsock_close(socksave);

				dccs.data[i]->connected = 1;
				report(DCC_SUCCESS, host, i);
				xfree(host);
				continue;
			}
			
			else {
				dccs.data[i]->dest_wa = 1;
#ifdef DEBUG
				printf("dest is now writeable\n");
#endif
			}
		}

		/* readsets */
		if (FD_ISSET(dccs.data[i]->src, readset)) {
			if (! dccs.data[i]->connected ) {
				/* We're still waiting for a connection. */
				/*
				 * Incoming! ...but don't accept it, try to
				 * establish the other connection first !
				 */
				if (! dccs.data[i]->dest) {
					/* Maybe we got that already. */
					if ((dccs.data[i]->dest = sock_open())
							< 0) {
						error(DCC_ERRSOCK, net_errstr,
								i);
						dcc_killbounce(i);
						continue;
					}
					sock_setnonblock(dccs.data[i]->dest);

					if (connect(dccs.data[i]->dest, (struct sockaddr *) &dccs.data[i]->destaddr, sizeof(struct sockaddr)) < 0 && (errno != EINPROGRESS)) {
						error(DCC_ERRCONNECT,
								net_errstr, i);
						dcc_killbounce(i);
						continue;
					}
				}
			}
			else {
				/* all connected */
#ifdef DEBUG
				printf("bouncing from src\n");
#endif
				if (! dcc_bouncedata(dccs.data[i]->src,
							dccs.data[i]->dest,
							&dccs.data[i]->dest_wa
							)) {
					report(DCC_END, i);
					dcc_killbounce(i);
					continue;
				}
			}
		}
		
		if (dccs.data[i]->connected &&
				FD_ISSET(dccs.data[i]->dest, readset)) {
#ifdef DEBUG
			printf("bouncing from dest\n");
#endif
			if (! dcc_bouncedata(dccs.data[i]->dest,
						dccs.data[i]->src,
						&dccs.data[i]->src_wa)) {
				report(DCC_END, i);
				dcc_killbounce(i);
				continue;
			}
		}
	} /* for */
} /* void dcc_socketcheck(fd_set *readset, fd_set *writeset) */



char *
dcc_initiate(char *param, size_t dsize, int fromclient)
{
	dcccommand	dcc;
	char		*dparam, *chop, *check;
	int		i;

	memset((void *) &dcc, 0, sizeof(dcccommand));

	dparam = xstrdup(param);
	chop = dparam;
	
	dcc.fromclient = fromclient;
	
#define DCCINITNULLRETURN { xfree(dparam); return NULL; }

	while ((chop = strchr(chop, ' ')) != NULL && *(++chop) != '\0') {
		switch (dcc.argc) {
			case 0:
				xstrncpy(dcc.type, chop, 15);
				if (strchr(dcc.type, ' ')) {
					*strchr(dcc.type, ' ') = 0;
				}
				dcc.type[15] = 0;
				upcase(dcc.type);
				break;
			case 1:
				dcc.arg1 = chop;
				break;
			case 2:
				*(chop - 1) = 0;
			case 3:
			case 4:
				dcc.args[dcc.argc - 2] =
					strtoul(chop, &check, 10);
				if (check == chop) {
					DCCINITNULLRETURN;
				}
				break;
		}
		dcc.argc++;
	}

	if (dcc.argc < 4) {
		DCCINITNULLRETURN;
	}

#ifdef DEBUG
	printf("DCC %d [%s][%s][%u][%u][%u]\n",
			dcc.argc, dcc.type, dcc.arg1,
			dcc.args[0], dcc.args[1], dcc.args[2]);
#endif

	for (i = 0; dcccalls[i].cmd; i++) {
		if (xstrcmp(dcccalls[i].cmd, dcc.type) == 0) {
			if (dcccalls[i].func(param, dsize, &dcc) == 0) {
				DCCINITNULLRETURN;
			}
			break;
		}
	}

	xfree(dparam);
	
	return param;;
} /* char *dcc_initiate(char *param, size_t n, int fromclient) */



static int
dcc_realinitiate(char *dest, size_t dsize, dcccommand *dcc)
{
	unsigned int	address, port;
	struct hostent	*host;
	char		*hostname;
	unsigned int	myport;
	int		i, dccindex;

	address = htonl(dcc->args[0]);
	port = dcc->args[1];

	if (dcc->fromclient == 0 && cfg.dccbindhost != NULL) {
		host = name_lookup(cfg.dccbindhost);
	}
	else {
		if (cfg.bind != NULL) {
			host = name_lookup(cfg.bind);
		}
		else {
			hostname = (char *) xmalloc(256);
			if (gethostname(hostname, 255)) {
				xfree(hostname);
				return 0;
			}
			host = name_lookup(hostname);
			xfree(hostname);
		}
	}

	/* TODO take care of host being NULL */

	dccindex = dcc_addbounce();
	dccs.data[dccindex]->src = sock_open();
	if (dccs.data[dccindex]->src < 0) {
		error(SOCK_ERROPEN, net_errstr);
		dcc_killbounce(dccindex);
		return 0;
	}

	sock_setnonblock(dccs.data[dccindex]->src);

	i = 15;
	do {
		myport = (random() & 0xffff) | 1024;
	} while (! sock_bind(dccs.data[dccindex]->src, NULL, myport) && --i);
	
	if (! i) {
		error(SOCK_GENERROR, "unable to bind to any port");
		dcc_killbounce(dccindex);
		return 0;
	}

	dccs.data[dccindex]->srcport = myport;
	
	if (! sock_listen(dccs.data[dccindex]->src)) {
		error(SOCK_ERRLISTEN);
		dcc_killbounce(dccindex);
		return 0;
	}

	snprintf(dest, dsize, "\1DCC %s %s %u %u", dcc->type, dcc->arg1,
			(unsigned int) ntohl(*(unsigned long int *)
					     host->h_addr),
			myport);
	dest[dsize - 1] = '\0';

	if (dcc->argc == 5) {
		snprintf(dest, dsize, "%s %u\1", dest, dcc->args[2]);
	}
	else {
		strcat(dest, "\1");
	}
	dest[dsize - 1] = '\0';

	host = name_lookup(inet_ntoa(*(struct in_addr *) &address));
	memcpy((char *) &dccs.data[dccindex]->destaddr.sin_addr, host->h_addr,
			host->h_length);
	dccs.data[dccindex]->destaddr.sin_port = htons((u_short) port);
	dccs.data[dccindex]->destaddr.sin_family = (short) host->h_addrtype;
	
	report(DCC_START, inet_ntoa(*(struct in_addr *) &address), port,
			dccindex);

	return 1;
} /* static int dcc_realinitiate(char *dest, size_t dsize, dcccommand *dcc) */



static int
dcc_resume(char *dest, size_t dsize, dcccommand *dcc)
{
	int	i;
	int	resume;

	resume = xstrcmp(dcc->type, "RESUME") == 0;

	for (i = 0; i < dccs.amount; i++) {
		if (dccs.data[i] == NULL) {
			continue;
		}

		/* RESUME */
		if (resume) {
			if (dccs.data[i]->srcport == dcc->args[0]) {
				snprintf(dest, dsize, "\1DCC RESUME %s %u %u\1",
						dcc->arg1,
						ntohs(dccs.data[i]->destaddr.sin_port),
						dcc->args[1]);
				dest[dsize - 1] = '\0';
				return 1;
			}
		}
		/* ACCEPT */
		else {
			if (ntohs(dccs.data[i]->destaddr.sin_port) ==
					dcc->args[0]) {
				snprintf(dest, dsize, "\1DCC ACCEPT %s %u %u\1",
						dcc->arg1,
						dccs.data[i]->srcport,
						dcc->args[1]);
				dest[dsize - 1] = '\0';
				return 1;
			}
		}
	}
	
	return 0;
} /* static int dcc_resume(char *dest, size_t dsize, dcccommand *dcc) */



#endif /* ifdef DCCBOUNCE */


syntax highlighted by Code2HTML, v. 0.9.1