/***************************************

    This is part of frox: A simple transparent FTP proxy
    Copyright (C) 2000 James Hollingshead

    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
  
     data.c -- Code for handling the data connection and picking fds to select.

  ***************************************/


#include <sys/ioctl.h>
#include <fcntl.h>
#include <syslog.h>

#include "data.h"
#include "control.h"
#include "cache.h"
#include "vscan.h"
#include "transdata.h"
#include "ssl.h"

void client_data_connect(void);
void server_data_connect(void);

void writebuf2client(void);
void writebuf2server(void);
void forwarddata2client(void);
void forwarddata2server(void);

void closecd(void);
void closesd(void);

void bwlimit(int bytes, int rate);

/* ------------------------------------------------------------- **
** Setup fd sets for reading/writing. We always listen to the client's
** control stream, and other fds are selected on depending on state.
** ------------------------------------------------------------- */
int setup_fds(fd_set * reads, fd_set * writes)
{
	int n;

	FD_ZERO(reads);
	FD_ZERO(writes);

	FD_SET(info->client_control.fd, reads);
	n = info->client_control.fd;

	if(info->server_control.fd != -1) {
		FD_SET(info->server_control.fd, reads);
		if(info->server_control.fd > n)
			n = info->server_control.fd;
	}

	if(info->server_data.fd != -1) {
		if(sstr_len(info->client_data.buf) == 0)
			FD_SET(info->server_data.fd, reads);

		if(sstr_len(info->server_data.buf) != 0)
			FD_SET(info->server_data.fd, writes);
		if(info->server_data.fd > n)
			n = info->server_data.fd;
	}
	if(info->client_data.fd != -1) {
		if(sstr_len(info->server_data.buf) == 0)
			FD_SET(info->client_data.fd, reads);
		if(sstr_len(info->client_data.buf) != 0)
			FD_SET(info->client_data.fd, writes);
		if(info->client_data.fd > n)
			n = info->client_data.fd;
	}
	if(info->client_listen != -1) {
		FD_SET(info->client_listen, reads);
		if(info->client_listen > n)
			n = info->client_listen;
	}
	if(info->server_listen != -1) {
		FD_SET(info->server_listen, reads);
		if(info->server_listen > n)
			n = info->server_listen;
	}
	return (n);
}

/* ------------------------------------------------------------- **
** Check the fd_sets, and do data connection forwarding if any.
** ------------------------------------------------------------- */
void do_dataforward(fd_set * reads, fd_set * writes)
{
	if(info->client_listen != -1 && FD_ISSET(info->client_listen, reads))
		client_data_connect();
	if(info->server_listen != -1 && FD_ISSET(info->server_listen, reads))
		server_data_connect();

	if(info->server_data.fd != -1)
		if(FD_ISSET(info->server_data.fd, reads))
			forwarddata2client();
	if(info->server_data.fd != -1)
		if(FD_ISSET(info->server_data.fd, writes))
			writebuf2server();

	if(info->client_data.fd != -1)
		if(FD_ISSET(info->client_data.fd, reads))
			forwarddata2server();
	if(info->client_data.fd != -1)
		if(FD_ISSET(info->client_data.fd, writes))
			writebuf2client();

}

/* ------------------------------------------------------------- **
**  Client just connected to the data line
** ------------------------------------------------------------- */
void client_data_connect()
{
	int len = sizeof(info->client_data.address);

	write_log(VERBOSE, "  Client has connected to proxy data line");
	info->client_data.fd = accept(info->client_listen,
				      (struct sockaddr *) &info->
				      client_data.address, &len);

	if(config.sameaddress) {
		if(info->client_data.address.sin_addr.s_addr !=
		   info->client_control.address.sin_addr.s_addr) {
			write_log(ATTACK,
				  "Blocked %s from connecting to data line",
				  addr2name(info->client_data.address.
					    sin_addr));
			rclose(&info->client_data.fd);
			return;
		}
	}
	il_free();		/* Remove the ipchains entry for intercepting this
				 * data connection.*/

	sstr_empty(info->client_data.buf);

	if(info->mode == PACONV)
		return;
	/* Cache code could have already connected us. */
	if(info->server_data.fd == -1)
		connect_server_data();

}

/* ------------------------------------------------------------- **
**  Server just connected to the data line
** ------------------------------------------------------------- */
void server_data_connect()
{
	int len = sizeof(info->server_data.address);

	write_log(VERBOSE, "  Server has connected to proxy data line");
	info->server_data.fd = accept(info->server_listen,
				      (struct sockaddr *) &info->
				      server_data.address, &len);
	if(config.sameaddress) {
		if(info->server_data.address.sin_addr.s_addr !=
		   info->server_control.address.sin_addr.s_addr) {
			write_log(ATTACK,
				  "Blocked %s from connecting to data line",
				  addr2name(info->server_data.address.
					    sin_addr));
			ssl_shutdown(&info->ssl_sd);
			rclose(&info->server_data.fd);
			return;
		}
	}

	sstr_empty(info->server_data.buf);
	info->ssl_sd = ssl_initfd(info->server_data.fd, SSL_DATA);
	if(info->mode == PACONV)
		return;
	connect_client_data();
}

/*Connect to server data port. Initialise ssl on it if necessary*/
int connect_server_data()
{
	info->server_data.fd =
		connect_to_socket(&info->server_data.address,
				  &config.tcpoutaddr, config.pasvports);
	if(info->server_data.fd == -1) {
		write_log(ERROR, "Failed to contact server data port");
		rclose(&info->client_data.fd);
		return (-1);
	}
	sstr_empty(info->server_data.buf);

	/*This shouldn't need to be non blocking as data is only read
	 *when select says it is there. Non blocking makes ssl really
	 *tricky*/
	/*fcntl(info->server_data.fd, F_SETFL, O_NONBLOCK); */
	info->ssl_sd = ssl_initfd(info->server_data.fd, SSL_DATA);
	return (0);
}

int connect_client_data()
{
	/*FIXME should use server_data.address.sin_addr, but 20 as port */
	if(config.transdata)
		info->client_data.fd =
			transp_connect(info->client_data.address,
				       info->server_data.address);
	else {
		info->client_data.fd =
			connect_to_socket(&info->client_data.address,
					  config.listen.s_addr ? &config.
					  listen : NULL, config.actvports);
	}
	if(info->client_data.fd == -1) {
		write_log(ERROR, "Failed to contact client data port");
		ssl_shutdown(&info->ssl_sd);
		rclose(&info->server_data.fd);
		return (-1);
	}
	sstr_empty(info->client_data.buf);
	fcntl(info->client_data.fd, F_SETFL, O_NONBLOCK);
	return (0);
}

/* ------------------------------------------------------------- **
** Forward as much data as possible from client-->server, and store
** the rest in server_data.buf. If maxrate is set then read at most
** 1/4 of a second's worth.
** ------------------------------------------------------------- */
void forwarddata2server()
{
	int i;

	i = sstr_append_read(info->client_data.fd, info->server_data.buf,
			     config.maxulrate ? config.maxulrate / 4 : 0);
	if(i < 1) {		/*Socket close or error */
		closecd();
		return;
	}
	if(config.maxulrate)
		bwlimit(i, config.maxulrate);
	if(info->state != UPLOAD)
		sstr_empty(info->server_data.buf);

	if(info->server_data.fd != -1)
		writebuf2server();
}

/* ------------------------------------------------------------- **
** Forward as much data as possible from server-->client, and store
** the rest in client_data.buf. If maxrate is set then read at most
** 1/4 of a second's worth.
**
** The vscan_inc_data(), cache_inc_data() order is because http
** caching uses cache_inc_data to strip http headers which must be
** done before data reaches the virus scanner, while local caching
** uses it to write file data to file which musn't be done until the
** vscan code has emptied the buffer. Without caching cache_inc_data
** has no effect so it makes no difference. Ugly I know, but I could
** think of no better way...
** ------------------------------------------------------------- */
void forwarddata2client()
{
	int i, dlrate;
	dlrate = info->cached ? config.cachedlrate : config.maxdlrate;
	dlrate = dlrate ? dlrate / 4 : 0;
	if(info->ssl_sd)
		i = ssl_append_read(info->ssl_sd,
				    info->client_data.buf, dlrate);
	else
		i = sstr_append_read(info->server_data.fd,
				     info->client_data.buf, dlrate);

	if(i < 1) {		/*Socket close or error */
		if(!vscan_switchover())
			closesd();
		return;
	}
	if(dlrate)
		bwlimit(i, dlrate);
	if(info->state != DOWNLOAD)
		sstr_empty(info->client_data.buf);

	if(config.cachemod && *config.cachemod == 'h') {	/*http caching */
		cache_inc_data(info->client_data.buf);
		vscan_inc(info->client_data.buf);
	} else {		/*local caching */
		vscan_inc(info->client_data.buf);
		cache_inc_data(info->client_data.buf);
	}
	if(info->client_data.fd != -1)
		writebuf2client();
}

/* ------------------------------------------------------------- **
**  Flush as much buffer as we can
** ------------------------------------------------------------- */
void writebuf2client()
{
	int i;

	i = sstr_write(info->client_data.fd, info->client_data.buf, 0);
	if(i == -1) {
		if(errno == EAGAIN)
			return;
		if(errno != EPIPE)
			debug_perr("writebuf2client()");
		closecd();
		return;
	}
	sstr_split(info->client_data.buf, NULL, 0, i);

	if(sstr_len(info->client_data.buf) == 0 && info->server_data.fd == -1)
		closecd();
	debug(".");
}

/* ------------------------------------------------------------- **
**  Flush as much buffer as we can
** ------------------------------------------------------------- */
void writebuf2server()
{
	int i;

	if(info->ssl_sd)
		i = ssl_write(info->ssl_sd, info->server_data.buf);
	else
		i = sstr_write(info->server_data.fd, info->server_data.buf,
			       0);
	if(i == -1) {
		if(errno == EAGAIN)
			return;
		if(errno != EPIPE)
			debug_perr("writebuf2server()");
		closesd();
		return;
	}
	sstr_split(info->server_data.buf, NULL, 0, i);

	if(sstr_len(info->server_data.buf) == 0 && info->client_data.fd == -1)
		closesd();
	debug(".");
}

/* ------------------------------------------------------------- **
** Called for all data transfers. If more bytes transferred since the
** last call than permitted in the elapsed time then pause. The value
** of last doesn't get changed between transfers even if there's a big
** pause in between, but this only means that the first BUF_LEN bytes
** of the new transfer get passed through immediately. Shouldn't be a
** problem. Maybe I can even call it a feature -- fast transfer of
** small files. ;)
**
** We sleep only once there is ALLOW_DISCREPANCY seconds worth of
** sleeping to do. If this is too small we are really inaccurate in
** our transfer rate - nanosleep() may have a granularity of around
** 10ms. If it is too big then the transfer speed will wax and wane a
** bit but be more accurate in the long run. Not sure what the best
** value is.
**
** bytes is no. of bytes waiting to be transferred. rate is bytes/sec
** ------------------------------------------------------------- */
#define ALLOW_DISCREPANCY 0.5
void bwlimit(int bytes, int rate)
{
#ifdef HAVE_NANOSLEEP
	static struct timeval last = { 0, 0 };
	static int bytecnt = 0;
	struct timeval now;
	double actualtime, mintime;

	if(!last.tv_sec) {
		gettimeofday(&last, NULL);
		return;
	}
	bytecnt += bytes;
	gettimeofday(&now, NULL);

	actualtime = (now.tv_sec - last.tv_sec) +
		(double) (now.tv_usec - last.tv_usec) / (double) 1000000;
	mintime = (double) bytecnt / (double) rate;

	if(actualtime < mintime) {
		struct timespec ts;
		double sleep_time = mintime - actualtime;

		if(sleep_time < ALLOW_DISCREPANCY)
			return;

		ts.tv_sec = (time_t) sleep_time;
		ts.tv_nsec = (long) ((sleep_time - ts.tv_sec) *
				     (double) 1000000000);
		nanosleep(&ts, &ts);

		gettimeofday(&last, NULL);
		bytecnt = 0;
		debug("|");
	} else {
		gettimeofday(&last, NULL);
		bytecnt = 0;
	}
#endif
}

/* ------------------------------------------------------------- **
** Close client data socket, and if there is nothing left to flush
** close the other one too. With downloads we only come here if the
** client aborted - we need to abort virus scanning.
** ------------------------------------------------------------- */
void closecd(void)
{
	write_log(VERBOSE, "Closing client data connection");
	info->state = NEITHER;
	rclose(&info->client_data.fd);
	if(sstr_len(info->server_data.buf) == 0) {
		write_log(VERBOSE, "Closing server data connection");
		ssl_shutdown(&info->ssl_sd);
		rclose(&info->server_data.fd);
		vscan_abort();
		cache_close_data();
	}
	xfer_log();
}

/* ------------------------------------------------------------- **
** Close data socket, and if there is nothing left to flush close the
** other one too.
** ------------------------------------------------------------- */
void closesd(void)
{
	write_log(VERBOSE, "Closing server data connection");

	ssl_shutdown(&info->ssl_sd);
	rclose(&info->server_data.fd);
	info->state = NEITHER;

	vscan_end();
	cache_close_data();

	if(sstr_len(info->client_data.buf) == 0) {
		write_log(VERBOSE, "Closing client data connection");
		rclose(&info->client_data.fd);
	}
	xfer_log();
}


syntax highlighted by Code2HTML, v. 0.9.1