/*
 *	binkleyforce -- unix FTN mailer project
 *	
 *	Copyright (c) 1998-2000 Alexander Belkin, 2:5020/1398.11
 *	
 *	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.
 *	
 *	$Id: prot_binkp.c,v 1.2 2001/08/30 21:31:19 kst Exp $
 */

#include "includes.h"
#include "confread.h"
#include "logger.h"
#include "util.h"
#include "io.h"
#include "session.h"
#include "outbound.h"
#include "prot_common.h"
#include "prot_binkp.h"

typedef enum {
	BPI_SendSysInfo,
	BPI_WaitADR,
	BPI_WaitPWD,
	BPI_Auth
} binkp_incoming_state;

typedef enum {
	BPO_SendSysInfo,
	BPO_WaitNUL,
	BPO_SendPWD,
	BPO_WaitADR,
	BPO_Auth,
	BPO_WaitOK
} binkp_outgoing_state;

#define GOTO(label,newrc)    { rc = (newrc); goto label; }

void binkp_process_NUL(s_binkp_sysinfo *remote_data, char *buffer)
{
	char *p, *q;
	
	if( strncmp(buffer, "SYS ", 4) == 0 )
		strnxcpy(remote_data->systname, buffer+4, sizeof(remote_data->systname));
	else if( strncmp(buffer, "ZYZ ", 4) == 0 )
		strnxcpy(remote_data->sysop, buffer+4, sizeof(remote_data->sysop));
	else if( strncmp(buffer, "LOC ", 4) == 0 )
		strnxcpy(remote_data->location, buffer+4, sizeof(remote_data->location));
	else if( strncmp(buffer, "PHN ", 4) == 0 )
		strnxcpy(remote_data->phone, buffer+4, sizeof(remote_data->phone));
	else if( strncmp(buffer, "NDL ", 4) == 0 )
		strnxcpy(remote_data->flags, buffer+4, sizeof(remote_data->flags));
	else if( strncmp(buffer, "TIME ", 5) == 0 )
		strnxcpy(remote_data->timestr, buffer+5, sizeof(remote_data->timestr));
	else if( strncmp(buffer, "OPT ", 4) == 0 )
	{
		if( *remote_data->opt )
		{
			strnxcat(remote_data->opt, " ", sizeof(remote_data->opt));
			strnxcat(remote_data->opt, buffer+4, sizeof(remote_data->opt));
		}
		else
			strnxcpy(remote_data->opt, buffer+4, sizeof(remote_data->opt));
		
		binkp_parse_options(remote_data, buffer+4);
	}
	else if( strncmp(buffer, "VER ", 4) == 0 )
	{
		/* <mailer> [<protocol>/<vermaj>.<vermin>] */
		if( (p = strchr(buffer+4, ' ')) )
		{
			strnxcpy(remote_data->progname, buffer+4,
					MIN(sizeof(remote_data->progname), p - (buffer+4) + 1));
			++p;
			if( (q = strchr(p, '/')) )
			{
				strnxcpy(remote_data->protname, p,
						MIN(sizeof(remote_data->protname), q - p + 1));
				sscanf(q+1, "%d.%d",
						&remote_data->majorver,
						&remote_data->minorver);
			}
		}
		else
			strnxcpy(remote_data->progname, buffer+4, sizeof(remote_data->progname));
	}
	else
		log("BinkP got invalid NUL: \"%s\"", string_printable(buffer));
}

void binkp_process_ADR(s_binkp_sysinfo *remote_data, char *buffer)
{
	s_faddr addr;
	char *p, *q;
	
	for( p = string_token(buffer, &q, NULL, 0); p;
	     p = string_token(NULL, &q, NULL, 0) )
	{
		if( ftn_addrparse(&addr, p, FALSE) )
			log("BinkP got unparsable address \"%s\"", string_printable(p));
		else
			session_addrs_add(&remote_data->addrs, &remote_data->anum, addr);
	}
}

int binkp_outgoing(s_binkp_sysinfo *local_data, s_binkp_sysinfo *remote_data)
{
	s_bpinfo bpi;
	binkp_outgoing_state binkp_state = BPO_SendSysInfo;
	int rc = HRC_OK;
	int recv_rc = 0;
	int send_rc = 0;
	bool send_ready = FALSE;
	bool recv_ready = FALSE;
	
	binkp_init_bpinfo(&bpi);
	
	while(1)
	{
		switch(binkp_state) {
		case BPO_SendSysInfo:
			binkp_queue_sysinfo(&bpi, local_data);
			binkp_state = BPO_WaitNUL;
			break;

		case BPO_SendPWD:
			if( *local_data->passwd == '\0' )
			{
				binkp_queuemsg(&bpi, BPMSG_PWD, NULL, "-");
			}
			else if( remote_data->options & BINKP_OPT_MD5 )
			{
				char digest_bin[16];
				char digest_hex[33];
				
				md5_cram_get(local_data->passwd, remote_data->challenge,
						remote_data->challenge_length, digest_bin);
			
				/* Encode digest to the hex string */
				string_bin_to_hex(digest_hex, digest_bin, 16);
				
				binkp_queuemsg(&bpi, BPMSG_PWD, "CRAM-MD5-", digest_hex);
			}
			else
				binkp_queuemsg(&bpi, BPMSG_PWD, NULL, local_data->passwd);

			binkp_state = BPO_WaitADR;
			break;
			
		case BPO_Auth:
			/* Set remote password same as local */
			strncpy(remote_data->passwd, local_data->passwd, BINKP_MAXPASSWD);
			remote_data->passwd[BINKP_MAXPASSWD] = '\0';

			if( !remote_data->anum )
			{
				binkp_queuemsg(&bpi, BPMSG_BSY, NULL, "No addresses was presented");
				GOTO(Exit, HRC_BUSY);
			}
			else if( session_addrs_check_genuine(remote_data->addrs, remote_data->anum,
			                             state.node.addr) )
			{
				binkp_queuemsg(&bpi, BPMSG_ERR, NULL, "Sorry, you are not who I need");
				GOTO(Exit, HRC_NO_ADDRESS);
			}
			else if( session_addrs_check(remote_data->addrs, remote_data->anum,
			                             remote_data->passwd, NULL, 0) )
			{
				binkp_queuemsg(&bpi, BPMSG_ERR, NULL, "Security violation");
				GOTO(Exit, HRC_BAD_PASSWD);
			}
			else if( session_addrs_lock(remote_data->addrs, remote_data->anum) )
			{
				binkp_queuemsg(&bpi, BPMSG_BSY, NULL, "All addresses are busy");
				GOTO(Exit, HRC_BUSY);
			}
			binkp_state = BPO_WaitOK;
			break;

		default:
			/* Avoid warnings */
		}
		
		/*
		 * Receive/Send next data block
		 */
		send_ready = recv_ready = FALSE;
		
		if( tty_select(&recv_ready, (bpi.opos || bpi.n_msgs) ?
		               &send_ready : NULL, bpi.timeout) < 0 )
			GOTO(Abort, HRC_OTHER_ERR);
		
		recv_rc = BPMSG_NONE;
		send_rc = 0;
		
		if( recv_ready && (recv_rc = binkp_recv(&bpi)) == BPMSG_EXIT )
			GOTO(Abort, HRC_OTHER_ERR);
		
		if( send_ready && (send_rc = binkp_send(&bpi)) < 0 )
			GOTO(Abort, HRC_OTHER_ERR);

		/*
		 * Handle received message
		 */
		switch(recv_rc) {
		case BPMSG_NONE:
			break;

		case BPMSG_NUL:
			if( binkp_state == BPO_WaitNUL || binkp_state == BPO_WaitADR )
			{
				binkp_process_NUL(remote_data, bpi.ibuf+1);
				if( binkp_state == BPO_WaitNUL )
					binkp_state = BPO_SendPWD;
			}
			break;
		
		case BPMSG_ADR:
			if( binkp_state == BPO_WaitADR )
			{
				binkp_process_ADR(remote_data, bpi.ibuf+1);
				binkp_state = BPO_Auth;
			}
			break;
		
		case BPMSG_OK:
			if( binkp_state == BPO_WaitOK )
				GOTO(Exit, HRC_OK);
			break;
		
		case BPMSG_ERR:
			log("BinkP error: \"%s\"", string_printable(bpi.ibuf+1));
			GOTO(Abort, HRC_FATAL_ERR);
			
		case BPMSG_BSY:
			log("BinkP busy: \"%s\"", string_printable(bpi.ibuf+1));
			GOTO(Abort, HRC_TEMP_ERR);
		}
	}

Exit:
	if( binkp_flush_queue(&bpi, bpi.timeout) && rc == HRC_OK )
		rc = HRC_OTHER_ERR;

Abort:
	binkp_deinit_bpinfo(&bpi);
	
	return rc;
}


int binkp_auth_incoming(s_binkp_sysinfo *remote_data)
{
	if( remote_data->challenge_length > 0
	 && strncmp(remote_data->passwd, "CRAM-MD5-", 9) == 0 )
	{
		return session_addrs_check(remote_data->addrs,
		                           remote_data->anum,
		                           remote_data->passwd + 9,
		                           remote_data->challenge,
		                           remote_data->challenge_length);
	}
	
	return session_addrs_check(remote_data->addrs, remote_data->anum,
	                           remote_data->passwd, NULL, 0);
}

int binkp_incoming(s_binkp_sysinfo *local_data, s_binkp_sysinfo *remote_data)
{
	s_bpinfo bpi;
	binkp_incoming_state binkp_state = BPI_SendSysInfo;
	int rc = HRC_OK;
	int recv_rc = 0;
	int send_rc = 0;
	bool send_ready = FALSE;
	bool recv_ready = FALSE;
	
	binkp_init_bpinfo(&bpi);
	
	while(1)
	{
		switch(binkp_state) {
		case BPI_SendSysInfo:
			binkp_queue_sysinfo(&bpi, local_data);
			binkp_state = BPI_WaitADR;
			break;
		
		case BPI_Auth:
			/* Set challenge string same as local */
			memcpy(remote_data->challenge, local_data->challenge,
					sizeof(remote_data->challenge));
			remote_data->challenge_length = local_data->challenge_length;
			
			/* Do authorization */
			if( !remote_data->anum )
			{
				binkp_queuemsg(&bpi, BPMSG_BSY, NULL, "No addresses was presented");
				GOTO(Exit, HRC_BUSY);
			}
			else if( binkp_auth_incoming(remote_data) )
			{
				binkp_queuemsg(&bpi, BPMSG_ERR, NULL, "Security violation");
				GOTO(Exit, HRC_BAD_PASSWD);
			}
			else if( session_addrs_lock(remote_data->addrs, remote_data->anum) )
			{
				binkp_queuemsg(&bpi, BPMSG_BSY, NULL, "All addresses are busy");
				GOTO(Exit, HRC_BUSY);
			}
			else
			{
				binkp_queuemsg(&bpi, BPMSG_OK, NULL, NULL);
				GOTO(Exit, HRC_OK);
			}
			break;

		default:
			/* Avoid warnings */
		}
		
		/*
		 * Receive/Send next data block
		 */
		send_ready = recv_ready = FALSE;
		
		if( tty_select(&recv_ready, (bpi.opos || bpi.n_msgs) ?
		               &send_ready : NULL, bpi.timeout) < 0 )
			GOTO(Abort, HRC_OTHER_ERR);
		
		recv_rc = BPMSG_NONE;
		send_rc = 0;
		
		if( recv_ready && (recv_rc = binkp_recv(&bpi)) == BPMSG_EXIT )
			GOTO(Abort, HRC_OTHER_ERR);
		
		if( send_ready && (send_rc = binkp_send(&bpi)) < 0 )
			GOTO(Abort, HRC_OTHER_ERR);

		/*
		 * Handle received message
		 */
		switch(recv_rc) {
		case BPMSG_NONE:
			break;

		case BPMSG_NUL:
			if( binkp_state == BPI_WaitADR )
				binkp_process_NUL(remote_data, bpi.ibuf+1);
			break;
		
		case BPMSG_ADR:
			if( binkp_state == BPI_WaitADR )
			{
				binkp_process_ADR(remote_data, bpi.ibuf+1);
				binkp_state = BPI_WaitPWD;
			}
			break;
		
		case BPMSG_PWD:
			if( binkp_state == BPI_WaitPWD )
			{
				strnxcpy(remote_data->passwd, bpi.ibuf+1, sizeof(remote_data->passwd));
				binkp_state = BPI_Auth;
			}
			break;
		
		case BPMSG_ERR:
			log("BinkP error: \"%s\"", string_printable(bpi.ibuf+1));
			GOTO(Abort, HRC_FATAL_ERR);
			
		case BPMSG_BSY:
			log("BinkP busy: \"%s\"", string_printable(bpi.ibuf+1));
			GOTO(Abort, HRC_TEMP_ERR);
		}
	}

Exit:
	if( binkp_flush_queue(&bpi, bpi.timeout) && rc == HRC_OK )
		rc = HRC_OTHER_ERR;

Abort:
	binkp_deinit_bpinfo(&bpi);
	
	return rc;
}

int binkp_transfer(s_protinfo *pi)
{
	int  i, n, rc = PRC_NOERROR;
	bool recv_ready = FALSE;
	bool send_ready = FALSE;
	bool sent_EOB = FALSE;
	bool rcvd_EOB = FALSE;
	bool send_file = FALSE;
	bool recv_file = FALSE;
	bool wait_got = FALSE;
	bool nofiles = FALSE;
	int  recv_rc = 0;
	int  send_rc = 0;
	char *fname = NULL;
	size_t fsize = 0;
	time_t ftime = 0;
	size_t foffs = 0;
	s_bpinfo bpi;
	
	binkp_init_bpinfo(&bpi);
		
	while( !sent_EOB || !rcvd_EOB )
	{
		if( !send_file && !sent_EOB && bpi.opos == 0 && bpi.n_msgs == 0 )
		{
			if( !p_tx_fopen(pi) )
			{
				send_file = TRUE;
				binkp_queuemsgf(&bpi, BPMSG_FILE, "%s %ld %ld 0",
					pi->send->net_name, (long)pi->send->bytes_total,
					(long)pi->send->mod_time);
			}
			else
				/* No more files */
				nofiles = TRUE;
		}
		
		if( send_file && bpi.opos == 0 && bpi.n_msgs == 0 )
		{
			if( (n = p_tx_readfile(bpi.obuf + BINKP_BLK_HDRSIZE,
			                       4096, pi)) < 0 )
			{
				p_tx_fclose(pi);
				send_file = FALSE;
			}
			else
			{
				binkp_puthdr(bpi.obuf, (unsigned)(n & 0x7fff));
				bpi.opos = n + BINKP_BLK_HDRSIZE;
				pi->send->bytes_sent += n;
				if( pi->send->eofseen )
				{
					wait_got = TRUE; send_file = FALSE;
					pi->send->status = FSTAT_WAITACK;
				}
			}
		}

		if( nofiles && !wait_got && !sent_EOB )
		{
			sent_EOB = TRUE;
			binkp_queuemsg(&bpi, BPMSG_EOB, NULL, NULL);
		}

		recv_ready = send_ready = FALSE;
		if( tty_select(&recv_ready, (bpi.opos || bpi.n_msgs) ?
		               &send_ready : NULL, bpi.timeout) < 0 )
			gotoexit(PRC_ERROR);
		
		recv_rc = BPMSG_NONE;
		send_rc = 0;
		
		if( recv_ready && (recv_rc = binkp_recv(&bpi)) == BPMSG_EXIT )
			gotoexit(PRC_ERROR);
		if( send_ready && (send_rc = binkp_send(&bpi)) < 0 )
			gotoexit(PRC_ERROR);
	
		switch(recv_rc) {
		case BPMSG_NONE:
			break;
		case BPMSG_DATA: /* Got new data block */
			if( recv_file )
			{
				if( (n = p_rx_writefile(bpi.ibuf, bpi.isize, pi)) < 0 )
				{
					/* error writing file */
					if( n == -2 )
					{
						binkp_queuemsgf(&bpi, BPMSG_GOT, "%s %ld %ld",
							pi->recv->net_name, (long)pi->recv->bytes_total,
							(long)pi->recv->mod_time);
					}
					else
					{
						binkp_queuemsgf(&bpi, BPMSG_SKIP, "%s %ld %ld",
							pi->recv->net_name, (long)pi->recv->bytes_total,
							(long)pi->recv->mod_time);
					}
					recv_file = FALSE;
					p_rx_fclose(pi);
				}
				else
				{
					pi->recv->bytes_received += bpi.isize;

					/* Was it the last data block? */
					if( pi->recv->bytes_received > pi->recv->bytes_total )
					{
						log("binkp got too many data (%ld, %ld expected)",
							(long)pi->recv->bytes_received,
							(long)pi->recv->bytes_total);

						recv_file = FALSE;
						pi->recv->status = FSTAT_REFUSED;
						(void)p_rx_fclose(pi);
						
						binkp_queuemsgf(&bpi, BPMSG_SKIP, "%s %ld %ld",
							pi->recv->net_name, (long)pi->recv->bytes_total,
							(long)pi->recv->mod_time);
					}
					else if( pi->recv->bytes_received == pi->recv->bytes_total )
					{
						recv_file = FALSE;
						pi->recv->status = FSTAT_SUCCESS;
						if( !p_rx_fclose(pi) )
						{
							binkp_queuemsgf(&bpi, BPMSG_GOT, "%s %ld %ld",
								pi->recv->net_name, (long)pi->recv->bytes_total,
								(long)pi->recv->mod_time);
						}
						else
						{
							binkp_queuemsgf(&bpi, BPMSG_SKIP, "%s %ld %ld",
								pi->recv->net_name, (long)pi->recv->bytes_total,
								(long)pi->recv->mod_time);
						}
					}
				}
			}
#ifdef DEBUG
			else
				DEB((D_PROT, "ignore received data block"));
#endif
			break;
			
		case BPMSG_FILE:
			if( recv_file )
				{ p_rx_fclose(pi); recv_file = FALSE; }
			
			if( binkp_parsfinfo(bpi.ibuf+1, &fname,
			    &fsize, &ftime, &foffs) )
			{
				binkp_queuemsg(&bpi, BPMSG_ERR, "FILE: ", "unparsable arguments");
				goto FinishSession;
			}
			
			if( pi->recv && !p_compfinfo(pi->recv, fname, fsize, ftime)
			 && pi->recv->bytes_skipped == foffs )
			{
				recv_file = TRUE;
				break;
			}
			
			switch(p_rx_fopen(pi, fname, fsize, ftime, 0)) {
			case 0:
				if( pi->recv->bytes_skipped == 0 )
				{
					recv_file = TRUE;
					break;
				}
				binkp_queuemsgf(&bpi, BPMSG_GET, "%s %ld %ld %ld",
					pi->recv->net_name, (long)pi->recv->bytes_total,
					(long)pi->recv->mod_time,
					(long)pi->recv->bytes_skipped);
				break;
			case 1:
				/* SKIP (non-destructive) */
				binkp_queuemsgf(&bpi, BPMSG_SKIP, "%s %ld %ld",
					pi->recv->net_name, (long)pi->recv->bytes_total,
					(long)pi->recv->mod_time);
				break;
			case 2:
				/* SKIP (destructive) */
				binkp_queuemsgf(&bpi, BPMSG_GOT, "%s %ld %ld",
					pi->recv->net_name, (long)pi->recv->bytes_total,
					(long)pi->recv->mod_time);
				break;
			default:
				ASSERT_MSG();
			}
			break;
			
		case BPMSG_EOB: /* End Of Batch */
			if( recv_file )
			{
				p_rx_fclose(pi);
				recv_file = FALSE;
			}
			rcvd_EOB = TRUE;
			break;
			
		case BPMSG_GOT:
		case BPMSG_SKIP:
			if( binkp_parsfinfo(bpi.ibuf+1, &fname, &fsize, &ftime, NULL) == 0 )
			{
				if( send_file && !p_compfinfo(pi->send, fname, fsize, ftime) )
				{
					/* We got GOT/SKIP for current file! */
					if( recv_rc == BPMSG_GOT )
						pi->send->status = FSTAT_SKIPPED;
					else
						pi->send->status = FSTAT_REFUSED;
					p_tx_fclose(pi);
					send_file = FALSE;
					break;
				}
				
				wait_got = FALSE;
				
				for( i = 0; i < pi->n_sentfiles; i++ )
				{
					if( pi->sentfiles[i].status == FSTAT_WAITACK
					 && !p_compfinfo(&pi->sentfiles[i], fname, fsize, ftime) )
					{
						s_finfo *tmp = pi->send;
						
						pi->send = &pi->sentfiles[i];
						if( recv_rc == BPMSG_GOT )
							pi->send->status = FSTAT_SUCCESS;
						else
							pi->send->status = FSTAT_REFUSED;
						p_tx_fclose(pi);
						
						pi->send = tmp;
						break;
					}
					else if( pi->sentfiles[i].status == FSTAT_WAITACK )
						wait_got = TRUE;
				}

				/*
				 * Check, maybe there are files still
				 * waiting for the GOT/SKIP acknwoledge
				 */
				while( i < pi->n_sentfiles && !wait_got )
				{
					if( pi->sentfiles[i].status == FSTAT_WAITACK )
						wait_got = TRUE;
					++i;
				}
			}
			break;
			
		case BPMSG_ERR:
			log("remote report error: \"%s\"", bpi.ibuf+1);
			gotoexit(PRC_ERROR);
			break;
			
		case BPMSG_BSY:
			log("remote busy error: \"%s\"", bpi.ibuf+1);
			gotoexit(PRC_ERROR);
			break;
			
		case BPMSG_GET:
			if( binkp_parsfinfo(bpi.ibuf+1, &fname, &fsize, &ftime, &foffs) == 0 )
			{
				if( send_file && !p_compfinfo(pi->send, fname, fsize, ftime) )
				{
					if( fseek(pi->send->fp, foffs, SEEK_SET) == -1 )
					{
						log("cannot send file from requested offset %ld", (long)foffs);
						p_tx_fclose(pi);
						send_file = FALSE;
					}
					else
					{
						log("sending \"%s\" from %ld offset",
							pi->send->fname, (long)foffs);
						pi->send->bytes_skipped = foffs;
						pi->send->bytes_sent = foffs;
						binkp_queuemsgf(&bpi, BPMSG_FILE, "%s %ld %ld %ld",
							pi->send->net_name, (long)pi->send->bytes_total,
							(long)pi->send->mod_time, (long)foffs);
					}
				}
			}
			break;
			
		default:
			log("binkp got unhandled msg #%d", recv_rc);
			break;
		}
	} /* end of while( !sent_EOB || !rcvd_EOB ) */
	
FinishSession:
	if( binkp_flush_queue(&bpi, bpi.timeout) && rc == PRC_NOERROR )
		rc = PRC_ERROR;
	
exit:
	if( pi->send && pi->send->fp ) p_tx_fclose(pi);
	if( pi->recv && pi->recv->fp ) p_rx_fclose(pi);

	binkp_deinit_bpinfo(&bpi);
	
	DEB((D_PROT, "binkp: BINKP exit = %d", rc));
	
	return rc;
}

