/* Internet FTP Server
 * Copyright 1991 Phil Karn, KA9Q
 */
#include <stdio.h>
#include <ctype.h>
#include <time.h>
#ifdef	__TURBOC__
#include <io.h>
#include <dir.h>
#endif
#include "global.h"
#include "mbuf.h"
#include "socket.h"
#include "ftp.h"
#include "ftpserv.h"
#include "proc.h"
#include "dirutil.h"
#include "commands.h"

static void ftpserv __ARGS((int s,void *unused,void *p));
static int pport __ARGS((struct sockaddr_in *sock,char *arg));
static void ftplogin __ARGS((struct ftpserv *ftp,char *pass));
static int sendit __ARGS((struct ftpserv *ftp,char *command,char *file));
static int recvit __ARGS((struct ftpserv *ftp,char *command,char *file));

/* Command table */
static char *commands[] = {
	"user",
	"acct",
	"pass",
	"type",
	"list",
	"cwd",
	"dele",
	"name",
	"quit",
	"retr",
	"stor",
	"port",
	"nlst",
	"pwd",
	"xpwd",			/* For compatibility with 4.2BSD */
	"mkd ",
	"xmkd",			/* For compatibility with 4.2BSD */
	"xrmd",			/* For compatibility with 4.2BSD */
	"rmd ",
	"stru",
	"mode",
	NULLCHAR
};

/* Response messages */
static char banner[] = "220 %s FTP version %s ready at %s\n";
static char badcmd[] = "500 Unknown command\n";
static char binwarn[] = "100 Warning: type is ASCII and %s appears to be binary\n";
static char unsupp[] = "500 Unsupported command or option\n";
static char givepass[] = "331 Enter PASS command\n";
static char logged[] = "230 Logged in\n";
static char typeok[] = "200 Type %s OK\n";
static char only8[] = "501 Only logical bytesize 8 supported\n";
static char deleok[] = "250 File deleted\n";
static char mkdok[] = "200 MKD ok\n";
static char delefail[] = "550 Delete failed: %s\n";
static char pwdmsg[] = "257 \"%s\" is current directory\n";
static char badtype[] = "501 Unknown type \"%s\"\n";
static char badport[] = "501 Bad port syntax\n";
static char unimp[] = "502 Command not yet implemented\n";
static char bye[] = "221 Goodbye!\n";
static char nodir[] = "553 Can't read directory \"%s\": %s\n";
static char cantopen[] = "550 Can't read file \"%s\": %s\n";
static char sending[] = "150 Opening data connection for %s %s\n";
static char cantmake[] = "553 Can't create \"%s\": %s\n";
static char writerr[] = "552 Write error: %s\n";
static char portok[] = "200 Port command okay\n";
static char rxok[] = "226 File received OK\n";
static char txok[] = "226 File sent OK\n";
static char noperm[] = "550 Permission denied\n";
static char noconn[] = "425 Data connection reset\n";
static char lowmem[] = "421 System overloaded, try again later\n";
static char notlog[] = "530 Please log in with USER and PASS\n";
static char userfirst[] = "503 Login with USER first.\n";
static char okay[] = "200 Ok\n";

static int Sftp = -1;	/* Prototype socket for service */

/* Start up FTP service */
int
ftpstart(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	struct sockaddr_in lsocket;
	int s;

	if(Sftp != -1){
		/* Already running! */
		return 0;
	}
	psignal(Curproc,0);	/* Don't keep the parser waiting */
	chname(Curproc,"FTP listener");

	lsocket.sin_family = AF_INET;
	lsocket.sin_addr.s_addr = INADDR_ANY;
	if(argc < 2)
		lsocket.sin_port = IPPORT_FTP;
	else
		lsocket.sin_port = atoi(argv[1]);

	Sftp = socket(AF_INET,SOCK_STREAM,0);
	bind(Sftp,(char *)&lsocket,sizeof(lsocket));
	listen(Sftp,1);
	for(;;){
		if((s = accept(Sftp,NULLCHAR,(int *)NULL)) == -1)
			break;	/* Service is shutting down */

		if(availmem() < Memthresh){
			usprintf(s,lowmem);
			shutdown(s,1);
		} else {
			/* Spawn a server */
			newproc("ftpserv",2048,ftpserv,s,NULL,NULL,0);
		}
	}
	return 0;
}
static void
ftpserv(s,unused,p)
int s;	/* Socket with user connection */
void *unused;
void *p;
{
	struct ftpserv ftp;
	char **cmdp,buf[512],*arg,*cp,*cp1,*file,*mode;
	long t;
	int cnt,i;
	struct sockaddr_in socket;

	sockmode(s,SOCK_ASCII);
	memset((char *)&ftp,0,sizeof(ftp));	/* Start with clear slate */
	ftp.data = -1;

	sockowner(s,Curproc);		/* We own it now */
	ftp.control = s;
	/* Set default data port */
	i = SOCKSIZE;
	getpeername(s,(char *)&socket,&i);
	socket.sin_port = IPPORT_FTPD;
	ASSIGN(ftp.port,socket);

	log(s,"open FTP");
	time(&t);
	cp = ctime(&t);
	if((cp1 = strchr(cp,'\n')) != NULLCHAR)
		*cp1 = '\0';
	usprintf(s,banner,Hostname,Version,cp);
loop:	if((cnt = recvline(s,buf,sizeof(buf))) == -1){
		/* He closed on us */
		goto finish;
	}
	if(cnt == 0){
		/* Can't be a legal FTP command */
		usprintf(ftp.control,badcmd);
		goto loop;
	}	
	rip(buf);
#ifdef	UNIX
	/* Translate first word to lower case */
	for(cp = buf;*cp != ' ' && *cp != '\0';cp++)
		*cp = tolower(*cp);
#else
	/* Translate entire buffer to lower case */
	for(cp = buf;*cp != '\0';cp++)
		*cp = tolower(*cp);
#endif
	/* Find command in table; if not present, return syntax error */
	for(cmdp = commands;*cmdp != NULLCHAR;cmdp++)
		if(strncmp(*cmdp,buf,strlen(*cmdp)) == 0)
			break;
	if(*cmdp == NULLCHAR){
		usprintf(ftp.control,badcmd);
		goto loop;
	}
	/* Allow only USER, PASS and QUIT before logging in */
	if(ftp.cd == NULLCHAR || ftp.path == NULLCHAR){
		switch(cmdp-commands){
		case USER_CMD:
		case PASS_CMD:
		case QUIT_CMD:
			break;
		default:
			usprintf(ftp.control,notlog);
			goto loop;
		}
	}
	arg = &buf[strlen(*cmdp)];
	while(*arg == ' ')
		arg++;

	/* Execute specific command */
	switch(cmdp-commands){
	case USER_CMD:
		free(ftp.username);
		ftp.username = strdup(arg);
		usprintf(ftp.control,givepass);
		break;
	case TYPE_CMD:
		switch(arg[0]){
		case 'A':
		case 'a':	/* Ascii */
			ftp.type = ASCII_TYPE;
			usprintf(ftp.control,typeok,arg);
			break;
		case 'l':
		case 'L':
			while(*arg != ' ' && *arg != '\0')
				arg++;
			if(*arg == '\0' || *++arg != '8'){
				usprintf(ftp.control,only8);
				break;
			}
			ftp.type = LOGICAL_TYPE;
			ftp.logbsize = 8;
			usprintf(ftp.control,typeok,arg);
			break;
		case 'B':
		case 'b':	/* Binary */
		case 'I':
		case 'i':	/* Image */
			ftp.type = IMAGE_TYPE;
			usprintf(ftp.control,typeok,arg);
			break;
		default:	/* Invalid */
			usprintf(ftp.control,badtype,arg);
			break;
		}
		break;
	case QUIT_CMD:
		usprintf(ftp.control,bye);
		goto finish;
	case RETR_CMD:
		file = pathname(ftp.cd,arg);
		switch(ftp.type){
		case IMAGE_TYPE:
		case LOGICAL_TYPE:
			mode = READ_BINARY;
			break;
		case ASCII_TYPE:
			mode = READ_TEXT;
			break;
		}
		if(!permcheck(ftp.path,ftp.perms,RETR_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if((ftp.fp = fopen(file,mode)) == NULLFILE){
			usprintf(ftp.control,cantopen,file,sys_errlist[errno]);
		} else {
			log(ftp.control,"RETR %s",file);
			if(ftp.type == ASCII_TYPE && isbinary(ftp.fp)){
				usprintf(ftp.control,binwarn,file);
			}
			sendit(&ftp,"RETR",file);
		}
		free(file);
		break;
	case STOR_CMD:
		file = pathname(ftp.cd,arg);
		switch(ftp.type){
		case IMAGE_TYPE:
		case LOGICAL_TYPE:
			mode = WRITE_BINARY;
			break;
		case ASCII_TYPE:
			mode = WRITE_TEXT;
			break;
		}
		if(!permcheck(ftp.path,ftp.perms,STOR_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if((ftp.fp = fopen(file,mode)) == NULLFILE){
			usprintf(ftp.control,cantmake,file,sys_errlist[errno]);
		} else {
			log(ftp.control,"STOR %s",file);
			recvit(&ftp,"STOR",file);
		}
		free(file);
		break;
	case PORT_CMD:
		if(pport(&ftp.port,arg) == -1){
			usprintf(ftp.control,badport);
		} else {
			usprintf(ftp.control,portok);
		}
		break;
#ifndef CPM
	case LIST_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,RETR_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if((ftp.fp = dir(file,1)) == NULLFILE){
			usprintf(ftp.control,nodir,file,sys_errlist[errno]);
		} else {
			sendit(&ftp,"LIST",file);
		}
		free(file);
		break;
	case NLST_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,RETR_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if((ftp.fp = dir(file,0)) == NULLFILE){
			usprintf(ftp.control,nodir,file,sys_errlist[errno]);
		} else {
			sendit(&ftp,"NLST",file);
		}
		free(file);
		break;
	case CWD_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,RETR_CMD,file)){
		 	usprintf(ftp.control,noperm);
			free(file);
#ifdef	MSDOS
		/* Don'tcha just LOVE %%$#@!! MS-DOS? */
		} else if(strcmp(file,"/") == 0 || access(file,0) == 0){
#else
		} else if(access(file,0) == 0){	/* See if it exists */
#endif
			/* Succeeded, record in control block */
			free(ftp.cd);
			ftp.cd = file;
			usprintf(ftp.control,pwdmsg,file);
		} else {
			/* Failed, don't change anything */
			usprintf(ftp.control,nodir,file,sys_errlist[errno]);
			free(file);
		}
		break;
	case XPWD_CMD:
	case PWD_CMD:
		usprintf(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:		
		usprintf(ftp.control,unimp);
		break;
	case DELE_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,DELE_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if(unlink(file) == 0){
			log(ftp.control,"DELE %s",file);
			usprintf(ftp.control,deleok);
		} else {
			usprintf(ftp.control,delefail,sys_errlist[errno]);
		}
		free(file);
		break;
	case PASS_CMD:
		if(ftp.username == NULLCHAR)
			usprintf(ftp.control,userfirst);
		else
			ftplogin(&ftp,arg);			
		break;
#ifndef	CPM
	case XMKD_CMD:
	case MKD_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,MKD_CMD,file)){
			usprintf(ftp.control,noperm);
#ifdef	UNIX
		} else if(mkdir(file,0777) == 0){
#else
		} else if(mkdir(file) == 0){
#endif
			log(ftp.control,"MKD %s",file);
			usprintf(ftp.control,mkdok);
		} else {
			usprintf(ftp.control,cantmake,file,sys_errlist[errno]);
		}
		free(file);
		break;
	case XRMD_CMD:
	case RMD_CMD:
		file = pathname(ftp.cd,arg);
		if(!permcheck(ftp.path,ftp.perms,RMD_CMD,file)){
		 	usprintf(ftp.control,noperm);
		} else if(rmdir(file) == 0){
			log(ftp.control,"RMD %s",file);
			usprintf(ftp.control,deleok);
		} else {
			usprintf(ftp.control,delefail,sys_errlist[errno]);
		}
		free(file);
		break;
	case STRU_CMD:
		if(tolower(arg[0]) != 'f')
			usprintf(ftp.control,unsupp);
		else
			usprintf(ftp.control,okay);
		break;
	case MODE_CMD:
		if(tolower(arg[0]) != 's')
			usprintf(ftp.control,unsupp);
		else
			usprintf(ftp.control,okay);
		break;
	}
#endif
	goto loop;
finish:
	log(ftp.control,"close FTP");
	/* Clean up */
	close_s(ftp.control);
	if(ftp.data != -1)
		close_s(ftp.data);
	if(ftp.fp != NULLFILE)
		fclose(ftp.fp);
	free(ftp.username);
	free(ftp.path);
	free(ftp.cd);
}

/* Shut down FTP server */
int
ftp0(argc,argv,p)
int argc;
char *argv[];
void *p;
{
	close_s(Sftp);
	Sftp = -1;
	return 0;
}
static
int
pport(sock,arg)
struct sockaddr_in *sock;
char *arg;
{
	int32 n;
	int i;

	n = 0;
	for(i=0;i<4;i++){
		n = atoi(arg) + (n << 8);
		if((arg = strchr(arg,',')) == NULLCHAR)
			return -1;
		arg++;
	}
	sock->sin_addr.s_addr = n;
	n = atoi(arg);
	if((arg = strchr(arg,',')) == NULLCHAR)
		return -1;
	arg++;
	n = atoi(arg) + (n << 8);
	sock->sin_port = n;
	return 0;
}

/* Subroutine for logging in the user whose name is name and password is pass.
   The buffer path should be long enough to keep a line from the userfile.
   If pwdignore is true, the password check will be overridden.
   The return value is the permissions field or -1 if the login failed.
   Path is set to point at the path field, and pwdignore will be true if no
   particular password was needed for this user.
 */
int
userlogin(name,pass,path,len,pwdignore)
char *name;
char *pass;
char **path;
int len;			/* Length of buffer pointed at by *path */
int *pwdignore;
{
	char *cp,*cp1;
	FILE *fp;
	int anony,perm;

	if((fp = fopen(Userfile,READ_TEXT)) == NULLFILE)
		/* Userfile doesn't exist */
		return -1;
	while(fgets(*path,len,fp),!feof(fp)){
		if(*path[0] == '#')
			continue;	/* Comment */
		if((cp = strchr(*path,' ')) == NULLCHAR)
			/* Bogus entry */
			continue;
		*cp++ = '\0';		/* Now points to password */
		if(stricmp(name,*path) == 0)
			break;		/* Found user name */
	}
	if(feof(fp)){
		/* User name not found in file */
		fclose(fp);
		return -1;
	}
	fclose(fp);
	/* Look for space after password field in file */
	if((cp1 = strchr(cp,' ')) == NULLCHAR)
		/* Invalid file entry */
		return -1;
	*cp1++ = '\0';	/* Now points to path field */
	anony = *pwdignore;
	if(strcmp(cp,"*") == 0)
		anony = 1;	/* User ID is password-free */
	if(!anony && strcmp(cp,pass) != 0)
		/* Password required, but wrong one given */
		return -1;
	if((cp = strchr(cp1,' ')) == NULLCHAR)
		/* Permission field missing */
		return -1;
	*cp++ = '\0';	/* now points to permission field */
	perm = atoi(cp);
#if   defined(AMIGA)
	/*
	 * Well, on the Amiga, a file can be referenced by many names:
	 * device names (DF0:) or volume names (My_Disk:).  This hunk of code
	 * passed the pathname specified in the ftpusers file, and gets the
	 * absolute path copied into the user's buffer.  We really should just
	 * allocate the buffer and return a pointer to it, since the caller
	 * really doesn't have a good idea how long the path string is..
	 */
	cp1 = pathname("", cp1);
	if (cp1)
		strcpy(*path, cp1);
	else
		**path = '\0';
	free(cp1);
#else
	strcpy(*path,cp1);
	/* Convert any backslashes to forward slashes, for backward
	 * compatibility with the old NET
	 */
	while((cp = strchr(*path,'\\')) != NULLCHAR)
		*cp = '/';
#endif
	*pwdignore = anony;
	/* Finally return the permission bits */
	return perm;
}
	
/* Attempt to log in the user whose name is in ftp->username and password
 * in pass
 */
static void
ftplogin(ftp,pass)
struct ftpserv *ftp;
char *pass;
{
	char *path;
	int anony = 0;

	path = mallocw(200);
	if((ftp->perms = userlogin(ftp->username,pass,&path,200,&anony))
	   == -1){
		usprintf(ftp->control,noperm);
		free(path);
		return;
	}
	/* Set up current directory and path prefix */
#if	defined(AMIGAGONE)
	ftp->cd = pathname("", path);
	ftp->path = strdup(ftp->cd);
	free(path);
#else
	ftp->cd = path;
	ftp->path = strdup(path);
#endif

	usprintf(ftp->control,logged);
	if(!anony)
		log(ftp->control,"%s logged in",ftp->username);
	else
		log(ftp->control,"%s logged in, ID %s",ftp->username,pass);
}

#ifdef	MSDOS
/* Illegal characters in a DOS filename */
static char badchars[] = "\"[]:|<>+=;,";
#endif

/* Return 1 if the file operation is allowed, 0 otherwise */
int
permcheck(path,perms,op,file)
char *path;
int perms;
int op;
char *file;
{
#ifdef	MSDOS
	char *cp;
#endif

	if(file == NULLCHAR || path == NULLCHAR)
		return 0;	/* Probably hasn't logged in yet */
#ifdef	MSDOS
	/* Check for characters illegal in MS-DOS file names */
	for(cp = badchars;*cp != '\0';cp++){
		if(strchr(file,*cp) != NULLCHAR)
			return 0;	
	}
#endif
#ifndef MAC
	/* The target file must be under the user's allowed search path */
	if(strncmp(file,path,strlen(path)) != 0)
		return 0;
#endif

	switch(op){
	case RETR_CMD:
		/* User must have permission to read files */
		if(perms & FTP_READ)
			return 1;
		return 0;
	case DELE_CMD:
	case RMD_CMD:
		/* User must have permission to (over)write files */
		if(perms & FTP_WRITE)
			return 1;
		return 0;
	case STOR_CMD:
	case MKD_CMD:
		/* User must have permission to (over)write files, or permission
		 * to create them if the file doesn't already exist
		 */
		if(perms & FTP_WRITE)
			return 1;
		if(access(file,2) == -1 && (perms & FTP_CREATE))
			return 1;
		return 0;
	}
	return 0;	/* "can't happen" -- keep lint happy */
}
static int
sendit(ftp,command,file)
struct ftpserv *ftp;
char *command;
char *file;
{
	long total;
	struct sockaddr_in dport;

	ftp->data = socket(AF_INET,SOCK_STREAM,0);
	dport.sin_family = AF_INET;
	dport.sin_addr.s_addr = INADDR_ANY;
	dport.sin_port = IPPORT_FTPD;
	bind(ftp->data,(char *)&dport,SOCKSIZE);
	usprintf(ftp->control,sending,command,file);
	if(connect(ftp->data,(char *)&ftp->port,SOCKSIZE) == -1){
		fclose(ftp->fp);
		ftp->fp = NULLFILE;
		close_s(ftp->data);
		ftp->data = -1;
		usprintf(ftp->control,noconn);
		return -1;
	}
	/* Do the actual transfer */
	total = sendfile(ftp->fp,ftp->data,ftp->type,0);

	if(total == -1){
		/* An error occurred on the data connection */
		usprintf(ftp->control,noconn);
		shutdown(ftp->data,2);	/* Blow away data connection */
	} else {
		usprintf(ftp->control,txok);
	}
	fclose(ftp->fp);
	ftp->fp = NULLFILE;
	close_s(ftp->data);
	ftp->data = -1;
	if(total == -1)
		return -1;
	else
		return 0;
}
static int
recvit(ftp,command,file)
struct ftpserv *ftp;
char *command;
char *file;
{
	struct sockaddr_in dport;
	long total;

	ftp->data = socket(AF_INET,SOCK_STREAM,0);
	dport.sin_family = AF_INET;
	dport.sin_addr.s_addr = INADDR_ANY;
	dport.sin_port = IPPORT_FTPD;
	bind(ftp->data,(char *)&dport,SOCKSIZE);
	usprintf(ftp->control,sending,command,file);
	if(connect(ftp->data,(char *)&ftp->port,SOCKSIZE) == -1){
		fclose(ftp->fp);
		ftp->fp = NULLFILE;
		close_s(ftp->data);
		ftp->data = -1;
		usprintf(ftp->control,noconn);
		return -1;
	}
	total = recvfile(ftp->fp,ftp->data,ftp->type,0);

#ifdef	CPM
	if(ftp->type == ASCII_TYPE)
		putc(CTLZ,ftp->fp);
#endif
	if(total == -1) {
		/* An error occurred while writing the file */
		usprintf(ftp->control,writerr,sys_errlist[errno]);
		shutdown(ftp->data,2);	/* Blow it away */
	} else {
		usprintf(ftp->control,rxok);
		close_s(ftp->data);
	}
	ftp->data = -1;
	fclose(ftp->fp);
	ftp->fp = NULLFILE;
	if(total == -1)
		return -1;
	else
		return 0;
}
