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

    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

  control.c -- forward and parse the control stream.
  
  ***************************************/


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

#include "control.h"
#include "common.h"
#include "ftp-cmds.h"
#include "data.h"
#include "ntp.h"
#include "ccp.h"
#include "vscan.h"
#include "cache.h"
#include "os.h"
#include "ssl.h"

int extract_clientcmd(sstr * buf, sstr ** cmd, sstr ** arg);
int extract_servercmd(sstr * buf, int *code, sstr ** msg);
static session_info *init_info(int fd, struct sockaddr_in source);
static void connect_to_server(void);

/* ------------------------------------------------------------- **
**  Sets up the control connection to the server and initialises
**  session info.
** ------------------------------------------------------------- */
void init_session(int fd, struct sockaddr_in source)
{
	info = init_info(fd, source);

	write_log(INFO, "Connect from %s",
		  sstr_buf(addr2name(info->client_control.address.sin_addr)));

	/*FIXME have a login function which deals with ntp and cache */
	ccp_changedest();
	ntp_changedest();
	info->final_server_address = info->server_control.address;

	write_log(INFO, "... to %s(%s)",
		  inet_ntoa(info->final_server_address.sin_addr),
		  sstr_buf(info->server_name));

#ifdef ENABLE_CHANGEPROC
	set_proc_title("frox: %s <-> %s",
		       inet_ntoa(info->client_control.address.sin_addr),
		       inet_ntoa(info->final_server_address.sin_addr));
#endif
	connect_to_server();

	ntp_senduser();

	run_proxy();
}

static void connect_to_server(void)
{
	/*Check for loops. Won't work if Listen undefined */
	if(info->server_control.address.sin_addr.s_addr
	   == config.listen_address.sin_addr.s_addr &&
	   info->server_control.address.sin_port
	   == config.listen_address.sin_port)
		die(ERROR, "Attempt to connect to self. "
		    "Do you need to set DoNTP to yes?",
		    421, "Proxy tried to loop. Closing connection", 0);
	if(info->server_control.address.sin_addr.s_addr == 0) {
		if(!config.ntp)
			die(ERROR,
			    "Frox unable to determine destination address. "
			    "Do you need to set DoNTP to yes?",
			    501, "Unable to contact server", 0);
		else if(config.ntpdest.sin_addr.s_addr)
			die(ERROR,
			    "Frox unable to determine destination address. "
			    "Try commenting out NTPAddress",
			    501, "Unable to contact server", 0);
		else
			die(ERROR,
			    "Frox unable to determine detination address. "
			    "See FAQ for troubleshooting.", 501,
			    "Unable to contact server", 0);
	}

	resolve_addr(&info->final_server_address.sin_addr, info->server_name);

	if(!config_connectionok(&info->client_control.address,
				&info->final_server_address,
				info->username ? sstr_buf(info->
							  username) : 0))
		die(ATTACK, "Denied by ACLs.", 501, "Connection denied. Bye",
		    0);

	if(config.ftpproxy.sin_addr.s_addr)
		info->server_control.address = config.ftpproxy;

	write_log(VERBOSE, "Connecting to server...");

	info->server_control.fd =
		connect_to_socket(&info->server_control.address,
				  &config.tcpoutaddr, config.contports);

	if(info->server_control.fd == -1)
		die(ERROR, "Connection closed -- unable to contact server",
		    501, "Proxy unable to contact ftp server", 0);

	write_log(VERBOSE, "     OK");

	if(config.loglevel >= VERBOSE) {	/*Save the overhead of DNS lookups */
		write_log(VERBOSE, "Apparent address = %s",
			  sstr_buf(addr2name
				   (info->apparent_server_address.sin_addr)));
		write_log(VERBOSE, "Real address = %s",
			  sstr_buf(addr2name
				   (info->final_server_address.sin_addr)));
		write_log(VERBOSE, "Proxy address = %s",
			  sstr_buf(addr2name
				   (info->server_control.address.sin_addr)));
	}
	ssl_init();
}

/* ------------------------------------------------------------- **
**  Allocate and initialise session_info structure. Also determine
**  destination address if possible.
**  ------------------------------------------------------------- */
static session_info *init_info(int fd, struct sockaddr_in source)
{
	session_info *i;
	i = (session_info *) malloc(sizeof(session_info));
	if(i == NULL)
		die(ERROR, "Malloc failed", 0, NULL, -1);
	memset(i, 0, sizeof(session_info));

	i->server_listen = i->client_listen = -1;
	i->client_data.fd = i->server_data.fd = -1;
	i->server_control.fd = -1;

	i->server_control.buf = sstr_init(BUF_LEN);
	i->client_control.buf = sstr_init(BUF_LEN);
	i->server_data.buf = sstr_init(DATA_BUF_LEN);
	i->client_data.buf = sstr_init(DATA_BUF_LEN);
	i->last_command = sstr_init(0);
	i->server_name = sstr_init(0);
	i->username = sstr_init(0);
	i->passwd = sstr_init(0);
	i->strictpath = sstr_init(0);
	i->cmd_arrays[CACHE_CMDS] = NULL;
	i->cmd_arrays[FTP_CMDS] = ftp_cmds;

	i->filename = sstr_init(0);

	if(config.apconv)
		i->mode = APCONV;
	else
		i->mode = ACTIVE;
	i->state = NEITHER;
	i->greeting = AWAITED;

	i->client_control.address = source;
	i->client_control.fd = fd;

	get_orig_dest(fd, &i->server_control.address);
	i->apparent_server_address = i->server_control.address;

	return i;
}

/* ------------------------------------------------------------- **
**  The main loop for each session.
** ------------------------------------------------------------- */
void run_proxy()
{
	int i;

	do {
		i = get_control_line(GET_BOTH);
		if(i & GET_CLNT)
			client_control_forward();
		if(i & GET_SRVR)
			server_control_forward();
	} while(TRUE);
}

/* ------------------------------------------------------------- **
** Forward buffered control stream data from client -> server.
** ------------------------------------------------------------- */
void client_control_forward()
{
	int i;
	sstr *cmd, *arg;

	while((i =
	       extract_clientcmd(info->client_control.buf, &cmd, &arg)) == 0)
		parse_client_cmd(cmd, arg);

	if(i < 0)
		die(ATTACK,
		    "Client is sending us a badly formed control stream.",
		    421, "You are sending me rubbish. Goodbye.", -1);
}

/* ------------------------------------------------------------- **
** Forward buffered control stream data from server -> client.
** ------------------------------------------------------------- */
void server_control_forward()
{
	int i, code;
	sstr *arg;

	while((i =
	       extract_servercmd(info->server_control.buf, &code, &arg)) == 0)
		if(!ssl_parsed_reply(code, arg) &&	/*Always returns 0 */
		   !cache_parsed_reply(code, arg) &&
		   !vscan_parsed_reply(code, arg))
			send_message(code, arg);

	if(i < 0)
		die(ATTACK,
		    "Server is sending us a badly formed control stream.",
		    421,
		    "FTP server is sending you rubbish! Closing connection.",
		    -1);
}

/* ------------------------------------------------------------- **
** Work through the arrays of commands to find a matching function.
** Currently there are only two arrays - one with specific functions 
** for during caching, and one with the defaults. An empty entry in
** the list should match all commands.
** ------------------------------------------------------------- */
void parse_client_cmd(sstr * cmd, sstr * arg)
{
	struct cmd_struct *p;
	int i;

	if(!ccp_allowcmd(cmd, arg))
		return;
	sstr_cpy(info->last_command, cmd);

	for(i = 0; i < NUM_CMD_ARRAYS; i++) {
		if(!info->cmd_arrays[i])
			continue;
		for(p = info->cmd_arrays[i]; p && p->cmd; p++) {
			if(!*p->name || !sstr_casecmp2(cmd, p->name)) {
				p->cmd(cmd, arg);
				return;
			}
		}
	}
	write_log(ERROR, "Command %s not implemented", sstr_buf(cmd));
	send_cmessage(502, "Command not implemented.");
}

/* ------------------------------------------------------------- **
** Get one command from client.
** ------------------------------------------------------------- */
void get_command(sstr ** cmd, sstr ** arg)
{
	int i;

	while((i =
	       extract_clientcmd(info->client_control.buf, cmd, arg)) == 1)
		if(get_control_line(GET_CLNT) <= 0)
			die(ERROR, "Arrrghh. Shouldn't be here", 0, NULL, -1);
}

/* ------------------------------------------------------------- **
** Get one reply from server. Ignore multi-line replies.
** ------------------------------------------------------------- */
void get_message(int *code, sstr ** msg)
{
	do {
		switch (extract_servercmd
			(info->server_control.buf, code, msg)) {
		case -1:
			die(ATTACK,
			    "Server is sending us a badly formed control stream.",
			    421, "FTP server is sending you rubbish!"
			    "Closing connection.", -1);
		case 0:
#ifdef DEBUG
			fprintf(stderr, "  s: \033[32m%d %s\033[37m\n",
				*code, msg ? sstr_buf(*msg) : "");
#endif
			if(*code > 0)
				return;
			continue;
		case 1:
			if(get_control_line(GET_SRVR) <= 0)
				die(ERROR, "Arrghh - shoudln't be here",
				    0, NULL, -1);
		}
	} while(TRUE);
}


void send_cmessage(int code, const char *msg)
{
	sstr *smsg;
	smsg = sstr_dup2(msg);

	send_message(code, smsg);

	sstr_free(smsg);
}

void send_ccommand(const char *cmd, const char *arg)
{
	sstr *scmd, *sarg;
	scmd = sstr_dup2(cmd);
	sarg = sstr_dup2(arg);

	send_command(scmd, sarg);

	sstr_free(scmd);
	sstr_free(sarg);
}

/* ------------------------------------------------------------- **
** Send command to server 
** ------------------------------------------------------------- */
void send_command(sstr * cmd, sstr * arg)
{
	sstr *buf;
	buf = sstr_init(MAX_LINE_LEN + 10);

	sstr_cat(buf, cmd);
	if(sstr_len(arg) != 0) {
		sstr_ncat2(buf, " ", 1);
		sstr_cat(buf, arg);
	}
	sstr_ncat2(buf, "\r\n", 2);

#ifdef DEBUG
	fprintf(stderr, "  C: \033[31m%s\033[37m", sstr_buf(buf));
#else
	write_log(VERBOSE, "  C: %s", sstr_buf(buf));
#endif
	if(info->ssl_sc)
		ssl_write(info->ssl_sc, buf);
	else
		sstr_write(info->server_control.fd, buf, 0);

	sstr_free(buf);
}

/* ------------------------------------------------------------- **
** Send message to client
** ------------------------------------------------------------- */
void send_message(int code, sstr * msg)
{
	sstr *buf;
	if(!ccp_allowmsg(&code, msg))
		return;

	buf = sstr_init(MAX_LINE_LEN + 10);

	if(code != 0)
		sstr_apprintf(buf, "%d%c", abs(code), code > 0 ? ' ' : '-');
	sstr_cat(buf, msg);
	sstr_ncat2(buf, "\r\n", 2);

#ifdef DEBUG
	fprintf(stderr, "  S: \033[34m%s\033[37m", sstr_buf(buf));
#else
	write_log(VERBOSE, "  S: %s", sstr_buf(buf));
#endif
	sstr_write(info->client_control.fd, buf, 0);
	sstr_free(buf);
}

int read_srvrctrl_data(void)
{
	if(info->ssl_sc)
		return ssl_append_read(info->ssl_sc,
				       info->server_control.buf, 0);
	else
		return sstr_append_read(info->server_control.fd,
					info->server_control.buf, 0);
}

/* ------------------------------------------------------------- **
** Central select bit. Deals with data connection
** forwarding/listening, and quits on ctrl connection close. Once
** there is a complete line read from one of the control connctions
** specified in "which" (GET_SRVR, GET_CTRL, or GET_SRVR|GET_CTRL) we
** return.
**
** The line read into the control connection buffer on function return
** contains a newline (\n), and is NULL terminated at some point
** beyond that. No other checking has been done on it.
**
** Return value is one of GET_SRVR, GET_CTRL or GET_SRVR|GET_CTRL.
** ------------------------------------------------------------- */
int get_control_line(int which)
{
	int ret = 0, i;
	fd_set reads, writes;

	do {
		i = setup_fds(&reads, &writes);
		alarm(config.timeout);
		if(select(i + 1, &reads, &writes, NULL, NULL) == -1) {
			if(errno == EINTR)
				continue;
			debug_perr("select");
			die(0, NULL, 0, NULL, -1);
		}

		do_dataforward(&reads, &writes);

		if(FD_ISSET(info->client_control.fd, &reads)) {
			i = sstr_append_read(info->client_control.fd,
					     info->client_control.buf, 0);
			if(i == 0)
				die(INFO, "Client closed connection",
				    0, NULL, 0);
			if(i < 0)
				die(ATTACK,
				    "Client flooding control connection", 421,
				    "You're sending rubbish. Goodbye", -1);
			if(sstr_hasline(info->client_control.buf))
				ret |= GET_CLNT;
		}

		if(info->server_control.fd != -1 &&
		   FD_ISSET(info->server_control.fd, &reads)) {
			i = read_srvrctrl_data();
			if(i == 0) {
				if(!cache_transferring())
					die(ERROR,
					    "Server closed the control connection",
					    0, NULL, 0);
				else {
					rclose(&info->server_control.fd);
					write_log(INFO,
						  "Server closed connection. Keeping going until cache done");
				}
			}
			if(i < 0)
				die(ATTACK,
				    "Server flooding the control connection",
				    421, "Server is sending rubbish."
				    "Closing connection", -1);
			if(sstr_hasline(info->server_control.buf))
				ret |= GET_SRVR;
		}

	} while(!(ret & which));
	return (ret);
}

/***************************************************************************
 *
 * Functions which read the raw control stream --- be careful
 *
 **************************************************************************/

/* ------------------------------------------------------------- **
** Removes one line of control stream from buf (up to '\r\n'). <= 5
** chars are returned in cmd (up to 4 + \0), and <= MAX_LINE_LEN in
** arg. Must accept any NULL terminated buf and give sane output.
**  
** returns 0 on success, 1 if there is not a complete line in
** buf, and -X on non-sane buf. contents of buf unchanged on
** return(1), undefined on return(-X)
** ------------------------------------------------------------- */
int extract_clientcmd(sstr * buf, sstr ** pcmd, sstr ** parg)
{
	static sstr *cmd = NULL, *arg = NULL;

	if(!cmd)
		cmd = sstr_init(4);
	if(!arg)
		arg = sstr_init(MAX_LINE_LEN);
	if(pcmd)
		*pcmd = cmd;
	if(parg)
		*parg = arg;

	if(!sstr_hasline(buf))
		return (1);
	switch (sstr_token(buf, cmd, " \t\n\r", 0)) {
	case -1:		/*Token doesn't fit in cmd */
		return (-1);
	case ' ':
	case '\t':
		sstr_token(buf, arg, "\r\n", 0);
		break;
	default:		/*end of line */
		sstr_empty(arg);
	}

	if(!config.nonasciiok)
		sstr_makeprintable(arg, '?');

	return (0);
}

/* ------------------------------------------------------------- **
** As extract_clientcmd, but returns the code as an int.
** If code==0 then we are in a multiline. code<0 means we are
** starting a multiline. 
** ------------------------------------------------------------- */
int extract_servercmd(sstr * buf, int *code, sstr ** pmsg)
{
	static int multiline = 0;
	static sstr *scode = NULL, *msg = NULL;

	if(!scode)
		scode = sstr_init(4);
	if(!msg)
		msg = sstr_init(MAX_LINE_LEN);
	if(pmsg)
		*pmsg = msg;

	if(!sstr_hasline(buf))
		return (1);

	if(sstr_token(buf, msg, "\r\n", 0) == -1)
		return (-1);

	if(!multiline ||
	   (sstr_atoi(msg) == multiline && sstr_getchar(msg, 3) == ' ')) {
		multiline = 0;

		if(sstr_split(msg, scode, 0, 4) == -1)
			return (-1);
		*code = sstr_atoi(scode);

		switch (sstr_getchar(scode, 3)) {
		case '-':
			multiline = *code;
			*code = -*code;
		case ' ':
			break;
		default:
			return (-1);
		}
	} else {
		*code = 0;
	}

	if(!config.nonasciiok)
		sstr_makeprintable(msg, '?');

	return (0);
}


syntax highlighted by Code2HTML, v. 0.9.1