summary refs log tree commit diff
diff options
context:
space:
mode:
authorLain Iwakura <lain@lainmail.xyz>2025-12-29 23:28:43 +0300
committerLain Iwakura <lain@lainmail.xyz>2025-12-29 23:28:43 +0300
commit5b209a46175d2276ef206e760f075ecbc59e8749 (patch)
treeef78bd3ba4c0a5500e88d08f4a057dcbe4284407
downloadftp-5b209a46175d2276ef206e760f075ecbc59e8749.tar.gz
ftp-5b209a46175d2276ef206e760f075ecbc59e8749.zip
feat(main): yes, works!
-rw-r--r--.gitignore2
-rw-r--r--Makefile50
-rw-r--r--cmds.c1663
-rw-r--r--cmds.h83
-rw-r--r--cmdtab.c215
-rw-r--r--compat.c330
-rw-r--r--compat.h115
-rw-r--r--complete.c381
-rw-r--r--cookie.c221
-rw-r--r--domacro.c146
-rw-r--r--extern.h152
-rw-r--r--fetch.c1789
-rw-r--r--ftp.11821
-rw-r--r--ftp.c2080
-rw-r--r--ftp_var.h241
-rw-r--r--list.c86
-rw-r--r--main.c1093
-rw-r--r--pathnames.h37
-rw-r--r--ruserpass.c317
-rw-r--r--small.c721
-rw-r--r--small.h35
-rw-r--r--stringlist.c97
-rw-r--r--stringlist.h55
-rw-r--r--tls_compat.c283
-rw-r--r--tls_compat.h60
-rw-r--r--util.c1167
26 files changed, 13240 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..02d0ba9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.o
+ftp
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3210093
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,50 @@
+CC ?= cc
+CFLAGS ?= -O2 -g
+CFLAGS += -Wall
+LDFLAGS ?=
+
+PROG = ftp
+SRCS = cmds.c cmdtab.c complete.c cookie.c domacro.c fetch.c ftp.c \
+	list.c main.c ruserpass.c small.c stringlist.c util.c compat.c
+
+ifndef NOSSL
+SRCS += tls_compat.c
+CFLAGS += -DNOSSL=0
+LDFLAGS += -lssl -lcrypto
+else
+CFLAGS += -DNOSSL=1
+endif
+
+ifndef SMALL
+CFLAGS += -DSMALL=0
+ifneq ($(shell pkg-config --exists libedit && echo yes),)
+CFLAGS += $(shell pkg-config --cflags libedit)
+LDFLAGS += $(shell pkg-config --libs libedit)
+else
+LDFLAGS += -ledit
+endif
+ifneq ($(shell uname),Darwin)
+LDFLAGS += -lcurses
+endif
+else
+CFLAGS += -DSMALL=1
+endif
+
+OBJS = $(SRCS:.c=.o)
+
+all: $(PROG)
+
+$(PROG): $(OBJS)
+	$(CC) $(LDFLAGS) -o $(PROG) $(OBJS)
+
+%.o: %.c
+	$(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+	rm -f $(OBJS) $(PROG)
+
+install: $(PROG)
+	install -d $(DESTDIR)/usr/bin
+	install -m 755 $(PROG) $(DESTDIR)/usr/bin/
+
+.PHONY: all clean install
\ No newline at end of file
diff --git a/cmds.c b/cmds.c
new file mode 100644
index 0000000..9daedea
--- /dev/null
+++ b/cmds.c
@@ -0,0 +1,1663 @@
+/*	$OpenBSD: cmds.c,v 1.85 2023/03/08 04:43:11 guenther Exp $	*/
+/*	$NetBSD: cmds.c,v 1.27 1997/08/18 10:20:15 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+/*
+ * FTP User Program -- Command Routines.
+ */
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <arpa/ftp.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fnmatch.h>
+#include <glob.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "ftp_var.h"
+#include "pathnames.h"
+#include "cmds.h"
+
+/*
+ * Set ascii transfer type.
+ */
+void
+setascii(int argc, char *argv[])
+{
+
+	stype[1] = "ascii";
+	settype(2, stype);
+}
+
+/*
+ * Set file transfer mode.
+ */
+void
+setftmode(int argc, char *argv[])
+{
+
+	fprintf(ttyout, "We only support %s mode, sorry.\n", modename);
+	code = -1;
+}
+
+/*
+ * Set file transfer format.
+ */
+void
+setform(int argc, char *argv[])
+{
+
+	fprintf(ttyout, "We only support %s format, sorry.\n", formname);
+	code = -1;
+}
+
+/*
+ * Set file transfer structure.
+ */
+void
+setstruct(int argc, char *argv[])
+{
+
+	fprintf(ttyout, "We only support %s structure, sorry.\n", structname);
+	code = -1;
+}
+
+void
+reput(int argc, char *argv[])
+{
+
+	(void)putit(argc, argv, 1);
+}
+
+void
+put(int argc, char *argv[])
+{
+
+	(void)putit(argc, argv, 0);
+}
+
+/*
+ * Send a single file.
+ */
+void
+putit(int argc, char *argv[], int restartit)
+{
+	char *cmd;
+	int loc = 0;
+	char *oldargv1, *oldargv2;
+
+	if (argc == 2) {
+		argc++;
+		argv[2] = argv[1];
+		loc++;
+	}
+	if (argc < 2 && !another(&argc, &argv, "local-file"))
+		goto usage;
+	if ((argc < 3 && !another(&argc, &argv, "remote-file")) || argc > 3) {
+usage:
+		fprintf(ttyout, "usage: %s local-file [remote-file]\n",
+		    argv[0]);
+		code = -1;
+		return;
+	}
+	oldargv1 = argv[1];
+	oldargv2 = argv[2];
+	if (!globulize(&argv[1])) {
+		code = -1;
+		return;
+	}
+	/*
+	 * If "globulize" modifies argv[1], and argv[2] is a copy of
+	 * the old argv[1], make it a copy of the new argv[1].
+	 */
+	if (argv[1] != oldargv1 && argv[2] == oldargv1) {
+		argv[2] = argv[1];
+	}
+	if (restartit == 1) {
+		if (curtype != type)
+			changetype(type, 0);
+		restart_point = remotesize(argv[2], 1);
+		if (restart_point < 0) {
+			restart_point = 0;
+			code = -1;
+			return;
+		}
+	}
+	if (strcmp(argv[0], "append") == 0) {
+		restartit = 1;
+	}
+	cmd = restartit ? "APPE" : ((sunique) ? "STOU" : "STOR");
+	if (loc && ntflag) {
+		argv[2] = dotrans(argv[2]);
+	}
+	if (loc && mapflag) {
+		argv[2] = domap(argv[2]);
+	}
+	sendrequest(cmd, argv[1], argv[2],
+	    argv[1] != oldargv1 || argv[2] != oldargv2);
+	restart_point = 0;
+	if (oldargv1 != argv[1])	/* free up after globulize() */
+		free(argv[1]);
+}
+
+/*
+ * Send multiple files.
+ */
+void
+mput(int argc, char *argv[])
+{
+	extern int optind, optreset;
+	int ch, i, restartit = 0;
+	sig_t oldintr;
+	char *cmd, *tp, *xargv[] = { argv[0], NULL, NULL };
+	const char *errstr;
+	static int depth = 0, max_depth = 0;
+
+	optind = optreset = 1;
+
+	if (depth)
+		depth++;
+
+	while ((ch = getopt(argc, argv, "cd:r")) != -1) {
+		switch(ch) {
+		case 'c':
+			restartit = 1;
+			break;
+		case 'd':
+			max_depth = strtonum(optarg, 0, INT_MAX, &errstr);
+			if (errstr != NULL) {
+				fprintf(ttyout, "bad depth value, %s: %s\n",
+				    errstr, optarg);
+				code = -1;
+				return;
+			}
+			break;
+		case 'r':
+			depth = 1;
+			break;
+		default:
+			goto usage;
+		}
+	}
+
+	if (argc - optind < 1 && !another(&argc, &argv, "local-files")) {
+usage:
+		fprintf(ttyout, "usage: %s [-cr] [-d depth] local-files\n",
+		    argv[0]);
+		code = -1;
+		return;
+	}
+
+	argv[optind - 1] = argv[0];
+	argc -= optind - 1;
+	argv += optind - 1;
+
+	mname = argv[0];
+	mflag = 1;
+
+	oldintr = signal(SIGINT, mabort);
+	(void)setjmp(jabort);
+	if (proxy) {
+		char *cp, *tp2, tmpbuf[PATH_MAX];
+
+		while ((cp = remglob(argv, 0, NULL)) != NULL) {
+			if (*cp == '\0') {
+				mflag = 0;
+				continue;
+			}
+			if (mflag && confirm(argv[0], cp)) {
+				tp = cp;
+				if (mcase) {
+					while (*tp && !islower((unsigned char)*tp)) {
+						tp++;
+					}
+					if (!*tp) {
+						tp = cp;
+						tp2 = tmpbuf;
+						while ((*tp2 = *tp) != '\0') {
+							if (isupper((unsigned char)*tp2)) {
+								*tp2 =
+								    tolower((unsigned char)*tp2);
+							}
+							tp++;
+							tp2++;
+						}
+					}
+					tp = tmpbuf;
+				}
+				if (ntflag) {
+					tp = dotrans(tp);
+				}
+				if (mapflag) {
+					tp = domap(tp);
+				}
+				if (restartit == 1) {
+					off_t ret;
+
+					if (curtype != type)
+						changetype(type, 0);
+					ret = remotesize(tp, 0);
+					restart_point = (ret < 0) ? 0 : ret;
+				}
+				cmd = restartit ? "APPE" : ((sunique) ?
+				    "STOU" : "STOR");
+				sendrequest(cmd, cp, tp,
+				    cp != tp || !interactive);
+				restart_point = 0;
+				if (!mflag && fromatty) {
+					if (confirm(argv[0], NULL))
+						mflag = 1;
+				}
+			}
+		}
+		(void)signal(SIGINT, oldintr);
+		mflag = 0;
+		return;
+	}
+
+	for (i = 1; i < argc; i++) {
+		char **cpp;
+		glob_t gl;
+		int flags;
+
+		/* Copy files without word expansion */
+		if (!doglob) {
+			if (mflag && confirm(argv[0], argv[i])) {
+				tp = (ntflag) ? dotrans(argv[i]) : argv[i];
+				tp = (mapflag) ? domap(tp) : tp;
+				if (restartit == 1) {
+					off_t ret;
+
+					if (curtype != type)
+						changetype(type, 0);
+					ret = remotesize(tp, 0);
+					restart_point = (ret < 0) ? 0 : ret;
+				}
+				cmd = restartit ? "APPE" : ((sunique) ?
+				    "STOU" : "STOR");
+				sendrequest(cmd, argv[i], tp,
+				    tp != argv[i] || !interactive);
+				restart_point = 0;
+				if (!mflag && fromatty) {
+					if (confirm(argv[0], NULL))
+						mflag = 1;
+				}
+			}
+			continue;
+		}
+
+		/* expanding file names */
+		memset(&gl, 0, sizeof(gl));
+		flags = GLOB_BRACE|GLOB_NOCHECK|GLOB_QUOTE|GLOB_TILDE;
+		if (glob(argv[i], flags, NULL, &gl) || gl.gl_pathc == 0) {
+			warnx("%s: not found", argv[i]);
+			globfree(&gl);
+			continue;
+		}
+
+		/* traverse all expanded file names */
+		for (cpp = gl.gl_pathv; cpp && *cpp != NULL; cpp++) {
+			struct stat filestat;
+
+			if (!mflag)
+				continue;
+			if (stat(*cpp, &filestat) != 0) {
+				warn("local: %s", *cpp);
+				continue;
+			}
+			if (S_ISDIR(filestat.st_mode) && depth == max_depth)
+				continue;
+			if (!confirm(argv[0], *cpp))
+				continue;
+
+			/*
+			 * If file is a directory then create a new one
+			 * at the remote machine.
+			 */
+			if (S_ISDIR(filestat.st_mode)) {
+				xargv[1] = *cpp;
+				makedir(2, xargv);
+				cd(2, xargv);
+				if (dirchange != 1) {
+					warnx("remote: %s", *cpp);
+					continue;
+				}
+
+				if (chdir(*cpp) != 0) {
+					warn("local: %s", *cpp);
+					goto out;
+				}
+
+				/* Copy the whole directory recursively. */
+				xargv[1] = "*";
+				mput(2, xargv);
+
+				if (chdir("..") != 0) {
+					mflag = 0;
+					warn("local: %s", *cpp);
+					goto out;
+				}
+
+ out:
+				xargv[1] = "..";
+				cd(2, xargv);
+				if (dirchange != 1) {
+					warnx("remote: %s", *cpp);
+					mflag = 0;
+				}
+				continue;
+			}
+
+			tp = (ntflag) ? dotrans(*cpp) : *cpp;
+			tp = (mapflag) ? domap(tp) : tp;
+			if (restartit == 1) {
+				off_t ret;
+
+				if (curtype != type)
+					changetype(type, 0);
+				ret = remotesize(tp, 0);
+				restart_point = (ret < 0) ? 0 : ret;
+			}
+			cmd = restartit ? "APPE" : ((sunique) ?
+			    "STOU" : "STOR");
+			sendrequest(cmd, *cpp, tp,
+			    *cpp != tp || !interactive);
+			restart_point = 0;
+			if (!mflag && fromatty) {
+				if (confirm(argv[0], NULL))
+					mflag = 1;
+			}
+		}
+		globfree(&gl);
+	}
+
+	(void)signal(SIGINT, oldintr);
+
+	if (depth)
+		depth--;
+	if (depth == 0 || mflag == 0)
+		depth = max_depth = mflag = 0;
+}
+
+void
+reget(int argc, char *argv[])
+{
+
+	(void)getit(argc, argv, 1, "a+w");
+}
+
+char *
+onoff(int bool)
+{
+
+	return (bool ? "on" : "off");
+}
+
+/*
+ * Show status.
+ */
+void
+status(int argc, char *argv[])
+{
+	int i;
+
+	if (connected)
+		fprintf(ttyout, "Connected %sto %s.\n",
+		    connected == -1 ? "and logged in" : "", hostname);
+	else
+		fputs("Not connected.\n", ttyout);
+	if (!proxy) {
+		pswitch(1);
+		if (connected) {
+			fprintf(ttyout, "Connected for proxy commands to %s.\n",
+			    hostname);
+		}
+		else {
+			fputs("No proxy connection.\n", ttyout);
+		}
+		pswitch(0);
+	}
+	fprintf(ttyout, "Gate ftp: %s, server %s, port %s.\n", onoff(gatemode),
+	    *gateserver ? gateserver : "(none)", gateport);
+	fprintf(ttyout, "Passive mode: %s.\n", onoff(passivemode));
+	fprintf(ttyout, "Mode: %s; Type: %s; Form: %s; Structure: %s.\n",
+		modename, typename, formname, structname);
+	fprintf(ttyout, "Verbose: %s; Bell: %s; Prompting: %s; Globbing: %s.\n",
+		onoff(verbose), onoff(bell), onoff(interactive),
+		onoff(doglob));
+	fprintf(ttyout, "Store unique: %s; Receive unique: %s.\n", onoff(sunique),
+		onoff(runique));
+	fprintf(ttyout, "Preserve modification times: %s.\n", onoff(preserve));
+	fprintf(ttyout, "Case: %s; CR stripping: %s.\n", onoff(mcase), onoff(crflag));
+	if (ntflag) {
+		fprintf(ttyout, "Ntrans: (in) %s (out) %s\n", ntin, ntout);
+	}
+	else {
+		fputs("Ntrans: off.\n", ttyout);
+	}
+	if (mapflag) {
+		fprintf(ttyout, "Nmap: (in) %s (out) %s\n", mapin, mapout);
+	}
+	else {
+		fputs("Nmap: off.\n", ttyout);
+	}
+	fprintf(ttyout, "Hash mark printing: %s; Mark count: %d; Progress bar: %s.\n",
+	    onoff(hash), mark, onoff(progress));
+	fprintf(ttyout, "Use of PORT/LPRT cmds: %s.\n", onoff(sendport));
+	fprintf(ttyout, "Use of EPSV/EPRT cmds for IPv4: %s%s.\n", onoff(epsv4),
+	    epsv4bad ? " (disabled for this connection)" : "");
+	fprintf(ttyout, "Command line editing: %s.\n", onoff(editing));
+	if (macnum > 0) {
+		fputs("Macros:\n", ttyout);
+		for (i=0; i<macnum; i++) {
+			fprintf(ttyout, "\t%s\n", macros[i].mac_name);
+		}
+	}
+	code = 0;
+}
+
+/*
+ * Toggle a variable
+ */
+int
+togglevar(int argc, char *argv[], int *var, const char *mesg)
+{
+	if (argc < 2) {
+		*var = !*var;
+	} else if (argc == 2 && strcasecmp(argv[1], "on") == 0) {
+		*var = 1;
+	} else if (argc == 2 && strcasecmp(argv[1], "off") == 0) {
+		*var = 0;
+	} else {
+		fprintf(ttyout, "usage: %s [on | off]\n", argv[0]);
+		return (-1);
+	}
+	if (mesg)
+		fprintf(ttyout, "%s %s.\n", mesg, onoff(*var));
+	return (*var);
+}
+
+/*
+ * Set beep on cmd completed mode.
+ */
+void
+setbell(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &bell, "Bell mode");
+}
+
+/*
+ * Set command line editing
+ */
+void
+setedit(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &editing, "Editing mode");
+	controlediting();
+}
+
+/*
+ * Toggle use of IPv4 EPSV/EPRT
+ */
+void
+setepsv4(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &epsv4, "EPSV/EPRT on IPv4");
+	epsv4bad = 0;
+}
+
+/*
+ * Turn on packet tracing.
+ */
+void
+settrace(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &trace, "Packet tracing");
+}
+
+/*
+ * Toggle hash mark printing during transfers, or set hash mark bytecount.
+ */
+void
+sethash(int argc, char *argv[])
+{
+	if (argc == 1)
+		hash = !hash;
+	else if (argc != 2) {
+		fprintf(ttyout, "usage: %s [on | off | size]\n", argv[0]);
+		code = -1;
+		return;
+	} else if (strcasecmp(argv[1], "on") == 0)
+		hash = 1;
+	else if (strcasecmp(argv[1], "off") == 0)
+		hash = 0;
+	else {
+		int nmark;
+		const char *errstr;
+
+		nmark = strtonum(argv[1], 1, INT_MAX, &errstr);
+		if (errstr) {
+			fprintf(ttyout, "bytecount value is %s: %s\n",
+			    errstr, argv[1]);
+			code = -1;
+			return;
+		}
+		mark = nmark;
+		hash = 1;
+	}
+	fprintf(ttyout, "Hash mark printing %s", onoff(hash));
+	if (hash)
+		fprintf(ttyout, " (%d bytes/hash mark)", mark);
+	fputs(".\n", ttyout);
+	code = hash;
+}
+
+/*
+ * Turn on printing of server echo's.
+ */
+void
+setverbose(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &verbose, "Verbose mode");
+}
+
+/*
+ * Toggle PORT/LPRT cmd use before each data connection.
+ */
+void
+setport(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &sendport, "Use of PORT/LPRT cmds");
+}
+
+/*
+ * Toggle transfer progress bar.
+ */
+void
+setprogress(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &progress, "Progress bar");
+}
+
+/*
+ * Turn on interactive prompting during mget, mput, and mdelete.
+ */
+void
+setprompt(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &interactive, "Interactive mode");
+}
+
+/*
+ * Toggle gate-ftp mode, or set gate-ftp server
+ */
+void
+setgate(int argc, char *argv[])
+{
+	static char gsbuf[HOST_NAME_MAX+1];
+
+	if (argc > 3) {
+		fprintf(ttyout, "usage: %s [on | off | host [port]]\n",
+		    argv[0]);
+		code = -1;
+		return;
+	} else if (argc < 2) {
+		gatemode = !gatemode;
+	} else {
+		if (argc == 2 && strcasecmp(argv[1], "on") == 0)
+			gatemode = 1;
+		else if (argc == 2 && strcasecmp(argv[1], "off") == 0)
+			gatemode = 0;
+		else {
+			if (argc == 3) {
+				gateport = strdup(argv[2]);
+				if (gateport == NULL)
+					err(1, NULL);
+			}
+			strlcpy(gsbuf, argv[1], sizeof(gsbuf));
+			gateserver = gsbuf;
+			gatemode = 1;
+		}
+	}
+	if (gatemode && (gateserver == NULL || *gateserver == '\0')) {
+		fprintf(ttyout,
+		    "Disabling gate-ftp mode - no gate-ftp server defined.\n");
+		gatemode = 0;
+	} else {
+		fprintf(ttyout, "Gate ftp: %s, server %s, port %s.\n",
+		    onoff(gatemode),
+		    *gateserver ? gateserver : "(none)", gateport);
+	}
+	code = gatemode;
+}
+
+/*
+ * Toggle metacharacter interpretation on local file names.
+ */
+void
+setglob(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &doglob, "Globbing");
+}
+
+/*
+ * Toggle preserving modification times on retrieved files.
+ */
+void
+setpreserve(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &preserve, "Preserve modification times");
+}
+
+/*
+ * Set debugging mode on/off and/or set level of debugging.
+ */
+void
+setdebug(int argc, char *argv[])
+{
+	if (argc > 2) {
+		fprintf(ttyout, "usage: %s [on | off | debuglevel]\n", argv[0]);
+		code = -1;
+		return;
+	} else if (argc == 2) {
+		if (strcasecmp(argv[1], "on") == 0)
+			debug = 1;
+		else if (strcasecmp(argv[1], "off") == 0)
+			debug = 0;
+		else {
+			const char *errstr;
+			int val;
+
+			val = strtonum(argv[1], 0, INT_MAX, &errstr);
+			if (errstr) {
+				fprintf(ttyout, "debugging value is %s: %s\n",
+				    errstr, argv[1]);
+				code = -1;
+				return;
+			}
+			debug = val;
+		}
+	} else
+		debug = !debug;
+	if (debug)
+		options |= SO_DEBUG;
+	else
+		options &= ~SO_DEBUG;
+	fprintf(ttyout, "Debugging %s (debug=%d).\n", onoff(debug), debug);
+	code = debug > 0;
+}
+
+/*
+ * Set current working directory on local machine.
+ */
+void
+lcd(int argc, char *argv[])
+{
+	char buf[PATH_MAX];
+	char *oldargv1;
+
+	if (argc < 2)
+		argc++, argv[1] = home;
+	if (argc != 2) {
+		fprintf(ttyout, "usage: %s [local-directory]\n", argv[0]);
+		code = -1;
+		return;
+	}
+	oldargv1 = argv[1];
+	if (!globulize(&argv[1])) {
+		code = -1;
+		return;
+	}
+	if (chdir(argv[1]) == -1) {
+		warn("local: %s", argv[1]);
+		code = -1;
+	} else {
+		if (getcwd(buf, sizeof(buf)) != NULL)
+			fprintf(ttyout, "Local directory now %s\n", buf);
+		else
+			warn("getcwd: %s", argv[1]);
+		code = 0;
+	}
+	if (oldargv1 != argv[1])	/* free up after globulize() */
+		free(argv[1]);
+}
+
+/*
+ * Delete a single file.
+ */
+void
+deletecmd(int argc, char *argv[])
+{
+
+	if ((argc < 2 && !another(&argc, &argv, "remote-file")) || argc > 2) {
+		fprintf(ttyout, "usage: %s remote-file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	(void)command("DELE %s", argv[1]);
+}
+
+/*
+ * Delete multiple files.
+ */
+void
+mdelete(int argc, char *argv[])
+{
+	sig_t oldintr;
+	char *cp;
+
+	if (argc < 2 && !another(&argc, &argv, "remote-files")) {
+		fprintf(ttyout, "usage: %s remote-files\n", argv[0]);
+		code = -1;
+		return;
+	}
+	mname = argv[0];
+	mflag = 1;
+	oldintr = signal(SIGINT, mabort);
+	(void)setjmp(jabort);
+	while ((cp = remglob(argv, 0, NULL)) != NULL) {
+		if (*cp == '\0') {
+			mflag = 0;
+			continue;
+		}
+		if (mflag && confirm(argv[0], cp)) {
+			(void)command("DELE %s", cp);
+			if (!mflag && fromatty) {
+				if (confirm(argv[0], NULL))
+					mflag = 1;
+			}
+		}
+	}
+	(void)signal(SIGINT, oldintr);
+	mflag = 0;
+}
+
+/*
+ * Rename a remote file.
+ */
+void
+renamefile(int argc, char *argv[])
+{
+
+	if (argc < 2 && !another(&argc, &argv, "from-name"))
+		goto usage;
+	if ((argc < 3 && !another(&argc, &argv, "to-name")) || argc > 3) {
+usage:
+		fprintf(ttyout, "usage: %s from-name to-name\n", argv[0]);
+		code = -1;
+		return;
+	}
+	if (command("RNFR %s", argv[1]) == CONTINUE)
+		(void)command("RNTO %s", argv[2]);
+}
+
+/*
+ * Get a directory listing of remote files.
+ */
+void
+ls(int argc, char *argv[])
+{
+	const char *cmd;
+	char *oldargv2, *globargv2;
+
+	if (argc < 2)
+		argc++, argv[1] = NULL;
+	if (argc < 3)
+		argc++, argv[2] = "-";
+	if (argc > 3) {
+		fprintf(ttyout, "usage: %s [remote-directory [local-file]]\n",
+		    argv[0]);
+		code = -1;
+		return;
+	}
+	cmd = strcmp(argv[0], "nlist") == 0 ? "NLST" : "LIST";
+	oldargv2 = argv[2];
+	if (strcmp(argv[2], "-") && !globulize(&argv[2])) {
+		code = -1;
+		return;
+	}
+	globargv2 = argv[2];
+	if (strcmp(argv[2], "-") && *argv[2] != '|' && (!globulize(&argv[2]) ||
+	    !confirm("output to local-file:", argv[2]))) {
+		code = -1;
+		goto freels;
+	}
+	recvrequest(cmd, argv[2], argv[1], "w", 0, 0);
+
+	/* flush results in case commands are coming from a pipe */
+	fflush(ttyout);
+freels:
+	if (argv[2] != globargv2)		/* free up after globulize() */
+		free(argv[2]);
+	if (globargv2 != oldargv2)
+		free(globargv2);
+}
+
+/*
+ * Get a directory listing of multiple remote files.
+ */
+void
+mls(int argc, char *argv[])
+{
+	sig_t oldintr;
+	int i;
+	char lmode[1], *dest, *odest;
+
+	if (argc < 2 && !another(&argc, &argv, "remote-files"))
+		goto usage;
+	if (argc < 3 && !another(&argc, &argv, "local-file")) {
+usage:
+		fprintf(ttyout, "usage: %s remote-files local-file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	odest = dest = argv[argc - 1];
+	argv[argc - 1] = NULL;
+	if (strcmp(dest, "-") && *dest != '|')
+		if (!globulize(&dest) ||
+		    !confirm("output to local-file:", dest)) {
+			code = -1;
+			return;
+	}
+	mname = argv[0];
+	mflag = 1;
+	oldintr = signal(SIGINT, mabort);
+	(void)setjmp(jabort);
+	for (i = 1; mflag && i < argc-1; ++i) {
+		*lmode = (i == 1) ? 'w' : 'a';
+		recvrequest("LIST", dest, argv[i], lmode, 0, 0);
+		if (!mflag && fromatty) {
+			if (confirm(argv[0], NULL))
+				mflag ++;
+		}
+	}
+	(void)signal(SIGINT, oldintr);
+	mflag = 0;
+	if (dest != odest)			/* free up after globulize() */
+		free(dest);
+}
+
+/*
+ * Do a shell escape
+ */
+void
+shell(int argc, char *argv[])
+{
+	pid_t pid;
+	sig_t old1, old2;
+	char shellnam[PATH_MAX], *shellp, *namep;
+	int wait_status;
+
+	old1 = signal (SIGINT, SIG_IGN);
+	old2 = signal (SIGQUIT, SIG_IGN);
+	if ((pid = fork()) == 0) {
+		(void)closefrom(3);
+		(void)signal(SIGINT, SIG_DFL);
+		(void)signal(SIGQUIT, SIG_DFL);
+		shellp = getenv("SHELL");
+		if (shellp == NULL || *shellp == '\0')
+			shellp = _PATH_BSHELL;
+		namep = strrchr(shellp, '/');
+		if (namep == NULL)
+			namep = shellp;
+		shellnam[0] = '-';
+		(void)strlcpy(shellnam + 1, ++namep, sizeof(shellnam) - 1);
+		if (strcmp(namep, "sh") != 0)
+			shellnam[0] = '+';
+		if (debug) {
+			fputs(shellp, ttyout);
+			fputc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (argc > 1) {
+			execl(shellp, shellnam, "-c", altarg, (char *)NULL);
+		}
+		else {
+			execl(shellp, shellnam, (char *)NULL);
+		}
+		warn("%s", shellp);
+		code = -1;
+		exit(1);
+	}
+	if (pid > 0)
+		while (wait(&wait_status) != pid)
+			;
+	(void)signal(SIGINT, old1);
+	(void)signal(SIGQUIT, old2);
+	if (pid == -1) {
+		warn("Try again later");
+		code = -1;
+	}
+	else {
+		code = 0;
+	}
+}
+
+/*
+ * Send new user information (re-login)
+ */
+void
+user(int argc, char *argv[])
+{
+	char acctname[80];
+	int n, aflag = 0;
+
+	if (argc < 2)
+		(void)another(&argc, &argv, "username");
+	if (argc < 2 || argc > 4) {
+		fprintf(ttyout, "usage: %s username [password [account]]\n",
+		    argv[0]);
+		code = -1;
+		return;
+	}
+	n = command("USER %s", argv[1]);
+	if (n == CONTINUE) {
+		if (argc < 3 )
+			argv[2] = getpass("Password:"), argc++;
+		n = command("PASS %s", argv[2]);
+	}
+	if (n == CONTINUE) {
+		if (argc < 4) {
+			(void)fputs("Account: ", ttyout);
+			(void)fflush(ttyout);
+			if (fgets(acctname, sizeof(acctname), stdin) == NULL) {
+				clearerr(stdin);
+				goto fail;
+			}
+
+			acctname[strcspn(acctname, "\n")] = '\0';
+
+			argv[3] = acctname;
+			argc++;
+		}
+		n = command("ACCT %s", argv[3]);
+		aflag++;
+	}
+	if (n != COMPLETE) {
+ fail:
+		fputs("Login failed.\n", ttyout);
+		return;
+	}
+	if (!aflag && argc == 4) {
+		(void)command("ACCT %s", argv[3]);
+	}
+	connected = -1;
+}
+
+/*
+ * Print working directory on remote machine.
+ */
+void
+pwd(int argc, char *argv[])
+{
+	int oldverbose = verbose;
+
+	/*
+	 * If we aren't verbose, this doesn't do anything!
+	 */
+	verbose = 1;
+	if (command("PWD") == ERROR && code == 500) {
+		fputs("PWD command not recognized, trying XPWD.\n", ttyout);
+		(void)command("XPWD");
+	}
+	verbose = oldverbose;
+}
+
+/*
+ * Print working directory on local machine.
+ */
+void
+lpwd(int argc, char *argv[])
+{
+	char buf[PATH_MAX];
+
+	if (getcwd(buf, sizeof(buf)) != NULL)
+		fprintf(ttyout, "Local directory %s\n", buf);
+	else
+		warn("getcwd");
+	code = 0;
+}
+
+/*
+ * Make a directory.
+ */
+void
+makedir(int argc, char *argv[])
+{
+
+	if ((argc < 2 && !another(&argc, &argv, "directory-name")) ||
+	    argc > 2) {
+		fprintf(ttyout, "usage: %s directory-name\n", argv[0]);
+		code = -1;
+		return;
+	}
+	if (command("MKD %s", argv[1]) == ERROR && code == 500) {
+		if (verbose)
+			fputs("MKD command not recognized, trying XMKD.\n", ttyout);
+		(void)command("XMKD %s", argv[1]);
+	}
+}
+
+/*
+ * Remove a directory.
+ */
+void
+removedir(int argc, char *argv[])
+{
+
+	if ((argc < 2 && !another(&argc, &argv, "directory-name")) ||
+	    argc > 2) {
+		fprintf(ttyout, "usage: %s directory-name\n", argv[0]);
+		code = -1;
+		return;
+	}
+	if (command("RMD %s", argv[1]) == ERROR && code == 500) {
+		if (verbose)
+			fputs("RMD command not recognized, trying XRMD.\n", ttyout);
+		(void)command("XRMD %s", argv[1]);
+	}
+}
+
+/*
+ * Send a line, verbatim, to the remote machine.
+ */
+void
+quote(int argc, char *argv[])
+{
+
+	if (argc < 2 && !another(&argc, &argv, "command line to send")) {
+		fprintf(ttyout, "usage: %s arg ...\n", argv[0]);
+		code = -1;
+		return;
+	}
+	quote1("", argc, argv);
+}
+
+/*
+ * Send a SITE command to the remote machine.  The line
+ * is sent verbatim to the remote machine, except that the
+ * word "SITE" is added at the front.
+ */
+void
+site(int argc, char *argv[])
+{
+
+	if (argc < 2 && !another(&argc, &argv, "arguments to SITE command")) {
+		fprintf(ttyout, "usage: %s arg ...\n", argv[0]);
+		code = -1;
+		return;
+	}
+	quote1("SITE", argc, argv);
+}
+
+/*
+ * Turn argv[1..argc) into a space-separated string, then prepend initial text.
+ * Send the result as a one-line command and get response.
+ */
+void
+quote1(const char *initial, int argc, char *argv[])
+{
+	int i, len;
+	char buf[BUFSIZ];		/* must be >= sizeof(line) */
+
+	(void)strlcpy(buf, initial, sizeof(buf));
+	if (argc > 1) {
+		for (i = 1, len = strlen(buf); i < argc && len < sizeof(buf)-1; i++) {
+			/* Space for next arg */
+			if (len > 1)
+				buf[len++] = ' ';
+
+			/* Sanity check */
+			if (len >= sizeof(buf) - 1)
+				break;
+
+			/* Copy next argument, NUL terminate always */
+			strlcpy(&buf[len], argv[i], sizeof(buf) - len);
+
+			/* Update string length */
+			len = strlen(buf);
+		}
+	}
+
+	/* Make double (triple?) sure the sucker is NUL terminated */
+	buf[sizeof(buf) - 1] = '\0';
+
+	if (command("%s", buf) == PRELIM) {
+		while (getreply(0) == PRELIM)
+			continue;
+	}
+}
+
+void
+do_chmod(int argc, char *argv[])
+{
+
+	if (argc < 2 && !another(&argc, &argv, "mode"))
+		goto usage;
+	if ((argc < 3 && !another(&argc, &argv, "file")) || argc > 3) {
+usage:
+		fprintf(ttyout, "usage: %s mode file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	(void)command("SITE CHMOD %s %s", argv[1], argv[2]);
+}
+
+void
+do_umask(int argc, char *argv[])
+{
+	int oldverbose = verbose;
+
+	verbose = 1;
+	(void)command(argc == 1 ? "SITE UMASK" : "SITE UMASK %s", argv[1]);
+	verbose = oldverbose;
+}
+
+void
+idle(int argc, char *argv[])
+{
+	int oldverbose = verbose;
+
+	verbose = 1;
+	(void)command(argc == 1 ? "SITE IDLE" : "SITE IDLE %s", argv[1]);
+	verbose = oldverbose;
+}
+
+/*
+ * Ask the other side for help.
+ */
+void
+rmthelp(int argc, char *argv[])
+{
+	int oldverbose = verbose;
+
+	verbose = 1;
+	(void)command(argc == 1 ? "HELP" : "HELP %s", argv[1]);
+	verbose = oldverbose;
+}
+
+/*
+ * Terminate session and exit.
+ */
+void
+quit(int argc, char *argv[])
+{
+
+	if (connected)
+		disconnect(0, 0);
+	pswitch(1);
+	if (connected) {
+		disconnect(0, 0);
+	}
+	exit(0);
+}
+
+void
+account(int argc, char *argv[])
+{
+	char *ap;
+
+	if (argc > 2) {
+		fprintf(ttyout, "usage: %s [password]\n", argv[0]);
+		code = -1;
+		return;
+	}
+	else if (argc == 2)
+		ap = argv[1];
+	else
+		ap = getpass("Account:");
+	(void)command("ACCT %s", ap);
+}
+
+jmp_buf abortprox;
+
+void
+proxabort(int signo)
+{
+	int save_errno = errno;
+
+	alarmtimer(0);
+	if (!proxy) {
+		pswitch(1);
+	}
+	if (connected) {
+		proxflag = 1;
+	}
+	else {
+		proxflag = 0;
+	}
+	pswitch(0);
+	errno = save_errno;
+	longjmp(abortprox, 1);
+}
+
+void
+doproxy(int argc, char *argv[])
+{
+	struct cmd *c;
+	int cmdpos;
+	sig_t oldintr;
+
+	if (argc < 2 && !another(&argc, &argv, "command")) {
+		fprintf(ttyout, "usage: %s command\n", argv[0]);
+		code = -1;
+		return;
+	}
+	c = getcmd(argv[1]);
+	if (c == (struct cmd *) -1) {
+		fputs("?Ambiguous command.\n", ttyout);
+		(void)fflush(ttyout);
+		code = -1;
+		return;
+	}
+	if (c == 0) {
+		fputs("?Invalid command.\n", ttyout);
+		(void)fflush(ttyout);
+		code = -1;
+		return;
+	}
+	if (!c->c_proxy) {
+		fputs("?Invalid proxy command.\n", ttyout);
+		(void)fflush(ttyout);
+		code = -1;
+		return;
+	}
+	if (setjmp(abortprox)) {
+		code = -1;
+		return;
+	}
+	oldintr = signal(SIGINT, proxabort);
+	pswitch(1);
+	if (c->c_conn && !connected) {
+		fputs("Not connected.\n", ttyout);
+		(void)fflush(ttyout);
+		pswitch(0);
+		(void)signal(SIGINT, oldintr);
+		code = -1;
+		return;
+	}
+	cmdpos = strcspn(line, " \t");
+	if (cmdpos > 0)		/* remove leading "proxy " from input buffer */
+		memmove(line, line + cmdpos + 1, strlen(line) - cmdpos + 1);
+	(*c->c_handler)(argc-1, argv+1);
+	if (connected) {
+		proxflag = 1;
+	}
+	else {
+		proxflag = 0;
+	}
+	pswitch(0);
+	(void)signal(SIGINT, oldintr);
+}
+
+void
+setcase(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &mcase, "Case mapping");
+}
+
+void
+setcr(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &crflag, "Carriage Return stripping");
+}
+
+void
+setntrans(int argc, char *argv[])
+{
+	if (argc == 1) {
+		ntflag = 0;
+		fputs("Ntrans off.\n", ttyout);
+		code = ntflag;
+		return;
+	}
+	ntflag++;
+	code = ntflag;
+	(void)strlcpy(ntin, argv[1], sizeof(ntin));
+	if (argc == 2) {
+		ntout[0] = '\0';
+		return;
+	}
+	(void)strlcpy(ntout, argv[2], sizeof(ntout));
+}
+
+void
+setnmap(int argc, char *argv[])
+{
+	char *cp;
+
+	if (argc == 1) {
+		mapflag = 0;
+		fputs("Nmap off.\n", ttyout);
+		code = mapflag;
+		return;
+	}
+	if ((argc < 3 && !another(&argc, &argv, "outpattern")) || argc > 3) {
+		fprintf(ttyout, "usage: %s [inpattern outpattern]\n", argv[0]);
+		code = -1;
+		return;
+	}
+	mapflag = 1;
+	code = 1;
+	cp = strchr(altarg, ' ');
+	if (proxy) {
+		while(*++cp == ' ')
+			continue;
+		altarg = cp;
+		cp = strchr(altarg, ' ');
+	}
+	*cp = '\0';
+	(void)strncpy(mapin, altarg, PATH_MAX - 1);
+	while (*++cp == ' ')
+		continue;
+	(void)strncpy(mapout, cp, PATH_MAX - 1);
+}
+
+void
+setpassive(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &passivemode,
+	    verbose ? "Passive mode" : NULL);
+}
+
+void
+setsunique(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &sunique, "Store unique");
+}
+
+void
+setrunique(int argc, char *argv[])
+{
+
+	code = togglevar(argc, argv, &runique, "Receive unique");
+}
+
+/* change directory to parent directory */
+void
+cdup(int argc, char *argv[])
+{
+	int r;
+
+	r = command("CDUP");
+	if (r == ERROR && code == 500) {
+		if (verbose)
+			fputs("CDUP command not recognized, trying XCUP.\n", ttyout);
+		r = command("XCUP");
+	}
+	if (r == COMPLETE)
+		dirchange = 1;
+}
+
+/*
+ * Restart transfer at specific point
+ */
+void
+restart(int argc, char *argv[])
+{
+	off_t nrestart_point;
+	char *ep;
+
+	if (argc != 2)
+		fputs("restart: offset not specified.\n", ttyout);
+	else {
+		nrestart_point = strtoll(argv[1], &ep, 10);
+		if (nrestart_point == LLONG_MAX || *ep != '\0')
+			fputs("restart: invalid offset.\n", ttyout);
+		else {
+			fprintf(ttyout, "Restarting at %lld. Execute get, put "
+				"or append to initiate transfer\n",
+				(long long)nrestart_point);
+			restart_point = nrestart_point;
+		}
+	}
+}
+
+/*
+ * Show remote system type
+ */
+void
+syst(int argc, char *argv[])
+{
+
+	(void)command("SYST");
+}
+
+void
+macdef(int argc, char *argv[])
+{
+	char *tmp;
+	int c;
+
+	if (macnum == 16) {
+		fputs("Limit of 16 macros have already been defined.\n", ttyout);
+		code = -1;
+		return;
+	}
+	if ((argc < 2 && !another(&argc, &argv, "macro-name")) || argc > 2) {
+		fprintf(ttyout, "usage: %s macro-name\n", argv[0]);
+		code = -1;
+		return;
+	}
+	if (interactive)
+		fputs(
+"Enter macro line by line, terminating it with a null line.\n", ttyout);
+	(void)strlcpy(macros[macnum].mac_name, argv[1],
+	    sizeof(macros[macnum].mac_name));
+	if (macnum == 0)
+		macros[macnum].mac_start = macbuf;
+	else
+		macros[macnum].mac_start = macros[macnum - 1].mac_end + 1;
+	tmp = macros[macnum].mac_start;
+	while (tmp != macbuf+4096) {
+		if ((c = getchar()) == EOF) {
+			fputs("macdef: end of file encountered.\n", ttyout);
+			code = -1;
+			return;
+		}
+		if ((*tmp = c) == '\n') {
+			if (tmp == macros[macnum].mac_start) {
+				macros[macnum++].mac_end = tmp;
+				code = 0;
+				return;
+			}
+			if (*(tmp-1) == '\0') {
+				macros[macnum++].mac_end = tmp - 1;
+				code = 0;
+				return;
+			}
+			*tmp = '\0';
+		}
+		tmp++;
+	}
+	while (1) {
+		while ((c = getchar()) != '\n' && c != EOF)
+			/* LOOP */;
+		if (c == EOF || getchar() == '\n') {
+			fputs("Macro not defined - 4K buffer exceeded.\n", ttyout);
+			code = -1;
+			return;
+		}
+	}
+}
+
+/*
+ * Get size of file on remote machine
+ */
+void
+sizecmd(int argc, char *argv[])
+{
+	off_t size;
+
+	if ((argc < 2 && !another(&argc, &argv, "file")) || argc > 2) {
+		fprintf(ttyout, "usage: %s file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	size = remotesize(argv[1], 1);
+	if (size != -1)
+		fprintf(ttyout, "%s\t%lld\n", argv[1], (long long)size);
+	code = size;
+}
+
+/*
+ * Get last modification time of file on remote machine
+ */
+void
+modtime(int argc, char *argv[])
+{
+	time_t mtime;
+
+	if ((argc < 2 && !another(&argc, &argv, "file")) || argc > 2) {
+		fprintf(ttyout, "usage: %s file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	mtime = remotemodtime(argv[1], 1);
+	if (mtime != -1)
+		fprintf(ttyout, "%s\t%s", argv[1], asctime(localtime(&mtime)));
+	code = mtime;
+}
+
+/*
+ * Show status on remote machine
+ */
+void
+rmtstatus(int argc, char *argv[])
+{
+
+	(void)command(argc > 1 ? "STAT %s" : "STAT" , argv[1]);
+}
+
+/*
+ * Get file if modtime is more recent than current file
+ */
+void
+newer(int argc, char *argv[])
+{
+
+	(void)getit(argc, argv, -1, "w");
+}
+
+/*
+ * Display one file through $PAGER (defaults to "more").
+ */
+void
+page(int argc, char *argv[])
+{
+	off_t orestart_point;
+	int ohash, overbose;
+	char *p, *pager, *oldargv1;
+
+	if ((argc < 2 && !another(&argc, &argv, "file")) || argc > 2) {
+		fprintf(ttyout, "usage: %s file\n", argv[0]);
+		code = -1;
+		return;
+	}
+	oldargv1 = argv[1];
+	if (!globulize(&argv[1])) {
+		code = -1;
+		return;
+	}
+	p = getenv("PAGER");
+	if (p == NULL || (*p == '\0'))
+		p = PAGER;
+	if (asprintf(&pager, "|%s", p) == -1)
+		errx(1, "Can't allocate memory for $PAGER");
+
+	orestart_point = restart_point;
+	ohash = hash;
+	overbose = verbose;
+	restart_point = hash = verbose = 0;
+	recvrequest("RETR", pager, argv[1], "r+w", 1, 0);
+	(void)free(pager);
+	restart_point = orestart_point;
+	hash = ohash;
+	verbose = overbose;
+	if (oldargv1 != argv[1])	/* free up after globulize() */
+		free(argv[1]);
+}
+
+#endif /* !SMALL */
+
diff --git a/cmds.h b/cmds.h
new file mode 100644
index 0000000..ca01845
--- /dev/null
+++ b/cmds.h
@@ -0,0 +1,83 @@
+/*	$OpenBSD: cmds.h,v 1.4 2019/05/16 12:44:17 florian Exp $	*/
+
+/*
+ * Copyright (c) 2009 Martynas Venckus <martynas@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+void	setascii(int, char **);
+void	setftmode(int, char **);
+void	setform(int, char **);
+void	setstruct(int, char **);
+void	reput(int, char **);
+void	put(int, char **);
+void	putit(int, char **, int);
+void	mput(int, char **);
+void	reget(int, char **);
+char   *onoff(int);
+void	status(int, char **);
+int	togglevar(int, char **, int *, const char *);
+void	setbell(int, char **);
+void	setedit(int, char **);
+void	setepsv4(int, char **);
+void	settrace(int, char **);
+void	sethash(int, char **);
+void	setverbose(int, char **);
+void	setport(int, char **);
+void	setprogress(int, char **);
+void	setprompt(int, char **);
+void	setgate(int, char **);
+void	setglob(int, char **);
+void	setpreserve(int, char **);
+void	setdebug(int, char **);
+void	lcd(int, char **);
+void	deletecmd(int, char **);
+void	mdelete(int, char **);
+void	renamefile(int, char **);
+void	ls(int, char **);
+void	mls(int, char **);
+void	shell(int, char **);
+void	user(int, char **);
+void	pwd(int, char **);
+void	lpwd(int, char **);
+void	makedir(int, char **);
+void	removedir(int, char **);
+void	quote(int, char **);
+void	site(int, char **);
+void	quote1(const char *, int, char **);
+void	do_chmod(int, char **);
+void	do_umask(int, char **);
+void	idle(int, char **);
+void	rmthelp(int, char **);
+void	quit(int, char **);
+void	account(int, char **);
+void	proxabort(int);
+void	doproxy(int, char **);
+void	setcase(int, char **);
+void	setcr(int, char **);
+void	setntrans(int, char **);
+void	setnmap(int, char **);
+void	setpassive(int, char **);
+void	setsunique(int, char **);
+void	setrunique(int, char **);
+void	cdup(int, char **);
+void	restart(int, char **);
+void	syst(int, char **);
+void	macdef(int, char **);
+void	sizecmd(int, char **);
+void	modtime(int, char **);
+void	rmtstatus(int, char **);
+void	newer(int, char **);
+void	page(int, char **);
+
diff --git a/cmdtab.c b/cmdtab.c
new file mode 100644
index 0000000..5b871dc
--- /dev/null
+++ b/cmdtab.c
@@ -0,0 +1,215 @@
+/*	$OpenBSD: cmdtab.c,v 1.31 2019/05/16 12:44:17 florian Exp $	*/
+/*	$NetBSD: cmdtab.c,v 1.17 1997/08/18 10:20:17 lukem Exp $	*/
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+#include <stdio.h>
+#include "ftp_var.h"
+#include "cmds.h"
+
+/*
+ * User FTP -- Command Tables.
+ */
+
+char	accounthelp[] =	"send account command to remote server";
+char	appendhelp[] =	"append to a file";
+char	asciihelp[] =	"set ascii transfer type";
+char	beephelp[] =	"beep when command completed";
+char	binaryhelp[] =	"set binary transfer type";
+char	casehelp[] =	"toggle mget upper/lower case id mapping";
+char	cdhelp[] =	"change remote working directory";
+char	cduphelp[] =	"change remote working directory to parent directory";
+char	chmodhelp[] =	"change file permissions of remote file";
+char	connecthelp[] =	"connect to remote ftp server";
+char	crhelp[] =	"toggle carriage return stripping on ascii gets";
+char	debughelp[] =	"toggle/set debugging mode";
+char	deletehelp[] =	"delete remote file";
+char	dirhelp[] =	"list contents of remote directory";
+char	disconhelp[] =	"terminate ftp session";
+char	domachelp[] =	"execute macro";
+char	edithelp[] =	"toggle command line editing";
+char	epsv4help[] =	"toggle use of EPSV/EPRT on IPv4 ftp";
+char	formhelp[] =	"set file transfer format";
+char	gatehelp[] =	"toggle gate-ftp; specify host[:port] to change proxy";
+char	globhelp[] =	"toggle metacharacter expansion of local file names";
+char	hashhelp[] =	"toggle printing `#' marks; specify number to set size";
+char	helphelp[] =	"print local help information";
+char	idlehelp[] =	"get (set) idle timer on remote side";
+char	lcdhelp[] =	"change local working directory";
+char	lpwdhelp[] =	"print local working directory";
+char	lshelp[] =	"list contents of remote directory";
+char	macdefhelp[] =  "define a macro";
+char	mdeletehelp[] =	"delete multiple files";
+char	mdirhelp[] =	"list contents of multiple remote directories";
+char	mgethelp[] =	"get multiple files";
+char	mkdirhelp[] =	"make directory on the remote machine";
+char	mlshelp[] =	"list contents of multiple remote directories";
+char	modehelp[] =	"set file transfer mode";
+char	modtimehelp[] = "show last modification time of remote file";
+char	mputhelp[] =	"send multiple files";
+char	newerhelp[] =	"get file if remote file is newer than local file ";
+char	nlisthelp[] =	"nlist contents of remote directory";
+char	nmaphelp[] =	"set templates for default file name mapping";
+char	ntranshelp[] =	"set translation table for default file name mapping";
+char	pagehelp[] =	"view a remote file through your pager";
+char	passivehelp[] =	"toggle passive transfer mode";
+char	porthelp[] =	"toggle use of PORT/LPRT cmd for each data connection";
+char	preservehelp[] ="toggle preservation of modification time of "
+			"retrieved files";
+char	progresshelp[] ="toggle transfer progress meter";
+char	prompthelp[] =	"toggle interactive prompting on multiple commands";
+char	proxyhelp[] =	"issue command on alternate connection";
+char	pwdhelp[] =	"print working directory on remote machine";
+char	quithelp[] =	"terminate ftp session and exit";
+char	quotehelp[] =	"send arbitrary ftp command";
+char	receivehelp[] =	"receive file";
+char	regethelp[] =	"get file restarting at end of local file";
+char	reputhelp[] =	"put file restarting at end of remote file";
+char	remotehelp[] =	"get help from remote server";
+char	renamehelp[] =	"rename file";
+char	resethelp[] =	"clear queued command replies";
+char	restarthelp[]=	"restart file transfer at bytecount";
+char	rmdirhelp[] =	"remove directory on the remote machine";
+char	rmtstatushelp[]="show status of remote machine";
+char	runiquehelp[] = "toggle store unique for local files";
+char	sendhelp[] =	"send one file";
+char	shellhelp[] =	"escape to the shell";
+char	sitehelp[] =	"send site specific command to remote server\n"
+			"\t\tTry \"rhelp site\" or \"site help\" "
+			"for more information";
+char	sizecmdhelp[] = "show size of remote file";
+char	statushelp[] =	"show current status";
+char	structhelp[] =	"set file transfer structure";
+char	suniquehelp[] = "toggle store unique on remote machine";
+char	systemhelp[] =  "show remote system type";
+char	tracehelp[] =	"toggle packet tracing";
+char	typehelp[] =	"set file transfer type";
+char	umaskhelp[] =	"get (set) umask on remote side";
+char	userhelp[] =	"send new user information";
+char	verbosehelp[] =	"toggle verbose mode";
+
+#define CMPL(x)	__STRING(x),
+#define CMPL0	"",
+#define H(x)	x
+
+struct cmd cmdtab[] = {
+	{ "!",		H(shellhelp),	0, 0, 0, CMPL0		shell },
+	{ "$",		H(domachelp),	1, 0, 0, CMPL0		domacro },
+	{ "account",	H(accounthelp),	0, 1, 1, CMPL0		account},
+	{ "append",	H(appendhelp),	1, 1, 1, CMPL(lr)	put },
+	{ "ascii",	H(asciihelp),	0, 1, 1, CMPL0		setascii },
+	{ "bell",	H(beephelp),	0, 0, 0, CMPL0		setbell },
+	{ "binary",	H(binaryhelp),	0, 1, 1, CMPL0		setbinary },
+	{ "bye",	H(quithelp),	0, 0, 0, CMPL0		quit },
+	{ "case",	H(casehelp),	0, 0, 1, CMPL0		setcase },
+	{ "cd",		H(cdhelp),	0, 1, 1, CMPL(r)	cd },
+	{ "cdup",	H(cduphelp),	0, 1, 1, CMPL0		cdup },
+	{ "chmod",	H(chmodhelp),	0, 1, 1, CMPL(nr)	do_chmod },
+	{ "close",	H(disconhelp),	0, 1, 1, CMPL0		disconnect },
+	{ "cr",		H(crhelp),	0, 0, 0, CMPL0		setcr },
+	{ "debug",	H(debughelp),	0, 0, 0, CMPL0		setdebug },
+	{ "delete",	H(deletehelp),	0, 1, 1, CMPL(r)	deletecmd },
+	{ "dir",	H(dirhelp),	1, 1, 1, CMPL(rl)	ls },
+	{ "disconnect",	H(disconhelp),	0, 1, 1, CMPL0		disconnect },
+	{ "edit",	H(edithelp),	0, 0, 0, CMPL0		setedit },
+	{ "epsv4",	H(epsv4help),	0, 0, 0, CMPL0		setepsv4 },
+	{ "exit",	H(quithelp),	0, 0, 0, CMPL0		quit },
+	{ "form",	H(formhelp),	0, 1, 1, CMPL0		setform },
+	{ "ftp",	H(connecthelp),	0, 0, 1, CMPL0		setpeer },
+	{ "get",	H(receivehelp),	1, 1, 1, CMPL(rl)	get },
+	{ "gate",	H(gatehelp),	0, 0, 0, CMPL0		setgate },
+	{ "glob",	H(globhelp),	0, 0, 0, CMPL0		setglob },
+	{ "hash",	H(hashhelp),	0, 0, 0, CMPL0		sethash },
+	{ "help",	H(helphelp),	0, 0, 1, CMPL(C)	help },
+	{ "idle",	H(idlehelp),	0, 1, 1, CMPL0		idle },
+	{ "image",	H(binaryhelp),	0, 1, 1, CMPL0		setbinary },
+	{ "lcd",	H(lcdhelp),	0, 0, 0, CMPL(l)	lcd },
+	{ "less",	H(pagehelp),	1, 1, 1, CMPL(r)	page },
+	{ "lpwd",	H(lpwdhelp),	0, 0, 0, CMPL0		lpwd },
+	{ "ls",		H(lshelp),	1, 1, 1, CMPL(rl)	ls },
+	{ "macdef",	H(macdefhelp),	0, 0, 0, CMPL0		macdef },
+	{ "mdelete",	H(mdeletehelp),	1, 1, 1, CMPL(R)	mdelete },
+	{ "mdir",	H(mdirhelp),	1, 1, 1, CMPL(R)	mls },
+	{ "mget",	H(mgethelp),	1, 1, 1, CMPL(R)	mget },
+	{ "mkdir",	H(mkdirhelp),	0, 1, 1, CMPL(r)	makedir },
+	{ "mls",	H(mlshelp),	1, 1, 1, CMPL(R)	mls },
+	{ "mode",	H(modehelp),	0, 1, 1, CMPL0		setftmode },
+	{ "modtime",	H(modtimehelp),	0, 1, 1, CMPL(r)	modtime },
+	{ "more",	H(pagehelp),	1, 1, 1, CMPL(r)	page },
+	{ "mput",	H(mputhelp),	1, 1, 1, CMPL(L)	mput },
+	{ "msend",	H(mputhelp),	1, 1, 1, CMPL(L)	mput },
+	{ "newer",	H(newerhelp),	1, 1, 1, CMPL(r)	newer },
+	{ "nlist",	H(nlisthelp),	1, 1, 1, CMPL(rl)	ls },
+	{ "nmap",	H(nmaphelp),	0, 0, 1, CMPL0		setnmap },
+	{ "ntrans",	H(ntranshelp),	0, 0, 1, CMPL0		setntrans },
+	{ "open",	H(connecthelp),	0, 0, 1, CMPL0		setpeer },
+	{ "page",	H(pagehelp),	1, 1, 1, CMPL(r)	page },
+	{ "passive",	H(passivehelp),	0, 0, 0, CMPL0		setpassive },
+	{ "preserve",	H(preservehelp),0, 0, 0, CMPL0		setpreserve },
+	{ "progress",	H(progresshelp),0, 0, 0, CMPL0		setprogress },
+	{ "prompt",	H(prompthelp),	0, 0, 0, CMPL0		setprompt },
+	{ "proxy",	H(proxyhelp),	0, 0, 1, CMPL(c)	doproxy },
+	{ "put",	H(sendhelp),	1, 1, 1, CMPL(lr)	put },
+	{ "pwd",	H(pwdhelp),	0, 1, 1, CMPL0		pwd },
+	{ "quit",	H(quithelp),	0, 0, 0, CMPL0		quit },
+	{ "quote",	H(quotehelp),	1, 1, 1, CMPL0		quote },
+	{ "recv",	H(receivehelp),	1, 1, 1, CMPL(rl)	get },
+	{ "reget",	H(regethelp),	1, 1, 1, CMPL(rl)	reget },
+	{ "rename",	H(renamehelp),	0, 1, 1, CMPL(rr)	renamefile },
+	{ "reput",	H(reputhelp),	1, 1, 1, CMPL(lr)	reput },
+	{ "reset",	H(resethelp),	0, 1, 1, CMPL0		reset },
+	{ "restart",	H(restarthelp),	1, 1, 1, CMPL0		restart },
+	{ "rhelp",	H(remotehelp),	0, 1, 1, CMPL0		rmthelp },
+	{ "rmdir",	H(rmdirhelp),	0, 1, 1, CMPL(r)	removedir },
+	{ "rstatus",	H(rmtstatushelp),0, 1, 1, CMPL(r)	rmtstatus },
+	{ "runique",	H(runiquehelp),	0, 0, 1, CMPL0		setrunique },
+	{ "send",	H(sendhelp),	1, 1, 1, CMPL(lr)	put },
+	{ "sendport",	H(porthelp),	0, 0, 0, CMPL0		setport },
+	{ "site",	H(sitehelp),	0, 1, 1, CMPL0		site },
+	{ "size",	H(sizecmdhelp),	1, 1, 1, CMPL(r)	sizecmd },
+	{ "status",	H(statushelp),	0, 0, 1, CMPL0		status },
+	{ "struct",	H(structhelp),	0, 1, 1, CMPL0		setstruct },
+	{ "sunique",	H(suniquehelp),	0, 0, 1, CMPL0		setsunique },
+	{ "system",	H(systemhelp),	0, 1, 1, CMPL0		syst },
+	{ "trace",	H(tracehelp),	0, 0, 0, CMPL0		settrace },
+	{ "type",	H(typehelp),	0, 1, 1, CMPL0		settype },
+	{ "umask",	H(umaskhelp),	0, 1, 1, CMPL0		do_umask },
+	{ "user",	H(userhelp),	0, 1, 1, CMPL0		user },
+	{ "verbose",	H(verbosehelp),	0, 0, 0, CMPL0		setverbose },
+	{ "?",		H(helphelp),	0, 0, 1, CMPL(C)	help },
+	{ 0 }
+};
+
+int	NCMDS = (sizeof(cmdtab) / sizeof(cmdtab[0])) - 1;
+
+#endif /* !SMALL */
+
diff --git a/compat.c b/compat.c
new file mode 100644
index 0000000..b578962
--- /dev/null
+++ b/compat.c
@@ -0,0 +1,330 @@
+#define COMPAT_C_IMPL
+#include "compat.h"
+
+#ifndef __OpenBSD__
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+
+#ifndef program_invocation_short_name
+extern char *__progname;
+#define program_invocation_short_name (__progname ? __progname : "ftp")
+#endif
+
+long long
+strtonum(const char *numstr, long long minval, long long maxval,
+    const char **errstrp)
+{
+	long long ll = 0;
+	int error = 0;
+	char *ep;
+	struct errval {
+		const char *errstr;
+		int err;
+	} ev[4] = {
+		{ NULL, 0 },
+		{ "too large", ERANGE },
+		{ "too small", ERANGE },
+		{ "invalid", EINVAL },
+	};
+
+	ev[0].err = errno;
+	errno = 0;
+	if (minval > maxval) {
+		error = 3;
+		ev[error].err = EINVAL;
+		ev[error].errstr = "invalid";
+	} else {
+		ll = strtoll(numstr, &ep, 10);
+		if (numstr == ep || *ep != '\0')
+			error = 3;
+		else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
+			error = 1;
+		else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
+			error = 2;
+	}
+	if (errstrp != NULL)
+		*errstrp = ev[error].errstr;
+	errno = ev[error].err;
+	if (error)
+		ll = 0;
+
+	return (ll);
+}
+
+size_t
+strlcpy(char *dst, const char *src, size_t siz)
+{
+	char *d = dst;
+	const char *s = src;
+	size_t n = siz;
+
+	if (n != 0) {
+		while (--n != 0) {
+			if ((*d++ = *s++) == '\0')
+				break;
+		}
+	}
+
+	if (n == 0) {
+		if (siz != 0)
+			*d = '\0';
+		while (*s++)
+			;
+	}
+
+	return(s - src - 1);
+}
+
+void
+err(int eval, const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	if (fmt != NULL) {
+		fprintf(stderr, "%s: ", program_invocation_short_name);
+		vfprintf(stderr, fmt, ap);
+		va_end(ap);
+		if (errno != 0)
+			fprintf(stderr, ": %s", strerror(errno));
+		fprintf(stderr, "\n");
+	} else {
+		if (errno != 0)
+			fprintf(stderr, "%s: %s\n", program_invocation_short_name,
+			    strerror(errno));
+		va_end(ap);
+	}
+	exit(eval);
+}
+
+void
+errx(int eval, const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	fprintf(stderr, "%s: ", program_invocation_short_name);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	fprintf(stderr, "\n");
+	exit(eval);
+}
+
+void
+warn(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	if (fmt != NULL) {
+		fprintf(stderr, "%s: ", program_invocation_short_name);
+		vfprintf(stderr, fmt, ap);
+		va_end(ap);
+		if (errno != 0)
+			fprintf(stderr, ": %s", strerror(errno));
+		fprintf(stderr, "\n");
+	} else {
+		if (errno != 0)
+			fprintf(stderr, "%s: %s\n", program_invocation_short_name,
+			    strerror(errno));
+		va_end(ap);
+	}
+	errno = 0;
+}
+
+void
+warnx(const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	fprintf(stderr, "%s: ", program_invocation_short_name);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	fprintf(stderr, "\n");
+}
+
+#if defined(__linux__) && !defined(HAVE_FUNOPEN)
+#define _GNU_SOURCE
+#include <stdio.h>
+
+struct funopen_cookie {
+	const void *cookie;
+	int (*readfn)(void *, char *, int);
+	int (*writefn)(void *, const char *, int);
+	fpos_t (*seekfn)(void *, fpos_t, int);
+	int (*closefn)(void *);
+};
+
+static ssize_t
+funopen_read(void *cookie, char *buf, size_t size)
+{
+	struct funopen_cookie *c = cookie;
+	if (c->readfn)
+		return c->readfn((void *)c->cookie, buf, (int)size);
+	return -1;
+}
+
+static ssize_t
+funopen_write(void *cookie, const char *buf, size_t size)
+{
+	struct funopen_cookie *c = cookie;
+	if (c->writefn)
+		return c->writefn((void *)c->cookie, buf, (int)size);
+	return -1;
+}
+
+static int
+funopen_seek(void *cookie, off64_t *offset, int whence)
+{
+	struct funopen_cookie *c = cookie;
+	fpos_t pos = (fpos_t)*offset;
+	int ret;
+	if (c->seekfn) {
+		ret = c->seekfn((void *)c->cookie, pos, whence);
+		if (ret == 0)
+			*offset = (off64_t)pos;
+		return ret;
+	}
+	return -1;
+}
+
+static int
+funopen_close(void *cookie)
+{
+	struct funopen_cookie *c = cookie;
+	if (c->closefn)
+		return c->closefn((void *)c->cookie);
+	free(c);
+	return 0;
+}
+
+FILE *
+funopen(const void *cookie, 
+    int (*readfn)(void *, char *, int),
+    int (*writefn)(void *, const char *, int),
+    fpos_t (*seekfn)(void *, fpos_t, int),
+    int (*closefn)(void *))
+{
+	struct funopen_cookie *c = malloc(sizeof(*c));
+	if (c == NULL)
+		return NULL;
+	c->cookie = cookie;
+	c->readfn = readfn;
+	c->writefn = writefn;
+	c->seekfn = seekfn;
+	c->closefn = closefn;
+
+	cookie_io_functions_t funcs = {
+		.read = funopen_read,
+		.write = funopen_write,
+		.seek = funopen_seek,
+		.close = funopen_close
+	};
+
+	return fopencookie(c, "r+", funcs);
+}
+#endif
+
+#if defined(__APPLE__)
+#include <vis.h>
+
+int
+strnvis_openbsd(char *dst, const char *src, size_t dlen, int flags)
+{
+	return strnvis(dst, dlen, src, flags);
+}
+#endif
+
+#ifndef __OpenBSD__
+#include <sys/types.h>
+#ifndef u_char
+#define u_char unsigned char
+#endif
+
+static const char Base64[] =
+	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char Pad64 = '=';
+
+int
+b64_ntop(u_char const *src, size_t srclength, char *target, size_t targsize)
+{
+	size_t datalength = 0;
+	u_char input[3];
+	u_char output[4];
+	u_int i;
+
+	while (2 < srclength) {
+		input[0] = *src++;
+		input[1] = *src++;
+		input[2] = *src++;
+		srclength -= 3;
+
+		output[0] = input[0] >> 2;
+		output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+		output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+		output[3] = input[2] & 0x3f;
+
+		if (datalength + 4 > targsize)
+			return (-1);
+		target[datalength++] = Base64[output[0]];
+		target[datalength++] = Base64[output[1]];
+		target[datalength++] = Base64[output[2]];
+		target[datalength++] = Base64[output[3]];
+	}
+
+	if (srclength != 0) {
+		input[0] = input[1] = input[2] = '\0';
+		for (i = 0; i < srclength; i++)
+			input[i] = *src++;
+
+		output[0] = input[0] >> 2;
+		output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+		output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+
+		if (datalength + 4 > targsize)
+			return (-1);
+		target[datalength++] = Base64[output[0]];
+		target[datalength++] = Base64[output[1]];
+		if (srclength == 1)
+			target[datalength++] = Pad64;
+		else
+			target[datalength++] = Base64[output[2]];
+		target[datalength++] = Pad64;
+	}
+	if (datalength >= targsize)
+		return (-1);
+	target[datalength] = '\0';
+	return (datalength);
+}
+#endif
+
+#if !defined(__OpenBSD__)
+#include <poll.h>
+#include <signal.h>
+
+int
+ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts,
+    const sigset_t *sigmask)
+{
+	int timeout_ms;
+
+	(void)sigmask; /* Ignoring signal mask for simplicity */
+
+	if (timeout_ts == NULL) {
+		timeout_ms = -1;
+	} else {
+		/* Convert timespec to milliseconds */
+		timeout_ms = timeout_ts->tv_sec * 1000 + timeout_ts->tv_nsec / 1000000;
+	}
+
+	return poll(fds, nfds, timeout_ms);
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/compat.h b/compat.h
new file mode 100644
index 0000000..ad0b45d
--- /dev/null
+++ b/compat.h
@@ -0,0 +1,115 @@
+#ifndef COMPAT_H
+#define COMPAT_H
+
+#include <sys/types.h>
+#include <limits.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+
+#ifndef __OpenBSD__
+#ifndef HOST_NAME_MAX
+#ifdef _POSIX_HOST_NAME_MAX
+#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+#else
+#define HOST_NAME_MAX 255
+#endif
+#endif
+
+#ifndef LOGIN_NAME_MAX
+#ifdef _POSIX_LOGIN_NAME_MAX
+#define LOGIN_NAME_MAX _POSIX_LOGIN_NAME_MAX
+#else
+#define LOGIN_NAME_MAX 256
+#endif
+#endif
+
+#ifndef HAVE_STRTONUM
+#define HAVE_STRTONUM
+long long strtonum(const char *numstr, long long minval, long long maxval,
+    const char **errstrp);
+#endif
+
+#ifndef HAVE_STRLCPY
+#define HAVE_STRLCPY
+size_t strlcpy(char *dst, const char *src, size_t siz);
+#endif
+
+#ifndef HAVE_ERR
+#define HAVE_ERR
+void err(int eval, const char *fmt, ...);
+void errx(int eval, const char *fmt, ...);
+void warn(const char *fmt, ...);
+void warnx(const char *fmt, ...);
+#endif
+
+#ifndef HAVE_TIMESPECADD
+#define HAVE_TIMESPECADD
+#define timespecadd(tsp, usp, vsp) \
+	do { \
+		(vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \
+		(vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \
+		if ((vsp)->tv_nsec >= 1000000000L) { \
+			(vsp)->tv_sec++; \
+			(vsp)->tv_nsec -= 1000000000L; \
+		} \
+	} while (0)
+#define timespecsub(tsp, usp, vsp) \
+	do { \
+		(vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \
+		(vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \
+		if ((vsp)->tv_nsec < 0) { \
+			(vsp)->tv_sec--; \
+			(vsp)->tv_nsec += 1000000000L; \
+		} \
+	} while (0)
+#endif
+
+#ifndef HAVE_PLEDGE
+#define HAVE_PLEDGE
+static inline int pledge(const char *promises, const char *execpromises) {
+	(void)promises;
+	(void)execpromises;
+	return 0;
+}
+#endif
+
+#if !defined(__OpenBSD__)
+#include <poll.h>
+#include <time.h>
+#include <signal.h>
+int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts,
+    const sigset_t *sigmask);
+#endif
+
+#if defined(__APPLE__) && !defined(HAVE_STRNVIS)
+#define HAVE_STRNVIS
+#ifndef COMPAT_C_IMPL
+int strnvis_openbsd(char *dst, const char *src, size_t dlen, int flags);
+#define strnvis strnvis_openbsd
+#endif
+#endif
+
+#ifndef HAVE_B64_NTOP
+#define HAVE_B64_NTOP
+#ifndef u_char
+#define u_char unsigned char
+#endif
+int b64_ntop(u_char const *src, size_t srclength, char *target, size_t targsize);
+#endif
+#endif
+
+#if defined(__linux__) && !defined(HAVE_FUNOPEN)
+#define HAVE_FUNOPEN
+#define _GNU_SOURCE
+#include <stdio.h>
+FILE *funopen(const void *cookie, 
+    int (*readfn)(void *, char *, int),
+    int (*writefn)(void *, const char *, int),
+    fpos_t (*seekfn)(void *, fpos_t, int),
+    int (*closefn)(void *));
+#endif
+
+#endif
\ No newline at end of file
diff --git a/complete.c b/complete.c
new file mode 100644
index 0000000..ef2478f
--- /dev/null
+++ b/complete.c
@@ -0,0 +1,381 @@
+/*	$OpenBSD: complete.c,v 1.33 2019/05/16 12:44:17 florian Exp $	*/
+/*	$NetBSD: complete.c,v 1.10 1997/08/18 10:20:18 lukem Exp $	*/
+
+/*-
+ * Copyright (c) 1997 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Luke Mewburn.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+/*
+ * FTP user program - command and file completion routines
+ */
+
+#include <ctype.h>
+#include <err.h>
+#include <dirent.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ftp_var.h"
+
+static int	     comparstr(const void *, const void *);
+static unsigned char complete_ambiguous(char *, int, StringList *);
+static unsigned char complete_command(char *, int);
+static unsigned char complete_local(char *, int);
+static unsigned char complete_remote(char *, int);
+static void          ftpvis(char *, size_t, const char *, size_t);
+
+static int
+comparstr(const void *a, const void *b)
+{
+	return (strcmp(*(char **)a, *(char **)b));
+}
+
+/*
+ * Determine if complete is ambiguous. If unique, insert.
+ * If no choices, error. If unambiguous prefix, insert that.
+ * Otherwise, list choices. words is assumed to be filtered
+ * to only contain possible choices.
+ * Args:
+ *	word	word which started the match
+ *	list	list by default
+ *	words	stringlist containing possible matches
+ */
+static unsigned char
+complete_ambiguous(char *word, int list, StringList *words)
+{
+	char insertstr[PATH_MAX * 2];
+	char *lastmatch;
+	int i, j;
+	size_t matchlen, wordlen;
+
+	wordlen = strlen(word);
+	if (words->sl_cur == 0)
+		return (CC_ERROR);	/* no choices available */
+
+	if (words->sl_cur == 1) {	/* only once choice available */
+		char *p = words->sl_str[0] + wordlen;
+		ftpvis(insertstr, sizeof(insertstr), p, strlen(p));
+		if (el_insertstr(el, insertstr) == -1)
+			return (CC_ERROR);
+		else
+			return (CC_REFRESH);
+	}
+
+	if (!list) {
+		lastmatch = words->sl_str[0];
+		matchlen = strlen(lastmatch);
+		for (i = 1 ; i < words->sl_cur ; i++) {
+			for (j = wordlen ; j < strlen(words->sl_str[i]); j++)
+				if (lastmatch[j] != words->sl_str[i][j])
+					break;
+			if (j < matchlen)
+				matchlen = j;
+		}
+		if (matchlen > wordlen) {
+			ftpvis(insertstr, sizeof(insertstr),
+			    lastmatch + wordlen, matchlen - wordlen);
+			if (el_insertstr(el, insertstr) == -1)
+				return (CC_ERROR);
+			else
+					/*
+					 * XXX: really want CC_REFRESH_BEEP
+					 */
+				return (CC_REFRESH);
+		}
+	}
+
+	putc('\n', ttyout);
+	qsort(words->sl_str, words->sl_cur, sizeof(char *), comparstr);
+	list_vertical(words);
+	return (CC_REDISPLAY);
+}
+
+/*
+ * Complete a command
+ */
+static unsigned char
+complete_command(char *word, int list)
+{
+	struct cmd *c;
+	StringList *words;
+	size_t wordlen;
+	unsigned char rv;
+
+	words = sl_init();
+	wordlen = strlen(word);
+
+	for (c = cmdtab; c->c_name != NULL; c++) {
+		if (wordlen > strlen(c->c_name))
+			continue;
+		if (strncmp(word, c->c_name, wordlen) == 0)
+			sl_add(words, c->c_name);
+	}
+
+	rv = complete_ambiguous(word, list, words);
+	sl_free(words, 0);
+	return (rv);
+}
+
+/*
+ * Complete a local file
+ */
+static unsigned char
+complete_local(char *word, int list)
+{
+	StringList *words;
+	char dir[PATH_MAX];
+	char *file;
+	DIR *dd;
+	struct dirent *dp;
+	unsigned char rv;
+
+	if ((file = strrchr(word, '/')) == NULL) {
+		dir[0] = '.';
+		dir[1] = '\0';
+		file = word;
+	} else {
+		if (file == word) {
+			dir[0] = '/';
+			dir[1] = '\0';
+		} else {
+			(void)strlcpy(dir, word, (size_t)(file - word) + 1);
+		}
+		file++;
+	}
+
+	if ((dd = opendir(dir)) == NULL)
+		return (CC_ERROR);
+
+	words = sl_init();
+
+	for (dp = readdir(dd); dp != NULL; dp = readdir(dd)) {
+		if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, ".."))
+			continue;
+		if (strlen(file) > dp->d_namlen)
+			continue;
+		if (strncmp(file, dp->d_name, strlen(file)) == 0) {
+			char *tcp;
+
+			tcp = strdup(dp->d_name);
+			if (tcp == NULL)
+				errx(1, "Can't allocate memory for local dir");
+			sl_add(words, tcp);
+		}
+	}
+	closedir(dd);
+
+	rv = complete_ambiguous(file, list, words);
+	sl_free(words, 1);
+	return (rv);
+}
+
+/*
+ * Complete a remote file
+ */
+static unsigned char
+complete_remote(char *word, int list)
+{
+	static StringList *dirlist;
+	static char	 lastdir[PATH_MAX];
+	StringList	*words;
+	char		 dir[PATH_MAX];
+	char		*file, *cp;
+	int		 i;
+	unsigned char	 rv;
+
+	char *dummyargv[] = { "complete", dir, NULL };
+
+	if ((file = strrchr(word, '/')) == NULL) {
+		dir[0] = '.';
+		dir[1] = '\0';
+		file = word;
+	} else {
+		cp = file;
+		while (*cp == '/' && cp > word)
+			cp--;
+		(void)strlcpy(dir, word, (size_t)(cp - word + 2));
+		file++;
+	}
+
+	if (dirchange || strcmp(dir, lastdir) != 0) {	/* dir not cached */
+		char *emesg;
+
+		sl_free(dirlist, 1);
+		dirlist = sl_init();
+
+		mflag = 1;
+		emesg = NULL;
+		if (debug)
+			(void)putc('\n', ttyout);
+		while ((cp = remglob(dummyargv, 0, &emesg)) != NULL) {
+			char *tcp;
+
+			if (!mflag)
+				continue;
+			if (*cp == '\0') {
+				mflag = 0;
+				continue;
+			}
+			tcp = strrchr(cp, '/');
+			if (tcp)
+				tcp++;
+			else
+				tcp = cp;
+			tcp = strdup(tcp);
+			if (tcp == NULL)
+				errx(1, "Can't allocate memory for remote dir");
+			sl_add(dirlist, tcp);
+		}
+		if (emesg != NULL) {
+			fprintf(ttyout, "\n%s\n", emesg);
+			return (CC_REDISPLAY);
+		}
+		(void)strlcpy(lastdir, dir, sizeof lastdir);
+		dirchange = 0;
+	}
+
+	words = sl_init();
+	for (i = 0; i < dirlist->sl_cur; i++) {
+		cp = dirlist->sl_str[i];
+		if (strlen(file) > strlen(cp))
+			continue;
+		if (strncmp(file, cp, strlen(file)) == 0)
+			sl_add(words, cp);
+	}
+	rv = complete_ambiguous(file, list, words);
+	sl_free(words, 0);
+	return (rv);
+}
+
+/*
+ * Generic complete routine
+ */
+unsigned char
+complete(EditLine *el, int ch)
+{
+	static char word[FTPBUFLEN];
+	static int lastc_argc, lastc_argo;
+	struct cmd *c;
+	const LineInfo *lf;
+	int celems, dolist;
+	size_t len;
+
+	lf = el_line(el);
+	len = lf->lastchar - lf->buffer;
+	if (len >= sizeof(line))
+		return (CC_ERROR);
+	(void)memcpy(line, lf->buffer, len);
+	line[len] = '\0';
+	cursor_pos = line + (lf->cursor - lf->buffer);
+	lastc_argc = cursor_argc;	/* remember last cursor pos */
+	lastc_argo = cursor_argo;
+	makeargv();			/* build argc/argv of current line */
+
+	if (cursor_argo >= sizeof(word))
+		return (CC_ERROR);
+
+	dolist = 0;
+			/* if cursor and word is same, list alternatives */
+	if (lastc_argc == cursor_argc && lastc_argo == cursor_argo
+	    && strncmp(word, margv[cursor_argc], cursor_argo) == 0)
+		dolist = 1;
+	else if (cursor_argo)
+		memcpy(word, margv[cursor_argc], cursor_argo);
+	word[cursor_argo] = '\0';
+
+	if (cursor_argc == 0)
+		return (complete_command(word, dolist));
+
+	c = getcmd(margv[0]);
+	if (c == (struct cmd *)-1 || c == 0)
+		return (CC_ERROR);
+	celems = strlen(c->c_complete);
+
+		/* check for 'continuation' completes (which are uppercase) */
+	if ((cursor_argc > celems) && (celems > 0)
+	    && isupper((unsigned char)c->c_complete[celems - 1]))
+		cursor_argc = celems;
+
+	if (cursor_argc > celems)
+		return (CC_ERROR);
+
+	switch (c->c_complete[cursor_argc - 1]) {
+	case 'l':			/* local complete */
+	case 'L':
+		return (complete_local(word, dolist));
+	case 'r':			/* remote complete */
+	case 'R':
+		if (connected != -1) {
+			fputs("\nMust be logged in to complete.\n", ttyout);
+			return (CC_REDISPLAY);
+		}
+		return (complete_remote(word, dolist));
+	case 'c':			/* command complete */
+	case 'C':
+		return (complete_command(word, dolist));
+	case 'n':			/* no complete */
+		return (CC_ERROR);
+	}
+
+	return (CC_ERROR);
+}
+
+/*
+ * Copy characters from src into dst, \ quoting characters that require it.
+ */
+static void
+ftpvis(char *dst, size_t dstlen, const char *src, size_t srclen)
+{
+	size_t	di, si;
+
+	di = si = 0;
+	while (di + 1 < dstlen && si < srclen && src[si] != '\0') {
+		switch (src[si]) {
+		case '\\':
+		case ' ':
+		case '\t':
+		case '\r':
+		case '\n':
+		case '"':
+			/* Need room for two characters and NUL, avoiding
+			 * incomplete escape sequences at end of dst. */
+			if (di + 3 >= dstlen)
+				break;
+			dst[di++] = '\\';
+			/* FALLTHROUGH */
+		default:
+			dst[di++] = src[si++];
+		}
+	}
+	if (dstlen != 0)
+		dst[di] = '\0';
+}
+#endif /* !SMALL */
diff --git a/cookie.c b/cookie.c
new file mode 100644
index 0000000..b5f5beb
--- /dev/null
+++ b/cookie.c
@@ -0,0 +1,221 @@
+/*	$OpenBSD: cookie.c,v 1.10 2021/02/16 16:27:34 naddy Exp $	*/
+
+/*
+ * Copyright (c) 2007 Pierre-Yves Ritschard <pyr@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef NOSSL
+
+#include <sys/types.h>
+#include <sys/queue.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "ftp_var.h"
+
+struct cookie {
+	TAILQ_ENTRY(cookie)	 entry;
+	TAILQ_ENTRY(cookie)	 tempentry;
+	u_int8_t		 flags;
+#define F_SECURE		 0x01
+#define F_TAILMATCH		 0x02
+#define F_NOEXPIRY		 0x04
+#define F_MATCHPATH		 0x08
+	time_t			 expires;
+	char			*domain;
+	char			*path;
+	char			*key;
+	char			*val;
+};
+TAILQ_HEAD(cookiejar, cookie);
+
+typedef enum {
+	DOMAIN = 0, TAILMATCH = 1, PATH = 2, SECURE = 3,
+	EXPIRES = 4, NAME = 5, VALUE = 6, DONE = 7
+} field_t;
+
+static struct cookiejar jar;
+
+void
+cookie_load(void)
+{
+	field_t		 field;
+	time_t		 date;
+	char		*line;
+	char		*lbuf = NULL;
+	size_t		 lbufsize = 0;
+	char		*param;
+	const char	*estr;
+	FILE		*fp;
+	struct cookie	*ck;
+
+	if (cookiefile == NULL)
+		return;
+
+	TAILQ_INIT(&jar);
+	fp = fopen(cookiefile, "r");
+	if (fp == NULL)
+		err(1, "cannot open cookie file %s", cookiefile);
+	date = time(NULL);
+	while (getline(&lbuf, &lbufsize, fp) != -1) {
+		line = lbuf;
+		line[strcspn(line, "\r\n")] = '\0';
+
+		line += strspn(line, " \t");
+		if ((*line == '#') || (*line == '\0')) {
+			continue;
+		}
+		field = DOMAIN;
+		ck = calloc(1, sizeof(*ck));
+		if (ck == NULL)
+			err(1, NULL);
+		while ((param = strsep(&line, "\t")) != NULL) {
+			switch (field) {
+			case DOMAIN:
+				if (*param == '.') {
+					if (asprintf(&ck->domain,
+					    "*%s", param) == -1)
+						err(1, NULL);
+				} else {
+					ck->domain = strdup(param);
+					if (ck->domain == NULL)
+						err(1, NULL);
+				}
+				break;
+			case TAILMATCH:
+				if (strcasecmp(param, "TRUE") == 0) {
+					ck->flags |= F_TAILMATCH;
+				} else if (strcasecmp(param, "FALSE") != 0) {
+					errx(1, "invalid cookie file");
+				}
+				break;
+			case PATH:
+				if (strcmp(param, "/") != 0) {
+					ck->flags |= F_MATCHPATH;
+					if (asprintf(&ck->path,
+					    "%s*", param) == -1)
+						err(1, NULL);
+				}
+				break;
+			case SECURE:
+				if (strcasecmp(param, "TRUE") == 0) {
+					ck->flags |= F_SECURE;
+				} else if (strcasecmp(param, "FALSE") != 0) {
+					errx(1, "invalid cookie file");
+				}
+				break;
+			case EXPIRES:
+				/*
+				 * rely on sizeof(time_t) being 4
+				 */
+				ck->expires = strtonum(param, 0,
+				    INT_MAX, &estr);
+				if (estr) {
+					if (errno == ERANGE)
+						ck->flags |= F_NOEXPIRY;
+					else
+						errx(1, "invalid cookie file");
+				}
+				break;
+			case NAME:
+				ck->key = strdup(param);
+				if (ck->key == NULL)
+					err(1, NULL);
+				break;
+			case VALUE:
+				ck->val = strdup(param);
+				if (ck->val == NULL)
+					err(1, NULL);
+				break;
+			case DONE:
+				errx(1, "invalid cookie file");
+				break;
+			}
+			field++;
+		}
+		if (field != DONE)
+			errx(1, "invalid cookie file");
+		if (ck->expires < date && !(ck->flags & F_NOEXPIRY)) {
+			free(ck->val);
+			free(ck->key);
+			free(ck->path);
+			free(ck->domain);
+			free(ck);
+		} else
+			TAILQ_INSERT_TAIL(&jar, ck, entry);
+	}
+	free(lbuf);
+	fclose(fp);
+}
+
+void
+cookie_get(const char *domain, const char *path, int secure, char **pstr)
+{
+	size_t		 len;
+	size_t		 headlen;
+	char		*head;
+	char		*str;
+	struct cookie	*ck;
+	struct cookiejar tempjar;
+
+	*pstr = NULL;
+
+	if (cookiefile == NULL)
+		return;
+
+	TAILQ_INIT(&tempjar);
+	len = strlen("Cookie\r\n");
+
+	TAILQ_FOREACH(ck, &jar, entry) {
+		if (fnmatch(ck->domain, domain, 0) == 0 &&
+		    (secure || !(ck->flags & F_SECURE))) {
+
+			if (ck->flags & F_MATCHPATH &&
+			    fnmatch(ck->path, path, 0) != 0)
+				continue;
+
+			len += strlen(ck->key) + strlen(ck->val) +
+			    strlen("; =");
+			TAILQ_INSERT_TAIL(&tempjar, ck, tempentry);
+		}
+	}
+	if (TAILQ_EMPTY(&tempjar))
+		return;
+	len += 1;
+	str = malloc(len);
+	if (str == NULL)
+		err(1, NULL);
+
+	(void)strlcpy(str, "Cookie:", len);
+	TAILQ_FOREACH(ck, &tempjar, tempentry) {
+		head = str + strlen(str);
+		headlen = len - strlen(str);
+
+		snprintf(head, headlen, "%s %s=%s",
+		    (ck == TAILQ_FIRST(&tempjar))? "" : ";", ck->key, ck->val);
+	}
+	if (strlcat(str, "\r\n", len) >= len)
+		errx(1, "cookie header truncated");
+	*pstr = str;
+}
+
+#endif /* !SMALL */
+
diff --git a/domacro.c b/domacro.c
new file mode 100644
index 0000000..cba0f9f
--- /dev/null
+++ b/domacro.c
@@ -0,0 +1,146 @@
+/*	$OpenBSD: domacro.c,v 1.22 2019/11/18 04:37:35 deraadt Exp $	*/
+/*	$NetBSD: domacro.c,v 1.10 1997/07/20 09:45:45 lukem Exp $	*/
+
+/*
+ * Copyright (c) 1985, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+#include <ctype.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "ftp_var.h"
+
+void
+domacro(int argc, char *argv[])
+{
+	int i, j, count = 2, loopflg = 0;
+	char *cp1, *cp2, line2[FTPBUFLEN];
+	struct cmd *c;
+
+	if (argc < 2 && !another(&argc, &argv, "macro name")) {
+		fprintf(ttyout, "usage: %s macro-name\n", argv[0]);
+		code = -1;
+		return;
+	}
+	for (i = 0; i < macnum; ++i) {
+		if (!strncmp(argv[1], macros[i].mac_name, 9)) {
+			break;
+		}
+	}
+	if (i == macnum) {
+		fprintf(ttyout, "'%s' macro not found.\n", argv[1]);
+		code = -1;
+		return;
+	}
+	(void)strlcpy(line2, line, sizeof(line2));
+TOP:
+	cp1 = macros[i].mac_start;
+	while (cp1 != macros[i].mac_end) {
+		while (isspace((unsigned char)*cp1)) {
+			cp1++;
+		}
+		cp2 = line;
+		while (*cp1 != '\0') {
+			switch(*cp1) {
+			case '\\':
+				*cp2++ = *++cp1;
+				break;
+			case '$':
+				if (isdigit((unsigned char)*(cp1 + 1))) {
+					j = 0;
+					while (isdigit((unsigned char)*++cp1)) {
+						j = 10*j +  *cp1 - '0';
+					}
+					cp1--;
+					if (argc - 2 >= j) {
+						(void)strlcpy(cp2, argv[j+1],
+						    sizeof(line) - (cp2 - line));
+						cp2 += strlen(argv[j+1]);
+					}
+					break;
+				}
+				if (*(cp1+1) == 'i') {
+					loopflg = 1;
+					cp1++;
+					if (count < argc) {
+						(void)strlcpy(cp2, argv[count],
+						    sizeof(line) - (cp2 - line));
+						cp2 += strlen(argv[count]);
+					}
+					break;
+				}
+				/* FALLTHROUGH */
+			default:
+				*cp2++ = *cp1;
+				break;
+			}
+			if (*cp1 != '\0') {
+				cp1++;
+			}
+		}
+		*cp2 = '\0';
+		makeargv();
+		c = getcmd(margv[0]);
+		if (c == (struct cmd *)-1) {
+			fputs("?Ambiguous command.\n", ttyout);
+			code = -1;
+		} else if (c == 0) {
+			fputs("?Invalid command.\n", ttyout);
+			code = -1;
+		} else if (c->c_conn && !connected) {
+			fputs("Not connected.\n", ttyout);
+			code = -1;
+		} else {
+			if (verbose) {
+				fputs(line, ttyout);
+				fputc('\n', ttyout);
+			}
+			(*c->c_handler)(margc, margv);
+			if (bell && c->c_bell) {
+				(void)putc('\007', ttyout);
+			}
+			(void)strlcpy(line, line2, sizeof(line));
+			makeargv();
+			argc = margc;
+			argv = margv;
+		}
+		if (cp1 != macros[i].mac_end) {
+			cp1++;
+		}
+	}
+	if (loopflg && ++count < argc) {
+		goto TOP;
+	}
+}
+
+#endif /* !SMALL */
+
diff --git a/extern.h b/extern.h
new file mode 100644
index 0000000..e8a8b3b
--- /dev/null
+++ b/extern.h
@@ -0,0 +1,152 @@
+/*	$OpenBSD: extern.h,v 1.54 2024/05/21 05:00:48 jsg Exp $	*/
+/*	$NetBSD: extern.h,v 1.17 1997/08/18 10:20:19 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*-
+ * Copyright (c) 1994 The Regents of the University of California.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)extern.h	8.3 (Berkeley) 10/9/94
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+void	abort_remote(FILE *);
+void	abortpt(int);
+void	abortrecv(int);
+void	alarmtimer(int);
+int	another(int *, char ***, const char *);
+int	auto_fetch(int, char **, char *);
+void	cdup(int, char **);
+void	cmdabort(int);
+void	cmdscanner(int);
+int	command(const char *, ...);
+int	confirm(const char *, const char *);
+FILE   *dataconn(const char *);
+int	foregroundproc(void);
+int	fileindir(const char *, const char *);
+struct cmd *getcmd(const char *);
+int	getreply(int);
+int	globulize(char **);
+char   *gunique(const char *);
+void	help(int, char **);
+char   *hookup(char *, char *);
+int	initconn(void);
+void	intr(void);
+int	isurl(const char *);
+int	ftp_login(const char *, char *, char *);
+void	lostpeer(void);
+void	makeargv(void);
+void	progressmeter(int, const char *);
+char   *prompt(void);
+void	proxtrans(const char *, const char *, const char *);
+void	psabort(int);
+void	psummary(int);
+void	pswitch(int);
+void	ptransfer(int);
+void	recvrequest(const char *, const char *, const char *,
+	    const char *, int, int);
+char   *remglob(char **, int, char **);
+off_t	remotesize(const char *, int);
+time_t	remotemodtime(const char *, int);
+void	reset(int, char **);
+void	rmthelp(int, char **);
+void	sethash(int, char **);
+void	setpeer(int, char **);
+void	setttywidth(int);
+char   *slurpstring(void);
+int	timed_connect(int s, const struct sockaddr *, socklen_t, int);
+
+__dead void	usage(void);
+
+void	cookie_get(const char *, const char *, int, char **);
+void	cookie_load(void);
+
+#ifndef SMALL
+void	abortsend(int);
+unsigned char complete(EditLine *, int);
+void	controlediting(void);
+void	domacro(int, char **);
+void	list_vertical(StringList *);
+void	parse_list(char **, char *);
+char   *remglob2(char **, int, char **, FILE **ftemp, char *type);
+int	ruserpass(const char *, char **, char **, char **);
+void	sendrequest(const char *, const char *, const char *, int);
+ssize_t http_time(time_t, char *, size_t);
+#endif /* !SMALL */
+
+extern jmp_buf	abortprox;
+extern int	abrtflag;
+extern FILE    *cout;
+extern int	data;
+extern char    *home;
+extern jmp_buf	jabort;
+extern int	family;
+extern int	proxy;
+extern char	reply_string[];
+extern off_t	restart_point;
+extern int	keep_alive_timeout;
+extern int	connect_timeout;
+extern int	pipeout;
+extern char	*action;
+
+#ifndef SMALL
+extern int	NCMDS;
+extern int	server_timestamps;
+#endif /* !SMALL */
+
+extern char *__progname;		/* from crt0.o */
+
diff --git a/fetch.c b/fetch.c
new file mode 100644
index 0000000..47161b0
--- /dev/null
+++ b/fetch.c
@@ -0,0 +1,1789 @@
+/*	$OpenBSD: fetch.c,v 1.221 2025/11/20 11:15:59 tb Exp $	*/
+/*	$NetBSD: fetch.c,v 1.14 1997/08/18 10:20:20 lukem Exp $	*/
+
+/*-
+ * Copyright (c) 1997 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Jason Thorpe and Luke Mewburn.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * FTP User Program -- Command line file retrieval
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <netinet/in.h>
+
+#include <arpa/ftp.h>
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <libgen.h>
+#include <netdb.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <vis.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#ifndef __APPLE__
+#include <resolv.h>
+#endif
+#include <utime.h>
+
+#include "compat.h"
+
+#if !defined(NOSSL) || !NOSSL
+#include "tls_compat.h"
+#else
+struct tls;
+typedef struct tls *tls_t;
+#endif
+
+#include "ftp_var.h"
+#include "cmds.h"
+
+static int	file_get(const char *, const char *);
+static int	url_get(const char *, const char *, const char *, int);
+static int	save_chunked(FILE *, tls_t, int , char *, size_t);
+static void	aborthttp(int);
+static char	hextochar(const char *);
+static char	*urldecode(const char *);
+static char	*recode_credentials(const char *_userinfo);
+static void	ftp_close(FILE **, tls_t *, int *);
+static const char *sockerror(tls_t);
+#ifdef SMALL
+#define 	ftp_printf(fp, ...) fprintf(fp, __VA_ARGS__)
+#else
+static int	ftp_printf(FILE *, const char *, ...);
+#endif /* SMALL */
+#ifndef NOSSL
+static int	proxy_connect(int, char *, char *);
+static int	stdio_tls_write_wrapper(void *, const char *, int);
+static int	stdio_tls_read_wrapper(void *, char *, int);
+#endif /* !NOSSL */
+
+#define	FTP_URL		"ftp://"	/* ftp URL prefix */
+#define	HTTP_URL	"http://"	/* http URL prefix */
+#define	HTTPS_URL	"https://"	/* https URL prefix */
+#define	FILE_URL	"file:"		/* file URL prefix */
+#define FTP_PROXY	"ftp_proxy"	/* env var with ftp proxy location */
+#define HTTP_PROXY	"http_proxy"	/* env var with http proxy location */
+
+#define EMPTYSTRING(x)	((x) == NULL || (*(x) == '\0'))
+
+static const char at_encoding_warning[] =
+    "Extra `@' characters in usernames and passwords should be encoded as %%40";
+
+static jmp_buf	httpabort;
+
+static int	redirect_loop;
+static int	retried;
+
+/*
+ * Determine whether the character needs encoding, per RFC2396.
+ */
+static int
+to_encode(const char *c0)
+{
+	/* 2.4.3. Excluded US-ASCII Characters */
+	const char *excluded_chars =
+	    " "		/* space */
+	    "<>#\""	/* delims (modulo "%", see below) */
+	    "{}|\\^[]`"	/* unwise */
+	    ;
+	const unsigned char *c = (const unsigned char *)c0;
+
+	/*
+	 * No corresponding graphic US-ASCII.
+	 * Control characters and octets not used in US-ASCII.
+	 */
+	return (iscntrl(*c) || !isascii(*c) ||
+
+	    /*
+	     * '%' is also reserved, if is not followed by two
+	     * hexadecimal digits.
+	     */
+	    strchr(excluded_chars, *c) != NULL ||
+	    (*c == '%' && (!isxdigit(c[1]) || !isxdigit(c[2]))));
+}
+
+/*
+ * Encode given URL, per RFC2396.
+ * Allocate and return string to the caller.
+ */
+static char *
+url_encode(const char *path)
+{
+	size_t i, length, new_length;
+	char *epath, *epathp;
+
+	length = new_length = strlen(path);
+
+	/*
+	 * First pass:
+	 * Count characters to encode and determine length of the final URL.
+	 */
+	for (i = 0; i < length; i++)
+		if (to_encode(path + i))
+			new_length += 2;
+
+	epath = epathp = malloc(new_length + 1);	/* One more for '\0'. */
+	if (epath == NULL)
+		err(1, "Can't allocate memory for URL encoding");
+
+	/*
+	 * Second pass:
+	 * Encode, and copy final URL.
+	 */
+	for (i = 0; i < length; i++)
+		if (to_encode(path + i)) {
+			snprintf(epathp, 4, "%%" "%02x",
+			    (unsigned char)path[i]);
+			epathp += 3;
+		} else
+			*(epathp++) = path[i];
+
+	*epathp = '\0';
+	return (epath);
+}
+
+/*
+ * Copy a local file (used by the OpenBSD installer).
+ * Returns -1 on failure, 0 on success
+ */
+static int
+file_get(const char *path, const char *outfile)
+{
+	struct stat	 st;
+	int		 fd, out = -1, rval = -1, save_errno;
+	volatile sig_t	 oldintr, oldinti;
+	const char	*savefile;
+	char		*buf = NULL, *cp, *pathbuf = NULL;
+	const size_t	 buflen = 128 * 1024;
+	off_t		 hashbytes;
+	ssize_t		 len, wlen;
+
+	direction = "received";
+
+	fd = open(path, O_RDONLY);
+	if (fd == -1) {
+		warn("Can't open file %s", path);
+		return -1;
+	}
+
+	if (fstat(fd, &st) == -1)
+		filesize = -1;
+	else
+		filesize = st.st_size;
+
+	if (outfile != NULL)
+		savefile = outfile;
+	else {
+		if (path[strlen(path) - 1] == '/')	/* Consider no file */
+			savefile = NULL;		/* after dir invalid. */
+		else {
+			pathbuf = strdup(path);
+			if (pathbuf == NULL)
+				errx(1, "Can't allocate memory for filename");
+			savefile = basename(pathbuf);
+		}
+	}
+
+	if (EMPTYSTRING(savefile)) {
+		warnx("No filename after directory (use -o): %s", path);
+		goto cleanup_copy;
+	}
+
+	/* Open the output file.  */
+	if (!pipeout) {
+		out = open(savefile, O_CREAT | O_WRONLY | O_TRUNC, 0666);
+		if (out == -1) {
+			warn("Can't open %s", savefile);
+			goto cleanup_copy;
+		}
+	} else
+		out = fileno(stdout);
+
+	if ((buf = malloc(buflen)) == NULL)
+		errx(1, "Can't allocate memory for transfer buffer");
+
+	/* Trap signals */
+	oldintr = NULL;
+	oldinti = NULL;
+	if (setjmp(httpabort)) {
+		if (oldintr)
+			(void)signal(SIGINT, oldintr);
+		if (oldinti)
+			(void)signal(SIGINFO, oldinti);
+		goto cleanup_copy;
+	}
+	oldintr = signal(SIGINT, aborthttp);
+
+	bytes = 0;
+	hashbytes = mark;
+	progressmeter(-1, path);
+
+	/* Finally, suck down the file. */
+	oldinti = signal(SIGINFO, psummary);
+	while ((len = read(fd, buf, buflen)) > 0) {
+		bytes += len;
+		for (cp = buf; len > 0; len -= wlen, cp += wlen) {
+			if ((wlen = write(out, cp, len)) == -1) {
+				warn("Writing %s", savefile);
+				signal(SIGINT, oldintr);
+				signal(SIGINFO, oldinti);
+				goto cleanup_copy;
+			}
+		}
+		if (hash && !progress) {
+			while (bytes >= hashbytes) {
+				(void)putc('#', ttyout);
+				hashbytes += mark;
+			}
+			(void)fflush(ttyout);
+		}
+	}
+	save_errno = errno;
+	signal(SIGINT, oldintr);
+	signal(SIGINFO, oldinti);
+	if (hash && !progress && bytes > 0) {
+		if (bytes < mark)
+			(void)putc('#', ttyout);
+		(void)putc('\n', ttyout);
+		(void)fflush(ttyout);
+	}
+	if (len == -1) {
+		warnc(save_errno, "Reading from file");
+		goto cleanup_copy;
+	}
+	progressmeter(1, NULL);
+	if (verbose)
+		ptransfer(0);
+
+	rval = 0;
+
+cleanup_copy:
+	free(buf);
+	free(pathbuf);
+	if (out >= 0 && out != fileno(stdout))
+		close(out);
+	close(fd);
+
+	return rval;
+}
+
+/*
+ * Retrieve URL, via the proxy in $proxyvar if necessary.
+ * Returns -1 on failure, 0 on success
+ */
+static int
+url_get(const char *origline, const char *proxyenv, const char *outfile, int lastfile)
+{
+	char pbuf[NI_MAXSERV], hbuf[NI_MAXHOST], *cp, *portnum, *path, ststr[4];
+	char *hosttail, *cause = "unknown", *newline, *host, *port, *buf = NULL;
+	char *epath, *redirurl, *loctail, *h, *p, gerror[200];
+	int error, isftpurl = 0, isredirect = 0, rval = -1;
+	int isunavail = 0, retryafter = -1;
+	struct addrinfo hints, *res0, *res;
+	const char *savefile;
+	char *pathbuf = NULL;
+	char *proxyurl = NULL;
+	char *credentials = NULL, *proxy_credentials = NULL;
+	int fd = -1, out = -1;
+	volatile sig_t oldintr, oldinti;
+	FILE *fin = NULL;
+	off_t hashbytes;
+	const char *errstr;
+	ssize_t len, wlen;
+	size_t bufsize;
+	char *proxyhost = NULL;
+#ifndef NOSSL
+	char *sslpath = NULL, *sslhost = NULL;
+	int ishttpsurl = 0;
+#endif /* !NOSSL */
+#ifndef SMALL
+	char *full_host = NULL;
+	const char *scheme;
+	char *locbase;
+	struct addrinfo *ares = NULL;
+	char tmbuf[32];
+	time_t mtime = 0;
+	struct stat stbuf;
+	struct tm lmt = { 0 };
+	struct timespec ts[2];
+#endif /* !SMALL */
+	tls_t tls = NULL;
+	int status;
+	int save_errno;
+	const size_t buflen = 128 * 1024;
+	int chunked = 0;
+
+	direction = "received";
+
+	newline = strdup(origline);
+	if (newline == NULL)
+		errx(1, "Can't allocate memory to parse URL");
+	if (strncasecmp(newline, HTTP_URL, sizeof(HTTP_URL) - 1) == 0) {
+		host = newline + sizeof(HTTP_URL) - 1;
+#ifndef SMALL
+		scheme = HTTP_URL;
+#endif /* !SMALL */
+	} else if (strncasecmp(newline, FTP_URL, sizeof(FTP_URL) - 1) == 0) {
+		host = newline + sizeof(FTP_URL) - 1;
+		isftpurl = 1;
+#ifndef SMALL
+		scheme = FTP_URL;
+#endif /* !SMALL */
+	} else if (strncasecmp(newline, HTTPS_URL, sizeof(HTTPS_URL) - 1) == 0) {
+#ifndef NOSSL
+		host = newline + sizeof(HTTPS_URL) - 1;
+		ishttpsurl = 1;
+#else
+		errx(1, "%s: No HTTPS support", newline);
+#endif /* !NOSSL */
+#ifndef SMALL
+		scheme = HTTPS_URL;
+#endif /* !SMALL */
+	} else
+		errx(1, "%s: URL not permitted", newline);
+
+	path = strchr(host, '/');		/* Find path */
+
+	/*
+	 * Look for auth header in host.
+	 * Basic auth from RFC 2617, valid characters for path are in
+	 * RFC 3986 section 3.3.
+	 */
+	if (!isftpurl) {
+		p = strchr(host, '@');
+		if (p != NULL && (path == NULL || p < path)) {
+			*p++ = '\0';
+			credentials = recode_credentials(host);
+
+			/* Overwrite userinfo */
+			memmove(host, p, strlen(p) + 1);
+			path = strchr(host, '/');
+		}
+	}
+
+	if (EMPTYSTRING(path)) {
+		if (outfile) {				/* No slash, but */
+			path = strchr(host,'\0');	/* we have outfile. */
+			goto noslash;
+		}
+		if (isftpurl)
+			goto noftpautologin;
+		warnx("No `/' after host (use -o): %s", origline);
+		goto cleanup_url_get;
+	}
+	*path++ = '\0';
+	if (EMPTYSTRING(path) && !outfile) {
+		if (isftpurl)
+			goto noftpautologin;
+		warnx("No filename after host (use -o): %s", origline);
+		goto cleanup_url_get;
+	}
+
+noslash:
+	if (outfile)
+		savefile = outfile;
+	else {
+		if (path[strlen(path) - 1] == '/')	/* Consider no file */
+			savefile = NULL;		/* after dir invalid. */
+		else {
+			pathbuf = strdup(path);
+			if (pathbuf == NULL)
+				errx(1, "Can't allocate memory for filename");
+			savefile = basename(pathbuf);
+		}
+	}
+
+	if (EMPTYSTRING(savefile)) {
+		if (isftpurl)
+			goto noftpautologin;
+		warnx("No filename after directory (use -o): %s", origline);
+		goto cleanup_url_get;
+	}
+
+#ifndef SMALL
+	if (resume && pipeout) {
+		warnx("can't append to stdout");
+		goto cleanup_url_get;
+	}
+#endif /* !SMALL */
+
+	if (proxyenv != NULL) {		/* use proxy */
+#ifndef NOSSL
+		if (ishttpsurl) {
+			sslpath = strdup(path);
+			sslhost = strdup(host);
+			if (! sslpath || ! sslhost)
+				errx(1, "Can't allocate memory for https path/host.");
+		}
+#endif /* !NOSSL */
+		proxyhost = strdup(host);
+		if (proxyhost == NULL)
+			errx(1, "Can't allocate memory for proxy host.");
+		proxyurl = strdup(proxyenv);
+		if (proxyurl == NULL)
+			errx(1, "Can't allocate memory for proxy URL.");
+		if (strncasecmp(proxyurl, HTTP_URL, sizeof(HTTP_URL) - 1) == 0)
+			host = proxyurl + sizeof(HTTP_URL) - 1;
+		else if (strncasecmp(proxyurl, FTP_URL, sizeof(FTP_URL) - 1) == 0)
+			host = proxyurl + sizeof(FTP_URL) - 1;
+		else {
+			warnx("Malformed proxy URL: %s", proxyenv);
+			goto cleanup_url_get;
+		}
+		if (EMPTYSTRING(host)) {
+			warnx("Malformed proxy URL: %s", proxyenv);
+			goto cleanup_url_get;
+		}
+		if (*--path == '\0')
+			*path = '/';		/* add / back to real path */
+		path = strchr(host, '/');	/* remove trailing / on host */
+		if (!EMPTYSTRING(path))
+			*path++ = '\0';		/* i guess this ++ is useless */
+
+		path = strchr(host, '@');	/* look for credentials in proxy */
+		if (!EMPTYSTRING(path)) {
+			*path = '\0';
+			if (strchr(host, ':') == NULL) {
+				warnx("Malformed proxy URL: %s", proxyenv);
+				goto cleanup_url_get;
+			}
+			proxy_credentials = recode_credentials(host);
+			*path = '@'; /* restore @ in proxyurl */
+
+			/*
+			 * This removes the password from proxyurl,
+			 * filling with stars
+			 */
+			for (host = 1 + strchr(proxyurl + 5, ':');  *host != '@';
+			    host++)
+				*host = '*';
+
+			host = path + 1;
+		}
+
+		path = newline;
+	}
+
+	if (*host == '[' && (hosttail = strrchr(host, ']')) != NULL &&
+	    (hosttail[1] == '\0' || hosttail[1] == ':')) {
+		host++;
+		*hosttail++ = '\0';
+#ifndef SMALL
+		if (asprintf(&full_host, "[%s]", host) == -1)
+			errx(1, "Cannot allocate memory for hostname");
+#endif /* !SMALL */
+	} else
+		hosttail = host;
+
+	portnum = strrchr(hosttail, ':');		/* find portnum */
+	if (portnum != NULL)
+		*portnum++ = '\0';
+#ifndef NOSSL
+	port = portnum ? portnum : (ishttpsurl ? httpsport : httpport);
+#else /* !NOSSL */
+	port = portnum ? portnum : httpport;
+#endif /* !NOSSL */
+
+#ifndef SMALL
+	if (full_host == NULL)
+		if ((full_host = strdup(host)) == NULL)
+			errx(1, "Cannot allocate memory for hostname");
+	if (debug)
+		fprintf(ttyout, "host %s, port %s, path %s, "
+		    "save as %s, auth %s.\n", host, port, path,
+		    savefile, credentials ? credentials : "none");
+#endif /* !SMALL */
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = family;
+	hints.ai_socktype = SOCK_STREAM;
+	error = getaddrinfo(host, port, &hints, &res0);
+	/*
+	 * If the services file is corrupt/missing, fall back
+	 * on our hard-coded defines.
+	 */
+	if (error == EAI_SERVICE && port == httpport) {
+		snprintf(pbuf, sizeof(pbuf), "%d", HTTP_PORT);
+		error = getaddrinfo(host, pbuf, &hints, &res0);
+#ifndef NOSSL
+	} else if (error == EAI_SERVICE && port == httpsport) {
+		snprintf(pbuf, sizeof(pbuf), "%d", HTTPS_PORT);
+		error = getaddrinfo(host, pbuf, &hints, &res0);
+#endif /* !NOSSL */
+	}
+	if (error) {
+		warnx("%s: %s", host, gai_strerror(error));
+		goto cleanup_url_get;
+	}
+
+#ifndef SMALL
+	if (srcaddr) {
+		hints.ai_flags |= AI_NUMERICHOST;
+		error = getaddrinfo(srcaddr, NULL, &hints, &ares);
+		if (error) {
+			warnx("%s: %s", srcaddr, gai_strerror(error));
+			goto cleanup_url_get;
+		}
+	}
+#endif /* !SMALL */
+
+	/* ensure consistent order of the output */
+	if (verbose)
+		setvbuf(ttyout, NULL, _IOLBF, 0);
+
+	fd = -1;
+	for (res = res0; res; res = res->ai_next) {
+		if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf,
+		    sizeof(hbuf), NULL, 0, NI_NUMERICHOST) != 0)
+			strlcpy(hbuf, "(unknown)", sizeof(hbuf));
+		if (verbose)
+			fprintf(ttyout, "Trying %s...\n", hbuf);
+
+		fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+		if (fd == -1) {
+			cause = "socket";
+			continue;
+		}
+
+#ifndef SMALL
+		if (srcaddr) {
+			if (ares->ai_family != res->ai_family) {
+				close(fd);
+				fd = -1;
+				errno = EINVAL;
+				cause = "bind";
+				continue;
+			}
+			if (bind(fd, ares->ai_addr, ares->ai_addrlen) == -1) {
+				save_errno = errno;
+				close(fd);
+				errno = save_errno;
+				fd = -1;
+				cause = "bind";
+				continue;
+			}
+		}
+#endif /* !SMALL */
+
+		error = timed_connect(fd, res->ai_addr, res->ai_addrlen,
+		    connect_timeout);
+		if (error != 0) {
+			save_errno = errno;
+			close(fd);
+			errno = save_errno;
+			fd = -1;
+			cause = "connect";
+			continue;
+		}
+
+		/* get port in numeric */
+		if (getnameinfo(res->ai_addr, res->ai_addrlen, NULL, 0,
+		    pbuf, sizeof(pbuf), NI_NUMERICSERV) == 0)
+			port = pbuf;
+		else
+			port = NULL;
+
+#ifndef NOSSL
+		if (proxyenv && sslhost)
+			proxy_connect(fd, sslhost, proxy_credentials);
+#endif /* !NOSSL */
+		break;
+	}
+	freeaddrinfo(res0);
+#ifndef SMALL
+	if (srcaddr)
+		freeaddrinfo(ares);
+#endif /* !SMALL */
+	if (fd < 0) {
+		warn("%s", cause);
+		goto cleanup_url_get;
+	}
+
+#ifndef NOSSL
+	if (ishttpsurl) {
+		ssize_t ret;
+		if (proxyenv && sslpath) {
+			ishttpsurl = 0;
+			proxyurl = NULL;
+			path = sslpath;
+		}
+		if (sslhost == NULL) {
+			sslhost = strdup(host);
+			if (sslhost == NULL)
+				errx(1, "Can't allocate memory for https host.");
+		}
+		if ((tls = tls_client()) == NULL) {
+			fprintf(ttyout, "failed to create SSL client\n");
+			goto cleanup_url_get;
+		}
+		if (tls_configure(tls, tls_config) != 0) {
+			fprintf(ttyout, "TLS configuration failure: %s\n",
+			    tls_error(tls));
+			goto cleanup_url_get;
+		}
+		if (tls_connect_socket(tls, fd, sslhost) != 0) {
+			fprintf(ttyout, "TLS connect failure: %s\n", tls_error(tls));
+			goto cleanup_url_get;
+		}
+		do {
+			ret = tls_handshake(tls);
+		} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+		if (ret != 0) {
+			fprintf(ttyout, "TLS handshake failure: %s\n", tls_error(tls));
+			goto cleanup_url_get;
+		}
+		fin = funopen(tls, stdio_tls_read_wrapper,
+		    stdio_tls_write_wrapper, NULL, NULL);
+	} else {
+		fin = fdopen(fd, "r+");
+		fd = -1;
+	}
+#else /* !NOSSL */
+	fin = fdopen(fd, "r+");
+	fd = -1;
+#endif /* !NOSSL */
+
+#ifdef SMALL
+	if (lastfile) {
+		if (pipeout) {
+			if (pledge("stdio rpath inet dns tty",  NULL) == -1)
+				err(1, "pledge");
+		} else {
+			if (pledge("stdio rpath wpath cpath inet dns tty", NULL) == -1)
+				err(1, "pledge");
+		}
+	}
+#endif
+
+	/*
+	 * Construct and send the request. Proxy requests don't want leading /.
+	 */
+#ifndef NOSSL
+	cookie_get(host, path, ishttpsurl, &buf);
+#endif /* !NOSSL */
+
+	epath = url_encode(path);
+	if (proxyurl) {
+		if (verbose) {
+			fprintf(ttyout, "Requesting %s (via %s)\n",
+			    origline, proxyurl);
+		}
+		/*
+		 * Host: directive must use the destination host address for
+		 * the original URI (path).
+		 */
+		ftp_printf(fin, "GET %s HTTP/1.1\r\n"
+		    "Connection: close\r\n"
+		    "Host: %s\r\n%s%s\r\n"
+		    "Accept: */*\r\n",
+		    epath, proxyhost, buf ? buf : "", httpuseragent);
+		if (credentials)
+			ftp_printf(fin, "Authorization: Basic %s\r\n",
+			    credentials);
+		if (proxy_credentials)
+			ftp_printf(fin, "Proxy-Authorization: Basic %s\r\n",
+			    proxy_credentials);
+		ftp_printf(fin, "\r\n");
+	} else {
+		if (verbose)
+			fprintf(ttyout, "Requesting %s\n", origline);
+#ifndef SMALL
+		if (resume || timestamp) {
+			if (stat(savefile, &stbuf) == 0) {
+				if (resume)
+					restart_point = stbuf.st_size;
+				if (timestamp)
+					mtime = stbuf.st_mtime;
+			} else {
+				restart_point = 0;
+				mtime = 0;
+			}
+		}
+#endif	/* SMALL */
+		ftp_printf(fin,
+		    "GET /%s HTTP/1.1\r\n"
+		    "Connection: close\r\n"
+		    "Host: ", epath);
+		if (proxyhost) {
+			ftp_printf(fin, "%s", proxyhost);
+			port = NULL;
+		} else if (strchr(host, ':')) {
+			/*
+			 * strip off scoped address portion, since it's
+			 * local to node
+			 */
+			h = strdup(host);
+			if (h == NULL)
+				errx(1, "Can't allocate memory.");
+			if ((p = strchr(h, '%')) != NULL)
+				*p = '\0';
+			ftp_printf(fin, "[%s]", h);
+			free(h);
+		} else
+			ftp_printf(fin, "%s", host);
+
+		/*
+		 * Send port number only if it's specified and does not equal
+		 * 80. Some broken HTTP servers get confused if you explicitly
+		 * send them the port number.
+		 */
+#ifndef NOSSL
+		if (port && strcmp(port, (ishttpsurl ? "443" : "80")) != 0)
+			ftp_printf(fin, ":%s", port);
+		if (restart_point)
+			ftp_printf(fin, "\r\nRange: bytes=%lld-",
+				(long long)restart_point);
+#else /* !NOSSL */
+		if (port && strcmp(port, "80") != 0)
+			ftp_printf(fin, ":%s", port);
+#endif /* !NOSSL */
+
+#ifndef SMALL
+		if (mtime && (http_time(mtime, tmbuf, sizeof(tmbuf)) != 0))
+			ftp_printf(fin, "\r\nIf-Modified-Since: %s", tmbuf);
+#endif /* SMALL */
+
+		ftp_printf(fin, "\r\n%s%s\r\n", buf ? buf : "", httpuseragent);
+		ftp_printf(fin, "Accept: */*\r\n");
+		if (credentials)
+			ftp_printf(fin, "Authorization: Basic %s\r\n",
+			    credentials);
+		ftp_printf(fin, "\r\n");
+	}
+	free(epath);
+
+#ifndef NOSSL
+	free(buf);
+#endif /* !NOSSL */
+	buf = NULL;
+	bufsize = 0;
+
+	if (fflush(fin) == EOF) {
+		warnx("Writing HTTP request: %s", sockerror(tls));
+		goto cleanup_url_get;
+	}
+	if ((len = getline(&buf, &bufsize, fin)) == -1) {
+		warnx("Receiving HTTP reply: %s", sockerror(tls));
+		goto cleanup_url_get;
+	}
+
+	while (len > 0 && (buf[len-1] == '\r' || buf[len-1] == '\n'))
+		buf[--len] = '\0';
+#ifndef SMALL
+	if (debug)
+		fprintf(ttyout, "received '%s'\n", buf);
+#endif /* !SMALL */
+
+	cp = strchr(buf, ' ');
+	if (cp == NULL)
+		goto improper;
+	else
+		cp++;
+
+	strlcpy(ststr, cp, sizeof(ststr));
+	status = strtonum(ststr, 200, 503, &errstr);
+	if (errstr) {
+		strnvis(gerror, cp, sizeof gerror, VIS_SAFE);
+		warnx("Error retrieving %s: %s", origline, gerror);
+		goto cleanup_url_get;
+	}
+
+	switch (status) {
+	case 200:	/* OK */
+#ifndef SMALL
+		/*
+		 * When we request a partial file, and we receive an HTTP 200
+		 * it is a good indication that the server doesn't support
+		 * range requests, and is about to send us the entire file.
+		 * If the restart_point == 0, then we are not actually
+		 * requesting a partial file, and an HTTP 200 is appropriate.
+		 */
+		if (resume && restart_point != 0) {
+			warnx("Server does not support resume.");
+			restart_point = resume = 0;
+		}
+		/* FALLTHROUGH */
+	case 206:	/* Partial Content */
+#endif /* !SMALL */
+		break;
+	case 301:	/* Moved Permanently */
+	case 302:	/* Found */
+	case 303:	/* See Other */
+	case 307:	/* Temporary Redirect */
+	case 308:	/* Permanent Redirect (RFC 7538) */
+		isredirect++;
+		if (redirect_loop++ > 10) {
+			warnx("Too many redirections requested");
+			goto cleanup_url_get;
+		}
+		break;
+#ifndef SMALL
+	case 304:	/* Not Modified */
+		warnx("File is not modified on the server");
+		goto cleanup_url_get;
+	case 416:	/* Requested Range Not Satisfiable */
+		warnx("File is already fully retrieved.");
+		goto cleanup_url_get;
+#endif /* !SMALL */
+	case 503:
+		isunavail = 1;
+		break;
+	default:
+		strnvis(gerror, cp, sizeof gerror, VIS_SAFE);
+		warnx("Error retrieving %s: %s", origline, gerror);
+		goto cleanup_url_get;
+	}
+
+	/*
+	 * Read the rest of the header.
+	 */
+	filesize = -1;
+
+	for (;;) {
+		if ((len = getline(&buf, &bufsize, fin)) == -1) {
+			warnx("Receiving HTTP reply: %s", sockerror(tls));
+			goto cleanup_url_get;
+		}
+
+		while (len > 0 && (buf[len-1] == '\r' || buf[len-1] == '\n' ||
+		    buf[len-1] == ' ' || buf[len-1] == '\t'))
+			buf[--len] = '\0';
+		if (len == 0)
+			break;
+#ifndef SMALL
+		if (debug)
+			fprintf(ttyout, "received '%s'\n", buf);
+#endif /* !SMALL */
+
+		/* Look for some headers */
+		cp = buf;
+#define CONTENTLEN "Content-Length:"
+		if (strncasecmp(cp, CONTENTLEN, sizeof(CONTENTLEN) - 1) == 0) {
+			cp += sizeof(CONTENTLEN) - 1;
+			cp += strspn(cp, " \t");
+			filesize = strtonum(cp, 0, LLONG_MAX, &errstr);
+			if (errstr != NULL)
+				goto improper;
+#ifndef SMALL
+			if (restart_point)
+				filesize += restart_point;
+#endif /* !SMALL */
+#define LOCATION "Location:"
+		} else if (isredirect &&
+		    strncasecmp(cp, LOCATION, sizeof(LOCATION) - 1) == 0) {
+			cp += sizeof(LOCATION) - 1;
+			cp += strspn(cp, " \t");
+			/*
+			 * If there is a colon before the first slash, this URI
+			 * is not relative. RFC 3986 4.2
+			 */
+			if (cp[strcspn(cp, ":/")] != ':') {
+#ifdef SMALL
+				errx(1, "Relative redirect not supported");
+#else /* SMALL */
+				/* XXX doesn't handle protocol-relative URIs */
+				if (*cp == '/') {
+					locbase = NULL;
+					cp++;
+				} else {
+					locbase = strdup(path);
+					if (locbase == NULL)
+						errx(1, "Can't allocate memory"
+						    " for location base");
+					loctail = strchr(locbase, '#');
+					if (loctail != NULL)
+						*loctail = '\0';
+					loctail = strchr(locbase, '?');
+					if (loctail != NULL)
+						*loctail = '\0';
+					loctail = strrchr(locbase, '/');
+					if (loctail == NULL) {
+						free(locbase);
+						locbase = NULL;
+					} else
+						loctail[1] = '\0';
+				}
+				/* Construct URL from relative redirect */
+				if (asprintf(&redirurl, "%s%s%s%s/%s%s",
+				    scheme, full_host,
+				    portnum ? ":" : "",
+				    portnum ? portnum : "",
+				    locbase ? locbase : "",
+				    cp) == -1)
+					errx(1, "Cannot build "
+					    "redirect URL");
+				free(locbase);
+#endif /* SMALL */
+			} else if ((redirurl = strdup(cp)) == NULL)
+				errx(1, "Cannot allocate memory for URL");
+			loctail = strchr(redirurl, '#');
+			if (loctail != NULL)
+				*loctail = '\0';
+			if (verbose) {
+				char *visbuf;
+				if (stravis(&visbuf, redirurl, VIS_SAFE) == -1)
+					err(1, "Cannot vis redirect URL");
+				fprintf(ttyout, "Redirected to %s\n", visbuf);
+				free(visbuf);
+			}
+			ftp_close(&fin, &tls, &fd);
+			rval = url_get(redirurl, proxyenv, savefile, lastfile);
+			free(redirurl);
+			goto cleanup_url_get;
+#define RETRYAFTER "Retry-After:"
+		} else if (isunavail &&
+		    strncasecmp(cp, RETRYAFTER, sizeof(RETRYAFTER) - 1) == 0) {
+			cp += sizeof(RETRYAFTER) - 1;
+			cp += strspn(cp, " \t");
+			retryafter = strtonum(cp, 0, 0, &errstr);
+			if (errstr != NULL)
+				retryafter = -1;
+#define TRANSFER_ENCODING "Transfer-Encoding:"
+		} else if (strncasecmp(cp, TRANSFER_ENCODING,
+			    sizeof(TRANSFER_ENCODING) - 1) == 0) {
+			cp += sizeof(TRANSFER_ENCODING) - 1;
+			cp += strspn(cp, " \t");
+			if (strcasecmp(cp, "chunked") == 0)
+				chunked = 1;
+#ifndef SMALL
+#define LAST_MODIFIED "Last-Modified:"
+		} else if (strncasecmp(cp, LAST_MODIFIED,
+			    sizeof(LAST_MODIFIED) - 1) == 0) {
+			cp += sizeof(LAST_MODIFIED) - 1;
+			cp += strspn(cp, " \t");
+			if (strptime(cp, "%a, %d %h %Y %T %Z", &lmt) == NULL)
+				server_timestamps = 0;
+#endif /* !SMALL */
+		}
+	}
+	free(buf);
+	buf = NULL;
+
+	/* Content-Length should be ignored for Transfer-Encoding: chunked */
+	if (chunked)
+		filesize = -1;
+
+	if (isunavail) {
+		if (retried || retryafter != 0)
+			warnx("Error retrieving %s: 503 Service Unavailable",
+			    origline);
+		else {
+			if (verbose)
+				fprintf(ttyout, "Retrying %s\n", origline);
+			retried = 1;
+			ftp_close(&fin, &tls, &fd);
+			rval = url_get(origline, proxyenv, savefile, lastfile);
+		}
+		goto cleanup_url_get;
+	}
+
+	/* Open the output file.  */
+	if (!pipeout) {
+#ifndef SMALL
+		if (resume)
+			out = open(savefile, O_CREAT | O_WRONLY | O_APPEND,
+				0666);
+		else
+#endif /* !SMALL */
+			out = open(savefile, O_CREAT | O_WRONLY | O_TRUNC,
+				0666);
+		if (out == -1) {
+			warn("Can't open %s", savefile);
+			goto cleanup_url_get;
+		}
+	} else {
+		out = fileno(stdout);
+#ifdef SMALL
+		if (lastfile) {
+			if (pledge("stdio tty", NULL) == -1)
+				err(1, "pledge");
+		}
+#endif
+	}
+
+	if ((buf = malloc(buflen)) == NULL)
+		errx(1, "Can't allocate memory for transfer buffer");
+
+	/* Trap signals */
+	oldintr = NULL;
+	oldinti = NULL;
+	if (setjmp(httpabort)) {
+		if (oldintr)
+			(void)signal(SIGINT, oldintr);
+		if (oldinti)
+			(void)signal(SIGINFO, oldinti);
+		goto cleanup_url_get;
+	}
+	oldintr = signal(SIGINT, aborthttp);
+
+	bytes = 0;
+	hashbytes = mark;
+	progressmeter(-1, path);
+
+	/* Finally, suck down the file. */
+	oldinti = signal(SIGINFO, psummary);
+	if (chunked) {
+		error = save_chunked(fin, tls, out, buf, buflen);
+		signal(SIGINT, oldintr);
+		signal(SIGINFO, oldinti);
+		if (error == -1)
+			goto cleanup_url_get;
+	} else {
+		while ((len = fread(buf, 1, buflen, fin)) > 0) {
+			bytes += len;
+			for (cp = buf; len > 0; len -= wlen, cp += wlen) {
+				if ((wlen = write(out, cp, len)) == -1) {
+					warn("Writing %s", savefile);
+					signal(SIGINT, oldintr);
+					signal(SIGINFO, oldinti);
+					goto cleanup_url_get;
+				}
+			}
+			if (hash && !progress) {
+				while (bytes >= hashbytes) {
+					(void)putc('#', ttyout);
+					hashbytes += mark;
+				}
+				(void)fflush(ttyout);
+			}
+		}
+		save_errno = errno;
+		signal(SIGINT, oldintr);
+		signal(SIGINFO, oldinti);
+		if (hash && !progress && bytes > 0) {
+			if (bytes < mark)
+				(void)putc('#', ttyout);
+			(void)putc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (len == 0 && ferror(fin)) {
+			errno = save_errno;
+			warnx("Reading from socket: %s", sockerror(tls));
+			goto cleanup_url_get;
+		}
+	}
+	progressmeter(1, NULL);
+	if (
+#ifndef SMALL
+		!resume &&
+#endif /* !SMALL */
+		filesize != -1 && len == 0 && bytes != filesize) {
+		if (verbose)
+			fputs("Read short file.\n", ttyout);
+		goto cleanup_url_get;
+	}
+
+	if (verbose)
+		ptransfer(0);
+
+	rval = 0;
+	goto cleanup_url_get;
+
+noftpautologin:
+	warnx(
+	    "Auto-login using ftp URLs isn't supported when using $ftp_proxy");
+	goto cleanup_url_get;
+
+improper:
+	warnx("Improper response from %s", host);
+
+cleanup_url_get:
+#ifndef SMALL
+	free(full_host);
+#endif /* !SMALL */
+#ifndef NOSSL
+	free(sslhost);
+#endif /* !NOSSL */
+	ftp_close(&fin, &tls, &fd);
+	if (out >= 0 && out != fileno(stdout)) {
+#ifndef SMALL
+		if (server_timestamps && lmt.tm_zone != NULL &&
+		    fstat(out, &stbuf) == 0 && S_ISREG(stbuf.st_mode) != 0) {
+			ts[0].tv_nsec = UTIME_NOW;
+			ts[1].tv_nsec = 0;
+			setenv("TZ", lmt.tm_zone, 1);
+			if (((ts[1].tv_sec = mktime(&lmt)) != -1) &&
+			    (futimens(out, ts) == -1))
+				warnx("Unable to set file modification time");
+		}
+#endif /* !SMALL */
+		close(out);
+	}
+	free(buf);
+	free(pathbuf);
+	free(proxyhost);
+	free(proxyurl);
+	free(newline);
+	free(credentials);
+	free(proxy_credentials);
+	return (rval);
+}
+
+static int
+save_chunked(FILE *fin, tls_t tls, int out, char *buf, size_t buflen)
+{
+
+	char			*header = NULL, *end, *cp;
+	unsigned long		chunksize;
+	size_t			hsize = 0, rlen, wlen;
+	ssize_t			written;
+	char			cr, lf;
+
+	for (;;) {
+		if (getline(&header, &hsize, fin) == -1)
+			break;
+		/* strip CRLF and any optional chunk extension */
+		header[strcspn(header, "; \t\r\n")] = '\0';
+		errno = 0;
+		chunksize = strtoul(header, &end, 16);
+		if (errno || header[0] == '\0' || *end != '\0' ||
+		    chunksize > INT_MAX) {
+			warnx("Invalid chunk size '%s'", header);
+			free(header);
+			return -1;
+		}
+
+		if (chunksize == 0) {
+			/* We're done.  Ignore optional trailer. */
+			free(header);
+			return 0;
+		}
+
+		for (written = 0; chunksize != 0; chunksize -= rlen) {
+			rlen = (chunksize < buflen) ? chunksize : buflen;
+			rlen = fread(buf, 1, rlen, fin);
+			if (rlen == 0)
+				break;
+			bytes += rlen;
+			for (cp = buf, wlen = rlen; wlen > 0;
+			    wlen -= written, cp += written) {
+				if ((written = write(out, cp, wlen)) == -1) {
+					warn("Writing output file");
+					free(header);
+					return -1;
+				}
+			}
+		}
+
+		if (rlen == 0 ||
+		    fread(&cr, 1, 1, fin) != 1 ||
+		    fread(&lf, 1, 1, fin) != 1)
+			break;
+
+		if (cr != '\r' || lf != '\n') {
+			warnx("Invalid chunked encoding");
+			free(header);
+			return -1;
+		}
+	}
+	free(header);
+
+	if (ferror(fin))
+		warnx("Error while reading from socket: %s", sockerror(tls));
+	else
+		warnx("Invalid chunked encoding: short read");
+
+	return -1;
+}
+
+/*
+ * Abort a http retrieval
+ */
+static void
+aborthttp(int signo)
+{
+	const char errmsg[] = "\nfetch aborted.\n";
+
+	write(fileno(ttyout), errmsg, sizeof(errmsg) - 1);
+	longjmp(httpabort, 1);
+}
+
+/*
+ * Retrieve multiple files from the command line, transferring
+ * files of the form "host:path", "ftp://host/path" using the
+ * ftp protocol, and files of the form "http://host/path" using
+ * the http protocol.
+ * If path has a trailing "/", then return (-1);
+ * the path will be cd-ed into and the connection remains open,
+ * and the function will return -1 (to indicate the connection
+ * is alive).
+ * If an error occurs the return value will be the offset+1 in
+ * argv[] of the file that caused a problem (i.e, argv[x]
+ * returns x+1)
+ * Otherwise, 0 is returned if all files retrieved successfully.
+ */
+int
+auto_fetch(int argc, char *argv[], char *outfile)
+{
+	char *xargv[5];
+	char *cp, *url, *host, *dir, *file, *portnum;
+	char *username, *pass, *pathstart;
+	char *ftpproxy, *httpproxy;
+	int rval, xargc, lastfile;
+	volatile int argpos;
+	int dirhasglob, filehasglob, oautologin;
+	char rempath[PATH_MAX];
+
+	argpos = 0;
+
+	if (setjmp(toplevel)) {
+		if (connected)
+			disconnect(0, NULL);
+		return (argpos + 1);
+	}
+	(void)signal(SIGINT, (sig_t)intr);
+	(void)signal(SIGPIPE, (sig_t)lostpeer);
+
+	if ((ftpproxy = getenv(FTP_PROXY)) != NULL && *ftpproxy == '\0')
+		ftpproxy = NULL;
+	if ((httpproxy = getenv(HTTP_PROXY)) != NULL && *httpproxy == '\0')
+		httpproxy = NULL;
+
+	/*
+	 * Loop through as long as there's files to fetch.
+	 */
+	url = username = pass = NULL;
+	for (rval = 0; (rval == 0) && (argpos < argc); argpos++) {
+		if (strchr(argv[argpos], ':') == NULL) {
+			warnx("No colon in URL: %s", argv[argpos]);
+			rval = argpos + 1;
+			continue;
+		}
+
+		free(url);
+		free(username);
+		free(pass);
+		url = username = pass = host = portnum = dir = file = NULL;
+
+		lastfile = (argv[argpos+1] == NULL);
+
+		/*
+		 * We muck with the string, so we make a copy.
+		 */
+		url = strdup(argv[argpos]);
+		if (url == NULL)
+			errx(1, "Can't allocate memory for auto-fetch.");
+
+		if (strncasecmp(url, FILE_URL, sizeof(FILE_URL) - 1) == 0) {
+			if (file_get(url + sizeof(FILE_URL) - 1, outfile) == -1)
+				rval = argpos + 1;
+			continue;
+		}
+
+		/*
+		 * Try HTTP URL-style arguments next.
+		 */
+		if (strncasecmp(url, HTTP_URL, sizeof(HTTP_URL) - 1) == 0 ||
+		    strncasecmp(url, HTTPS_URL, sizeof(HTTPS_URL) -1) == 0) {
+			redirect_loop = 0;
+			retried = 0;
+			if (url_get(url, httpproxy, outfile, lastfile) == -1)
+				rval = argpos + 1;
+			continue;
+		}
+
+		/*
+		 * Try FTP URL-style arguments next. If ftpproxy is
+		 * set, use url_get() instead of standard ftp.
+		 * Finally, try host:file.
+		 */
+		host = url;
+		if (strncasecmp(url, FTP_URL, sizeof(FTP_URL) - 1) == 0) {
+			char *passend, *passagain, *userend;
+
+			if (ftpproxy) {
+				if (url_get(url, ftpproxy, outfile, lastfile) == -1)
+					rval = argpos + 1;
+				continue;
+			}
+			host += sizeof(FTP_URL) - 1;
+			dir = strchr(host, '/');
+
+			/* Look for [user:pass@]host[:port] */
+
+			/* check if we have "user:pass@" */
+			userend = strchr(host, ':');
+			passend = strchr(host, '@');
+			if (passend && userend && userend < passend &&
+			    (!dir || passend < dir)) {
+				username = host;
+				pass = userend + 1;
+				host = passend + 1;
+				*userend = *passend = '\0';
+				passagain = strchr(host, '@');
+				if (strchr(pass, '@') != NULL ||
+				    (passagain != NULL && passagain < dir)) {
+					warnx(at_encoding_warning);
+					username = pass = NULL;
+					goto bad_ftp_url;
+				}
+
+				if (EMPTYSTRING(username)) {
+bad_ftp_url:
+					warnx("Invalid URL: %s", argv[argpos]);
+					rval = argpos + 1;
+					username = pass = NULL;
+					continue;
+				}
+				username = urldecode(username);
+				pass = urldecode(pass);
+			}
+
+			/* check [host]:port, or [host] */
+			if (host[0] == '[') {
+				cp = strchr(host, ']');
+				if (cp && (!dir || cp < dir)) {
+					if (cp + 1 == dir || cp[1] == ':') {
+						host++;
+						*cp++ = '\0';
+					} else
+						cp = NULL;
+				} else
+					cp = host;
+			} else
+				cp = host;
+
+			/* split off host[:port] if there is */
+			if (cp) {
+				portnum = strchr(cp, ':');
+				pathstart = strchr(cp, '/');
+				/* : in path is not a port # indicator */
+				if (portnum && pathstart &&
+				    pathstart < portnum)
+					portnum = NULL;
+
+				if (!portnum)
+					;
+				else {
+					if (!dir)
+						;
+					else if (portnum + 1 < dir) {
+						*portnum++ = '\0';
+						/*
+						 * XXX should check if portnum
+						 * is decimal number
+						 */
+					} else {
+						/* empty portnum */
+						goto bad_ftp_url;
+					}
+				}
+			} else
+				portnum = NULL;
+		} else {			/* classic style `host:file' */
+			dir = strchr(host, ':');
+		}
+
+		/*
+		 * If dir is NULL, the file wasn't specified
+		 * (URL looked something like ftp://host)
+		 */
+		if (dir != NULL)
+			*dir++ = '\0';
+
+		if (EMPTYSTRING(host)) {
+			warnx("No host name in URL: %s", argv[argpos]);
+			rval = argpos + 1;
+			continue;
+		}
+
+		/*
+		 * Extract the file and (if present) directory name.
+		 */
+		if (!EMPTYSTRING(dir)) {
+			cp = strrchr(dir, '/');
+			if (cp != NULL) {
+				*cp++ = '\0';
+				file = cp;
+			} else {
+				file = dir;
+				dir = NULL;
+			}
+		}
+#ifndef SMALL
+		if (debug)
+			fprintf(ttyout,
+			    "user %s:%s host %s port %s dir %s file %s\n",
+			    username, pass ? "XXXX" : NULL, host, portnum,
+			    dir, file);
+#endif /* !SMALL */
+
+		/*
+		 * Set up the connection.
+		 */
+		if (connected)
+			disconnect(0, NULL);
+		xargv[0] = __progname;
+		xargv[1] = host;
+		xargv[2] = NULL;
+		xargc = 2;
+		if (!EMPTYSTRING(portnum)) {
+			xargv[2] = portnum;
+			xargv[3] = NULL;
+			xargc = 3;
+		}
+		oautologin = autologin;
+		if (username == NULL)
+			anonftp = 1;
+		else {
+			anonftp = 0;
+			autologin = 0;
+		}
+		setpeer(xargc, xargv);
+		autologin = oautologin;
+		if (connected == 0 ||
+		    (connected == 1 && autologin && (username == NULL ||
+		    !ftp_login(host, username, pass)))) {
+			warnx("Can't connect or login to host `%s'", host);
+			rval = argpos + 1;
+			continue;
+		}
+
+		/* Always use binary transfers. */
+		setbinary(0, NULL);
+
+		dirhasglob = filehasglob = 0;
+		if (doglob) {
+			if (!EMPTYSTRING(dir) &&
+			    strpbrk(dir, "*?[]{}") != NULL)
+				dirhasglob = 1;
+			if (!EMPTYSTRING(file) &&
+			    strpbrk(file, "*?[]{}") != NULL)
+				filehasglob = 1;
+		}
+
+		/* Change directories, if necessary. */
+		if (!EMPTYSTRING(dir) && !dirhasglob) {
+			xargv[0] = "cd";
+			xargv[1] = dir;
+			xargv[2] = NULL;
+			cd(2, xargv);
+			if (!dirchange) {
+				rval = argpos + 1;
+				continue;
+			}
+		}
+
+		if (EMPTYSTRING(file)) {
+#ifndef SMALL
+			rval = -1;
+#else /* !SMALL */
+			recvrequest("NLST", "-", NULL, "w", 0, 0);
+			rval = 0;
+#endif /* !SMALL */
+			continue;
+		}
+
+		if (verbose)
+			fprintf(ttyout, "Retrieving %s/%s\n", dir ? dir : "", file);
+
+		if (dirhasglob) {
+			snprintf(rempath, sizeof(rempath), "%s/%s", dir, file);
+			file = rempath;
+		}
+
+		/* Fetch the file(s). */
+		xargc = 2;
+		xargv[0] = "get";
+		xargv[1] = file;
+		xargv[2] = NULL;
+		if (dirhasglob || filehasglob) {
+			int ointeractive;
+
+			ointeractive = interactive;
+			interactive = 0;
+			xargv[0] = "mget";
+#ifndef SMALL
+			if (resume) {
+				xargc = 3;
+				xargv[1] = "-c";
+				xargv[2] = file;
+				xargv[3] = NULL;
+			}
+#endif /* !SMALL */
+			mget(xargc, xargv);
+			interactive = ointeractive;
+		} else {
+			if (outfile != NULL) {
+				xargv[2] = outfile;
+				xargv[3] = NULL;
+				xargc++;
+			}
+#ifndef SMALL
+			if (resume)
+				reget(xargc, xargv);
+			else
+#endif /* !SMALL */
+				get(xargc, xargv);
+		}
+
+		if ((code / 100) != COMPLETE)
+			rval = argpos + 1;
+	}
+	free(url);
+	free(username);
+	free(pass);
+	if (connected && rval != -1)
+		disconnect(0, NULL);
+	return (rval);
+}
+
+char *
+urldecode(const char *str)
+{
+	char *ret, c;
+	int i, reallen;
+
+	if (str == NULL)
+		return NULL;
+	if ((ret = malloc(strlen(str) + 1)) == NULL)
+		err(1, "Can't allocate memory for URL decoding");
+	for (i = 0, reallen = 0; str[i] != '\0'; i++, reallen++, ret++) {
+		c = str[i];
+		if (c == '+') {
+			*ret = ' ';
+			continue;
+		}
+
+		/* Cannot use strtol here because next char
+		 * after %xx may be a digit.
+		 */
+		if (c == '%' && isxdigit((unsigned char)str[i + 1]) &&
+		    isxdigit((unsigned char)str[i + 2])) {
+			*ret = hextochar(&str[i + 1]);
+			i += 2;
+			continue;
+		}
+		*ret = c;
+	}
+	*ret = '\0';
+
+	return ret - reallen;
+}
+
+static char *
+recode_credentials(const char *userinfo)
+{
+	char *ui, *creds;
+	size_t ulen, credsize;
+
+	/* url-decode the user and pass */
+	ui = urldecode(userinfo);
+
+	ulen = strlen(ui);
+	credsize = (ulen + 2) / 3 * 4 + 1;
+	creds = malloc(credsize);
+	if (creds == NULL)
+		errx(1, "out of memory");
+	if (b64_ntop((u_char const *)ui, ulen, creds, credsize) == -1)
+		errx(1, "error in base64 encoding");
+	free(ui);
+	return (creds);
+}
+
+static char
+hextochar(const char *str)
+{
+	unsigned char c, ret;
+
+	c = str[0];
+	ret = c;
+	if (isalpha(c))
+		ret -= isupper(c) ? 'A' - 10 : 'a' - 10;
+	else
+		ret -= '0';
+	ret *= 16;
+
+	c = str[1];
+	ret += c;
+	if (isalpha(c))
+		ret -= isupper(c) ? 'A' - 10 : 'a' - 10;
+	else
+		ret -= '0';
+	return ret;
+}
+
+int
+isurl(const char *p)
+{
+
+	if (strncasecmp(p, FTP_URL, sizeof(FTP_URL) - 1) == 0 ||
+	    strncasecmp(p, HTTP_URL, sizeof(HTTP_URL) - 1) == 0 ||
+#ifndef NOSSL
+	    strncasecmp(p, HTTPS_URL, sizeof(HTTPS_URL) - 1) == 0 ||
+#endif /* !NOSSL */
+	    strncasecmp(p, FILE_URL, sizeof(FILE_URL) - 1) == 0 ||
+	    strstr(p, ":/"))
+		return (1);
+	return (0);
+}
+
+#ifndef SMALL
+static int
+ftp_printf(FILE *fp, const char *fmt, ...)
+{
+	va_list	ap;
+	int	ret;
+
+	va_start(ap, fmt);
+	ret = vfprintf(fp, fmt, ap);
+	va_end(ap);
+
+	if (debug) {
+		va_start(ap, fmt);
+		vfprintf(ttyout, fmt, ap);
+		va_end(ap);
+	}
+
+	return ret;
+}
+#endif /* !SMALL */
+
+static void
+ftp_close(FILE **fin, tls_t *tls, int *fd)
+{
+#ifndef NOSSL
+	int	ret;
+
+	if (*tls != NULL) {
+		if (tls_session_fd != -1)
+			dprintf(STDERR_FILENO, "tls session resumed: %s\n",
+			    tls_conn_session_resumed(*tls) ? "yes" : "no");
+		do {
+			ret = tls_close(*tls);
+		} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+		tls_free(*tls);
+		*tls = NULL;
+	}
+	if (*fd != -1) {
+		close(*fd);
+		*fd = -1;
+	}
+#endif
+	if (*fin != NULL) {
+		fclose(*fin);
+		*fin = NULL;
+	}
+}
+
+static const char *
+sockerror(tls_t tls)
+{
+	int	save_errno = errno;
+#ifndef NOSSL
+	if (tls != NULL) {
+		const char *tlserr = tls_error(tls);
+		if (tlserr != NULL)
+			return tlserr;
+	}
+#endif
+	return strerror(save_errno);
+}
+
+#ifndef NOSSL
+static int
+proxy_connect(int socket, char *host, char *cookie)
+{
+	int l;
+	char buf[1024];
+	char *connstr, *hosttail, *port;
+
+	if (*host == '[' && (hosttail = strrchr(host, ']')) != NULL &&
+		(hosttail[1] == '\0' || hosttail[1] == ':')) {
+		host++;
+		*hosttail++ = '\0';
+	} else
+		hosttail = host;
+
+	port = strrchr(hosttail, ':');		/* find portnum */
+	if (port != NULL)
+		*port++ = '\0';
+	if (!port)
+		port = "443";
+
+	if (cookie) {
+		l = asprintf(&connstr, "CONNECT %s:%s HTTP/1.1\r\n"
+			"Host: %s:%s\r\n"
+			"Proxy-Authorization: Basic %s\r\n%s\r\n\r\n",
+			host, port, host, port, cookie, HTTP_USER_AGENT);
+	} else {
+		l = asprintf(&connstr, "CONNECT %s:%s HTTP/1.1\r\n"
+			"Host: %s:%s\r\n%s\r\n\r\n",
+			host, port, host, port, HTTP_USER_AGENT);
+	}
+
+	if (l == -1)
+		errx(1, "Could not allocate memory to assemble connect string!");
+#ifndef SMALL
+	if (debug)
+		printf("%s", connstr);
+#endif /* !SMALL */
+	if (write(socket, connstr, l) != l)
+		err(1, "Could not send connect string");
+	read(socket, &buf, sizeof(buf)); /* only proxy header XXX: error handling? */
+	free(connstr);
+	return(200);
+}
+
+static int
+stdio_tls_write_wrapper(void *arg, const char *buf, int len)
+{
+	tls_t tls = arg;
+	ssize_t ret;
+
+	do {
+		ret = tls_write(tls, buf, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+
+	return ret;
+}
+
+static int
+stdio_tls_read_wrapper(void *arg, char *buf, int len)
+{
+	tls_t tls = arg;
+	ssize_t ret;
+
+	do {
+		ret = tls_read(tls, buf, len);
+	} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+
+	return ret;
+}
+#endif /* !NOSSL */
diff --git a/ftp.1 b/ftp.1
new file mode 100644
index 0000000..c5dde04
--- /dev/null
+++ b/ftp.1
@@ -0,0 +1,1821 @@
+.\"	$OpenBSD: ftp.1,v 1.124 2022/09/15 12:47:10 millert Exp $
+.\"	$NetBSD: ftp.1,v 1.22 1997/08/18 10:20:22 lukem Exp $
+.\"
+.\" Copyright (c) 1985, 1989, 1990, 1993
+.\"	The Regents of the University of California.  All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\"    may be used to endorse or promote products derived from this software
+.\"    without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\"	@(#)ftp.1	8.3 (Berkeley) 10/9/94
+.\"
+.Dd $Mdocdate: September 15 2022 $
+.Dt FTP 1
+.Os
+.Sh NAME
+.Nm ftp
+.Nd Internet file transfer program
+.Sh SYNOPSIS
+.Nm ftp
+.Op Fl 46AadEegiMmnptVv
+.Op Fl D Ar title
+.Op Fl k Ar seconds
+.Op Fl P Ar port
+.Op Fl r Ar seconds
+.Op Fl s Ar sourceaddr
+.Op Ar host Op Ar port
+.Nm ftp
+.Op Fl C
+.Op Fl N Ar name
+.Op Fl o Ar output
+.Op Fl s Ar sourceaddr
+.Sm off
+.Pf ftp:// Op Ar user : password No @
+.Ar host Op : Ar port
+.No / Ar file Op /
+.Sm on
+.Ar ...
+.Nm ftp
+.Op Fl CTu
+.Op Fl c Ar cookie
+.Op Fl N Ar name
+.Op Fl o Ar output
+.Op Fl S Ar ssl_options
+.Op Fl s Ar sourceaddr
+.Op Fl U Ar useragent
+.Op Fl w Ar seconds
+.Sm off
+.Pf http Oo s Oc ://
+.Op Ar user : password No @
+.Ar host Op : Ar port
+.No / Ar file
+.Sm on
+.Ar ...
+.Nm ftp
+.Op Fl C
+.Op Fl N Ar name
+.Op Fl o Ar output
+.Op Fl s Ar sourceaddr
+.Pf file: Ar
+.Nm ftp
+.Op Fl C
+.Op Fl N Ar name
+.Op Fl o Ar output
+.Op Fl s Ar sourceaddr
+.Ar host : Ns / Ns Ar file Ns Op /
+.Ar ...
+.Sh DESCRIPTION
+.Nm
+is the user interface to the Internet standard File Transfer
+Protocol (FTP).
+The program allows a user to transfer files to and from a
+remote network site.
+.Pp
+The latter four usage formats will fetch a file using either the
+FTP, HTTP, or HTTPS protocols into the current directory.
+This is ideal for scripts.
+Refer to
+.Sx AUTO-FETCHING FILES
+below for more information.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl 4
+Forces
+.Nm
+to use IPv4 addresses only.
+.It Fl 6
+Forces
+.Nm
+to use IPv6 addresses only.
+.It Fl A
+Force active mode FTP.
+By default,
+.Nm
+will try to use passive mode FTP and fall back to active mode
+if passive is not supported by the server.
+This option causes
+.Nm
+to always use an active connection.
+It is only useful for connecting
+to very old servers that do not implement passive mode properly.
+.It Fl a
+Causes
+.Nm
+to bypass the normal login procedure and use an anonymous login instead.
+.It Fl C
+Continue a previously interrupted file transfer.
+.Nm
+will continue transferring from an offset equal to the length of
+.Ar file .
+.Pp
+Resuming HTTP(S) transfers are only supported
+if the remote server supports the
+.Dq Range
+header.
+.It Fl c Ar cookie
+Load a Netscape-like cookiejar file
+for HTTP and HTTPS transfers.
+With this option relevant cookies from the jar are sent with each HTTP(S)
+request.
+Setting the
+.Ev http_cookies
+environment variable has the same effect.
+If both the
+.Ev http_cookies
+environment variable is set and the
+.Fl c
+argument is given, the latter takes precedence.
+.It Fl D Ar title
+Specify a short
+.Ar title
+for the start of the progress bar.
+.It Fl d
+Enables debugging.
+.It Fl E
+Disables EPSV/EPRT command on IPv4 connections.
+.It Fl e
+Disables command line editing.
+Useful for Emacs ange-ftp.
+.It Fl g
+Disables file name globbing.
+.It Fl i
+Turns off interactive prompting during
+multiple file transfers.
+.It Fl k Ar seconds
+When greater than zero,
+sends a byte after each
+.Ar seconds
+period over the control connection during long transfers,
+so that incorrectly configured network equipment won't
+aggressively drop it.
+The FTP protocol supports a
+.Dv NOOP
+command that can be used for that purpose.
+This assumes the FTP server can deal with extra commands coming over
+the control connection during a transfer.
+Well-behaved servers queue those commands, and process them after the
+transfer.
+By default,
+.Nm
+will send a byte every 60 seconds.
+.It Fl M
+Causes
+.Nm
+to never display the progress meter in cases where it would do
+so by default.
+.It Fl m
+Causes
+.Nm
+to always display the progress meter in cases where it would not do
+so by default.
+.It Fl N Ar name
+Use this alternative name instead of
+.Nm
+in some error reports.
+.It Fl n
+Restrains
+.Nm
+from attempting
+.Dq auto-login
+upon initial connection.
+If auto-login is enabled,
+.Nm
+will check the
+.Pa .netrc
+file (see below) in the user's home directory for an entry describing
+an account on the remote machine.
+If no entry exists,
+.Nm
+will prompt for the remote machine login name (default is the user
+identity on the local machine) and, if necessary, prompt for a password
+and an account with which to log in.
+.It Fl o Ar output
+When fetching a single file or URL, save the contents in
+.Ar output .
+To make the contents go to stdout,
+use
+.Sq -
+for
+.Ar output .
+.It Fl P Ar port
+Sets the port number to
+.Ar port .
+.It Fl p
+Enable passive mode operation for use behind connection filtering firewalls.
+This option has been deprecated as
+.Nm
+now tries to use passive mode by default, falling back to active mode
+if the server does not support passive connections.
+.It Fl r Ar seconds
+Retry to connect if failed, pausing for number of
+.Ar seconds .
+.It Fl S Ar ssl_options
+SSL/TLS options to use with HTTPS transfers.
+The following settings are available:
+.Bl -tag -width Ds
+.It Cm cafile Ns = Ns Ar /path/to/cert.pem
+PEM encoded file containing CA certificates used for certificate
+validation.
+.It Cm capath Ns = Ns Ar /path/to/certs/
+Directory containing PEM encoded CA certificates used for certificate
+validation.
+Such a directory can be prepared using the c_rehash script distributed with
+OpenSSL.
+.It Cm ciphers Ns = Ns Ar cipher_list
+Specify the list of ciphers that will be used by
+.Nm .
+See the
+.Xr openssl 1
+.Cm ciphers
+subcommand.
+.It Cm depth Ns = Ns Ar max_depth
+Maximum depth of the certificate chain allowed when performing
+validation.
+.It Cm do
+Perform server certificate validation.
+.It Cm dont
+Don't perform server certificate validation.
+.It Cm muststaple
+Require the server to present a valid OCSP stapling in the TLS handshake.
+.It Cm noverifytime
+Disable validation of certificate times and OCSP validation.
+.It Cm protocols Ns = Ns Ar protocol_list
+Specify the TLS protocols that will be supported by
+.Nm
+(see
+.Xr tls_config_parse_protocols 3
+for details).
+.It Cm session Ns = Ns Ar /path/to/session
+Specify a file to use for TLS session data.
+If this file has a non-zero length, the session data will be read from this file
+and the client will attempt to resume the TLS session with the server.
+Upon completion of a successful TLS handshake this file will be updated
+with new session data, if available.
+This file will be created if it does not already exist.
+.El
+.Pp
+By default, server certificate validation is performed, and if it fails
+.Nm
+will abort.
+If no
+.Cm cafile
+or
+.Cm capath
+setting is provided,
+.Pa /etc/ssl/cert.pem
+will be used.
+.It Fl s Ar sourceaddr
+Set the source address for connections, which is useful on machines
+with multiple interfaces.
+.It Fl T
+Send an
+.Dq If-Modified-Since
+header to the remote to determine if the remote file's timestamp
+has changed.
+.It Fl t
+Enables packet tracing.
+.It Fl U Ar useragent
+Set
+.Ar useragent
+as the User-Agent for HTTP(S) URL requests.
+If not specified, the default User-Agent is
+.Dq OpenBSD ftp .
+.It Fl u
+Disable setting the local file's timestamps based
+on the
+.Dq Last-Modified
+header.
+By default the local file's timestamps are set to match those
+from the remote.
+.It Fl V
+Disable verbose mode, overriding the default of enabled when input
+is from a terminal.
+.It Fl v
+Enable verbose mode.
+This is the default if input is from a terminal.
+Forces
+.Nm
+to show all responses from the remote server, as well
+as report on data transfer statistics.
+.It Fl w Ar seconds
+Wait for
+.Ar seconds
+for the remote server to connect before giving up.
+.El
+.Pp
+The host with which
+.Nm
+is to communicate may be specified on the command line.
+If this is done,
+.Nm
+will immediately attempt to establish a connection to an
+FTP server on that host; otherwise,
+.Nm
+will enter its command interpreter and await instructions
+from the user.
+When
+.Nm
+is awaiting commands, the prompt
+.Dq ftp\*(Gt
+is provided to the user.
+The following commands are recognized
+by
+.Nm :
+.Bl -tag -width Ds
+.It Ic \&! Oo Ar command
+.Op Ar arg ...
+.Oc
+Invoke an interactive shell on the local machine.
+If there are arguments, the first is taken to be a command to execute
+directly, with the rest of the arguments as its arguments.
+.It Ic \&$ Ar macro-name Op Ar arg ...
+Execute the macro
+.Ar macro-name
+that was defined with the
+.Ic macdef
+command.
+Arguments are passed to the macro unglobbed.
+.It Ic \&? Op Ar command
+A synonym for
+.Ic help .
+.It Ic account Op Ar password
+Supply a supplemental password required by a remote system for access
+to resources once a login has been successfully completed.
+If no argument is included, the user will be prompted for an account
+password in a non-echoing input mode.
+.It Ic append Ar local-file Op Ar remote-file
+Append a local file to a file on the remote machine.
+If
+.Ar remote-file
+is left unspecified, the local file name is used in naming the
+remote file after being altered by any
+.Ic ntrans
+or
+.Ic nmap
+setting.
+File transfer uses the current settings for
+.Ic type ,
+.Ic format ,
+.Ic mode ,
+and
+.Ic structure .
+.It Ic ascii
+Set the file transfer
+.Ic type
+to network ASCII.
+.It Ic bell Op Ic on | off
+Arrange that a bell be sounded after each file transfer
+command is completed.
+.It Ic binary
+Set the file transfer
+.Ic type
+to support binary image transfer.
+This is the default type.
+.It Ic bye
+Terminate the FTP session with the remote server and exit
+.Nm .
+An end-of-file will also terminate the session and exit.
+.It Ic case Op Ic on | off
+Toggle remote computer file name case mapping during
+.Ic mget
+commands.
+When
+.Ic case
+is on (default is off), remote computer file names with all letters in
+upper case are written in the local directory with the letters mapped
+to lower case.
+.It Ic cd Ar remote-directory
+Change the working directory on the remote machine
+to
+.Ar remote-directory .
+.It Ic cdup
+Change the remote machine working directory to the parent of the
+current remote machine working directory.
+.It Ic chmod Ar mode file
+Change the permission modes of
+.Ar file
+on the remote
+system to
+.Ar mode .
+.It Ic close
+Terminate the FTP session with the remote server and
+return to the command interpreter.
+Any defined macros are erased.
+.It Ic cr Op Ic on | off
+Toggle carriage return stripping during
+ASCII type file retrieval.
+Records are denoted by a carriage return/linefeed sequence
+during ASCII type file transfer.
+When
+.Ic cr
+is on (the default), carriage returns are stripped from this
+sequence to conform with the
+.Ux
+single linefeed record delimiter.
+Records on non-UNIX
+remote systems may contain single linefeeds;
+when an ASCII type transfer is made, these linefeeds may be
+distinguished from a record delimiter only when
+.Ic cr
+is off.
+.It Ic debug Oo Ic on | off |
+.Ar debuglevel
+.Oc
+Toggle debugging mode.
+If an optional
+.Ar debuglevel
+is specified, it is used to set the debugging level.
+When debugging is on,
+.Nm
+prints each command sent to the remote machine,
+preceded by the string
+.Ql --\*(Gt .
+.It Ic delete Ar remote-file
+Delete the file
+.Ar remote-file
+on the remote machine.
+.It Ic dir Op Ar remote-directory Op Ar local-file
+A synonym for
+.Ic ls .
+.It Ic disconnect
+A synonym for
+.Ic close .
+.It Ic edit Op Ic on | off
+Toggle command line editing, and context sensitive command and file
+completion.
+This is automatically enabled if input is from a terminal, and
+disabled otherwise.
+.It Ic epsv4 Op Ic on | off
+Toggle use of EPSV/EPRT command on IPv4 connection.
+.It Ic exit
+A synonym for
+.Ic bye .
+.It Ic form Ar format
+Set the file transfer
+.Ic form
+to
+.Ar format .
+The default format is
+.Dq file .
+.It Ic ftp Ar host Op Ar port
+A synonym for
+.Ic open .
+.It Ic gate Oo Ic on | off |
+.Ar host Op Ar port
+.Oc
+Toggle gate-ftp mode.
+This will not be permitted if the gate-ftp server hasn't been set
+(either explicitly by the user, or from the
+.Ev FTPSERVER
+environment variable).
+If
+.Ar host
+is given,
+then gate-ftp mode will be enabled, and the gate-ftp server will be set to
+.Ar host .
+If
+.Ar port
+is also given, that will be used as the port to connect to on the
+gate-ftp server.
+.It Ic get Ar remote-file Op Ar local-file
+Retrieve the
+.Ar remote-file
+and store it on the local machine.
+If the local
+file name is not specified, it is given the same
+name it has on the remote machine, subject to
+alteration by the current
+.Ic case ,
+.Ic ntrans ,
+and
+.Ic nmap
+settings.
+The current settings for
+.Ic type ,
+.Ic form ,
+.Ic mode ,
+and
+.Ic structure
+are used while transferring the file.
+.It Ic glob Op Ic on | off
+Toggle filename expansion for
+.Ic mdelete ,
+.Ic mget
+and
+.Ic mput .
+If globbing is turned off with
+.Ic glob ,
+the file name arguments
+are taken literally and not expanded.
+Globbing for
+.Ic mput
+is done as in
+.Xr csh 1 .
+For
+.Ic mdelete
+and
+.Ic mget ,
+each remote file name is expanded
+separately on the remote machine and the lists are not merged.
+Expansion of a directory name is likely to be
+different from expansion of the name of an ordinary file:
+the exact result depends on the foreign operating system and FTP server,
+and can be previewed by doing
+.Dq mls remote-files - .
+Note:
+.Ic mget
+and
+.Ic mput
+are not meant to transfer
+entire directory subtrees of files.
+That can be done by
+transferring a
+.Xr tar 1
+archive of the subtree (in binary mode).
+.It Ic hash Oo Ic on | off |
+.Ar size
+.Oc
+Toggle hash mark
+.Pq Ql #
+printing for each data block transferred.
+The size of a data block defaults to 1024 bytes.
+This can be changed by specifying
+.Ar size
+in bytes.
+.It Ic help Op Ar command
+Print an informative message about the meaning of
+.Ar command .
+If no argument is given,
+.Nm
+prints a list of the known commands.
+.It Ic idle Op Ar seconds
+Set the inactivity timer on the remote server to
+.Ar seconds
+seconds.
+If
+.Ar seconds
+is omitted, the current inactivity timer is printed.
+.It Ic lcd Op Ar local-directory
+Change the working directory on the local machine.
+If
+no
+.Ar local-directory
+is specified, the user's home directory is used.
+.It Ic less Ar file
+A synonym for
+.Ic page .
+.It Ic lpwd
+Print the working directory on the local machine.
+.It Ic ls Op Ar remote-directory Op Ar local-file
+Print a listing of the contents of a directory on the remote machine.
+The listing includes any system-dependent information that the server
+chooses to include; for example, most
+.Ux
+systems will produce output from the command
+.Ql ls -l .
+If
+.Ar remote-directory
+is left unspecified, the current working directory is used.
+If interactive prompting is on,
+.Nm
+will prompt the user to verify that the last argument is indeed the
+target local file for receiving
+.Ic ls
+output.
+If no local file is specified, or if
+.Ar local-file
+is
+.Sq - ,
+the output is sent to the terminal.
+.It Ic macdef Ar macro-name
+Define a macro.
+Subsequent lines are stored as the macro
+.Ar macro-name ;
+a null line (consecutive newline characters
+in a file or
+carriage returns from the terminal) terminates macro input mode.
+There is a limit of 16 macros and 4096 total characters in all
+defined macros.
+Macro names can be a maximum of 8 characters.
+Macros are only applicable to the current session they are
+defined in (or if defined outside a session, to the session
+invoked with the next
+.Ic open
+command), and remain defined until a
+.Ic close
+command is executed.
+To invoke a macro,
+use the
+.Ic $
+command (see above).
+.Pp
+The macro processor interprets
+.Ql $
+and
+.Ql \e
+as special characters.
+A
+.Ql $
+followed by a number (or numbers) is replaced by the
+corresponding argument on the macro invocation command line.
+A
+.Ql $
+followed by an
+.Sq i
+tells the macro processor that the
+executing macro is to be looped.
+On the first pass
+.Ql $i
+is
+replaced by the first argument on the macro invocation command line,
+on the second pass it is replaced by the second argument, and so on.
+A
+.Ql \e
+followed by any character is replaced by that character.
+Use the
+.Ql \e
+to prevent special treatment of the
+.Ql $ .
+.It Ic mdelete Op Ar remote-files
+Delete the
+.Ar remote-files
+on the remote machine.
+.It Ic mdir Ar remote-files local-file
+A synonym for
+.Ic mls .
+.It Xo Ic mget
+.Op Fl cnr
+.Op Fl d Ar depth
+.Ar remote-files
+.Xc
+Expand the
+.Ar remote-files
+on the remote machine
+and do a
+.Ic get
+for each file name thus produced.
+See
+.Ic glob
+for details on the filename expansion.
+Resulting file names will then be processed according to
+.Ic case ,
+.Ic ntrans ,
+and
+.Ic nmap
+settings.
+Files are transferred into the local working directory,
+which can be changed with
+.Ql lcd directory ;
+new local directories can be created with
+.Ql "\&! mkdir directory" .
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl c
+Use
+.Ic reget
+instead of
+.Ic get .
+.It Fl d Ar depth
+Specify the maximum recursion level
+.Ar depth .
+The default is 0, which means unlimited.
+.It Fl n
+Use
+.Ic newer
+instead of
+.Ic get .
+.It Fl r
+Recursively descend the directory tree, transferring all files and
+directories.
+.El
+.It Ic mkdir Ar directory-name
+Make a directory on the remote machine.
+.It Ic mls Ar remote-files local-file
+Like
+.Ic ls ,
+except multiple remote files may be specified,
+and the
+.Ar local-file
+must be specified.
+If interactive prompting is on,
+.Nm
+will prompt the user to verify that the last argument is indeed the
+target local file for receiving
+.Ic mls
+output.
+.It Ic mode Op Ar mode-name
+Set the file transfer
+.Ic mode
+to
+.Ar mode-name .
+The default mode is
+.Dq stream
+mode.
+.It Ic modtime Ar file
+Show the last modification time of
+.Ar file
+on the remote machine.
+.It Ic more Ar file
+A synonym for
+.Ic page .
+.It Xo Ic mput
+.Op Fl cr
+.Op Fl d Ar depth
+.Ar local-files
+.Xc
+Expand wild cards in the list of local files given as arguments
+and do a
+.Ic put
+for each file in the resulting list.
+See
+.Ic glob
+for details of filename expansion.
+Resulting file names will then be processed according to
+.Ic ntrans
+and
+.Ic nmap
+settings.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl c
+Use
+.Ic reput
+instead of
+.Ic put .
+.It Fl d Ar depth
+Specify the maximum recursion level
+.Ar depth .
+The default is 0, which means unlimited.
+.It Fl r
+Recursively descend the directory tree, transferring all files and
+directories.
+.El
+.It Xo Ic msend
+.Op Fl c
+.Ar local-files
+.Xc
+A synonym for
+.Ic mput .
+.It Ic newer Ar remote-file Op Ar local-file
+Get the file only if the modification time of the remote file is more
+recent than the file on the current system.
+If the file does not
+exist on the current system, the remote file is considered
+.Ic newer .
+Otherwise, this command is identical to
+.Ar get .
+.It Ic nlist Op Ar remote-directory Op Ar local-file
+Print a list of the files in a
+directory on the remote machine.
+If
+.Ar remote-directory
+is left unspecified, the current working directory is used.
+If interactive prompting is on,
+.Nm
+will prompt the user to verify that the last argument is indeed the
+target local file for receiving
+.Ic nlist
+output.
+If no local file is specified, or if
+.Ar local-file
+is
+.Sq - ,
+the output is sent to the terminal.
+Note that on some servers, the
+.Ic nlist
+command will only return information on normal files (not directories
+or special files).
+.It Ic nmap Op Ar inpattern outpattern
+Set or unset the filename mapping mechanism.
+If no arguments are specified, the filename mapping mechanism is unset.
+If arguments are specified, remote filenames are mapped during
+.Ic mput
+commands and
+.Ic put
+commands issued without a specified remote target filename.
+If arguments are specified, local filenames are mapped during
+.Ic mget
+commands and
+.Ic get
+commands issued without a specified local target filename.
+This command is useful when connecting to a non-UNIX remote computer
+with different file naming conventions or practices.
+.Pp
+The mapping follows the pattern set by
+.Ar inpattern
+and
+.Ar outpattern .
+.Ar inpattern
+is a template for incoming filenames (which may have already been
+processed according to the
+.Ic ntrans
+and
+.Ic case
+settings).
+Variable templating is accomplished by including the
+sequences
+.Ql $1 ,
+.Ql $2 ,
+\&...,
+.Ql $9
+in
+.Ar inpattern .
+Use
+.Ql \e
+to prevent this special treatment of the
+.Ql $
+character.
+All other characters are treated literally, and are used to determine the
+.Ic nmap
+.Ar inpattern
+variable values.
+.Pp
+For example, given
+.Ar inpattern
+$1.$2 and the remote file name "mydata.data", $1 would have the value
+"mydata", and $2 would have the value "data".
+The
+.Ar outpattern
+determines the resulting mapped filename.
+The sequences
+.Ql $1 ,
+.Ql $2 ,
+\&...,
+.Ql $9
+are replaced by any value resulting from the
+.Ar inpattern
+template.
+The sequence
+.Ql $0
+is replaced by the original filename.
+Additionally, the sequence
+.Sq Op Ar seq1 , Ar seq2
+is replaced by
+.Ar seq1
+if
+.Ar seq1
+is not a null string; otherwise it is replaced by
+.Ar seq2 .
+For example:
+.Pp
+.Dl nmap $1.$2.$3 [$1,$2].[$2,file]
+.Pp
+This command would yield the output filename
+.Pa myfile.data
+for input filenames
+.Pa myfile.data
+and
+.Pa myfile.data.old ;
+.Pa myfile.file
+for the input filename
+.Pa myfile ;
+and
+.Pa myfile.myfile
+for the input filename
+.Pa .myfile .
+Spaces may be included in
+.Ar outpattern
+by quoting them,
+as in the following example:
+.Bd -literal -offset indent
+nmap $1.$2 "$1 $2"
+.Ed
+.Pp
+Use the
+.Ql \e
+character to prevent special treatment
+of the
+.Ql $ ,
+.Ql \&[ ,
+.Ql \&] ,
+and
+.Ql \&,
+characters.
+.It Ic ntrans Op Ar inchars Op Ar outchars
+Set or unset the filename character translation mechanism.
+If no arguments are specified, the filename character
+translation mechanism is unset.
+If arguments are specified, characters in
+remote filenames are translated during
+.Ic mput
+commands and
+.Ic put
+commands issued without a specified remote target filename.
+If arguments are specified, characters in
+local filenames are translated during
+.Ic mget
+commands and
+.Ic get
+commands issued without a specified local target filename.
+This command is useful when connecting to a non-UNIX remote computer
+with different file naming conventions or practices.
+Characters in a filename matching a character in
+.Ar inchars
+are replaced with the corresponding character in
+.Ar outchars .
+If the character's position in
+.Ar inchars
+is longer than the length of
+.Ar outchars ,
+the character is deleted from the file name.
+.It Ic open Ar host Op Ar port
+Establish a connection to the specified
+.Ar host
+FTP server.
+An optional port number may be supplied,
+in which case
+.Nm
+will attempt to contact an FTP server at that port.
+If the
+.Ic auto-login
+option is on (default),
+.Nm
+will also attempt to automatically log the user in to
+the FTP server (see below).
+.It Ic page Ar file
+Retrieve
+.Ic file
+and display with the program defined in
+.Ev PAGER
+(defaulting to
+.Xr more 1
+if
+.Ev PAGER
+is null or not defined).
+.It Ic passive Op Ic on | off
+Toggle passive mode.
+If passive mode is turned on (default is on),
+.Nm
+will send a
+.Dv EPSV
+command for all data connections instead of the usual
+.Dv PORT
+command.
+The
+.Dv PASV
+command requests that the remote server open a port for the data connection
+and return the address of that port.
+The remote server listens on that port and the client connects to it.
+When using the more traditional
+.Dv PORT
+command, the client listens on a port and sends that address to the remote
+server, who connects back to it.
+Passive mode is useful when using
+.Nm
+through a gateway router or host that controls the directionality of
+traffic.
+(Note that though FTP servers are required to support the
+.Dv PASV
+command by RFC 1123, some do not.)
+.It Ic preserve Op Ic on | off
+Toggle preservation of modification times on retrieved files.
+.It Ic progress Op Ic on | off
+Toggle display of transfer progress bar.
+The progress bar will be disabled for a transfer that has
+.Ar local-file
+as
+.Sq -
+or a command that starts with
+.Sq \&| .
+Refer to
+.Sx FILE NAMING CONVENTIONS
+for more information.
+.It Ic prompt Op Ic on | off
+Toggle interactive prompting.
+Interactive prompting
+occurs during multiple file transfers to allow the
+user to selectively retrieve or store files.
+If prompting is turned off (default is on), any
+.Ic mget
+or
+.Ic mput
+will transfer all files, and any
+.Ic mdelete
+will delete all files.
+.Pp
+When prompting is on, the following commands are available at a prompt:
+.Bl -tag -width 2n -offset indent
+.It Ic ?\&
+Print help message.
+.It Ic a
+Answer
+.Dq yes
+to the current file and automatically answer
+.Dq yes
+to any remaining files for the current command.
+.It Ic n
+Do not transfer the file.
+.It Ic p
+Answer
+.Dq yes
+to the current file and turn off prompt mode
+(as if
+.Dq prompt off
+had been given).
+.It Ic q
+Answer
+.Dq no
+to the current file and automatically answer
+.Dq no
+to any remaining files for the current command.
+.It Ic y
+Transfer the file.
+.El
+.It Ic proxy Ar command
+Execute an FTP command on a secondary control connection.
+This command allows simultaneous connection to two remote FTP
+servers for transferring files between the two servers.
+The first
+.Ic proxy
+command should be an
+.Ic open ,
+to establish the secondary control connection.
+Enter the command
+.Ic proxy ?\&
+to see other FTP commands executable on the
+secondary connection.
+The following commands behave differently when prefaced by
+.Ic proxy :
+.Ic open
+will not define new macros during the auto-login process;
+.Ic close
+will not erase existing macro definitions;
+.Ic get
+and
+.Ic mget
+transfer files from the host on the primary control connection
+to the host on the secondary control connection; and
+.Ic put ,
+.Ic mput ,
+and
+.Ic append
+transfer files from the host on the secondary control connection
+to the host on the primary control connection.
+Third party file transfers depend upon support of the FTP protocol
+.Dv PASV
+command by the server on the secondary control connection.
+.It Ic put Ar local-file Op Ar remote-file
+Store a local file on the remote machine.
+If
+.Ar remote-file
+is left unspecified, the local file name is used
+after processing according to any
+.Ic ntrans
+or
+.Ic nmap
+settings
+in naming the remote file.
+File transfer uses the
+current settings for
+.Ic type ,
+.Ic format ,
+.Ic mode ,
+and
+.Ic structure .
+.It Ic pwd
+Print the name of the current working directory on the remote
+machine.
+.It Ic quit
+A synonym for
+.Ic bye .
+.It Ic quote Ar arg ...
+The arguments specified are sent, verbatim, to the remote FTP server.
+.It Ic recv Ar remote-file Op Ar local-file
+A synonym for
+.Ic get .
+.It Ic reget Ar remote-file Op Ar local-file
+Reget acts like get, except that if
+.Ar local-file
+exists and is
+smaller than
+.Ar remote-file ,
+.Ar local-file
+is presumed to be
+a partially transferred copy of
+.Ar remote-file
+and the transfer
+is continued from the apparent point of failure.
+This command
+is useful when transferring very large files over networks that
+are prone to dropping connections.
+.It Ic rename Ar from-name to-name
+Rename the file
+.Ar from-name
+on the remote machine to the file
+.Ar to-name .
+.It Ic reput Ar local-file Op Ar remote-file
+Reput acts like put, except that if
+.Ar remote-file
+exists and is
+smaller than
+.Ar local-file ,
+.Ar remote-file
+is presumed to be
+a partially transferred copy of
+.Ar local-file
+and the transfer
+is continued from the apparent point of failure.
+This command
+is useful when transferring very large files over networks that
+are prone to dropping connections.
+.It Ic reset
+Clear reply queue.
+This command re-synchronizes command/reply sequencing with the remote
+FTP server.
+Resynchronization may be necessary following a violation of the FTP protocol
+by the remote server.
+.It Ic restart Ar marker
+Restart the immediately following
+.Ic get
+or
+.Ic put
+at the
+indicated
+.Ar marker .
+On
+.Ux
+systems,
+.Ar marker
+is usually a byte
+offset into the file.
+.It Ic rhelp Op Ar command-name
+Request help from the remote FTP server.
+If a
+.Ar command-name
+is specified, it is supplied to the server as well.
+.It Ic rmdir Ar directory-name
+Delete a directory on the remote machine.
+.It Ic rstatus Op Ar file
+With no arguments, show status of remote machine.
+If
+.Ar file
+is specified, show status of
+.Ar file
+on remote machine.
+.It Ic runique Op Ic on | off
+Toggle storing of files on the local system with unique filenames.
+If a file already exists with a name equal to the target
+local filename for a
+.Ic get
+or
+.Ic mget
+command, a
+.Dq .1
+is appended to the name.
+If the resulting name matches another existing file,
+a
+.Dq .2
+is appended to the original name.
+If this process continues up to
+.Dq .99 ,
+an error message is printed, and the transfer does not take place.
+The generated unique filename will be reported.
+Note that
+.Ic runique
+will not affect local files generated from a shell command
+(see below).
+The default value is off.
+.It Ic send Ar local-file Op Ar remote-file
+A synonym for
+.Ic put .
+.It Ic sendport Op Ic on | off
+Toggle the use of
+.Dv PORT
+commands.
+By default,
+.Nm
+will attempt to use a
+.Dv PORT
+command when establishing
+a connection for each data transfer.
+The use of
+.Dv PORT
+commands can prevent delays
+when performing multiple file transfers.
+If the
+.Dv PORT
+command fails,
+.Nm
+will use the default data port.
+When the use of
+.Dv PORT
+commands is disabled, no attempt will be made to use
+.Dv PORT
+commands for each data transfer.
+This is useful for certain FTP implementations which do ignore
+.Dv PORT
+commands but, incorrectly, indicate they've been accepted.
+.It Ic site Ar arg ...
+The arguments specified are sent, verbatim, to the remote FTP server as a
+.Dv SITE
+command.
+.It Ic size Ar file
+Return size of
+.Ar file
+on remote machine.
+.It Ic status
+Show the current status of
+.Nm .
+.\" .It Ic struct Op Ar struct-name
+.\" Set the file transfer
+.\" .Ar structure
+.\" to
+.\" .Ar struct-name .
+.\" By default,
+.\" .Dq file
+.\" structure is used.
+.It Ic sunique Op Ic on | off
+Toggle storing of files on remote machine under unique file names.
+The remote FTP server must support the FTP protocol
+.Dv STOU
+command for
+successful completion.
+The remote server will report the unique name.
+Default value is off.
+.It Ic system
+Show the type of operating system running on the remote machine.
+.It Ic trace Op Ic on | off
+Toggle packet tracing.
+.It Ic type Op Ar type-name
+Set the file transfer
+.Ic type
+to
+.Ar type-name .
+If no type is specified, the current type
+is printed.
+The default type is
+.Dq binary .
+.It Ic umask Op Ar newmask
+Set the default umask on the remote server to
+.Ar newmask .
+If
+.Ar newmask
+is omitted, the current umask is printed.
+.It Xo
+.Ic user Ar username
+.Op Ar password Op Ar account
+.Xc
+Identify yourself to the remote FTP server.
+If the
+.Ar password
+is not specified and the server requires it,
+.Nm
+will prompt the user for it (after disabling local echo).
+If an
+.Ar account
+field is not specified, and the FTP server requires it,
+the user will be prompted for it.
+If an
+.Ar account
+field is specified, an account command will
+be relayed to the remote server after the login sequence
+is completed if the remote server did not require it
+for logging in.
+Unless
+.Nm
+is invoked with
+.Dq auto-login
+disabled, this process is done automatically on initial connection to the
+FTP server.
+.It Ic verbose Op Ic on | off
+Toggle verbose mode.
+In verbose mode, all responses from
+the FTP server are displayed to the user.
+In addition,
+if verbose is on, when a file transfer completes, statistics
+regarding the efficiency of the transfer are reported.
+By default,
+verbose is on.
+.El
+.Pp
+Command arguments which have embedded spaces may be quoted with
+quote
+.Pq Ql \&"
+marks.
+.Pp
+Commands which toggle settings can take an explicit
+.Ic on
+or
+.Ic off
+argument to force the setting appropriately.
+.Pp
+If
+.Nm
+receives a
+.Dv SIGINFO
+(see the
+.Dq status
+argument of
+.Xr stty 1 )
+signal whilst a transfer is in progress, the current transfer rate
+statistics will be written to the standard error output, in the
+same format as the standard completion message.
+.Sh AUTO-FETCHING FILES
+In addition to standard commands, this version of
+.Nm
+supports an auto-fetch feature.
+To enable auto-fetch, simply pass the list of hostnames/files
+on the command line.
+.Pp
+The following formats are valid syntax for an auto-fetch element:
+.Bl -tag -width Ds
+.It Ar host : Ns / Ns Ar file Ns Op /
+.Dq Classic
+.Nm
+format.
+.Sm off
+.It Xo
+.Pf ftp:// Op Ar user : password No @
+.Ar host Op : Ar port
+.No / Ar file Op /
+.Xc
+.Sm on
+An FTP URL, retrieved using the FTP protocol if
+.Ev ftp_proxy
+isn't defined.
+Otherwise, transfer using HTTP via the proxy defined in
+.Ev ftp_proxy .
+If a
+.Ar user
+and
+.Ar password
+are given and
+.Ev ftp_proxy
+isn't defined,
+log in as
+.Ar user
+with a password of
+.Ar password .
+.Sm off
+.It Xo
+.Pf http:// Op Ar user : password No @
+.Ar host Op : Ar port
+.No / Ar file
+.Xc
+.Sm on
+An HTTP URL, retrieved using the HTTP protocol.
+If
+.Ev http_proxy
+is defined, it is used as a URL to an HTTP proxy server.
+If a
+.Ar user
+and
+.Ar password
+are given and
+.Ev http_proxy
+isn't defined,
+log in as
+.Ar user
+with a password of
+.Ar password
+using Basic authentication.
+.Sm off
+.It Xo
+.Pf https:// Op Ar user : password No @
+.Ar host Op : Ar port
+.No / Ar file
+.Xc
+.Sm on
+An HTTPS URL, retrieved using the HTTPS protocol.
+If
+.Ev http_proxy
+is defined, this HTTPS proxy server will be used to fetch the
+file using the CONNECT method.
+If a
+.Ar user
+and
+.Ar password
+are given and
+.Ev http_proxy
+isn't defined,
+log in as
+.Ar user
+with a password of
+.Ar password
+using Basic authentication.
+.It Pf file: Ar file
+.Ar file
+is retrieved from a mounted file system.
+.El
+.Pp
+If a classic format or an FTP URL format has a trailing
+.Sq / ,
+then
+.Nm
+will connect to the site and
+.Ic cd
+to the directory given as the path, and leave the user in interactive
+mode ready for further input.
+.Pp
+If
+.Ar file
+contains a glob character and globbing is enabled
+(see
+.Ic glob ) ,
+then the equivalent of
+.Ic mget Ar file
+is performed.
+.Pp
+If no
+.Fl o
+option is specified, and
+the directory component of
+.Ar file
+contains no globbing characters,
+then
+it is stored in the current directory as the
+.Xr basename 1
+of
+.Ar file .
+If
+.Fl o Ar output
+is specified, then
+.Ar file
+is stored as
+.Ar output .
+Otherwise, the remote name is used as the local name.
+.Sh ABORTING A FILE TRANSFER
+To abort a file transfer, use the terminal interrupt key
+(usually Ctrl-C).
+Sending transfers will be immediately halted.
+Receiving transfers will be halted by sending an FTP protocol
+.Dv ABOR
+command to the remote server, and discarding any further data received.
+The speed at which this is accomplished depends upon the remote
+server's support for
+.Dv ABOR
+processing.
+If the remote server does not support the
+.Dv ABOR
+command, an
+.Ql ftp\*(Gt
+prompt will not appear until the remote server has completed
+sending the requested file.
+.Pp
+The terminal interrupt key sequence will be ignored when
+.Nm
+has completed any local processing and is awaiting a reply
+from the remote server.
+A long delay in this mode may result from the ABOR processing described
+above, or from unexpected behavior by the remote server, including
+violations of the FTP protocol.
+If the delay results from unexpected remote server behavior, the local
+.Nm
+program must be killed by hand.
+.Sh FILE NAMING CONVENTIONS
+Files specified as arguments to
+.Nm
+commands are processed according to the following rules.
+.Bl -enum
+.It
+If
+.Sq -
+is specified as a local file name, the standard input (for reading)
+or standard output (for writing)
+is used.
+.It
+If the first character of a local file name is
+.Sq \&| ,
+the
+remainder of the argument is interpreted as a shell command.
+.Nm
+then forks a shell, using
+.Xr popen 3
+with the argument supplied, and reads (writes) from the standard output
+(standard input).
+If the shell command includes spaces, the argument
+must be quoted; e.g.,
+.Qq ls -lt .
+A particularly
+useful example of this mechanism is:
+.Qq ls \&. |more .
+.It
+Failing the above checks, if
+.Dq globbing
+is enabled,
+local file names are expanded
+according to the rules used in the
+.Xr csh 1
+.Ic glob
+command.
+If the
+.Nm
+command expects a single local file (e.g.,
+.Ic put ) ,
+only the first filename generated by the
+.Dq globbing
+operation is used.
+.It
+For
+.Ic mget
+commands and
+.Ic get
+commands with unspecified local file names, the local filename is
+the remote filename, which may be altered by a
+.Ic case ,
+.Ic ntrans ,
+or
+.Ic nmap
+setting.
+The resulting filename may then be altered if
+.Ic runique
+is on.
+.It
+For
+.Ic mput
+commands and
+.Ic put
+commands with unspecified remote file names, the remote filename is
+the local filename, which may be altered by a
+.Ic ntrans
+or
+.Ic nmap
+setting.
+The resulting filename may then be altered by the remote server if
+.Ic sunique
+is on.
+.El
+.Sh FILE TRANSFER PARAMETERS
+The FTP specification specifies many parameters which may
+affect a file transfer.
+The
+.Ic type
+may be one of
+.Dq ascii ,
+.Dq binary ,
+or
+.Dq image .
+.Nm
+supports the ASCII and image types of file transfer.
+.Pp
+.Nm
+supports only the default values for the remaining
+file transfer parameters:
+.Ic mode ,
+.Ic form ,
+and
+.Ic struct .
+.Sh THE .netrc FILE
+The
+.Pa .netrc
+file contains login and initialization information
+used by the auto-login process.
+It resides in the user's home directory.
+The following tokens are recognized; they may be separated by spaces,
+tabs, or new-lines:
+.Bl -tag -width password
+.It Ic machine Ar name
+Identify a remote machine
+.Ar name .
+The auto-login process searches the
+.Pa .netrc
+file for a
+.Ic machine
+token that matches the remote machine specified on the
+.Nm
+command line or as an
+.Ic open
+command argument.
+Once a match is made, the subsequent
+.Pa .netrc
+tokens are processed,
+stopping when the end of file is reached or another
+.Ic machine
+or a
+.Ic default
+token is encountered.
+.It Ic default
+This is the same as
+.Ic machine
+.Ar name
+except that
+.Ic default
+matches any name.
+There can be only one
+.Ic default
+token, and it must be after all
+.Ic machine
+tokens.
+This is normally used as:
+.Pp
+.Dl default login anonymous password user@site
+.Pp
+thereby giving the user
+.Ar automatic
+anonymous FTP login to
+machines not specified in
+.Pa .netrc .
+This can be overridden
+by using the
+.Fl n
+flag to disable auto-login.
+.It Ic login Ar name
+Identify a user on the remote machine.
+If this token is present, the auto-login process will initiate
+a login using the specified
+.Ar name .
+.It Ic password Ar string
+Supply a password.
+If this token is present, the auto-login process will supply the
+specified string if the remote server requires a password as part
+of the login process.
+Note that if this token is present in the
+.Pa .netrc
+file for any user other
+than
+.Ar anonymous ,
+.Nm
+will abort the auto-login process if the
+.Pa .netrc
+is readable by
+anyone besides the user.
+.It Ic account Ar string
+Supply an additional account password.
+If this token is present, the auto-login process will supply the
+specified string if the remote server requires an additional
+account password, or the auto-login process will initiate an
+.Dv ACCT
+command if it does not.
+.It Ic macdef Ar name
+Define a macro.
+This token functions like the
+.Nm
+.Ic macdef
+command functions.
+A macro is defined with the specified name; its contents begin with the
+next
+.Pa .netrc
+line and continue until a null line (consecutive new-line
+characters) is encountered.
+Like the other tokens in the
+.Pa .netrc
+file, a
+.Ic macdef
+is applicable only to the
+.Ic machine
+definition preceding it.
+A
+.Ic macdef
+entry cannot be utilized by multiple
+.Ic machine
+definitions; rather, it must be defined following each
+.Ic machine
+it is intended to be used with.
+If a macro named
+.Ic init
+is defined, it is automatically executed as the last step in the
+auto-login process.
+.El
+.Sh COMMAND LINE EDITING
+.Nm
+supports interactive command line editing, via the
+.Xr editline 3
+library.
+It is enabled with the
+.Ic edit
+command, and is enabled by default if input is from a tty.
+Previous lines can be recalled and edited with the arrow keys,
+and other GNU Emacs-style editing keys may be used as well.
+.Pp
+The
+.Xr editline 3
+library is configured with a
+.Pa .editrc
+file \- refer to
+.Xr editrc 5
+for more information.
+.Pp
+An extra key binding is available to
+.Nm
+to provide context sensitive command and filename completion
+(including remote file completion).
+To use this, bind a key to the
+.Xr editline 3
+command
+.Ic ftp-complete .
+By default, this is bound to the TAB key.
+.Sh ENVIRONMENT
+.Nm
+utilizes the following environment variables:
+.Bl -tag -width "FTPSERVERPORT"
+.It Ev FTPMODE
+Overrides the default operation mode.
+Recognized values are:
+.Pp
+.Bl -tag -width "passive  " -offset indent -compact
+.It passive
+passive mode FTP only
+.It active
+active mode FTP only
+.It auto
+automatic determination of passive or active (this is the default)
+.It gate
+gate-ftp mode
+.El
+.It Ev FTPSERVER
+Host to use as gate-ftp server when
+.Ic gate
+is enabled.
+.It Ev FTPSERVERPORT
+Port to use when connecting to gate-ftp server when
+.Ic gate
+is enabled.
+Default is port returned by a
+.Fn getservbyname
+lookup of
+.Dq ftpgate/tcp .
+.It Ev HOME
+For default location of a
+.Pa .netrc
+file, if one exists.
+.It Ev PAGER
+Used by
+.Ic page
+to display files.
+.It Ev SHELL
+For default shell.
+.It Ev ftp_proxy
+URL of FTP proxy to use when making FTP URL requests
+(if not defined, use the standard FTP protocol).
+.It Ev http_proxy
+URL of HTTP proxy to use when making HTTP or HTTPS URL requests.
+.It Ev http_cookies
+Path of a Netscape-like cookiejar file to use when making
+HTTP or HTTPS URL requests.
+.El
+.Sh PORT ALLOCATION
+For active mode data connections,
+.Nm
+will listen to a random high TCP port.
+The interval of ports used are configurable using
+.Xr sysctl 8
+variables
+.Va net.inet.ip.porthifirst
+and
+.Va net.inet.ip.porthilast .
+.Sh SEE ALSO
+.Xr basename 1 ,
+.Xr csh 1 ,
+.Xr more 1 ,
+.Xr stty 1 ,
+.Xr tar 1 ,
+.Xr tftp 1 ,
+.Xr editline 3 ,
+.Xr getservbyname 3 ,
+.Xr popen 3 ,
+.Xr editrc 5 ,
+.Xr services 5 ,
+.Xr ftp-proxy 8 ,
+.Xr ftpd 8
+.Sh STANDARDS
+.Rs
+.%A J. Postel
+.%A J. Reynolds
+.%D October 1985
+.%R RFC 959
+.%T FILE TRANSFER PROTOCOL (FTP)
+.Re
+.Pp
+.Rs
+.%A P. Hethmon
+.%D March 2007
+.%R RFC 3659
+.%T Extensions to FTP
+.Re
+.Sh HISTORY
+The
+.Nm
+command appeared in
+.Bx 4.2 .
+.Sh BUGS
+Correct execution of many commands depends upon proper behavior
+by the remote server.
+.Pp
+In the recursive mode of
+.Ic mget ,
+files and directories starting with whitespace are ignored
+because the list cannot be parsed any other way.
diff --git a/ftp.c b/ftp.c
new file mode 100644
index 0000000..807a4e9
--- /dev/null
+++ b/ftp.c
@@ -0,0 +1,2080 @@
+/*	$OpenBSD: ftp.c,v 1.109 2023/03/08 04:43:11 guenther Exp $	*/
+/*	$NetBSD: ftp.c,v 1.27 1997/08/18 10:20:23 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+#include <arpa/ftp.h>
+#include <arpa/telnet.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <poll.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ftp_var.h"
+
+union sockaddr_union {
+	struct sockaddr		sa;
+	struct sockaddr_in	sin;
+	struct sockaddr_in6	sin6;
+};
+
+union sockaddr_union myctladdr, hisctladdr, data_addr;
+
+int	data = -1;
+int	abrtflag = 0;
+jmp_buf	ptabort;
+int	ptabflg;
+int	ptflag = 0;
+off_t	restart_point = 0;
+
+
+FILE	*cin, *cout;
+
+char *
+hookup(char *host, char *port)
+{
+	int s, tos, error;
+	static char hostnamebuf[HOST_NAME_MAX+1];
+	struct addrinfo hints, *res, *res0;
+#ifndef SMALL
+	struct addrinfo *ares;
+#endif
+	char hbuf[NI_MAXHOST];
+	char *cause = "unknown";
+	socklen_t namelen;
+
+	epsv4bad = 0;
+
+	memset((char *)&hisctladdr, 0, sizeof (hisctladdr));
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_flags = AI_CANONNAME;
+	hints.ai_family = family;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = 0;
+	error = getaddrinfo(host, port, &hints, &res0);
+	if (error == EAI_SERVICE) {
+		/*
+		 * If the services file is corrupt/missing, fall back
+		 * on our hard-coded defines.
+		 */
+		char pbuf[NI_MAXSERV];
+
+		pbuf[0] = '\0';
+		if (strcmp(port, "ftp") == 0)
+			snprintf(pbuf, sizeof(pbuf), "%d", FTP_PORT);
+		else if (strcmp(port, "ftpgate") == 0)
+			snprintf(pbuf, sizeof(pbuf), "%d", GATE_PORT);
+		else if (strcmp(port, "http") == 0)
+			snprintf(pbuf, sizeof(pbuf), "%d", HTTP_PORT);
+#ifndef SMALL
+		else if (strcmp(port, "https") == 0)
+			snprintf(pbuf, sizeof(pbuf), "%d", HTTPS_PORT);
+#endif /* !SMALL */
+		if (pbuf[0])
+			error = getaddrinfo(host, pbuf, &hints, &res0);
+	}
+	if (error) {
+		if (error == EAI_SERVICE)
+			warnx("%s: bad port number `%s'", host, port);
+		else
+			warnx("%s: %s", host, gai_strerror(error));
+		code = -1;
+		return (0);
+	}
+
+	if (res0->ai_canonname)
+		strlcpy(hostnamebuf, res0->ai_canonname, sizeof(hostnamebuf));
+	else
+		strlcpy(hostnamebuf, host, sizeof(hostnamebuf));
+	hostname = hostnamebuf;
+
+#ifndef SMALL
+	if (srcaddr) {
+		struct addrinfo ahints;
+
+		memset(&ahints, 0, sizeof(ahints));
+		ahints.ai_family = family;
+		ahints.ai_socktype = SOCK_STREAM;
+		ahints.ai_flags |= AI_NUMERICHOST;
+		ahints.ai_protocol = 0;
+
+		error = getaddrinfo(srcaddr, NULL, &ahints, &ares);
+		if (error) {
+			warnx("%s: %s", srcaddr, gai_strerror(error));
+			code = -1;
+			return (0);
+		}
+	}
+#endif /* !SMALL */
+
+	s = -1;
+	for (res = res0; res; res = res->ai_next) {
+		if (res0->ai_next)	/* if we have multiple possibilities */
+		{
+			if (getnameinfo(res->ai_addr, res->ai_addrlen,
+			    hbuf, sizeof(hbuf), NULL, 0, NI_NUMERICHOST) != 0)
+				strlcpy(hbuf, "unknown", sizeof(hbuf));
+			if (verbose)
+				fprintf(ttyout, "Trying %s...\n", hbuf);
+		}
+		s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+		if (s == -1) {
+			cause = "socket";
+			continue;
+		}
+#ifndef SMALL
+		if (srcaddr) {
+			if (ares->ai_family != res->ai_family) {
+				close(s);
+				s = -1;
+				errno = EINVAL;
+				cause = "bind";
+				continue;
+			}
+			if (bind(s, ares->ai_addr, ares->ai_addrlen) == -1) {
+				cause = "bind";
+				error = errno;
+				close(s);
+				errno = error;
+				s = -1;
+				continue;
+			}
+		}
+#endif /* !SMALL */
+		error = timed_connect(s, res->ai_addr, res->ai_addrlen,
+		    connect_timeout);
+		if (error != 0) {
+			/* this "if" clause is to prevent print warning twice */
+			if (verbose && res->ai_next) {
+				if (getnameinfo(res->ai_addr, res->ai_addrlen,
+				    hbuf, sizeof(hbuf), NULL, 0,
+				    NI_NUMERICHOST) != 0)
+					strlcpy(hbuf, "(unknown)",
+					    sizeof(hbuf));
+				warn("connect to address %s", hbuf);
+			}
+			cause = "connect";
+			error = errno;
+			close(s);
+			errno = error;
+			s = -1;
+			continue;
+		}
+
+		/* finally we got one */
+		break;
+	}
+	if (s == -1) {
+		warn("%s", cause);
+		code = -1;
+		freeaddrinfo(res0);
+		return 0;
+	}
+	memcpy(&hisctladdr, res->ai_addr, res->ai_addrlen);
+	namelen = res->ai_addrlen;
+	freeaddrinfo(res0);
+	res0 = res = NULL;
+#ifndef SMALL
+	if (srcaddr) {
+		freeaddrinfo(ares);
+		ares = NULL;
+	}
+#endif /* !SMALL */
+	if (getsockname(s, &myctladdr.sa, &namelen) == -1) {
+		warn("getsockname");
+		code = -1;
+		goto bad;
+	}
+	if (hisctladdr.sa.sa_family == AF_INET) {
+		tos = IPTOS_LOWDELAY;
+		if (setsockopt(s, IPPROTO_IP, IP_TOS, (char *)&tos, sizeof(int)) == -1)
+			warn("setsockopt TOS (ignored)");
+	}
+	cin = fdopen(s, "r");
+	cout = fdopen(s, "w");
+	if (cin == NULL || cout == NULL) {
+		warnx("fdopen failed.");
+		if (cin)
+			(void)fclose(cin);
+		if (cout)
+			(void)fclose(cout);
+		code = -1;
+		goto bad;
+	}
+	if (verbose)
+		fprintf(ttyout, "Connected to %s.\n", hostname);
+	if (getreply(0) > 2) {	/* read startup message from server */
+		if (cin)
+			(void)fclose(cin);
+		if (cout)
+			(void)fclose(cout);
+		code = -1;
+		goto bad;
+	}
+	{
+	int ret, on = 1;
+
+	ret = setsockopt(s, SOL_SOCKET, SO_OOBINLINE, (char *)&on, sizeof(on));
+#ifndef SMALL
+	if (ret == -1 && debug)
+		warn("setsockopt");
+#else
+	(void)ret;
+#endif /* !SMALL */
+	}
+
+	return (hostname);
+bad:
+	(void)close(s);
+	return (NULL);
+}
+
+void
+cmdabort(int signo)
+{
+	int save_errno = errno;
+
+	alarmtimer(0);
+	(void) write(fileno(ttyout), "\n\r", 2);
+	abrtflag++;
+
+	errno = save_errno;
+	if (ptflag)
+		longjmp(ptabort, 1);
+}
+
+int
+command(const char *fmt, ...)
+{
+	va_list ap;
+	int r;
+	sig_t oldintr;
+
+	abrtflag = 0;
+#ifndef SMALL
+	if (debug) {
+		fputs("---> ", ttyout);
+		va_start(ap, fmt);
+		if (strncmp("PASS ", fmt, 5) == 0)
+			fputs("PASS XXXX", ttyout);
+		else if (strncmp("ACCT ", fmt, 5) == 0)
+			fputs("ACCT XXXX", ttyout);
+		else
+			vfprintf(ttyout, fmt, ap);
+		va_end(ap);
+		putc('\n', ttyout);
+		(void)fflush(ttyout);
+	}
+#endif /* !SMALL */
+	if (cout == NULL) {
+		warnx("No control connection for command.");
+		code = -1;
+		return (0);
+	}
+	oldintr = signal(SIGINT, cmdabort);
+	va_start(ap, fmt);
+	vfprintf(cout, fmt, ap);
+	va_end(ap);
+	fputs("\r\n", cout);
+	(void)fflush(cout);
+	cpend = 1;
+	r = getreply(!strcmp(fmt, "QUIT"));
+	if (abrtflag && oldintr != SIG_IGN)
+		(*oldintr)(SIGINT);
+	(void)signal(SIGINT, oldintr);
+	return (r);
+}
+
+int keep_alive_timeout = 60;		/* 0 -> no timeout */
+
+static int full_noops_sent = 0;
+static time_t last_timestamp = 0;	/* 0 -> no measurement yet */
+static char noop[] = "NOOP\r\n";
+#define NOOP_LENGTH (sizeof noop - 1)
+static int current_nop_pos = 0;		/* 0 -> no noop started */
+
+/* to achieve keep alive, we send noop one byte at a time */
+static void
+send_noop_char(void)
+{
+#ifndef SMALL
+	if (debug)
+		fprintf(ttyout, "---> %c\n", noop[current_nop_pos]);
+#endif /* !SMALL */
+	fputc(noop[current_nop_pos++], cout);
+	(void)fflush(cout);
+	if (current_nop_pos >= NOOP_LENGTH) {
+		full_noops_sent++;
+		current_nop_pos = 0;
+	}
+}
+
+static void
+may_reset_noop_timeout(void)
+{
+	if (keep_alive_timeout != 0)
+		last_timestamp = time(NULL);
+}
+
+static void
+may_receive_noop_ack(void)
+{
+	int i;
+
+	if (cout == NULL) {
+		/* Lost connection;  so just pretend we're fine. */
+		current_nop_pos = full_noops_sent = 0;
+		return;
+	}
+
+	/* finish sending last incomplete noop */
+	if (current_nop_pos != 0) {
+		fputs(&(noop[current_nop_pos]), cout);
+#ifndef SMALL
+		if (debug)
+			fprintf(ttyout, "---> %s\n", &(noop[current_nop_pos]));
+#endif /* !SMALL */
+		(void)fflush(cout);
+		current_nop_pos = 0;
+		full_noops_sent++;
+	}
+	/* and get the replies */
+	for (i = 0; i < full_noops_sent; i++)
+		(void)getreply(0);
+
+	full_noops_sent = 0;
+}
+
+static void
+may_send_noop_char(void)
+{
+	if (keep_alive_timeout != 0) {
+		if (last_timestamp != 0) {
+			time_t t = time(NULL);
+
+			if (t - last_timestamp >= keep_alive_timeout) {
+				last_timestamp = t;
+				send_noop_char();
+			}
+		} else {
+			last_timestamp = time(NULL);
+		}
+	}
+}
+
+char reply_string[BUFSIZ];		/* first line of previous reply */
+
+int
+getreply(int expecteof)
+{
+	char current_line[BUFSIZ];	/* last line of previous reply */
+	int c, n, lineno;
+	int dig;
+	int originalcode = 0, continuation = 0;
+	sig_t oldintr;
+	int pflag = 0;
+	char *cp, *pt = pasv;
+
+	memset(current_line, 0, sizeof(current_line));
+	oldintr = signal(SIGINT, cmdabort);
+	for (lineno = 0 ;; lineno++) {
+		dig = n = code = 0;
+		cp = current_line;
+		while ((c = fgetc(cin)) != '\n') {
+			if (c == IAC) {		/* handle telnet commands */
+				switch (c = fgetc(cin)) {
+				case WILL:
+				case WONT:
+					c = fgetc(cin);
+					fprintf(cout, "%c%c%c", IAC, DONT, c);
+					(void)fflush(cout);
+					break;
+				case DO:
+				case DONT:
+					c = fgetc(cin);
+					fprintf(cout, "%c%c%c", IAC, WONT, c);
+					(void)fflush(cout);
+					break;
+				default:
+					break;
+				}
+				continue;
+			}
+			dig++;
+			if (c == EOF) {
+				if (expecteof) {
+					(void)signal(SIGINT, oldintr);
+					code = 221;
+					return (0);
+				}
+				lostpeer();
+				if (verbose) {
+					fputs(
+"421 Service not available, remote server has closed connection.\n", ttyout);
+					(void)fflush(ttyout);
+				}
+				code = 421;
+				return (4);
+			}
+			if (c != '\r' && (verbose > 0 ||
+			    ((verbose > -1 && n == '5' && dig > 4) &&
+			    (((!n && c < '5') || (n && n < '5')) ||
+			    !retry_connect)))) {
+				if (proxflag &&
+				   (dig == 1 || (dig == 5 && verbose == 0)))
+					fprintf(ttyout, "%s:", hostname);
+				(void)putc(c, ttyout);
+			}
+			if (dig < 4 && isdigit(c))
+				code = code * 10 + (c - '0');
+			if (!pflag && (code == 227 || code == 228))
+				pflag = 1;
+			else if (!pflag && code == 229)
+				pflag = 100;
+			if (dig > 4 && pflag == 1 && isdigit(c))
+				pflag = 2;
+			if (pflag == 2) {
+				if (c != '\r' && c != ')') {
+					if (pt < &pasv[sizeof(pasv) - 1])
+						*pt++ = c;
+				} else {
+					*pt = '\0';
+					pflag = 3;
+				}
+			}
+			if (pflag == 100 && c == '(')
+				pflag = 2;
+			if (dig == 4 && c == '-') {
+				if (continuation)
+					code = 0;
+				continuation++;
+			}
+			if (n == 0)
+				n = c;
+			if (cp < &current_line[sizeof(current_line) - 1])
+				*cp++ = c;
+		}
+		if (verbose > 0 || ((verbose > -1 && n == '5') &&
+		    (n < '5' || !retry_connect))) {
+			(void)putc(c, ttyout);
+			(void)fflush (ttyout);
+		}
+		if (lineno == 0) {
+			size_t len = cp - current_line;
+
+			if (len > sizeof(reply_string))
+				len = sizeof(reply_string);
+
+			(void)strlcpy(reply_string, current_line, len);
+		}
+		if (continuation && code != originalcode) {
+			if (originalcode == 0)
+				originalcode = code;
+			continue;
+		}
+		*cp = '\0';
+		if (n != '1')
+			cpend = 0;
+		(void)signal(SIGINT, oldintr);
+		if (code == 421 || originalcode == 421)
+			lostpeer();
+		if (abrtflag && oldintr != cmdabort && oldintr != SIG_IGN)
+			(*oldintr)(SIGINT);
+		return (n - '0');
+	}
+}
+
+#ifndef SMALL
+jmp_buf	sendabort;
+
+void
+abortsend(int signo)
+{
+	int save_errno = errno;
+	alarmtimer(0);
+	mflag = 0;
+	abrtflag = 0;
+#define MSG "\nsend aborted\nwaiting for remote to finish abort.\n"
+	(void) write(fileno(ttyout), MSG, strlen(MSG));
+#undef MSG
+
+	errno = save_errno;
+	longjmp(sendabort, 1);
+}
+
+void
+sendrequest(const char *cmd, const char *local, const char *remote,
+    int printnames)
+{
+	struct stat st;
+	int c, d;
+	FILE * volatile fin, * volatile dout;
+	int (* volatile closefunc)(FILE *);
+	volatile sig_t oldinti, oldintr, oldintp;
+	volatile off_t hashbytes;
+	char * volatile lmode;
+	char buf[BUFSIZ], *bufp;
+	int oprogress, serrno;
+
+	hashbytes = mark;
+	direction = "sent";
+	dout = NULL;
+	bytes = 0;
+	filesize = -1;
+	oprogress = progress;
+	if (verbose && printnames) {
+		if (local && *local != '-')
+			fprintf(ttyout, "local: %s ", local);
+		if (remote)
+			fprintf(ttyout, "remote: %s\n", remote);
+	}
+	if (proxy) {
+		proxtrans(cmd, local, remote);
+		return;
+	}
+	if (curtype != type)
+		changetype(type, 0);
+	closefunc = NULL;
+	oldintr = NULL;
+	oldintp = NULL;
+	oldinti = NULL;
+	lmode = "w";
+	if (setjmp(sendabort)) {
+		while (cpend) {
+			(void)getreply(0);
+		}
+		if (data >= 0) {
+			(void)close(data);
+			data = -1;
+		}
+		if (oldintr)
+			(void)signal(SIGINT, oldintr);
+		if (oldintp)
+			(void)signal(SIGPIPE, oldintp);
+		if (oldinti)
+			(void)signal(SIGINFO, oldinti);
+		progress = oprogress;
+		code = -1;
+		return;
+	}
+	oldintr = signal(SIGINT, abortsend);
+	oldinti = signal(SIGINFO, psummary);
+	if (strcmp(local, "-") == 0) {
+		fin = stdin;
+		if (progress == 1)
+			progress = 0;
+	} else if (*local == '|') {
+		oldintp = signal(SIGPIPE, SIG_IGN);
+		fin = popen(local + 1, "r");
+		if (fin == NULL) {
+			warn("%s", local + 1);
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGPIPE, oldintp);
+			(void)signal(SIGINFO, oldinti);
+			code = -1;
+			return;
+		}
+		if (progress == 1)
+			progress = 0;
+		closefunc = pclose;
+	} else {
+		fin = fopen(local, "r");
+		if (fin == NULL) {
+			warn("local: %s", local);
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			code = -1;
+			return;
+		}
+		closefunc = fclose;
+		if (fstat(fileno(fin), &st) == -1 ||
+		    (st.st_mode & S_IFMT) != S_IFREG) {
+			fprintf(ttyout, "%s: not a plain file.\n", local);
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			fclose(fin);
+			code = -1;
+			return;
+		}
+		filesize = st.st_size;
+	}
+	if (initconn()) {
+		(void)signal(SIGINT, oldintr);
+		(void)signal(SIGINFO, oldinti);
+		if (oldintp)
+			(void)signal(SIGPIPE, oldintp);
+		code = -1;
+		progress = oprogress;
+		if (closefunc != NULL)
+			(*closefunc)(fin);
+		return;
+	}
+	if (setjmp(sendabort))
+		goto abort;
+
+	if (restart_point &&
+	    (strcmp(cmd, "STOR") == 0 || strcmp(cmd, "APPE") == 0)) {
+		int rc = -1;
+
+		switch (curtype) {
+		case TYPE_A:
+			rc = fseeko(fin, restart_point, SEEK_SET);
+			break;
+		case TYPE_I:
+			if (lseek(fileno(fin), restart_point, SEEK_SET) != -1)
+				rc = 0;
+			break;
+		}
+		if (rc == -1) {
+			warn("local: %s", local);
+			progress = oprogress;
+			if (closefunc != NULL)
+				(*closefunc)(fin);
+			return;
+		}
+		if (command("REST %lld", (long long) restart_point)
+			!= CONTINUE) {
+			progress = oprogress;
+			if (closefunc != NULL)
+				(*closefunc)(fin);
+			return;
+		}
+		lmode = "r+w";
+	}
+	if (remote) {
+		if (command("%s %s", cmd, remote) != PRELIM) {
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			progress = oprogress;
+			if (oldintp)
+				(void)signal(SIGPIPE, oldintp);
+			if (closefunc != NULL)
+				(*closefunc)(fin);
+			return;
+		}
+	} else
+		if (command("%s", cmd) != PRELIM) {
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			progress = oprogress;
+			if (oldintp)
+				(void)signal(SIGPIPE, oldintp);
+			if (closefunc != NULL)
+				(*closefunc)(fin);
+			return;
+		}
+	dout = dataconn(lmode);
+	if (dout == NULL)
+		goto abort;
+	progressmeter(-1, remote);
+	may_reset_noop_timeout();
+	oldintp = signal(SIGPIPE, SIG_IGN);
+	serrno = 0;
+	switch (curtype) {
+
+	case TYPE_I:
+		d = 0;
+		while ((c = read(fileno(fin), buf, sizeof(buf))) > 0) {
+			may_send_noop_char();
+			bytes += c;
+			for (bufp = buf; c > 0; c -= d, bufp += d)
+				if ((d = write(fileno(dout), bufp, (size_t)c))
+				    <= 0)
+					break;
+			if (hash && (!progress || filesize < 0) ) {
+				while (bytes >= hashbytes) {
+					(void)putc('#', ttyout);
+					hashbytes += mark;
+				}
+				(void)fflush(ttyout);
+			}
+		}
+		if (c == -1 || d == -1)
+			serrno = errno;
+		if (hash && (!progress || filesize < 0) && bytes > 0) {
+			if (bytes < mark)
+				(void)putc('#', ttyout);
+			(void)putc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (c < 0)
+			warnc(serrno, "local: %s", local);
+		if (d < 0) {
+			if (serrno != EPIPE)
+				warnc(serrno, "netout");
+			bytes = -1;
+		}
+		break;
+
+	case TYPE_A:
+		while ((c = fgetc(fin)) != EOF) {
+			may_send_noop_char();
+			if (c == '\n') {
+				while (hash && (!progress || filesize < 0) &&
+				    (bytes >= hashbytes)) {
+					(void)putc('#', ttyout);
+					(void)fflush(ttyout);
+					hashbytes += mark;
+				}
+				if (ferror(dout))
+					break;
+				(void)putc('\r', dout);
+				bytes++;
+			}
+			(void)putc(c, dout);
+			bytes++;
+		}
+		if (ferror(fin) || ferror(dout))
+			serrno = errno;
+		if (hash && (!progress || filesize < 0)) {
+			if (bytes < hashbytes)
+				(void)putc('#', ttyout);
+			(void)putc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (ferror(fin))
+			warnc(serrno, "local: %s", local);
+		if (ferror(dout)) {
+			if (errno != EPIPE)
+				warnc(serrno, "netout");
+			bytes = -1;
+		}
+		break;
+	}
+	progressmeter(1, NULL);
+	progress = oprogress;
+	if (closefunc != NULL)
+		(*closefunc)(fin);
+	(void)fclose(dout);
+	(void)getreply(0);
+	may_receive_noop_ack();
+	(void)signal(SIGINT, oldintr);
+	(void)signal(SIGINFO, oldinti);
+	if (oldintp)
+		(void)signal(SIGPIPE, oldintp);
+	if (bytes > 0)
+		ptransfer(0);
+	return;
+abort:
+	(void)signal(SIGINT, oldintr);
+	(void)signal(SIGINFO, oldinti);
+	progress = oprogress;
+	if (oldintp)
+		(void)signal(SIGPIPE, oldintp);
+	if (!cpend) {
+		code = -1;
+		return;
+	}
+	if (data >= 0) {
+		(void)close(data);
+		data = -1;
+	}
+	if (dout)
+		(void)fclose(dout);
+	(void)getreply(0);
+	code = -1;
+	if (closefunc != NULL && fin != NULL)
+		(*closefunc)(fin);
+	if (bytes > 0)
+		ptransfer(0);
+}
+#endif /* !SMALL */
+
+jmp_buf	recvabort;
+
+void
+abortrecv(int signo)
+{
+
+	alarmtimer(0);
+	mflag = 0;
+	abrtflag = 0;
+	fputs("\nreceive aborted\nwaiting for remote to finish abort.\n", ttyout);
+	(void)fflush(ttyout);
+	longjmp(recvabort, 1);
+}
+
+void
+recvrequest(const char *cmd, const char * volatile local, const char *remote,
+    const char *lmode, int printnames, int ignorespecial)
+{
+	FILE * volatile fout, * volatile din;
+	int (* volatile closefunc)(FILE *);
+	volatile sig_t oldinti, oldintr, oldintp;
+	int c, d, serrno;
+	volatile int is_retr, tcrflag, bare_lfs;
+	static size_t bufsize;
+	static char *buf;
+	volatile off_t hashbytes;
+	struct stat st;
+	time_t mtime;
+	int oprogress;
+	int opreserve;
+
+	fout = NULL;
+	din = NULL;
+	oldinti = NULL;
+	hashbytes = mark;
+	direction = "received";
+	bytes = 0;
+	bare_lfs = 0;
+	filesize = -1;
+	oprogress = progress;
+	opreserve = preserve;
+	is_retr = strcmp(cmd, "RETR") == 0;
+	if (is_retr && verbose && printnames) {
+		if (local && (ignorespecial || *local != '-'))
+			fprintf(ttyout, "local: %s ", local);
+		if (remote)
+			fprintf(ttyout, "remote: %s\n", remote);
+	}
+	if (proxy && is_retr) {
+		proxtrans(cmd, local, remote);
+		return;
+	}
+	closefunc = NULL;
+	oldintr = NULL;
+	oldintp = NULL;
+	tcrflag = !crflag && is_retr;
+	if (setjmp(recvabort)) {
+		while (cpend) {
+			(void)getreply(0);
+		}
+		if (data >= 0) {
+			(void)close(data);
+			data = -1;
+		}
+		if (oldintr)
+			(void)signal(SIGINT, oldintr);
+		if (oldinti)
+			(void)signal(SIGINFO, oldinti);
+		progress = oprogress;
+		preserve = opreserve;
+		code = -1;
+		return;
+	}
+	oldintr = signal(SIGINT, abortrecv);
+	oldinti = signal(SIGINFO, psummary);
+	if (ignorespecial || (strcmp(local, "-") && *local != '|')) {
+		if (access(local, W_OK) == -1) {
+			char *dir;
+
+			if (errno != ENOENT && errno != EACCES) {
+				warn("local: %s", local);
+				(void)signal(SIGINT, oldintr);
+				(void)signal(SIGINFO, oldinti);
+				code = -1;
+				return;
+			}
+			dir = strrchr(local, '/');
+			if (dir != NULL)
+				*dir = 0;
+			d = access(dir == local ? "/" : dir ? local : ".", W_OK);
+			if (dir != NULL)
+				*dir = '/';
+			if (d == -1) {
+				warn("local: %s", local);
+				(void)signal(SIGINT, oldintr);
+				(void)signal(SIGINFO, oldinti);
+				code = -1;
+				return;
+			}
+			if (!runique && errno == EACCES &&
+			    chmod(local, (S_IRUSR|S_IWUSR)) == -1) {
+				warn("local: %s", local);
+				(void)signal(SIGINT, oldintr);
+				(void)signal(SIGINFO, oldinti);
+				code = -1;
+				return;
+			}
+			if (runique && errno == EACCES &&
+			   (local = gunique(local)) == NULL) {
+				(void)signal(SIGINT, oldintr);
+				(void)signal(SIGINFO, oldinti);
+				code = -1;
+				return;
+			}
+		} else if (runique && (local = gunique(local)) == NULL) {
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			code = -1;
+			return;
+		}
+	}
+	if (!is_retr) {
+		if (curtype != TYPE_A)
+			changetype(TYPE_A, 0);
+	} else {
+		if (curtype != type)
+			changetype(type, 0);
+		filesize = remotesize(remote, 0);
+	}
+	if (initconn()) {
+		(void)signal(SIGINT, oldintr);
+		(void)signal(SIGINFO, oldinti);
+		code = -1;
+		return;
+	}
+	if (setjmp(recvabort))
+		goto abort;
+	if (is_retr && restart_point &&
+	    command("REST %lld", (long long) restart_point) != CONTINUE)
+		return;
+	if (remote) {
+		if (command("%s %s", cmd, remote) != PRELIM) {
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			return;
+		}
+	} else {
+		if (command("%s", cmd) != PRELIM) {
+			(void)signal(SIGINT, oldintr);
+			(void)signal(SIGINFO, oldinti);
+			return;
+		}
+	}
+	din = dataconn("r");
+	if (din == NULL)
+		goto abort;
+	if (!ignorespecial && strcmp(local, "-") == 0) {
+		fout = stdout;
+		preserve = 0;
+	} else if (!ignorespecial && *local == '|') {
+		oldintp = signal(SIGPIPE, SIG_IGN);
+		fout = popen(local + 1, "w");
+		if (fout == NULL) {
+			warn("%s", local+1);
+			goto abort;
+		}
+		if (progress == 1)
+			progress = 0;
+		preserve = 0;
+		closefunc = pclose;
+	} else {
+		fout = fopen(local, lmode);
+		if (fout == NULL) {
+			warn("local: %s", local);
+			goto abort;
+		}
+		closefunc = fclose;
+	}
+	if (fstat(fileno(fout), &st) == -1 || st.st_blksize == 0)
+		st.st_blksize = BUFSIZ;
+	if (st.st_blksize > bufsize) {
+		(void)free(buf);
+		buf = malloc((unsigned)st.st_blksize);
+		if (buf == NULL) {
+			warn("malloc");
+			bufsize = 0;
+			goto abort;
+		}
+		bufsize = st.st_blksize;
+	}
+	if ((st.st_mode & S_IFMT) != S_IFREG) {
+		if (progress == 1)
+			progress = 0;
+		preserve = 0;
+	}
+	progressmeter(-1, remote);
+	may_reset_noop_timeout();
+	serrno = 0;
+	switch (curtype) {
+
+	case TYPE_I:
+		if (restart_point &&
+		    lseek(fileno(fout), restart_point, SEEK_SET) == -1) {
+			warn("local: %s", local);
+			progress = oprogress;
+			preserve = opreserve;
+			if (closefunc != NULL)
+				(*closefunc)(fout);
+			return;
+		}
+		errno = d = 0;
+		while ((c = read(fileno(din), buf, bufsize)) > 0) {
+			ssize_t	wr;
+			size_t	rd = c;
+
+			may_send_noop_char();
+			d = 0;
+			do {
+				wr = write(fileno(fout), buf + d, rd);
+				if (wr == -1) {
+					d = -1;
+					break;
+				}
+				d += wr;
+				rd -= wr;
+			} while (d < c);
+			if (rd != 0)
+				break;
+			bytes += c;
+			if (hash && (!progress || filesize < 0)) {
+				while (bytes >= hashbytes) {
+					(void)putc('#', ttyout);
+					hashbytes += mark;
+				}
+				(void)fflush(ttyout);
+			}
+		}
+		if (c == -1 || d < c)
+			serrno = errno;
+		if (hash && (!progress || filesize < 0) && bytes > 0) {
+			if (bytes < mark)
+				(void)putc('#', ttyout);
+			(void)putc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (c < 0) {
+			if (serrno != EPIPE)
+				warnc(serrno, "netin");
+			bytes = -1;
+		}
+		if (d < c) {
+			if (d < 0)
+				warnc(serrno, "local: %s", local);
+			else
+				warnx("%s: short write", local);
+		}
+		break;
+
+	case TYPE_A:
+		if (restart_point) {
+			int i, n, ch;
+
+			if (fseek(fout, 0L, SEEK_SET) == -1)
+				goto done;
+			n = restart_point;
+			for (i = 0; i++ < n;) {
+				if ((ch = fgetc(fout)) == EOF) {
+					if (!ferror(fout))
+						errno = 0;
+					goto done;
+				}
+				if (ch == '\n')
+					i++;
+			}
+			if (fseek(fout, 0L, SEEK_CUR) == -1) {
+done:
+				if (errno)
+					warn("local: %s", local);
+				else
+					warnx("local: %s", local);
+				progress = oprogress;
+				preserve = opreserve;
+				if (closefunc != NULL)
+					(*closefunc)(fout);
+				return;
+			}
+		}
+		while ((c = fgetc(din)) != EOF) {
+			may_send_noop_char();
+			if (c == '\n')
+				bare_lfs++;
+			while (c == '\r') {
+				while (hash && (!progress || filesize < 0) &&
+				    (bytes >= hashbytes)) {
+					(void)putc('#', ttyout);
+					(void)fflush(ttyout);
+					hashbytes += mark;
+				}
+				bytes++;
+				if ((c = fgetc(din)) != '\n' || tcrflag) {
+					if (ferror(fout))
+						goto break2;
+					(void)putc('\r', fout);
+					if (c == '\0') {
+						bytes++;
+						goto contin2;
+					}
+					if (c == EOF)
+						goto contin2;
+				}
+			}
+			(void)putc(c, fout);
+			bytes++;
+	contin2:	;
+		}
+break2:
+		if (ferror(din) || ferror(fout))
+			serrno = errno;
+		if (bare_lfs) {
+			fprintf(ttyout,
+"WARNING! %d bare linefeeds received in ASCII mode.\n", bare_lfs);
+			fputs("File may not have transferred correctly.\n",
+			    ttyout);
+		}
+		if (hash && (!progress || filesize < 0)) {
+			if (bytes < hashbytes)
+				(void)putc('#', ttyout);
+			(void)putc('\n', ttyout);
+			(void)fflush(ttyout);
+		}
+		if (ferror(din)) {
+			if (serrno != EPIPE)
+				warnc(serrno, "netin");
+			bytes = -1;
+		}
+		if (ferror(fout))
+			warnc(serrno, "local: %s", local);
+		break;
+	}
+	progressmeter(1, NULL);
+	progress = oprogress;
+	preserve = opreserve;
+	if (closefunc != NULL)
+		(*closefunc)(fout);
+	(void)signal(SIGINT, oldintr);
+	(void)signal(SIGINFO, oldinti);
+	if (oldintp)
+		(void)signal(SIGPIPE, oldintp);
+	(void)fclose(din);
+	(void)getreply(0);
+	may_receive_noop_ack();
+	if (bytes >= 0 && is_retr) {
+		if (bytes > 0)
+			ptransfer(0);
+		if (preserve && (closefunc == fclose)) {
+			mtime = remotemodtime(remote, 0);
+			if (mtime != -1) {
+				struct timespec times[2];
+
+				times[0].tv_nsec = UTIME_OMIT;
+				times[1].tv_sec = mtime;
+				times[1].tv_nsec = 0;
+				if (utimensat(AT_FDCWD, local, times, 0) == -1)
+					fprintf(ttyout,
+				"Can't change modification time on %s to %s",
+					    local, asctime(localtime(&mtime)));
+			}
+		}
+	}
+	return;
+
+abort:
+	/* abort using RFC959 recommended IP,SYNC sequence */
+	progress = oprogress;
+	preserve = opreserve;
+	if (oldintp)
+		(void)signal(SIGPIPE, oldintp);
+	(void)signal(SIGINT, SIG_IGN);
+	if (!cpend) {
+		code = -1;
+		(void)signal(SIGINT, oldintr);
+		(void)signal(SIGINFO, oldinti);
+		return;
+	}
+
+	abort_remote(din);
+	code = -1;
+	if (data >= 0) {
+		(void)close(data);
+		data = -1;
+	}
+	if (closefunc != NULL && fout != NULL)
+		(*closefunc)(fout);
+	if (din)
+		(void)fclose(din);
+	if (bytes > 0)
+		ptransfer(0);
+	(void)signal(SIGINT, oldintr);
+	(void)signal(SIGINFO, oldinti);
+}
+
+/*
+ * Need to start a listen on the data channel before we send the command,
+ * otherwise the server's connect may fail.
+ */
+int
+initconn(void)
+{
+	char *p, *a;
+	int result = ERROR, tmpno = 0;
+	int on = 1;
+	int error;
+	u_int addr[16], port[2];
+	u_int af, hal, pal;
+	char *pasvcmd = NULL;
+	socklen_t namelen;
+#ifndef SMALL
+	struct addrinfo *ares;
+#endif
+
+	if (myctladdr.sa.sa_family == AF_INET6
+	 && (IN6_IS_ADDR_LINKLOCAL(&myctladdr.sin6.sin6_addr)
+	  || IN6_IS_ADDR_SITELOCAL(&myctladdr.sin6.sin6_addr))) {
+		warnx("use of scoped address can be troublesome");
+	}
+#ifndef SMALL
+	if (srcaddr) {
+		struct addrinfo ahints;
+
+		memset(&ahints, 0, sizeof(ahints));
+		ahints.ai_family = family;
+		ahints.ai_socktype = SOCK_STREAM;
+		ahints.ai_flags |= AI_NUMERICHOST;
+		ahints.ai_protocol = 0;
+
+		error = getaddrinfo(srcaddr, NULL, &ahints, &ares);
+		if (error) {
+			warnx("%s: %s", srcaddr, gai_strerror(error));
+			code = -1;
+			return (0);
+		}
+	}
+#endif /* !SMALL */
+reinit:
+	if (passivemode) {
+		data_addr = myctladdr;
+		data = socket(data_addr.sa.sa_family, SOCK_STREAM, 0);
+		if (data == -1) {
+			warn("socket");
+			return (1);
+		}
+#ifndef SMALL
+		if (srcaddr) {
+			if (bind(data, ares->ai_addr, ares->ai_addrlen) == -1) {
+				warn("bind");
+				close(data);
+				return (1);
+			}
+		}
+		if ((options & SO_DEBUG) &&
+		    setsockopt(data, SOL_SOCKET, SO_DEBUG, (char *)&on,
+		    sizeof(on)) == -1)
+			warn("setsockopt (ignored)");
+#endif /* !SMALL */
+		switch (data_addr.sa.sa_family) {
+		case AF_INET:
+			if (epsv4 && !epsv4bad) {
+				int ov;
+				/* shut this command up in case it fails */
+				ov = verbose;
+				verbose = -1;
+				result = command(pasvcmd = "EPSV");
+				/*
+				 * now back to whatever verbosity we had before
+				 * and we can try PASV
+				 */
+				verbose = ov;
+				if (code / 10 == 22 && code != 229) {
+					fputs(
+"wrong server: return code must be 229\n",
+						ttyout);
+					result = COMPLETE + 1;
+				}
+				if (result != COMPLETE) {
+					epsv4bad = 1;
+#ifndef SMALL
+					if (debug) {
+						fputs(
+"disabling epsv4 for this connection\n",
+						    ttyout);
+					}
+#endif /* !SMALL */
+				}
+			}
+			if (result != COMPLETE)
+				result = command(pasvcmd = "PASV");
+			break;
+		case AF_INET6:
+			result = command(pasvcmd = "EPSV");
+			if (code / 10 == 22 && code != 229) {
+				fputs(
+"wrong server: return code must be 229\n",
+					ttyout);
+				result = COMPLETE + 1;
+			}
+			if (result != COMPLETE)
+				result = command(pasvcmd = "LPSV");
+			break;
+		default:
+			result = COMPLETE + 1;
+			break;
+		}
+		if (result != COMPLETE) {
+			if (activefallback) {
+				(void)close(data);
+				data = -1;
+				passivemode = 0;
+				activefallback = 0;
+				goto reinit;
+			}
+			fputs("Passive mode refused.\n", ttyout);
+			goto bad;
+		}
+
+#define pack2(var, off) \
+	(((var[(off) + 0] & 0xff) << 8) | ((var[(off) + 1] & 0xff) << 0))
+#define pack4(var, off) \
+	(((var[(off) + 0] & 0xff) << 24) | ((var[(off) + 1] & 0xff) << 16) | \
+	 ((var[(off) + 2] & 0xff) << 8) | ((var[(off) + 3] & 0xff) << 0))
+
+		/*
+		 * What we've got at this point is a string of comma separated
+		 * one-byte unsigned integer values, separated by commas.
+		 */
+		if (!pasvcmd)
+			goto bad;
+		if (strcmp(pasvcmd, "PASV") == 0) {
+			if (data_addr.sa.sa_family != AF_INET) {
+				fputs(
+"Passive mode AF mismatch. Shouldn't happen!\n", ttyout);
+				goto bad;
+			}
+			if (code / 10 == 22 && code != 227) {
+				fputs("wrong server: return code must be 227\n",
+					ttyout);
+				goto bad;
+			}
+			error = sscanf(pasv, "%u,%u,%u,%u,%u,%u",
+					&addr[0], &addr[1], &addr[2], &addr[3],
+					&port[0], &port[1]);
+			if (error != 6) {
+				fputs(
+"Passive mode address scan failure. Shouldn't happen!\n", ttyout);
+				goto bad;
+			}
+			memset(&data_addr, 0, sizeof(data_addr));
+			data_addr.sin.sin_family = AF_INET;
+			data_addr.sin.sin_len = sizeof(struct sockaddr_in);
+			data_addr.sin.sin_addr.s_addr =
+				htonl(pack4(addr, 0));
+			data_addr.sin.sin_port = htons(pack2(port, 0));
+		} else if (strcmp(pasvcmd, "LPSV") == 0) {
+			if (code / 10 == 22 && code != 228) {
+				fputs("wrong server: return code must be 228\n",
+					ttyout);
+				goto bad;
+			}
+			switch (data_addr.sa.sa_family) {
+			case AF_INET:
+				error = sscanf(pasv,
+"%u,%u,%u,%u,%u,%u,%u,%u,%u",
+					&af, &hal,
+					&addr[0], &addr[1], &addr[2], &addr[3],
+					&pal, &port[0], &port[1]);
+				if (error != 9) {
+					fputs(
+"Passive mode address scan failure. Shouldn't happen!\n", ttyout);
+					goto bad;
+				}
+				if (af != 4 || hal != 4 || pal != 2) {
+					fputs(
+"Passive mode AF mismatch. Shouldn't happen!\n", ttyout);
+					error = 1;
+					goto bad;
+				}
+
+				memset(&data_addr, 0, sizeof(data_addr));
+				data_addr.sin.sin_family = AF_INET;
+				data_addr.sin.sin_len = sizeof(struct sockaddr_in);
+				data_addr.sin.sin_addr.s_addr =
+					htonl(pack4(addr, 0));
+				data_addr.sin.sin_port = htons(pack2(port, 0));
+				break;
+			case AF_INET6:
+				error = sscanf(pasv,
+"%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u",
+					&af, &hal,
+					&addr[0], &addr[1], &addr[2], &addr[3],
+					&addr[4], &addr[5], &addr[6], &addr[7],
+					&addr[8], &addr[9], &addr[10],
+					&addr[11], &addr[12], &addr[13],
+					&addr[14], &addr[15],
+					&pal, &port[0], &port[1]);
+				if (error != 21) {
+					fputs(
+"Passive mode address scan failure. Shouldn't happen!\n", ttyout);
+					goto bad;
+				}
+				if (af != 6 || hal != 16 || pal != 2) {
+					fputs(
+"Passive mode AF mismatch. Shouldn't happen!\n", ttyout);
+					goto bad;
+				}
+
+				memset(&data_addr, 0, sizeof(data_addr));
+				data_addr.sin6.sin6_family = AF_INET6;
+				data_addr.sin6.sin6_len = sizeof(struct sockaddr_in6);
+			    {
+				u_int32_t *p32;
+				p32 = (u_int32_t *)&data_addr.sin6.sin6_addr;
+				p32[0] = htonl(pack4(addr, 0));
+				p32[1] = htonl(pack4(addr, 4));
+				p32[2] = htonl(pack4(addr, 8));
+				p32[3] = htonl(pack4(addr, 12));
+			    }
+				data_addr.sin6.sin6_port = htons(pack2(port, 0));
+				break;
+			default:
+				fputs("Bad family!\n", ttyout);
+				goto bad;
+			}
+		} else if (strcmp(pasvcmd, "EPSV") == 0) {
+			char delim[4];
+
+			port[0] = 0;
+			if (code / 10 == 22 && code != 229) {
+				fputs("wrong server: return code must be 229\n",
+					ttyout);
+				goto bad;
+			}
+			if (sscanf(pasv, "%c%c%c%d%c", &delim[0],
+					&delim[1], &delim[2], &port[1],
+					&delim[3]) != 5) {
+				fputs("parse error!\n", ttyout);
+				goto bad;
+			}
+			if (delim[0] != delim[1] || delim[0] != delim[2]
+			 || delim[0] != delim[3]) {
+				fputs("parse error!\n", ttyout);
+				goto bad;
+			}
+			data_addr = hisctladdr;
+			data_addr.sin.sin_port = htons(port[1]);
+		} else
+			goto bad;
+
+		error = timed_connect(data, &data_addr.sa, data_addr.sa.sa_len,
+		    connect_timeout);
+		if (error != 0) {
+			if (activefallback) {
+				(void)close(data);
+				data = -1;
+				passivemode = 0;
+				activefallback = 0;
+				goto reinit;
+			}
+			warn("connect");
+			goto bad;
+		}
+		if (data_addr.sa.sa_family == AF_INET) {
+			on = IPTOS_THROUGHPUT;
+			if (setsockopt(data, IPPROTO_IP, IP_TOS, (char *)&on,
+			    sizeof(int)) == -1)
+				warn("setsockopt TOS (ignored)");
+		}
+		return (0);
+	}
+
+noport:
+	data_addr = myctladdr;
+	if (sendport)
+		data_addr.sin.sin_port = 0;	/* let system pick one */
+	if (data != -1)
+		(void)close(data);
+	data = socket(data_addr.sa.sa_family, SOCK_STREAM, 0);
+	if (data == -1) {
+		warn("socket");
+		if (tmpno)
+			sendport = 1;
+		return (1);
+	}
+	if (!sendport)
+		if (setsockopt(data, SOL_SOCKET, SO_REUSEADDR, (char *)&on,
+				sizeof(on)) == -1) {
+			warn("setsockopt (reuse address)");
+			goto bad;
+		}
+	switch (data_addr.sa.sa_family) {
+	case AF_INET:
+		on = IP_PORTRANGE_HIGH;
+		if (setsockopt(data, IPPROTO_IP, IP_PORTRANGE,
+		    (char *)&on, sizeof(on)) == -1)
+			warn("setsockopt IP_PORTRANGE (ignored)");
+		break;
+	case AF_INET6:
+		on = IPV6_PORTRANGE_HIGH;
+		if (setsockopt(data, IPPROTO_IPV6, IPV6_PORTRANGE,
+		    (char *)&on, sizeof(on)) == -1)
+			warn("setsockopt IPV6_PORTRANGE (ignored)");
+		break;
+	}
+	if (bind(data, &data_addr.sa, data_addr.sa.sa_len) == -1) {
+		warn("bind");
+		goto bad;
+	}
+#ifndef SMALL
+	if (options & SO_DEBUG &&
+	    setsockopt(data, SOL_SOCKET, SO_DEBUG, (char *)&on,
+			sizeof(on)) == -1)
+		warn("setsockopt (ignored)");
+#endif /* !SMALL */
+	namelen = sizeof(data_addr);
+	if (getsockname(data, &data_addr.sa, &namelen) == -1) {
+		warn("getsockname");
+		goto bad;
+	}
+	if (listen(data, 1) == -1)
+		warn("listen");
+
+#define	UC(b)	(((int)b)&0xff)
+
+	if (sendport) {
+		char hname[NI_MAXHOST], pbuf[NI_MAXSERV];
+		int af_tmp;
+		union sockaddr_union tmp;
+
+		tmp = data_addr;
+		switch (tmp.sa.sa_family) {
+		case AF_INET:
+			if (!epsv4 || epsv4bad) {
+				result = COMPLETE +1;
+				break;
+			}
+			/*FALLTHROUGH*/
+		case AF_INET6:
+			if (tmp.sa.sa_family == AF_INET6)
+				tmp.sin6.sin6_scope_id = 0;
+			af_tmp = (tmp.sa.sa_family == AF_INET) ? 1 : 2;
+			if (getnameinfo(&tmp.sa, tmp.sa.sa_len, hname,
+			    sizeof(hname), pbuf, sizeof(pbuf),
+			    NI_NUMERICHOST | NI_NUMERICSERV)) {
+				result = ERROR;
+			} else {
+				result = command("EPRT |%d|%s|%s|",
+				    af_tmp, hname, pbuf);
+				if (result != COMPLETE) {
+					epsv4bad = 1;
+#ifndef SMALL
+					if (debug) {
+						fputs(
+"disabling epsv4 for this connection\n",
+						    ttyout);
+					}
+#endif /* !SMALL */
+				}
+			}
+			break;
+		default:
+			result = COMPLETE + 1;
+			break;
+		}
+		if (result == COMPLETE)
+			goto skip_port;
+
+		switch (data_addr.sa.sa_family) {
+		case AF_INET:
+			a = (char *)&data_addr.sin.sin_addr;
+			p = (char *)&data_addr.sin.sin_port;
+			result = command("PORT %d,%d,%d,%d,%d,%d",
+				 UC(a[0]), UC(a[1]), UC(a[2]), UC(a[3]),
+				 UC(p[0]), UC(p[1]));
+			break;
+		case AF_INET6:
+			a = (char *)&data_addr.sin6.sin6_addr;
+			p = (char *)&data_addr.sin6.sin6_port;
+			result = command(
+"LPRT %d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
+				 6, 16,
+				 UC(a[0]),UC(a[1]),UC(a[2]),UC(a[3]),
+				 UC(a[4]),UC(a[5]),UC(a[6]),UC(a[7]),
+				 UC(a[8]),UC(a[9]),UC(a[10]),UC(a[11]),
+				 UC(a[12]),UC(a[13]),UC(a[14]),UC(a[15]),
+				 2, UC(p[0]), UC(p[1]));
+			break;
+		default:
+			result = COMPLETE + 1; /* xxx */
+		}
+	skip_port:
+
+		if (result == ERROR && sendport == -1) {
+			sendport = 0;
+			tmpno = 1;
+			goto noport;
+		}
+		return (result != COMPLETE);
+	}
+	if (tmpno)
+		sendport = 1;
+	if (data_addr.sa.sa_family == AF_INET) {
+		on = IPTOS_THROUGHPUT;
+		if (setsockopt(data, IPPROTO_IP, IP_TOS, (char *)&on,
+		    sizeof(int)) == -1)
+			warn("setsockopt TOS (ignored)");
+	}
+	return (0);
+bad:
+	(void)close(data), data = -1;
+	if (tmpno)
+		sendport = 1;
+	return (1);
+}
+
+FILE *
+dataconn(const char *lmode)
+{
+	union sockaddr_union from;
+	socklen_t fromlen = myctladdr.sa.sa_len;
+	int s;
+
+	if (passivemode)
+		return (fdopen(data, lmode));
+
+	s = accept(data, &from.sa, &fromlen);
+	if (s == -1) {
+		warn("accept");
+		(void)close(data), data = -1;
+		return (NULL);
+	}
+	(void)close(data);
+	data = s;
+	if (from.sa.sa_family == AF_INET) {
+		int tos = IPTOS_THROUGHPUT;
+		if (setsockopt(s, IPPROTO_IP, IP_TOS, (char *)&tos,
+				sizeof(int)) == -1) {
+			warn("setsockopt TOS (ignored)");
+		}
+	}
+	return (fdopen(data, lmode));
+}
+
+void
+psummary(int signo)
+{
+	int save_errno = errno;
+
+	if (bytes > 0)
+		ptransfer(1);
+	errno = save_errno;
+}
+
+void
+psabort(int signo)
+{
+
+	alarmtimer(0);
+	abrtflag++;
+}
+
+void
+pswitch(int flag)
+{
+	sig_t oldintr;
+	static struct comvars {
+		int connect;
+		char name[HOST_NAME_MAX+1];
+		union sockaddr_union mctl;
+		union sockaddr_union hctl;
+		FILE *in;
+		FILE *out;
+		int tpe;
+		int curtpe;
+		int cpnd;
+		int sunqe;
+		int runqe;
+		int mcse;
+		int ntflg;
+		char nti[17];
+		char nto[17];
+		int mapflg;
+		char mi[PATH_MAX];
+		char mo[PATH_MAX];
+	} proxstruct, tmpstruct;
+	struct comvars *ip, *op;
+
+	abrtflag = 0;
+	oldintr = signal(SIGINT, psabort);
+	if (flag) {
+		if (proxy)
+			return;
+		ip = &tmpstruct;
+		op = &proxstruct;
+		proxy++;
+	} else {
+		if (!proxy)
+			return;
+		ip = &proxstruct;
+		op = &tmpstruct;
+		proxy = 0;
+	}
+	ip->connect = connected;
+	connected = op->connect;
+	if (hostname) {
+		(void)strlcpy(ip->name, hostname, sizeof(ip->name));
+	} else
+		ip->name[0] = '\0';
+	hostname = op->name;
+	ip->hctl = hisctladdr;
+	hisctladdr = op->hctl;
+	ip->mctl = myctladdr;
+	myctladdr = op->mctl;
+	ip->in = cin;
+	cin = op->in;
+	ip->out = cout;
+	cout = op->out;
+	ip->tpe = type;
+	type = op->tpe;
+	ip->curtpe = curtype;
+	curtype = op->curtpe;
+	ip->cpnd = cpend;
+	cpend = op->cpnd;
+	ip->sunqe = sunique;
+	sunique = op->sunqe;
+	ip->runqe = runique;
+	runique = op->runqe;
+	ip->mcse = mcase;
+	mcase = op->mcse;
+	ip->ntflg = ntflag;
+	ntflag = op->ntflg;
+	(void)strlcpy(ip->nti, ntin, sizeof(ip->nti));
+	(void)strlcpy(ntin, op->nti, sizeof ntin);
+	(void)strlcpy(ip->nto, ntout, sizeof(ip->nto));
+	(void)strlcpy(ntout, op->nto, sizeof ntout);
+	ip->mapflg = mapflag;
+	mapflag = op->mapflg;
+	(void)strlcpy(ip->mi, mapin, sizeof(ip->mi));
+	(void)strlcpy(mapin, op->mi, sizeof mapin);
+	(void)strlcpy(ip->mo, mapout, sizeof(ip->mo));
+	(void)strlcpy(mapout, op->mo, sizeof mapout);
+	(void)signal(SIGINT, oldintr);
+	if (abrtflag) {
+		abrtflag = 0;
+		(*oldintr)(SIGINT);
+	}
+}
+
+void
+abortpt(int signo)
+{
+
+	alarmtimer(0);
+	putc('\n', ttyout);
+	(void)fflush(ttyout);
+	ptabflg++;
+	mflag = 0;
+	abrtflag = 0;
+	longjmp(ptabort, 1);
+}
+
+void
+proxtrans(const char *cmd, const char *local, const char *remote)
+{
+	volatile sig_t oldintr;
+	int prox_type, nfnd;
+	volatile int secndflag;
+	char * volatile cmd2;
+	struct pollfd pfd[1];
+
+	oldintr = NULL;
+	secndflag = 0;
+	if (strcmp(cmd, "RETR"))
+		cmd2 = "RETR";
+	else
+		cmd2 = runique ? "STOU" : "STOR";
+	if ((prox_type = type) == 0) {
+		if (unix_server && unix_proxy)
+			prox_type = TYPE_I;
+		else
+			prox_type = TYPE_A;
+	}
+	if (curtype != prox_type)
+		changetype(prox_type, 1);
+	if (command("PASV") != COMPLETE) {
+		fputs("proxy server does not support third party transfers.\n",
+		    ttyout);
+		return;
+	}
+	pswitch(0);
+	if (!connected) {
+		fputs("No primary connection.\n", ttyout);
+		pswitch(1);
+		code = -1;
+		return;
+	}
+	if (curtype != prox_type)
+		changetype(prox_type, 1);
+	if (command("PORT %s", pasv) != COMPLETE) {
+		pswitch(1);
+		return;
+	}
+	if (setjmp(ptabort))
+		goto abort;
+	oldintr = signal(SIGINT, abortpt);
+	if (command("%s %s", cmd, remote) != PRELIM) {
+		(void)signal(SIGINT, oldintr);
+		pswitch(1);
+		return;
+	}
+	sleep(2);
+	pswitch(1);
+	secndflag++;
+	if (command("%s %s", cmd2, local) != PRELIM)
+		goto abort;
+	ptflag++;
+	(void)getreply(0);
+	pswitch(0);
+	(void)getreply(0);
+	(void)signal(SIGINT, oldintr);
+	pswitch(1);
+	ptflag = 0;
+	fprintf(ttyout, "local: %s remote: %s\n", local, remote);
+	return;
+abort:
+	(void)signal(SIGINT, SIG_IGN);
+	ptflag = 0;
+	if (strcmp(cmd, "RETR") && !proxy)
+		pswitch(1);
+	else if (!strcmp(cmd, "RETR") && proxy)
+		pswitch(0);
+	if (!cpend && !secndflag) {  /* only here if cmd = "STOR" (proxy=1) */
+		if (command("%s %s", cmd2, local) != PRELIM) {
+			pswitch(0);
+			if (cpend)
+				abort_remote(NULL);
+		}
+		pswitch(1);
+		if (ptabflg)
+			code = -1;
+		(void)signal(SIGINT, oldintr);
+		return;
+	}
+	if (cpend)
+		abort_remote(NULL);
+	pswitch(!proxy);
+	if (!cpend && !secndflag) {  /* only if cmd = "RETR" (proxy=1) */
+		if (command("%s %s", cmd2, local) != PRELIM) {
+			pswitch(0);
+			if (cpend)
+				abort_remote(NULL);
+			pswitch(1);
+			if (ptabflg)
+				code = -1;
+			(void)signal(SIGINT, oldintr);
+			return;
+		}
+	}
+	if (cpend)
+		abort_remote(NULL);
+	pswitch(!proxy);
+	if (cpend) {
+		pfd[0].fd = fileno(cin);
+		pfd[0].events = POLLIN;
+		if ((nfnd = poll(pfd, 1, 10 * 1000)) <= 0) {
+			if (nfnd == -1)
+				warn("abort");
+			if (ptabflg)
+				code = -1;
+			lostpeer();
+		}
+		(void)getreply(0);
+		(void)getreply(0);
+	}
+	if (proxy)
+		pswitch(0);
+	pswitch(1);
+	if (ptabflg)
+		code = -1;
+	(void)signal(SIGINT, oldintr);
+}
+
+#ifndef SMALL
+void
+reset(int argc, char *argv[])
+{
+	struct pollfd pfd[1];
+	int nfnd = 1;
+
+	pfd[0].fd = fileno(cin);
+	pfd[0].events = POLLIN;
+	while (nfnd > 0) {
+		if ((nfnd = poll(pfd, 1, 0)) == -1) {
+			warn("reset");
+			code = -1;
+			lostpeer();
+		} else if (nfnd) {
+			(void)getreply(0);
+		}
+	}
+}
+#endif
+
+char *
+gunique(const char *local)
+{
+	static char new[PATH_MAX];
+	char *cp = strrchr(local, '/');
+	int d, count=0;
+	char ext = '1';
+
+	if (cp)
+		*cp = '\0';
+	d = access(cp == local ? "/" : cp ? local : ".", W_OK);
+	if (cp)
+		*cp = '/';
+	if (d == -1) {
+		warn("local: %s", local);
+		return ((char *) 0);
+	}
+	(void)strlcpy(new, local, sizeof new);
+	cp = new + strlen(new);
+	*cp++ = '.';
+	while (!d) {
+		if (++count == 100) {
+			fputs("runique: can't find unique file name.\n", ttyout);
+			return ((char *) 0);
+		}
+		*cp++ = ext;
+		*cp = '\0';
+		if (ext == '9')
+			ext = '0';
+		else
+			ext++;
+		if ((d = access(new, F_OK)) == -1)
+			break;
+		if (ext != '0')
+			cp--;
+		else if (*(cp - 2) == '.')
+			*(cp - 1) = '1';
+		else {
+			*(cp - 2) = *(cp - 2) + 1;
+			cp--;
+		}
+	}
+	return (new);
+}
+
+jmp_buf forceabort;
+
+static void
+abortforce(int signo)
+{
+	int save_errno = errno;
+
+#define MSG	"Forced abort.  The connection will be closed.\n"
+	(void) write(fileno(ttyout), MSG, strlen(MSG));
+#undef MSG
+
+	errno = save_errno;
+	longjmp(forceabort, 1);
+}
+
+void
+abort_remote(FILE *din)
+{
+	char buf[BUFSIZ];
+	nfds_t nfds;
+	int nfnd;
+	struct pollfd pfd[2];
+	sig_t oldintr;
+
+	if (cout == NULL || setjmp(forceabort)) {
+		if (cout)
+			fclose(cout);
+		warnx("Lost control connection for abort.");
+		if (ptabflg)
+			code = -1;
+		lostpeer();
+		return;
+	}
+
+	oldintr = signal(SIGINT, abortforce);
+
+	/*
+	 * send IAC in urgent mode instead of DM because 4.3BSD places oob mark
+	 * after urgent byte rather than before as is protocol now
+	 */
+	snprintf(buf, sizeof buf, "%c%c%c", IAC, IP, IAC);
+	if (send(fileno(cout), buf, 3, MSG_OOB) != 3)
+		warn("abort");
+	fprintf(cout, "%cABOR\r\n", DM);
+	(void)fflush(cout);
+	pfd[0].fd = fileno(cin);
+	pfd[0].events = POLLIN;
+	nfds = 1;
+	if (din) {
+		pfd[1].fd = fileno(din);
+		pfd[1].events = POLLIN;
+		nfds++;
+	}
+	if ((nfnd = poll(pfd, nfds, 10 * 1000)) <= 0) {
+		if (nfnd == -1)
+			warn("abort");
+		if (ptabflg)
+			code = -1;
+		lostpeer();
+	}
+	if (din && (pfd[1].revents & POLLIN)) {
+		while (read(fileno(din), buf, BUFSIZ) > 0)
+			/* LOOP */;
+	}
+	if (getreply(0) == ERROR && code == 552) {
+		/* 552 needed for nic style abort */
+		(void)getreply(0);
+	}
+	(void)getreply(0);
+	(void)signal(SIGINT, oldintr);
+}
diff --git a/ftp_var.h b/ftp_var.h
new file mode 100644
index 0000000..878ae4d
--- /dev/null
+++ b/ftp_var.h
@@ -0,0 +1,241 @@
+/*	$OpenBSD: ftp_var.h,v 1.46 2021/02/02 12:58:42 robert Exp $	*/
+/*	$NetBSD: ftp_var.h,v 1.18 1997/08/18 10:20:25 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)ftp_var.h	8.4 (Berkeley) 10/9/94
+ */
+
+/*
+ * FTP global variables.
+ */
+
+#include <sys/signal.h>
+#include <limits.h>
+#include <setjmp.h>
+
+#include "compat.h"
+
+#ifndef SMALL
+#include <histedit.h>
+#endif /* !SMALL */
+
+#ifndef NOSSL
+#ifdef __OpenBSD__
+#include <tls.h>
+#else
+#include "tls_compat.h"
+#endif
+#endif
+
+#include "stringlist.h"
+#include "extern.h"
+#include "small.h"
+
+#define HASHBYTES	1024
+#define FTPBUFLEN	PATH_MAX + 200
+
+#define STALLTIME	5	/* # of seconds of no xfer before "stalling" */
+
+#define	FTP_PORT	21	/* default if ! getservbyname("ftp/tcp") */
+#define	GATE_PORT	21	/* default if ! getservbyname("ftpgate/tcp") */
+#define	HTTP_PORT	80	/* default if ! getservbyname("http/tcp") */
+#define	HTTPS_PORT	443	/* default if ! getservbyname("https/tcp") */
+#define	HTTP_USER_AGENT	"User-Agent: OpenBSD ftp"	/* User-Agent string sent to web server */
+
+#define PAGER		"more"	/* default pager if $PAGER isn't set */
+
+/*
+ * Options and other state info.
+ */
+extern int	trace;		/* trace packets exchanged */
+extern int	hash;		/* print # for each buffer transferred */
+extern int	mark;		/* number of bytes between hashes */
+extern int	sendport;	/* use PORT/LPRT cmd for each data connection */
+extern int	verbose;	/* print messages coming back from server */
+extern int	connected;	/* 1 = connected to server, -1 = logged in */
+extern int	fromatty;	/* input is from a terminal */
+extern int	interactive;	/* interactively prompt on m* cmds */
+#ifndef SMALL
+extern int	confirmrest;	/* confirm rest of current m* cmd */
+extern int	debug;		/* debugging level */
+extern int	bell;		/* ring bell on cmd completion */
+extern char   *altarg;		/* argv[1] with no shell-like preprocessing  */
+#endif /* !SMALL */
+extern int	doglob;		/* glob local file names */
+extern int	autologin;	/* establish user account on connection */
+extern int	proxy;		/* proxy server connection active */
+extern int	proxflag;	/* proxy connection exists */
+extern int	gatemode;	/* use gate-ftp */
+extern char   *gateserver;	/* server to use for gate-ftp */
+extern int	sunique;	/* store files on server with unique name */
+extern int	runique;	/* store local files with unique name */
+extern int	mcase;		/* map upper to lower case for mget names */
+extern int	ntflag;		/* use ntin ntout tables for name translation */
+extern int	mapflag;	/* use mapin mapout templates on file names */
+extern int	preserve;	/* preserve modification time on files */
+extern int	progress;	/* display transfer progress bar */
+extern int	code;		/* return/reply code for ftp command */
+extern int	crflag;		/* if 1, strip car. rets. on ascii gets */
+extern char	pasv[BUFSIZ];	/* passive port for proxy data connection */
+extern int	passivemode;	/* passive mode enabled */
+extern int	activefallback;	/* fall back to active mode if passive fails */
+extern char	ntin[17];	/* input translation table */
+extern char	ntout[17];	/* output translation table */
+extern char	mapin[PATH_MAX];	/* input map template */
+extern char	mapout[PATH_MAX];	/* output map template */
+extern char	typename[32];	/* name of file transfer type */
+extern int	type;		/* requested file transfer type */
+extern int	curtype;	/* current file transfer type */
+extern char	structname[32];	/* name of file transfer structure */
+extern int	stru;		/* file transfer structure */
+extern char	formname[32];	/* name of file transfer format */
+extern int	form;		/* file transfer format */
+extern char	modename[32];	/* name of file transfer mode */
+extern int	mode;		/* file transfer mode */
+extern char	bytename[32];	/* local byte size in ascii */
+extern int	bytesize;	/* local byte size in binary */
+extern int	anonftp;	/* automatic anonymous login */
+extern int	dirchange;	/* remote directory changed by cd command */
+extern unsigned int retry_connect;	/* retry connect if failed */
+extern int	ttywidth;	/* width of tty */
+extern int	epsv4;		/* use EPSV/EPRT on IPv4 connections */
+extern int	epsv4bad;	/* EPSV doesn't work on the current server */
+
+#ifndef SMALL
+extern int	  editing;	/* command line editing enabled */
+extern EditLine *el;		/* editline(3) status structure */
+extern History  *hist;		/* editline(3) history structure */
+extern char	 *cursor_pos;	/* cursor position we're looking for */
+extern size_t	  cursor_argc;	/* location of cursor in margv */
+extern size_t	  cursor_argo;	/* offset of cursor in margv[cursor_argc] */
+extern int	  resume;	/* continue transfer */
+extern int	  timestamp;    /* send an If-Modified-Since header */
+extern char	 *srcaddr;	/* source address to bind to */
+#endif /* !SMALL */
+
+extern char	 *cookiefile;	/* cookie jar to use */
+
+extern off_t	bytes;		/* current # of bytes read */
+extern off_t	filesize;	/* size of file being transferred */
+extern char   *direction;	/* direction transfer is occurring */
+
+extern char   *hostname;	/* name of host connected to */
+extern int	unix_server;	/* server is unix, can use binary for ascii */
+extern int	unix_proxy;	/* proxy is unix, can use binary for ascii */
+
+extern char *ftpport;		/* port number to use for ftp connections */
+extern char *httpport;		/* port number to use for http connections */
+#ifndef NOSSL
+extern char *httpsport;		/* port number to use for https connections */
+#endif /* !SMALL */
+extern char *httpuseragent;	/* user agent for http(s) connections */
+extern char *gateport;		/* port number to use for gateftp connections */
+
+extern jmp_buf	toplevel;	/* non-local goto stuff for cmd scanner */
+
+#ifndef SMALL
+extern char	line[FTPBUFLEN];	/* input line buffer */
+extern char	*argbase;	/* current storage point in arg buffer */
+extern char	*stringbase;	/* current scan point in line buffer */
+extern char	argbuf[FTPBUFLEN];	/* argument storage buffer */
+extern StringList *marg_sl;	/* stringlist containing margv */
+extern int	margc;		/* count of arguments on input line */
+extern int	options;	/* used during socket creation */
+#endif /* !SMALL */
+
+#define margv (marg_sl->sl_str)	/* args parsed from input line */
+extern int	cpend;		/* flag: if != 0, then pending server reply */
+extern int	mflag;		/* flag: if != 0, then active multi command */
+
+/*
+ * Format of command table.
+ */
+struct cmd {
+	char	*c_name;	/* name of command */
+	char	*c_help;	/* help string */
+	char	 c_bell;	/* give bell when command completes */
+	char	 c_conn;	/* must be connected to use command */
+	char	 c_proxy;	/* proxy server may execute */
+#ifndef SMALL
+	char	*c_complete;	/* context sensitive completion list */
+#endif /* !SMALL */
+	void	(*c_handler)(int, char **); /* function to call */
+};
+
+struct macel {
+	char mac_name[9];	/* macro name */
+	char *mac_start;	/* start of macro in macbuf */
+	char *mac_end;		/* end of macro in macbuf */
+};
+
+#ifndef SMALL
+extern int macnum;		/* number of defined macros */
+extern struct macel macros[16];
+extern char macbuf[4096];
+#endif /* !SMALL */
+
+extern FILE	*ttyout;	/* stdout or stderr, depending on interactive */
+
+extern struct cmd cmdtab[];
+
+#ifndef NOSSL
+#include "tls_compat.h"
+extern tls_config_t tls_config;
+extern int tls_session_fd;
+#endif /* !NOSSL */
diff --git a/list.c b/list.c
new file mode 100644
index 0000000..d95945e
--- /dev/null
+++ b/list.c
@@ -0,0 +1,86 @@
+/*	$OpenBSD: list.c,v 1.9 2019/05/16 12:44:18 florian Exp $	*/
+
+/*
+ * Copyright (c) 2008 Martynas Venckus <martynas@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SMALL
+
+#include <string.h>
+
+void	parse_list(char **, char *);
+
+static void
+parse_unix(char **line, char *type)
+{
+	char *tok;
+	int field = 0;
+
+	while ((tok = strsep(line, " \t")) != NULL) {
+		if (*tok == '\0')
+			continue;
+
+		if (field == 0)
+			*type = *tok;
+
+		if (field == 7) {
+			if (line == NULL || *line == NULL)
+				break;
+			while (**line == ' ' || **line == '\t')
+				(*line)++;
+			break;
+		}
+
+		field++;
+	}
+}
+
+static void
+parse_windows(char **line, char *type)
+{
+	char *tok;
+	int field = 0;
+
+	*type = '-';
+	while ((tok = strsep(line, " \t")) != NULL) {
+		if (*tok == '\0')
+			continue;
+
+		if (field == 2 && strcmp(tok, "<DIR>") == 0)
+			*type = 'd';
+
+		if (field == 2) {
+			if (line == NULL || *line == NULL)
+				break;
+			while (**line == ' ' || **line == '\t')
+				(*line)++;
+			break;
+		}
+
+		field++;
+	}
+}
+
+void
+parse_list(char **line, char *type)
+{
+	if (**line >= '0' && **line <= '9')
+		parse_windows(line, type);
+	else
+		parse_unix(line, type);
+}
+
+#endif /* !SMALL */
+
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..0124714
--- /dev/null
+++ b/main.c
@@ -0,0 +1,1093 @@
+/*	$OpenBSD: main.c,v 1.146 2023/12/23 23:03:00 kn Exp $	*/
+/*	$NetBSD: main.c,v 1.24 1997/08/18 10:20:26 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * FTP User Program -- Command Interface.
+ */
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifndef NOSSL
+#include "tls_compat.h"
+#endif
+
+#include "cmds.h"
+#include "ftp_var.h"
+
+int	trace;
+int	hash;
+int	mark;
+int	sendport;
+int	verbose;
+int	connected;
+int	fromatty;
+int	interactive;
+#ifndef SMALL
+int	confirmrest;
+int	debug;
+int	bell;
+char   *altarg;
+#endif /* !SMALL */
+int	doglob;
+int	autologin;
+int	proxy;
+int	proxflag;
+int	gatemode;
+char   *gateserver;
+int	sunique;
+int	runique;
+int	mcase;
+int	ntflag;
+int	mapflag;
+int	preserve;
+int	progress;
+int	code;
+int	crflag;
+char	pasv[BUFSIZ];
+int	passivemode;
+int	activefallback;
+char	ntin[17];
+char	ntout[17];
+char	mapin[PATH_MAX];
+char	mapout[PATH_MAX];
+char	typename[32];
+int	type;
+int	curtype;
+char	structname[32];
+int	stru;
+char	formname[32];
+int	form;
+char	modename[32];
+int	mode;
+char	bytename[32];
+int	bytesize;
+int	anonftp;
+int	dirchange;
+unsigned int retry_connect;
+int	ttywidth;
+int	epsv4;
+int	epsv4bad;
+
+#ifndef SMALL
+int	  editing;
+EditLine *el;
+History  *hist;
+char	 *cursor_pos;
+size_t	  cursor_argc;
+size_t	  cursor_argo;
+int	  resume;
+char	 *srcaddr;
+int  	  timestamp;
+#endif /* !SMALL */
+
+char	 *cookiefile;
+
+off_t	bytes;
+off_t	filesize;
+char   *direction;
+
+char   *hostname;
+int	unix_server;
+int	unix_proxy;
+
+char *ftpport;
+char *httpport;
+#ifndef NOSSL
+char *httpsport;
+#endif /* !SMALL */
+char *httpuseragent;
+char *gateport;
+
+jmp_buf	toplevel;
+
+#ifndef SMALL
+char	line[FTPBUFLEN];
+char	*argbase;
+char	*stringbase;
+char	argbuf[FTPBUFLEN];
+StringList *marg_sl;
+int	margc;
+int	options;
+#endif /* !SMALL */
+
+int	cpend;
+int	mflag;
+
+#ifndef SMALL
+int macnum;
+struct macel macros[16];
+char macbuf[4096];
+#endif /* !SMALL */
+
+FILE	*ttyout;
+
+int connect_timeout;
+
+#ifndef SMALL
+/* enable using server timestamps by default */
+int server_timestamps = 1;
+#endif
+
+#ifndef NOSSL
+char * const ssl_verify_opts[] = {
+#define SSL_CAFILE		0
+	"cafile",
+#define SSL_CAPATH		1
+	"capath",
+#define SSL_CIPHERS		2
+	"ciphers",
+#define SSL_DONTVERIFY		3
+	"dont",
+#define SSL_DOVERIFY		4
+	"do",
+#define SSL_VERIFYDEPTH		5
+	"depth",
+#define SSL_MUSTSTAPLE		6
+	"muststaple",
+#define SSL_NOVERIFYTIME	7
+	"noverifytime",
+#define SSL_SESSION		8
+	"session",
+#define SSL_PROTOCOLS		9
+	"protocols",
+	NULL
+};
+
+tls_config_t tls_config;
+int tls_session_fd = -1;
+
+static void
+process_ssl_options(char *cp)
+{
+	const char *errstr;
+	char *str;
+	int depth;
+	uint32_t protocols;
+
+	while (*cp) {
+		switch (getsubopt(&cp, ssl_verify_opts, &str)) {
+		case SSL_CAFILE:
+			if (str == NULL)
+				errx(1, "missing CA file");
+			if (tls_config_set_ca_file(tls_config, str) != 0)
+				errx(1, "tls ca file failed: %s",
+				    tls_config_error(tls_config));
+			break;
+		case SSL_CAPATH:
+			if (str == NULL)
+				errx(1, "missing CA directory path");
+			if (tls_config_set_ca_path(tls_config, str) != 0)
+				errx(1, "tls ca path failed: %s",
+				    tls_config_error(tls_config));
+			break;
+		case SSL_CIPHERS:
+			if (str == NULL)
+				errx(1, "missing cipher list");
+			if (tls_config_set_ciphers(tls_config, str) != 0)
+				errx(1, "tls ciphers failed: %s",
+				    tls_config_error(tls_config));
+			break;
+		case SSL_DONTVERIFY:
+			tls_config_insecure_noverifycert(tls_config);
+			tls_config_insecure_noverifyname(tls_config);
+			break;
+		case SSL_DOVERIFY:
+			tls_config_verify(tls_config);
+			break;
+		case SSL_VERIFYDEPTH:
+			if (str == NULL)
+				errx(1, "missing depth");
+			depth = strtonum(str, 0, INT_MAX, &errstr);
+			if (errstr)
+				errx(1, "certificate validation depth is %s",
+				    errstr);
+			tls_config_set_verify_depth(tls_config, depth);
+			break;
+		case SSL_MUSTSTAPLE:
+			tls_config_ocsp_require_stapling(tls_config);
+			break;
+		case SSL_NOVERIFYTIME:
+			tls_config_insecure_noverifytime(tls_config);
+			break;
+		case SSL_SESSION:
+			if (str == NULL)
+				errx(1, "missing session file");
+			if ((tls_session_fd = open(str, O_RDWR|O_CREAT,
+			    0600)) == -1)
+				err(1, "failed to open or create session file "
+				    "'%s'", str);
+			if (tls_config_set_session_fd(tls_config,
+			    tls_session_fd) == -1)
+				errx(1, "failed to set session: %s",
+				    tls_config_error(tls_config));
+			break;
+		case SSL_PROTOCOLS:
+			if (str == NULL)
+				errx(1, "missing protocol name");
+			if (tls_config_parse_protocols(&protocols, str) != 0)
+				errx(1, "failed to parse TLS protocols");
+			if (tls_config_set_protocols(tls_config, protocols) != 0)
+				errx(1, "failed to set TLS protocols: %s",
+				    tls_config_error(tls_config));
+			break;
+		default:
+			errx(1, "unknown -S suboption `%s'",
+			    suboptarg ? suboptarg : "");
+			/* NOTREACHED */
+		}
+	}
+}
+#endif /* !NOSSL */
+
+int family = PF_UNSPEC;
+int pipeout;
+
+int
+main(volatile int argc, char *argv[])
+{
+	int ch, rval;
+#ifndef SMALL
+	int top;
+#endif
+	struct passwd *pw = NULL;
+	char *cp, homedir[PATH_MAX];
+	char *outfile = NULL;
+	const char *errstr;
+	int dumb_terminal = 0;
+
+	ftpport = "ftp";
+	httpport = "http";
+#ifndef NOSSL
+	httpsport = "https";
+#endif /* !NOSSL */
+	gateport = getenv("FTPSERVERPORT");
+	if (gateport == NULL || *gateport == '\0')
+		gateport = "ftpgate";
+	doglob = 1;
+	interactive = 1;
+	autologin = 1;
+	passivemode = 1;
+	activefallback = 1;
+	preserve = 1;
+	verbose = 0;
+	progress = 0;
+	gatemode = 0;
+#ifndef NOSSL
+	cookiefile = NULL;
+#endif /* NOSSL */
+#ifndef SMALL
+	editing = 0;
+	el = NULL;
+	hist = NULL;
+	resume = 0;
+	timestamp = 0;
+	srcaddr = NULL;
+	marg_sl = sl_init();
+#endif /* !SMALL */
+	mark = HASHBYTES;
+	epsv4 = 1;
+	epsv4bad = 0;
+
+	/* Set default operation mode based on FTPMODE environment variable */
+	if ((cp = getenv("FTPMODE")) != NULL && *cp != '\0') {
+		if (strcmp(cp, "passive") == 0) {
+			passivemode = 1;
+			activefallback = 0;
+		} else if (strcmp(cp, "active") == 0) {
+			passivemode = 0;
+			activefallback = 0;
+		} else if (strcmp(cp, "gate") == 0) {
+			gatemode = 1;
+		} else if (strcmp(cp, "auto") == 0) {
+			passivemode = 1;
+			activefallback = 1;
+		} else
+			warnx("unknown FTPMODE: %s.  Using defaults", cp);
+	}
+
+	if (strcmp(__progname, "gate-ftp") == 0)
+		gatemode = 1;
+	gateserver = getenv("FTPSERVER");
+	if (gateserver == NULL)
+		gateserver = "";
+	if (gatemode) {
+		if (*gateserver == '\0') {
+			warnx(
+"Neither $FTPSERVER nor $GATE_SERVER is defined; disabling gate-ftp");
+			gatemode = 0;
+		}
+	}
+
+	cp = getenv("TERM");
+	dumb_terminal = (cp == NULL || *cp == '\0' || !strcmp(cp, "dumb") ||
+	    !strcmp(cp, "emacs") || !strcmp(cp, "su"));
+	fromatty = isatty(fileno(stdin));
+	if (fromatty) {
+		verbose = 1;		/* verbose if from a tty */
+#ifndef SMALL
+		if (!dumb_terminal)
+			editing = 1;	/* editing mode on if tty is usable */
+#endif /* !SMALL */
+	}
+
+	ttyout = stdout;
+	if (isatty(fileno(ttyout)) && !dumb_terminal && foregroundproc())
+		progress = 1;		/* progress bar on if tty is usable */
+
+#ifndef NOSSL
+	cookiefile = getenv("http_cookies");
+	if (tls_config == NULL) {
+		tls_config = tls_config_new();
+		if (tls_config == NULL)
+			errx(1, "tls config failed");
+		if (tls_config_set_protocols(tls_config,
+		    TLS_PROTOCOLS_ALL) != 0)
+			errx(1, "tls set protocols failed: %s",
+			    tls_config_error(tls_config));
+		if (tls_config_set_ciphers(tls_config, "DEFAULT") != 0)
+			errx(1, "tls set ciphers failed: %s",
+			    tls_config_error(tls_config));
+	}
+#endif /* !NOSSL */
+
+	httpuseragent = NULL;
+
+	while ((ch = getopt(argc, argv,
+		    "46AaCc:dD:EeN:gik:Mmno:pP:r:S:s:TtU:uvVw:")) != -1) {
+		switch (ch) {
+		case '4':
+			family = PF_INET;
+			break;
+		case '6':
+			family = PF_INET6;
+			break;
+		case 'A':
+			activefallback = 0;
+			passivemode = 0;
+			break;
+
+		case 'N':
+			setprogname(optarg);
+			break;
+		case 'a':
+			anonftp = 1;
+			break;
+
+		case 'C':
+#ifndef SMALL
+			resume = 1;
+#endif /* !SMALL */
+			break;
+
+		case 'c':
+#ifndef SMALL
+			cookiefile = optarg;
+#endif /* !SMALL */
+			break;
+
+		case 'D':
+			action = optarg;
+			break;
+		case 'd':
+#ifndef SMALL
+			options |= SO_DEBUG;
+			debug++;
+#endif /* !SMALL */
+			break;
+
+		case 'E':
+			epsv4 = 0;
+			break;
+
+		case 'e':
+#ifndef SMALL
+			editing = 0;
+#endif /* !SMALL */
+			break;
+
+		case 'g':
+			doglob = 0;
+			break;
+
+		case 'i':
+			interactive = 0;
+			break;
+
+		case 'k':
+			keep_alive_timeout = strtonum(optarg, 0, INT_MAX,
+			    &errstr);
+			if (errstr != NULL) {
+				warnx("keep alive amount is %s: %s", errstr,
+					optarg);
+				usage();
+			}
+			break;
+		case 'M':
+			progress = 0;
+			break;
+		case 'm':
+			progress = -1;
+			break;
+
+		case 'n':
+			autologin = 0;
+			break;
+
+		case 'o':
+			outfile = optarg;
+			pipeout = strcmp(outfile, "-") == 0;
+			ttyout = pipeout ? stderr : stdout;
+			break;
+
+		case 'p':
+			passivemode = 1;
+			activefallback = 0;
+			break;
+
+		case 'P':
+			ftpport = optarg;
+			break;
+
+		case 'r':
+			retry_connect = strtonum(optarg, 0, INT_MAX, &errstr);
+			if (errstr != NULL) {
+				warnx("retry amount is %s: %s", errstr,
+					optarg);
+				usage();
+			}
+			break;
+
+		case 'S':
+#ifndef NOSSL
+			process_ssl_options(optarg);
+#endif /* !NOSSL */
+			break;
+
+		case 's':
+#ifndef SMALL
+			srcaddr = optarg;
+#endif /* !SMALL */
+			break;
+
+#ifndef SMALL
+		case 'T':
+			timestamp = 1;
+			break;
+#endif /* !SMALL */
+		case 't':
+			trace = 1;
+			break;
+
+#ifndef SMALL
+		case 'U':
+			free (httpuseragent);
+			if (strcspn(optarg, "\r\n") != strlen(optarg))
+				errx(1, "Invalid User-Agent: %s.", optarg);
+			if (asprintf(&httpuseragent, "User-Agent: %s",
+			    optarg) == -1)
+				errx(1, "Can't allocate memory for HTTP(S) "
+				    "User-Agent");
+			break;
+		case 'u':
+			server_timestamps = 0;
+			break;
+#endif /* !SMALL */
+
+		case 'v':
+			verbose = 1;
+			break;
+
+		case 'V':
+			verbose = 0;
+			break;
+
+		case 'w':
+			connect_timeout = strtonum(optarg, 0, 200, &errstr);
+			if (errstr)
+				errx(1, "-w: %s", errstr);
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+#ifndef NOSSL
+	cookie_load();
+#endif /* !NOSSL */
+	if (httpuseragent == NULL)
+		httpuseragent = HTTP_USER_AGENT;
+
+	cpend = 0;	/* no pending replies */
+	proxy = 0;	/* proxy not active */
+	crflag = 1;	/* strip c.r. on ascii gets */
+	sendport = -1;	/* not using ports */
+	/*
+	 * Set up the home directory in case we're globbing.
+	 */
+	cp = getlogin();
+	if (cp != NULL) {
+		pw = getpwnam(cp);
+	}
+	if (pw == NULL)
+		pw = getpwuid(getuid());
+	if (pw != NULL) {
+		(void)strlcpy(homedir, pw->pw_dir, sizeof homedir);
+		home = homedir;
+	}
+
+	setttywidth(0);
+	(void)signal(SIGWINCH, setttywidth);
+
+	if (argc > 0) {
+		if (isurl(argv[0])) {
+			if (pipeout) {
+				if (pledge("stdio rpath dns tty inet",
+				    NULL) == -1)
+					err(1, "pledge");
+			} else {
+#ifndef SMALL
+			    if (outfile == NULL) {
+				if (pledge("stdio rpath wpath cpath dns tty inet proc exec fattr",
+				    NULL) == -1)
+					err(1, "pledge");
+			    } else
+#endif /* !SMALL */
+				if (pledge("stdio rpath wpath cpath dns tty inet fattr",
+				    NULL) == -1)
+					err(1, "pledge");
+			}
+
+			rval = auto_fetch(argc, argv, outfile);
+			/* -1 == connected and cd-ed */
+			if (rval >= 0 || outfile != NULL)
+				exit(rval);
+		} else {
+#ifndef SMALL
+			char *xargv[5];
+
+			if (setjmp(toplevel))
+				exit(0);
+			(void)signal(SIGINT, (sig_t)intr);
+			(void)signal(SIGPIPE, (sig_t)lostpeer);
+			xargv[0] = __progname;
+			xargv[1] = argv[0];
+			xargv[2] = argv[1];
+			xargv[3] = argv[2];
+			xargv[4] = NULL;
+			do {
+				setpeer(argc+1, xargv);
+				if (!retry_connect)
+					break;
+				if (!connected) {
+					macnum = 0;
+					fputs("Retrying...\n", ttyout);
+					sleep(retry_connect);
+				}
+			} while (!connected);
+			retry_connect = 0; /* connected, stop hiding msgs */
+#endif /* !SMALL */
+		}
+	}
+#ifndef SMALL
+	controlediting();
+	top = setjmp(toplevel) == 0;
+	if (top) {
+		(void)signal(SIGINT, (sig_t)intr);
+		(void)signal(SIGPIPE, (sig_t)lostpeer);
+	}
+	for (;;) {
+		cmdscanner(top);
+		top = 1;
+	}
+#else /* !SMALL */
+	usage();
+#endif /* !SMALL */
+}
+
+void
+intr(void)
+{
+	int save_errno = errno;
+
+	write(fileno(ttyout), "\n\r", 2);
+	alarmtimer(0);
+
+	errno = save_errno;
+	longjmp(toplevel, 1);
+}
+
+void
+lostpeer(void)
+{
+	int save_errno = errno;
+
+	alarmtimer(0);
+	if (connected) {
+		if (cout != NULL) {
+			(void)shutdown(fileno(cout), SHUT_RDWR);
+			(void)fclose(cout);
+			cout = NULL;
+		}
+		if (data >= 0) {
+			(void)shutdown(data, SHUT_RDWR);
+			(void)close(data);
+			data = -1;
+		}
+		connected = 0;
+	}
+	pswitch(1);
+	if (connected) {
+		if (cout != NULL) {
+			(void)shutdown(fileno(cout), SHUT_RDWR);
+			(void)fclose(cout);
+			cout = NULL;
+		}
+		connected = 0;
+	}
+	proxflag = 0;
+	pswitch(0);
+	errno = save_errno;
+}
+
+#ifndef SMALL
+/*
+ * Generate a prompt
+ */
+char *
+prompt(void)
+{
+	return ("ftp> ");
+}
+
+/*
+ * Command parser.
+ */
+void
+cmdscanner(int top)
+{
+	struct cmd *c;
+	int num;
+	HistEvent hev;
+
+	if (!top && !editing)
+		(void)putc('\n', ttyout);
+	for (;;) {
+		if (!editing) {
+			if (fromatty) {
+				fputs(prompt(), ttyout);
+				(void)fflush(ttyout);
+			}
+			if (fgets(line, sizeof(line), stdin) == NULL)
+				quit(0, 0);
+			num = strlen(line);
+			if (num == 0)
+				break;
+			if (line[--num] == '\n') {
+				if (num == 0)
+					break;
+				line[num] = '\0';
+			} else if (num == sizeof(line) - 2) {
+				fputs("sorry, input line too long.\n", ttyout);
+				while ((num = getchar()) != '\n' && num != EOF)
+					/* void */;
+				break;
+			} /* else it was a line without a newline */
+		} else {
+			const char *buf;
+			cursor_pos = NULL;
+
+			if ((buf = el_gets(el, &num)) == NULL || num == 0) {
+				putc('\n', ttyout);
+				fflush(ttyout);
+				quit(0, 0);
+			}
+			if (buf[--num] == '\n') {
+				if (num == 0)
+					break;
+			}
+			if (num >= sizeof(line)) {
+				fputs("sorry, input line too long.\n", ttyout);
+				break;
+			}
+			memcpy(line, buf, (size_t)num);
+			line[num] = '\0';
+			history(hist, &hev, H_ENTER, buf);
+		}
+
+		makeargv();
+		if (margc == 0)
+			continue;
+		c = getcmd(margv[0]);
+		if (c == (struct cmd *)-1) {
+			fputs("?Ambiguous command.\n", ttyout);
+			continue;
+		}
+		if (c == 0) {
+			/*
+			 * Give editline(3) a shot at unknown commands.
+			 * XXX - bogus commands with a colon in
+			 *       them will not elicit an error.
+			 */
+			if (editing &&
+			    el_parse(el, margc, (const char **)margv) != 0)
+				fputs("?Invalid command.\n", ttyout);
+			continue;
+		}
+		if (c->c_conn && !connected) {
+			fputs("Not connected.\n", ttyout);
+			continue;
+		}
+		confirmrest = 0;
+		(*c->c_handler)(margc, margv);
+		if (bell && c->c_bell)
+			(void)putc('\007', ttyout);
+		if (c->c_handler != help)
+			break;
+	}
+	(void)signal(SIGINT, (sig_t)intr);
+	(void)signal(SIGPIPE, (sig_t)lostpeer);
+}
+
+struct cmd *
+getcmd(const char *name)
+{
+	const char *p, *q;
+	struct cmd *c, *found;
+	int nmatches, longest;
+
+	if (name == NULL)
+		return (0);
+
+	longest = 0;
+	nmatches = 0;
+	found = 0;
+	for (c = cmdtab; (p = c->c_name) != NULL; c++) {
+		for (q = name; *q == *p++; q++)
+			if (*q == 0)		/* exact match? */
+				return (c);
+		if (!*q) {			/* the name was a prefix */
+			if (q - name > longest) {
+				longest = q - name;
+				nmatches = 1;
+				found = c;
+			} else if (q - name == longest)
+				nmatches++;
+		}
+	}
+	if (nmatches > 1)
+		return ((struct cmd *)-1);
+	return (found);
+}
+
+/*
+ * Slice a string up into argc/argv.
+ */
+
+int slrflag;
+
+void
+makeargv(void)
+{
+	char *argp;
+
+	stringbase = line;		/* scan from first of buffer */
+	argbase = argbuf;		/* store from first of buffer */
+	slrflag = 0;
+	marg_sl->sl_cur = 0;		/* reset to start of marg_sl */
+	for (margc = 0; ; margc++) {
+		argp = slurpstring();
+		sl_add(marg_sl, argp);
+		if (argp == NULL)
+			break;
+	}
+	if (cursor_pos == line) {
+		cursor_argc = 0;
+		cursor_argo = 0;
+	} else if (cursor_pos != NULL) {
+		cursor_argc = margc;
+		cursor_argo = strlen(margv[margc-1]);
+	}
+}
+
+#define INC_CHKCURSOR(x)	{ (x)++ ; \
+				if (x == cursor_pos) { \
+					cursor_argc = margc; \
+					cursor_argo = ap-argbase; \
+					cursor_pos = NULL; \
+				} }
+
+/*
+ * Parse string into argbuf;
+ * implemented with FSM to
+ * handle quoting and strings
+ */
+char *
+slurpstring(void)
+{
+	int got_one = 0;
+	char *sb = stringbase;
+	char *ap = argbase;
+	char *tmp = argbase;		/* will return this if token found */
+
+	if (*sb == '!' || *sb == '$') {	/* recognize ! as a token for shell */
+		switch (slrflag) {	/* and $ as token for macro invoke */
+			case 0:
+				slrflag++;
+				INC_CHKCURSOR(stringbase);
+				return ((*sb == '!') ? "!" : "$");
+				/* NOTREACHED */
+			case 1:
+				slrflag++;
+				altarg = stringbase;
+				break;
+			default:
+				break;
+		}
+	}
+
+S0:
+	switch (*sb) {
+
+	case '\0':
+		goto OUT;
+
+	case ' ':
+	case '\t':
+		INC_CHKCURSOR(sb);
+		goto S0;
+
+	default:
+		switch (slrflag) {
+			case 0:
+				slrflag++;
+				break;
+			case 1:
+				slrflag++;
+				altarg = sb;
+				break;
+			default:
+				break;
+		}
+		goto S1;
+	}
+
+S1:
+	switch (*sb) {
+
+	case ' ':
+	case '\t':
+	case '\0':
+		goto OUT;	/* end of token */
+
+	case '\\':
+		INC_CHKCURSOR(sb);
+		goto S2;	/* slurp next character */
+
+	case '"':
+		INC_CHKCURSOR(sb);
+		goto S3;	/* slurp quoted string */
+
+	default:
+		*ap = *sb;	/* add character to token */
+		ap++;
+		INC_CHKCURSOR(sb);
+		got_one = 1;
+		goto S1;
+	}
+
+S2:
+	switch (*sb) {
+
+	case '\0':
+		goto OUT;
+
+	default:
+		*ap = *sb;
+		ap++;
+		INC_CHKCURSOR(sb);
+		got_one = 1;
+		goto S1;
+	}
+
+S3:
+	switch (*sb) {
+
+	case '\0':
+		goto OUT;
+
+	case '"':
+		INC_CHKCURSOR(sb);
+		goto S1;
+
+	default:
+		*ap = *sb;
+		ap++;
+		INC_CHKCURSOR(sb);
+		got_one = 1;
+		goto S3;
+	}
+
+OUT:
+	if (got_one)
+		*ap++ = '\0';
+	argbase = ap;			/* update storage pointer */
+	stringbase = sb;		/* update scan pointer */
+	if (got_one) {
+		return (tmp);
+	}
+	switch (slrflag) {
+		case 0:
+			slrflag++;
+			break;
+		case 1:
+			slrflag++;
+			altarg = (char *) 0;
+			break;
+		default:
+			break;
+	}
+	return (NULL);
+}
+
+/*
+ * Help command.
+ * Call each command handler with argc == 0 and argv[0] == name.
+ */
+void
+help(int argc, char *argv[])
+{
+	struct cmd *c;
+
+	if (argc == 1) {
+		StringList *buf;
+
+		buf = sl_init();
+		fprintf(ttyout, "%sommands may be abbreviated.  Commands are:\n\n",
+		    proxy ? "Proxy c" : "C");
+		for (c = cmdtab; c < &cmdtab[NCMDS]; c++)
+			if (c->c_name && (!proxy || c->c_proxy))
+				sl_add(buf, c->c_name);
+		list_vertical(buf);
+		sl_free(buf, 0);
+		return;
+	}
+
+#define HELPINDENT ((int) sizeof("disconnect"))
+
+	while (--argc > 0) {
+		char *arg;
+
+		arg = *++argv;
+		c = getcmd(arg);
+		if (c == (struct cmd *)-1)
+			fprintf(ttyout, "?Ambiguous help command %s\n", arg);
+		else if (c == NULL)
+			fprintf(ttyout, "?Invalid help command %s\n", arg);
+		else
+			fprintf(ttyout, "%-*s\t%s\n", HELPINDENT,
+				c->c_name, c->c_help);
+	}
+}
+#endif /* !SMALL */
+
+__dead void
+usage(void)
+{
+	fprintf(stderr, "usage: "
+#ifndef SMALL
+	    "ftp [-46AadEegiMmnptVv] [-D title] [-k seconds] [-P port] "
+	    "[-r seconds]\n"
+	    "           [-s sourceaddr] [host [port]]\n"
+	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr]\n"
+	    "           ftp://[user:password@]host[:port]/file[/] ...\n"
+	    "       ftp [-CTu] [-c cookie] [-N name] [-o output] [-S ssl_options] "
+	    "[-s sourceaddr]\n"
+	    "           [-U useragent] [-w seconds] "
+	    "http[s]://[user:password@]host[:port]/file ...\n"
+	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr] file:file ...\n"
+	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr] host:/file[/] ...\n"
+#else /* !SMALL */
+	    "ftp [-N name] [-o output] "
+	    "ftp://[user:password@]host[:port]/file[/] ...\n"
+#ifndef NOSSL
+	    "       ftp [-N name] [-o output] [-S ssl_options] [-w seconds] "
+	    "http[s]://[user:password@]host[:port]/file ...\n"
+#else
+	    "       ftp [-N name] [-o output] [-w seconds] http://host[:port]/file ...\n"
+#endif /* NOSSL */
+	    "       ftp [-N name] [-o output] file:file ...\n"
+	    "       ftp [-N name] [-o output] host:/file[/] ...\n"
+#endif /* !SMALL */
+	    );
+	exit(1);
+}
diff --git a/pathnames.h b/pathnames.h
new file mode 100644
index 0000000..7524460
--- /dev/null
+++ b/pathnames.h
@@ -0,0 +1,37 @@
+/*	$OpenBSD: pathnames.h,v 1.9 2019/05/16 12:44:18 florian Exp $	*/
+/*	$NetBSD: pathnames.h,v 1.7 1997/01/09 20:19:40 tls Exp $	*/
+
+/*
+ * Copyright (c) 1989, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ *	@(#)pathnames.h	8.1 (Berkeley) 6/6/93
+ */
+
+#include <paths.h>
+
+#define	TMPFILE		"ftpXXXXXXXXXX"
diff --git a/ruserpass.c b/ruserpass.c
new file mode 100644
index 0000000..6e5adb5
--- /dev/null
+++ b/ruserpass.c
@@ -0,0 +1,317 @@
+/*	$OpenBSD: ruserpass.c,v 1.33 2019/06/28 13:35:01 deraadt Exp $	*/
+/*	$NetBSD: ruserpass.c,v 1.14 1997/07/20 09:46:01 lukem Exp $	*/
+
+/*
+ * Copyright (c) 1985, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ftp_var.h"
+
+static	int token(void);
+static	FILE *cfile;
+
+#define	DEFAULT	1
+#define	LOGIN	2
+#define	PASSWD	3
+#define	ACCOUNT 4
+#define MACDEF  5
+#define	ID	10
+#define	MACH	11
+
+static char tokval[100];
+
+static struct toktab {
+	char *tokstr;
+	int tval;
+} toktab[]= {
+	{ "default",	DEFAULT },
+	{ "login",	LOGIN },
+	{ "password",	PASSWD },
+	{ "passwd",	PASSWD },
+	{ "account",	ACCOUNT },
+	{ "machine",	MACH },
+	{ "macdef",	MACDEF },
+	{ NULL,		0 }
+};
+
+int
+ruserpass(const char *host, char **aname, char **apass, char **aacct)
+{
+	char *hdir, buf[PATH_MAX], *tmp;
+	char myname[HOST_NAME_MAX+1], *mydomain;
+	int t, i, c, usedefault = 0;
+	struct stat stb;
+
+	hdir = getenv("HOME");
+	if (hdir == NULL || *hdir == '\0')
+		return (0);
+	i = snprintf(buf, sizeof(buf), "%s/.netrc", hdir);
+	if (i < 0 || i >= sizeof(buf)) {
+		warnc(ENAMETOOLONG, "%s/.netrc", hdir);
+		return (0);
+	}
+	cfile = fopen(buf, "r");
+	if (cfile == NULL) {
+		if (errno != ENOENT)
+			warn("%s", buf);
+		return (0);
+	}
+	if (gethostname(myname, sizeof(myname)) == -1)
+		myname[0] = '\0';
+	if ((mydomain = strchr(myname, '.')) == NULL)
+		mydomain = "";
+next:
+	while ((t = token()) > 0) switch(t) {
+
+	case DEFAULT:
+		usedefault = 1;
+		/* FALLTHROUGH */
+
+	case MACH:
+		if (!usedefault) {
+			if ((t = token()) == -1)
+				goto bad;
+			if (t != ID)
+				continue;
+			/*
+			 * Allow match either for user's input host name
+			 * or official hostname.  Also allow match of
+			 * incompletely-specified host in local domain.
+			 */
+			if (strcasecmp(host, tokval) == 0)
+				goto match;
+			if (strcasecmp(hostname, tokval) == 0)
+				goto match;
+			if ((tmp = strchr(hostname, '.')) != NULL &&
+			    strcasecmp(tmp, mydomain) == 0 &&
+			    strncasecmp(hostname, tokval,
+			    (size_t)(tmp - hostname)) == 0 &&
+			    tokval[tmp - hostname] == '\0')
+				goto match;
+			if ((tmp = strchr(host, '.')) != NULL &&
+			    strcasecmp(tmp, mydomain) == 0 &&
+			    strncasecmp(host, tokval,
+			    (size_t)(tmp - host)) == 0 &&
+			    tokval[tmp - host] == '\0')
+				goto match;
+			continue;
+		}
+	match:
+		while ((t = token()) > 0 &&
+		    t != MACH && t != DEFAULT) switch(t) {
+
+		case LOGIN:
+			if ((t = token()) == -1)
+				goto bad;
+			if (t) {
+				if (*aname == 0) {
+					if ((*aname = strdup(tokval)) == NULL)
+						err(1, "strdup");
+				} else {
+					if (strcmp(*aname, tokval))
+						goto next;
+				}
+			}
+			break;
+		case PASSWD:
+			if ((*aname == NULL || strcmp(*aname, "anonymous")) &&
+			    fstat(fileno(cfile), &stb) >= 0 &&
+			    (stb.st_mode & 077) != 0) {
+	warnx("Error: .netrc file is readable by others.");
+	warnx("Remove password or make file unreadable by others.");
+				goto bad;
+			}
+			if ((t = token()) == -1)
+				goto bad;
+			if (t && *apass == 0) {
+				if ((*apass = strdup(tokval)) == NULL)
+					err(1, "strdup");
+			}
+			break;
+		case ACCOUNT:
+			if (fstat(fileno(cfile), &stb) >= 0
+			    && (stb.st_mode & 077) != 0) {
+	warnx("Error: .netrc file is readable by others.");
+	warnx("Remove account or make file unreadable by others.");
+				goto bad;
+			}
+			if ((t = token()) == -1)
+				goto bad;
+			if (t && *aacct == 0) {
+				if ((*aacct = strdup(tokval)) == NULL)
+					err(1, "strdup");
+			}
+			break;
+		case MACDEF:
+			if (proxy) {
+				(void)fclose(cfile);
+				return (0);
+			}
+			while ((c = fgetc(cfile)) != EOF)
+				if (c != ' ' && c != '\t')
+					break;
+			if (c == EOF || c == '\n') {
+				fputs("Missing macdef name argument.\n", ttyout);
+				goto bad;
+			}
+			if (macnum == 16) {
+				fputs(
+"Limit of 16 macros have already been defined.\n", ttyout);
+				goto bad;
+			}
+			tmp = macros[macnum].mac_name;
+			*tmp++ = c;
+			for (i=0; i < 8 && (c = fgetc(cfile)) != EOF &&
+			    !isspace(c); ++i) {
+				*tmp++ = c;
+			}
+			if (c == EOF) {
+				fputs(
+"Macro definition missing null line terminator.\n", ttyout);
+				goto bad;
+			}
+			*tmp = '\0';
+			if (c != '\n') {
+				while ((c = fgetc(cfile)) != EOF && c != '\n');
+			}
+			if (c == EOF) {
+				fputs(
+"Macro definition missing null line terminator.\n", ttyout);
+				goto bad;
+			}
+			if (macnum == 0) {
+				macros[macnum].mac_start = macbuf;
+			}
+			else {
+				macros[macnum].mac_start =
+				    macros[macnum-1].mac_end + 1;
+			}
+			tmp = macros[macnum].mac_start;
+			while (tmp != macbuf + 4096) {
+				if ((c = fgetc(cfile)) == EOF) {
+				fputs(
+"Macro definition missing null line terminator.\n", ttyout);
+					goto bad;
+				}
+				*tmp = c;
+				if (*tmp == '\n') {
+					if (tmp == macros[macnum].mac_start) {
+						macros[macnum++].mac_end = tmp;
+						break;
+					} else if (*(tmp-1) == '\0') {
+						macros[macnum++].mac_end =
+						    tmp - 1;
+						break;
+					}
+					*tmp = '\0';
+				}
+				tmp++;
+			}
+			if (tmp == macbuf + 4096) {
+				fputs("4K macro buffer exceeded.\n", ttyout);
+				goto bad;
+			}
+			break;
+		default:
+			warnx("Unknown .netrc keyword %s", tokval);
+			break;
+		}
+		goto done;
+	}
+done:
+	if (t == -1)
+		goto bad;
+	(void)fclose(cfile);
+	return (0);
+bad:
+	(void)fclose(cfile);
+	return (-1);
+}
+
+static int
+token(void)
+{
+	char *cp;
+	int c;
+	struct toktab *t;
+
+	if (feof(cfile) || ferror(cfile))
+		return (0);
+	while ((c = fgetc(cfile)) != EOF &&
+	    (c == '\n' || c == '\t' || c == ' ' || c == ','))
+		continue;
+	if (c == EOF)
+		return (0);
+	cp = tokval;
+	if (c == '"') {
+		while ((c = fgetc(cfile)) != EOF && c != '"') {
+			if (c == '\\' && (c = fgetc(cfile)) == EOF)
+				break;
+			*cp++ = c;
+			if (cp == tokval + sizeof(tokval)) {
+				warnx("Token in .netrc too long");
+				return (-1);
+			}
+		}
+	} else {
+		*cp++ = c;
+		while ((c = fgetc(cfile)) != EOF
+		    && c != '\n' && c != '\t' && c != ' ' && c != ',') {
+			if (c == '\\' && (c = fgetc(cfile)) == EOF)
+				break;
+			*cp++ = c;
+			if (cp == tokval + sizeof(tokval)) {
+				warnx("Token in .netrc too long");
+				return (-1);
+			}
+		}
+	}
+	*cp = 0;
+	if (tokval[0] == 0)
+		return (0);
+	for (t = toktab; t->tokstr; t++)
+		if (!strcmp(t->tokstr, tokval))
+			return (t->tval);
+	return (ID);
+}
+
+#endif /* !SMALL */
+
diff --git a/small.c b/small.c
new file mode 100644
index 0000000..484d78f
--- /dev/null
+++ b/small.c
@@ -0,0 +1,721 @@
+/*	$OpenBSD: small.c,v 1.13 2023/03/08 04:43:11 guenther Exp $	*/
+/*	$NetBSD: cmds.c,v 1.27 1997/08/18 10:20:15 lukem Exp $	*/
+
+/*
+ * Copyright (C) 1997 and 1998 WIDE Project.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the project nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * FTP User Program -- Command Routines.
+ */
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <arpa/ftp.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fnmatch.h>
+#include <glob.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "ftp_var.h"
+#include "pathnames.h"
+#include "small.h"
+
+jmp_buf	jabort;
+char   *mname;
+char   *home = "/";
+
+struct	types {
+	char	*t_name;
+	char	*t_mode;
+	int	t_type;
+} types[] = {
+	{ "ascii",	"A",	TYPE_A },
+	{ "binary",	"I",	TYPE_I },
+	{ "image",	"I",	TYPE_I },
+	{ NULL }
+};
+
+/*
+ * Set transfer type.
+ */
+void
+settype(int argc, char *argv[])
+{
+	struct types *p;
+	int comret;
+
+	if (argc > 2) {
+		char *sep;
+
+		fprintf(ttyout, "usage: %s [", argv[0]);
+		sep = "";
+		for (p = types; p->t_name; p++) {
+			fprintf(ttyout, "%s%s", sep, p->t_name);
+			sep = " | ";
+		}
+		fputs("]\n", ttyout);
+		code = -1;
+		return;
+	}
+	if (argc < 2) {
+		fprintf(ttyout, "Using %s mode to transfer files.\n", typename);
+		code = 0;
+		return;
+	}
+	for (p = types; p->t_name; p++)
+		if (strcmp(argv[1], p->t_name) == 0)
+			break;
+	if (p->t_name == 0) {
+		fprintf(ttyout, "%s: unknown mode.\n", argv[1]);
+		code = -1;
+		return;
+	}
+	comret = command("TYPE %s", p->t_mode);
+	if (comret == COMPLETE) {
+		(void)strlcpy(typename, p->t_name, sizeof typename);
+		curtype = type = p->t_type;
+	}
+}
+
+/*
+ * Internal form of settype; changes current type in use with server
+ * without changing our notion of the type for data transfers.
+ * Used to change to and from ascii for listings.
+ */
+void
+changetype(int newtype, int show)
+{
+	struct types *p;
+	int comret, oldverbose = verbose;
+
+	if (newtype == 0)
+		newtype = TYPE_I;
+	if (newtype == curtype)
+		return;
+	if (
+#ifndef SMALL
+	    !debug &&
+#endif /* !SMALL */
+	    show == 0)
+		verbose = 0;
+	for (p = types; p->t_name; p++)
+		if (newtype == p->t_type)
+			break;
+	if (p->t_name == 0) {
+		warnx("internal error: unknown type %d.", newtype);
+		return;
+	}
+	if (newtype == TYPE_L && bytename[0] != '\0')
+		comret = command("TYPE %s %s", p->t_mode, bytename);
+	else
+		comret = command("TYPE %s", p->t_mode);
+	if (comret == COMPLETE)
+		curtype = newtype;
+	verbose = oldverbose;
+}
+
+char *stype[] = {
+	"type",
+	"",
+	0
+};
+
+/*
+ * Set binary transfer type.
+ */
+void
+setbinary(int argc, char *argv[])
+{
+
+	stype[1] = "binary";
+	settype(2, stype);
+}
+
+void
+get(int argc, char *argv[])
+{
+
+	(void)getit(argc, argv, 0, restart_point ? "a+w" : "w" );
+}
+
+/*
+ * Receive one file.
+ */
+int
+getit(int argc, char *argv[], int restartit, const char *mode)
+{
+	int loc = 0;
+	int rval = 0;
+	char *oldargv1, *oldargv2, *globargv2;
+
+	if (argc == 2) {
+		argc++;
+		argv[2] = argv[1];
+		loc++;
+	}
+#ifndef SMALL
+	if (argc < 2 && !another(&argc, &argv, "remote-file"))
+		goto usage;
+	if ((argc < 3 && !another(&argc, &argv, "local-file")) || argc > 3) {
+usage:
+		fprintf(ttyout, "usage: %s remote-file [local-file]\n",
+		    argv[0]);
+		code = -1;
+		return (0);
+	}
+#endif /* !SMALL */
+	oldargv1 = argv[1];
+	oldargv2 = argv[2];
+	if (!globulize(&argv[2])) {
+		code = -1;
+		return (0);
+	}
+	globargv2 = argv[2];
+	if (loc && mcase) {
+		char *tp = argv[1], *tp2, tmpbuf[PATH_MAX];
+
+		while (*tp && !islower((unsigned char)*tp)) {
+			tp++;
+		}
+		if (!*tp) {
+			tp = argv[2];
+			tp2 = tmpbuf;
+			while ((*tp2 = *tp) != '\0') {
+				if (isupper((unsigned char)*tp2)) {
+					*tp2 = tolower((unsigned char)*tp2);
+				}
+				tp++;
+				tp2++;
+			}
+			argv[2] = tmpbuf;
+		}
+	}
+	if (loc && ntflag)
+		argv[2] = dotrans(argv[2]);
+	if (loc && mapflag)
+		argv[2] = domap(argv[2]);
+#ifndef SMALL
+	if (restartit) {
+		struct stat stbuf;
+		int ret;
+
+		ret = stat(argv[2], &stbuf);
+		if (restartit == 1) {
+			restart_point = (ret < 0) ? 0 : stbuf.st_size;
+		} else {
+			if (ret == 0) {
+				time_t mtime;
+
+				mtime = remotemodtime(argv[1], 0);
+				if (mtime == -1)
+					goto freegetit;
+				if (stbuf.st_mtime >= mtime) {
+					rval = 1;
+					fprintf(ttyout,
+						"Local file \"%s\" is newer "\
+						"than remote file \"%s\".\n",
+						argv[2], argv[1]);
+					goto freegetit;
+				}
+			}
+		}
+	}
+#endif /* !SMALL */
+
+	recvrequest("RETR", argv[2], argv[1], mode,
+	    argv[1] != oldargv1 || argv[2] != oldargv2 || !interactive, loc);
+	restart_point = 0;
+#ifndef SMALL
+freegetit:
+#endif
+	if (oldargv2 != globargv2)	/* free up after globulize() */
+		free(globargv2);
+	return (rval);
+}
+
+/* XXX - Signal race. */
+void
+mabort(int signo)
+{
+	int save_errno = errno;
+
+	alarmtimer(0);
+	(void) write(fileno(ttyout), "\n\r", 2);
+#ifndef SMALL
+	if (mflag && fromatty) {
+		/* XXX signal race, crazy unbelievable stdio misuse */
+		if (confirm(mname, NULL)) {
+			errno = save_errno;
+			longjmp(jabort, 1);
+		}
+	}
+#endif /* !SMALL */
+	mflag = 0;
+	errno = save_errno;
+	longjmp(jabort, 1);
+}
+
+/*
+ * Get multiple files.
+ */
+void
+mget(int argc, char *argv[])
+{
+	extern int optind, optreset;
+	sig_t oldintr;
+	int xargc = 2;
+	char *cp, localcwd[PATH_MAX], *xargv[] = { argv[0], NULL, NULL };
+	static int restartit = 0;
+#ifndef SMALL
+	extern char *optarg;
+	const char *errstr;
+	int ch, i = 1;
+	char type = 0, *dummyargv[] = { argv[0], ".", NULL };
+	FILE *ftemp = NULL;
+	static int depth = 0, max_depth = 0;
+
+	optind = optreset = 1;
+
+	if (depth)
+		depth++;
+
+	while ((ch = getopt(argc, argv, "cd:nr")) != -1) {
+		switch(ch) {
+		case 'c':
+			restartit = 1;
+			break;
+		case 'd':
+			max_depth = strtonum(optarg, 0, INT_MAX, &errstr);
+			if (errstr != NULL) {
+				fprintf(ttyout, "bad depth value, %s: %s\n",
+				    errstr, optarg);
+				code = -1;
+				return;
+			}
+			break;
+		case 'n':
+			restartit = -1;
+			break;
+		case 'r':
+			depth = 1;
+			break;
+		default:
+			goto usage;
+		}
+	}
+
+	if (argc - optind < 1 && !another(&argc, &argv, "remote-files")) {
+usage:
+		fprintf(ttyout, "usage: %s [-cnr] [-d depth] remote-files\n",
+		    argv[0]);
+		code = -1;
+		return;
+	}
+
+	argv[optind - 1] = argv[0];
+	argc -= optind - 1;
+	argv += optind - 1;
+#endif /* !SMALL */
+
+	mname = argv[0];
+	mflag = 1;
+	if (getcwd(localcwd, sizeof(localcwd)) == NULL)
+		err(1, "can't get cwd");
+
+	oldintr = signal(SIGINT, mabort);
+	(void)setjmp(jabort);
+	while ((cp =
+#ifdef SMALL
+	    remglob(argv, proxy, NULL)) != NULL
+	    ) {
+#else /* SMALL */
+	    depth ? remglob2(dummyargv, proxy, NULL, &ftemp, &type) :
+	    remglob(argv, proxy, NULL)) != NULL
+	    || (mflag && depth && ++i < argc)
+	    ) {
+		if (cp == NULL)
+			continue;
+#endif /* SMALL */
+		if (*cp == '\0') {
+			mflag = 0;
+			continue;
+		}
+		if (!mflag)
+			continue;
+#ifndef SMALL
+		if (depth && fnmatch(argv[i], cp, FNM_PATHNAME) != 0)
+			continue;
+#endif /* !SMALL */
+		if (!fileindir(cp, localcwd)) {
+			fprintf(ttyout, "Skipping non-relative filename `%s'\n",
+			    cp);
+			continue;
+		}
+#ifndef SMALL
+		if (type == 'd' && depth == max_depth)
+			continue;
+		if (!confirm(argv[0], cp))
+			continue;
+		if (type == 'd') {
+			mkdir(cp, 0755);
+			if (chdir(cp) != 0) {
+				warn("local: %s", cp);
+				continue;
+			}
+
+			xargv[1] = cp;
+			cd(xargc, xargv);
+			if (dirchange != 1)
+				goto out;
+
+			xargv[1] = "*";
+			mget(xargc, xargv);
+
+			xargv[1] = "..";
+			cd(xargc, xargv);
+			if (dirchange != 1) {
+				mflag = 0;
+				goto out;
+			}
+
+out:
+			if (chdir("..") != 0) {
+				warn("local: %s", cp);
+				mflag = 0;
+			}
+			continue;
+		}
+		if (type == 's')
+			/* Currently ignored. */
+			continue;
+#endif /* !SMALL */
+		xargv[1] = cp;
+		(void)getit(xargc, xargv, restartit,
+		    (restartit == 1 || restart_point) ? "a+w" : "w");
+#ifndef SMALL
+		if (!mflag && fromatty) {
+			if (confirm(argv[0], NULL))
+				mflag = 1;
+		}
+#endif /* !SMALL */
+	}
+	(void)signal(SIGINT, oldintr);
+#ifndef SMALL
+	if (depth)
+		depth--;
+	if (depth == 0 || mflag == 0)
+		depth = max_depth = mflag = restartit = 0;
+#else /* !SMALL */
+	mflag = 0;
+#endif /* !SMALL */
+}
+
+/*
+ * Set current working directory on remote machine.
+ */
+void
+cd(int argc, char *argv[])
+{
+	int r;
+
+#ifndef SMALL
+	if ((argc < 2 && !another(&argc, &argv, "remote-directory")) ||
+	    argc > 2) {
+		fprintf(ttyout, "usage: %s remote-directory\n", argv[0]);
+		code = -1;
+		return;
+	}
+#endif /* !SMALL */
+	r = command("CWD %s", argv[1]);
+	if (r == ERROR && code == 500) {
+		if (verbose)
+			fputs("CWD command not recognized, trying XCWD.\n", ttyout);
+		r = command("XCWD %s", argv[1]);
+	}
+	if (r == ERROR && code == 550) {
+		dirchange = 0;
+		return;
+	}
+	if (r == COMPLETE)
+		dirchange = 1;
+}
+
+/*
+ * Terminate session, but don't exit.
+ */
+void
+disconnect(int argc, char *argv[])
+{
+
+	if (!connected)
+		return;
+	(void)command("QUIT");
+	if (cout) {
+		(void)fclose(cout);
+	}
+	cout = NULL;
+	connected = 0;
+	data = -1;
+#ifndef SMALL
+	if (!proxy) {
+		macnum = 0;
+	}
+#endif /* !SMALL */
+}
+
+char *
+dotrans(char *name)
+{
+	static char new[PATH_MAX];
+	char *cp1, *cp2 = new;
+	int i, ostop, found;
+
+	for (ostop = 0; *(ntout + ostop) && ostop < 16; ostop++)
+		continue;
+	for (cp1 = name; *cp1; cp1++) {
+		found = 0;
+		for (i = 0; *(ntin + i) && i < 16; i++) {
+			if (*cp1 == *(ntin + i)) {
+				found++;
+				if (i < ostop) {
+					*cp2++ = *(ntout + i);
+				}
+				break;
+			}
+		}
+		if (!found) {
+			*cp2++ = *cp1;
+		}
+	}
+	*cp2 = '\0';
+	return (new);
+}
+
+char *
+domap(char *name)
+{
+	static char new[PATH_MAX];
+	char *cp1 = name, *cp2 = mapin;
+	char *tp[9], *te[9];
+	int i, toks[9], toknum = 0, match = 1;
+
+	for (i=0; i < 9; ++i) {
+		toks[i] = 0;
+	}
+	while (match && *cp1 && *cp2) {
+		switch (*cp2) {
+			case '\\':
+				if (*++cp2 != *cp1) {
+					match = 0;
+				}
+				break;
+			case '$':
+				if (*(cp2+1) >= '1' && (*cp2+1) <= '9') {
+					if (*cp1 != *(++cp2+1)) {
+						toks[toknum = *cp2 - '1']++;
+						tp[toknum] = cp1;
+						while (*++cp1 && *(cp2+1)
+							!= *cp1);
+						te[toknum] = cp1;
+					}
+					cp2++;
+					break;
+				}
+				/* FALLTHROUGH */
+			default:
+				if (*cp2 != *cp1) {
+					match = 0;
+				}
+				break;
+		}
+		if (match && *cp1) {
+			cp1++;
+		}
+		if (match && *cp2) {
+			cp2++;
+		}
+	}
+	if (!match && *cp1) /* last token mismatch */
+	{
+		toks[toknum] = 0;
+	}
+	cp1 = new;
+	*cp1 = '\0';
+	cp2 = mapout;
+	while (*cp2) {
+		match = 0;
+		switch (*cp2) {
+			case '\\':
+				if (*(cp2 + 1)) {
+					*cp1++ = *++cp2;
+				}
+				break;
+			case '[':
+LOOP:
+				if (*++cp2 == '$' && isdigit((unsigned char)*(cp2 + 1))) {
+					if (*++cp2 == '0') {
+						char *cp3 = name;
+
+						while (*cp3) {
+							*cp1++ = *cp3++;
+						}
+						match = 1;
+					} else if (toks[toknum = *cp2 - '1']) {
+						char *cp3 = tp[toknum];
+
+						while (cp3 != te[toknum]) {
+							*cp1++ = *cp3++;
+						}
+						match = 1;
+					}
+				} else {
+					while (*cp2 && *cp2 != ',' &&
+					    *cp2 != ']') {
+						if (*cp2 == '\\') {
+							cp2++;
+						} else if (*cp2 == '$' &&
+						    isdigit((unsigned char)*(cp2 + 1))) {
+							if (*++cp2 == '0') {
+							   char *cp3 = name;
+
+							   while (*cp3) {
+								*cp1++ = *cp3++;
+							   }
+							} else if (toks[toknum =
+							    *cp2 - '1']) {
+								char *cp3=tp[toknum];
+
+								while (cp3 !=
+								    te[toknum]) {
+									*cp1++ = *cp3++;
+								}
+							}
+						} else if (*cp2) {
+							*cp1++ = *cp2++;
+						}
+					}
+					if (!*cp2) {
+						fputs(
+"nmap: unbalanced brackets.\n", ttyout);
+						return (name);
+					}
+					match = 1;
+					cp2--;
+				}
+				if (match) {
+					while (*++cp2 && *cp2 != ']') {
+						if (*cp2 == '\\' && *(cp2 + 1)) {
+							cp2++;
+						}
+					}
+					if (!*cp2) {
+						fputs(
+"nmap: unbalanced brackets.\n", ttyout);
+						return (name);
+					}
+					break;
+				}
+				switch (*++cp2) {
+					case ',':
+						goto LOOP;
+					case ']':
+						break;
+					default:
+						cp2--;
+						goto LOOP;
+				}
+				break;
+			case '$':
+				if (isdigit((unsigned char)*(cp2 + 1))) {
+					if (*++cp2 == '0') {
+						char *cp3 = name;
+
+						while (*cp3) {
+							*cp1++ = *cp3++;
+						}
+					} else if (toks[toknum = *cp2 - '1']) {
+						char *cp3 = tp[toknum];
+
+						while (cp3 != te[toknum]) {
+							*cp1++ = *cp3++;
+						}
+					}
+					break;
+				}
+				/* FALLTHROUGH */
+			default:
+				*cp1++ = *cp2;
+				break;
+		}
+		cp2++;
+	}
+	*cp1 = '\0';
+	if (!*new) {
+		return (name);
+	}
+	return (new);
+}
+
diff --git a/small.h b/small.h
new file mode 100644
index 0000000..6dc289f
--- /dev/null
+++ b/small.h
@@ -0,0 +1,35 @@
+/*	$OpenBSD: small.h,v 1.3 2019/05/16 12:44:18 florian Exp $	*/
+
+/*
+ * Copyright (c) 2009 Martynas Venckus <martynas@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+extern jmp_buf jabort;
+extern char   *mname;
+extern char   *home;
+extern char   *stype[];
+
+void	settype(int, char **);
+void	changetype(int, int);
+void	setbinary(int, char **);
+void	get(int, char **);
+int	getit(int, char **, int, const char *);
+void	mabort(int);
+void	mget(int, char **);
+void	cd(int, char **);
+void	disconnect(int, char **);
+char   *dotrans(char *);
+char   *domap(char *);
+
diff --git a/stringlist.c b/stringlist.c
new file mode 100644
index 0000000..00eb711
--- /dev/null
+++ b/stringlist.c
@@ -0,0 +1,97 @@
+/*	$OpenBSD: stringlist.c,v 1.14 2019/05/16 12:44:18 florian Exp $	*/
+/*	$NetBSD: stringlist.c,v 1.2 1997/01/17 07:26:20 lukem Exp $	*/
+
+/*
+ * Copyright (c) 1994 Christos Zoulas
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+#include <stdio.h>
+#include <string.h>
+#include <err.h>
+#include <stdlib.h>
+
+#include "stringlist.h"
+
+#define _SL_CHUNKSIZE	20
+
+/*
+ * sl_init(): Initialize a string list
+ */
+StringList *
+sl_init(void)
+{
+	StringList *sl = malloc(sizeof(StringList));
+	if (sl == NULL)
+		err(1, "stringlist");
+
+	sl->sl_cur = 0;
+	sl->sl_max = _SL_CHUNKSIZE;
+	sl->sl_str = calloc(sl->sl_max, sizeof(char *));
+	if (sl->sl_str == NULL)
+		err(1, "stringlist");
+	return sl;
+}
+
+
+/*
+ * sl_add(): Add an item to the string list
+ */
+void
+sl_add(StringList *sl, char *name)
+{
+	if (sl->sl_cur == sl->sl_max - 1) {
+		sl->sl_max += _SL_CHUNKSIZE;
+		sl->sl_str = reallocarray(sl->sl_str, sl->sl_max,
+		    sizeof(char *));
+		if (sl->sl_str == NULL)
+			err(1, "stringlist");
+	}
+	sl->sl_str[sl->sl_cur++] = name;
+}
+
+
+/*
+ * sl_free(): Free a stringlist
+ */
+void
+sl_free(StringList *sl, int all)
+{
+	size_t i;
+
+	if (sl == NULL)
+		return;
+	if (sl->sl_str) {
+		if (all)
+			for (i = 0; i < sl->sl_cur; i++)
+				free(sl->sl_str[i]);
+		free(sl->sl_str);
+	}
+	free(sl);
+}
+
+#endif /* !SMALL */
+
diff --git a/stringlist.h b/stringlist.h
new file mode 100644
index 0000000..5e9e090
--- /dev/null
+++ b/stringlist.h
@@ -0,0 +1,55 @@
+/*	$OpenBSD: stringlist.h,v 1.9 2024/05/21 05:00:48 jsg Exp $	*/
+/*	$NetBSD: stringlist.h,v 1.2 1997/01/17 06:11:36 lukem Exp $	*/
+
+/*
+ * Copyright (c) 1994 Christos Zoulas
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef SMALL
+
+#ifndef _STRINGLIST_H
+#define _STRINGLIST_H
+
+#include <sys/types.h>
+
+/*
+ * Simple string list
+ */
+typedef struct _stringlist {
+	char	**sl_str;
+	size_t	  sl_max;
+	size_t	  sl_cur;
+} StringList;
+
+__BEGIN_DECLS
+StringList	*sl_init(void);
+void		 sl_add(StringList *, char *);
+void		 sl_free(StringList *, int);
+__END_DECLS
+
+#endif /* _STRINGLIST_H */
+
+#endif /* !SMALL */
+
diff --git a/tls_compat.c b/tls_compat.c
new file mode 100644
index 0000000..cde80a9
--- /dev/null
+++ b/tls_compat.c
@@ -0,0 +1,283 @@
+#include "tls_compat.h"
+
+#ifndef NOSSL
+
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/x509v3.h>
+
+static char tls_error_buf[256];
+static tls_config_t global_tls_config = NULL;
+
+tls_config_t
+tls_config_new(void)
+{
+	tls_config_t config;
+	SSL_CTX *ctx;
+
+	SSL_library_init();
+	SSL_load_error_strings();
+	OpenSSL_add_all_algorithms();
+
+	ctx = SSL_CTX_new(TLS_client_method());
+	if (ctx == NULL)
+		return NULL;
+
+	config = calloc(1, sizeof(*config));
+	if (config == NULL) {
+		SSL_CTX_free(ctx);
+		return NULL;
+	}
+
+	config->ctx = ctx;
+	config->verify_depth = 100;
+	config->noverifycert = 0;
+	config->noverifyname = 0;
+	config->noverifytime = 0;
+	config->muststaple = 0;
+	config->session_fd = -1;
+
+	SSL_CTX_set_options(ctx, SSL_OP_LEGACY_SERVER_CONNECT);
+	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
+
+	return config;
+}
+
+void
+tls_config_free(tls_config_t config)
+{
+	if (config) {
+		if (config->ctx)
+			SSL_CTX_free(config->ctx);
+		free(config);
+	}
+}
+
+int
+tls_config_set_ca_file(tls_config_t config, const char *ca_file)
+{
+	if (SSL_CTX_load_verify_locations(config->ctx, ca_file, NULL) != 1)
+		return -1;
+	return 0;
+}
+
+int
+tls_config_set_ca_path(tls_config_t config, const char *ca_path)
+{
+	if (SSL_CTX_load_verify_locations(config->ctx, NULL, ca_path) != 1)
+		return -1;
+	return 0;
+}
+
+int
+tls_config_set_ciphers(tls_config_t config, const char *ciphers)
+{
+	if (SSL_CTX_set_cipher_list(config->ctx, ciphers) != 1)
+		return -1;
+	return 0;
+}
+
+void
+tls_config_insecure_noverifycert(tls_config_t config)
+{
+	config->noverifycert = 1;
+	SSL_CTX_set_verify(config->ctx, SSL_VERIFY_NONE, NULL);
+}
+
+void
+tls_config_insecure_noverifyname(tls_config_t config)
+{
+	config->noverifyname = 1;
+}
+
+void
+tls_config_verify(tls_config_t config)
+{
+	config->noverifycert = 0;
+	config->noverifyname = 0;
+	SSL_CTX_set_verify(config->ctx, SSL_VERIFY_PEER, NULL);
+}
+
+int
+tls_config_set_verify_depth(tls_config_t config, int depth)
+{
+	config->verify_depth = depth;
+	SSL_CTX_set_verify_depth(config->ctx, depth);
+	return 0;
+}
+
+void
+tls_config_ocsp_require_stapling(tls_config_t config)
+{
+	config->muststaple = 1;
+}
+
+void
+tls_config_insecure_noverifytime(tls_config_t config)
+{
+	config->noverifytime = 1;
+	X509_STORE *store = SSL_CTX_get_cert_store(config->ctx);
+	if (store)
+		X509_STORE_set_flags(store, X509_V_FLAG_NO_CHECK_TIME);
+}
+
+int
+tls_config_set_session_fd(tls_config_t config, int fd)
+{
+	config->session_fd = fd;
+	return 0;
+}
+
+int
+tls_config_parse_protocols(uint32_t *protocols, const char *protostr)
+{
+	*protocols = 0;
+	if (strstr(protostr, "TLSv1.3") || strstr(protostr, "tlsv1.3"))
+		*protocols |= (1 << 0);
+	if (strstr(protostr, "TLSv1.2") || strstr(protostr, "tlsv1.2"))
+		*protocols |= (1 << 1);
+	if (strstr(protostr, "TLSv1.1") || strstr(protostr, "tlsv1.1"))
+		*protocols |= (1 << 2);
+	if (strstr(protostr, "TLSv1.0") || strstr(protostr, "tlsv1.0") || strstr(protostr, "TLSv1") || strstr(protostr, "tlsv1"))
+		*protocols |= (1 << 3);
+	if (*protocols == 0)
+		return -1;
+	return 0;
+}
+
+int
+tls_config_set_protocols(tls_config_t config, uint32_t protocols)
+{
+	long options = 0;
+	if (!(protocols & (1 << 3)))
+		options |= SSL_OP_NO_TLSv1;
+	if (!(protocols & (1 << 2)))
+		options |= SSL_OP_NO_TLSv1_1;
+	if (!(protocols & (1 << 1)))
+		options |= SSL_OP_NO_TLSv1_2;
+	if (!(protocols & (1 << 0)))
+		options |= SSL_OP_NO_TLSv1_3;
+	SSL_CTX_set_options(config->ctx, options);
+	return 0;
+}
+
+const char *
+tls_config_error(tls_config_t config)
+{
+	unsigned long err = ERR_get_error();
+	if (err)
+		ERR_error_string_n(err, tls_error_buf, sizeof(tls_error_buf));
+	else
+		strcpy(tls_error_buf, "unknown error");
+	return tls_error_buf;
+}
+
+tls_t
+tls_client(void)
+{
+	tls_t tls = calloc(1, sizeof(*tls));
+	if (tls == NULL)
+		return NULL;
+	return tls;
+}
+
+int
+tls_configure(tls_t ctx, tls_config_t config)
+{
+	ctx->ctx = config->ctx;
+	global_tls_config = config;
+	return 0;
+}
+
+int
+tls_connect_socket(tls_t ctx, int s, const char *servername)
+{
+	ctx->fd = s;
+	ctx->ssl = SSL_new(ctx->ctx);
+	if (ctx->ssl == NULL)
+		return -1;
+	if (SSL_set_fd(ctx->ssl, s) != 1)
+		return -1;
+	if (servername && SSL_set_tlsext_host_name(ctx->ssl, servername) != 1)
+		return -1;
+	return 0;
+}
+
+int
+tls_handshake(tls_t ctx)
+{
+	int ret = SSL_connect(ctx->ssl);
+	if (ret <= 0) {
+		int err = SSL_get_error(ctx->ssl, ret);
+		if (err == SSL_ERROR_WANT_READ)
+			return TLS_WANT_POLLIN;
+		if (err == SSL_ERROR_WANT_WRITE)
+			return TLS_WANT_POLLOUT;
+		return -1;
+	}
+	return 0;
+}
+
+int
+tls_read(tls_t ctx, void *buf, size_t buflen)
+{
+	return SSL_read(ctx->ssl, buf, buflen);
+}
+
+int
+tls_write(tls_t ctx, const void *buf, size_t buflen)
+{
+	return SSL_write(ctx->ssl, buf, buflen);
+}
+
+int
+tls_close(tls_t ctx)
+{
+	if (ctx && ctx->ssl) {
+		int ret = SSL_shutdown(ctx->ssl);
+		if (ret < 0) {
+			int err = SSL_get_error(ctx->ssl, ret);
+			if (err == SSL_ERROR_WANT_READ)
+				return TLS_WANT_POLLIN;
+			if (err == SSL_ERROR_WANT_WRITE)
+				return TLS_WANT_POLLOUT;
+		}
+		SSL_free(ctx->ssl);
+		ctx->ssl = NULL;
+	}
+	return 0;
+}
+
+void
+tls_free(tls_t ctx)
+{
+	if (ctx) {
+		if (ctx->ssl)
+			SSL_free(ctx->ssl);
+		free(ctx);
+	}
+}
+
+const char *
+tls_error(tls_t ctx)
+{
+	unsigned long err = ERR_get_error();
+	if (err)
+		ERR_error_string_n(err, tls_error_buf, sizeof(tls_error_buf));
+	else
+		strcpy(tls_error_buf, "unknown error");
+	return tls_error_buf;
+}
+
+int
+tls_conn_session_resumed(tls_t ctx)
+{
+	return SSL_session_reused(ctx->ssl);
+}
+
+#endif
\ No newline at end of file
diff --git a/tls_compat.h b/tls_compat.h
new file mode 100644
index 0000000..3283fa4
--- /dev/null
+++ b/tls_compat.h
@@ -0,0 +1,60 @@
+#ifndef TLS_COMPAT_H
+#define TLS_COMPAT_H
+
+#if !defined(NOSSL) || !NOSSL
+
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/x509v3.h>
+
+#define TLS_PROTOCOLS_ALL 0xffffffff
+
+typedef struct tls {
+	SSL *ssl;
+	SSL_CTX *ctx;
+	int fd;
+} *tls_t;
+
+typedef struct tls_config {
+	SSL_CTX *ctx;
+	int verify_depth;
+	int noverifycert;
+	int noverifyname;
+	int noverifytime;
+	int muststaple;
+	int session_fd;
+} *tls_config_t;
+
+#define TLS_WANT_POLLIN SSL_ERROR_WANT_READ
+#define TLS_WANT_POLLOUT SSL_ERROR_WANT_WRITE
+
+tls_config_t tls_config_new(void);
+void tls_config_free(tls_config_t config);
+int tls_config_set_ca_file(tls_config_t config, const char *ca_file);
+int tls_config_set_ca_path(tls_config_t config, const char *ca_path);
+int tls_config_set_ciphers(tls_config_t config, const char *ciphers);
+void tls_config_insecure_noverifycert(tls_config_t config);
+void tls_config_insecure_noverifyname(tls_config_t config);
+void tls_config_verify(tls_config_t config);
+int tls_config_set_verify_depth(tls_config_t config, int depth);
+void tls_config_ocsp_require_stapling(tls_config_t config);
+void tls_config_insecure_noverifytime(tls_config_t config);
+int tls_config_set_session_fd(tls_config_t config, int fd);
+int tls_config_parse_protocols(uint32_t *protocols, const char *protostr);
+int tls_config_set_protocols(tls_config_t config, uint32_t protocols);
+const char *tls_config_error(tls_config_t config);
+
+tls_t tls_client(void);
+int tls_configure(tls_t ctx, tls_config_t config);
+int tls_connect_socket(tls_t ctx, int s, const char *servername);
+int tls_handshake(tls_t ctx);
+int tls_read(tls_t ctx, void *buf, size_t buflen);
+int tls_write(tls_t ctx, const void *buf, size_t buflen);
+int tls_close(tls_t ctx);
+void tls_free(tls_t ctx);
+const char *tls_error(tls_t ctx);
+int tls_conn_session_resumed(tls_t ctx);
+
+#endif
+
+#endif
\ No newline at end of file
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..91c689b
--- /dev/null
+++ b/util.c
@@ -0,0 +1,1167 @@
+/*	$OpenBSD: util.c,v 1.99 2025/12/21 07:29:03 tb Exp $	*/
+/*	$NetBSD: util.c,v 1.12 1997/08/18 10:20:27 lukem Exp $	*/
+
+/*-
+ * Copyright (c) 1997-1999 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Luke Mewburn.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Jason R. Thorpe of the Numerical Aerospace Simulation Facility,
+ * NASA Ames Research Center.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Copyright (c) 1985, 1989, 1993, 1994
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * FTP User Program -- Misc support routines
+ */
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <arpa/ftp.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <glob.h>
+#include <poll.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ftp_var.h"
+#include "pathnames.h"
+
+#define MINIMUM(a, b)	(((a) < (b)) ? (a) : (b))
+#define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
+
+static void updateprogressmeter(int);
+
+/*
+ * Connect to peer server and
+ * auto-login, if possible.
+ */
+void
+setpeer(int argc, char *argv[])
+{
+	char *host, *port;
+
+	if (connected) {
+		fprintf(ttyout, "Already connected to %s, use close first.\n",
+		    hostname);
+		code = -1;
+		return;
+	}
+#ifndef SMALL
+	if (argc < 2)
+		(void)another(&argc, &argv, "to");
+	if (argc < 2 || argc > 3) {
+		fprintf(ttyout, "usage: %s host [port]\n", argv[0]);
+		code = -1;
+		return;
+	}
+#endif /* !SMALL */
+	if (gatemode)
+		port = gateport;
+	else
+		port = ftpport;
+	if (argc > 2)
+		port = argv[2];
+
+	if (gatemode) {
+		if (gateserver == NULL || *gateserver == '\0')
+			errx(1, "gateserver not defined (shouldn't happen)");
+		host = hookup(gateserver, port);
+	} else
+		host = hookup(argv[1], port);
+
+	if (host) {
+		int overbose;
+
+		if (gatemode) {
+			if (command("PASSERVE %s", argv[1]) != COMPLETE)
+				return;
+			if (verbose)
+				fprintf(ttyout,
+				    "Connected via pass-through server %s\n",
+				    gateserver);
+		}
+
+		connected = 1;
+		/*
+		 * Set up defaults for FTP.
+		 */
+		(void)strlcpy(formname, "non-print", sizeof formname);
+		form = FORM_N;
+		(void)strlcpy(modename, "stream", sizeof modename);
+		mode = MODE_S;
+		(void)strlcpy(structname, "file", sizeof structname);
+		stru = STRU_F;
+		(void)strlcpy(bytename, "8", sizeof bytename);
+		bytesize = 8;
+
+		/*
+		 * Set type to 0 (not specified by user),
+		 * meaning binary by default, but don't bother
+		 * telling server.  We can use binary
+		 * for text files unless changed by the user.
+		 */
+		(void)strlcpy(typename, "binary", sizeof typename);
+		curtype = TYPE_A;
+		type = 0;
+		if (autologin)
+			(void)ftp_login(argv[1], NULL, NULL);
+
+		overbose = verbose;
+#ifndef SMALL
+		if (!debug)
+#endif /* !SMALL */
+			verbose = -1;
+		if (command("SYST") == COMPLETE && overbose) {
+			char *cp, c;
+			c = 0;
+			cp = strchr(reply_string + 4, ' ');
+			if (cp == NULL)
+				cp = strchr(reply_string + 4, '\r');
+			if (cp) {
+				if (cp[-1] == '.')
+					cp--;
+				c = *cp;
+				*cp = '\0';
+			}
+
+			fprintf(ttyout, "Remote system type is %s.\n", reply_string + 4);
+			if (cp)
+				*cp = c;
+		}
+		if (!strncmp(reply_string, "215 UNIX Type: L8", 17)) {
+			if (proxy)
+				unix_proxy = 1;
+			else
+				unix_server = 1;
+			if (overbose)
+				fprintf(ttyout, "Using %s mode to transfer files.\n",
+				    typename);
+		} else {
+			if (proxy)
+				unix_proxy = 0;
+			else
+				unix_server = 0;
+		}
+		verbose = overbose;
+	}
+}
+
+/*
+ * login to remote host, using given username & password if supplied
+ */
+int
+ftp_login(const char *host, char *user, char *pass)
+{
+	char tmp[80], *acctname = NULL, host_name[HOST_NAME_MAX+1];
+	char anonpass[LOGIN_NAME_MAX + 1 + HOST_NAME_MAX+1];	/* "user@hostname" */
+	int n, aflag = 0, retry = 0;
+	struct passwd *pw;
+
+#ifndef SMALL
+	if (user == NULL && !anonftp) {
+		if (ruserpass(host, &user, &pass, &acctname) < 0) {
+			code = -1;
+			return (0);
+		}
+	}
+#endif /* !SMALL */
+
+	/*
+	 * Set up arguments for an anonymous FTP session, if necessary.
+	 */
+	if ((user == NULL || pass == NULL) && anonftp) {
+		memset(anonpass, 0, sizeof(anonpass));
+		memset(host_name, 0, sizeof(host_name));
+
+		/*
+		 * Set up anonymous login password.
+		 */
+		if ((user = getlogin()) == NULL) {
+			if ((pw = getpwuid(getuid())) == NULL)
+				user = "anonymous";
+			else
+				user = pw->pw_name;
+		}
+		gethostname(host_name, sizeof(host_name));
+#ifndef DONT_CHEAT_ANONPASS
+		/*
+		 * Every anonymous FTP server I've encountered
+		 * will accept the string "username@", and will
+		 * append the hostname itself.  We do this by default
+		 * since many servers are picky about not having
+		 * a FQDN in the anonymous password. - thorpej@netbsd.org
+		 */
+		snprintf(anonpass, sizeof(anonpass) - 1, "%s@",
+		    user);
+#else
+		snprintf(anonpass, sizeof(anonpass) - 1, "%s@%s",
+		    user, hp->h_name);
+#endif
+		pass = anonpass;
+		user = "anonymous";	/* as per RFC 1635 */
+	}
+
+tryagain:
+	if (retry)
+		user = "ftp";		/* some servers only allow "ftp" */
+
+	while (user == NULL) {
+		char *myname = getlogin();
+
+		if (myname == NULL && (pw = getpwuid(getuid())) != NULL)
+			myname = pw->pw_name;
+		if (myname)
+			fprintf(ttyout, "Name (%s:%s): ", host, myname);
+		else
+			fprintf(ttyout, "Name (%s): ", host);
+		user = myname;
+		if (fgets(tmp, sizeof(tmp), stdin) != NULL) {
+			tmp[strcspn(tmp, "\n")] = '\0';
+			if (tmp[0] != '\0')
+				user = tmp;
+		} else
+			exit(0);
+	}
+	n = command("USER %s", user);
+	if (n == CONTINUE) {
+		if (pass == NULL)
+			pass = getpass("Password:");
+		n = command("PASS %s", pass);
+	}
+	if (n == CONTINUE) {
+		aflag++;
+		if (acctname == NULL)
+			acctname = getpass("Account:");
+		n = command("ACCT %s", acctname);
+	}
+	if ((n != COMPLETE) ||
+	    (!aflag && acctname != NULL && command("ACCT %s", acctname) != COMPLETE)) {
+		warnx("Login %s failed.", user);
+		if (retry || !anonftp)
+			return (0);
+		else
+			retry = 1;
+		goto tryagain;
+	}
+	if (proxy)
+		return (1);
+	connected = -1;
+#ifndef SMALL
+	for (n = 0; n < macnum; ++n) {
+		if (!strcmp("init", macros[n].mac_name)) {
+			(void)strlcpy(line, "$init", sizeof line);
+			makeargv();
+			domacro(margc, margv);
+			break;
+		}
+	}
+#endif /* SMALL */
+	return (1);
+}
+
+/*
+ * `another' gets another argument, and stores the new argc and argv.
+ * It reverts to the top level (via main.c's intr()) on EOF/error.
+ *
+ * Returns false if no new arguments have been added.
+ */
+#ifndef SMALL
+int
+another(int *pargc, char ***pargv, const char *prompt)
+{
+	int len = strlen(line), ret;
+
+	if (len >= sizeof(line) - 3) {
+		fputs("sorry, arguments too long.\n", ttyout);
+		intr();
+	}
+	fprintf(ttyout, "(%s) ", prompt);
+	line[len++] = ' ';
+	if (fgets(&line[len], (int)(sizeof(line) - len), stdin) == NULL) {
+		clearerr(stdin);
+		intr();
+	}
+	len += strlen(&line[len]);
+	if (len > 0 && line[len - 1] == '\n')
+		line[len - 1] = '\0';
+	makeargv();
+	ret = margc > *pargc;
+	*pargc = margc;
+	*pargv = margv;
+	return (ret);
+}
+#endif /* !SMALL */
+
+/*
+ * glob files given in argv[] from the remote server.
+ * if errbuf isn't NULL, store error messages there instead
+ * of writing to the screen.
+ * if type isn't NULL, use LIST instead of NLST, and store filetype.
+ * 'd' means directory, 's' means symbolic link, '-' means plain
+ * file.
+ */
+char *
+remglob2(char *argv[], int doswitch, char **errbuf, FILE **ftemp, char *type)
+{
+	char temp[PATH_MAX], *bufp, *cp, *lmode;
+	static char buf[PATH_MAX], **args;
+	int oldverbose, oldhash, fd;
+
+	if (!mflag) {
+		if (!doglob)
+			args = NULL;
+		else {
+			if (*ftemp) {
+				(void)fclose(*ftemp);
+				*ftemp = NULL;
+			}
+		}
+		return (NULL);
+	}
+	if (!doglob) {
+		if (args == NULL)
+			args = argv;
+		if ((cp = *++args) == NULL)
+			args = NULL;
+		return (cp);
+	}
+	if (*ftemp == NULL) {
+		int len;
+
+		cp = _PATH_TMP;
+		len = strlen(cp);
+		if (len + sizeof(TMPFILE) + (cp[len-1] != '/') > sizeof(temp)) {
+			warnx("unable to create temporary file: %s",
+			    strerror(ENAMETOOLONG));
+			return (NULL);
+		}
+
+		(void)strlcpy(temp, cp, sizeof temp);
+		if (temp[len-1] != '/')
+			temp[len++] = '/';
+		(void)strlcpy(&temp[len], TMPFILE, sizeof temp - len);
+		if ((fd = mkstemp(temp)) == -1) {
+			warn("unable to create temporary file: %s", temp);
+			return (NULL);
+		}
+		close(fd);
+		oldverbose = verbose;
+		verbose = (errbuf != NULL) ? -1 : 0;
+		oldhash = hash;
+		hash = 0;
+		if (doswitch)
+			pswitch(!proxy);
+		for (lmode = "w"; *++argv != NULL; lmode = "a")
+			recvrequest(type ? "LIST" : "NLST", temp, *argv, lmode,
+			    0, 0);
+		if ((code / 100) != COMPLETE) {
+			if (errbuf != NULL)
+				*errbuf = reply_string;
+		}
+		if (doswitch)
+			pswitch(!proxy);
+		verbose = oldverbose;
+		hash = oldhash;
+		*ftemp = fopen(temp, "r");
+		(void)unlink(temp);
+		if (*ftemp == NULL) {
+			if (errbuf == NULL)
+				fputs("can't find list of remote files, oops.\n",
+				    ttyout);
+			else
+				*errbuf =
+				    "can't find list of remote files, oops.";
+			return (NULL);
+		}
+	}
+#ifndef SMALL
+again:
+#endif
+	if (fgets(buf, sizeof(buf), *ftemp) == NULL) {
+		(void)fclose(*ftemp);
+		*ftemp = NULL;
+		return (NULL);
+	}
+
+	buf[strcspn(buf, "\n")] = '\0';
+	bufp = buf;
+
+#ifndef SMALL
+	if (type) {
+		parse_list(&bufp, type);
+		if (!bufp ||
+		    (bufp[0] == '.' &&	/* LIST defaults to -a on some ftp */
+		    (bufp[1] == '\0' ||	/* servers.  Ignore '.' and '..'. */
+		    (bufp[1] == '.' && bufp[2] == '\0'))))
+			goto again;
+	}
+#endif /* !SMALL */
+
+	return (bufp);
+}
+
+/*
+ * wrapper for remglob2
+ */
+char *
+remglob(char *argv[], int doswitch, char **errbuf)
+{
+	static FILE *ftemp = NULL;
+
+	return remglob2(argv, doswitch, errbuf, &ftemp, NULL);
+}
+
+#ifndef SMALL
+int
+confirm(const char *cmd, const char *file)
+{
+	char str[BUFSIZ];
+
+	if (file && (confirmrest || !interactive))
+		return (1);
+top:
+	if (file)
+		fprintf(ttyout, "%s %s? ", cmd, file);
+	else
+		fprintf(ttyout, "Continue with %s? ", cmd);
+	(void)fflush(ttyout);
+	if (fgets(str, sizeof(str), stdin) == NULL)
+		goto quit;
+	switch (tolower((unsigned char)*str)) {
+		case '?':
+			fprintf(ttyout,
+			    "?	help\n"
+			    "a	answer yes to all\n"
+			    "n	answer no\n"
+			    "p	turn off prompt mode\n"
+			    "q	answer no to all\n"
+			    "y	answer yes\n");
+			goto top;
+		case 'a':
+			confirmrest = 1;
+			fprintf(ttyout, "Prompting off for duration of %s.\n",
+			    cmd);
+			break;
+		case 'n':
+			return (0);
+		case 'p':
+			interactive = 0;
+			fputs("Interactive mode: off.\n", ttyout);
+			break;
+		case 'q':
+quit:
+			mflag = 0;
+			clearerr(stdin);
+			return (0);
+		case 'y':
+			return(1);
+			break;
+		default:
+			fprintf(ttyout, "?, a, n, p, q, y "
+			    "are the only acceptable commands!\n");
+			goto top;
+			break;
+	}
+	return (1);
+}
+#endif /* !SMALL */
+
+/*
+ * Glob a local file name specification with
+ * the expectation of a single return value.
+ * Can't control multiple values being expanded
+ * from the expression, we return only the first.
+ */
+int
+globulize(char **cpp)
+{
+	glob_t gl;
+	int flags;
+
+	if (!doglob)
+		return (1);
+
+	flags = GLOB_BRACE|GLOB_NOCHECK|GLOB_QUOTE|GLOB_TILDE;
+	memset(&gl, 0, sizeof(gl));
+	if (glob(*cpp, flags, NULL, &gl) ||
+	    gl.gl_pathc == 0) {
+		warnx("%s: not found", *cpp);
+		globfree(&gl);
+		return (0);
+	}
+		/* XXX: caller should check if *cpp changed, and
+		 *	free(*cpp) if that is the case
+		 */
+	*cpp = strdup(gl.gl_pathv[0]);
+	if (*cpp == NULL)
+		err(1, NULL);
+	globfree(&gl);
+	return (1);
+}
+
+/*
+ * determine size of remote file
+ */
+off_t
+remotesize(const char *file, int noisy)
+{
+	int overbose;
+	off_t size;
+
+	overbose = verbose;
+	size = -1;
+#ifndef SMALL
+	if (!debug)
+#endif /* !SMALL */
+		verbose = -1;
+	if (command("SIZE %s", file) == COMPLETE) {
+		char *cp, *ep;
+
+		cp = strchr(reply_string, ' ');
+		if (cp != NULL) {
+			cp++;
+			size = strtoll(cp, &ep, 10);
+			if (*ep != '\0' && !isspace((unsigned char)*ep))
+				size = -1;
+		}
+	} else if (noisy
+#ifndef SMALL
+	    && !debug
+#endif /* !SMALL */
+	    ) {
+		fputs(reply_string, ttyout);
+		fputc('\n', ttyout);
+	}
+	verbose = overbose;
+	return (size);
+}
+
+/*
+ * determine last modification time (in GMT) of remote file
+ */
+time_t
+remotemodtime(const char *file, int noisy)
+{
+	int overbose;
+	time_t rtime;
+	int ocode;
+
+	overbose = verbose;
+	ocode = code;
+	rtime = -1;
+#ifndef SMALL
+	if (!debug)
+#endif /* !SMALL */
+		verbose = -1;
+	if (command("MDTM %s", file) == COMPLETE) {
+		struct tm timebuf;
+		int yy, mo, day, hour, min, sec;
+		/*
+		 * time-val = 14DIGIT [ "." 1*DIGIT ]
+		 *		YYYYMMDDHHMMSS[.sss]
+		 * mdtm-response = "213" SP time-val CRLF / error-response
+		 */
+		/* TODO: parse .sss as well, use timespecs. */
+		char *timestr = reply_string;
+
+		/* Repair `19%02d' bug on server side */
+		while (!isspace((unsigned char)*timestr))
+			timestr++;
+		while (isspace((unsigned char)*timestr))
+			timestr++;
+		if (strncmp(timestr, "191", 3) == 0) {
+			fprintf(ttyout,
+	    "Y2K warning! Fixed incorrect time-val received from server.\n");
+			timestr[0] = ' ';
+			timestr[1] = '2';
+			timestr[2] = '0';
+		}
+		sscanf(reply_string, "%*s %04d%02d%02d%02d%02d%02d", &yy, &mo,
+			&day, &hour, &min, &sec);
+		memset(&timebuf, 0, sizeof(timebuf));
+		timebuf.tm_sec = sec;
+		timebuf.tm_min = min;
+		timebuf.tm_hour = hour;
+		timebuf.tm_mday = day;
+		timebuf.tm_mon = mo - 1;
+		timebuf.tm_year = yy - 1900;
+		timebuf.tm_isdst = -1;
+		rtime = mktime(&timebuf);
+		if (rtime == -1 && (noisy
+#ifndef SMALL
+		    || debug
+#endif /* !SMALL */
+		    ))
+			fprintf(ttyout, "Can't convert %s to a time.\n", reply_string);
+		else
+			rtime += timebuf.tm_gmtoff;	/* conv. local -> GMT */
+	} else if (noisy
+#ifndef SMALL
+	    && !debug
+#endif /* !SMALL */
+	    ) {
+		fputs(reply_string, ttyout);
+		fputc('\n', ttyout);
+	}
+	verbose = overbose;
+	if (rtime == -1)
+		code = ocode;
+	return (rtime);
+}
+
+/*
+ * Ensure file is in or under dir.
+ * Returns 1 if so, 0 if not (or an error occurred).
+ */
+int
+fileindir(const char *file, const char *dir)
+{
+	char	parentdirbuf[PATH_MAX], *parentdir;
+	char	realdir[PATH_MAX];
+	size_t	dirlen;
+
+					/* determine parent directory of file */
+	(void)strlcpy(parentdirbuf, file, sizeof(parentdirbuf));
+	parentdir = dirname(parentdirbuf);
+	if (strcmp(parentdir, ".") == 0)
+		return 1;		/* current directory is ok */
+
+					/* find the directory */
+	if (realpath(parentdir, realdir) == NULL) {
+		warn("Unable to determine real path of `%s'", parentdir);
+		return 0;
+	}
+	if (realdir[0] != '/')		/* relative result is ok */
+		return 1;
+
+	dirlen = strlen(dir);
+	if (strncmp(realdir, dir, dirlen) == 0 &&
+	    (realdir[dirlen] == '/' || realdir[dirlen] == '\0'))
+		return 1;
+	return 0;
+}
+
+
+/*
+ * Returns true if this is the controlling/foreground process, else false.
+ */
+int
+foregroundproc(void)
+{
+	static pid_t pgrp = -1;
+	int ctty_pgrp;
+
+	if (pgrp == -1)
+		pgrp = getpgrp();
+
+	return((ioctl(STDOUT_FILENO, TIOCGPGRP, &ctty_pgrp) != -1 &&
+	    ctty_pgrp == pgrp));
+}
+
+static void
+updateprogressmeter(int signo)
+{
+	int save_errno = errno;
+
+	/* update progressmeter if foreground process or in -m mode */
+	if (foregroundproc() || progress == -1)
+		progressmeter(0, NULL);
+	errno = save_errno;
+}
+
+/*
+ * Display a transfer progress bar if progress is non-zero.
+ * SIGALRM is hijacked for use by this function.
+ * - Before the transfer, set filesize to size of file (or -1 if unknown),
+ *   and call with flag = -1. This starts the once per second timer,
+ *   and a call to updateprogressmeter() upon SIGALRM.
+ * - During the transfer, updateprogressmeter will call progressmeter
+ *   with flag = 0
+ * - After the transfer, call with flag = 1
+ */
+static struct timespec start;
+
+char *action;
+
+void
+progressmeter(int flag, const char *filename)
+{
+	/*
+	 * List of order of magnitude prefixes.
+	 * The last is `P', as 2^64 = 16384 Petabytes
+	 */
+	static const char prefixes[] = " KMGTP";
+
+	static struct timespec lastupdate;
+	static off_t lastsize;
+	static char *title = NULL;
+	struct timespec now, td, wait;
+	off_t cursize, abbrevsize;
+	double elapsed;
+	int ratio, barlength, i, remaining, overhead = 30;
+	char buf[512], *filenamebuf;
+
+	if (flag == -1) {
+		clock_gettime(CLOCK_MONOTONIC, &start);
+		lastupdate = start;
+		lastsize = restart_point;
+	}
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	if (!progress || filesize < 0)
+		return;
+	cursize = bytes + restart_point;
+
+	if (filesize)
+		ratio = cursize * 100 / filesize;
+	else
+		ratio = 100;
+	ratio = MAXIMUM(ratio, 0);
+	ratio = MINIMUM(ratio, 100);
+	if (!verbose && flag == -1) {
+		if ((filenamebuf = strdup(filename)) != NULL &&
+		    (filename = basename(filenamebuf)) != NULL) {
+			free(title);
+			title = strdup(filename);
+		}
+		free(filenamebuf);
+	}
+
+	buf[0] = 0;
+	if (!verbose && action != NULL) {
+		int l = strlen(action);
+		char *dotdot = "";
+
+		if (l < 7)
+			l = 7;
+		else if (l > 12) {
+			l = 12;
+			dotdot = "...";
+			overhead += 3;
+		}
+		snprintf(buf, sizeof(buf), "\r%-*.*s%s ", l, l, action,
+		    dotdot);
+		overhead += l + 1;
+	} else
+		snprintf(buf, sizeof(buf), "\r");
+
+	if (!verbose && title != NULL) {
+		int l = strlen(title);
+		char *dotdot = "";
+
+		if (l < 12)
+			l = 12;
+		else if (l > 25) {
+			l = 22;
+			dotdot = "...";
+			overhead += 3;
+		}
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    "%-*.*s%s %3d%% ", l, l, title,
+		    dotdot, ratio);
+		overhead += l + 1;
+	} else
+		snprintf(buf, sizeof(buf), "\r%3d%% ", ratio);
+
+	barlength = ttywidth - overhead;
+	if (barlength > 0) {
+		i = barlength * ratio / 100;
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    "|%.*s%*s|", i,
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************",
+		    barlength - i, "");
+	}
+
+	i = 0;
+	abbrevsize = cursize;
+	while (abbrevsize >= 100000 && i < sizeof(prefixes)-1) {
+		i++;
+		abbrevsize >>= 10;
+	}
+	snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+	    " %5lld %c%c ", (long long)abbrevsize, prefixes[i],
+	    prefixes[i] == ' ' ? ' ' : 'B');
+
+	timespecsub(&now, &lastupdate, &wait);
+	if (cursize > lastsize) {
+		lastupdate = now;
+		lastsize = cursize;
+		if (wait.tv_sec >= STALLTIME) {	/* fudge out stalled time */
+			start.tv_sec += wait.tv_sec;
+			start.tv_nsec += wait.tv_nsec;
+		}
+		wait.tv_sec = 0;
+	}
+
+	timespecsub(&now, &start, &td);
+	elapsed = td.tv_sec + (td.tv_nsec / 1000000000.0);
+
+	if (flag == 1) {
+		i = (int)elapsed / 3600;
+		if (i)
+			snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+			    "%2d:", i);
+		else
+			snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+			    "   ");
+		i = (int)elapsed % 3600;
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    "%02d:%02d    ", i / 60, i % 60);
+	} else if (bytes <= 0 || elapsed <= 0.0 || cursize > filesize) {
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    "   --:-- ETA");
+	} else if (wait.tv_sec >= STALLTIME) {
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    " - stalled -");
+	} else {
+		remaining = (int)((filesize - restart_point) /
+				  (bytes / elapsed) - elapsed);
+		i = remaining / 3600;
+		if (i)
+			snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+			    "%2d:", i);
+		else
+			snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+			    "   ");
+		i = remaining % 3600;
+		snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
+		    "%02d:%02d ETA", i / 60, i % 60);
+	}
+	(void)write(fileno(ttyout), buf, strlen(buf));
+
+	if (flag == -1) {
+		(void)signal(SIGALRM, updateprogressmeter);
+		alarmtimer(1);		/* set alarm timer for 1 Hz */
+	} else if (flag == 1) {
+		alarmtimer(0);
+		(void)putc('\n', ttyout);
+		free(title);
+		title = NULL;
+	}
+	fflush(ttyout);
+}
+
+/*
+ * Display transfer statistics.
+ * Requires start to be initialised by progressmeter(-1),
+ * direction to be defined by xfer routines, and filesize and bytes
+ * to be updated by xfer routines
+ * If siginfo is nonzero, an ETA is displayed, and the output goes to STDERR
+ * instead of TTYOUT.
+ */
+void
+ptransfer(int siginfo)
+{
+	struct timespec now, td;
+	double elapsed, pace;
+	off_t bs;
+	int meg, remaining, hh;
+	char buf[100];
+
+	if (!verbose && !siginfo)
+		return;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	timespecsub(&now, &start, &td);
+	elapsed = td.tv_sec + (td.tv_nsec / 1000000000.0);
+	bs = bytes / (elapsed == 0.0 ? 1 : elapsed);
+	meg = 0;
+	if (bs > (1024 * 1024))
+		meg = 1;
+
+	pace = bs / (1024.0 * (meg ? 1024.0 : 1.0));
+	(void)snprintf(buf, sizeof(buf),
+	    "%lld byte%s %s in %lld.%02d seconds (%lld.%02d %sB/s)\n",
+	    (long long)bytes, bytes == 1 ? "" : "s", direction,
+	    (long long)elapsed, (int)(elapsed * 100.0) % 100,
+	    (long long)pace, (int)(pace * 100.0) % 100,
+	    meg ? "M" : "K");
+
+	if (siginfo && bytes > 0 && elapsed > 0.0 && filesize >= 0 &&
+	    bytes + restart_point <= filesize) {
+		remaining = (int)((filesize - restart_point) /
+		    (bytes / elapsed) - elapsed);
+		hh = remaining / 3600;
+		remaining %= 3600;
+
+		/* "buf+len(buf) -1" to overwrite \n */
+		snprintf(buf + strlen(buf) - 1, sizeof(buf) - strlen(buf),
+		    "  ETA: %02d:%02d:%02d\n", hh, remaining / 60,
+		    remaining % 60);
+	}
+	(void)write(siginfo ? STDERR_FILENO : fileno(ttyout), buf, strlen(buf));
+}
+
+/*
+ * List words in stringlist, vertically arranged
+ */
+#ifndef SMALL
+void
+list_vertical(StringList *sl)
+{
+	int i, j, w;
+	int columns, width, lines;
+	char *p;
+
+	width = 0;
+
+	for (i = 0 ; i < sl->sl_cur ; i++) {
+		w = strlen(sl->sl_str[i]);
+		if (w > width)
+			width = w;
+	}
+	width = (width + 8) &~ 7;
+
+	columns = ttywidth / width;
+	if (columns == 0)
+		columns = 1;
+	lines = (sl->sl_cur + columns - 1) / columns;
+	for (i = 0; i < lines; i++) {
+		for (j = 0; j < columns; j++) {
+			p = sl->sl_str[j * lines + i];
+			if (p)
+				fputs(p, ttyout);
+			if (j * lines + i + lines >= sl->sl_cur) {
+				putc('\n', ttyout);
+				break;
+			}
+			w = strlen(p);
+			while (w < width) {
+				w = (w + 8) &~ 7;
+				(void)putc('\t', ttyout);
+			}
+		}
+	}
+}
+#endif /* !SMALL */
+
+/*
+ * Update the global ttywidth value, using TIOCGWINSZ.
+ */
+void
+setttywidth(int signo)
+{
+	int save_errno = errno;
+	struct winsize winsize;
+
+	if (ioctl(fileno(ttyout), TIOCGWINSZ, &winsize) != -1)
+		ttywidth = winsize.ws_col ? winsize.ws_col : 80;
+	else
+		ttywidth = 80;
+	errno = save_errno;
+}
+
+/*
+ * Set the SIGALRM interval timer for wait seconds, 0 to disable.
+ */
+void
+alarmtimer(int wait)
+{
+	int save_errno = errno;
+	struct itimerval itv;
+
+	itv.it_value.tv_sec = wait;
+	itv.it_value.tv_usec = 0;
+	itv.it_interval = itv.it_value;
+	setitimer(ITIMER_REAL, &itv, NULL);
+	errno = save_errno;
+}
+
+/*
+ * Setup or cleanup EditLine structures
+ */
+#ifndef SMALL
+void
+controlediting(void)
+{
+	HistEvent hev;
+
+	if (editing && el == NULL && hist == NULL) {
+		el = el_init(__progname, stdin, ttyout, stderr); /* init editline */
+		hist = history_init();		/* init the builtin history */
+		history(hist, &hev, H_SETSIZE, 100);	/* remember 100 events */
+		el_set(el, EL_HIST, history, hist);	/* use history */
+
+		el_set(el, EL_EDITOR, "emacs");	/* default editor is emacs */
+		el_set(el, EL_PROMPT, prompt);	/* set the prompt function */
+
+		/* add local file completion, bind to TAB */
+		el_set(el, EL_ADDFN, "ftp-complete",
+		    "Context sensitive argument completion",
+		    complete);
+		el_set(el, EL_BIND, "^I", "ftp-complete", NULL);
+
+		el_source(el, NULL);	/* read ~/.editrc */
+		el_set(el, EL_SIGNAL, 1);
+	} else if (!editing) {
+		if (hist) {
+			history_end(hist);
+			hist = NULL;
+		}
+		if (el) {
+			el_end(el);
+			el = NULL;
+		}
+	}
+}
+#endif /* !SMALL */
+
+/*
+ * connect(2) with an optional timeout if secs > 0.
+ */
+int
+timed_connect(int s, const struct sockaddr *name, socklen_t namelen, int secs)
+{
+	struct timespec now, target, timebuf, *timeout = NULL;
+	int flags, nready, optval, ret = -1;
+	socklen_t optlen;
+	struct pollfd pfd;
+
+	if (secs > 0) {
+		timebuf.tv_sec = secs;
+		timebuf.tv_nsec = 0;
+		timeout = &timebuf;
+		clock_gettime(CLOCK_MONOTONIC, &target);
+		timespecadd(&target, timeout, &target);
+	}
+
+	flags = fcntl(s, F_GETFL, 0);
+	if (flags == -1) {
+		warn("fcntl(F_GETFL)");
+		return -1;
+	}
+	if (!(flags & O_NONBLOCK)) {
+		if (fcntl(s, F_SETFL, flags | O_NONBLOCK) == -1) {
+			warn("fcntl(F_SETFL)");
+			return -1;
+		}
+	}
+
+	ret = connect(s, name, namelen);
+	if (ret == 0 || errno != EINPROGRESS)
+		goto done;
+
+	for (;;) {
+		pfd.fd = s;
+		pfd.events = POLLOUT;
+		nready = ppoll(&pfd, 1, timeout, NULL);
+		switch (nready) {
+		case -1:
+			if (errno != EINTR && errno != EAGAIN) {
+				warn("ppoll");
+				goto done;
+			}
+			if (timeout == NULL)
+				continue;
+			clock_gettime(CLOCK_MONOTONIC, &now);
+			timespecsub(&now, &target, timeout);
+			if (timeout->tv_sec >= 0)
+				continue;
+			/* FALLTHROUGH */
+		case 0:
+			errno = ETIMEDOUT;
+			goto done;
+		default:
+			optlen = sizeof(optval);
+			ret = getsockopt(s, SOL_SOCKET, SO_ERROR, &optval,
+			    &optlen);
+			if (ret == 0 && optval != 0) {
+				ret = -1;
+				errno = optval;
+			}
+			goto done;
+		}
+	}
+
+done:
+	if (!(flags & O_NONBLOCK)) {
+		if (fcntl(s, F_SETFL, flags) == -1) {
+			warn("fcntl(F_SETFL)");
+			ret = -1;
+		}
+	}
+
+	return ret;
+}
+
+#ifndef SMALL
+ssize_t
+http_time(time_t t, char *tmbuf, size_t len)
+{
+	struct tm tm;
+ 
+	/* New HTTP/1.1 RFC 7231 prefers IMF-fixdate from RFC 5322 */
+	if (gmtime_r(&t, &tm) == NULL)
+		return 0;
+	else
+		return (strftime(tmbuf, len, "%a, %d %h %Y %T GMT", &tm));
+}
+#endif /* !SMALL */