/* FTP client (interactive user) code */
#include <stdio.h>
#include "global.h"
#include "mbuf.h"
#include "netuser.h"
#include "icmp.h"
#include "timer.h"
#include "tcp.h"
#include "ftp.h"
#include "session.h"
#include "cmdparse.h"
#ifdef ATARI_ST
# ifdef __TURBOC__
#  include <tos.h>
# else
#  include <osbind.h>
# endif
#endif

extern struct session *current;
extern char nospace[];
extern char badhost[];
static char notsess[] = "Not an FTP session!\n";
static char cantwrite[] = "Can't write %s\n";
static char cantread[] = "Can't read %s\n";

#if (defined(MSC) || (defined(ATARI_ST) && defined(__TURBOC__)))
static int sndftpmsg(struct ftp *ftp,char *fmt,...);
#endif

int donothing(),doftpcd(),dolist(),doget(),dols(),doput(),dotype(),doabort(),
	domkdir(),dormdir(),dotput();

struct cmds ftpabort[] = {
	"",		donothing,	0,	NULLCHAR,		NULLCHAR,
	"abort",	doabort,	0,	NULLCHAR,		NULLCHAR,
	NULLCHAR,	NULLFP,		0,	"Only valid command is \"abort\"", NULLCHAR
};

struct cmds ftpcmds[] = {
	"",		donothing,	0,	NULLCHAR,		NULLCHAR,
	"cd",		doftpcd,	2,	"<directory>",		NULLCHAR,
	"dir",		dolist,		0,	NULLCHAR,		NULLCHAR,
	"list",		dolist,		0,	NULLCHAR,		NULLCHAR,
	"get",		doget,		2,	"<remotefile> [<localfile>]",	NULLCHAR,
	"ls",		dols,		0,	NULLCHAR,		NULLCHAR,
	"mkdir",	domkdir,	2,	"<directory>",		NULLCHAR,
	"nlst",		dols,		0,	NULLCHAR,		NULLCHAR,
	"rmdir",	dormdir,	2,	"<directory>",		NULLCHAR,
	"put",		doput,		2,	"<localfile> [<remotefile>]",	NULLCHAR,
	"type",		dotype,		0,	NULLCHAR,		NULLCHAR,
#if (defined(MSDOS))
	"tput",		dotput,		2,	"<localspec>",		NULLCHAR,
#endif
#if (defined(ATARI_ST))
	"tput",		dotput,		2,	"<localspec> [x]",	NULLCHAR,
#endif
	NULLCHAR,	NULLFP,		0,	NULLCHAR,		NULLCHAR
};

/* Handle top-level FTP command */
doftp(argc,argv)
int argc;
char *argv[];
{
	int32 resolve();
	int ftpparse();
	void ftpccr(),ftpccs();
	struct session *s;
	struct ftp *ftp,*ftp_create();
	struct tcb *tcb;
	struct socket lsocket,fsocket;
	char tos = 0;

	lsocket.address = ip_addr;
	lsocket.port = lport++;
	if((fsocket.address = resolve(argv[1])) == 0){
		printf(badhost,argv[1]);
		return 1;
	}
	if(argc < 3)
		fsocket.port = FTP_PORT;
	else {
		if((fsocket.port = atoi(argv[2])) == 0)
			fsocket.port = FTP_PORT;
		tos = get_tos(argv[3]);
	}
	/* Allocate a session control block */
	if((s = newsession()) == NULLSESSION){
		printf("Too many sessions\n");
		return 1;
	}
	current = s;
	if((s->name = malloc((unsigned)strlen(argv[1])+1)) != NULLCHAR)
		strcpy(s->name,argv[1]);
	s->type = FTP;
	s->parse = ftpparse;

	/* Allocate an FTP control block */
	if((ftp = ftp_create(0)) == NULLFTP){
		s->type = FREE;
		printf(nospace);
		return 1;
	}
	ftp->state = COMMAND_STATE;
	s->cb.ftp = ftp;	/* Downward link */
	ftp->session = s;	/* Upward link */

	/* Now open the control connection */
	tcb = open_tcp(&lsocket,&fsocket,TCP_ACTIVE,
		0,ftpccr,NULLVFP,ftpccs,tos,(char *)ftp);
	ftp->control = tcb;
	go();
	return 0;
}
/* Parse user FTP commands */
int
ftpparse(line,len)
char *line;
int16 len;
{
	struct mbuf *bp;

	if(current->cb.ftp->state != COMMAND_STATE){
		/* The only command allowed in data transfer state is ABORT */
		if(cmdparse(ftpabort,line) == -1){
			printf("Transfer in progress; only ABORT is acceptable\n");
		}
		fflush(stdout);
		return;
	}

	/* Save it now because cmdparse modifies the original */
	bp = qdata(line,len);

	if(cmdparse(ftpcmds,line) == -1){
		/* Send it direct */
		if(bp != NULLBUF)
			send_tcp(current->cb.ftp->control,bp);
		else
			printf(nospace);
	} else {
		free_p(bp);
	}
	fflush(stdout);
}
/* Handle null line to avoid trapping on first command in table */
static
int
donothing(argc,argv)
int argc;
char *argv[];
{
	return 0;
}
/* Translate 'cd' to 'cwd' for convenience */
static
int
doftpcd(argc,argv)
int argc;
char *argv[];
{
	register struct ftp *ftp;

	ftp = current->cb.ftp;
	return sndftpmsg(ftp,"CWD %s\r\n",argv[1]);
}
/* Translate 'mkdir' to 'xmkd' for convenience */
static
int
domkdir(argc,argv)
int argc;
char *argv[];
{
	register struct ftp *ftp;

	ftp = current->cb.ftp;
	return sndftpmsg(ftp,"XMKD %s\r\n",argv[1]);
}
/* Translate 'rmdir' to 'xrmd' for convenience */
static
int
dormdir(argc,argv)
int argc;
char *argv[];
{
	register struct ftp *ftp;

	ftp = current->cb.ftp;
	return sndftpmsg(ftp,"XRMD %s\r\n",argv[1]);
}
/* Handle "type" command from user */
static
int
dotype(argc,argv)
int argc;
char *argv[];
{
	register struct ftp *ftp;

	ftp = current->cb.ftp;
	if(argc < 2){
		switch(ftp->type){
		case IMAGE_TYPE:
			printf("Image\n");
			break;
		case ASCII_TYPE:
			printf("Ascii\n");
			break;
		}
		return 0;
	}
	switch(*argv[1]){
	case 'i':
	case 'b':
		ftp->type = IMAGE_TYPE;
		sndftpmsg(ftp,"TYPE I\r\n");
		break;
	case 'a':
		ftp->type = ASCII_TYPE;
		sndftpmsg(ftp,"TYPE A\r\n");
		break;
	case 'l':
		if (argc > 2){
			ftp->type = IMAGE_TYPE;
			sndftpmsg(ftp,"TYPE L %s\r\n",argv[2]);
			break;
		}
		/* no arg after TYPE L, drop in default
		   instead of generating BUS ERROR on Atari ST (and others) */
	default:
		printf("Invalid type %s\n",argv[1]);
		return 1;
	}
	return 0;
}
/* Start receive transfer. Syntax: get <remote name> [<local name>] */
static
doget(argc,argv)
int argc;
char *argv[];
{
	void ftpdr(),ftpcds();
	char *remotename,*localname;
	register struct ftp *ftp;
	char *mode;

	if((ftp = current->cb.ftp) == NULLFTP){
		printf(notsess);
		return 1;
	}
	remotename = argv[1];
	if(argc < 3)
		localname = remotename;
	else
		localname = argv[2];

	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);

	if(ftp->type == IMAGE_TYPE)
		mode = binmode[WRITE_BINARY];
	else
		mode = "w";

	if((ftp->fp = fopen(localname,mode)) == NULLFILE){
		printf(cantwrite,localname);
		return 1;
	}
	ftp->state = RECEIVING_STATE;
	ftpsetup(ftp,ftpdr,NULLVFP,ftpcds);

	/* Generate the command to start the transfer */
	return sndftpmsg(ftp,"RETR %s\r\n",remotename);
}
/* List remote directory. Syntax: dir <remote directory/file> [<local name>] */
static
dolist(argc,argv)
int argc;
char *argv[];
{
	void ftpdr(),ftpcds();
	register struct ftp *ftp;

	if((ftp = current->cb.ftp) == NULLFTP){
		printf(notsess);
		return 1;
	}
	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);

	if(argc < 3){
		ftp->fp = stdout;
	} else if((ftp->fp = fopen(argv[2],"w")) == NULLFILE){
		printf(cantwrite,argv[2]);
		return 1;
	}
	ftp->state = RECEIVING_STATE;
	ftpsetup(ftp,ftpdr,NULLVFP,ftpcds);
	/* Generate the command to start the transfer
	 * It's done this way to avoid confusing the 4.2 FTP server
	 * if there's no argument
	 */
	if(argc > 1)
		return sndftpmsg(ftp,"LIST %s\r\n",argv[1]);
	else
		return sndftpmsg(ftp,"LIST\r\n");
}
/* Abbreviated (name only) list of remote directory.
 * Syntax: ls <remote directory/file> [<local name>]
 */
static
dols(argc,argv)
int argc;
char *argv[];
{
	void ftpdr(),ftpcds();
	register struct ftp *ftp;

	if((ftp = current->cb.ftp) == NULLFTP){
		printf(notsess);
		return 1;
	}
	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);

	if(argc < 3){
		ftp->fp = stdout;
	} else if((ftp->fp = fopen(argv[2],"w")) == NULLFILE){
		printf(cantwrite,argv[2]);
		return 1;
	}
	ftp->state = RECEIVING_STATE;
	ftpsetup(ftp,ftpdr,NULLVFP,ftpcds);
	/* Generate the command to start the transfer */
	if(argc > 1)
		return sndftpmsg(ftp,"NLST %s\r\n",argv[1]);
	else
		return sndftpmsg(ftp,"NLST\r\n");
}
/* Start transmit. Syntax: put <local name> [<remote name>] */
static
doput(argc,argv)
int argc;
char *argv[];
{
	void ftpdt(),ftpcds();
	char *remotename,*localname;
	char *mode;
	struct ftp *ftp;

	if((ftp = current->cb.ftp) == NULLFTP){
		printf(notsess);
		return 1;
	}
	localname = argv[1];
	if(argc < 3)
		remotename = localname;
	else
		remotename = argv[2];

	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);

	if(ftp->type == IMAGE_TYPE)
		mode = binmode[READ_BINARY];
	else
		mode = "r";

	if((ftp->fp = fopen(localname,mode)) == NULLFILE){
		printf(cantread,localname);
		return 1;
	}
	ftp->state = SENDING_STATE;
	ftpsetup(ftp,NULLVFP,ftpdt,ftpcds);

	/* Generate the command to start the transfer */
	return sndftpmsg(ftp,"STOR %s\r\n",remotename);
}
#if (defined(MSDOS) || defined(ATARI_ST))
/* Transmit tree. Syntax: tput <local spec> */
static
dotput(argc,argv)
int argc;
char *argv[];
{
	char *p;
	struct ftp *ftp;
	struct ftp_list *fl,**flp;
	int num;
	char localspec[128],localname[128],name[15];
	void ftp_dellist();
	int isdir(),iswild();
	char *basename();

	if((ftp = current->cb.ftp) == NULLFTP){
		printf(notsess);
		return 1;
	}
	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);
	ftp->fp = NULLFILE;
	ftp_dellist(ftp);

	/* Create a (dummy) ftp_list node containing the spec */
	if ((fl = (struct ftp_list *) calloc(1,sizeof(struct ftp_list) + strlen(argv[1]))) == NULL) {
	    printf(nospace);
	    return 1;
	}
	strcpy(fl->localname,argv[1]);
	cleanup_fname(fl->localname);

	ftp->ftp_list = fl;			/* 1st item of the list */

	/* scan the ever-growing list until all directories are processed */
	for (flp = &fl->next; fl != NULLFTPLIST; fl = fl->next) {
	    strcpy(localspec,fl->localname);
	    if ((fl->isdir = isdir(localspec)) != 0) /* directory - take all it's files */
		strcat(localspec,"/*.*");
	    cleanup_fname(localspec);
	    if (iswild(localspec)) {
		strcpy(localname,localspec);
		p = basename(localname);
		/* scan wildcard pattern, append files to end of list */
		for (num = 0; filedir(localspec,num,name), name[0] != '\0'; num++) {
		    if (name[0] == '.')
			continue;		/* ignore '.' and '..' */
		    strcpy(p,name);		/* append name to dir */
		    if ((*flp = (struct ftp_list *) calloc(1,sizeof(struct ftp_list) + strlen(localname))) == NULL) {
			printf(nospace);
			ftp_dellist(ftp);
			return 1;
		    }
		    strcpy((*flp)->localname,localname);
		    flp = &(*flp)->next;
		}
	    }
	}
	/* Remove the dummy 1st element (the spec) if not a directory */
	if (!(fl = ftp->ftp_list)->isdir && iswild(fl->localname)) {
	    ftp->ftp_list = fl->next;
	    free(fl);
	}
	if (ftp->ftp_list == NULLFTPLIST) {
	    printf("No file %s\n",argv[1]);
	    return 0;
	}
	ftp->xatr = (argc > 2);		/* extra arg = use XATR */
	return donextput(ftp);		/* trigger the multi-store op */
}
static
donextput(ftp)
register struct ftp *ftp;
{
	struct ftp_list *fl;
	char *mode;
	int rv;
	void ftpdt(),ftpcds();

	if(ftp->state != COMMAND_STATE)
		return 1;

	if((fl = ftp->ftp_list) == NULLFTPLIST)
		return -1;

	ftp->ftp_list = fl->next;

	if(fl->isdir) {			/* is it a directory? */
	    rv = sndftpmsg(ftp,"XMKD %s\r\n",fl->localname);
	} else {
	    if(ftp->type == IMAGE_TYPE)
		mode = binmode[READ_BINARY];
	    else
		mode = "r";

	    if((ftp->fp = fopen(fl->localname,mode)) == NULLFILE){
		printf(cantread,fl->localname);
		return 1;
	    }
	    ftp->state = SENDING_STATE;
	    ftpsetup(ftp,NULLVFP,ftpdt,ftpcds);

	    /* Generate the command to start the transfer */
	    rv = sndftpmsg(ftp,"STOR %s\r\n",fl->localname);

#if (defined (ATARI_ST))
	    /* When required, send an XATR command (set attibutes) */
	    if (ftp->xatr) {
		int attr;
		unsigned long datime;
		unsigned int info[2];
		char buf[26];

		if ((attr = Fattrib(fl->localname,0,0)) < 0) /* get attribs */
		    attr = 0;

		Fdatime(info,fileno(ftp->fp),0);	/* get date/time */
# if (defined(MWC) && (MWC < 306))
		datime = info[0] | ((long) info[1] << Sixteen);
# else
		datime = info[0] | ((long) info[1] << 16);
# endif

		sprintf(buf,"%u,%u,%u,%u,%u,%u",
			hibyte(hiword(datime)),
			lobyte(hiword(datime)),
			hibyte(loword(datime)),
			lobyte(loword(datime)),
			hibyte(attr),
			lobyte(attr));

		rv |= sndftpmsg(ftp,"XATR %s\r\n",buf);
	    }
#endif
	}

	free(fl);
	return rv;
}
#endif
/* Abort a GET or PUT operation in progress. Note: this will leave
 * the partial file on the local or remote system
 */
doabort(argc,argv)
int argc;
char *argv[];
{
	register struct ftp *ftp;

	ftp = current->cb.ftp;

	/* Close the local file */
	if(ftp->fp != NULLFILE && ftp->fp != stdout)
		fclose(ftp->fp);
	ftp->fp = NULLFILE;
#if (defined(MSDOS) || defined(ATARI_ST))
	ftp_dellist(ftp);
#endif
	switch(ftp->state){
	case SENDING_STATE:
		/* Send a premature EOF.
		 * Unfortunately we can't just reset the connection
		 * since the remote side might end up waiting forever
		 * for us to send something.
		 */
		close_tcp(ftp->data);
		printf("Put aborted\n");
		break;
	case RECEIVING_STATE:
		/* Just exterminate the data channel TCB; this will
		 * generate a RST on the next data packet which will
		 * abort the sender
		 */
		del_tcp(ftp->data);
		ftp->data = NULLTCB;
		printf("Get aborted\n");
		break;
	}
	ftp->state = COMMAND_STATE;
	fflush(stdout);
}
/* create data port, and send PORT message */
static
ftpsetup(ftp,recv,send,state)
struct ftp *ftp;
void (*send)();
void (*recv)();
void (*state)();
{
	struct socket lsocket;
	struct mbuf *bp;

	lsocket.address = ip_addr;
	lsocket.port = lport++;

	/* Compose and send PORT a,a,a,a,p,p message */

	if((bp = alloc_mbuf(35)) == NULLBUF){	/* 5 more than worst case */
		printf(nospace);
		return;
	}
	/* I know, this looks gross, but it works! */
	sprintf(bp->data,"PORT %u,%u,%u,%u,%u,%u\r\n",
		hibyte(hiword(lsocket.address)),
		lobyte(hiword(lsocket.address)),
		hibyte(loword(lsocket.address)),
		lobyte(loword(lsocket.address)),
		hibyte(lsocket.port),
		lobyte(lsocket.port));
	bp->cnt = strlen(bp->data);
	send_tcp(ftp->control,bp);

	/* Post a listen on the data connection */
	ftp->data = open_tcp(&lsocket,NULLSOCK,TCP_PASSIVE,0,
		recv,send,state,ftp->control->tos,(char *)ftp);
}
/* FTP Client Control channel Receiver upcall routine */
void
ftpccr(tcb,cnt)
register struct tcb *tcb;
int16 cnt;
{
	struct mbuf *bp;
	struct ftp *ftp;
	extern int flowctrl,ttydriv();

	if((ftp = (struct ftp *)tcb->user) == NULLFTP){
		/* Unknown connection; kill it */
		close_tcp(tcb);
		return;
	}
	/* Hold output if we're not the current session, and while typing */
	if(mode != CONV_MODE || current == NULLSESSION || current->cb.ftp != ftp
	   || (flowctrl && ttydriv(-1,NULL)))
		return;

	if(recv_tcp(tcb,&bp,cnt) > 0){
		while(bp != NULLBUF){
			fwrite(bp->data,1,(unsigned)bp->cnt,stdout);
			if(current->record != NULLFILE)
				fwrite(bp->data,1,(unsigned)bp->cnt,current->record);
			bp = free_mbuf(bp);
		}
		fflush(stdout);
	}
#if (defined(MSDOS) || defined(ATARI_ST))
	if (ftp->state == COMMAND_STATE)
		donextput(ftp);
#endif
}

/* FTP Client Control channel State change upcall routine */
static
void
ftpccs(tcb,old,new)
register struct tcb *tcb;
char old,new;
{
	void ftp_delete();
	struct ftp *ftp;
	char notify = 0;
	extern char *tcpstates[];
	extern char *reasons[];
	extern char *unreach[];
	extern char *exceed[];

	/* Can't add a check for unknown connection here, it would loop
	 * on a close upcall! We're just careful later on.
	 */
	ftp = (struct ftp *)tcb->user;

	if(current != NULLSESSION && current->cb.ftp == ftp)
		notify = 1;

	switch(new){
	case CLOSE_WAIT:
		if(notify)
			printf("%s\n",tcpstates[new]);
		close_tcp(tcb);
		break;
	case CLOSED:	/* heh heh */
		if(notify){
			printf("%s (%s",tcpstates[new],reasons[tcb->reason]);
			if(tcb->reason == NETWORK){
				switch(tcb->type){
				case DEST_UNREACH:
					printf(": %s unreachable",unreach[tcb->code]);
					break;
				case TIME_EXCEED:
					printf(": %s time exceeded",exceed[tcb->code]);
					break;
				}
			}
			printf(")\n");
			cmdmode();
		}
		del_tcp(tcb);
		if(ftp != NULLFTP)
			ftp_delete(ftp);
		break;
	default:
		if(notify)
			printf("%s\n",tcpstates[new]);
		break;
	}
	if(notify)
		fflush(stdout);
}
/* FTP Client Data channel State change upcall handler */
static
void
ftpcds(tcb,old,new)
struct tcb *tcb;
char old,new;
{
	struct ftp *ftp;
	unsigned long xfertime;

	if((ftp = (struct ftp *)tcb->user) == NULLFTP){
		/* Unknown connection, kill it */
		close_tcp(tcb);
		return;
	}
	switch(new){
	case ESTABLISHED:
		/* Start the timer so we can print the effective speed */
		ftp->xfertime.start = MAX_TIME;
		start_timer(&ftp->xfertime);
		break;

	case FINWAIT2:
	case TIME_WAIT:
		if(ftp->state == SENDING_STATE){
			/* We've received an ack of our FIN, so
			 * return to command mode
			 */
			ftp->state = COMMAND_STATE;
			if(current != NULLSESSION && current->cb.ftp == ftp){
				printf("Put complete, %lu bytes sent",
					tcb->snd.una - tcb->iss - 2);
				if ((xfertime = TICK2MS(ftp->xfertime.start - ftp->xfertime.count)) != 0)
					printf(" in %lu seconds (%lu bits/s)",
						(xfertime + 500L) / 1000L,
						((unsigned long) 8000 * (tcb->snd.una - tcb->iss - 2)) / xfertime);
				stop_timer(&ftp->xfertime);
				printf("\n");
				fflush(stdout);
			}
		}
		break;
	case CLOSE_WAIT:
		close_tcp(tcb);
		if(ftp->state == RECEIVING_STATE){
			/* End of file received on incoming file */
#ifdef	CPM
			if(ftp->type == ASCII_TYPE)
				putc(CTLZ,ftp->fp);
#endif
			if(ftp->fp != stdout)
				fclose(ftp->fp);
			ftp->fp = NULLFILE;
			ftp->state = COMMAND_STATE;
			if(current != NULLSESSION && current->cb.ftp == ftp){
				printf("Get complete, %lu bytes received",
					tcb->rcv.nxt - tcb->irs - 2);
				if ((xfertime = TICK2MS(ftp->xfertime.start - ftp->xfertime.count)) != 0)
					printf(" in %lu seconds (%lu bits/s)",
						(xfertime + 500L) / 1000L,
						((unsigned long) 8000 * (tcb->rcv.nxt - tcb->irs - 2)) / xfertime);
				stop_timer(&ftp->xfertime);
				printf("\n");
				fflush(stdout);
			}
		}
		break;
	case CLOSED:
		ftp->data = NULLTCB;
		del_tcp(tcb);
		break;
	}
}
/* Send a message on the control channel */
/*VARARGS*/
static
int
sndftpmsg(ftp,fmt,arg)
struct ftp *ftp;
char *fmt;
char *arg;
{
	struct mbuf *bp;
	int16 len;

	len = strlen(fmt) + strlen(arg) + 10;	/* fudge factor */
	if((bp = alloc_mbuf(len)) == NULLBUF){
		printf(nospace);
		return 1;
	}
	sprintf(bp->data,fmt,arg);
	bp->cnt = strlen(bp->data);
	send_tcp(ftp->control,bp);
	return 0;
}
