/*
	Internet FTP Server
	Copyright 1991 Phil Karn, KA9Q
*/
 
/****************************************************************************
*	$Id: ftpserv.c 1.3 94/02/02 14:38:06 ROOT_DOS Exp $
*	01 Feb 94	1.2		GT	New sendfile () and recvfile () prototypes
*
*  Atari Version by David Nash - dnash@chaos.demon.co.uk
*
*  	Allow access to any disk if ftpuser path is '/'
*     Add CDUP command 
****************************************************************************/

#include <stdio.h>
#include <ctype.h>
#include <time.h>
#ifdef ATARI
#include <errno.h> 	/* ST NOS - sys_errlist > strerror	*/
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#ifdef	__TURBOC__
#include <io.h>
#include <dir.h>
#endif

#include "global.h"
#include "mbuf.h"
#include "proc.h"
#include "socket.h"
#include "dirutil.h"
#include "commands.h"
#include "files.h"
#include "ftp.h"
#include "ftpserv.h"

static void __stdargs 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));

#ifdef ATARI
int	statx(const char *path, struct stat *statbuf);
#endif

/* 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",
	"cdup",
	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 giveanonpass[]="331 Enter Email address as password\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
__stdargs 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;

#ifdef ATARI
	struct stat statbuf;
#endif	
	
	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);
		if ((cp=userlookup(ftp.username,&cp1,NULLCHARP,NULLINT,NULL))!=NULLCHAR)
		{
			if (strcmp(cp1,"@")==0)
				usprintf(ftp.control,giveanonpass);
			else
				usprintf(ftp.control,givepass);
			free(cp);
		}
		else
			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,strerror(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,strerror(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,strerror(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,strerror(errno));
		} else {
			sendit(&ftp,"NLST",file);
		}
		free(file);
		break;
	case CWD_CMD:
	case CDUP_CMD:
		if (*arg == '\0')
			file = pathname(ftp.cd,"..");		/* CDUP	*/
		else
			file = pathname(ftp.cd,arg);		/* CWD	*/

		if(!permcheck(ftp.path,ftp.perms,RETR_CMD,file)){
		 	usprintf(ftp.control,noperm);
			free(file);
			break;
		}
	
#if defined(MSDOS) 
		if(strcmp(file,"/") == 0 || access(file,0) == 0) {

#elif defined(ATARI)
		if(statx(file, &statbuf) == 0 ||
         (file[1] == ':' && file[2] == '\0')) {

#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,strerror(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 CDUP_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,strerror(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,strerror(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,strerror(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;
}

/* 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);
}

#if defined(MSDOS) || defined(ATARI)
/* 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;
{
#if defined(MSDOS) || defined(ATARI)
	char *cp;
#endif

	if(file == NULLCHAR || path == NULLCHAR)
		return 0;	/* Probably hasn't logged in yet */
#if defined(MSDOS) || defined(ATARI)
	/* Check for characters illegal in MS-DOS file names */
	for(cp = badchars;*cp != '\0';cp++){
		if(strchr(file,*cp) != NULLCHAR && file[1] != ':')  /* check ':' DFN */
			return 0;	
	}
#endif
#ifndef MAC
	/* The target file must be under the user's allowed search path */
   /* allow any file if path is just '\' - DFN                     */
	if(strncmp(file,path,strlen(path)) != 0)
      if (strcmp(path, "\\") != 0 && strcmp(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,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,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,strerror(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;
}

