/*
 * ftp.c : This code implements an asynchronous ftp client.
 *
 * George Ferguson, ferguson@cs.rochester.edu, 23 Apr 1993.
 *
 * The client is represented by an instance of the FtpContext structure,
 * which includes the state of the client with respect to a finite-state
 * model of the FTP process. All aspects of the connection are contained
 * in the structure, so a process can have multiple outstanding FTP
 * sessions (subject to filesystem limitations, of course).
 *
 * Refer to RFC959 for details of the state model.
 *
 * Two external functions must be provided:
 *   void RegisterFtpFd( ftpc, fd, flags, func )
 *      Should arrange to call FUNC passing FTPC whenever FD is ready
 *      as indicated by FLAGS (from <fcntl.h>).
 *   void UnregisterFtpFd( ftpc, fd )
 *      Cancels a previous registration of FD for FTPC.
 * Samples of these using select(2) are given below in the STANDALONE
 * section. Xarchie uses the XtAppAddInput() mechanism. The code also
 * uses alert0() and status0() for messages, sysError() for error
 * messages from system calls, and ftpPrompt() if prompting is enabled.
 *
 * To use these routines:
 *   ftpc = ftpNewContext(...);
 *   ftpStart(ftpc);
 * The function you registered as the "done" parameter in ftpNewContext
 * will be called back when the process finishes (for any reason). You
 * should call ftpFreeContext() in this callback (or register it as the
 * callback, or register NULL).
 *
 * This code is decidely *not* a general-purpose FTP library. It is
 * designed to support the batch transfers required by xarchie. I suppose
 * that it could be modified to be more general without too much effort.
 * In particular, many of the routines called by ftpProcessReply could
 * be made into external interface functions. Volunteers?
 *
 * 28 Apr 1993: Status mesage for bytes transferred was backwards.
 *  1 Jun 1993: Fixes for ISC. Are these needed for other SYS5?
 * 30 Jun 1993: Fixes for Encore Umax (used cpp symbol "umax" as a guess).
 * 24 Aug 1993: Allow 200 reply for CWD as well as 250.
 *              Non-blocking IO defs for NeXT.
 *  5 Jul 1994: Fixed bug in CR stripping.
 *              Hopefully improved size-of-file detection.
 *              Non-blocking IO defs for Ultrix.
 */
#include <stdio.h>
#include <errno.h>
#ifndef MSDOS
extern int errno;	/* Encore needs this despite <errno.h> */
#endif
#include "config.h"
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif
#include "ftp.h"	/* includes <sys/types.h> and <netinet/in.h> */
#include "sysdefs.h"	/* char *malloc() */
#include "stringdefs.h" /* char *strcpy(), *index(); */
#include "debug.h"	/* MSG[0-3]() macros */

/* These functions have to be provided: */
extern void RegisterFtpFd(/* ftpc, fd, O_RDWR | O_RDONLY, func */);
extern void UnregisterFtpFd(/* ftpc, fd */);
extern int ftpPrompt(/* ftpc */);
extern void status0(/* str */);
extern void alert0(/* str */);
extern void sysError(/* str */);

/*
 * Portable non-blocking I/O macros, I hope.
 *   hp300 : From Andy.Linton@comp.vuw.ac.nz
 *   sgi   : From amoss@cs.huji.ac.il
 *   umax  : From Gerry.Tomlinson@newcastle.ac.uk
 *   NeXT  : From jk@zarniwoop.pc-labor.uni-bremen.de
 *   ultrix: From hurmejw@pb.com from cmr@world.std.com
 *   __clipper__: From mlcampb1@mikec.b30.ingr.com (Intergraph, apparently)
 *   alliant : From rdr@gscs.com
 */
#if defined(hp300) || defined(NeXT) || defined(alliant)
  /* Here's for BSD, maybe, but Sys5 can't tell this from EOF. */
# include <fcntl.h>
# define MAKE_NONBLOCKING(fd) fcntl(fd,F_SETFL,O_NDELAY)
# define ITWOULDBLOCK EWOULDBLOCK
#else
#if defined(sgi) || defined(ultrix) || defined(__clipper__)
# include <fcntl.h>
# define MAKE_NONBLOCKING(fd) fcntl(fd,F_SETFL,O_NONBLOCK)
# define ITWOULDBLOCK EWOULDBLOCK
#else
#if defined(umax)
# define MAKE_NONBLOCKING(fd) fcntl(fd,F_SETFL,FNDELAY)
# define ITWOULDBLOCK EWOULDBLOCK
#else
  /* This is POSIX, the default, which uses EAGAIN. */
# include <fcntl.h>
# define MAKE_NONBLOCKING(fd) fcntl(fd,F_SETFL,O_NONBLOCK)
# define ITWOULDBLOCK EAGAIN
#endif /* umax */
#endif /* sgi || ultrix || __clipper__ */
#endif /* hp300 || NeXT || alliant */
/*
 * Networking includes from Brendan Kehoe (dirsend.c)
 */
#include <netdb.h>
#include <sys/socket.h>
#ifndef hpux
# include <arpa/inet.h>
#endif
/* Interactive UNIX keeps some of the socket definitions in funny places.  */
#ifdef ISC
# include <net/errno.h>		/* I wonder about this... */
#endif /* ISC */

#ifndef IPPORT_FTP
#define IPPORT_FTP 21		/* From <netinet/in.h> */
#endif

#define NewString(S)		strcpy(malloc(strlen(S)+1),S)
#define ftpfilecmdstr(X)	((X)==FTP_GET ? "GET" : "PUT")

/*
 * Functions defined here:
 */
FtpContext *ftpNewContext();
void ftpStart(),ftpAbort();
void ftpFreeContext();

/* Initialization */
static int ftpGetHostAndPort();
/* Connection establishment */
static void ftpConnectCallback();
static int ftpGetSocket(),ftpConnect();
/* Reading server replies */
static void ftpInitReply(),ftpReplyCallback();
/* Main FTP client state-machine */
static void ftpProcessReply();
static void ftpAlert(),ftpHandleErrorThenQuit(),ftpDone();
static void ftpSendType(),ftpSendNextCwd();
static int ftpSendPort();
static void ftpSendGetPut();
static int ftpAcceptDataConn(),ftpSetupLocalData();
/* Data connection processing */
static void ftpDataCallback();
static void ftpReadData(),ftpWriteData(),ftpCleanupDataConn();
/* Sending commands to the server */
static void ftpStartSendingCmd(),ftpWriteCallback();
static void ftpSendCmd(),ftpSendAbort();
#ifdef DEBUG
char *ftpstatestr();
#endif

/*	-	-	-	-	-	-	-	-	*/
/* Exported functions: */

/*
 * Returns a pointer to a new FtpContext for which the connection is
 * started. The external function RegisterFtpFd() is used to register
 * the control socket for subsequent processing by ftpProcess().
 */
FtpContext *
ftpNewContext(hostname,user,pass,cwd,local_dir,type,stripcr,prompt,
	      filecmd,files,num_files,trace,done1,done)
char *hostname,*user,*pass,*cwd,*local_dir;
int type,stripcr,prompt,filecmd;
char **files;
int num_files;
FtpTraceProc trace;
FtpCallbackProc done1,done;
{
    FtpContext *ftpc;
    int i;

    /* Initialize the data structure */
    if ((ftpc=(FtpContext *)malloc(sizeof(FtpContext))) == NULL) {
	fprintf(stderr,"ftpNewContext: malloc failed\n");
	return(NULL);
    }
    /* User-specified stuff */
    ftpc->hostname = NewString(hostname);
    ftpc->user = NewString(user);
    ftpc->pass = NewString(pass);
    ftpc->cwd = NewString(cwd);
    ftpc->wd = ftpc->cwd;
    ftpc->local_dir = NewString(local_dir);
    ftpc->type = type;
    ftpc->stripcr = stripcr;
    ftpc->prompt = prompt;
    ftpc->filecmd = filecmd;
    ftpc->num_files = num_files;
    ftpc->files = (char **)calloc(num_files,sizeof(char *));
    for (i=0; i < num_files; i++)
	ftpc->files[i] = NewString(files[i]);
    ftpc->this_file = -1;	/* preincrement later */
    ftpc->this_size = 0;
    ftpc->trace = trace;
    ftpc->done1 = done1;
    ftpc->done = done;
    /* Default stuff */
    ftpc->reply[0] = '\0';
    ftpc->stripcr = 1;
    /* Undefined stuff */
    ftpc->ctrl = ftpc->data = ftpc->port = ftpc->local_fd = -1;
    ftpc->state = ftpc->iostate = ftpc->reply_state = ftpc->saved_state = 0;
    ftpc->retcode = ftpc->tmpcode = 0;
    ftpc->cmd = NULL;
    /* Go start the connection */
    if (ftpGetHostAndPort(ftpc) < 0) {
	ftpFreeContext(ftpc);
	return(NULL);
    }
    ftpc->state = 0;
    return(ftpc);
}

void
ftpFreeContext(ftpc)
FtpContext *ftpc;
{
    int i;

    DEBUG1("ftpFreeContext: ftpc=0x%x\n",ftpc);
    if (ftpc->ctrl != -1) {
	UnregisterFtpFd(ftpc,ftpc->ctrl);
	DEBUG1("ftpFreeContext: closing ctrl %d\n",ftpc->ctrl);
	close(ftpc->ctrl);
    }
    if (ftpc->data != -1) {
	UnregisterFtpFd(ftpc,ftpc->data);
	DEBUG1("ftpFreeContext: closing data %d\n",ftpc->data);
	close(ftpc->data);
    }
    if (ftpc->port != -1) {
	DEBUG1("ftpFreeContext: closing port %d\n",ftpc->port);
	close(ftpc->port);
    }
    if (ftpc->local_fd != -1) {
	DEBUG1("ftpFreeContext: closing local_fd %d\n",ftpc->local_fd);
	close(ftpc->local_fd);
    }
    if (ftpc->hostname)
	free(ftpc->hostname);
    if (ftpc->user)
	free(ftpc->user);
    if (ftpc->pass)
	free(ftpc->pass);
    if (ftpc->cwd)
	free(ftpc->cwd);
    if (ftpc->local_dir)
	free(ftpc->local_dir);
    if (ftpc->files) {
	for (i=0; i < ftpc->num_files; i++)
	    if (ftpc->files[i]) free(ftpc->files[i]);
	free((char *)(ftpc->files));
    }
    if (ftpc->h_addr_list)
	free((char *)(ftpc->h_addr_list));
    DEBUG0("ftpFreeContext: done\n");
}

void
ftpStart(ftpc)
FtpContext *ftpc;
{
    DEBUG1("ftpStart: ftpc=0x%x\n",ftpc);
    ftpc->state = FTPS_OPEN;
    ftpConnectCallback(ftpc);		/* get things started */
    DEBUG0("ftpStart: done\n");
}

void
ftpAbort(ftpc)
FtpContext *ftpc;
{
    DEBUG1("ftpAbort: ftpc=0x%x\n",ftpc);
    /*
     * If we're aborting before any files transferred or after an
     * abort or quit, just close down the whole connection.
     */
    if (ftpc->state < FTPS_READY || ftpc->state > FTPS_EOF) {
	ftpDone(ftpc);
	return;
    }
    /*
     * Close the dataconn so we don't get swamped in select() while
     * waiting for the response to the abort on the ctrlconn.
     */
    if (ftpc->data != -1) {
	UnregisterFtpFd(ftpc,ftpc->data);
    }
    /* Then send the abort sequence */
    ftpSendAbort(ftpc);
    DEBUG0("ftpAbort: done\n");
}

/*	-	-	-	-	-	-	-	-	*/
/* Initialization: */
/*
 * This function is called from ftpNewContext() to initialize the host
 * address information in the FTPC.
 */
static int
ftpGetHostAndPort(ftpc)
FtpContext *ftpc;
{
    struct servent *serv;
    struct hostent *host;
    unsigned long hostaddr;
    char msg[256];
    int i;

    DEBUG2("ftpGetHostAndPort: ftpc=0x%x: \"%s\"\n",ftpc,ftpc->hostname);
    /*
     * Get ftp port
     */
    DEBUG0("ftpGetHostAndPort: getting ftp service port\n");
    serv = getservbyname("ftp","tcp");
    /* UCX needs 0 or -1 */
    if (serv == (struct servent *)0 || serv == (struct servent *)-1) {
	alert0("Can't find service 'ftp/tcp' in list of services -- using default port");
	ftpc->servport = htons((unsigned short)IPPORT_FTP);
    } else {
	ftpc->servport = (unsigned short)serv->s_port;
    }
    DEBUG1("ftpGetHostAndPort: ftp service port is %d\n",ftpc->servport);
    /*
     * Get host address
     */
    sprintf(msg,"Getting address for host \"%.200s\"",ftpc->hostname);
    status0(msg);
    if ((host=gethostbyname(ftpc->hostname)) == NULL) {
	DEBUG0("ftpGetHostAndPort: gethostbyname failed, trying inet_addr()\n");
	/*
	 * If gethostbyname fails, then maybe we've been given an IP
	 * address directly. Let's see.
	 */
	hostaddr = inet_addr(ftpc->hostname);
	if (hostaddr == (unsigned long)-1) {
	    /*
	     * Nope - complete failure.
	     */
	    sprintf(msg,"Can't find address of host \"%.200s\"",
		    ftpc->hostname);
	    alert0(msg);
	    return(-1);
	} else {
	    /*
	     * inet_addr succeeded, so we make a dummy array of hostaddrs.
	     */
	    ftpc->h_addr_list = (char **)calloc(2,sizeof(char *));
	    bcopy((char *)&hostaddr,ftpc->h_addr_list[0],sizeof(char *));
	    hostaddr = (unsigned long)0;
	    bcopy((char *)&hostaddr,ftpc->h_addr_list[1],sizeof(char *));
	    ftpc->this_addr = ftpc->h_addr_list-1; /* preincrement later */
	    DEBUG1("ftpGetHostAndPort: inet_addr returned %s\n",
		   inet_ntoa(*(struct in_addr*)(ftpc->h_addr_list[0])));
	    return(0);
	}
    } else {
	/*
	 * If gethostbyname succeeeds, it fills in a list of addresses.
	 * We'll copy them into the ftpc.
	 */
	for (i=0; host->h_addr_list[i]; i++)
	    /*EMPTY*/;
	DEBUG1("ftpGetHostAndPort: gethostbynname returned %d addr(s)\n",i);
	ftpc->h_addr_list = (char **)calloc(i+1,sizeof(char *));
	for (i=0; host->h_addr_list[i]; i++) {
	    bcopy(host->h_addr_list[i],
		  (char *)(ftpc->h_addr_list+i),sizeof(char *));
	    DEBUG2("ftpGetHostAndPort: h_addr_list[%d] = %s\n",i,
		   inet_ntoa(*(struct in_addr*)(ftpc->h_addr_list+i)));
	}
	bzero((char *)(ftpc->h_addr_list+i),sizeof(char *));
	ftpc->this_addr = ftpc->h_addr_list-1;	/* preincrement later */
	return(0);
    }
}

/*	-	-	-	-	-	-	-	-	*/
/* Connection establishment: */
/*
 * This function goes through the list of addresses trying to connect to
 * the server. It is initially called from ftpNewContext(), and subsequently
 * whenever the ctrl socket it ready for writing (indicating connect()
 * completed, possibly with an error).
 */
static void
ftpConnectCallback(ftpc)
FtpContext *ftpc;
{
    int retcode;
    char msg[256];

    DEBUG2("ftpConnectCallback: ftpc=0x%x, state=%s\n",
	   ftpc,ftpstatestr(ftpc->state));
  redo:
    switch (ftpc->state) {
      case FTPS_OPEN:
	ftpc->this_addr += 1;			/* preincrement */
	DEBUG1("ftpConnectCallback: OPEN: %s\n",
	       inet_ntoa(*(struct in_addr*)(ftpc->this_addr)));
	if (*(ftpc->this_addr)) {		/* Not at last host */
	    if (ftpGetSocket(ftpc) < 0) {
		ftpDone(ftpc);
	    } else {
		/* Arrange to get called back if connect() would block */
		RegisterFtpFd(ftpc,ftpc->ctrl,O_RDWR,ftpConnectCallback);
		ftpc->state = FTPS_CONNECT;
		goto redo;
	    }
	} else {				/* All hosts failed */
	    sprintf(msg,"Couldn't connect to host \"%s\"",ftpc->hostname);
	    alert0(msg);
	    ftpDone(ftpc);
	}
	break;
      case FTPS_CONNECT:
	/* We previously registered either above or below */
	UnregisterFtpFd(ftpc,ftpc->ctrl);
	retcode = ftpConnect(ftpc);
	if (retcode < 0) {
	    DEBUG1("ftpConnectCallback: ftpConnect failed, closing ctrl %d\n",
		   ftpc->ctrl);
	    close(ftpc->ctrl);
	    ftpc->ctrl = -1;
	    ftpc->state = FTPS_OPEN;		/* try next one */
	    goto redo;
	} else if (retcode == 0) {
	    DEBUG0("ftpConnectCallback: ftpConnect would block\n");
	    /* Arrange to get called back when connect() won't block */
	    RegisterFtpFd(ftpc,ftpc->ctrl,O_RDWR,ftpConnectCallback);
	    ftpc->state = FTPS_CONNECT;		/* try again when ready */
	} else {
	    DEBUG0("ftpConnectCallback: ftpConnect ok\n");
	    ftpInitReply(ftpc);			/* ok... */
	    ftpc->state = FTPS_CONNECTED;	/* move on */
	}
	break;
      default:
	fprintf(stderr,"ftpConnectCallback: unknown state %d\n",ftpc->state);
	abort();
    }
    DEBUG0("ftpConnectCallback: done\n");
}
	    
/*	-	-	-	-	-	-	-	-	*/
/* Functions called by ftpConnectCallback(): */
/*
 * This function opens a new socket for the ctrl connection, and sets
 * it up for non-blocking IO. Everything is setup for later connect().
 */
static int
ftpGetSocket(ftpc)
FtpContext *ftpc;
{
    DEBUG1("ftpGetSocket: addr=%s\n",
	   inet_ntoa(*(struct in_addr*)(ftpc->this_addr)));
    /* Get a socket */
    if ((ftpc->ctrl=socket(AF_INET,SOCK_STREAM,0)) < 0) {
	sysError("socket(ftpGetSocket)");
	return(-1);
    }
    DEBUG1("ftpGetSocket: socket() returned %d\n",ftpc->ctrl);
    /*
     * According to jbh@moses.aii.COM, we can't do this until we connect
     * on (at least) ISC variants of SYS5. I have not had problems with
     * other SYS5 systems, including HPUX. When ISC is defined, we do it
     * in ftpConnect(), although we may then block in connect().
     */
#ifndef ISC
    /* Setup socket for async i/o */
    MAKE_NONBLOCKING(ftpc->ctrl);
#endif
    /* Set up the address spec. */
    bzero((char *)&(ftpc->saddr),sizeof(struct sockaddr_in));
    ftpc->saddr.sin_family = AF_INET;
    ftpc->saddr.sin_port = ftpc->servport;
    ftpc->saddr.sin_addr = *((struct in_addr *)(ftpc->this_addr));
    /* Ready to connect() */
    DEBUG0("ftpGetSocket: done\n");
    return(0);
}

/*
 * Calls connect(). Returns -1 if this host is botched, 0 if connect()
 * would have blocked, or 1 if we should proceed. 
 */
static int
ftpConnect(ftpc)
FtpContext *ftpc;
{
    int retcode,addrlen;
    char msg[256];

    sprintf(msg,"Connecting to %.200s (%s)",
	    ftpc->hostname,inet_ntoa(*(struct in_addr*)(ftpc->this_addr)));
    status0(msg);
    DEBUG0("ftpConnect: calling connect()...\n");
    retcode = connect(ftpc->ctrl,(struct sockaddr *)&(ftpc->saddr),
		      sizeof(struct sockaddr_in));
    if (retcode < 0 && errno == EINPROGRESS) {
	DEBUG0("ftpConnect: connect() EINPROGRESS\n");
	return(0);
    } else if (retcode < 0 && errno != EISCONN) {
#ifdef DEBUG
	perror("ftpConnect: connect()");
#endif
	return(-1);
    }
#ifdef DEBUG
      else if (retcode < 0 && errno == EISCONN)
	  fprintf(stderr,"ftpConnect: connect() EISCONN\n");
      else
	  fprintf(stderr,"ftpConnect: connect() returned ok\n");
#endif
     /* According to jbh@moses.aii.COM, see comments in ftpGetSocket(). */
#ifdef ISC
    MAKE_NONBLOCKING(ftpc->ctrl); /* Setup socket for async i/o */
#endif
    /* Get name (address) of socket */
    addrlen = sizeof(struct sockaddr_in);
    retcode =
	getsockname(ftpc->ctrl,(struct sockaddr *)&(ftpc->saddr),&addrlen);
    if (retcode < 0) {
	sysError("getsockname(ftpConnect)");
	return(-1);
    }
    DEBUG1("ftpConnect: getsockname() returned %s\n",
	   inet_ntoa(ftpc->saddr.sin_addr));
    return(1);
}

/*	-	-	-	-	-	-	-	-	*/
/* Reading server replies: */
/*
 * This function must be called to initialize the reply in the FTPC and
 * to ensure that the appropriate callback is registered to read it.
 */
static void
ftpInitReply(ftpc)
FtpContext *ftpc;
{
    DEBUG1("ftpInitReply: ftpc=0x%lx\n",ftpc);
    ftpc->retcode = 0;
    ftpc->reply_len = 0;
    *(ftpc->reply) = '\0';
    ftpc->reply_state = FTPS_REPLY_CODE;
    /* We're expecting a reply, so register ctrl for reading only */
    RegisterFtpFd(ftpc,ftpc->ctrl,O_RDONLY,ftpReplyCallback);
    DEBUG0("ftpInitReply: done\n");
}

/*
 * This function is the low-level reply parser. It reads and
 * parses reply messages from the server, handles TELNET commands,
 * and translate codes in the reply prefix into integers. It stores the
 * reply code in ftpc->retcode and puts the text of the reply in ftpc->reply.
 * It is called whenever the ctrlconn is ready for reading and we are
 * expecting a reply (ie., after ftpInitReply()). Once the reply is
 * complete, we call ftpProcessReply().
 */
static void
ftpReplyCallback(ftpc)
FtpContext *ftpc;
{
    int n;
    unsigned char c,cc;

    for (;;) {
/*
	DEBUG2("ftpReplyCallback: ftpc=0x%x, reply_state=%s\n",
	       ftpc,ftpstatestr(ftpc->reply_state));
*/
	n = read(ftpc->ctrl,&c,1);		/* Read a byte */
	if (n < 0 && errno != ITWOULDBLOCK) {		/* Error */
	    sysError("read(ftpReplyCallback)");
	    DEBUG0("ftpReplyCallback: error reading ctrlconn\n");
	    ftpDone(ftpc);
	    return;
	} else if (n < 0 && errno == ITWOULDBLOCK) {	/* Would block */
	    /*DEBUG0("ftpReplyCallback: ctrlconn would block\n");*/
	    return;
	} else if (n == 0) {			/* EOF */
	    *(ftpc->reply+ftpc->reply_len) = '\0';
	    /* Could check against current retcode, if any... */
	    if (ftpc->state == FTPS_QUIT)
		ftpc->retcode = FTP_SERVICE_CLOSING;
	    else
		ftpc->retcode = FTP_SERVICE_UNAVAILABLE;
	    DEBUG1("ftpReplyCallback: EOF: retcode = %d\n",ftpc->retcode);
	    /* Reply is done, go process it */
	    break;
	}
	/*
	 * Otherwise, we got something, process it.
	 */
	if (c == IAC) {			/* Telnet IAC */
	    ftpc->saved_state = ftpc->reply_state;
	    ftpc->reply_state = FTPS_REPLY_IAC1;
	    DEBUG1("ftpReplyCallback: IAC (saved_state=%d)\n",ftpc->saved_state);
	    continue;
	} else if (c == '\r') {		/* Skip <CR>, is this ok? */
	    continue;
	}
	/*
	 * We got something that's not IAC, process it.
	 */
	/*DEBUG2("ftpReplyCallback: `%c' (%d)\n",c,c);*/
	switch (ftpc->reply_state) {
	  case FTPS_REPLY_IAC1:
	    DEBUG1("ftpReplyCallback: IAC %c",c);
	    ftpc->reply_state = FTPS_REPLY_IAC2;
	    break;
	  case FTPS_REPLY_IAC2:
	    switch (c) {
	      case WILL:
	      case WONT: c = DONT;
			 break;
	      case DO:
	      case DONT: c = WONT;
			 break;
	      default:
		DEBUG0(" (ignored)\n");
		continue;
	    }
	    DEBUG1(", reply %c\n",c);
	    cc = IAC;
	    write(ftpc->ctrl,&cc,1);
	    write(ftpc->ctrl,&c,1);
	    ftpc->reply_state = FTPS_REPLY_IAC3;
	    break;
	  case FTPS_REPLY_IAC3:
	    write(ftpc->ctrl,&c,1);
	    ftpc->reply_state = ftpc->saved_state;
	    DEBUG1("ftpReplyCallback: IAC done, restored state = %d\n",
		   ftpc->reply_state);
	    break;
	  case FTPS_REPLY_CODE:
	    if (c < '0' || c > '9') {
		*(ftpc->reply+ftpc->reply_len) = '\0';
		DEBUG1("ftpReplyCallback: CODE: retcode = %d\n",ftpc->retcode);
		goto done;
	    }
	    *(ftpc->reply+ftpc->reply_len++) = c;
	    ftpc->retcode = ftpc->retcode * 10 + c - '0';
	    /*DEBUG1("ftpReplyCallback: retcode now %d\n",ftpc->retcode);*/
	    if (ftpc->retcode >= 100)
		ftpc->reply_state = FTPS_REPLY_CONT;
	    break;
	  case FTPS_REPLY_CONT:
	    /* we reach here after we finished reading the code or when we
	     * struck a line beginning with at least three digits, check if
	     * this is the last line of the reply
	     */
	    *(ftpc->reply+ftpc->reply_len++) = c;
	    if (c == '-') {
/*
		DEBUG0("ftpReplyCallback: continuation\n");
*/
		ftpc->reply_state = FTPS_REPLY_MORE;
	    } else {
/*
		DEBUG0("ftpReplyCallback: final line\n");
*/
		ftpc->reply_state = FTPS_REPLY_LAST;
	    }
	    break;
	  case FTPS_REPLY_LAST:
	    if (c == '\n') {
		*(ftpc->reply+ftpc->reply_len) = '\0';
/*
		DEBUG1("ftpReplyCallback: LAST: retcode = %d\n",ftpc->retcode);
*/
		goto done;
	    } else {
		*(ftpc->reply+ftpc->reply_len++) = c;
/*
		DEBUG3("ftpReplyCallback: LAST: %03d: \"%.*s\"\n",
		     ftpc->reply_len,ftpc->reply_len,ftpc->reply);
*/
	    }
	    break;
	  case FTPS_REPLY_MORE:
	    *(ftpc->reply+ftpc->reply_len++) = c;
/*
	    DEBUG3("ftpReplyCallback: MORE: %03d: \"%.*s\"\n",
		   ftpc->reply_len,ftpc->reply_len,ftpc->reply);
*/
	    if (c == '\n') {
		ftpc->tmpcode = 0;
		ftpc->reply_state = FTPS_REPLY_CHCK;
	    }
	    break;
	  case FTPS_REPLY_CHCK:
	    if (c < '0' || c > '9') {
		*(ftpc->reply+ftpc->reply_len++) = c;
/*
		DEBUG3("ftpReplyCallback: CHCK: %03d: \"%.*s\"\n",
		       ftpc->reply_len,ftpc->reply_len,ftpc->reply);
*/
		ftpc->reply_state = FTPS_REPLY_MORE;
	    } else {
		ftpc->tmpcode = ftpc->tmpcode * 10 + c - '0';
		if (ftpc->tmpcode >= 100) {
		    if (ftpc->tmpcode != ftpc->retcode)
			ftpc->reply_state = FTPS_REPLY_MORE;
		    else
			ftpc->reply_state = FTPS_REPLY_CONT;
		}
	    }
	    break;
	  default:
	    fprintf(stderr,"ftpReplyCallback: unknown reply_state %d\n",
		    ftpc->reply_state);
	    abort();
	}
    }
    /* if we get here, the reply is complete */
  done:
    UnregisterFtpFd(ftpc,ftpc->ctrl);
    if (ftpc->trace) {
	(*(ftpc->trace))(ftpc,0,ftpc->reply);
    }
    ftpProcessReply(ftpc);
}

/*	-	-	-	-	-	-	-	-	*/
/*
 * This function implements the high-level FTP protocol using the state
 * field of the FTPC. It is called from ftpReplyCallback() once we've read
 * a reply and need to process it.
 */
static void
ftpProcessReply(ftpc)
FtpContext *ftpc;
{
    char cmd[256];

    DEBUG3("ftpProcessReply: ftpc=0x%x, state=%s, retcode=%d\n",
	   ftpc,ftpstatestr(ftpc->state),ftpc->retcode);
  redo:
    switch (ftpc->state) {
      case FTPS_CONNECTED:
	if (ftpc->retcode == FTP_SERVICE_RDY_TIME) {	/* delay NNN minutes */
	    DEBUG0("ftpProcessReply: server not ready\n");
	    ftpAlert(ftpc);
	} else if (ftpc->retcode == FTP_SERVICE_RDY_USER) {/* ready for USER */
	    sprintf(cmd,"USER %s",ftpc->user);
	    status0(cmd);
	    ftpc->state = FTPS_USER;
	    ftpSendCmd(ftpc,cmd);
	} else {				/* FTP_SERVICE_UNAVAILABLE */
	    ftpAlert(ftpc);
	    ftpDone(ftpc);
	}
	break;
      case FTPS_USER:
	if (ftpc->retcode == FTP_LOGIN_OK) {	/* USER ok, no PASS needed */
	    ftpc->retcode = FTP_FILE_ACTION_OK;
	    ftpc->state = FTPS_CWD;
	    goto redo;
	} else if (ftpc->retcode == FTP_LOGIN_NEED_PASSWD) {	/* USER ok   */
	    sprintf(cmd,"PASS %s",ftpc->pass);			/* need PASS */
	    status0(cmd);
	    ftpc->state = FTPS_PASS;
	    ftpSendCmd(ftpc,cmd);
	} else {				/* ACCT needed or error */
	    ftpHandleErrorThenQuit(ftpc);
	}
	break;
      case FTPS_PASS:
	if (ftpc->retcode == FTP_LOGIN_OK) {	/* PASS ok, ready to go */
	    ftpc->retcode = FTP_FILE_ACTION_OK;
	    ftpc->state = FTPS_CWD;
	    goto redo;
	} else {				/* ACCT needed or error */
	    ftpHandleErrorThenQuit(ftpc);
	}
	break;
      case FTPS_CWD:
	/* can come here direct from USER or PASS also... */
	if (ftpc->retcode == FTP_FILE_ACTION_OK ||
	    ftpc->retcode == FTP_COMMAND_OK) {	/* last CWD ok */
	    if (ftpc->wd == NULL || *(ftpc->wd) == '\0') { /* CWD done */
		ftpc->state = FTPS_TYPE;
		ftpSendType(ftpc);
	    } else {				/* send next part of CWD */
		ftpc->state = FTPS_CWD;
		ftpSendNextCwd(ftpc);
	    }
	} else {				/* last CWD failed */
	    ftpHandleErrorThenQuit(ftpc);
	}
	break;
      case FTPS_TYPE:
	if (ftpc->retcode == FTP_COMMAND_OK) {	/* TYPE ok */
	    ftpc->retcode = FTP_FILE_ACTION_OK;
	    ftpc->state = FTPS_READY;
	    goto redo;
	} else {				/* TYPE failed */
	    ftpHandleErrorThenQuit(ftpc);
	}
	break;
      case FTPS_READY:
	/* can get here from TYPE or EOF */
	ftpc->this_file += 1;
	if (ftpc->this_file < ftpc->num_files) {  /* files left to transfer */
	    sprintf(cmd,"File %s?",ftpc->files[ftpc->this_file]);
	    status0(cmd);
	    if (ftpc->prompt && !ftpPrompt(ftpc)) {
		goto redo;
	    }
	    if (ftpSendPort(ftpc) == 0) {	/*   PORT ok locally */
		ftpc->state = FTPS_PORT;
	    } else {				/*   PORT failed locally */
		ftpc->state = FTPS_QUIT;	/*     bag the whole thing */
		ftpSendCmd(ftpc,"QUIT");
	    }
	} else {				/* no files left to transfer */
	    DEBUG0("ftpProcessReply: no more files\n");
	    ftpc->state = FTPS_QUIT;
	    ftpSendCmd(ftpc,"QUIT");
	}
	break;
      case FTPS_PORT:
	if (ftpc->retcode == FTP_COMMAND_OK) {	/* PORT command ok */
	    ftpc->state = FTPS_GETPUT;
	    ftpSendGetPut(ftpc);
	} else {				/* PORT failed */
	    if (ftpc->port >= 0) {
		DEBUG1("ftpProcessReply: closing port %d\n",ftpc->port);
		close(ftpc->port);
		ftpc->port = -1;
	    }
	    ftpHandleErrorThenQuit(ftpc);
	}
	break;
      case FTPS_GETPUT:
	if (FTP_REPLY_PRELIM(ftpc->retcode)) { /* dataconn ready */
	    char *s;
	    status0(ftpc->reply+4);
	    if ((s=rindex(ftpc->reply,'('))) {
		if (sscanf(s,"(%d",&(ftpc->this_size)) != 1)
		    ftpc->this_size = 0;
	    }
	    if (ftpAcceptDataConn(ftpc) < 0) {		/*  local failure */
		if (ftpc->port != -1) {			/*    give up */
		    DEBUG1("ftpProcessReply: closing port %d\n",ftpc->port);
		    close(ftpc->port);
		    ftpc->port = -1;
		}
		ftpc->state = FTPS_QUIT;
		ftpSendCmd(ftpc,"QUIT");
	    } else if (ftpSetupLocalData(ftpc) < 0) {	/*  local failure */
		UnregisterFtpFd(ftpc,ftpc->data);	/*    don't give up */
		if (ftpc->data != -1) {
		    DEBUG1("ftpProcessReply: closing data %d\n",ftpc->data);
		    close(ftpc->data);
		    ftpc->data = -1;
		}
		ftpc->state = FTPS_READY;		/*    do next file */
		goto redo;
	    } else {				/* all ok locally */
		ftpc->state = FTPS_TRANSFER;
	    }
	} else if (ftpc->retcode == FTP_FILE_UNAVAILABLE ||	/* datacon */
		   ftpc->retcode == FTP_ACTION_NOT_TAKEN) {	/* failed  */
	    if (ftpc->port != -1) {				/*  minor  */
		DEBUG1("ftpProcessReply: closing port %d\n",ftpc->port);
		close(ftpc->port);				/*  error */
		ftpc->port = -1;
	    }
	    ftpAlert(ftpc);				/* error msg */
	    ftpc->state = FTPS_READY;		/* next file */
	    goto redo;
	} else {
	    ftpHandleErrorThenQuit(ftpc);	/* real error */
	}
	break;
      case FTPS_TRANSFER:
	/* Shouldn't get here since dataconn is separately managed */
	DEBUG0("ftpProcessReply: called in state TRANSFER!\n");
	break;
      case FTPS_EOF:
	/* Called when the dataconn is closed, for whatever reason */
	if (!FTP_REPLY_OK(ftpc->retcode)) {	/* dataconn closed error */
	    ftpAlert(ftpc);
	}
	ftpc->state = FTPS_READY;
	goto redo;
      case FTPS_QUIT:
	if (ftpc->retcode != FTP_SERVICE_CLOSING) {	/* error */
	    ftpAlert(ftpc);
	}
	ftpDone(ftpc);				/* close, deallocate */
	break;
      case FTPS_ABORT:
	if (ftpc->retcode == 552) {		/* NIC-style abort */
	    ftpc->state = FTPS_ABORT;		/* get another reply */
	} else {
	    if (ftpc->retcode != FTP_DATA_CLOSE_ABORT) { /* 426      */
		ftpAlert(ftpc);				 /* expected */
	    }
	    ftpc->state = FTPS_ABORT2;		/* get real reply */
	    ftpInitReply(ftpc);
	}
	break;
      case FTPS_ABORT2:
	if (!FTP_REPLY_OK(ftpc->retcode)) {	/* abort error */
	    ftpAlert(ftpc);
	}
	ftpCleanupDataConn(ftpc);
	ftpc->state = FTPS_READY;
	goto redo;
      default:
	fprintf(stderr,"ftpProcessReply: unknown state: %d\n",ftpc->state);
	abort();
    }
    DEBUG0("ftpProcessReply: done\n");
}

/*
 * This function just puts up an FTP error message.
 */
static void
ftpAlert(ftpc)
FtpContext *ftpc;
{
    char buf[256],*msg;
    int len;

    msg = ftpc->reply+4;		/* skip code */
    len = strlen(msg);
    if (len < 230) {
	sprintf(buf,"FTP Error %d:\n %s",ftpc->retcode,msg);
    } else {
	sprintf(buf,"FTP Error %d: ...\n %.230s",ftpc->retcode,msg+len-230);
    }
    alert0(buf);
}

/*
 * This prints an error message, then sends the QUIT command (or
 * calls ftpDone() if the server shut down).
 */
static void
ftpHandleErrorThenQuit(ftpc)
FtpContext *ftpc;
{
    ftpAlert(ftpc);
    /* Don't bother with QUIT if remote host closing down */
    if (ftpc->retcode == FTP_SERVICE_UNAVAILABLE) {
	ftpDone(ftpc);
    } else {
	status0("Quitting...");
	ftpc->state = FTPS_QUIT;
	ftpSendCmd(ftpc,"QUIT");
    }
}

/*
 * This calls the done function (or ftpFreeContext() if none is defined
 * for FTPC. All connections should pass through here to be cleaned up
 * regardless of how they got here.
 */
static void
ftpDone(ftpc)
FtpContext *ftpc;
{
    DEBUG1("ftpDone: ftpc=0x%x\n",ftpc);
    if (ftpc->done != NULL) {
	(*(ftpc->done))(ftpc);
    } else {
	ftpFreeContext(ftpc);
    }
    DEBUG0("ftpDone: done\n");
}

/*	-	-	-	-	-	-	-	-	*/
/* Functions called by ftpProcessReply(): */

static void
ftpSendType(ftpc)
FtpContext *ftpc;
{
    char cmd[16];

    DEBUG2("ftpSendType: ftpc=0x%x, type=%d\n",ftpc,ftpc->type);
    ftpc->state = FTPS_TYPE;
    switch (ftpc->type) {
      case TYPE_A:
 	sprintf(cmd,"TYPE A");
	break;
      case TYPE_E:
	sprintf(cmd,"TYPE E");
	break;
      case TYPE_I:
	sprintf(cmd,"TYPE I");
	break;
      default:
	sprintf(cmd,"TYPE L %d",(char *)ftpc->type);
    }
    status0(cmd);
    ftpSendCmd(ftpc,cmd);
    DEBUG0("ftpSendType: done\n");
}

static void
ftpSendNextCwd(ftpc)
FtpContext *ftpc;
{
    char *slash,cmd[256];

    DEBUG2("ftpSendNextCwd: ftpc=0x%x, wd=\"%s\"\n",ftpc,ftpc->wd);
    if (*(ftpc->wd) == '/') {	/* Leading slash treated specially... */
	ftpc->state = FTPS_CWD;
	ftpSendCmd(ftpc,"CWD /");
	ftpc->wd += 1;
    } else {			/* Normal case */
	if ((slash=index(ftpc->wd,'/')) != NULL) {
	    *slash = '\0';
	}
	sprintf(cmd,"CWD %s",ftpc->wd);
	status0(cmd);
	ftpc->state = FTPS_CWD;
	ftpSendCmd(ftpc,cmd);
	if (slash) {
	    ftpc->wd = slash+1;
	} else {
	    /* set wd to end of string */
	    while (*(ftpc->wd) != '\0')
		ftpc->wd += 1;
	}
    }
    DEBUG0("ftpSendNextCwd: done\n");
}

/*
 * Performs the PORT command. The new port is saved in f->port.
 * Returns < 0 if some local error, otherwise 0 if we got to sending
 * the command.
 */
static int
ftpSendPort(ftpc)
FtpContext *ftpc;
{
    char cmd[64];
    struct sockaddr_in addr;
    int addrlen;

    DEBUG1("ftpSendPort: ftpc=0x%lx\n",ftpc);
    if ((ftpc->port=socket(AF_INET,SOCK_STREAM,0)) < 0) {
	sysError("socket(ftpSendPort)");
	DEBUG0("ftpSendPort: returning -1\n");
	return(-1);
    }
    DEBUG1("ftpSendPort: socket() returned %d\n",ftpc->port);
    addr = ftpc->saddr;
    addr.sin_port = 0;
    if (bind(ftpc->port,(struct sockaddr *)&addr,
	     sizeof(struct sockaddr_in)) < 0) {
	sysError("bind(ftpSendPort)");
	DEBUG1("ftpSendPort: closing port %d\n",ftpc->port);
	close(ftpc->port);
	DEBUG0("ftpSendPort: returning -1\n");
	return(-1);
    }
    DEBUG0("ftpSendPort: bind() succeeded\n");
    if (listen(ftpc->port,1) < 0) {
	sysError("listen(ftpSendPort)");
	DEBUG1("ftpSendPort: closing port %d\n",ftpc->port);
	close(ftpc->port);
	DEBUG0("ftpSendPort: returning -1\n");
	return(-1);
    }
    DEBUG0("ftpSendPort: listen() succeeded\n");
    addrlen = sizeof(struct sockaddr_in);
    if (getsockname(ftpc->port,(struct sockaddr *)&addr,&addrlen) < 0) {
	sysError("getsockname(ftpSendPort)");
	DEBUG1("ftpSendPort: closing port %d\n",ftpc->port);
	close(ftpc->port);
	DEBUG0("ftpSendPort: returning -1\n");
	return(-1);
    }
    DEBUG2("ftpSendPort: PORT address: %s; port: %d\n",
	   inet_ntoa(addr.sin_addr),addr.sin_port);
    sprintf(cmd,"PORT %d,%d,%d,%d,%d,%d",
	    (int)((unsigned char *)&addr.sin_addr.s_addr)[0],
	    (int)((unsigned char *)&addr.sin_addr.s_addr)[1],
	    (int)((unsigned char *)&addr.sin_addr.s_addr)[2],
	    (int)((unsigned char *)&addr.sin_addr.s_addr)[3],
	    (int)((unsigned char *)&addr.sin_port)[0],
	    (int)((unsigned char *)&addr.sin_port)[1]);
    status0(cmd);
    ftpc->state = FTPS_PORT;
    ftpSendCmd(ftpc,cmd);
    DEBUG0("ftpSendPort: returning 0\n");
    return(0);
}


/*
 * After PORT is ok, send GET or PUT to open the dataconn.
 */
static void
ftpSendGetPut(ftpc)
FtpContext *ftpc;
{
    char cmd[MAXPATHLEN];

    DEBUG2("ftpSendGetPut: ftpc=0x%x, fcmd=%d\n",ftpc,ftpc->filecmd);
    ftpc->state = FTPS_GETPUT;
    switch (ftpc->filecmd) {
      case FTP_GET:
	sprintf(cmd,"RETR %s",ftpc->files[ftpc->this_file]);
	break;
      case FTP_PUT:
	sprintf(cmd,"STOR %s",ftpc->files[ftpc->this_file]);
	break;
    }
    status0(cmd);
    ftpSendCmd(ftpc,cmd);
    DEBUG0("ftpSendGetPut: done\n");
}

/*
 * After GET/PUT, accepts the connection from the PORT and closes the
 * the PORT. Returns the dataconn fd (also in ftpc->data).
 */
static int
ftpAcceptDataConn(ftpc)
FtpContext *ftpc;
{
    int datacon,addrlen;
    struct sockaddr_in addr;

    DEBUG1("ftpAcceptDataConn: fcmd %d successful\n",ftpc->filecmd);
    addrlen = sizeof(struct sockaddr_in);
    if ((datacon=accept(ftpc->port,(struct sockaddr *)&addr,&addrlen)) < 0) {
	sysError("accept(ftpAcceptDataConn)");
	DEBUG0("ftpAcceptDataConn: returning -1\n");
	return(-1);
    }
    DEBUG1("ftpAcceptDataConn: closing port %d\n",ftpc->port);
    close(ftpc->port);
    ftpc->port = -1;
    DEBUG1("ftpAcceptDataConn: registering dataconn %d\n",datacon);
    ftpc->data = datacon;
    MAKE_NONBLOCKING(ftpc->data);
    RegisterFtpFd(ftpc,ftpc->data,O_RDONLY,ftpDataCallback);
    DEBUG1("ftpAcceptDataConn: returning dataconn = %d\n",datacon);
    return(datacon);
}

/*
 * Once the remote file is ready for transfer through the dataconn, this
 * function sets up the local end. Returns -1 on error, otherwise 0.
 */
static int
ftpSetupLocalData(ftpc)
FtpContext *ftpc;
{
    char filename[MAXPATHLEN];

    DEBUG1("ftpSetupLocalData: ftpc=0x%x\n",ftpc);
    if (ftpc->local_dir && *(ftpc->local_dir)) {
	sprintf(filename,"%s/%s",ftpc->local_dir,ftpc->files[ftpc->this_file]);
    } else {
	strcpy(filename,ftpc->files[ftpc->this_file]);
    }
    DEBUG1("ftpSetupLocalData: opening \"%s\"\n",filename);
    if (ftpc->filecmd == FTP_GET) {
	ftpc->local_fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0644);
    } else {
	ftpc->local_fd = open(filename,O_RDONLY,0);
    }
    if (ftpc->local_fd < 0) {
	sysError(filename);
	DEBUG0("ftpSetupLocalData: returning -1\n");
	return(-1);
    }
    DEBUG1("ftpSetupLocalData: got local fd = %d\n",ftpc->local_fd);
    ftpc->num_bytes = 0;
    DEBUG0("ftpSetupLocalData: returning 0\n");
    return(0);
}

/*	-	-	-	-	-	-	-	-	*/
/* Data connection processing: */
/*
 * This function is called whenever the dataconn is ready for read/write.
 */
static void
ftpDataCallback(ftpc)
FtpContext *ftpc;
{
    DEBUG2("ftpDataCallback: ftpc=0x%x, fcmd=%d\n",ftpc,ftpc->filecmd);
    switch (ftpc->filecmd) {
      case FTP_GET:
	ftpReadData(ftpc);
	break;
      case FTP_PUT:
	ftpWriteData(ftpc);
	break;
      default:
	fprintf(stderr,"ftpDataCallback: unknown cmd: %d\n",ftpc->filecmd);
	abort();
    }
    DEBUG0("ftpDataCallback: done\n");
}

/*
 * This function is called to read from the dataconn and write to the
 * local_fd. On EOF, it sets the state field to FTPS_EOF and calls
 * ftpInitReply() to continue the session. On error, it calls
 * ftpSendAbort() and then ftpInitReply(). Otherwise the transfer
 * is ongoing, so it doesn't do anything.
 */
static void
ftpReadData(ftpc)
FtpContext *ftpc;
{
    char buf[BUFSIZ*4],*s,*t,msg[256];
    int nread,i;

    DEBUG0("ftpReadData: reading\n");
    /* Read one chunk of stuff and write it locally (don't loop or
     * X won't get a chance to dispatch).
     */
    nread = read(ftpc->data,buf,sizeof(buf));
    if (nread > 0) {
	/* Strip CR's if necessary */
	if (ftpc->type == TYPE_A && ftpc->stripcr) {
	    DEBUG0("ftpReadData: stripping <CR>\n");
	    for (s=t=buf,i=nread; i--; ++s) {
		if (*s != '\r') {
		    *t++ = *s;
		} else {
		    nread -= 1;
		}
	    }
	}
	/* Write locally */
	if (write(ftpc->local_fd,buf,nread) != nread) {
	    /* Error when writing */
	    sysError("write(ftpReadData)");
	    ftpCleanupDataConn(ftpc);
	    ftpSendAbort(ftpc);
	    ftpInitReply(ftpc);
	} else {
	    /* write was ok */
	    ftpc->num_bytes += nread;
	    DEBUG1("ftpReadData: total = %d\n",ftpc->num_bytes);
	    if (ftpc->this_size != 0) {
		sprintf(msg,"%.200s: %d/%d",ftpc->files[ftpc->this_file],
			(char *)(ftpc->num_bytes),(char *)(ftpc->this_size));
	    } else {
		sprintf(msg,"%.200s: %d bytes",ftpc->files[ftpc->this_file],
			(char *)(ftpc->num_bytes));
	    }
	    status0(msg);
	}
    } else if (nread < 0 && errno == ITWOULDBLOCK) {
	/* Nothing more ready now, keep waiting */
	DEBUG0("ftpReadData: dataconn would block\n");
    } else if (nread < 0) {
	/* Error when reading */
	sysError("read(ftpReadData)");
	DEBUG0("ftpReadData: error reading dataconn\n");
	ftpCleanupDataConn(ftpc);
	ftpSendAbort(ftpc);
	ftpInitReply(ftpc);
    } else if (nread == 0) {
	/* We got EOF on remote file. */
	DEBUG0("ftpReadData: EOF on dataconn\n");
	ftpCleanupDataConn(ftpc);
	if (ftpc->done1 != NULL)
	    (*(ftpc->done1))(ftpc);
	ftpc->state = FTPS_EOF;
	ftpInitReply(ftpc);
    }
}

/*ARGSUSED*/
static void
ftpWriteData(ftpc)
FtpContext *ftpc;
{
    /*EMPTY*/
}

static void
ftpCleanupDataConn(ftpc)
FtpContext *ftpc;
{
    /* Note: We may have already unregistered, eg, in ftpAbort(). */
    if (ftpc->state != FTPS_ABORT2)
	UnregisterFtpFd(ftpc,ftpc->data);
    if (ftpc->data != -1) {
	DEBUG1("ftpCleanupDataConn: closing data %d\n",ftpc->data);
	close(ftpc->data);
	ftpc->data = -1;
    }
    if (ftpc->local_fd != -1) {
	DEBUG1("ftpCleanupDataConn: closing local_fd %d\n",ftpc->local_fd);
	close(ftpc->local_fd);
	ftpc->local_fd = -1;
    }
}

/*	-	-	-	-	-	-	-	-	*/
/* Sending commands to the server: */
/*
 * Adds CRLF and calls to ftpStartSendingCmd().
 */
static void
ftpSendCmd(ftpc,cmd)
FtpContext *ftpc;
char *cmd;
{
    char buf[MAXPATHLEN];

    if (ftpc->trace) {
	(*(ftpc->trace))(ftpc,1,cmd);
    }
    sprintf(buf,"%s\r\n",cmd);
    ftpStartSendingCmd(ftpc,buf);
}

/*
 * Handles abort processing by sending the Telnet abort sequence in the
 * out-of-band stream of the ctrl conn and the ABOR command on the regular
 * stream.
 */
static void
ftpSendAbort(ftpc)
FtpContext *ftpc;
{
    char buf[8];
    int sent;

    DEBUG1("ftpSendAbort: ftpc=0x%lx\n",ftpc);
    DEBUG0("ftpSendAbort: sending <IAC><IP><IAC> in OOB\n");
    sprintf(buf,"%c%c%c",IAC,IP,IAC);
    do {
	sent = send(ftpc->ctrl,buf,3,MSG_OOB);
    } while (sent == -1 && errno == EINTR);
    if (sent != 3) {
	sysError("send(ftpSendAbort)");
	ftpDone(ftpc);
    }
    DEBUG0("ftpSendAbort: sending <DM>ABOR\n");
    sprintf(buf,"%cABOR\r\n",DM);
    do {
	sent = write(ftpc->ctrl,buf,strlen(buf));
    } while (sent == -1 && errno == EINTR);
    if (sent != strlen(buf)) {
	sysError("write(ftpSendAbort)");
	ftpDone(ftpc);
    }
    ftpc->state = FTPS_ABORT;
    ftpInitReply(ftpc);
    status0("Aborting...");
    DEBUG0("ftpSendAbort: done\n");
}

/*
 * This function starts sending CMD to the server over the control
 * connection. The writes can block, hence this has to be able to finish
 * later, but since they usually don't block, we don't set up to retry
 * unless we have to. If the command is completely sent, then we call
 * ftpInitReply() otherwise we setup to finish by registering the ctrlconn
 * for writing via callback ftpWriteCallback().
 */
static void
ftpStartSendingCmd(ftpc,cmd)
FtpContext *ftpc;
char *cmd;
{
    int cmdlen,numsent;

    DEBUG2("ftpStartSendingCmd: ftpc=0x%x: \"%s\"\n",ftpc,cmd);
    cmdlen = strlen(cmd);
    numsent = write(ftpc->ctrl,cmd,cmdlen);
    if (numsent < 0 && errno != ITWOULDBLOCK) {
	sysError("write(ftpStartSendingCmd)");
	DEBUG0("ftpStartSendingCmd: error writing ctrlconn\n");
	ftpDone(ftpc);
	return;
    } else if (numsent == cmdlen) {
	DEBUG0("ftpStartSendingCmd: write all done\n");
	ftpInitReply(ftpc);
	return;
    } else if (errno == ITWOULDBLOCK) {
	/* Otherwise the write would block, nothing sent */
	DEBUG0("ftpStartSendingCmd: write would block\n");
	numsent = 0;
    } else {
	/* Incomplete write */
	DEBUG1("ftpStartSendingCmd: write incomplete (%d bytes sent)\n",numsent);
    }
    /* Either way, if we get here there's something left do */
    cmd += numsent;
    if ((ftpc->cmd=malloc(strlen(cmd)+1)) == NULL) {
	sysError("malloc(ftpStartSendingCmd)");
	ftpDone(ftpc);
	return;
    }
    strcpy(ftpc->cmd,cmd);
    DEBUG1("ftpStartSendingCmd: pending: \"%s\"\n",ftpc->cmd);
    RegisterFtpFd(ftpc,ftpc->ctrl,O_WRONLY,ftpWriteCallback);
}

/*
 * This function is called when the ctrlconn is ready for writing.
 * It sends as much as possible. If that completes the cmd, then it
 * calls ftpInitReply(), otherwise updates the cmd with what's left
 * and returns.
 */
static void
ftpWriteCallback(ftpc)
FtpContext *ftpc;
{
    char *cmd;
    int cmdlen,numsent;

    cmd = ftpc->cmd;
    DEBUG2("ftpWriteCallback: ftpc=0x%x: \"%s\"\n",ftpc,cmd);
    cmdlen = strlen(ftpc->cmd);
    numsent = write(ftpc->ctrl,cmd,cmdlen);
    if (numsent < 0 && errno != ITWOULDBLOCK) {
	sysError("write(ftpWriteCallback)");
	DEBUG0("ftpWriteCallback: error writing ctrlconn\n");
	ftpDone(ftpc);
	return;
    } else if (numsent == cmdlen) {
	DEBUG0("ftpWriteCallback: write all done\n");
	UnregisterFtpFd(ftpc,ftpc->ctrl);
	free(ftpc->cmd);
	ftpInitReply(ftpc);
	return;
    } else if (errno == ITWOULDBLOCK) {
	/* Otherwise the write would block, nothing sent */
	DEBUG0("ftpWriteCallback: write would block\n");
	numsent = 0;
    } else {
	/* Incomplete write */
	DEBUG1("ftpWriteCallback: write incomplete (%d bytes sent)\n",numsent);
    }
    /* Either way, if we get here there's something left do */
    cmd += numsent;
    free(ftpc->cmd);
    if ((ftpc->cmd=malloc(strlen(cmd)+1)) == NULL) {
	sysError("malloc(ftpWriteCallback)");
	ftpDone(ftpc);
	return;
    }
    strcpy(ftpc->cmd,cmd);
    DEBUG1("ftpWriteCallback: pending: \"%s\"\n",ftpc->cmd);
}

/*	-	-	-	-	-	-	-	-	*/
/* Printing utilities for debugging */

#ifdef DEBUG
char *
ftpstatestr(state)
int state;
{
    char buf[8];

    switch (state) {
	/* state */
      case FTPS_OPEN:		return("OPEN");
      case FTPS_CONNECT:	return("CONNECT");
      case FTPS_CONNECTED:	return("CONNECTED");
      case FTPS_USER:		return("USER");
      case FTPS_PASS:		return("PASS");
      case FTPS_CWD:		return("CWD");
      case FTPS_TYPE:		return("TYPE");
      case FTPS_READY:		return("READY");
      case FTPS_PORT:		return("PORT");
      case FTPS_GETPUT:		return("GETPUT");
      case FTPS_TRANSFER:	return("TRANSFER");
      case FTPS_EOF:		return("EOF");
      case FTPS_QUIT:		return("QUIT");
      case FTPS_ABORT:		return("ABORT");
      case FTPS_ABORT2:		return("ABORT2");
	/* reply_state */
      case FTPS_REPLY_CODE:	return("CODE");
      case FTPS_REPLY_CONT:	return("CONT");
      case FTPS_REPLY_LAST:	return("LAST");
      case FTPS_REPLY_MORE:	return("MORE");
      case FTPS_REPLY_CHCK:	return("CHCK");
      case FTPS_REPLY_IAC1:	return("IAC1");
      case FTPS_REPLY_IAC2:	return("IAC2");
      case FTPS_REPLY_IAC3:	return("IAC3");
      default:			sprintf(buf,"?%d?",state);
				return(buf);
    }
}
#endif /* DEBUG */


/*	-	-	-	-	-	-	-	-	*/
/* Sample code for standalone client */

#ifdef STANDALONE

/* This is from Brendan... */
#include <sys/types.h>		/* this may/will define FD_SET etc */
#ifdef u3b2
# include <sys/inet.h>		/* THIS does FD_SET etc on AT&T 3b2s.  */
#endif

fd_set readfds,writefds;
typedef void (*PF)();
PF funcs[FD_SETSIZE];
FtpContext *contexts[FD_SETSIZE];

void
RegisterFtpFd(ftpc,fd,flags,func)
FtpContext *ftpc;
int fd,flags;
void (*func)();
{
    switch (flags) {
      case O_RDONLY:
	fprintf(stderr,"REGISTER fd %d, flags=RO\n",fd);
	FD_SET(fd,&readfds);
	FD_CLR(fd,&writefds);
	break;
      case O_WRONLY:
	fprintf(stderr,"REGISTER fd %d, flags=WO\n",fd);
	FD_CLR(fd,&readfds);
	FD_SET(fd,&writefds);
	break;
      case O_RDWR:
	fprintf(stderr,"REGISTER fd %d, flags=RW\n",fd);
	FD_SET(fd,&readfds);
	FD_SET(fd,&writefds);
	break;
    }
    funcs[fd] = func;
    contexts[fd] = ftpc;
}

/*ARGSUSED*/
void
UnregisterFtpFd(ftpc,fd)
FtpContext *ftpc;
int fd;
{
    fprintf(stderr,"UNREGISTER fd %d\n",fd);
    FD_CLR(fd,&readfds);
    FD_CLR(fd,&writefds);
    funcs[fd] = NULL;
    contexts[fd] = NULL;
}

void
done(ftpc)
FtpContext *ftpc;
{
    fprintf(stderr,"DONE!\n");
    ftpFreeContext(ftpc);
    exit(0);
}
    
void
done1(ftpc)
FtpContext *ftpc;
{
    fprintf(stderr,"DONE1: \"%s\"\n",ftpc->files[ftpc->this_file]);
}
    
/*ARGSUSED*/
void
trace(ftpc,who,text)
FtpContext *ftpc;
int who;		/* 0 => recvd, non-0 => sent */
char *text;
{
    fprintf(stderr,"TRACE: ");
    if (who)	/* non-zero == us */
	fprintf(stderr,"ftp> ");
    fprintf(stderr,"%s",text);
    if (*(text+strlen(text)-1) != '\n')
	fprintf(stderr,"\n",text);
}
    
char *program;

main(argc,argv)
int argc;
char *argv[];
{
    char *host,*cwd;
    FtpContext *ftpc;
    fd_set rfds,wfds;
    int fd;

    if (argc < 3) {
	fprintf(stderr,"usage: %s host cwd files...\n",argv[0]);
	exit(1);
    }
    argc -= 1;
    program = *argv++;
    argc -= 1;
    host = *argv++;
    argc -= 1;
    cwd = *argv++;
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    ftpc = ftpNewContext(host,"anonymous","ferguson@cs.rochester.edu",
			 cwd,TYPE_I,1,1,FTP_GET,argv,argc,trace,done1,done);
    printf("Calling ftpStart...\n");
    ftpStart(ftpc);
    while (1) {
	bcopy((char *)&readfds,(char *)&rfds,sizeof(fd_set));
	bcopy((char *)&writefds,(char *)&wfds,sizeof(fd_set));
	fprintf(stderr,"select(): R=");
	for (fd=0; fd < FD_SETSIZE; fd++) {
	    if (FD_ISSET(fd,&rfds))
		fprintf(stderr,"%d,",fd);
	}
	fprintf(stderr," W=");
	for (fd=0; fd < FD_SETSIZE; fd++) {
	    if (FD_ISSET(fd,&wfds))
		fprintf(stderr,"%d,",fd);
	}
	fprintf(stderr,"\n");
	if (select(FD_SETSIZE,&rfds,&wfds,NULL,NULL) < 0) {
	    perror(program);
	    exit(1);
	}
	for (fd=0; fd < FD_SETSIZE; fd++) {
	    if (FD_ISSET(fd,&rfds) || FD_ISSET(fd,&wfds)) {
		fprintf(stderr,"selected fd %d...\n",fd);
		(*(funcs[fd]))(contexts[fd]);
	    }
	}
    }
}

void
sysError(s)
char *s;
{
    extern int errno;
    extern char *sys_errlist[];
    fprintf(stderr,"SYSERR: %s: %s\n",s,sys_errlist[errno]);
}

void
alert0(s)
char *s;
{
    fprintf(stderr,"ALERT: %s\n",s);
}

void
status0(s)
char *s;
{
    fprintf(stderr,"STATUS: %s\n",s);
}

int
ftpPrompt(ftpc)
FtpContext *ftpc;
{
    char c;

    fprintf(stderr,"CONFIRM: %s %s [ynaq]",
	    ftpfilecmdstr(ftpc->filecmd),ftpc->files[ftpc->this_file]);
    c = getchar();
    if (c != '\n')
	(void)getchar();
    switch (c) {
      case 'y': return(1);
      case 'n': return(0);
      case 'a': ftpc->prompt = 0;
	        return(1);
      case 'q': ftpc->this_file = ftpc->num_files;
	        return(0);
      default: return(1);
    }
}
#endif /*STANDALONE*/
