/* FTP Server state machine - see RFC 959 */

#define	LINELEN		128	/* Length of command buffer */

#include <stdio.h>
#include "machdep.h"
#include "mbuf.h"
#include "netuser.h"
#include "timer.h"
#include "tcp.h"
#include "ftp.h"

/* Command table */
static char *commands[] = {
	"user",
#define	USER_CMD	0
	"acct",
#define	ACCT_CMD	1
	"pass",
#define	PASS_CMD	2
	"type",
#define	TYPE_CMD	3
	"list",
#define	LIST_CMD	4
	"cwd",
#define	CWD_CMD		5
	"dele",
#define	DELE_CMD	6
	"name",
#define	NAME_CMD	7
	"quit",
#define	QUIT_CMD	8
	"retr",
#define	RETR_CMD	9
	"stor",
#define	STOR_CMD	10
	"port",
#define	PORT_CMD	11
	"nlst",
#define	NLST_CMD	12
	"pwd",
#define	PWD_CMD		13
	"xpwd",			/* For compatibility with 4.2BSD */
#define	XPWD_CMD	14
	NULLCHAR
};

/* Response messages */
static char banner[] = "220 %s FTP Ready\r\n";
static char badcmd[] = "500 Unknown command\r\n";
static char nopass[] = "202 Password not needed\r\n";
static char logged[] = "230 Logged in\r\n";
static char typeok[] = "200 Type OK\r\n";
static char cwdok[] = "250 CWD OK\r\n";
static char pwdmsg[] = "257 \"%s\" is current directory\r\n";
static char badtype[] = "501 Unknown type\r\n";
static char badport[] = "501 Bad port syntax\r\n";
static char unimp[] = "502 Command not yet implemented\r\n";
static char bye[] = "221 Goodbye!\r\n";
static char nodir[] = "553 Can't read directory\r\n";
static char cantopen[] = "550 Can't open file\r\n";
static char sending[] = "150 Opening data connection for %s %s\r\n";
static char cantmake[] = "553 Can't create file\r\n";
static char portok[] = "200 Port command okay\r\n";
static char rxok[] = "226 File received OK\r\n";
static char txok[] = "226 File sent OK\r\n";

static struct tcb *ftp_tcb;

/* Start up FTP service */
ftp_start(argc,argv)
int argc;
char *argv[];
{
	struct socket lsocket;
	void r_ftp(),s_ftp();

	lsocket.address = ip_addr;
	if(argc < 2)
		lsocket.port = FTP_PORT;
	else
		lsocket.port = atoi(argv[1]);

	ftp_tcb = open_tcp(&lsocket,NULLSOCK,TCP_PASSIVE,0,r_ftp,NULLVFP,s_ftp,0,(int *)NULL);
}
ftp_stop()
{
	if(ftp_tcb != NULLTCB)
		close_tcp(ftp_tcb);
}
/* FTP server control channel connection state change upcall handler */
static
void
s_ftp(tcb,old,new)
struct tcb *tcb;
char old,new;
{
	extern char hostname[];
	struct ftp *ftp,*ftp_create();
	void ftp_delete();
	char *inet_ntoa(),*pwd();

	switch(new){
#ifdef	QUICKSTART
	case SYN_RECEIVED:
#else
	case ESTABLISHED:
#endif
		if((ftp = ftp_create(LINELEN)) == NULLFTP){
			/* No space, kill connection */
			close_tcp(tcb);
			return;
		}
		ftp->control = tcb;	/* Downward link */
		tcb->user = (int *)ftp;	/* Upward link */

		/* Set default data port */
		ftp->port.address = tcb->conn.remote.address;
		ftp->port.port = FTPD_PORT;

		/* Note current directory */
#ifndef	AMIGA
		ftp->cd = pwd();
#endif
		log(tcb,"open FTP");
		tprintf(ftp->control,banner,hostname);
		break;		
	case CLOSE_WAIT:
		close_tcp(tcb);
		break;
	case CLOSED:
		log(tcb,"close FTP");
		if((ftp = (struct ftp *)tcb->user) != NULLFTP)
			ftp_delete(ftp);
		/* Check if server is being shut down */
		if(tcb == ftp_tcb)
			ftp_tcb = NULLTCB;
		del_tcp(tcb);
		break;
	}
}

/* FTP control channel receiver upcall handler */
static
void
r_ftp(tcb,cnt)
struct tcb *tcb;
int16 cnt;
{
	register struct ftp *ftp;
	char *index(),c;
	struct mbuf *bp;
	void docommand();

	if((ftp = (struct ftp *)tcb->user) == NULLFTP){
		/* Unknown connection, just kill it */
		close_tcp(tcb);
		return;
	}
	switch(ftp->state){
	case COMMAND_STATE:
		/* Assemble an input line in the session buffer. Return if incomplete */
		recv_tcp(tcb,&bp,0);
		while(pullup(&bp,&c,1) == 1){
			switch(c){
			case '\r':	/* Strip cr's */
				continue;
			case '\n':	/* Complete line; process it */
				ftp->buf[ftp->cnt] = '\0';
				docommand(ftp);
				ftp->cnt = 0;
				break;
			default:	/* Assemble line */
				if(ftp->cnt != LINELEN-1)
					ftp->buf[ftp->cnt++] = c;
				break;
			}
		}
		/* else no linefeed present yet to terminate command */
		break;
	case SENDING_STATE:
	case RECEIVING_STATE:
		/* Leave commands pending on receive queue until
		 * present command is done
		 */
		break;
	}
}

/* FTP server data channel connection state change upcall handler */
void
s_ftpd(tcb,old,new)
struct tcb *tcb;
char old,new;
{
	struct ftp *ftp;
#ifndef	CPM
#ifndef	AMIGA
	char *cdsave;
#endif
#endif

	if((ftp = (struct ftp *)tcb->user) == NULLFTP){
		/* Unknown connection, kill it */
		close_tcp(tcb);
		return;
	}
	switch(new){
	case FINWAIT2:
	case TIME_WAIT:
		if(ftp != NULLFTP && ftp->state == SENDING_STATE){
			/* We've received an ack of our FIN, so
			 * send a completion message on the control channel
			 */
			ftp->state = COMMAND_STATE;
			tprintf(ftp->control,txok);
			/* Kick command parser if something is waiting */
			if(ftp->control->rcvcnt != 0)
				r_ftp(ftp->control,ftp->control->rcvcnt);
		}
		break;		
	case CLOSE_WAIT:
		close_tcp(tcb);
		if(ftp != NULLFTP && ftp->state == RECEIVING_STATE){
			/* End of file received on incoming file */
#ifdef	CPM
			if(ftp->type == ASCII_TYPE)
				putc(CTLZ,ftp->fp);
#endif
#ifndef	CPM
#ifndef	AMIGA
			cdsave = pwd();		/* Save current directory */
			chdir(ftp->cd);		/* Switch to user's directory*/
#endif
#endif
			fclose(ftp->fp);
#ifndef	CPM
#ifndef	AMIGA
			if(cdsave != NULLCHAR){
				chdir(cdsave);		/* And back */
				free(cdsave);
			}
#endif
#endif
			ftp->fp = NULLFILE;
			ftp->state = COMMAND_STATE;
			tprintf(ftp->control,rxok);
			/* Kick command parser if something is waiting */
			if(ftp->control->rcvcnt != 0)
				r_ftp(ftp->control,ftp->control->rcvcnt);
		}
		break;
	case CLOSED:
		if(ftp != NULLFTP)
			ftp->data = NULLTCB;
		del_tcp(tcb);
		break;
	}
}

/* Parse and execute ftp commands */
static
void
docommand(ftp)
register struct ftp *ftp;
{
	void r_ftpd(),t_ftpd(),s_ftpd();
	char *cmd,*arg,*cp,**cmdp;
	char *index(),*malloc(),*strcpy();
	struct socket dport;
#ifndef	CPM
#ifndef	AMIGA
	FILE *dir();
	char *cdsave;
#endif
#endif

	cmd = ftp->buf;
	if(ftp->cnt == 0){
		/* Can't be a legal FTP command */
		tprintf(ftp->control,badcmd);
		return;
	}	
	cmd = ftp->buf;

	/* Translate entire buffer to lower case */
	for(cp = cmd;*cp != '\0';cp++)
		*cp = tolower(*cp);

	/* Find command in table; if not present, return syntax error */
	for(cmdp = commands;*cmdp != NULLCHAR;cmdp++)
		if(strncmp(*cmdp,cmd,strlen(*cmdp)) == 0)
			break;
	if(*cmdp == NULLCHAR){
		tprintf(ftp->control,badcmd);
		return;
	}
	arg = &cmd[strlen(*cmdp)];
	while(*arg == ' ')
		arg++;
	/* Execute specific command */
	switch(cmdp-commands){
	case USER_CMD:
		if((ftp->username = malloc((unsigned)strlen(arg)+1)) == NULLCHAR){
			close_tcp(ftp->control);
			break;
		}
		strcpy(ftp->username,arg);
		tprintf(ftp->control,logged);
		break;
	case TYPE_CMD:
		switch(*arg){
		case 'a':	/* Ascii */
			ftp->type = ASCII_TYPE;
			tprintf(ftp->control,typeok);
			break;
		case 'b':	/* Binary */
		case 'i':	/* Image */
			ftp->type = IMAGE_TYPE;
			tprintf(ftp->control,typeok);
			break;
		default:	/* Invalid */
			tprintf(ftp->control,badtype);
			break;
		}
		break;
	case QUIT_CMD:
		tprintf(ftp->control,bye);
		close_tcp(ftp->control);
		break;
	case RETR_CMD:
		/* Disk operation; return ACK now */
		tcp_output(ftp->control);
#ifndef	CPM
#ifndef	AMIGA
		cdsave = pwd();		/* Save current directory */
		chdir(ftp->cd);		/* Switch to user's directory*/
#endif
#endif
		ftp->fp = fopen(arg,"r");
#ifndef	CPM
#ifndef	AMIGA
		chdir(cdsave);		/* And back */
		free(cdsave);
#endif
#endif
		if(ftp->fp == NULLFILE){
			tprintf(ftp->control,cantopen);
		} else {
			log(ftp->control,"RETR %s/%s",ftp->cd,arg);
			dport.address = ip_addr;
			dport.port = FTPD_PORT;
			ftp->state = SENDING_STATE;
			tprintf(ftp->control,sending,"RETR",arg);

			/* This hack is just so we can talk to ourselves */
			ftp->data = open_tcp(&dport,&ftp->port,TCP_PASSIVE,
			 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);

			ftp->data = open_tcp(&dport,&ftp->port,TCP_ACTIVE,
			 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);
		}
		break;
	case STOR_CMD:
		/* Disk operation; return ACK now */
		tcp_output(ftp->control);
#ifndef	CPM
#ifndef	AMIGA
		cdsave = pwd();		/* Save current directory */
		chdir(ftp->cd);		/* Switch to user's directory */
#endif
#endif	
		ftp->fp = fopen(arg,"w");
#ifndef	CPM
#ifndef	AMIGA
		chdir(cdsave);			/* And back */
		free(cdsave);
#endif
#endif		
		if(ftp->fp == NULLFILE){
			tprintf(ftp->control,cantmake);
		} else {
			log(ftp->control,"STOR %s/%s",ftp->cd,arg);
			dport.address = ip_addr;
			dport.port = FTPD_PORT;
			ftp->state = RECEIVING_STATE;
			tprintf(ftp->control,sending,"STOR",arg);

			/* This hack is just so we can talk to ourselves */
			ftp->data = open_tcp(&dport,&ftp->port,TCP_PASSIVE,
			 0,r_ftpd,NULLVFP,s_ftpd,ftp->control->tos,(int *)ftp);

			ftp->data = open_tcp(&dport,&ftp->port,TCP_ACTIVE,
			 0,r_ftpd,NULLVFP,s_ftpd,ftp->control->tos,(int *)ftp);
		}
		break;
	case PORT_CMD:
		if(pport(&ftp->port,arg) == -1){
			tprintf(ftp->control,badport);
		} else {
			tprintf(ftp->control,portok);
		}
		break;
/* #ifndef CPM */
#ifndef	AMIGA
	case LIST_CMD:
		/* Disk operation; return ACK now */
		tcp_output(ftp->control);

		cdsave = pwd();		/* Save current directory */
		chdir(ftp->cd);		/* Switch to user's directory */
		ftp->fp = dir(arg,1);
		chdir(cdsave);		/* And back */
		free(cdsave);

		if(ftp->fp == NULLFILE){
			tprintf(ftp->control,nodir);
			break;
		}			
		dport.address = ip_addr;
		dport.port = FTPD_PORT;
		ftp->state = SENDING_STATE;
		tprintf(ftp->control,sending,"LIST",arg);

		/* This hack is just so we can talk to ourselves */
		ftp->data = open_tcp(&dport,&ftp->port,TCP_PASSIVE,
		 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);

		ftp->data = open_tcp(&dport,&ftp->port,TCP_ACTIVE,
		 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);
		break;
	case NLST_CMD:
		/* Disk operation; return ACK now */
		tcp_output(ftp->control);

		cdsave = pwd();		/* Save current directory */
		chdir(ftp->cd);		/* Switch to user's directory */
		ftp->fp = dir(arg,0);
		chdir(cdsave);		/* And back */
		free(cdsave);

		if(ftp->fp == NULLFILE){
			tprintf(ftp->control,nodir);
			break;
		}			
		dport.address = ip_addr;
		dport.port = FTPD_PORT;
		ftp->state = SENDING_STATE;
		tprintf(ftp->control,sending,"NLST",arg);

		/* This hack is just so we can talk to ourselves */
		ftp->data = open_tcp(&dport,&ftp->port,TCP_PASSIVE,
		 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);

		ftp->data = open_tcp(&dport,&ftp->port,TCP_ACTIVE,
		 0,NULLVFP,t_ftpd,s_ftpd,ftp->control->tos,(int *)ftp);
		break;
	case CWD_CMD:
		tcp_output(ftp->control);	/* Disk operation; return ACK now */

		cdsave = pwd();		/* Save current directory */
		chdir(ftp->cd);		/* Go to user's context */
		if(chdir(arg) == 0){	/* Attempt switch */
			/* Succeeded, record in control block */
			free(ftp->cd);
			ftp->cd = pwd();
			tprintf(ftp->control,cwdok);
		} else {
			/* Failed, don't change anything */
			tprintf(ftp->control,nodir);
		}
		chdir(cdsave);			/* Go back */
		free(cdsave);
		break;
	case XPWD_CMD:
	case PWD_CMD:
		tprintf(ftp->control,pwdmsg,ftp->cd);
		break;
#else
	case LIST_CMD:
	case NLST_CMD:
	case CWD_CMD:
	case XPWD_CMD:
	case PWD_CMD:
#endif
	case ACCT_CMD:		
	case DELE_CMD:
		tprintf(ftp->control,unimp);
		break;
	case PASS_CMD:
		tprintf(ftp->control,nopass);
		break;
	}
}
static
int
pport(sock,arg)
struct socket *sock;
char *arg;
{
	int32 n;
	int atoi(),i;

	n = 0;
	for(i=0;i<4;i++){
		n = atoi(arg) + (n << 8);
		if((arg = index(arg,',')) == NULLCHAR)
			return -1;
		arg++;
	}
	sock->address = n;
	n = atoi(arg);
	if((arg = index(arg,',')) == NULLCHAR)
		return -1;
	arg++;
	n = atoi(arg) + (n << 8);
	sock->port = n;
	return 0;
}
