about summary refs log tree commit diff
path: root/Src
diff options
context:
space:
mode:
Diffstat (limited to 'Src')
-rw-r--r--Src/Modules/zftp.c1472
1 files changed, 1070 insertions, 402 deletions
diff --git a/Src/Modules/zftp.c b/Src/Modules/zftp.c
index ca0843419..ef85dd330 100644
--- a/Src/Modules/zftp.c
+++ b/Src/Modules/zftp.c
@@ -29,43 +29,195 @@
 
 /*
  * TODO:
+ *   should be eight-bit clean, but isn't.
+ *   tracking of logical rather than physical directories, like nochaselinks
+ *     (usually PWD returns physical directory).
  *   can signal handling be improved?
- *   error messages may need tidying up.
  *   maybe we should block CTRL-c on some more operations,
  *     otherwise you can get the connection closed prematurely.
  *   some way of turning off progress reports when backgrounded
  *     would be nice, but the shell doesn't make it easy to find that out.
- *   the progress reports 100% a bit prematurely:  the data may still
- *     be in transit, and we are stuck waiting for a message from the
- *     server.  but there's really nothing else to do.  it's worst
- *     with small files.
  *   proxy/gateway connections if i knew what to do
  *   options to specify e.g. a non-standard port
- *   optimizing things out is hard in general when you don't know what
- *     the shell's going to want, but they may be places to second guess
- *     the user.  Some of the variables could be made special and so
- *     only retrieve things like the current directory when necessary.
- *     But it's much neater to have ordinary variables, which the shell
- *     can manage without our interference, and getting the directory
- *     just every time it changes isn't so bad.  The user can always
- *     toggle the `Dumb' preference if it's feeling clever.
  */
-#include "zftp.mdh"
-#include "zftp.pro"
 
+/* needed in prototypes for statics */
+struct hostent;
+struct in_addr;
+struct sockaddr_in;
+struct sockaddr_in6;
+union zftp_sockaddr;
+struct zftp_session;
+typedef struct zftp_session *Zftp_session;
+
+#include <sys/types.h>
 #include <sys/socket.h>
 #include <netdb.h>
 #include <netinet/in_systm.h>
 #include <netinet/in.h>
 #include <netinet/ip.h>
 #include <arpa/inet.h>
+
+#include "zftp.mdh"
+#include "zftp.pro"
+
 /* it's a TELNET based protocol, but don't think I like doing this */
 #include <arpa/telnet.h>
 
-/* bet there are machines which have neither INADDR_NONE nor in_addr_t. */
-#ifndef INADDR_NONE
-#define INADDR_NONE (in_addr_t)-1
+/*
+ * We use poll() in preference to select because some subset of manuals says
+ * that's the thing to do, plus it's a bit less fiddly.  I don't actually
+ * have access to a system with poll but not select, however, though
+ * both bits of the code have been tested on a machine with both.
+ */
+#ifdef HAVE_POLL_H
+# include <poll.h>
 #endif
+#if defined(HAVE_POLL) && !defined(POLLIN) && !defined(POLLNORM)
+# undef HAVE_POLL
+#endif
+
+/* Is IPv6 supported by the library? */
+
+#if defined(AF_INET6) && defined(IN6ADDR_LOOPBACK_INIT) \
+	&& defined(HAVE_INET_NTOP) && defined(HAVE_INET_PTON)
+# define SUPPORT_IPV6 1
+#endif
+
+union zftp_sockaddr {
+	struct sockaddr a;
+	struct sockaddr_in in;
+#ifdef SUPPORT_IPV6
+	struct sockaddr_in6 in6;
+#endif
+};
+
+/* We use the RFC 2553 interfaces.  If the functions don't exist in the library,
+   simulate them. */
+
+#ifndef INET_ADDRSTRLEN
+# define INET_ADDRSTRLEN 16
+#endif
+
+#ifndef INET6_ADDRSTRLEN
+# define INET6_ADDRSTRLEN 46
+#endif
+
+/**/  
+#ifndef HAVE_INET_NTOP
+
+/**/    
+static char const *
+inet_ntop(int af, void const *cp, char *buf, size_t len)
+{       
+        if(af != AF_INET) {
+                errno = EAFNOSUPPORT;
+                return NULL;
+        } 
+        if(len < INET_ADDRSTRLEN) {
+                errno = ENOSPC;
+                return NULL;
+        }
+        strcpy(buf, inet_ntoa(*(struct in_addr *)cp));
+        return buf;
+}
+
+/**/  
+#endif /* !HAVE_INET_NTOP */
+
+/**/
+#ifndef HAVE_INET_PTON
+
+/**/
+# ifndef HAVE_INET_ATON
+
+#  ifndef INADDR_NONE
+#   define INADDR_NONE 0xffffffffUL
+#  endif
+
+/**/
+static int inet_aton(char const *src, struct in_addr *dst)
+{
+    return (dst->s_addr = inet_addr(src)) != INADDR_NONE;
+}
+
+/**/
+# endif /* !HAVE_INET_ATON */
+
+/**/
+static int
+inet_pton(int af, char const *src, void *dst)
+{
+        if(af != AF_INET) {
+                errno = EAFNOSUPPORT;
+                return -1;
+        }
+        return !!inet_aton(src, dst);
+}
+
+/**/
+#endif /* !HAVE_INET_PTON */
+
+/**/
+#ifndef HAVE_GETIPNODEBYNAME
+
+/* note: this is not a complete implementation.  If ignores the flags,
+   and does not provide the memory allocation of the standard interface.
+   Each returned structure will overwrite the previous one. */
+
+/**/
+static struct hostent *
+getipnodebyname(char const *name, int af, int flags, int *errorp)
+{
+	static struct hostent ahe;
+	static char nbuf[16];
+	static char *addrlist[] = { nbuf, NULL };
+# ifdef SUPPORT_IPV6
+	static char pbuf[INET6_ADDRSTRLEN];
+# else
+	static char pbuf[INET_ADDRSTRLEN];
+# endif
+	struct hostent *he;
+	if(inet_pton(af, name, nbuf) == 1) {
+		inet_ntop(af, nbuf, pbuf, sizeof(pbuf));
+		ahe.h_name = pbuf;
+		ahe.h_aliases = addrlist+1;
+		ahe.h_addrtype = af;
+		ahe.h_length = (af == AF_INET) ? 4 : 16;
+		ahe.h_addr_list = addrlist;
+		return &ahe;
+	}
+	he = gethostbyname2(name, af);
+	if(!he)
+		*errorp = h_errno;
+	return he;
+}
+
+/**/
+# ifndef HAVE_GETHOSTBYNAME2
+
+/**/
+static struct hostent *
+gethostbyname2(char const *name, int af)
+{
+	if(af != AF_INET) {
+		h_errno = NO_RECOVERY;
+		return NULL;
+	}
+	return gethostbyname(name);
+}
+
+/**/
+# endif /* !HAVE_GETHOSTBYNAME2 */
+
+/**/
+static void
+freehostent(struct hostent *ptr)
+{
+}
+
+/**/
+#endif /* !HAVE_GETIPNODEBYNAME */
 
 /*
  * For FTP block mode
@@ -104,7 +256,7 @@ enum {
     ZFHD_EORB = 128		/* block is end of file */
 };
 
-typedef int (*readwrite_t)(int, char *, size_t, int);
+typedef int (*readwrite_t)(int, char *, off_t, int);
 
 struct zftpcmd {
     const char *nam;
@@ -124,7 +276,9 @@ enum {
     ZFTP_HERE  = 0x0100,	/* here rather than over there */
     ZFTP_CDUP  = 0x0200,	/* CDUP rather than CWD */
     ZFTP_REST  = 0x0400,	/* restart: set point in remote file */
-    ZFTP_RECV  = 0x0800		/* receive rather than send */
+    ZFTP_RECV  = 0x0800,	/* receive rather than send */
+    ZFTP_TEST  = 0x1000,	/* test command, don't test */
+    ZFTP_SESS  = 0x2000		/* session command, don't need status */
 };
 
 typedef struct zftpcmd *Zftpcmd;
@@ -134,6 +288,7 @@ static struct zftpcmd zftpcmdtab[] = {
     { "params", zftp_params, 0, 4, 0 },
     { "login", zftp_login, 0, 3, ZFTP_CONN },
     { "user", zftp_login, 0, 3, ZFTP_CONN },
+    { "test", zftp_test, 0, 0, ZFTP_TEST },
     { "cd", zftp_cd, 1, 1, ZFTP_CONN|ZFTP_LOGI },
     { "cdup", zftp_cd, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_CDUP },
     { "dir", zftp_dir, 0, -1, ZFTP_CONN|ZFTP_LOGI },
@@ -158,7 +313,9 @@ static struct zftpcmd zftpcmdtab[] = {
     { "site", zftp_quote, 1, -1, ZFTP_CONN|ZFTP_SITE },
     { "close", zftp_close, 0, 0, ZFTP_CONN },
     { "quit", zftp_close, 0, 0, ZFTP_CONN },
-    { 0, 0, 0, 0}
+    { "session", zftp_session, 0, 1, ZFTP_SESS },
+    { "rmsession", zftp_rmsession, 0, 1, ZFTP_SESS },
+    { 0, 0, 0, 0, 0 }
 };
 
 static struct builtin bintab[] = {
@@ -180,16 +337,11 @@ static char *zfparams[] = {
 enum {
     ZFPM_READONLY = 0x01,	/* make parameter readonly */
     ZFPM_IFUNSET  = 0x02,	/* only set if not already set */
-    ZFPM_INTEGER  = 0x04	/* passed pointer to long */
+    ZFPM_INTEGER  = 0x04	/* passed pointer to off_t */
 };
 
-/*
- * Basic I/O variables for control connection:
- * zcfd != -1 is a flag that there is a connection open.
- */
-static int zcfd = -1;
-static FILE *zcin;
-static struct sockaddr_in zsock;
+/* Number of connections actually open */
+static int zfnopen;
 
 /*
  * zcfinish = 0 keep going
@@ -201,12 +353,6 @@ static int zcfinish;
 static int zfclosing;
 
 /*
- * Now stuff for data connection
- */
-static int zdfd = -1;
-static struct sockaddr_in zdsock;
-
-/*
  * Stuff about last message:  last line of message and status code.
  * The reply is also stored in $ZFTP_REPLY; we keep these separate
  * for convenience.
@@ -214,9 +360,6 @@ static struct sockaddr_in zdsock;
 static char *lastmsg, lastcodestr[4];
 static int lastcode;
 
-/* flag for remote system is UNIX --- useful to know as things are simpler */
-static int zfis_unix, zfpassive_conn;
-
 /* remote system has size, mdtm commands */
 enum {
     ZFCP_UNKN = 0,		/* dunno if it works on this server */
@@ -224,8 +367,6 @@ enum {
     ZFCP_NOPE = 2		/* it doesn't */
 };
 
-static int zfhas_size, zfhas_mdtm;
-
 /*
  * We keep an fd open for communication between the main shell
  * and forked off bits and pieces.  This allows us to know
@@ -235,25 +376,26 @@ static int zfhas_size, zfhas_mdtm;
  * --- we don't try to track it because it's too complicated.
  */
 enum {
-    ZFST_ASCI = 0x00,		/* type for next transfer is ASCII */
-    ZFST_IMAG = 0x01,		/* type for next transfer is image */
+    ZFST_ASCI = 0x0000,		/* type for next transfer is ASCII */
+    ZFST_IMAG = 0x0001,		/* type for next transfer is image */
 
-    ZFST_TMSK = 0x01,		/* mask for type flags */
-    ZFST_TBIT = 0x01,		/* number of bits in type flags */
+    ZFST_TMSK = 0x0001,		/* mask for type flags */
+    ZFST_TBIT = 0x0001,		/* number of bits in type flags */
 
-    ZFST_CASC = 0x00,		/* current type is ASCII - default */
-    ZFST_CIMA = 0x02,		/* current type is image */
+    ZFST_CASC = 0x0000,		/* current type is ASCII - default */
+    ZFST_CIMA = 0x0002,		/* current type is image */
 
-    ZFST_STRE = 0x00,		/* stream mode - default */
-    ZFST_BLOC = 0x04,		/* block mode */
+    ZFST_STRE = 0x0000,		/* stream mode - default */
+    ZFST_BLOC = 0x0004,		/* block mode */
 
-    ZFST_MMSK = 0x04,		/* mask for mode flags */
+    ZFST_MMSK = 0x0004,		/* mask for mode flags */
 
-    ZFST_LOGI = 0x08,		/* user logged in */
-    ZFST_NOPS = 0x10,		/* server doesn't understand PASV */
-    ZFST_NOSZ = 0x20,		/* server doesn't send `(XXXX bytes)' reply */
-    ZFST_TRSZ = 0x40,		/* tried getting 'size' from reply */
-    ZFST_CLOS = 0x80		/* connection closed */
+    ZFST_LOGI = 0x0008,		/* user logged in */
+    ZFST_SYST = 0x0010,		/* done system type check */
+    ZFST_NOPS = 0x0020,		/* server doesn't understand PASV */
+    ZFST_NOSZ = 0x0040,		/* server doesn't send `(XXXX bytes)' reply */
+    ZFST_TRSZ = 0x0080,		/* tried getting 'size' from reply */
+    ZFST_CLOS = 0x0100		/* connection closed */
 };
 #define ZFST_TYPE(x) (x & ZFST_TMSK)
 /*
@@ -263,7 +405,8 @@ enum {
 #define ZFST_CTYP(x) ((x >> ZFST_TBIT) & ZFST_TMSK)
 #define ZFST_MODE(x) (x & ZFST_MMSK)
 
-static int zfstatfd = -1, zfstatus;
+/* fd containing status for all sessions and array for internal use */
+static int zfstatfd = -1, *zfstatusp;
 
 /* Preferences, read in from the `zftp_prefs' array variable */
 enum {
@@ -273,11 +416,43 @@ enum {
 };
 
 /* The flags as stored internally. */
-int zfprefs;
+static int zfprefs;
+
+/*
+ * Data node for linked list of sessions.
+ *
+ * Memory management notes:
+ *   name is permanently allocated and remains for the life of the node.
+ *   userparams is set directly by zftp_params and also freed with the node.
+ *   params and its data are allocated when we need
+ *     to save an existing session, and are freed when we switch back
+ *     to that session.
+ *   The node itself is deleted when we remove it from the list.
+ */
+struct zftp_session {
+    char *name;			/* name of session */
+    char **params;		/* parameters ordered as in zfparams */
+    char **userparams;		/* user parameters set by zftp_params */
+    int cfd;			/* control file descriptor */
+    FILE *cin;			/* control input file */
+    union zftp_sockaddr sock;	/* this end of the control connection */
+    union zftp_sockaddr peer;	/* far end of the control connection */
+    int dfd;			/* data connection */
+    int has_size;		/* understands SIZE? */
+    int has_mdtm;		/* understands MDTM? */
+};
+
+/* List of active sessions */
+static LinkList zfsessions;
 
+/* Current session */
+static Zftp_session zfsess;
 
-/* zfuserparams is the storage area for zftp_params() */
-char **zfuserparams;
+/* Number of current session, corresponding to position in list */
+static int zfsessno;
+
+/* Total number of sessions */
+static int zfsesscnt;
 
 /*
  * Bits and pieces for dealing with SIGALRM (and SIGPIPE, but that's
@@ -379,7 +554,7 @@ zfpipe()
 
 /**/
 static void
-zfunalarm()
+zfunalarm(void)
 {
     if (oalremain) {
 	/*
@@ -446,7 +621,7 @@ zfmovefd(int fd)
  * set a non-special parameter.
  * if ZFPM_IFUNSET, don't set if it already exists.
  * if ZFPM_READONLY, make it readonly, but only when creating it.
- * if ZFPM_INTEGER, val pointer is to long (NB not int), don't free.
+ * if ZFPM_INTEGER, val pointer is to off_t (NB not int), don't free.
  */
 /**/
 static void
@@ -473,7 +648,7 @@ zfsetparam(char *name, void *val, int flags)
 	return;
     }
     if (type == PM_INTEGER)
-	pm->sets.ifn(pm, *(long *)val);
+	pm->sets.ifn(pm, *(off_t *)val);
     else
 	pm->sets.cfn(pm, (char *)val);
 }
@@ -542,7 +717,7 @@ zfgetline(char *ln, int lnsize, int tmout)
     if (setjmp(zfalrmbuf)) {
 	alarm(0);
 	zwarnnam("zftp", "timeout getting response", NULL, 0);
-	return 5;
+	return 6;
     }
     zfalarm(tmout);
 
@@ -554,12 +729,12 @@ zfgetline(char *ln, int lnsize, int tmout)
      * But then we get `frustrated user syndrome'.
      */
     for (;;) {
-	ch = fgetc(zcin);
+	ch = fgetc(zfsess->cin);
 
 	switch(ch) {
 	case EOF:
-	    if (ferror(zcin) && errno == EINTR) {
-		clearerr(zcin);
+	    if (ferror(zfsess->cin) && errno == EINTR) {
+		clearerr(zfsess->cin);
 		continue;
 	    }
 	    zcfinish = 2;
@@ -567,7 +742,7 @@ zfgetline(char *ln, int lnsize, int tmout)
 
 	case '\r':
 	    /* always precedes something else */
-	    ch = fgetc(zcin);
+	    ch = fgetc(zfsess->cin);
 	    if (ch == EOF) {
 		zcfinish = 2;
 		break;
@@ -594,26 +769,26 @@ zfgetline(char *ln, int lnsize, int tmout)
 	     * oh great, now it's sending TELNET commands.  try
 	     * to persuade it not to.
 	     */
-	    ch = fgetc(zcin);
+	    ch = fgetc(zfsess->cin);
 	    switch (ch) {
 	    case WILL:
 	    case WONT:
-		ch = fgetc(zcin);
+		ch = fgetc(zfsess->cin);
 		/* whatever it wants to do, stop it. */
 		cmdbuf[0] = (char)IAC;
 		cmdbuf[1] = (char)DONT;
 		cmdbuf[2] = ch;
-		write(zcfd, cmdbuf, 3);
+		write(zfsess->cfd, cmdbuf, 3);
 		continue;
 
 	    case DO:
 	    case DONT:
-		ch = fgetc(zcin);
+		ch = fgetc(zfsess->cin);
 		/* well, tough, we're not going to. */
 		cmdbuf[0] = (char)IAC;
 		cmdbuf[1] = (char)WONT;
 		cmdbuf[2] = ch;
-		write(zcfd, cmdbuf, 3);
+		write(zfsess->cfd, cmdbuf, 3);
 		continue;
 
 	    case EOF:
@@ -658,13 +833,13 @@ zfgetline(char *ln, int lnsize, int tmout)
 
 /**/
 static int 
-zfgetmsg()
+zfgetmsg(void)
 {
     char line[256], *ptr, *verbose;
     int stopit, printing = 0, tmout;
 
-    if (zcfd == -1)
-	return 5;
+    if (zfsess->cfd == -1)
+	return 6;
     if (!(verbose = getsparam("ZFTP_VERBOSE")))
 	verbose = "";
     zsfree(lastmsg);
@@ -674,12 +849,12 @@ zfgetmsg()
 
     zfgetline(line, 256, tmout);
     ptr = line;
-    if (zfdrrrring || !isdigit((int)*ptr) || !isdigit((int)ptr[1]) || 
-	!isdigit((int)ptr[2])) {
+    if (zfdrrrring || !isdigit(STOUC(*ptr)) || !isdigit(STOUC(ptr[1])) || 
+	!isdigit(STOUC(ptr[2]))) {
 	/* timeout, or not talking FTP.  not really interested. */
 	zcfinish = 2;
 	if (!zfclosing)
-	    zfclose();
+	    zfclose(0);
 	lastmsg = ztrdup("");
 	strcpy(lastcodestr, "000");
 	zfsetparam("ZFTP_REPLY", ztrdup(lastmsg), ZFPM_READONLY);
@@ -748,10 +923,10 @@ zfgetmsg()
      */
     if ((zcfinish == 2 || lastcode == 421) && !zfclosing) {
 	zcfinish = 2;		/* don't need to tell server */
-	zfclose();
+	zfclose(0);
 	/* unexpected, so tell user */
 	zwarnnam("zftp", "remote server has closed connection", NULL, 0);
-	return 6;		/* pretend it failed, because it did */
+	return 6;
     }
     if (lastcode == 530) {
 	/* user not logged in */
@@ -789,21 +964,22 @@ zfsendcmd(char *cmd)
      */
     int ret, tmout;
 
-    if (zcfd == -1)
-	return 5;
+    if (zfsess->cfd == -1)
+	return 6;
     tmout = getiparam("ZFTP_TMOUT");
     if (setjmp(zfalrmbuf)) {
 	alarm(0);
 	zwarnnam("zftp", "timeout sending message", NULL, 0);
-	return 5;
+	return 6;
     }
     zfalarm(tmout);
-    ret = write(zcfd, cmd, strlen(cmd));
+    ret = write(zfsess->cfd, cmd, strlen(cmd));
     alarm(0);
 
     if (ret <= 0) {
-	zwarnnam("zftp send", "failed sending control message", NULL, 0);
-	return 5;		/* FTP status code */
+	zwarnnam("zftp send", "failure sending control message: %e",
+		 NULL, errno);
+	return 6;
     }
 
     return zfgetmsg();
@@ -814,62 +990,106 @@ zfsendcmd(char *cmd)
 
 /**/
 static int
-zfopendata(char *name)
+zfopendata(char *name, union zftp_sockaddr *zdsockp, int *is_passivep)
 {
     if (!(zfprefs & (ZFPF_SNDP|ZFPF_PASV))) {
 	zwarnnam(name, "Must set preference S or P to transfer data", NULL, 0);
 	return 1;
     }
-    zdfd = zfmovefd(socket(AF_INET, SOCK_STREAM, 0));
-    if (zdfd < 0) {
+    zfsess->dfd = socket(AF_INET, SOCK_STREAM, 0);
+    if (zfsess->dfd < 0) {
 	zwarnnam(name, "can't get data socket: %e", NULL, errno);
 	return 1;
     }
 
-    zdsock = zsock;
-    zdsock.sin_family = AF_INET;
-
-    if (!(zfstatus & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
-	char *ptr;
-	int i, nums[6], err;
-	unsigned char iaddr[4], iport[2];
+    if (!(zfstatusp[zfsessno] & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
+	char *psv_cmd;
+	int err, salen;
 
-	if (zfsendcmd("PASV\r\n") == 6)
+#ifdef SUPPORT_IPV6
+	if(zfsess->peer.a.sa_family == AF_INET6)
+	    psv_cmd = "EPSV\r\n";
+	else
+#endif /* SUPPORT_IPV6 */
+	    psv_cmd = "PASV\r\n";
+	if (zfsendcmd(psv_cmd) == 6)
 	    return 1;
 	else if (lastcode >= 500 && lastcode <= 504) {
 	    /*
 	     * Fall back to send port mode.  That will
 	     * test the preferences for whether that's OK.
 	     */
-	    zfstatus |= ZFST_NOPS;
-	    zfclosedata();
-	    return zfopendata(name);
-	}
-	/*
-	 * OK, now we need to know what port we're looking at,
-	 * which is cunningly concealed in the reply.
-	 * lastmsg already has the reply code expunged.
-	 */
-	for (ptr = lastmsg; *ptr; ptr++)
-	    if (isdigit(*ptr))
-		break;
-	if (sscanf(ptr, "%d,%d,%d,%d,%d,%d",
-		   nums, nums+1, nums+2, nums+3, nums+4, nums+5) != 6) {
-	    zwarnnam(name, "bad response to PASV: %s", lastmsg, 0);
+	    zfstatusp[zfsessno] |= ZFST_NOPS;
 	    zfclosedata();
-	    return 1;
+	    return zfopendata(name, zdsockp, is_passivep);
 	}
-	for (i = 0; i < 4; i++)
-	    iaddr[i] = STOUC(nums[i]);
-	iport[0] = STOUC(nums[4]);
-	iport[1] = STOUC(nums[5]);
+	zdsockp->a.sa_family = zfsess->peer.a.sa_family;
+#ifdef SUPPORT_IPV6
+	if(zfsess->peer.a.sa_family == AF_INET6) {
+	    /* see RFC 2428 for explanation */
+	    char const *ptr, *end;
+	    char delim, portbuf[6], *pbp;
+	    unsigned long portnum;
+	    ptr = strchr(lastmsg, '(');
+	    if(!ptr) {
+	    bad_epsv:
+		zwarnnam(name, "bad response to EPSV: %s", lastmsg, 0);
+		zfclosedata();
+		return 1;
+	    }
+	    delim = ptr[1];
+	    if(delim < 33 || delim > 126 || ptr[2] != delim || ptr[3] != delim)
+		goto bad_epsv;
+	    ptr += 3;
+	    end = strchr(ptr, delim);
+	    if(!end || end[1] != ')')
+		goto bad_epsv;
+	    while(ptr != end && *ptr == '0')
+		ptr++;
+	    if(ptr == end || (end-ptr) > 5 || !isdigit(STOUC(*ptr)))
+		goto bad_epsv;
+	    memcpy(portbuf, ptr, (end-ptr));
+	    portbuf[end-ptr] = 0;
+	    portnum = strtoul(portbuf, &pbp, 10);
+	    if(*pbp || portnum > 65535UL)
+		goto bad_epsv;
+	    *zdsockp = zfsess->peer;
+	    zdsockp->in6.sin6_port = htons((unsigned)portnum);
+	    salen = sizeof(struct sockaddr_in6);
+	} else
+#endif /* SUPPORT_IPV6 */
+	{
+	    char *ptr;
+	    int i, nums[6];
+	    unsigned char iaddr[4], iport[2];
 
-	memcpy(&zdsock.sin_addr, iaddr, sizeof(iaddr));
-	memcpy(&zdsock.sin_port, iport, sizeof(iport));
+	    /*
+	     * OK, now we need to know what port we're looking at,
+	     * which is cunningly concealed in the reply.
+	     * lastmsg already has the reply code expunged.
+	     */
+	    for (ptr = lastmsg; *ptr; ptr++)
+		if (isdigit(STOUC(*ptr)))
+		    break;
+	    if (sscanf(ptr, "%d,%d,%d,%d,%d,%d",
+		       nums, nums+1, nums+2, nums+3, nums+4, nums+5) != 6) {
+		zwarnnam(name, "bad response to PASV: %s", lastmsg, 0);
+		zfclosedata();
+		return 1;
+	    }
+	    for (i = 0; i < 4; i++)
+		iaddr[i] = STOUC(nums[i]);
+	    iport[0] = STOUC(nums[4]);
+	    iport[1] = STOUC(nums[5]);
+
+	    memcpy(&zdsockp->in.sin_addr, iaddr, sizeof(iaddr));
+	    memcpy(&zdsockp->in.sin_port, iport, sizeof(iport));
+	    salen = sizeof(struct sockaddr_in);
+	}
 
 	/* we should timeout this connect */
 	do {
-	    err = connect(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock));
+	    err = connect(zfsess->dfd, (struct sockaddr *)zdsockp, salen);
 	} while (err && errno == EINTR && !errflag);
 
 	if (err) {
@@ -878,10 +1098,13 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	zfpassive_conn = 1;
+	*is_passivep = 1;
     } else {
+#ifdef SUPPORT_IPV6
+	char portcmd[8+INET6_ADDRSTRLEN+9];
+#else
 	char portcmd[40];
-	unsigned char *addr, *port;
+#endif
 	int ret, len;
 
 	if (!(zfprefs & ZFPF_SNDP)) {
@@ -889,14 +1112,24 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	zdsock.sin_port = 0;	/* to be set by bind() */
-	len = sizeof(zdsock);
+	*zdsockp = zfsess->sock;
+#ifdef SUPPORT_IPV6
+	if(zdsockp->a.sa_family == AF_INET6) {
+	    zdsockp->in6.sin6_port = 0;	/* to be set by bind() */
+	    len = sizeof(struct sockaddr_in6);
+	} else
+#endif /* SUPPORT_IPV6 */
+	{
+	    zdsockp->in.sin_port = 0;	/* to be set by bind() */
+	    len = sizeof(struct sockaddr_in);
+	}
 	/* need to do timeout stuff and probably handle EINTR here */
-	if (bind(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock)) < 0)
+	if (bind(zfsess->dfd, (struct sockaddr *)zdsockp, len) < 0)
 	    ret = 1;
-	else if (getsockname(zdfd, (struct sockaddr *)&zdsock, &len) < 0)
+	else if (getsockname(zfsess->dfd, (struct sockaddr *)zdsockp,
+			     &len) < 0)
 	    ret = 2;
-	else if (listen(zdfd, 1) < 0)
+	else if (listen(zfsess->dfd, 1) < 0)
 	    ret = 3;
 	else
 	    ret = 0;
@@ -909,16 +1142,28 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	addr = (unsigned char *) &zdsock.sin_addr;
-	port = (unsigned char *) &zdsock.sin_port;
-	sprintf(portcmd, "PORT %d,%d,%d,%d,%d,%d\r\n",
-		addr[0],addr[1],addr[2],addr[3],port[0],port[1]);
+#ifdef SUPPORT_IPV6
+	if(zdsockp->a.sa_family == AF_INET6) {
+	    /* see RFC 2428 for explanation */
+	    strcpy(portcmd, "EPRT |2|");
+	    inet_ntop(AF_INET6, &zdsockp->in6.sin6_addr,
+		portcmd+8, INET6_ADDRSTRLEN);
+	    sprintf(strchr(portcmd, 0), "|%u|\r\n",
+		(unsigned)ntohs(zdsockp->in6.sin6_port));
+	} else
+#endif /* SUPPORT_IPV6 */
+	{
+	    unsigned char *addr = (unsigned char *) &zdsockp->in.sin_addr;
+	    unsigned char *port = (unsigned char *) &zdsockp->in.sin_port;
+	    sprintf(portcmd, "PORT %d,%d,%d,%d,%d,%d\r\n",
+		    addr[0],addr[1],addr[2],addr[3],port[0],port[1]);
+	}
 	if (zfsendcmd(portcmd) >= 5) {
 	    zwarnnam(name, "port command failed", NULL, 0);
 	    zfclosedata();
 	    return 1;
 	}
-	zfpassive_conn = 0;
+	*is_passivep = 0;
     }
 
     return 0;
@@ -930,15 +1175,15 @@ zfopendata(char *name)
 static void
 zfclosedata(void)
 {
-    if (zdfd == -1)
+    if (zfsess->dfd == -1)
 	return;
-    close(zdfd);
-    zdfd = -1;
+    close(zfsess->dfd);
+    zfsess->dfd = -1;
 }
 
 /*
  * Set up a data connection and use cmd to initiate a transfer.
- * The actual data fd will be zdfd; the calling routine
+ * The actual data fd will be zfsess->dfd; the calling routine
  * must handle the data itself.
  * rest is a REST command to specify starting somewhere other
  * then the start of the remote file.
@@ -952,9 +1197,10 @@ zfclosedata(void)
 static int
 zfgetdata(char *name, char *rest, char *cmd, int getsize)
 {
-    int len, newfd;
+    int len, newfd, is_passive;
+    union zftp_sockaddr zdsock;
 
-    if (zfopendata(name))
+    if (zfopendata(name, &zdsock, &is_passive))
 	return 1;
 
     /*
@@ -976,7 +1222,8 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 	zfclosedata();
 	return 1;
     }
-    if (getsize || (!(zfstatus & ZFST_TRSZ) && !strncmp(cmd, "RETR", 4))) {
+    if (getsize || (!(zfstatusp[zfsessno] & ZFST_TRSZ) &&
+		    !strncmp(cmd, "RETR", 4))) {
 	/*
 	 * See if we got something like:
 	 *   Opening data connection for nortypix.gif (1234567 bytes).
@@ -984,25 +1231,25 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 	 * can avoid sending a special SIZE command beforehand.
 	 */
 	char *ptr = strstr(lastmsg, "bytes");
-	zfstatus |= ZFST_NOSZ|ZFST_TRSZ;
+	zfstatusp[zfsessno] |= ZFST_NOSZ|ZFST_TRSZ;
 	if (ptr) {
-	    while (ptr > lastmsg && !isdigit(*ptr))
+	    while (ptr > lastmsg && !isdigit(STOUC(*ptr)))
 		ptr--;
-	    while (ptr > lastmsg && isdigit(ptr[-1]))
+	    while (ptr > lastmsg && isdigit(STOUC(ptr[-1])))
 		ptr--;
-	    if (isdigit(*ptr)) {
-		zfstatus &= ~ZFST_NOSZ;
+	    if (isdigit(STOUC(*ptr))) {
+		zfstatusp[zfsessno] &= ~ZFST_NOSZ;
 		if (getsize) {
-		    long sz = zstrtol(ptr, NULL, 10);
+		    off_t sz = zstrtol(ptr, NULL, 10);
 		    zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
 		}
 	    }
 	}
     }
 
-    if (!zfpassive_conn) {
+    if (!is_passive) {
 	/*
-	 * the current zdfd is the socket we opened, but we need
+	 * the current zfsess->dfd is the socket we opened, but we need
 	 * to let the server set up a different fd for reading/writing.
 	 * then we can close the fd we were listening for a connection on.
 	 * don't expect me to understand this, i'm only the programmer.
@@ -1010,13 +1257,21 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 
 	/* accept the connection */
 	len = sizeof(zdsock);
-	newfd = zfmovefd(accept(zdfd, (struct sockaddr *)&zdsock, &len));
+	newfd = zfmovefd(accept(zfsess->dfd, (struct sockaddr *)&zdsock, 
+				&len));
+	if (newfd < 0)
+	    zwarnnam(name, "unable to accept data: %e", NULL, errno);
 	zfclosedata();
-	if (newfd < 0) {
-	    zwarnnam(name, "unable to accept data.", NULL, 0);
+	if (newfd < 0)
 	    return 1;
-	}
-	zdfd = newfd;		/* this is now the actual data fd */
+	zfsess->dfd = newfd;	/* this is now the actual data fd */
+    } else {
+	/*
+	 * We avoided dup'ing zfsess->dfd up to this point, to try to keep
+	 * things simple, so we now need to move it out of the way
+	 * of the user-visible fd's.
+	 */
+	zfsess->dfd = zfmovefd(zfsess->dfd);
     }
 
 
@@ -1035,20 +1290,21 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 
 	li.l_onoff = 1;
 	li.l_linger = 120;
-	setsockopt(zdfd, SOL_SOCKET, SO_LINGER, (char *)&li, sizeof(li));
+	setsockopt(zfsess->dfd, SOL_SOCKET, SO_LINGER,
+		   (char *)&li, sizeof(li));
     }
 #endif
 #if defined(IP_TOS) && defined(IPTOS_THROUGHPUT)
     /* try to get high throughput, snigger */
     {
 	int arg = IPTOS_THROUGHPUT;
-	setsockopt(zdfd, IPPROTO_IP, IP_TOS, (char *)&arg, sizeof(arg));
+	setsockopt(zfsess->dfd, IPPROTO_IP, IP_TOS, (char *)&arg, sizeof(arg));
     }
 #endif
 #if defined(F_SETFD) && defined(FD_CLOEXEC)
 	/* If the shell execs a program, we don't want this fd left open. */
 	len = FD_CLOEXEC;
-	fcntl(zdfd, F_SETFD, &len);
+	fcntl(zfsess->dfd, F_SETFD, &len);
 #endif
 
     return 0;
@@ -1067,9 +1323,9 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 
 /**/
 static int
-zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
+zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)
 {
-    long sz = -1;
+    off_t sz = -1;
     char *mt = NULL;
     int ret;
 
@@ -1079,15 +1335,15 @@ zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
 	*retmdtm = NULL;
     if (remote) {
 	char *cmd;
-	if ((zfhas_size == ZFCP_NOPE && retsize) ||
-	    (zfhas_mdtm == ZFCP_NOPE && retmdtm))
+	if ((zfsess->has_size == ZFCP_NOPE && retsize) ||
+	    (zfsess->has_mdtm == ZFCP_NOPE && retmdtm))
 	    return 2;
 
 	/*
 	 * File is coming from over there.
 	 * Make sure we get the type right.
 	 */
-	zfsettype(ZFST_TYPE(zfstatus));
+	zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));
 	if (retsize) {
 	    cmd = tricat("SIZE ", fnam, "\r\n");
 	    ret = zfsendcmd(cmd);
@@ -1096,9 +1352,9 @@ zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
 		return 1;
 	    else if (lastcode < 300) {
 		sz = zstrtol(lastmsg, 0, 10);
-		zfhas_size = ZFCP_YUPP;
+		zfsess->has_size = ZFCP_YUPP;
 	    } else if (lastcode >= 500 && lastcode <= 504) {
-		zfhas_size = ZFCP_NOPE;
+		zfsess->has_size = ZFCP_NOPE;
 		return 2;
 	    } else if (lastcode == 550)
 		return 1;
@@ -1113,9 +1369,9 @@ zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
 		return 1;
 	    else if (lastcode < 300) {
 		mt = ztrdup(lastmsg);
-		zfhas_mdtm = ZFCP_YUPP;
+		zfsess->has_mdtm = ZFCP_YUPP;
 	    } else if (lastcode >= 500 && lastcode <= 504) {
-		zfhas_mdtm = ZFCP_NOPE;
+		zfsess->has_mdtm = ZFCP_NOPE;
 		return 2;
 	    } else if (lastcode == 550)
 		return 1;
@@ -1128,7 +1384,7 @@ zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
 
 	if ((fd == -1 ? stat(fnam, &statbuf) : fstat(fd, &statbuf)) < 0)
 	    return 1;
-	/* make sure it's long, since this has to be a pointer */
+	/* make sure it's off_t, since this has to be a pointer */
 	sz = statbuf.st_size;
 
 	if (retmdtm) {
@@ -1156,9 +1412,9 @@ zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
 
 /**/
 static void
-zfstarttrans(char *nam, int recv, long sz)
+zfstarttrans(char *nam, int recv, off_t sz)
 {
-    long cnt = 0;
+    off_t cnt = 0;
     /*
      * sz = -1 signifies error getting size.  don't set ZFTP_SIZE if sz is
      * zero, either: it probably came from an fstat() on a pipe, so it
@@ -1187,7 +1443,7 @@ zfendtrans()
 
 /**/
 static int
-zfread(int fd, char *bf, size_t sz, int tmout)
+zfread(int fd, char *bf, off_t sz, int tmout)
 {
     int ret;
 
@@ -1212,7 +1468,7 @@ zfread(int fd, char *bf, size_t sz, int tmout)
 
 /**/
 static int
-zfwrite(int fd, char *bf, size_t sz, int tmout)
+zfwrite(int fd, char *bf, off_t sz, int tmout)
 {
     int ret;
 
@@ -1239,11 +1495,11 @@ static int zfread_eof;
 
 /**/
 static int
-zfread_block(int fd, char *bf, size_t sz, int tmout)
+zfread_block(int fd, char *bf, off_t sz, int tmout)
 {
     int n;
     struct zfheader hdr;
-    size_t blksz, cnt;
+    off_t blksz, cnt;
     char *bfptr;
     do {
 	/* we need the header */
@@ -1251,7 +1507,7 @@ zfread_block(int fd, char *bf, size_t sz, int tmout)
 	    n = zfread(fd, (char *)&hdr, sizeof(hdr), tmout);
 	} while (n < 0 && errno == EINTR);
 	if (n != 3 && !zfdrrrring) {
-	    zwarnnam("zftp", "failed to read FTP block header", NULL, 0);
+	    zwarnnam("zftp", "failure reading FTP block header", NULL, 0);
 	    return n;
 	}
 	/* size is stored in network byte order */
@@ -1291,11 +1547,11 @@ zfread_block(int fd, char *bf, size_t sz, int tmout)
 
 /**/
 static int
-zfwrite_block(int fd, char *bf, size_t sz, int tmout)
+zfwrite_block(int fd, char *bf, off_t sz, int tmout)
 {
     int n;
     struct zfheader hdr;
-    size_t cnt;
+    off_t cnt;
     char *bfptr;
     /* we need the header */
     do {
@@ -1305,7 +1561,7 @@ zfwrite_block(int fd, char *bf, size_t sz, int tmout)
 	n = zfwrite(fd, (char *)&hdr, sizeof(hdr), tmout);
     } while (n < 0 && errno == EINTR);
     if (n != 3 && !zfdrrrring) {
-	zwarnnam("zftp", "failed to write FTP block header", NULL, 0);
+	zwarnnam("zftp", "failure writing FTP block header", NULL, 0);
 	return n;
     }
     bfptr = bf;
@@ -1336,7 +1592,7 @@ zfwrite_block(int fd, char *bf, size_t sz, int tmout)
 
 /**/
 static int
-zfsenddata(char *name, int recv, int progress, long startat)
+zfsenddata(char *name, int recv, int progress, off_t startat)
 {
 #define ZF_BUFSIZE 32768
 #define ZF_ASCSIZE (ZF_BUFSIZE/2)
@@ -1344,35 +1600,39 @@ zfsenddata(char *name, int recv, int progress, long startat)
     int n, ret = 0, gotack = 0, fdin, fdout, fromasc = 0, toasc = 0;
     int rtmout = 0, wtmout = 0;
     char lsbuf[ZF_BUFSIZE], *ascbuf = NULL, *optr;
-    long sofar = 0, last_sofar = 0;
+    off_t sofar = 0, last_sofar = 0;
     readwrite_t read_ptr = zfread, write_ptr = zfwrite;
-    List l;
+    Eprog prog;
 
-    if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+    if (progress && (prog = getshfunc("zftp_progress")) != &dummy_eprog) {
 	/*
 	 * progress to set up:  ZFTP_COUNT is zero.
 	 * We do this here in case we needed to wait for a RETR
 	 * command to tell us how many bytes are coming.
 	 */
-	doshfunc("zftp_progress", l, NULL, 0, 1);
+	int osc = sfcontext;
+
+	sfcontext = SFC_HOOK;
+	doshfunc("zftp_progress", prog, NULL, 0, 1);
+	sfcontext = osc;
 	/* Now add in the bit of the file we've got/sent already */
 	sofar = last_sofar = startat;
     }
     if (recv) {
-	fdin = zdfd;
+	fdin = zfsess->dfd;
 	fdout = 1;
 	rtmout = getiparam("ZFTP_TMOUT");
-	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	if (ZFST_CTYP(zfstatusp[zfsessno]) == ZFST_ASCI)
 	    fromasc = 1;
-	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	if (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC)
 	    read_ptr = zfread_block;
     } else {
 	fdin = 0;
-	fdout = zdfd;
+	fdout = zfsess->dfd;
 	wtmout = getiparam("ZFTP_TMOUT");
-	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	if (ZFST_CTYP(zfstatusp[zfsessno]) == ZFST_ASCI)
 	    toasc = 1;
-	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	if (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC)
 	    write_ptr = zfwrite_block;
     }
 
@@ -1481,9 +1741,13 @@ zfsenddata(char *name, int recv, int progress, long startat)
 	} else
 	    break;
 	if (!ret && sofar != last_sofar && progress &&
-	    (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    (prog = getshfunc("zftp_progress")) != &dummy_eprog) {
+	    int osc = sfcontext;
+
 	    zfsetparam("ZFTP_COUNT", &sofar, ZFPM_READONLY|ZFPM_INTEGER);
-	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	    sfcontext = SFC_HOOK;
+	    doshfunc("zftp_progress", prog, NULL, 0, 1);
+	    sfcontext = osc;
 	    last_sofar = sofar;
 	}
     }
@@ -1493,7 +1757,8 @@ zfsenddata(char *name, int recv, int progress, long startat)
      * so we don't need to force the control connection to close.
      */
     zfdrrrring = 0;
-    if (!errflag && !ret && !recv && ZFST_MODE(zfstatus) == ZFST_BLOC) {
+    if (!errflag && !ret && !recv &&
+	ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC) {
 	/* send an end-of-file marker block */
 	ret = (zfwrite_block(fdout, lsbuf, 0, wtmout) < 0);
     }
@@ -1522,8 +1787,8 @@ zfsenddata(char *name, int recv, int progress, long startat)
 
 	/* the following is black magic, as far as I'm concerned. */
 	/* what are we going to do if it fails?  not a lot, actually. */
-	send(zcfd, (char *)msg, 3, 0);
-	send(zcfd, (char *)msg+3, 1, MSG_OOB);
+	send(zfsess->cfd, (char *)msg, 3, 0);
+	send(zfsess->cfd, (char *)msg+3, 1, MSG_OOB);
 
 	zfsendcmd("ABOR\r\n");
 	if (lastcode == 226) {
@@ -1563,16 +1828,16 @@ zfsenddata(char *name, int recv, int progress, long startat)
 static int
 zftp_open(char *name, char **args, int flags)
 {
-    struct in_addr ipaddr;
     struct protoent *zprotop;
     struct servent *zservp;
     struct hostent *zhostp = NULL;
-    char **addrp, tbuf[2] = "X", *fname;
+    char **addrp, *fname;
     int err, len, tmout;
+    int herrno, af, salen;
 
     if (!*args) {
-	if (zfuserparams)
-	    args = zfuserparams;
+	if (zfsess->userparams)
+	    args = zfsess->userparams;
 	else {
 	    zwarnnam(name, "no host specified", NULL, 0);
 	    return 1;
@@ -1584,8 +1849,8 @@ zftp_open(char *name, char **args, int flags)
      * Probably this is the safest thing to do.  It's possible
      * a `QUIT' will hang, though.
      */
-    if (zcfd != -1)
-	zfclose();
+    if (zfsess->cfd != -1)
+	zfclose(0);
 
     /* this is going to give 0.  why bother? */
     zprotop = getprotobyname("tcp");
@@ -1611,99 +1876,126 @@ zftp_open(char *name, char **args, int flags)
 	    zwarnnam(name, "timeout connecting to %s", hname, 0);
 	else
 	    zwarnnam(name, "timeout on host name lookup", NULL, 0);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
     zfalarm(tmout);
 
-    /*
-     * Now this is what I like.  A library which provides decent higher
-     * level functions to do things like converting address types.  It saves
-     * so much trouble.  Pity about the rest of the network interface, though.
-     */
-    ipaddr.s_addr = inet_addr(args[0]);
-    if (ipaddr.s_addr != INADDR_NONE) {
-	/*
-	 * hmmm, we don't necessarily want to do this... maybe the
-	 * user is actively trying to avoid a bad nameserver.
-	 * perhaps better just to set ZFTP_HOST to the dot address, too.
-	 * that way shell functions know how it was opened.
-	 *
-	 * 	zhostp = gethostbyaddr(&ipaddr, sizeof(ipaddr), AF_INET);
-	 *
-	 * or, we could have a `no_lookup' flag.
-	 */
-	zfsetparam("ZFTP_HOST", ztrdup(args[0]), ZFPM_READONLY);
-	zsock.sin_family = AF_INET;
-    } else {
-	zhostp = gethostbyname(args[0]);
+#ifdef SUPPORT_IPV6
+    for(af=AF_INET6; 1; af = AF_INET)
+# define SUCCEEDED() break
+# define FAILED() if(af == AF_INET) { } else continue
+#else
+    af = AF_INET;
+# define SUCCEEDED() do { } while(0)
+# define FAILED() do { } while(0)
+#endif
+    {
+    	zhostp = getipnodebyname(args[0], af, 0, &herrno);
 	if (!zhostp || errflag) {
 	    /* should use herror() here if available, but maybe
 	     * needs configure test. on AIX it's present but not
 	     * in headers.
 	     */
+	    FAILED();
 	    zwarnnam(name, "host not found: %s", args[0], 0);
 	    alarm(0);
 	    return 1;
 	}
-	zsock.sin_family = zhostp->h_addrtype;
 	zfsetparam("ZFTP_HOST", ztrdup(zhostp->h_name), ZFPM_READONLY);
-    }
 
-    zsock.sin_port = ntohs(zservp->s_port);
-    zcfd = zfmovefd(socket(zsock.sin_family, SOCK_STREAM, 0));
-    if (zcfd < 0) {
-	zwarnnam(name, "socket failed: %e", NULL, errno);
-	zfunsetparam("ZFTP_HOST");
-	alarm(0);
-	return 1;
-    }
+	zfsess->peer.a.sa_family = af;
+#ifdef SUPPORT_IPV6
+	if(af == AF_INET6) {
+	    zfsess->peer.in6.sin6_port = zservp->s_port;
+	    zfsess->peer.in6.sin6_flowinfo = 0;
+# ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID
+	    zfsess->peer.in6.sin6_scope_id = 0;
+# endif
+	    salen = sizeof(struct sockaddr_in6);
+	} else
+#endif /* SUPPORT_IPV6 */
+	{
+	    zfsess->peer.in.sin_port = zservp->s_port;
+	    salen = sizeof(struct sockaddr_in);
+	}
 
-#if defined(F_SETFD) && defined(FD_CLOEXEC)
-    /* If the shell execs a program, we don't want this fd left open. */
-    len = FD_CLOEXEC;
-    fcntl(zcfd, F_SETFD, &len);
-#endif
+	zfsess->cfd = socket(af, SOCK_STREAM, 0);
+	if (zfsess->cfd < 0) {
+	    freehostent(zhostp);
+	    zfunsetparam("ZFTP_HOST");
+	    FAILED();
+	    zwarnnam(name, "socket failed: %e", NULL, errno);
+	    alarm(0);
+	    return 1;
+	}
+	/* counts as `open' so long as it's not negative */
+	zfnopen++;
 
-    /*
-     * now connect the socket.  manual pages all say things like `this is all
-     * explained oh-so-wonderfully in some other manual page'.  not.
-     */
+	/*
+	 * now connect the socket.  manual pages all say things like `this is
+	 * all explained oh-so-wonderfully in some other manual page'.  not.
+	 */
 
-    err = 1;
+	err = 1;
 
-    if (ipaddr.s_addr != INADDR_NONE) {
-	/* dot address */
-	memcpy(&zsock.sin_addr, &ipaddr, sizeof(ipaddr));
-	do {
-	    err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
-	} while (err && errno == EINTR && !errflag);
-    } else {
-	/* host name: try all possible IP's */
-	for (addrp = zhostp->h_addr_list; *addrp; addrp++) {
-	    memcpy(&zsock.sin_addr, *addrp, zhostp->h_length);
+	/* try all possible IP's */
+	for (addrp = zhostp->h_addr_list; err && *addrp; addrp++) {
+#ifdef SUPPORT_IPV6
+	    if(af == AF_INET6)
+		memcpy(&zfsess->peer.in6.sin6_addr, *addrp, zhostp->h_length);
+	    else
+#endif /* SUPPORT_IPV6 */
+		memcpy(&zfsess->peer.in.sin_addr, *addrp, zhostp->h_length);
 	    do {
-		err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+		err = connect(zfsess->cfd, (struct sockaddr *)&zfsess->peer,
+			      salen);
 	    } while (err && errno == EINTR && !errflag);
 	    /* you can check whether it's worth retrying here */
 	}
-    }
 
-    alarm(0);
+	if (err) {
+	    freehostent(zhostp);
+	    zfclose(0);
+	    FAILED();
+	    zwarnnam(name, "connect failed: %e", NULL, errno);
+	    alarm(0);
+	    return 1;
+	}
 
-    if (err < 0) {
-	zwarnnam(name, "connect failed: %e", NULL, errno);
-	zfclose();
-	return 1;
+	SUCCEEDED();
     }
-    zfsetparam("ZFTP_IP", ztrdup(inet_ntoa(zsock.sin_addr)), ZFPM_READONLY);
+    alarm(0);
+    {
+#ifdef SUPPORT_IPV6
+	char pbuf[INET6_ADDRSTRLEN];
+#else
+	char pbuf[INET_ADDRSTRLEN];
+#endif
+	addrp--;
+	inet_ntop(af, *addrp, pbuf, sizeof(pbuf));
+	zfsetparam("ZFTP_IP", ztrdup(pbuf), ZFPM_READONLY);
+    }
+    freehostent(zhostp);
     /* now we can talk to the control connection */
     zcfinish = 0;
 
-    len = sizeof(zsock);
-    if (getsockname(zcfd, (struct sockaddr *)&zsock, &len) < 0) {
+    /*
+     * Move the fd out of the user-visible range.  We need to do
+     * this after the connect() on some systems.
+     */
+    zfsess->cfd = zfmovefd(zfsess->cfd);
+
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+    /* If the shell execs a program, we don't want this fd left open. */
+    len = FD_CLOEXEC;
+    fcntl(zfsess->cfd, F_SETFD, &len);
+#endif
+
+    len = sizeof(zfsess->sock);
+    if (getsockname(zfsess->cfd, (struct sockaddr *)&zfsess->sock, &len) < 0) {
 	zwarnnam(name, "getsockname failed: %e", NULL, errno);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
     /* nice to get some options right, ignore if they don't work */
@@ -1713,89 +2005,75 @@ zftp_open(char *name, char **args, int flags)
      * do clever things with SIGURG.
      */
     len = 1;
-    setsockopt(zcfd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
+    setsockopt(zfsess->cfd, SOL_SOCKET, SO_OOBINLINE,
+	       (char *)&len, sizeof(len));
 #endif
 #if defined(IP_TOS) && defined(IPTOS_LOWDELAY)
     /* for control connection we want low delay.  please don't laugh. */
     len = IPTOS_LOWDELAY;
-    setsockopt(zcfd, IPPROTO_IP, IP_TOS, (char *)&len, sizeof(len));
+    setsockopt(zfsess->cfd, IPPROTO_IP, IP_TOS, (char *)&len, sizeof(len));
 #endif
 
     /*
      * We use stdio with line buffering for convenience on input.
      * On output, we can just dump a complete message to the fd via write().
      */
-    zcin = fdopen(zcfd, "r");
+    zfsess->cin = fdopen(zfsess->cfd, "r");
 
-    if (!zcin) {
+    if (!zfsess->cin) {
 	zwarnnam(name, "file handling error", NULL, 0);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
 
 #ifdef _IONBF
-    setvbuf(zcin, NULL, _IONBF, 0);
+    setvbuf(zfsess->cin, NULL, _IONBF, 0);
 #else
-    setlinebuf(zcin);
+    setlinebuf(zfsess->cin);
 #endif
 
     /*
      * now see what the remote server has to say about that.
      */
     if (zfgetmsg() >= 4) {
-	zfclose();
+	zfclose(0);
 	return 1;
     }
 
-    zfis_unix = 0;
-    zfhas_size = zfhas_mdtm = ZFCP_UNKN;
-    zdfd = -1;
+    zfsess->has_size = zfsess->has_mdtm = ZFCP_UNKN;
+    zfsess->dfd = -1;
     /* initial status: open, ASCII data, stream mode 'n' stuff */
-    zfstatus = 0;
+    zfstatusp[zfsessno] = 0;
 
-    /* open file for saving the current status */
-    fname = gettempname();
-    zfstatfd = open(fname, O_RDWR|O_CREAT, 0600);
-    DPUTS(zfstatfd == -1, "zfstatfd not created");
+    /*
+     * Open file for saving the current status.
+     * We keep this open at the end of the session because
+     * it is used to store the status for all sessions.
+     * However, it is closed whenever there are no connections open.
+     */
+    if (zfstatfd == -1) {
+	fname = gettempname();
+	zfstatfd = open(fname, O_RDWR|O_CREAT, 0600);
+	DPUTS(zfstatfd == -1, "zfstatfd not created");
 #if defined(F_SETFD) && defined(FD_CLOEXEC)
-    /* If the shell execs a program, we don't want this fd left open. */
-    len = FD_CLOEXEC;
-    fcntl(zfstatfd, F_SETFD, &len);
+	/* If the shell execs a program, we don't want this fd left open. */
+	len = FD_CLOEXEC;
+	fcntl(zfstatfd, F_SETFD, &len);
 #endif
-    unlink(fname);
-
-    /* now find out what system we're connected to */
-    if (!(zfprefs & ZFPF_DUMB) && zfsendcmd("SYST\r\n") == 2) {
-	char *ptr = lastmsg, *eptr, *systype;
-	for (eptr = ptr; *eptr; eptr++)
-	    ;
-	systype = ztrduppfx(ptr, eptr-ptr);
-	if (!strncmp(systype, "UNIX Type: L8", 13)) {
-	    /*
-	     * Use binary for transfers.  This simple test saves much
-	     * hassle for all concerned, particularly me.
-	     */
-	    zfstatus |= ZFST_IMAG;
-	    zfis_unix = 1;
-	}
-	/*
-	 * we could set zfis_unix based just on the UNIX part,
-	 * but I don't really know the consequences of that.
-	 */
-	zfsetparam("ZFTP_SYSTEM", systype, ZFPM_READONLY);
-    } else if (zcfd == -1) {
+	unlink(fname);
+    }
+
+    if (zfsess->cfd == -1) {
 	/* final paranoid check */
 	return 1;
     }
 	
-    tbuf[0] = (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I';
-    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
     zfsetparam("ZFTP_MODE", ztrdup("S"), ZFPM_READONLY);
     /* if remaining arguments, use them to log in. */
-    if (zcfd > -1 && *++args)
+    if (zfsess->cfd > -1 && *++args)
 	return zftp_login(name, args, flags);
     /* if something wayward happened, connection was already closed */
-    return zcfd == -1;
+    return zfsess->cfd == -1;
 }
 
 /*
@@ -1877,8 +2155,8 @@ zftp_params(char *name, char **args, int flags)
     int i, j, len;
 
     if (!*args) {
-	if (zfuserparams) {
-	    for (aptr = zfuserparams, i = 0; *aptr; aptr++, i++) {
+	if (zfsess->userparams) {
+	    for (aptr = zfsess->userparams, i = 0; *aptr; aptr++, i++) {
 		if (i == 2) {
 		    len = strlen(*aptr);
 		    for (j = 0; j < len; j++)
@@ -1887,23 +2165,24 @@ zftp_params(char *name, char **args, int flags)
 		} else
 		    fprintf(stdout, "%s\n", *aptr);
 	    }
-	}
-	return 0;
+	    return 0;
+	} else
+	    return 1;
     }
     if (!strcmp(*args, "-")) {
-	if (zfuserparams)
-	    freearray(zfuserparams);
-	zfuserparams = 0;
+	if (zfsess->userparams)
+	    freearray(zfsess->userparams);
+	zfsess->userparams = 0;
 	return 0;
     }
     len = arrlen(args);
     newarr = (char **)zcalloc((len+1)*sizeof(char *));
     for (aptr = args, i = 0; *aptr && !errflag; aptr++, i++) {
 	char *str;
-	if (!strcmp(*aptr, "?"))
-	    str = zfgetinfo(prompts[i], i == 2);
+	if (**aptr == '?')
+	    str = zfgetinfo((*aptr)[1] ? (*aptr+1) : prompts[i], i == 2);
 	else
-	    str = *aptr;
+	    str = (**aptr == '\\') ? *aptr+1 : *aptr;
 	newarr[i] = ztrdup(str);
     }
     if (errflag) {
@@ -1913,9 +2192,9 @@ zftp_params(char *name, char **args, int flags)
 	zfree(newarr, len+1);
 	return 1;
     }
-    if (zfuserparams)
-	freearray(zfuserparams);
-    zfuserparams = newarr;
+    if (zfsess->userparams)
+	freearray(zfsess->userparams);
+    zfsess->userparams = newarr;
     return 0;
 }
 
@@ -1926,13 +2205,13 @@ static int
 zftp_login(char *name, char **args, int flags)
 {
     char *ucmd, *passwd = NULL, *acct = NULL;
-    char *user;
+    char *user, tbuf[2] = "X";
     int stopit;
 
-    if ((zfstatus & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
+    if ((zfstatusp[zfsessno] & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
 	return 1;
 
-    zfstatus &= ~ZFST_LOGI;
+    zfstatusp[zfsessno] &= ~ZFST_LOGI;
     if (*args) {
 	user = *args++;
     } else {
@@ -1988,7 +2267,7 @@ zftp_login(char *name, char **args, int flags)
     }
 
     zsfree(ucmd);
-    if (zcfd == -1)
+    if (zfsess->cfd == -1)
 	return 1;
     if (stopit == 2 || (lastcode != 230 && lastcode != 202)) {
 	zwarnnam(name, "login failed", NULL, 0);
@@ -2001,12 +2280,40 @@ zftp_login(char *name, char **args, int flags)
 	    cnt++;
 	zwarnnam(name, "warning: %d comand arguments not used\n", NULL, cnt);
     }
-    zfstatus |= ZFST_LOGI;
+    zfstatusp[zfsessno] |= ZFST_LOGI;
     zfsetparam("ZFTP_USER", ztrdup(user), ZFPM_READONLY);
     if (acct)
 	zfsetparam("ZFTP_ACCOUNT", ztrdup(acct), ZFPM_READONLY);
 
     /*
+     * Now find out what system we're connected to. Some systems
+     * won't let us do this until we're logged in; it's fairly safe
+     * to delay it here for all systems.
+     */
+    if (!(zfprefs & ZFPF_DUMB) && !(zfstatusp[zfsessno] & ZFST_SYST)) {
+	if (zfsendcmd("SYST\r\n") == 2) {
+	    char *ptr = lastmsg, *eptr, *systype;
+	    for (eptr = ptr; *eptr; eptr++)
+		;
+	    systype = ztrduppfx(ptr, eptr-ptr);
+	    if (!strncmp(systype, "UNIX Type: L8", 13)) {
+		/*
+		 * Use binary for transfers.  This simple test saves much
+		 * hassle for all concerned, particularly me.
+		 *
+		 * We could set this based just on the UNIX part,
+		 * but I don't really know the consequences of that.
+		 */
+		zfstatusp[zfsessno] |= ZFST_IMAG;
+	    }
+	    zfsetparam("ZFTP_SYSTEM", systype, ZFPM_READONLY);
+	}
+	zfstatusp[zfsessno] |= ZFST_SYST;
+    }
+    tbuf[0] = (ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI) ? 'A' : 'I';
+    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
+
+    /*
      * Get the directory.  This is possibly an unnecessary overhead, of
      * course, but when you're being driven by shell functions there's
      * just no way of telling.
@@ -2014,6 +2321,70 @@ zftp_login(char *name, char **args, int flags)
     return zfgetcwd();
 }
 
+/*
+ * See if the server wants to tell us something.  On a timeout, we usually
+ * have a `421 Timeout' or something such waiting for us, so we read
+ * it here.  As well as being called explicitly by the user
+ * (precmd is a very good place for this, it's cheap since it has
+ * no network overhead), we call it in the bin_zftp front end if we
+ * have a connection and weren't going to call it anyway.
+ *
+ * Poll-free and select-free systems are few and far between these days,
+ * but I'm willing to consider suggestions.
+ */
+
+/**/
+static int
+zftp_test(char *name, char **args, int flags)
+{
+#if defined(HAVE_POLL) || defined(HAVE_SELECT)
+    int ret;
+# ifdef HAVE_POLL
+    struct pollfd pfd;
+# else
+    fd_set f;
+    struct timeval tv;
+# endif /* HAVE_POLL */
+
+    if (zfsess->cfd == -1)
+	return 1;
+
+# ifdef HAVE_POLL
+#  ifndef POLLIN
+    /* safety first, though I think POLLIN is more common */
+#   define POLLIN POLLNORM
+#  endif /* HAVE_POLL */
+    pfd.fd = zfsess->cfd;
+    pfd.events = POLLIN;
+    if ((ret = poll(&pfd, 1, 0)) < 0 && errno != EINTR && errno != EAGAIN)
+	zfclose(0);
+    else if (ret > 0 && pfd.revents) {
+	/* handles 421 (maybe a bit noisily?) */
+	zfgetmsg();
+    }
+# else
+    FD_ZERO(&f);
+    FD_SET(zfsess->cfd, &f);
+    tv.tv_sec = 0;
+    tv.tv_usec = 0;
+    if ((ret = select(zfsess->cfd +1, (SELECT_ARG_2_T) &f,
+		      NULL, NULL, &tv)) < 0
+	&& errno != EINTR)
+	zfclose(0);
+    else if (ret > 0) {
+	/* handles 421 */
+	zfgetmsg();
+    }
+# endif /* HAVE_POLL */
+    /* if we now have zfsess->cfd == -1, then we've just been dumped out. */
+    return (zfsess->cfd == -1) ? 2 : 0;
+#else
+    zfwarnnam(name, "not supported on this system.", NULL, 0);
+    return 3;
+#endif /* defined(HAVE_POLL) || defined(HAVE_SELECT) */
+}
+
+
 /* do ls or dir on the remote directory */
 
 /**/
@@ -2075,7 +2446,7 @@ zfgetcwd(void)
 {
     char *ptr, *eptr;
     int endc;
-    List l;
+    Eprog prog;
 
     if (zfprefs & ZFPF_DUMB)
 	return 1;
@@ -2102,9 +2473,13 @@ zfgetcwd(void)
      * front end.  By putting it here, and in close when ZFTP_PWD is unset,
      * we at least cover the bases.
      */
-    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
-	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+    if ((prog = getshfunc("zftp_chpwd")) != &dummy_eprog) {
+	int osc = sfcontext;
 
+	sfcontext = SFC_HOOK;
+	doshfunc("zftp_chpwd", prog, NULL, 0, 1);
+	sfcontext = osc;
+    }
     return 0;
 }
 
@@ -2117,14 +2492,14 @@ static int
 zfsettype(int type)
 {
     char buf[] = "TYPE X\r\n";
-    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatus))
+    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatusp[zfsessno]))
 	return 0;
     buf[5] = (ZFST_TYPE(type) == ZFST_ASCI) ? 'A' : 'I';
     if (zfsendcmd(buf) > 2)
 	return 1;
-    zfstatus &= ~(ZFST_TMSK << ZFST_TBIT);
+    zfstatusp[zfsessno] &= ~(ZFST_TMSK << ZFST_TBIT);
     /* shift the type left to set the current type bits */;
-    zfstatus |= type << ZFST_TBIT;
+    zfstatusp[zfsessno] |= type << ZFST_TBIT;
     return 0;
 }
 
@@ -2145,11 +2520,12 @@ zftp_type(char *name, char **args, int flags)
 	 * Since this is supposed to be a low-level basis for
 	 * an FTP system, just print the single code letter.
 	 */
-	printf("%c\n", (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I');
+	printf("%c\n", (ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI) ?
+	       'A' : 'I');
 	fflush(stdout);
 	return 0;
     } else {
-	nt = toupper(*str);
+	nt = toupper(STOUC(*str));
 	/*
 	 * RFC959 specifies other types, but these are the only
 	 * ones we know what to do with.
@@ -2163,8 +2539,8 @@ zftp_type(char *name, char **args, int flags)
 	    nt = 'I';
     }
 
-    zfstatus &= ~ZFST_TMSK;
-    zfstatus |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
+    zfstatusp[zfsessno] &= ~ZFST_TMSK;
+    zfstatusp[zfsessno] |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
     tbuf[0] = nt;
     zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
     return 0;
@@ -2178,11 +2554,12 @@ zftp_mode(char *name, char **args, int flags)
     int nt;
 
     if (!(str = *args)) {
-	printf("%c\n", (ZFST_MODE(zfstatus) == ZFST_STRE) ? 'S' : 'B');
+	printf("%c\n", (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_STRE) ?
+	       'S' : 'B');
 	fflush(stdout);
 	return 0;
     }
-    nt = str[0] = toupper(*str);
+    nt = str[0] = toupper(STOUC(*str));
     if (str[1] || (nt != 'S' && nt != 'B')) {
 	zwarnnam(name, "transfer mode %s not recognised", str, 0);
 	return 1;
@@ -2190,8 +2567,8 @@ zftp_mode(char *name, char **args, int flags)
     cmd[5] = (char) nt;
     if (zfsendcmd(cmd) > 2)
 	return 1;
-    zfstatus &= ZFST_MMSK;
-    zfstatus |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
+    zfstatusp[zfsessno] &= ZFST_MMSK;
+    zfstatusp[zfsessno] |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
     zfsetparam("ZFTP_MODE", ztrdup(str), ZFPM_READONLY);
     return 0;
 }
@@ -2202,7 +2579,7 @@ zftp_local(char *name, char **args, int flags)
 {
     int more = !!args[1], ret = 0, dofd = !*args;
     while (*args || dofd) {
-	long sz;
+	off_t sz;
 	char *mt;
 	int newret = zfstats(*args, !(flags & ZFTP_HERE), &sz, &mt,
 			     dofd ? 0 : -1);
@@ -2219,7 +2596,12 @@ zftp_local(char *name, char **args, int flags)
 	    fputs(*args, stdout);
 	    fputc(' ', stdout);
 	}
+#ifdef OFF_T_IS_64_BIT
+	printf("%s %s\n", output64(sz), mt);
+#else
+	DPUTS(sizeof(sz) > 4, "Shell compiled with wrong off_t size");
 	printf("%ld %s\n", sz, mt);
+#endif
 	zsfree(mt);
 	if (dofd)
 	    break;
@@ -2249,7 +2631,7 @@ zftp_getput(char *name, char **args, int flags)
 {
     int ret = 0, recv = (flags & ZFTP_RECV), getsize = 0, progress = 1;
     char *cmd = recv ? "RETR " : (flags & ZFTP_APPE) ? "APPE " : "STOR ";
-    List l;
+    Eprog prog;
 
     /*
      * At this point I'd like to set progress to 0 if we're
@@ -2260,15 +2642,15 @@ zftp_getput(char *name, char **args, int flags)
      * somewhere or in the background.  This seems to me a problem.
      */
 
-    zfsettype(ZFST_TYPE(zfstatus));
+    zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));
 
     if (recv)
 	fflush(stdout);		/* since we may be using fd 1 */
     for (; *args; args++) {
 	char *ln, *rest = NULL;
-	long startat = 0;
-	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
-	    long sz;
+	off_t startat = 0;
+	if (progress && (prog = getshfunc("zftp_progress")) != &dummy_eprog) {
+	    off_t sz;
 	    /*
 	     * This calls the SIZE command to get the size for remote
 	     * files.  Some servers send the size with the reply to
@@ -2278,7 +2660,7 @@ zftp_getput(char *name, char **args, int flags)
 	     * of zftp_progress is delayed until zfsenddata().
 	     */
 	    if ((!(zfprefs & ZFPF_DUMB) &&
-		 (zfstatus & (ZFST_NOSZ|ZFST_TRSZ)) != ZFST_TRSZ)
+		 (zfstatusp[zfsessno] & (ZFST_NOSZ|ZFST_TRSZ)) != ZFST_TRSZ)
 		|| !recv) {
 		/* the final 0 is a local fd to fstat if recv is zero */
 		zfstats(*args, recv, &sz, NULL, 0);
@@ -2296,16 +2678,27 @@ zftp_getput(char *name, char **args, int flags)
 	}
 
 	ln = tricat(cmd, *args, "\r\n");
-	/* note zdfd doesn't exist till zfgetdata() creates it */
-	if (zfgetdata(name, rest, ln, getsize) ||
-	    zfsenddata(name, recv, progress, startat))
+	/* note zfsess->dfd doesn't exist till zfgetdata() creates it */
+	if (zfgetdata(name, rest, ln, getsize))
+	    ret = 2;
+	else if (zfsenddata(name, recv, progress, startat))
 	    ret = 1;
 	zsfree(ln);
-	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	/*
+	 * The progress report isn't started till zfsenddata(), where
+	 * it's the first item.  Hence we send a final progress report
+	 * if and only if we called zfsenddata();
+	 */
+	if (progress && ret != 2 &&
+	    (prog = getshfunc("zftp_progress")) != &dummy_eprog) {
 	    /* progress to finish: ZFTP_TRANSFER set to GF or PF */
+	    int osc = sfcontext;
+
 	    zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "GF" : "PF"),
 		       ZFPM_READONLY);
-	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	    sfcontext = SFC_HOOK;
+	    doshfunc("zftp_progress", prog, NULL, 0, 1);
+	    sfcontext = osc;
 	}
 	if (rest) {
 	    zsfree(rest);
@@ -2315,7 +2708,7 @@ zftp_getput(char *name, char **args, int flags)
 	    break;
     }
     zfendtrans();
-    return ret;
+    return ret != 0;
 }
 
 /*
@@ -2393,14 +2786,22 @@ zftp_quote(char *name, char **args, int flags)
     return ret;
 }
 
-/* Close the connection, ending the session */
+/*
+ * Close the connection, ending the session.  With leaveparams,
+ * don't do anything to the external status (parameters, zftp_chpwd),
+ * probably because this isn't the current session.
+ */
 
 /**/
-static int
-zftp_close(char *name, char **args, int flags)
+static void
+zfclose(int leaveparams)
 {
     char **aptr;
-    List l;
+    Eprog prog;
+
+    if (zfsess->cfd == -1)
+	return;
+
     zfclosing = 1;
     if (zcfinish != 2) {
 	/*
@@ -2410,41 +2811,263 @@ zftp_close(char *name, char **args, int flags)
 	 */
 	zfsendcmd("QUIT\r\n");
     }
-    if (zcin)
-	fclose(zcin);
-    zcin = NULL;
-    close(zcfd);
-    zcfd = -1;
+    if (zfsess->cin) {
+	fclose(zfsess->cin);
+	zfsess->cin = NULL;
+    }
+    if (zfsess->cfd != -1) {
+	zfnopen--;
+	close(zfsess->cfd);
+	zfsess->cfd = -1;
+    }
+
+    if (zfstatfd != -1) {
+	zfstatusp[zfsessno] |= ZFST_CLOS;
+	if (!zfnopen) {
+	    /* Write the final status in case this is a subshell */
+	    lseek(zfstatfd, zfsessno*sizeof(int), 0);
+	    write(zfstatfd, zfstatusp+zfsessno, sizeof(int));
+
+	    close(zfstatfd);
+	    zfstatfd = -1;
+	}
+    }
 
-    /* Write the final status in case this is a subshell */
-    zfstatus |= ZFST_CLOS;
-    lseek(zfstatfd, 0, 0);
-    write(zfstatfd, &zfstatus, sizeof(zfstatus));
-    close(zfstatfd);
-    zfstatfd = -1;
+    if (!leaveparams) {
+	/* Unset the non-special parameters */
+	for (aptr = zfparams; *aptr; aptr++)
+	    zfunsetparam(*aptr);
 
-    /* Unset the non-special parameters */
-    for (aptr = zfparams; *aptr; aptr++)
-	zfunsetparam(*aptr);
+	/* Now ZFTP_PWD is unset.  It's up to zftp_chpwd to notice. */
+	if ((prog = getshfunc("zftp_chpwd")) != &dummy_eprog) {
+	    int osc = sfcontext;
 
-    /* Now ZFTP_PWD is unset.  It's up to zftp_chpwd to notice. */
-    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
-	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+	    sfcontext = SFC_HOOK;
+	    doshfunc("zftp_chpwd", prog, NULL, 0, 1);
+	    sfcontext = osc;
+	}
+    }
 
     /* tidy up status variables, because mess is bad */
     zfclosing = zfdrrrring = 0;
+}
+
+/* Safe front end to zftp_close() from within the package */
 
+/**/
+static int
+zftp_close(char *name, char **args, int flags)
+{
+    zfclose(0);
     return 0;
 }
 
-/* Safe front end to zftp_close() from within the package */
+
+/*
+ * Session management routines.  A session consists of various
+ * internal variables describing the connection, the set of shell
+ * parameters --- the same set which is unset by closing a connection ---
+ * and the set of host/user parameters if set by zftp params.
+ */
+
+/*
+ * Switch to a new session, creating it if necessary.
+ * Sets zfsessno, zfsess and $ZFTP_SESSION; updates zfsesscnt and zfstatusp.
+ */
+
+/**/
+static void
+newsession(char *nm)
+{
+    LinkNode nptr;
+
+    for (zfsessno = 0, nptr = firstnode(zfsessions);
+	 nptr; zfsessno++, incnode(nptr)) {
+	zfsess = (Zftp_session) nptr->dat;
+	if (!strcmp(zfsess->name, nm))
+	    break;
+    }
+
+    if (!nptr) {
+	zfsess = (Zftp_session) zcalloc(sizeof(struct zftp_session));
+	zfsess->name = ztrdup(nm);
+	zfsess->cfd = zfsess->dfd = -1;
+	zfsess->params = (char **) zcalloc(sizeof(zfparams));
+	zaddlinknode(zfsessions, zfsess);
+
+	zfsesscnt++;
+	zfstatusp = (int *)zrealloc(zfstatusp, sizeof(int)*zfsesscnt);
+	zfstatusp[zfsessno] = 0;
+    }
+
+    zfsetparam("ZFTP_SESSION", ztrdup(zfsess->name), ZFPM_READONLY);
+}
+
+/* Save the existing session: this just means saving the parameters. */
+
+static void
+savesession()
+{
+    char **ps, **pd, *val;
+
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++) {
+	if (*pd)
+	    zsfree(*pd);
+	if ((val = getsparam(*ps)))
+	    *pd = ztrdup(val);
+	else
+	    *pd = NULL;
+    }
+    *pd = NULL;
+}
+
+/*
+ * Switch to session nm, creating it if necessary.
+ * Just call newsession, then set up the session-specific parameters.
+ */
+
+/**/
+static void
+switchsession(char *nm)
+{
+    char **ps, **pd;
+
+    newsession(nm);
+
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++) {
+	if (*pd) {
+	    /* Use the permanently allocated string for the parameter */
+	    zfsetparam(*ps, *pd, ZFPM_READONLY);
+	    *pd = NULL;
+	} else
+	    zfunsetparam(*ps);
+    }
+}
 
 /**/
 static void
-zfclose(void)
+freesession(Zftp_session sptr)
 {
-    if (zcfd != -1)
-	zftp_close("zftp close", NULL, 0);
+    char **ps, **pd;
+    zsfree(sptr->name);
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++)
+	if (*pd)
+	    zsfree(*pd);
+    zfree(zfsess->params, sizeof(zfparams));
+    if (sptr->userparams)
+	freearray(sptr->userparams);
+    zfree(sptr, sizeof(struct zftp_session));
+}
+
+/**/
+static int
+zftp_session(char *name, char **args, int flags)
+{
+    if (!*args) {
+	LinkNode nptr;
+
+	for (nptr = firstnode(zfsessions); nptr; incnode(nptr))
+	    printf("%s\n", ((Zftp_session)nptr->dat)->name);
+	return 0;
+    }
+
+    /*
+     * Check if we are already in the required session: if so,
+     * it's a no-op, not an error.
+     */
+    if (!strcmp(*args, zfsess->name))
+	return 0;
+
+    savesession();
+    switchsession(*args);
+    return 0;
+}
+
+/* Remove a session and free it */
+
+/**/
+static int
+zftp_rmsession(char *name, char **args, int flags)
+{
+    int no;
+    LinkNode nptr;
+    Zftp_session sptr = NULL;
+    char *newsess = NULL;
+
+    /* Find the session in the list: either the current one, or by name */
+    for (no = 0, nptr = firstnode(zfsessions); nptr; no++, incnode(nptr)) {
+	sptr = (Zftp_session) nptr->dat;
+	if ((!*args && sptr == zfsess) ||
+	    (*args && !strcmp(sptr->name, *args)))
+	    break;
+    }
+    if (!nptr)
+	return 1;
+
+    if (sptr == zfsess) {
+	/* Freeing current session: make sure it's closed */
+	zfclosedata();
+	zfclose(0);
+
+	/*
+	 * Choose new session to switch to if any: first in list
+	 * excluding the one just freed.
+	 */
+	if (zfsesscnt > 1) {
+	    LinkNode newn = firstnode(zfsessions);
+	    if (newn == nptr)
+		incnode(newn);
+	    newsess = ((Zftp_session)newn->dat)->name;
+	}
+    } else {
+	Zftp_session oldsess = zfsess;
+	zfsess = sptr;
+	/*
+	 * Freeing another session: don't need to switch, just
+	 * tell zfclose() not to delete parameters etc.
+	 */
+	zfclosedata();
+	zfclose(1);
+	zfsess = oldsess;
+    }
+    remnode(zfsessions, nptr);
+    freesession(sptr);
+
+    /*
+     * Fix up array of status pointers.
+     */
+    if (--zfsesscnt) {
+	/*
+	 * Some remaining, so just shift up
+	 */
+	int *newstatusp = (int *)zalloc(sizeof(int)*zfsesscnt);
+	int *src, *dst, i;
+	for (i = 0, src = zfstatusp, dst = newstatusp; i < zfsesscnt;
+	     i++, src++, dst++) {
+	    if (i == no)
+		src++;
+	    *dst = *src;
+	}
+	zfree(zfstatusp, sizeof(int)*(zfsesscnt+1));
+	zfstatusp = newstatusp;
+
+	/*
+	 * Maybe we need to switch to one of the remaining sessions.
+	 */
+	if (newsess)
+	    switchsession(newsess);
+    } else {
+	zfree(zfstatusp, sizeof(int));
+	zfstatusp = NULL;
+
+	/*
+	 * We've just deleted the last session, so we need to
+	 * start again from scratch.
+	 */
+	newsession("default");
+    }
+
+    return 0;
 }
 
 /* The builtin command frontend to the rest of the package */
@@ -2453,10 +3076,10 @@ zfclose(void)
 static int
 bin_zftp(char *name, char **args, char *ops, int func)
 {
-    char fullname[11] = "zftp ";
+    char fullname[20] = "zftp ";
     char *cnam = *args++, *prefs, *ptr;
     Zftpcmd zptr;
-    int n, ret;
+    int n, ret = 0;
 
     for (zptr = zftpcmdtab; zptr->nam; zptr++)
 	if (!strcmp(zptr->nam, cnam))
@@ -2476,40 +3099,57 @@ bin_zftp(char *name, char **args, char *ops, int func)
     }
 
     strcat(fullname, cnam);
-    if (zfstatfd != -1) {
+    if (zfstatfd != -1 && !(zptr->flags & ZFTP_SESS)) {
 	/* Get the status in case it was set by a forked process */
-	int oldstatus = zfstatus;
+	int oldstatus = zfstatusp[zfsessno];
 	lseek(zfstatfd, 0, 0);
-	read(zfstatfd, &zfstatus, sizeof(zfstatus));
-	if (zcfd != -1 && (zfstatus & ZFST_CLOS)) {
+	read(zfstatfd, zfstatusp, sizeof(int)*zfsesscnt);
+	if (zfsess->cfd != -1 && (zfstatusp[zfsessno] & ZFST_CLOS)) {
 	    /* got closed in subshell without us knowing */
 	    zcfinish = 2;
-	    zfclose();
+	    zfclose(0);
 	} else {
 	    /*
 	     * fix up status types: unfortunately they may already
 	     * have been looked at between being changed in the subshell
 	     * and now, but we can't help that.
 	     */
-	    if (ZFST_TYPE(oldstatus) != ZFST_TYPE(zfstatus))
+	    if (ZFST_TYPE(oldstatus) != ZFST_TYPE(zfstatusp[zfsessno]))
 		zfsetparam("ZFTP_TYPE",
-			   ztrdup(ZFST_TYPE(zfstatus) == ZFST_ASCI ?
+			   ztrdup(ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI ?
 				  "A" : "I"), ZFPM_READONLY);
-	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatus))
+	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatusp[zfsessno]))
 		zfsetparam("ZFTP_MODE",
-			   ztrdup(ZFST_MODE(zfstatus) == ZFST_BLOC ?
+			   ztrdup(ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC ?
 				  "B" : "S"), ZFPM_READONLY);
 	}
     }
-    if ((zptr->flags & ZFTP_CONN) && zcfd == -1) {
-	zwarnnam(fullname, "not connected.", NULL, 0);
+#if defined(HAVE_SELECT) || defined (HAVE_POLL)
+    if (zfsess->cfd != -1 && !(zptr->flags & (ZFTP_TEST|ZFTP_SESS))) {
+	/*
+	 * Test the connection for a bad fd or incoming message, but
+	 * only if the connection was last heard of open, and
+	 * if we are not about to call the test command anyway.
+	 * Not worth it unless we have select() or poll().
+	 */
+	ret = zftp_test("zftp test", NULL, 0);
+    }
+#endif
+    if ((zptr->flags & ZFTP_CONN) && zfsess->cfd == -1) {
+	if (ret != 2) {
+	    /*
+	     * with ret == 2, we just got dumped out in the test,
+	     * so enough messages already.
+	     */	       
+	    zwarnnam(fullname, "not connected.", NULL, 0);
+	}
 	return 1;
     }
 
     if ((prefs = getsparam("ZFTP_PREFS"))) {
 	zfprefs = 0;
 	for (ptr = prefs; *ptr; ptr++) {
-	    switch (toupper(*ptr)) {
+	    switch (toupper(STOUC(*ptr))) {
 	    case 'S':
 		/* sendport */
 		zfprefs |= ZFPF_SNDP;
@@ -2544,12 +3184,15 @@ bin_zftp(char *name, char **args, char *ops, int func)
     if (zfdrrrring) {
 	/* had a timeout, close the connection */
 	zcfinish = 2;		/* don't try sending QUIT */
-	zfclose();
+	zfclose(0);
     }
     if (zfstatfd != -1) {
-	/* Set the status in case another process needs to know */
-	lseek(zfstatfd, 0, 0);
-	write(zfstatfd, &zfstatus, sizeof(zfstatus));
+	/*
+	 * Set the status in case another process needs to know,
+	 * but only for the active session.
+	 */
+	lseek(zfstatfd, zfsessno*sizeof(int), 0);
+	write(zfstatfd, zfstatusp+zfsessno, sizeof(int));
     }
     return ret;
 }
@@ -2558,39 +3201,64 @@ bin_zftp(char *name, char **args, char *ops, int func)
 
 /**/
 int
-boot_zftp(Module m)
+setup_(Module m)
+{
+    return 0;
+}
+
+/**/
+int
+boot_(Module m)
 {
     int ret;
     if ((ret = addbuiltins(m->nam, bintab,
 			   sizeof(bintab)/sizeof(*bintab))) == 1) {
 	/* if successful, set some default parameters */
-	long tmout_def = 60;
+	off_t tmout_def = 60;
 	zfsetparam("ZFTP_VERBOSE", ztrdup("450"), ZFPM_IFUNSET);
 	zfsetparam("ZFTP_TMOUT", &tmout_def, ZFPM_IFUNSET|ZFPM_INTEGER);
 	zfsetparam("ZFTP_PREFS", ztrdup("PS"), ZFPM_IFUNSET);
 	/* default preferences if user deletes variable */
 	zfprefs = ZFPF_SNDP|ZFPF_PASV;
+    
+	zfsessions = znewlinklist();
+	newsession("default");
     }
+
     return !ret;
 }
 
-#ifdef MODULE
-
 /**/
 int
-cleanup_zftp(Module m)
+cleanup_(Module m)
 {
     /*
      * There are various parameters hanging around, but they're
      * all non-special so are entirely non-life-threatening.
      */
-    zfclosedata();
-    zfclose();
+    LinkNode nptr;
+    Zftp_session cursess = zfsess;
+    for (zfsessno = 0, nptr = firstnode(zfsessions); nptr;
+	 zfsessno++, incnode(nptr)) {
+	zfsess = (Zftp_session)nptr->dat;
+	zfclosedata();
+	/*
+	 * When closing the current session, do the usual unsetting,
+	 * otherwise don't.
+	 */
+	zfclose(zfsess != cursess);
+    }
     zsfree(lastmsg);
-    if (zfuserparams)
-	freearray(zfuserparams);
+    zfunsetparam("ZFTP_SESSION");
+    freelinklist(zfsessions, (FreeFunc) freesession);
+    zfree(zfstatusp, sizeof(int)*zfsesscnt);
     deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
     return 0;
 }
 
-#endif
+/**/
+int
+finish_(Module m)
+{
+    return 0;
+}