/* FTP client (interactive user) code */
#include <stdio.h>
#include "global.h"
#include "mbuf.h"
#include "session.h"
#include "cmdparse.h"
#include "timer.h"
#include "proc.h"
#include "tty.h"
#include "socket.h"
#include "ftp.h"
#include "ftpcli.h"
#include "commands.h"
#include "netuser.h"

static int doascii __ARGS((int argc,char *argv[],void *p));
static int dobinary __ARGS((int argc,char *argv[],void *p));
static int doftpcd __ARGS((int argc,char *argv[],void *p));
static int doget __ARGS((int argc,char *argv[],void *p));
static int dolist __ARGS((int argc,char *argv[],void *p));
static int dols __ARGS((int argc,char *argv[],void *p));
static int domkdir __ARGS((int argc,char *argv[],void *p));
static int donothing __ARGS((int argc,char *argv[],void *p));
static int doput __ARGS((int argc,char *argv[],void *p));
static int doquit __ARGS((int argc,char *argv[],void *p));
static int dormdir __ARGS((int argc,char *argv[],void *p));
static int dotype __ARGS((int argc,char *argv[],void *p));
static int getline __ARGS((struct session *sp,char *prompt,char *buf,int n));
static int getresp __ARGS((int s,int mincode));
static int getsub __ARGS((struct ftpcli *ftp,char *command,char *remotename,
	char *localname));
static void sendport __ARGS((int s,struct sockaddr_in *socket));

static char Notsess[] = "Not an FTP session!\n";

static struct cmds Ftpcmds[] = {
	"",		donothing,	0, 0, NULLCHAR,
	"ascii",	doascii,	0, 0, NULLCHAR,
	"binary",	dobinary,	0, 0, NULLCHAR,
	"cd",		doftpcd,	0, 2, "cd <directory>",
	"dir",		dolist,		0, 0, NULLCHAR,
	"list",		dolist,		0, 0, NULLCHAR,
	"get",		doget,		0, 2, "get remotefile <localfile>",
	"ls",		dols,		0, 0, NULLCHAR,
	"mkdir",	domkdir,	0, 2, "mkdir <directory>",
	"nlst",		dols,		0, 0, NULLCHAR,
	"quit",		doquit,		0, 0, NULLCHAR,
	"rmdir",	dormdir,	0, 2, "rmdir <directory>",
	"put",		doput,		0, 2, "put localfile <remotefile>",
	"type",		dotype,		0, 0, NULLCHAR,
	NULLCHAR,	NULLFP,		0, 0, NULLCHAR,
};

/* Handle top-level FTP command */
int
doftp(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	struct session *sp;
	struct ftpcli ftp;
	struct sockaddr_in fsocket;
	int resp;
	char *buf,*bufsav,*cp;
	int control;

	/* Allocate a session control block */
	if((sp = newsession(argv[1],FTP)) == NULLSESSION){
		tprintf("Too many sessions\n");
		freeargs(argc,argv);
		return 1;
	}
	memset((char *)&ftp,0,sizeof(ftp));
	ftp.control = ftp.data = -1;

	sp->cb.ftp = &ftp;	/* Downward link */
	ftp.session = sp;	/* Upward link */

	fsocket.sin_family = AF_INET;
	if(argc < 3)
		fsocket.sin_port = IPPORT_FTP;
	else
		fsocket.sin_port = atoi(argv[2]);

	freeargs(argc,argv);
	tprintf("Resolving %s... ",sp->name);
	if((fsocket.sin_addr.s_addr = resolve(sp->name)) == 0){
		tprintf(Badhost,sp->name);
		keywait(NULLCHAR,1);
		freesession(sp);
		return 1;
	}
	/* Open the control connection */
	if((control = sp->s = ftp.control = socket(AF_INET,SOCK_STREAM,0)) == -1){
		tprintf("Can't create socket\n");
		keywait(NULLCHAR,1);
		freesession(sp);
		return 1;
	}
	tprintf("Trying %s...\n",psocket((struct sockaddr *)&fsocket));
	if(connect(control,(char *)&fsocket,sizeof(fsocket)) == -1)
		goto quit;
	tprintf("FTP session %u connected to %s\n",(unsigned)(sp-Sessions),
		sp->name);

	/* Wait for greeting from server */
	resp = getresp(control,200);

	if(resp >= 400)
		goto quit;
	/* Now process responses and commands */
	buf = mallocw(LINELEN);
	while(resp != -1){
		if(resp == 220){
			/* Sign-on banner; prompt for and send USER command */
			getline(sp,"Enter user name: ",buf,LINELEN);
			/* Send the command only if the user response
			 * was non-null
			 */
			if(buf[0] != '\n'){
				usprintf(control,"USER %s",buf);
				resp = getresp(control,200);
			} else
				resp = 200;	/* dummy */
		} else if(resp == 331){
			/* turn off echo */
			sp->ttystate.echo = 0;
			getline(sp,"Password: ",buf,LINELEN);
			tprintf("\n");
			/* Turn echo back on */
			sp->ttystate.echo = 1;
			/* Send the command only if the user response
			 * was non-null
			 */
			if(buf[0] != '\n'){
				usprintf(control,"PASS %s",buf);
				resp = getresp(control,200);
			} else
				resp = 200;	/* dummy */
		} else {
			/* Test the control channel first */
			if(sockstate(control) == NULLCHAR)
				break;

			getline(sp,"ftp> ",buf,LINELEN);

			/* Copy because cmdparse modifies the original */
			bufsav = strdup(buf);
			if((resp = cmdparse(Ftpcmds,buf,&ftp)) != -1){
				/* Valid command, free buffer and get another */
				free(bufsav);
			} else {
				/* Not a local cmd, send to remote server */
				usputs(control,bufsav);
				free(bufsav);
				resp = getresp(control,200);
			}
		}
	}
	free(buf);
quit:	cp = sockerr(control);
	tprintf("FTP session %u closed: %s\n",(unsigned)(sp - Sessions),
	 cp != NULLCHAR ? cp : "EOF");

	if(ftp.fp != NULLFILE && ftp.fp != stdout)
		fclose(ftp.fp);
	if(ftp.data != -1)
		close_s(ftp.data);
	if(ftp.control != -1)
		close_s(ftp.control);
	keywait(NULLCHAR,1);
	if(ftp.session != NULLSESSION)
		freesession(ftp.session);
	return 0;
}

/* Handle null line to avoid trapping on first command in table */
static int
donothing(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	return 0;
}
/* Close session */
static int
doquit(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP)
		return -1;
	usprintf(ftp->control,"QUIT\n");
	getresp(ftp->control,200);	/* Get the closing message */
	getresp(ftp->control,200);	/* Wait for the server to close */
	return -1;
}

/* Translate 'cd' to 'cwd' for convenience */
static int
doftpcd(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP)
		return -1;
	usprintf(ftp->control,"CWD %s\n",argv[1]);
	return getresp(ftp->control,200);
}
/* Translate 'mkdir' to 'xmkd' for convenience */
static int
domkdir(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP)
		return -1;
	usprintf(ftp->control,"XMKD %s\n",argv[1]);
	return getresp(ftp->control,200);
}
/* Translate 'rmdir' to 'xrmd' for convenience */
static int
dormdir(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP)
		return -1;
	usprintf(ftp->control,"XRMD %s\n",argv[1]);
	return getresp(ftp->control,200);
}
static int
dobinary(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *args[2];

	args[1] = "I";
	return dotype(2,args,p);
}
static int
doascii(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *args[2];

	args[1] = "A";
	return dotype(2,args,p);
}

/* Handle "type" command from user */
static int
dotype(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP)
		return -1;
	if(argc < 2){
		switch(ftp->type){
		case IMAGE_TYPE:
			tprintf("Image\n");
			break;
		case ASCII_TYPE:
			tprintf("Ascii\n");
			break;
		case LOGICAL_TYPE:
			tprintf("Logical bytesize %u\n",ftp->logbsize);
			break;
		}
		return 0;
	}
	switch(*argv[1]){
	case 'i':
	case 'I':
	case 'b':
	case 'B':
		ftp->typesent = ftp->type = IMAGE_TYPE;
		usprintf(ftp->control,"TYPE I\n");
		break;
	case 'a':
	case 'A':
		ftp->typesent = ftp->type = ASCII_TYPE;
		usprintf(ftp->control,"TYPE A\n");
		break;
	case 'L':
	case 'l':
		ftp->typesent = ftp->type = LOGICAL_TYPE;
		ftp->logbsize = atoi(argv[2]);
		usprintf(ftp->control,"TYPE L %s\n",argv[2]);
		break;
	default:
		tprintf("Invalid type %s\n",argv[1]);
		return 1;
	}
	return getresp(ftp->control,200);
}
/* Start receive transfer. Syntax: get <remote name> [<local name>] */
static int
doget(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *remotename,*localname;
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP){
		tprintf(Notsess);
		return 1;
	}
	remotename = argv[1];
	if(argc < 3)
		localname = remotename;
	else
		localname = argv[2];

	return getsub(ftp,"RETR",remotename,localname);
}
/* List remote directory. Syntax: dir <remote files> [<local name>] */
static int
dolist(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *remotename,*localname;
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP){
		tprintf(Notsess);
		return 1;
	}
	remotename = argv[1];
	if(argc > 2)
		localname = argv[2];
	else
		localname = NULLCHAR;
	return getsub(ftp,"LIST",remotename,localname);
}
/* Remote directory list, short form. Syntax: ls <remote files> [<local name>] */
static int
dols(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *remotename,*localname;
	register struct ftpcli *ftp;

	ftp = (struct ftpcli *)p;
	if(ftp == NULLFTP){
		tprintf(Notsess);
		return 1;
	}
	remotename = argv[1];
	if(argc > 2)
		localname = argv[2];
	else
		localname = NULLCHAR;
	return getsub(ftp,"NLST",remotename,localname);
}
/* Common code to LIST/NLST/RETR */
static int
getsub(ftp,command,remotename,localname)
register struct ftpcli *ftp;
char *command,*remotename,*localname;
{
	unsigned long total;
	FILE *fp;
	int cnt,resp,i,control;
	char *mode;
	struct sockaddr_in lsocket;
	struct sockaddr_in lcsocket;
	int32 startclk,rate;

	if(ftp == NULLFTP)
		return -1;
	control = ftp->control;

	switch(ftp->type){
	case IMAGE_TYPE:
	case LOGICAL_TYPE:
		mode = WRITE_BINARY;
		break;
	case ASCII_TYPE:
		mode = WRITE_TEXT;
		break;
	}
	/* Send TYPE message, if necessary */
	if(strcmp(command,"LIST") == 0 || strcmp(command,"NLST") == 0){
		if(ftp->typesent != ASCII_TYPE){
			/* Directory listings are always in ASCII */
			usprintf(control,"TYPE A\n");
			ftp->typesent = ASCII_TYPE;
			resp = getresp(control,200);
			if(resp == -1 || resp > 299){
				return 1;
			}
		}
	} else if(ftp->typesent != ftp->type){
		switch(ftp->type){
		case ASCII_TYPE:
			usprintf(control,"TYPE A\n");
			break;
		case IMAGE_TYPE:
			usprintf(control,"TYPE I\n");
			break;
		case LOGICAL_TYPE:
			usprintf(control,"TYPE L %d\n",ftp->logbsize);
			break;
		}
		ftp->typesent = ftp->type;
		resp = getresp(control,200);
		if(resp == -1 || resp > 299){
			return 1;
		}
	}
	if(localname == NULLCHAR){
		fp = NULLFILE;
	} else if((fp = fopen(localname,mode)) == NULLFILE){
		tprintf("Can't write %s: %s\n",localname,sys_errlist[errno]);
		return 1;
	}
	/* Open the data connection */
	ftp->data = socket(AF_INET,SOCK_STREAM,0);
	listen(ftp->data,0);	/* Accept only one connection */
	ftp->state = RECEIVING_STATE;

	/* Send the PORT message and wait for ack. Use the IP address
	 * on the local end of our control connection.
	 */
	i = SOCKSIZE;
	getsockname(ftp->data,(char *)&lsocket,&i);
	i = SOCKSIZE;
	getsockname(ftp->control,(char *)&lcsocket,&i);
	lsocket.sin_addr.s_addr = lcsocket.sin_addr.s_addr;
	sendport(control,&lsocket);
	resp = getresp(control,200);
	if(resp == -1 || resp > 299){
		/* Error, quit */
		if(fp != NULLFILE && fp != stdout)
			fclose(fp);
		close_s(ftp->data);
		ftp->data = -1;
		ftp->state = COMMAND_STATE;
		return 1;
	}
	/* Generate the command to start the transfer and wait for ack */
	if(remotename != NULLCHAR)
		usprintf(control,"%s %s\n",command,remotename);
	else
		usprintf(control,"%s\n",command);
	/* Get the intermediate "150" response */
	resp = getresp(control,100);
	if(resp == -1 || resp >= 400){
		/* Error, quit */
		if(fp != NULLFILE && fp != stdout)
			fclose(fp);
		close_s(ftp->data);
		ftp->data = -1;
		ftp->state = COMMAND_STATE;
		return 1;
	}
	/* Wait for the server to open the data connection */
	cnt = 0;
	ftp->data = accept(ftp->data,NULLCHAR,&cnt);
	startclk = Clock;

	total = recvfile(fp,ftp->data,ftp->type);
	/* Immediately close the data connection; some servers (e.g., TOPS-10)
	 * wait for the data connection to close completely before returning
	 * the completion message on the control channel
	 */
	close_s(ftp->data);
	ftp->data = -1;

#ifdef	CPM
	if(fp != NULLFILE && ftp->type == ASCII_TYPE)
		fputc(CTLZ,fp);
#endif
	if(fp != NULLFILE && fp != stdout)
		fclose(fp);
	startclk = Clock - startclk;
	if(startclk != 0)
		rate = (total*1000)/(startclk*MSPTICK);
	else
		rate = 0;
	if(total != -1)
		tprintf("Get complete: %lu bytes in %lu sec (%lu/sec)\n",
		 total,(startclk*MSPTICK)/1000,rate);
	else
		tprintf("Error or abort during data transfer\n");
	getresp(control,200);

	ftp->state = COMMAND_STATE;
	return 0;
}
/* Send a file. Syntax: put <local name> [<remote name>] */
static int
doput(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	char *remotename,*localname,*mode;
	register struct ftpcli *ftp;
	int i,resp,control;
	unsigned long total;
	FILE *fp;
	struct sockaddr_in lsocket,lcsocket;
	int32 startclk,rate;

	if((ftp = (struct ftpcli *)p) == NULLFTP)
		return -1;
	control = ftp->control;

	if(ftp == NULLFTP){
		tprintf(Notsess);
		return 1;
	}
	localname = argv[1];
	if(argc < 3)
		remotename = localname;
	else
		remotename = argv[2];

	if(ftp->type == IMAGE_TYPE)
		mode = READ_BINARY;
	else
		mode = READ_TEXT;

	/* Send TYPE message, if necessary */
	if(ftp->typesent != ftp->type){
		switch(ftp->type){
		case ASCII_TYPE:
			usprintf(control,"TYPE A\n");
			break;
		case IMAGE_TYPE:
			usprintf(control,"TYPE I\n");
			break;
		case LOGICAL_TYPE:
			usprintf(control,"TYPE L %d\n",ftp->logbsize);
			break;
		}
		ftp->typesent = ftp->type;
		resp = getresp(control,200);
		if(resp == -1 || resp > 299){
			return 1;
		}
	}
	if((fp = fopen(localname,mode)) == NULLFILE){
		tprintf("Can't read %s: %s\n",localname,sys_errlist[errno]);
		return 1;
	}
	/* Open the data connection */
	ftp->data = socket(AF_INET,SOCK_STREAM,0);
	listen(ftp->data,0);

	ftp->state = SENDING_STATE;

	/* Send the PORT message and wait for ack. Use the IP address
	 * on the local end of our control connection.
	 */
	i = SOCKSIZE;
	getsockname(ftp->data,(char *)&lsocket,&i);
	i = SOCKSIZE;
	getsockname(ftp->control,(char *)&lcsocket,&i);
	lsocket.sin_addr.s_addr = lcsocket.sin_addr.s_addr;
	sendport(control,&lsocket);
	resp = getresp(control,200);
	if(resp == -1 || resp > 299){
		/* Error, quit */
		fclose(fp);
		close_s(ftp->data);
		ftp->data = -1;
		ftp->state = COMMAND_STATE;
		return 1;
	}
	/* Generate the command to start the transfer and wait for ack */
	usprintf(control,"STOR %s\n",remotename);
	resp = getresp(control,100);
	if(resp == -1 || resp >= 400){
		/* Error, quit */
		fclose(fp);
		close_s(ftp->data);
		ftp->data = -1;
		ftp->state = COMMAND_STATE;
		return 1;
	}
	/* Wait for the data connection to open. Otherwise the first
	 * block of data would go out with the SYN, and this may confuse
	 * some other TCPs
	 */
	accept(ftp->data,NULLCHAR,(int *)NULL);

	startclk = Clock;

	total = sendfile(fp,ftp->data,ftp->type);
	close_s(ftp->data);
	ftp->data = -1;
	fclose(fp);

	startclk = Clock - startclk;
	if(startclk != 0)
		rate = (total*1000)/(MSPTICK*startclk);
	else
		rate = 0;
	if(total != -1)
		tprintf("Put complete: %lu bytes in %lu sec (%lu/sec)\n",
		 total,(startclk*MSPTICK)/1000,rate);
	else
		tprintf("Error or abort during data transfer\n");

	getresp(control,200);
	ftp->state = COMMAND_STATE;
	return 1;
}
/* Abort a GET or PUT operation in progress. Note: this will leave
 * the partial file on the local or remote system
 */
int
doabort(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	register struct session *sp;
	register struct ftpcli *ftp;

	sp = (struct session *)p;
	if(sp == NULLSESSION)
		return -1;

	/* Default is the current session, but it can be overridden with
	 * an argument.
	 */
	if(argc > 1)
		sp = sessptr(argv[1]);

	if(sp == NULLSESSION || sp->type != FTP){
		tprintf("Not an active FTP session\n");
		return 1;
	}
	ftp = sp->cb.ftp;

	switch(ftp->state){
	case COMMAND_STATE:
		tprintf("No active transfer\n");
		return 1;
	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.
		 */
		shutdown(ftp->data,1);
		break;
	case RECEIVING_STATE:
		/* Just blow away the receive socket */
		shutdown(ftp->data,2);
		break;
	}
	return 0;
}
/* send PORT message */
static void
sendport(s,socket)
int s;
struct sockaddr_in *socket;
{
	/* Send PORT a,a,a,a,p,p message */
	usprintf(s,"PORT %u,%u,%u,%u,%u,%u\n",
		hibyte(hiword(socket->sin_addr.s_addr)),
		lobyte(hiword(socket->sin_addr.s_addr)),
		hibyte(loword(socket->sin_addr.s_addr)),
		lobyte(loword(socket->sin_addr.s_addr)),
		hibyte(socket->sin_port),
		lobyte(socket->sin_port));
}

/* Wait for, read and display response from FTP server. Return the result code.
 */
static int
getresp(s,mincode)
int s;
int mincode;	/* Keep reading until at least this code comes back */
{
	register char *line;
	int rval;

	line = mallocw(LINELEN);
	for(;;){
		/* Get line */
		if(recvline(s,line,LINELEN) == -1){
			rval = -1;
			break;
		}
		rip(line);		/* Remove cr/lf */
		tprintf("%s\n",line);	/* Display to user */

		/* Messages with dashes are continued */
		if(line[3] != '-' && (rval = atoi(line)) >= mincode)
			break;
	}
	free(line);
	return rval;
}

/* Issue a prompt and read a line from the user */
static int
getline(sp,prompt,buf,n)
struct session *sp;
char *prompt;
char *buf;
int n;
{
	/* If there's something already there, don't issue prompt */
	if(socklen(sp->input,0) == 0)
		tprintf(prompt);

	usflush(sp->output);
	return recvline(sp->input,buf,n);
}
