An HTTP(S), FTP client. draft default tip
authorSunil Nimmagadda <sunil@nimmagadda.net>
Tue, 06 Dec 2022 13:51:55 +0000
changeset 0 1d0ce1ebbc72
An HTTP(S), FTP client. Found a copy of some old OpenBSD days hacking stashed somewhere in the backups. This version saw the light of the day as official OpenBSD ftp(1) for a grand total of 1 day :-)
Makefile
cmd.c
file.c
ftp.1
ftp.c
ftp.h
http.c
main.c
progressmeter.c
regress/Makefile
regress/unit-tests/Makefile
regress/unit-tests/url_parse/Makefile
regress/unit-tests/url_parse/test_url_parse.c
url.c
util.c
xmalloc.c
xmalloc.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,14 @@
+# Define SMALL to disable command line editing
+#CFLAGS+=-DSMALL
+
+PROG=	ftp
+SRCS=	cmd.c file.c ftp.c http.c main.c progressmeter.c url.c util.c xmalloc.c
+
+LDADD+=	-ledit -lcurses -lutil -ltls -lssl -lcrypto
+DPADD+=	${LIBEDIT} ${LIBCURSES} ${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO}
+
+regression-tests:
+	@echo Running regression tests...
+	@cd ${.CURDIR}/regress && ${MAKE} depend && exec ${MAKE} regress
+
+.include <bsd.prog.mk>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,637 @@
+/*
+ * Copyright (c) 2018 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <arpa/telnet.h>
+
+#include <err.h>
+#include <errno.h>
+#include <histedit.h>
+#include <libgen.h>
+#include <limits.h>
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ftp.h"
+
+#define ARGVMAX	64
+
+static void	 cmd_interrupt(int);
+static int	 cmd_lookup(const char *);
+static FILE	*data_fopen(const char *);
+static void	 do_open(int, char **);
+static void	 do_help(int, char **);
+static void	 do_quit(int, char **);
+static void	 do_ls(int, char **);
+static void	 do_pwd(int, char **);
+static void	 do_cd(int, char **);
+static void	 do_get(int, char **);
+static void	 do_passive(int, char **);
+static void	 do_lcd(int, char **);
+static void	 do_lpwd(int, char **);
+static void	 do_put(int, char **);
+static void	 do_mget(int, char **);
+static void	 ftp_abort(void);
+static char	*prompt(void);
+
+static FILE	*ctrl_fp, *data_fp;
+
+static struct {
+	const char	 *name;
+	const char	 *info;
+	void		(*cmd)(int, char **);
+	int		  conn_required;
+} cmd_tbl[] = {
+	{ "open", "connect to remote ftp server", do_open, 0 },
+	{ "close", "terminate ftp session", do_quit, 1 },
+	{ "help", "print local help information", do_help, 0 },
+	{ "?", "print local help information", do_help, 0 },
+	{ "quit", "terminate ftp session and exit", do_quit, 0 },
+	{ "exit", "terminate ftp session and exit", do_quit, 0 },
+	{ "ls", "list contents of remote directory", do_ls, 1 },
+	{ "pwd", "print working directory on remote machine", do_pwd, 1 },
+	{ "cd", "change remote working directory", do_cd, 1 },
+	{ "nlist", "nlist contents of remote directory", do_ls, 1 },
+	{ "get", "receive file", do_get, 1 },
+	{ "passive", "toggle passive transfer mode", do_passive, 0 },
+	{ "lcd", "change local working directory", do_lcd, 0 },
+	{ "lpwd", "print local working directory", do_lpwd, 0 },
+	{ "put", "send one file", do_put, 1 },
+	{ "mget", "get multiple files", do_mget, 1 },
+	{ "mput", "send multiple files", do_mget, 1 },
+};
+
+static void
+cmd_interrupt(int signo)
+{
+	const char	msg[] = "\rwaiting for remote to finish abort\n";
+	int		save_errno = errno;
+
+	if (data_fp != NULL)
+		(void)write(STDERR_FILENO, msg, sizeof(msg) - 1);
+
+	interrupted = 1;
+	errno = save_errno;
+}
+
+void
+cmd(const char *host, const char *port, const char *path)
+{
+	HistEvent	  hev;
+	EditLine	 *el;
+	History		 *hist;
+	const char	 *line;
+	char		**ap, *argv[ARGVMAX], *cp;
+	int		  count, i;
+
+	if ((el = el_init(getprogname(), stdin, stdout, stderr)) == NULL)
+		err(1, "couldn't initialise editline");
+
+	if ((hist = history_init()) == NULL)
+		err(1, "couldn't initialise editline history");
+
+	history(hist, &hev, H_SETSIZE, 100);
+	el_set(el, EL_HIST, history, hist);
+	el_set(el, EL_PROMPT, prompt);
+	el_set(el, EL_EDITOR, "emacs");
+	el_set(el, EL_TERMINAL, NULL);
+	el_set(el, EL_SIGNAL, 1);
+	el_source(el, NULL);
+
+	if (host != NULL) {
+		argv[0] = "open";
+		argv[1] = (char *)host;
+		argv[2] = port ? (char *)port : "21";
+		do_open(3, argv);
+		/* If we don't have a connection, exit */
+		if (ctrl_fp == NULL)
+			exit(1);
+
+		if (path != NULL) {
+			argv[0] = "cd";
+			argv[1] = (char *)path;
+			do_cd(2, argv);
+		}
+	}
+
+	for (;;) {
+		signal(SIGINT, SIG_IGN);
+		if ((line = el_gets(el, &count)) == NULL || count <= 0) {
+			if (verbose)
+				fprintf(stderr, "\n");
+			argv[0] = "quit";
+			do_quit(1, argv);
+			break;
+		}
+
+		if (count <= 1)
+			continue;
+
+		if ((cp = strrchr(line, '\n')) != NULL)
+			*cp = '\0';
+
+		history(hist, &hev, H_ENTER, line);
+		for (ap = argv; ap < &argv[ARGVMAX - 1] &&
+		    (*ap = strsep((char **)&line, " \t")) != NULL;) {
+			if (**ap != '\0')
+				ap++;
+		}
+		*ap = NULL;
+
+		if (argv[0] == NULL)
+			continue;
+
+		if ((i = cmd_lookup(argv[0])) == -1) {
+			fprintf(stderr, "Invalid command.\n");
+			continue;
+		}
+
+		if (cmd_tbl[i].conn_required && ctrl_fp == NULL) {
+			fprintf(stderr, "Not connected.\n");
+			continue;
+		}
+
+		interrupted = 0;
+		signal(SIGINT, cmd_interrupt);
+		cmd_tbl[i].cmd(ap - argv, argv);
+
+		if (strcmp(cmd_tbl[i].name, "quit") == 0 ||
+		    strcmp(cmd_tbl[i].name, "exit") == 0)
+			break;
+	}
+
+	el_end(el);
+}
+
+static int
+cmd_lookup(const char *cmd)
+{
+	size_t	i;
+
+	for (i = 0; i < nitems(cmd_tbl); i++)
+		if (strcmp(cmd, cmd_tbl[i].name) == 0)
+			return i;
+
+	return -1;
+}
+
+static char *
+prompt(void)
+{
+	return "ftp> ";
+}
+
+static FILE *
+data_fopen(const char *mode)
+{
+	int	 fd;
+
+	fd = activemode ? ftp_eprt(ctrl_fp) : ftp_epsv(ctrl_fp);
+	if (fd == -1) {
+		if (io_debug)
+			fprintf(stderr, "Failed to open data connection");
+
+		return NULL;
+	}
+
+	return fdopen(fd, mode);
+}
+
+static void
+ftp_abort(void)
+{
+	char	buf[BUFSIZ];
+
+	snprintf(buf, sizeof buf, "%c%c%c", IAC, IP, IAC);
+	if (send(fileno(ctrl_fp), buf, 3, MSG_OOB) != 3)
+		warn("abort");
+
+	ftp_command(ctrl_fp, "%cABOR", DM);
+}
+
+static void
+do_open(int argc, char **argv)
+{
+	const char	*host = NULL, *port = "21";
+	char		*buf = NULL;
+	size_t		 n = 0;
+	int		 sock;
+
+	if (ctrl_fp != NULL) {
+		fprintf(stderr, "already connected, use close first.\n");
+		return;
+	}
+
+	switch (argc) {
+	case 3:
+		port = argv[2];
+		/* FALLTHROUGH */
+	case 2:
+		host = argv[1];
+		break;
+	default:
+		fprintf(stderr, "usage: open host [port]\n");
+		return;
+	}
+
+	if ((sock = tcp_connect(host, port, 0)) == -1)
+		return;
+
+	fprintf(stderr, "Connected to %s.\n", host);
+	if ((ctrl_fp = fdopen(sock, "r+")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	/* greeting */
+	ftp_getline(&buf, &n, 0, ctrl_fp);
+	free(buf);
+	if (ftp_auth(ctrl_fp, NULL, NULL) != P_OK) {
+		fclose(ctrl_fp);
+		ctrl_fp = NULL;
+	}
+}
+
+static void
+do_help(int argc, char **argv)
+{
+	size_t	i;
+	int	j;
+
+	if (argc == 1) {
+		for (i = 0; i < nitems(cmd_tbl); i++)
+			fprintf(stderr, "%s\n", cmd_tbl[i].name);
+
+		return;
+	}
+
+	for (i = 1; i < (size_t)argc; i++) {
+		if ((j = cmd_lookup(argv[i])) == -1)
+			fprintf(stderr, "invalid help command %s\n", argv[i]);
+		else
+			fprintf(stderr, "%s\t%s\n", argv[i], cmd_tbl[j].info);
+	}
+}
+
+static void
+do_quit(int argc, char **argv)
+{
+	if (ctrl_fp == NULL)
+		return;
+
+	ftp_command(ctrl_fp, "QUIT");
+	fclose(ctrl_fp);
+	ctrl_fp = NULL;
+}
+
+static void
+do_ls(int argc, char **argv)
+{
+	FILE		*dst_fp = stdout;
+	const char	*cmd, *local_fname = NULL, *remote_dir = NULL;
+	char		*buf = NULL;
+	size_t		 n = 0;
+	ssize_t		 len;
+	int		 r;
+
+	switch (argc) {
+	case 3:
+		if (strcmp(argv[2], "-") != 0)
+			local_fname = argv[2];
+		/* FALLTHROUGH */
+	case 2:
+		remote_dir = argv[1];
+		/* FALLTHROUGH */
+	case 1:
+		break;
+	default:
+		fprintf(stderr, "usage: ls [remote-directory [local-file]]\n");
+		return;
+	}
+
+	if ((data_fp = data_fopen("r")) == NULL)
+		return;
+
+	if (local_fname && (dst_fp = fopen(local_fname, "w")) == NULL) {
+		warn("fopen %s", local_fname);
+		fclose(data_fp);
+		data_fp = NULL;
+		return;
+	}
+
+	cmd = (strcmp(argv[0], "ls") == 0) ? "LIST" : "NLST";
+	if (remote_dir != NULL)
+		r = ftp_command(ctrl_fp, "%s %s", cmd, remote_dir);
+	else
+		r = ftp_command(ctrl_fp, "%s", cmd);
+
+	if (r != P_PRE) {
+		fclose(data_fp);
+		data_fp = NULL;
+		if (dst_fp != stdout)
+			fclose(dst_fp);
+
+		return;
+	}
+
+	while ((len = getline(&buf, &n, data_fp)) != -1 && !interrupted) {
+		buf[len - 1] = '\0';
+		if (len >= 2 && buf[len - 2] == '\r')
+			buf[len - 2] = '\0';
+
+		fprintf(dst_fp, "%s\n", buf);
+	}
+
+	if (interrupted)
+		ftp_abort();
+
+	fclose(data_fp);
+	data_fp = NULL;
+	ftp_getline(&buf, &n, 0, ctrl_fp);
+	free(buf);
+	if (dst_fp != stdout)
+		fclose(dst_fp);
+}
+
+static void
+do_get(int argc, char **argv)
+{
+	FILE		*dst_fp;
+	const char	*local_fname = NULL, *p, *remote_fname;
+	char		*buf = NULL;
+	size_t		 n = 0;
+	off_t		 file_sz, offset = 0;
+
+	switch (argc) {
+	case 3:
+		local_fname = argv[2];
+		/* FALLTHROUGH */
+	case 2:
+		remote_fname = argv[1];
+		break;
+	default:
+		fprintf(stderr, "usage: get remote-file [local-file]\n");
+		return;
+	}
+
+	if (local_fname == NULL)
+		local_fname = remote_fname;
+
+	if (ftp_command(ctrl_fp, "TYPE I") != P_OK)
+		return;
+
+	log_info("local: %s remote: %s\n", local_fname, remote_fname);
+	if (ftp_size(ctrl_fp, remote_fname, &file_sz, &buf) != P_OK) {
+		fprintf(stderr, "%s", buf);
+		return;
+	}
+
+	if ((data_fp = data_fopen("r")) == NULL)
+		return;
+
+	if ((dst_fp = fopen(local_fname, "w")) == NULL) {
+		warn("%s", local_fname);
+		fclose(data_fp);
+		data_fp = NULL;
+		return;
+	}
+
+	if (ftp_command(ctrl_fp, "RETR %s", remote_fname) != P_PRE) {
+		fclose(data_fp);
+		data_fp = NULL;
+		fclose(dst_fp);
+		return;
+	}
+
+	if (progressmeter) {
+		p = basename(remote_fname);
+		start_progress_meter(p, NULL, file_sz, &offset);
+	}
+
+	copy_file(dst_fp, data_fp, &offset);
+	if (progressmeter)
+		stop_progress_meter();
+
+	if (interrupted)
+		ftp_abort();
+
+	fclose(data_fp);
+	data_fp = NULL;
+	fclose(dst_fp);
+	ftp_getline(&buf, &n, 0, ctrl_fp);
+	free(buf);
+}
+
+static void
+do_pwd(int argc, char **argv)
+{
+	ftp_command(ctrl_fp, "PWD");
+}
+
+static void
+do_cd(int argc, char **argv)
+{
+	if (argc != 2) {
+		fprintf(stderr, "usage: cd remote-directory\n");
+		return;
+	}
+
+	ftp_command(ctrl_fp, "CWD %s", argv[1]);
+}
+
+static void
+do_passive(int argc, char **argv)
+{
+	switch (argc) {
+	case 1:
+		break;
+	case 2:
+		if (strcmp(argv[1], "on") == 0 || strcmp(argv[1], "off") == 0)
+			break;
+
+		/* FALLTHROUGH */
+	default:
+		fprintf(stderr, "usage: passive [on | off]\n");
+		return;
+	}
+
+	if (argv[1] != NULL) {
+		activemode = (strcmp(argv[1], "off") == 0) ? 1 : 0;
+		fprintf(stderr, "passive mode is %s\n", argv[1]);
+		return;
+	}
+
+	activemode = !activemode;
+	fprintf(stderr, "passive mode is %s\n", activemode ? "off" : "on");
+}
+
+static void
+do_lcd(int argc, char **argv)
+{
+	struct passwd	*pw = NULL;
+	const char	*dir, *login;
+	char		 cwd[PATH_MAX];
+
+	switch (argc) {
+	case 1:
+	case 2:
+		break;
+	default:
+		fprintf(stderr, "usage: lcd [local-directory]\n");
+		return;
+	}
+
+	if ((login = getlogin()) != NULL)
+		pw = getpwnam(login);
+
+	if (pw == NULL && (pw = getpwuid(getuid())) == NULL) {
+		fprintf(stderr, "Failed to get home directory\n");
+		return;
+	}
+
+	dir = argv[1] ? argv[1] : pw->pw_dir;
+	if (chdir(dir) != 0) {
+		warn("local: %s", dir);
+		return;
+	}
+
+	if (getcwd(cwd, sizeof cwd) == NULL) {
+		warn("getcwd");
+		return;
+	}
+
+	fprintf(stderr, "Local directory now %s\n", cwd);
+}
+
+static void
+do_lpwd(int argc, char **argv)
+{
+	char	cwd[PATH_MAX];
+
+	if (getcwd(cwd, sizeof cwd) == NULL) {
+		warn("getcwd");
+		return;
+	}
+
+	fprintf(stderr, "Local directory %s\n", cwd);
+}
+
+static void
+do_put(int argc, char **argv)
+{
+	struct stat	 sb;
+	FILE		*src_fp;
+	const char	*local_fname, *p, *remote_fname = NULL;
+	char		*buf = NULL;
+	size_t		 n = 0;
+	off_t		 file_sz, offset = 0;
+
+	switch (argc) {
+	case 3:
+		remote_fname = argv[2];
+		/* FALLTHROUGH */
+	case 2:
+		local_fname = argv[1];
+		break;
+	default:
+		fprintf(stderr, "usage: put local-file [remote-file]\n");
+		return;
+	}
+
+	if (remote_fname == NULL)
+		remote_fname = local_fname;
+
+	if (ftp_command(ctrl_fp, "TYPE I") != P_OK)
+		return;
+
+	log_info("local: %s remote: %s\n", local_fname, remote_fname);
+	if ((data_fp = data_fopen("w")) == NULL)
+		return;
+
+	if ((src_fp = fopen(local_fname, "r")) == NULL) {
+		warn("%s", local_fname);
+		fclose(data_fp);
+		data_fp = NULL;
+		return;
+	}
+
+	if (fstat(fileno(src_fp), &sb) != 0) {
+		warn("%s", local_fname);
+		fclose(data_fp);
+		data_fp = NULL;
+		fclose(src_fp);
+		return;
+	}
+	file_sz = sb.st_size;
+
+	if (ftp_command(ctrl_fp, "STOR %s", remote_fname) != P_PRE) {
+		fclose(data_fp);
+		data_fp = NULL;
+		fclose(src_fp);
+		return;
+	}
+
+	if (progressmeter) {
+		p = basename(remote_fname);
+		start_progress_meter(p, NULL, file_sz, &offset);
+	}
+
+	copy_file(data_fp, src_fp, &offset);
+	if (progressmeter)
+		stop_progress_meter();
+
+	if (interrupted)
+		ftp_abort();
+
+	fclose(data_fp);
+	data_fp = NULL;
+	fclose(src_fp);
+	ftp_getline(&buf, &n, 0, ctrl_fp);
+	free(buf);
+}
+
+static void
+do_mget(int argc, char **argv)
+{
+	void		(*fn)(int, char **);
+	const char	 *usage;
+	char		 *args[2];
+	int		  i;
+
+	if (strcmp(argv[0], "mget") == 0) {
+		fn = do_get;
+		args[0] = "get";
+		usage = "mget remote-files";
+	} else {
+		fn = do_put;
+		args[0] = "put";
+		usage = "mput local-files";
+	}
+
+	if (argc == 1) {
+		fprintf(stderr, "usage: %s\n", usage);
+		return;
+	}
+
+	for (i = 1; i < argc && !interrupted; i++) {
+		args[1] = argv[i];
+		fn(2, args);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/file.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <sys/stat.h>
+
+#include <err.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+#include "ftp.h"
+
+static FILE	*src_fp;
+
+struct url *
+file_get(struct url *url, off_t *offset, off_t *sz)
+{
+	struct stat	sb;
+	int		src_fd;
+
+	if ((src_fd = fd_request(url->path, O_RDONLY, NULL)) == -1)
+		err(1, "Can't open file %s", url->path);
+
+	if (fstat(src_fd, &sb) == 0)
+		*sz = sb.st_size;
+
+	if ((src_fp = fdopen(src_fd, "r")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	if (*offset && fseeko(src_fp, *offset, SEEK_SET) == -1)
+		err(1, "%s: fseeko", __func__);
+
+	return url;
+}
+
+void
+file_save(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	copy_file(dst_fp, src_fp, offset);
+	fclose(src_fp);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ftp.1	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,411 @@
+.\"	$OpenBSD: ftp.1,v 1.114 2019/05/15 11:53:22 kmos Exp $
+.\"	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
+.\"
+.\" Copyright (c) 2015 Sunil Nimmagadda <sunil@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.
+.\"
+.Dd $Mdocdate: August 13 2015 $
+.Dt FTP 1
+.Os
+.Sh NAME
+.Nm ftp
+.Nd Internet file transfer program
+.Sh SYNOPSIS
+.Nm
+.Op Fl 46AVv
+.Op Fl N Ar name
+.Op Fl D Ar title
+.Op Ar host Op Ar port
+.Nm
+.Op Fl 46ACMmVv
+.Op Fl N Ar name
+.Op Fl D Ar title
+.Op Fl o Ar output
+.Op Fl S Ar tls_options
+.Op Fl U Ar useragent
+.Op Fl w Ar seconds
+.Ar url ...
+.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 usage format 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 addreses 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 C
+Continue a previously interrupted file transfer.
+.Nm
+will continue transferring from an offset equal to the length of file.
+.Pp
+Resuming HTTP(S) transfers are only supported if the remote server supports the
+.Dq Range
+header.
+.It Fl D Ar title
+Specify a short title for the start of the progress bar.
+.It Fl M
+Causes
+.Nm
+to never display the progress meter in cases where it would do so by default.
+.It Fl N Ar name
+Use this alternative name instead of
+.Nm
+in some error reports.
+.It Fl m
+Causes
+.Nm
+to display the progress meter in cases where it would not do so by default.
+.It Fl o Ar output
+When fetching a file or URL, save the contents in
+.Ar output .
+To make the contents go to stdout, use `-' for
+.Ar output .
+.It Fl S Ar tls_options
+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.
+.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 dont
+Don't perform server certificate validation.
+.It Cm protocols Ns = Ns Ar string
+Specify the TLS protocols to use.
+If not specified the value
+.Qq all
+is used.
+Refer to the
+.Xr tls_config_parse_protocols 3
+function for other valid protocol string values.
+.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 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 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 V
+Disable verbose mode.
+.It Fl v
+Enable verbose mode.
+This is the default if input if 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
+Abort a slow connection after
+.Ar seconds .
+.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 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.
+.It Ic close
+Terminate the FTP session with the remote server and
+return to the command interpreter.
+.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 \&? Op Ar command
+A synonym for
+.Ic help .
+.It Ic quit
+Terminate the FTP session with the remote server and exit
+.Nm .
+.It Ic exit
+A synonym for
+.Ic quit .
+.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 no local file is specified, or if
+.Ar local-file
+is
+.Sq - ,
+the output is sent to the terminal.
+.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 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 pwd
+Print the name of the current working directory on the remote
+machine.
+.It Ic cd Ar remote-directory
+Change the working directory on the remote machine
+to
+.Ar remote-directory .
+.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.
+.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 EPRT
+command.
+The
+.Dv EPSV
+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 EPRT
+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.
+.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 lpwd
+Print the working directory on the local machine.
+.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.
+.It Ic mget Ar remote-files
+Do a
+.Ic get
+for each file name specified.
+.It Ic mput Ar local-files
+Do a
+.Ic put
+for each file name specified.
+.El
+.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
+.Sm off
+.It Xo ftp://
+.Ar host Op : Ar port
+.No / Ar file
+.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 .
+.Sm off
+.It Xo http://
+.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.
+.Sm off
+.It Xo https://
+.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.
+.It Pf file: Ar file
+.Ar file
+is retrieved from a mounted file system.
+.El
+.Sh ENVIRONMENT
+.Nm
+utilizes the following environment variables:
+.Bl -tag -width Ds
+.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(S) 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 HISTORY
+The
+.Nm
+command first appeard in
+.Bx 4.2 .
+A complete rewrite of the
+.Nm
+command first appeared in
+.Ox x.x .
+.Sh AUTHORS
+.An Sunil Nimmagadda Aq Mt sunil@openbsd.org
+.Sh CAVEATS
+While aborting a data transfer, certain FTP servers violate
+the protocol by not responding with a 426 reply first, thereby making
+.Nm
+wait indefinitely for a correct reply.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ftp.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,445 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <sys/socket.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+static FILE	*ctrl_fp;
+static int	 data_fd;
+
+void
+ftp_connect(struct url *url, int timeout)
+{
+	char		*buf = NULL;
+	size_t		 n = 0;
+	int		 sock;
+
+	if ((sock = tcp_connect(url->host, url->port, timeout)) == -1)
+		exit(1);
+
+	if ((ctrl_fp = fdopen(sock, "r+")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	/* greeting */
+	if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK) {
+		warnx("Can't connect to host `%s'", url->host);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+
+	free(buf);
+	log_info("Connected to %s\n", url->host);
+	if (ftp_auth(ctrl_fp, NULL, NULL) != P_OK) {
+		warnx("Can't login to host `%s'", url->host);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+}
+
+struct url *
+ftp_get(struct url *url, off_t *offset, off_t *sz)
+{
+	char	*buf = NULL, *dir, *file;
+
+	log_info("Using binary mode to transfer files.\n");
+	if (ftp_command(ctrl_fp, "TYPE I") != P_OK)
+		errx(1, "Failed to set mode to binary");
+
+	dir = dirname(url->path);
+	if (ftp_command(ctrl_fp, "CWD %s", dir) != P_OK)
+		errx(1, "CWD command failed");
+
+	log_info("Retrieving %s\n", url->path);
+	file = basename(url->path);
+	if (oarg && strcmp(oarg, "-") == 0)
+		log_info("remote: %s\n", file);
+	else
+		log_info("local: %s remote: %s\n", oarg ? oarg : file , file);
+
+	if (ftp_size(ctrl_fp, file, sz, &buf) != P_OK) {
+		fprintf(stderr, "%s", buf);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+	free(buf);
+
+	if (activemode)
+		data_fd = ftp_eprt(ctrl_fp);
+	else if ((data_fd = ftp_epsv(ctrl_fp)) == -1)
+		data_fd = ftp_eprt(ctrl_fp);
+
+	if (data_fd == -1)
+		errx(1, "Failed to establish data connection");
+
+	if (*offset && ftp_command(ctrl_fp, "REST %lld", *offset) != P_INTER)
+		errx(1, "REST command failed");
+
+	if (ftp_command(ctrl_fp, "RETR %s", file) != P_PRE) {
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+
+	return url;
+}
+
+void
+ftp_save(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	struct sockaddr_storage	 ss;
+	FILE			*data_fp;
+	socklen_t		 len;
+	int			 s;
+
+	if (activemode) {
+		len = sizeof(ss);
+		if ((s = accept(data_fd, (struct sockaddr *)&ss, &len)) == -1)
+			err(1, "%s: accept", __func__);
+
+		close(data_fd);
+		data_fd = s;
+	}
+
+	if ((data_fp = fdopen(data_fd, "r")) == NULL)
+		err(1, "%s: fdopen data_fd", __func__);
+
+	copy_file(dst_fp, data_fp, offset);
+	fclose(data_fp);
+}
+
+void
+ftp_close(struct url *url)
+{
+	char	*buf = NULL;
+	size_t	 n = 0;
+
+	/*
+	 * Reading reply here after progressmeter stops.
+	 */
+	if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK)
+		errx(1, "%s: %s", __func__, buf);
+
+	free(buf);
+	ftp_command(ctrl_fp, "QUIT");
+	fclose(ctrl_fp);
+}
+
+int
+ftp_getline(char **lineptr, size_t *n, int suppress_output, FILE *fp)
+{
+	ssize_t		 len;
+	char		*bufp, code[4];
+	const char	*errstr;
+	int		 lookup[] = { P_PRE, P_OK, P_INTER, N_TRANS, N_PERM };
+
+
+	if ((len = getline(lineptr, n, fp)) == -1)
+		err(1, "%s: getline", __func__);
+
+	bufp = *lineptr;
+	if (!suppress_output)
+		log_info("%s", bufp);
+
+	if (len < 4)
+		errx(1, "%s: line too short", __func__);
+
+	(void)strlcpy(code, bufp, sizeof code);
+	if (bufp[3] == ' ')
+		goto done;
+
+	/* multi-line reply */
+	while (!(strncmp(code, bufp, 3) == 0 && bufp[3] == ' ')) {
+		if ((len = getline(lineptr, n, fp)) == -1)
+			err(1, "%s: getline", __func__);
+
+		bufp = *lineptr;
+		if (!suppress_output)
+			log_info("%s", bufp);
+
+		if (len < 4)
+			continue;
+	}
+
+ done:
+	(void)strtonum(code, 100, 553, &errstr);
+	if (errstr)
+		errx(1, "%s: Response code is %s: %s", __func__, errstr, code);
+
+	return lookup[code[0] - '1'];
+}
+
+int
+ftp_command(FILE *fp, const char *fmt, ...)
+{
+	va_list	 ap;
+	char	*buf = NULL, *cmd;
+	size_t	 n = 0;
+	int	 r;
+
+	va_start(ap, fmt);
+	r = vasprintf(&cmd, fmt, ap);
+	va_end(ap);
+	if (r < 0)
+		errx(1, "%s: vasprintf", __func__);
+
+	if (io_debug)
+		fprintf(stderr, ">>> %s\n", cmd);
+
+	if (fprintf(fp, "%s\r\n", cmd) < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	free(cmd);
+	r = ftp_getline(&buf, &n, 0, fp);
+	free(buf);
+	return r;
+
+}
+
+int
+ftp_auth(FILE *fp, const char *user, const char *pass)
+{
+	char	*addr = NULL, hn[HOST_NAME_MAX+1], *un;
+	int	 code;
+
+	code = ftp_command(fp, "USER %s", user ? user : "anonymous");
+	if (code != P_OK && code != P_INTER)
+		return code;
+
+	if (pass == NULL) {
+		if (gethostname(hn, sizeof hn) == -1)
+			err(1, "%s: gethostname", __func__);
+
+		un = getlogin();
+		xasprintf(&addr, "%s@%s", un ? un : "anonymous", hn);
+	}
+
+	code = ftp_command(fp, "PASS %s", pass ? pass : addr);
+	free(addr);
+	return code;
+}
+
+int
+ftp_size(FILE *fp, const char *fn, off_t *sizep, char **buf)
+{
+	size_t	 n = 0;
+	off_t	 file_sz;
+	int	 code;
+
+	if (io_debug)
+		fprintf(stderr, ">>> SIZE %s\n", fn);
+
+	if (fprintf(fp, "SIZE %s\r\n", fn) < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	if ((code = ftp_getline(buf, &n, 1, fp)) != P_OK)
+		return code;
+
+	if (sscanf(*buf, "%*u %lld", &file_sz) != 1)
+		errx(1, "%s: sscanf size", __func__);
+
+	if (file_sz < 0 || file_sz > INT64_MAX)
+		errx(1, "%s: size out of bounds: %lld", __func__, file_sz);
+
+	if (sizep)
+		*sizep = file_sz;
+
+	return code;
+}
+
+int
+ftp_eprt(FILE *fp)
+{
+	struct sockaddr_storage	 ss;
+	char			 addr[NI_MAXHOST], port[NI_MAXSERV], *eprt;
+	socklen_t		 len;
+	int			 e, on, ret, sock;
+
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getsockname(fileno(fp), (struct sockaddr *)&ss, &len) == -1) {
+		warn("%s: getsockname", __func__);
+		return -1;
+	}
+
+	/* pick a free port */
+	switch (ss.ss_family) {
+	case AF_INET:
+		((struct sockaddr_in *)&ss)->sin_port = 0;
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)&ss)->sin6_port = 0;
+		break;
+	default:
+		errx(1, "%s: Invalid socket family", __func__);
+	}
+
+	if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) {
+		warn("%s: socket", __func__);
+		return -1;
+	}
+
+	switch (ss.ss_family) {
+	case AF_INET:
+		on = IP_PORTRANGE_HIGH;
+		if (setsockopt(sock, IPPROTO_IP, IP_PORTRANGE,
+		    (char *)&on, sizeof(on)) < 0)
+			warn("setsockopt IP_PORTRANGE (ignored)");
+		break;
+	case AF_INET6:
+		on = IPV6_PORTRANGE_HIGH;
+		if (setsockopt(sock, IPPROTO_IPV6, IPV6_PORTRANGE,
+		    (char *)&on, sizeof(on)) < 0)
+			warn("setsockopt IPV6_PORTRANGE (ignored)");
+		break;
+	}
+
+	if (bind(sock, (struct sockaddr *)&ss, len) == -1) {
+		close(sock);
+		warn("%s: bind", __func__);
+		return -1;
+	}
+
+	if (listen(sock, 1) == -1) {
+		close(sock);
+		warn("%s: listen", __func__);
+		return -1;
+	}
+
+	/* Find out the ephemeral port chosen */
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getsockname(sock, (struct sockaddr *)&ss, &len) == -1) {
+		close(sock);
+		warn("%s: getsockname", __func__);
+		return -1;
+	}
+
+	if ((e = getnameinfo((struct sockaddr *)&ss, len,
+	    addr, sizeof(addr), port, sizeof(port),
+	    NI_NUMERICHOST | NI_NUMERICSERV)) != 0) {
+		close(sock);
+		warn("%s: getnameinfo: %s", __func__, gai_strerror(e));
+		return -1;
+	}
+
+	xasprintf(&eprt, "EPRT |%d|%s|%s|",
+	    ss.ss_family == AF_INET ? 1 : 2, addr, port);
+
+	ret = ftp_command(fp, "%s", eprt);
+	free(eprt);
+	if (ret != P_OK) {
+		close(sock);
+		return -1;
+	}
+
+	return sock;
+}
+
+int
+ftp_epsv(FILE *fp)
+{
+	struct sockaddr_storage	 ss;
+	char			*buf = NULL, delim[4], *s, *e;
+	size_t			 n = 0;
+	socklen_t		 len;
+	int			 error, port, sock;
+
+	if (io_debug)
+		fprintf(stderr, ">>> EPSV\n");
+
+	if (fprintf(fp, "EPSV\r\n") < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	if (ftp_getline(&buf, &n, 1, fp) != P_OK) {
+		free(buf);
+		return -1;
+	}
+
+	if ((s = strchr(buf, '(')) == NULL || (e = strchr(s, ')')) == NULL) {
+		warnx("Malformed EPSV reply");
+		free(buf);
+		return -1;
+	}
+
+	s++;
+	*e = '\0';
+	if (sscanf(s, "%c%c%c%d%c", &delim[0], &delim[1], &delim[2],
+	    &port, &delim[3]) != 5) {
+		warnx("EPSV parse error");
+		free(buf);
+		return -1;
+	}
+	free(buf);
+
+	if (delim[0] != delim[1] || delim[0] != delim[2]
+	    || delim[0] != delim[3]) {
+		warnx("EPSV parse error");
+		return -1;
+	}
+
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getpeername(fileno(fp), (struct sockaddr *)&ss, &len) == -1) {
+		warn("%s: getpeername", __func__);
+		return -1;
+	}
+
+	switch (ss.ss_family) {
+	case AF_INET:
+		((struct sockaddr_in *)&ss)->sin_port = htons(port);
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)&ss)->sin6_port = htons(port);
+		break;
+	default:
+		errx(1, "%s: Invalid socket family", __func__);
+	}
+
+	if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) {
+		warn("%s: socket", __func__);
+		return -1;
+	}
+
+	for (error = connect(sock, (struct sockaddr *)&ss, len);
+	     error != 0 && errno == EINTR; error = connect_wait(sock))
+		continue;
+
+	if (error != 0) {
+		warn("%s: connect", __func__);
+		return -1;
+	}
+
+	return sock;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ftp.h	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@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	FTP_H
+#define	FTP_H
+
+#include <sys/types.h>
+
+#include <signal.h>
+#include <stdarg.h>
+
+#define	S_HTTP	0
+#define S_FTP	1
+#define S_FILE	2
+#define S_HTTPS	3
+
+#define TMPBUF_LEN	131072
+
+#define P_PRE	100
+#define P_OK	200
+#define P_INTER	300
+#define N_TRANS	400
+#define	N_PERM	500
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif /* nitems */
+
+struct url {
+	int	 scheme;
+	int	 ip_literal;
+	char	*host;
+	char	*port;
+	char	*path;
+	char	*basic_auth;
+};
+
+/* main.c */
+extern volatile sig_atomic_t interrupted;
+extern struct url	*ftp_proxy, *http_proxy;
+extern const char	*useragent;
+extern char		*oarg;
+extern int		 activemode, family, io_debug, verbose, progressmeter;
+
+int		 fd_request(char *, int, off_t *);
+
+/* cmd.c */
+void		 cmd(const char *, const char *, const char *);
+
+/* file.c */
+struct url	*file_get(struct url *, off_t *, off_t *);
+void		 file_save(struct url *, FILE *, off_t *);
+
+/* ftp.c */
+void		 ftp_connect(struct url *, int);
+struct url	*ftp_get(struct url *, off_t *, off_t *);
+void		 ftp_close(struct url *);
+void		 ftp_save(struct url *, FILE *, off_t *);
+int		 ftp_auth(FILE *, const char *, const char *);
+int		 ftp_command(FILE *, const char *, ...)
+		     __attribute__((__format__ (printf, 2, 3)))
+		     __attribute__((__nonnull__ (2)));
+int		 ftp_eprt(FILE *);
+int		 ftp_epsv(FILE *);
+int		 ftp_getline(char **, size_t *, int, FILE *);
+int		 ftp_size(FILE *, const char *, off_t *, char **);
+
+/* http.c */
+void		 http_connect(struct url *, int);
+struct url	*http_get(struct url *, off_t *, off_t *);
+void		 http_close(struct url *);
+void		 http_save(struct url *, FILE *, off_t *);
+void		 https_init(char *);
+
+/* progressmeter.c */
+void		start_progress_meter(const char *, const char *, off_t, off_t *);
+void		stop_progress_meter(void);
+
+/* url.c */
+int		 url_scheme_lookup(const char *);
+void		 url_connect(struct url *, int);
+char		*url_encode(const char *);
+void		 url_free(struct url *);
+struct url	*xurl_parse(const char *);
+struct url	*url_parse(const char *);
+struct url	*url_request(struct url *, off_t *, off_t *);
+void		 url_save(struct url *, FILE *, off_t *);
+void		 url_close(struct url *);
+char		*url_str(struct url *);
+const char	*url_scheme_str(int);
+const char	*url_port_str(int);
+
+/* util.c */
+int		 connect_wait(int);
+void		 copy_file(FILE *, FILE *, off_t *);
+int		 tcp_connect(const char *, const char *, int);
+void		 log_request(const char *, struct url *, struct url *);
+void		 log_info(const char *, ...)
+		     __attribute__((__format__ (printf, 1, 2)))
+		     __attribute__((__nonnull__ (1)));
+
+#endif	/* FTP_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/http.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,833 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@openbsd.org>
+ * Copyright (c) 2012 - 2015 Reyk Floeter <reyk@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.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#ifndef NOSSL
+#include <tls.h>
+#endif /* NOSSL */
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+#define MAX_REDIRECTS		10
+
+#ifndef NOSSL
+#define MINBUF			128
+
+static struct tls_config	*tls_config;
+static struct tls		*ctx;
+static int			 tls_session_fd = -1;
+static char * const		 tls_verify_opts[] = {
+#define HTTP_TLS_CAFILE		0
+	"cafile",
+#define HTTP_TLS_CAPATH		1
+	"capath",
+#define HTTP_TLS_CIPHERS	2
+	"ciphers",
+#define HTTP_TLS_DONTVERIFY	3
+	"dont",
+#define HTTP_TLS_VERIFYDEPTH	4
+	"depth",
+#define HTTP_TLS_MUSTSTAPLE	5
+	"muststaple",
+#define HTTP_TLS_NOVERIFYTIME	6
+	"noverifytime",
+#define HTTP_TLS_SESSION	7
+	"session",
+#define HTTP_TLS_DOVERIFY	8
+	"do",
+	NULL
+};
+#endif /* NOSSL */
+
+/*
+ * HTTP status codes based on IANA assignments (2014-06-11 version):
+ * https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * plus legacy (306) and non-standard (420).
+ */
+static struct http_status {
+	int		 code;
+	const char	*name;
+} http_status[] = {
+	{ 100,	"Continue" },
+	{ 101,	"Switching Protocols" },
+	{ 102,	"Processing" },
+	/* 103-199 unassigned */
+	{ 200,	"OK" },
+	{ 201,	"Created" },
+	{ 202,	"Accepted" },
+	{ 203,	"Non-Authoritative Information" },
+	{ 204,	"No Content" },
+	{ 205,	"Reset Content" },
+	{ 206,	"Partial Content" },
+	{ 207,	"Multi-Status" },
+	{ 208,	"Already Reported" },
+	/* 209-225 unassigned */
+	{ 226,	"IM Used" },
+	/* 227-299 unassigned */
+	{ 300,	"Multiple Choices" },
+	{ 301,	"Moved Permanently" },
+	{ 302,	"Found" },
+	{ 303,	"See Other" },
+	{ 304,	"Not Modified" },
+	{ 305,	"Use Proxy" },
+	{ 306,	"Switch Proxy" },
+	{ 307,	"Temporary Redirect" },
+	{ 308,	"Permanent Redirect" },
+	/* 309-399 unassigned */
+	{ 400,	"Bad Request" },
+	{ 401,	"Unauthorized" },
+	{ 402,	"Payment Required" },
+	{ 403,	"Forbidden" },
+	{ 404,	"Not Found" },
+	{ 405,	"Method Not Allowed" },
+	{ 406,	"Not Acceptable" },
+	{ 407,	"Proxy Authentication Required" },
+	{ 408,	"Request Timeout" },
+	{ 409,	"Conflict" },
+	{ 410,	"Gone" },
+	{ 411,	"Length Required" },
+	{ 412,	"Precondition Failed" },
+	{ 413,	"Payload Too Large" },
+	{ 414,	"URI Too Long" },
+	{ 415,	"Unsupported Media Type" },
+	{ 416,	"Range Not Satisfiable" },
+	{ 417,	"Expectation Failed" },
+	{ 418,	"I'm a teapot" },
+	/* 419-421 unassigned */
+	{ 420,	"Enhance Your Calm" },
+	{ 422,	"Unprocessable Entity" },
+	{ 423,	"Locked" },
+	{ 424,	"Failed Dependency" },
+	/* 425 unassigned */
+	{ 426,	"Upgrade Required" },
+	/* 427 unassigned */
+	{ 428,	"Precondition Required" },
+	{ 429,	"Too Many Requests" },
+	/* 430 unassigned */
+	{ 431,	"Request Header Fields Too Large" },
+	/* 432-450 unassigned */
+	{ 451,	"Unavailable For Legal Reasons" },
+	/* 452-499 unassigned */
+	{ 500,	"Internal Server Error" },
+	{ 501,	"Not Implemented" },
+	{ 502,	"Bad Gateway" },
+	{ 503,	"Service Unavailable" },
+	{ 504,	"Gateway Timeout" },
+	{ 505,	"HTTP Version Not Supported" },
+	{ 506,	"Variant Also Negotiates" },
+	{ 507,	"Insufficient Storage" },
+	{ 508,	"Loop Detected" },
+	/* 509 unassigned */
+	{ 510,	"Not Extended" },
+	{ 511,	"Network Authentication Required" },
+	/* 512-599 unassigned */
+	{ 0,	NULL },
+};
+
+struct http_headers {
+	char	*location;
+	off_t	 content_length;
+	int	 chunked;
+	int	 retry_after;
+};
+
+static void		 decode_chunk(int, uint, FILE *);
+static char		*header_lookup(const char *, const char *);
+static const char	*http_error(int);
+static int		 http_status_cmp(const void *, const void *);
+static void		 http_headers_free(struct http_headers *);
+static ssize_t		 http_getline(int, char **, size_t *);
+static void		 http_proxy_connect(struct url *, struct url *);
+static char		*http_prepare_request(struct url *, off_t *);
+static size_t		 http_read(int, char *, size_t);
+static struct url	*http_redirect(struct url *, char *);
+static void		 http_copy_chunks(struct url *, FILE *, off_t *);
+static int		 http_request(int, const char *,
+			    struct http_headers **);
+static char		*relative_path_resolve(const char *, const char *);
+
+#ifndef NOSSL
+static void		 tls_copy_file(struct url *, FILE *, off_t *);
+static ssize_t		 tls_getline(char **, size_t *, struct tls *);
+#endif /* NOSSL */
+
+static FILE		*fp;
+static int		 chunked;
+
+void
+http_connect(struct url *url, int timeout)
+{
+	static struct url	*proxy;
+	const char		*host, *port;
+	int			 sock;
+
+	proxy = (url->scheme == S_HTTPS || url->scheme == S_HTTP) ?
+	    http_proxy : ftp_proxy;
+
+	host = proxy ? proxy->host : url->host;
+	port = proxy ? proxy->port : url->port;
+	if ((sock = tcp_connect(host, port, timeout)) == -1)
+		exit(1);
+
+	if ((fp = fdopen(sock, "r+")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	if (proxy)
+		http_proxy_connect(proxy, url);
+
+#ifndef NOSSL
+	if (url->scheme != S_HTTPS)
+		return;
+
+	if ((ctx = tls_client()) == NULL)
+		errx(1, "failed to create tls client");
+
+	if (tls_configure(ctx, tls_config) != 0)
+		errx(1, "%s: %s", __func__, tls_error(ctx));
+
+	if (tls_connect_socket(ctx, fileno(fp), url->host) != 0)
+		errx(1, "%s: %s", __func__, tls_error(ctx));
+#endif /* NOSSL */
+}
+
+static void
+http_proxy_connect(struct url *proxy, struct url *url)
+{
+	struct http_headers	*headers;
+	char			*auth = NULL, *req;
+	int			 authlen = 0, code;
+
+	if (proxy->basic_auth) {
+		authlen = xasprintf(&auth,
+		    "Proxy-Authorization: Basic %s\r\n", proxy->basic_auth);
+	}
+
+	xasprintf(&req,
+	    "CONNECT %s:%s HTTP/1.0\r\n"
+	    "User-Agent: %s\r\n"
+	    "%s"
+	    "\r\n",
+	    url->host, url->port,
+	    useragent,
+	    proxy->basic_auth ? auth : "");
+
+	freezero(auth, authlen);
+	if ((code = http_request(S_HTTP, req, &headers)) != 200)
+		errx(1, "%s: Failed to CONNECT to %s:%s: %d %s",
+			__func__, url->host, url->port, code, http_error(code));
+
+	free(req);
+	http_headers_free(headers);
+}
+
+static char *
+http_prepare_request(struct url *url, off_t *offset)
+{
+	char	*auth = NULL, *path = NULL, *range = NULL, *req;
+	int	 authlen = 0;
+
+	if (*offset)
+		xasprintf(&range, "Range: bytes=%lld-\r\n", *offset);
+
+	if (url->basic_auth) {
+		authlen = xasprintf(&auth,
+		    "Authorization: Basic %s\r\n", url->basic_auth);
+	}
+
+	if (url->path)
+		path = url_encode(url->path);
+
+	xasprintf(&req,
+	    "GET %s HTTP/1.1\r\n"
+	    "Host: %s\r\n"
+	    "%s"
+	    "%s"
+	    "Connection: close\r\n"
+	    "User-Agent: %s\r\n"
+	    "\r\n",
+	    path ? path : "/",
+	    url->host,
+	    *offset ? range : "",
+	    url->basic_auth ? auth : "",
+	    useragent);
+
+	free(range);
+	freezero(auth, authlen);
+	free(path);
+	return req;
+}
+
+struct url *
+http_get(struct url *url, off_t *offset, off_t *sz)
+{
+	struct http_headers	*headers;
+	char			*req;
+	int			 code, redirects = 0, retry = 0;
+
+	do {
+		log_request("Requesting", url, http_proxy);
+		req = http_prepare_request(url, offset);
+		code = http_request(url->scheme, req, &headers);
+		free(req);
+		switch (code) {
+		case 200:
+			if (*offset) {
+				warnx("Server does not support resume.");
+				*offset = 0;
+			}
+			break;
+		case 206:
+			break;
+		case 301:
+		case 302:
+		case 303:
+		case 307:
+			http_close(url);
+			if (++redirects > MAX_REDIRECTS)
+				errx(1, "Too many redirections requested.");
+
+			if (headers->location == NULL) {
+				errx(1,
+				    "%s: Location header missing", __func__);
+			}
+
+			url = http_redirect(url, headers->location);
+			http_headers_free(headers);
+			log_request("Redirected to", url, http_proxy);
+			http_connect(url, 0);
+			break;
+		case 416:
+			errx(1, "File is already fully retrieved.");
+			break;
+		case 503:
+			if (headers->retry_after == 0 && retry == 0) {
+				http_close(url);
+				http_headers_free(headers);
+				retry = 1;
+				log_request("Retrying", url, http_proxy);
+				http_connect(url, 0);
+				break;
+			}
+			/* FALLTHROUGH */
+		default:
+			errx(1, "Error retrieving file: %d %s",
+			    code, http_error(code));
+		}
+	} while (code == 301 || code == 302 ||
+	    code == 303 || code == 307 || code == 503);
+
+	*sz = headers->content_length + *offset;
+	chunked = headers->chunked;
+	http_headers_free(headers);
+	return url;
+}
+
+void
+http_save(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	if (chunked)
+		http_copy_chunks(url, dst_fp, offset);
+#ifndef NOSSL
+	else if (url->scheme == S_HTTPS)
+		tls_copy_file(url, dst_fp, offset);
+#endif /* NOSSL */
+	else
+		copy_file(dst_fp, fp, offset);
+}
+
+static struct url *
+http_redirect(struct url *old_url, char *location)
+{
+	struct url	*new_url;
+
+	/* absolute uri reference */
+	if (strncasecmp(location, "http", 4) == 0 ||
+	    strncasecmp(location, "https", 5) == 0) {
+		new_url = xurl_parse(location);
+		goto done;
+	}
+
+	/* relative uri reference */
+	new_url = xcalloc(1, sizeof *new_url);
+	new_url->scheme = old_url->scheme;
+	new_url->host = xstrdup(old_url->host);
+	new_url->port = xstrdup(old_url->port);
+
+	/* absolute-path reference */
+	if (location[0] == '/')
+		new_url->path = xstrdup(location);
+	else
+		new_url->path = relative_path_resolve(old_url->path, location);
+
+ done:
+	url_free(old_url);
+	return new_url;
+}
+
+static char *
+relative_path_resolve(const char *base_path, const char *location)
+{
+	char	*new_path, *p;
+
+	/* trim fragment component from both uri */
+	if ((p = strchr(location, '#')) != NULL)
+		*p = '\0';
+	if (base_path && (p = strchr(base_path, '#')) != NULL)
+		*p = '\0';
+
+	if (base_path == NULL)
+		xasprintf(&new_path, "/%s", location);
+	else if (base_path[strlen(base_path) - 1] == '/')
+		xasprintf(&new_path, "%s%s", base_path, location);
+	else {
+		p = dirname(base_path);
+		xasprintf(&new_path, "%s/%s",
+		    strcmp(p, ".") == 0 ? "" : p, location);
+	}
+
+	return new_path;
+}
+
+static void
+http_copy_chunks(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	char	*buf = NULL;
+	size_t	 n = 0;
+	uint	 chunk_sz;
+
+	http_getline(url->scheme, &buf, &n);
+	if (sscanf(buf, "%x", &chunk_sz) != 1)
+		errx(1, "%s: Failed to get chunk size", __func__);
+
+	while (chunk_sz > 0) {
+		decode_chunk(url->scheme, chunk_sz, dst_fp);
+		*offset += chunk_sz;
+		http_getline(url->scheme, &buf, &n);
+		if (sscanf(buf, "%x", &chunk_sz) != 1)
+			errx(1, "%s: Failed to get chunk size", __func__);
+	}
+
+	free(buf);
+}
+
+static void
+decode_chunk(int scheme, uint sz, FILE *dst_fp)
+{
+	size_t	bufsz;
+	size_t	r;
+	char	buf[BUFSIZ], crlf[2];
+
+	bufsz = sizeof(buf);
+	while (sz > 0) {
+		if (sz < bufsz)
+			bufsz = sz;
+
+		r = http_read(scheme, buf, bufsz);
+		if (fwrite(buf, 1, r, dst_fp) != r)
+			errx(1, "%s: fwrite", __func__);
+
+		sz -= r;
+	}
+
+	/* CRLF terminating the chunk */
+	if (http_read(scheme, crlf, sizeof(crlf)) != sizeof(crlf))
+		errx(1, "%s: Failed to read terminal crlf", __func__);
+
+	if (crlf[0] != '\r' || crlf[1] != '\n')
+		errx(1, "%s: Invalid chunked encoding", __func__);
+}
+
+void
+http_close(struct url *url)
+{
+#ifndef NOSSL
+	ssize_t	r;
+
+	if (url->scheme == S_HTTPS) {
+		if (tls_session_fd != -1)
+			dprintf(STDERR_FILENO, "tls session resumed: %s\n",
+			    tls_conn_session_resumed(ctx) ? "yes" : "no");
+
+		do {
+			r = tls_close(ctx);
+		} while (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT);
+		tls_free(ctx);
+	}
+
+#endif /* NOSSL */
+	fclose(fp);
+	chunked = 0;
+}
+
+static int
+http_request(int scheme, const char *req, struct http_headers **hdrs)
+{
+	struct http_headers	*headers;
+	const char		*e;
+	char			*buf = NULL, *p;
+	size_t			 n = 0;
+	ssize_t			 buflen;
+	uint			 code;
+#ifndef NOSSL
+	size_t			 len;
+	ssize_t			 nw;
+#endif /* NOSSL */
+
+	if (io_debug)
+		fprintf(stderr, "<<< %s", req);
+
+	switch (scheme) {
+#ifndef NOSSL
+	case S_HTTPS:
+		len = strlen(req);
+		while (len > 0) {
+			nw = tls_write(ctx, req, len);
+			if (nw == TLS_WANT_POLLIN || nw == TLS_WANT_POLLOUT)
+				continue;
+			if (nw < 0)
+				errx(1, "tls_write: %s", tls_error(ctx));
+			req += nw;
+			len -= nw;
+		}
+		break;
+#endif /* NOSSL */
+	case S_HTTP:
+		if (fprintf(fp, "%s", req) < 0)
+			errx(1, "%s: fprintf", __func__);
+		(void)fflush(fp);
+		break;
+	}
+
+	http_getline(scheme, &buf, &n);
+	if (io_debug)
+		fprintf(stderr, ">>> %s", buf);
+
+	if (sscanf(buf, "%*s %u %*s", &code) != 1)
+		errx(1, "%s: failed to extract status code", __func__);
+
+	if (code < 100 || code > 511)
+		errx(1, "%s: invalid status code %d", __func__, code);
+
+	headers = xcalloc(1, sizeof *headers);
+	headers->retry_after = -1;
+	for (;;) {
+		buflen = http_getline(scheme, &buf, &n);
+		buflen -= 1;
+		if (buflen > 0 && buf[buflen - 1] == '\r')
+			buflen -= 1;
+		buf[buflen] = '\0';
+
+		if (io_debug)
+			fprintf(stderr, ">>> %s\n", buf);
+
+		if (buflen == 0)
+			break; /* end of headers */
+
+		if ((p = header_lookup(buf, "Content-Length:")) != NULL) {
+			headers->content_length = strtonum(p, 0, INT64_MAX, &e);
+			if (e)
+				err(1, "%s: Content-Length is %s: %lld",
+				    __func__, e, headers->content_length);
+		}
+
+		if ((p = header_lookup(buf, "Location:")) != NULL)
+			headers->location = xstrdup(p);
+
+		if ((p = header_lookup(buf, "Transfer-Encoding:")) != NULL)
+			if (strcasestr(p, "chunked") != NULL)
+				headers->chunked = 1;
+
+		if ((p = header_lookup(buf, "Retry-After:")) != NULL) {
+			headers->retry_after = strtonum(p, 0, 0, &e);
+			if (e)
+				headers->retry_after = -1;
+		}
+	}
+
+	*hdrs = headers;
+	free(buf);
+	return code;
+}
+
+static void
+http_headers_free(struct http_headers *headers)
+{
+	if (headers == NULL)
+		return;
+
+	free(headers->location);
+	free(headers);
+}
+
+static char *
+header_lookup(const char *buf, const char *key)
+{
+	char	*p;
+
+	if (strncasecmp(buf, key, strlen(key)) == 0) {
+		if ((p = strchr(buf, ' ')) == NULL)
+			errx(1, "Failed to parse %s", key);
+		return ++p;
+	}
+
+	return NULL;
+}
+
+static ssize_t
+http_getline(int scheme, char **buf, size_t *n)
+{
+	ssize_t	buflen;
+
+	switch (scheme) {
+#ifndef NOSSL
+	case S_HTTPS:
+		if ((buflen = tls_getline(buf, n, ctx)) == -1)
+			errx(1, "%s: tls_getline", __func__);
+		break;
+#endif /* NOSSL */
+	case S_HTTP:
+		if ((buflen = getline(buf, n, fp)) == -1)
+			err(1, "%s: getline", __func__);
+		break;
+	default:
+		errx(1, "%s: invalid scheme", __func__);
+	}
+
+	return buflen;
+}
+
+static size_t
+http_read(int scheme, char *buf, size_t size)
+{
+	size_t	r;
+#ifndef NOSSL
+	ssize_t	rs;
+#endif /* NOSSL */
+
+	switch (scheme) {
+#ifndef NOSSL
+	case S_HTTPS:
+		do {
+			rs = tls_read(ctx, buf, size);
+		} while (rs == TLS_WANT_POLLIN || rs == TLS_WANT_POLLOUT);
+		if (rs == -1)
+			errx(1, "%s: tls_read: %s", __func__, tls_error(ctx));
+		r = rs;
+		break;
+#endif /* NOSSL */
+	case S_HTTP:
+		if ((r = fread(buf, 1, size, fp)) < size)
+			if (!feof(fp))
+				errx(1, "%s: fread", __func__);
+		break;
+	default:
+		errx(1, "%s: invalid scheme", __func__);
+	}
+
+	return r;
+}
+
+static const char *
+http_error(int code)
+{
+	struct http_status	error, *res;
+
+	/* Set up key */
+	error.code = code;
+
+	if ((res = bsearch(&error, http_status,
+	    sizeof(http_status) / sizeof(http_status[0]) - 1,
+	    sizeof(http_status[0]), http_status_cmp)) != NULL)
+		return (res->name);
+
+	return (NULL);
+}
+
+static int
+http_status_cmp(const void *a, const void *b)
+{
+	const struct http_status *ea = a;
+	const struct http_status *eb = b;
+
+	return (ea->code - eb->code);
+}
+
+#ifndef NOSSL
+void
+https_init(char *tls_options)
+{
+	char		*str;
+	int		 depth;
+	const char	*ca_file, *errstr;
+
+	if (tls_init() != 0)
+		errx(1, "tls_init failed");
+
+	if ((tls_config = tls_config_new()) == NULL)
+		errx(1, "tls_config_new 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, "legacy") != 0)
+		errx(1, "tls set ciphers failed: %s",
+		    tls_config_error(tls_config));
+
+	ca_file = tls_default_ca_cert_file();
+	while (tls_options && *tls_options) {
+		switch (getsubopt(&tls_options, tls_verify_opts, &str)) {
+		case HTTP_TLS_CAFILE:
+			if (str == NULL)
+				errx(1, "missing CA file");
+			ca_file = str;
+			break;
+		case HTTP_TLS_CAPATH:
+			if (str == NULL)
+				errx(1, "missing ca path");
+			if (tls_config_set_ca_path(tls_config, str) != 0)
+				errx(1, "tls ca path failed");
+			break;
+		case HTTP_TLS_CIPHERS:
+			if (str == NULL)
+				errx(1, "missing cipher list");
+			if (tls_config_set_ciphers(tls_config, str) != 0)
+				errx(1, "tls set ciphers failed");
+			break;
+		case HTTP_TLS_DONTVERIFY:
+			tls_config_insecure_noverifycert(tls_config);
+			tls_config_insecure_noverifyname(tls_config);
+			break;
+		case HTTP_TLS_VERIFYDEPTH:
+			if (str == NULL)
+				errx(1, "missing depth");
+			depth = strtonum(str, 0, INT_MAX, &errstr);
+			if (errstr)
+				errx(1, "Cert validation depth is %s", errstr);
+			tls_config_set_verify_depth(tls_config, depth);
+			break;
+		case HTTP_TLS_MUSTSTAPLE:
+			tls_config_ocsp_require_stapling(tls_config);
+			break;
+		case HTTP_TLS_NOVERIFYTIME:
+			tls_config_insecure_noverifytime(tls_config);
+			break;
+		case HTTP_TLS_SESSION:
+			if (str == NULL)
+				errx(1, "missing session file");
+			tls_session_fd = open(str, O_RDWR|O_CREAT, 0600);
+			if (tls_session_fd == -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 HTTP_TLS_DOVERIFY:
+			/* For compatibility, we do verify by default */
+			break;
+		default:
+			errx(1, "Unknown -S suboption `%s'",
+			    suboptarg ? suboptarg : "");
+		}
+	}
+
+	if (tls_config_set_ca_file(tls_config, ca_file) == -1)
+		errx(1, "tls_config_set_ca_file failed");
+}
+
+static ssize_t
+tls_getline(char **buf, size_t *buflen, struct tls *tls)
+{
+	char		*newb;
+	size_t		 newlen, off;
+	int		 ret;
+	unsigned char	 c;
+
+	if (buf == NULL || buflen == NULL)
+		return -1;
+
+	/* If buf is NULL, we have to assume a size of zero */
+	if (*buf == NULL)
+		*buflen = 0;
+
+	off = 0;
+	do {
+		do {
+			ret = tls_read(tls, &c, 1);
+		} while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT);
+		if (ret == -1)
+			return -1;
+
+		/* Ensure we can handle it */
+		if (off + 2 > SSIZE_MAX)
+			return -1;
+
+		newlen = off + 2; /* reserve space for NUL terminator */
+		if (newlen > *buflen) {
+			newlen = newlen < MINBUF ? MINBUF : *buflen * 2;
+			newb = recallocarray(*buf, *buflen, newlen, 1);
+			if (newb == NULL)
+				return -1;
+
+			*buf = newb;
+			*buflen = newlen;
+		}
+
+		*(*buf + off) = c;
+		off += 1;
+	} while (c != '\n');
+
+	*(*buf + off) = '\0';
+	return off;
+}
+
+static void
+tls_copy_file(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	char	*tmp_buf;
+	ssize_t	 r;
+
+	tmp_buf = xmalloc(TMPBUF_LEN);
+	for (;;) {
+		do {
+			r = tls_read(ctx, tmp_buf, TMPBUF_LEN);
+		} while (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT);
+
+		if (r == -1)
+			errx(1, "%s: tls_read: %s", __func__, tls_error(ctx));
+		else if (r == 0)
+			break;
+
+		*offset += r;
+		if (fwrite(tmp_buf, 1, r, dst_fp) != (size_t)r)
+			err(1, "%s: fwrite", __func__);
+	}
+	free(tmp_buf);
+}
+#endif /* NOSSL */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,526 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <sys/cdefs.h>
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <imsg.h>
+#include <libgen.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+#define	IMSG_OPEN	 1
+
+static int		 auto_fetch(int, char **);
+static void		 child(int, int, char **);
+static int		 parent(int, pid_t);
+static struct url	*proxy_parse(const char *);
+static int		 stdout_copy(const char *);
+static int		 append(const char *);
+static int		 save(const char *);
+static int		 slurp(struct url *, FILE *, off_t *, off_t);
+static char		*output_fname(struct url *, const char *);
+static void		 re_exec(int, int, char **);
+static __dead void	 usage(void);
+static int		 read_message(struct imsgbuf *, struct imsg *);
+static void		 send_message(struct imsgbuf *, int, uint32_t, void *,
+			     size_t, int);
+
+struct url		*ftp_proxy, *http_proxy;
+const char		*useragent = "OpenBSD ftp";
+char			*oarg;
+int			 activemode, family = AF_UNSPEC, io_debug;
+int			 progressmeter, verbose = 1;
+volatile sig_atomic_t	 interrupted = 0;
+
+static struct imsgbuf	 child_ibuf;
+static const char	*title;
+static char		*tls_options;
+static int		 connect_timeout, resume;
+
+int
+main(int argc, char **argv)
+{
+	const char	 *e;
+	char		**save_argv, *term;
+	int		  ch, csock, dumb_terminal, rexec, save_argc;
+
+	if (isatty(fileno(stdin)) != 1)
+		verbose = 0;
+
+	io_debug = getenv("IO_DEBUG") != NULL;
+	term = getenv("TERM");
+	dumb_terminal = (term == NULL || *term == '\0' ||
+	    !strcmp(term, "dumb") || !strcmp(term, "emacs") ||
+	    !strcmp(term, "su"));
+	if (isatty(STDOUT_FILENO) && isatty(STDERR_FILENO) && !dumb_terminal)
+		progressmeter = 1;
+
+	csock = rexec = 0;
+	save_argc = argc;
+	save_argv = argv;
+	while ((ch = getopt(argc, argv,
+	    "46AaCc:dD:Eegik:MmN:no:pP:r:S:s:tU:vVw:xz:")) != -1) {
+		switch (ch) {
+		case '4':
+			family = AF_INET;
+			break;
+		case '6':
+			family = AF_INET6;
+			break;
+		case 'A':
+			activemode = 1;
+			break;
+		case 'C':
+			resume = 1;
+			break;
+		case 'D':
+			title = optarg;
+			break;
+		case 'o':
+			oarg = optarg;
+			if (!strlen(oarg))
+				oarg = NULL;
+			break;
+		case 'M':
+			progressmeter = 0;
+			break;
+		case 'm':
+			progressmeter = 1;
+			break;
+		case 'N':
+			setprogname(optarg);
+			break;
+		case 'S':
+			tls_options = optarg;
+			break;
+		case 'U':
+			useragent = optarg;
+			break;
+		case 'V':
+			verbose = 0;
+			break;
+		case 'v':
+			verbose = 1;
+			break;
+		case 'w':
+			connect_timeout = strtonum(optarg, 0, 200, &e);
+			if (e)
+				errx(1, "-w: %s", e);
+			break;
+		/* options for internal use only */
+		case 'x':
+			rexec = 1;
+			break;
+		case 'z':
+			csock = strtonum(optarg, 3, getdtablesize() - 1, &e);
+			if (e)
+				errx(1, "-z: %s", e);
+			break;
+		/* Ignoring all remaining options */
+		case 'a':
+		case 'c':
+		case 'd':
+		case 'E':
+		case 'e':
+		case 'g':
+		case 'i':
+		case 'k':
+		case 'n':
+		case 'P':
+		case 'p':
+		case 'r':
+		case 's':
+		case 't':
+			warnx("Ignoring getopt: %c", ch);
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (rexec)
+		child(csock, argc, argv);
+
+#ifndef SMALL
+	struct url	*url;
+
+	switch (argc) {
+	case 0:
+		cmd(NULL, NULL, NULL);
+		return 0;
+	case 1:
+	case 2:
+		switch (url_scheme_lookup(argv[0])) {
+		case -1:
+			cmd(argv[0], argv[1], NULL);
+			return 0;
+		case S_FTP:
+			url = xurl_parse(argv[0]);
+			if (url->path &&
+			    url->path[strlen(url->path) - 1] != '/')
+				break; /* auto fetch */
+
+			cmd(url->host, url->port, url->path);
+			return 0;
+		}
+		break;
+	}
+#else
+	if (argc == 0)
+		usage();
+#endif /* SMALL */
+
+	return auto_fetch(save_argc, save_argv);
+}
+
+static int
+auto_fetch(int sargc, char **sargv)
+{
+	pid_t	pid;
+	int	sp[2];
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) != 0)
+		err(1, "socketpair");
+
+	switch (pid = fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		close(sp[0]);
+		re_exec(sp[1], sargc, sargv);
+	}
+
+	close(sp[1]);
+	return parent(sp[0], pid);
+}
+
+static void
+re_exec(int sock, int argc, char **argv)
+{
+	char	**nargv, *sock_str;
+	int	  i, j, nargc;
+
+	nargc = argc + 4;
+	nargv = xcalloc(nargc, sizeof(*nargv));
+	xasprintf(&sock_str, "%d", sock);
+	i = 0;
+	nargv[i++] = argv[0];
+	nargv[i++] = "-z";
+	nargv[i++] = sock_str;
+	nargv[i++] = "-x";
+	for (j = 1; j < argc; j++)
+		nargv[i++] = argv[j];
+
+	execvp(nargv[0], nargv);
+	err(1, "execvp");
+}
+
+static int
+parent(int sock, pid_t child_pid)
+{
+	struct imsgbuf	ibuf;
+	struct imsg	imsg;
+	struct stat	sb;
+	off_t		offset;
+	int		fd, save_errno, sig, status;
+
+	setproctitle("%s", "parent");
+	if (pledge("stdio cpath rpath wpath sendfd", NULL) == -1)
+		err(1, "pledge");
+
+	imsg_init(&ibuf, sock);
+	for (;;) {
+		if (read_message(&ibuf, &imsg) == 0)
+			break;
+
+		if (imsg.hdr.type != IMSG_OPEN)
+			errx(1, "%s: IMSG_OPEN expected", __func__);
+
+		offset = 0;
+		fd = open(imsg.data, imsg.hdr.peerid, 0666);
+		save_errno = errno;
+		if (fd != -1 && fstat(fd, &sb) == 0) {
+			if (sb.st_mode & S_IFDIR) {
+				close(fd);
+				fd = -1;
+				save_errno = EISDIR;
+			} else
+				offset = sb.st_size;
+		}
+
+		send_message(&ibuf, IMSG_OPEN, save_errno,
+		    &offset, sizeof offset, fd);
+		imsg_free(&imsg);
+	}
+
+	close(sock);
+	if (waitpid(child_pid, &status, 0) == -1 && errno != ECHILD)
+		err(1, "wait");
+
+	sig = WTERMSIG(status);
+	if (WIFSIGNALED(status) && sig != SIGPIPE)
+		errx(1, "child terminated: signal %d", sig);
+
+	return WEXITSTATUS(status);
+}
+
+static void
+child(int sock, int argc, char **argv)
+{
+	int	i, to_stdout = 0, r = 0;
+
+	setproctitle("%s", "child");
+
+#ifndef NOSSL
+	/*
+	 * TLS can't be init-ed on first use as filesystem(ca file) isn't
+	 * available after pledge(2).
+	 */
+	https_init(tls_options);
+#endif /* NOSSL */
+
+	if (pledge("stdio inet dns recvfd tty unveil", NULL) == -1)
+		err(1, "pledge");
+	if (!progressmeter &&
+	    pledge("stdio inet dns recvfd unveil", NULL) == -1)
+		err(1, "pledge");
+
+	imsg_init(&child_ibuf, sock);
+	ftp_proxy = proxy_parse("ftp_proxy");
+	http_proxy = proxy_parse("http_proxy");
+
+	if (oarg) {
+		if (strcmp(oarg, "-") == 0) {
+			to_stdout = 1;
+			if (resume)
+				errx(1, "can't append to stdout");
+		} else if (unveil(oarg, "w") == -1)
+			err(1, "unveil");
+
+		if (unveil(NULL, NULL) == -1)
+			err(1, "unveil");
+	}
+
+	for (i = 0; i < argc; i++) {
+		if (to_stdout)
+			r = stdout_copy(argv[i]);
+		else if (resume)
+			r = append(argv[i]);
+		else
+			r = save(argv[i]);
+	}
+
+	exit(r);
+}
+
+static int
+stdout_copy(const char *arg)
+{
+	struct url	*url;
+	off_t		 offset = 0, sz = 0;
+
+	url = xurl_parse(arg);
+	url_connect(url, connect_timeout);
+	url = url_request(url, &offset, &sz);
+	return slurp(url, stdout, &offset, sz);
+}
+
+static int
+append(const char *arg)
+{
+	struct url	*url;
+	FILE		*fp;
+	char		*fname;
+	off_t		 offset = 0, sz = 0;
+	int		 fd;
+
+	url = xurl_parse(arg);
+	url_connect(url, connect_timeout);
+	fname = output_fname(url, arg);
+	fd = fd_request(fname, O_WRONLY|O_APPEND, &offset);
+	url = url_request(url, &offset, &sz);
+	/* If HTTP server doesn't support range requests, truncate. */
+	if (fd != -1 && offset == 0)
+		if (ftruncate(fd, 0) != 0)
+			err(1, "ftruncate");
+
+	if (fd == -1 &&
+	    (fd = fd_request(fname, O_CREAT|O_TRUNC|O_WRONLY, NULL)) == -1)
+		err(1, "Can't open file %s", fname);
+
+	if ((fp = fdopen(fd, "w")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	return slurp(url, fp, &offset, sz);
+}
+
+static int
+save(const char *arg)
+{
+	struct url	*url;
+	FILE		*fp;
+	char		*fname;
+	off_t		 offset = 0, sz = 0;
+	int		 fd, r;
+
+	url = xurl_parse(arg);
+	url_connect(url, connect_timeout);
+	url = url_request(url, &offset, &sz);
+	fname = output_fname(url, arg);
+	if ((fd = fd_request(fname, O_CREAT|O_TRUNC|O_WRONLY, NULL)) == -1)
+		err(1, "Can't open file %s", fname);
+
+	if ((fp = fdopen(fd, "w")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	return slurp(url, fp, &offset, sz);
+}
+
+static int
+slurp(struct url *url, FILE *fp, off_t *offset, off_t sz)
+{
+	start_progress_meter(basename(url->path), title, sz, offset);
+	url_save(url, fp, offset);
+	stop_progress_meter();
+	url_close(url);
+	url_free(url);
+	if (fp != stdout)
+		fclose(fp);
+
+	if (sz != 0 && *offset != sz) {
+		log_info("Read short file\n");
+		return 1;
+	}
+
+	return 0;
+}
+
+static char *
+output_fname(struct url *url, const char *arg)
+{
+	char	*fname;
+
+	fname = oarg ? oarg : basename(url->path);
+	if (strcmp(fname, "/") == 0)
+		errx(1, "No filename after host (use -o): %s", arg);
+
+	if (strcmp(fname, ".") == 0)
+		errx(1, "No '/' after host (use -o): %s", arg);
+
+	return fname;
+}
+
+int
+fd_request(char *path, int flags, off_t *offset)
+{
+	struct imsg	 imsg;
+	off_t		*poffset;
+	int		 fd, save_errno;
+
+	send_message(&child_ibuf, IMSG_OPEN, flags, path, strlen(path) + 1, -1);
+	if (read_message(&child_ibuf, &imsg) == 0)
+		return -1;
+
+	if (imsg.hdr.type != IMSG_OPEN)
+		errx(1, "%s: IMSG_OPEN expected", __func__);
+
+	fd = imsg.fd;
+	if (offset) {
+		poffset = imsg.data;
+		*offset = *poffset;
+	}
+
+	save_errno = imsg.hdr.peerid;
+	imsg_free(&imsg);
+	errno = save_errno;
+	return fd;
+}
+
+void
+send_message(struct imsgbuf *ibuf, int type, uint32_t peerid,
+    void *msg, size_t msglen, int fd)
+{
+	if (imsg_compose(ibuf, type, peerid, 0, fd, msg, msglen) != 1)
+		err(1, "imsg_compose");
+
+	if (imsg_flush(ibuf) != 0)
+		err(1, "imsg_flush");
+}
+
+int
+read_message(struct imsgbuf *ibuf, struct imsg *imsg)
+{
+	int	n;
+
+	if ((n = imsg_read(ibuf)) == -1)
+		err(1, "%s: imsg_read", __func__);
+	if (n == 0)
+		return 0;
+
+	if ((n = imsg_get(ibuf, imsg)) == -1)
+		err(1, "%s: imsg_get", __func__);
+	if (n == 0)
+		return 0;
+
+	return n;
+}
+
+static struct url *
+proxy_parse(const char *name)
+{
+	struct url	*proxy;
+	char		*str;
+
+	if ((str = getenv(name)) == NULL)
+		return NULL;
+
+	if (strlen(str) == 0)
+		return NULL;
+
+	proxy = xurl_parse(str);
+	if (proxy->scheme != S_HTTP)
+		errx(1, "Malformed proxy URL: %s", str);
+
+	return proxy;
+}
+
+static __dead void
+usage(void)
+{
+	fprintf(stderr,
+	    "usage:\t%s [-46AVv] [-D title] [host [port]]\n"
+	    "\t%s [-46ACVMmVv] [-N name] [-D title] [-o output]\n"
+	    "\t\t [-S tls_options] [-U useragent] [-w seconds] url ...\n",
+	    getprogname(), getprogname());
+
+	exit(1);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/progressmeter.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@openbsd.org>
+ * Copyright (c) 2003 Nils Nordman.  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.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+
+#include <err.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ftp.h"
+
+#define DEFAULT_WINSIZE 80
+#define MAX_WINSIZE 512
+#define UPDATE_INTERVAL 1	/* update the progress meter every second */
+#define STALL_TIME 5		/* we're stalled after this many seconds */
+
+time_t	monotime(void);
+
+/* formats and inserts the specified size into the given buffer */
+static void format_size(char *, int, off_t);
+static void format_rate(char *, int, off_t);
+
+/* window resizing */
+static void sig_winch(int);
+static void setscreensize(void);
+
+/* updates the progressmeter to reflect the current state of the transfer */
+void refresh_progress_meter(void);
+
+/* signal handler for updating the progress meter */
+static void update_progress_meter(int);
+
+static const char *title;	/* short title for the start of progress bar */
+static time_t start;		/* start progress */
+static time_t last_update;	/* last progress update */
+static off_t start_pos;		/* initial position of transfer */
+static off_t end_pos;		/* ending position of transfer */
+static off_t cur_pos;		/* transfer position as of last refresh */
+static off_t offset;		/* initial offset from start_pos */
+static volatile off_t *counter;	/* progress counter */
+static long stalled;		/* how long we have been stalled */
+static int bytes_per_second;	/* current speed in bytes per second */
+static int win_size;		/* terminal window size */
+static volatile sig_atomic_t win_resized; /* for window resizing */
+static const char *filename;	/* To be displayed in non-verbose mode */
+/* units for format_size */
+static const char unit[] = " KMGT";
+
+time_t
+monotime(void)
+{
+	struct timespec ts;
+
+	if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
+		err(1, "monotime");
+
+	return ts.tv_sec;
+}
+
+static void
+format_rate(char *buf, int size, off_t bytes)
+{
+	int i;
+
+	bytes *= 100;
+	for (i = 0; bytes >= 100*1000 && unit[i] != 'T'; i++)
+		bytes = (bytes + 512) / 1024;
+	if (i == 0) {
+		i++;
+		bytes = (bytes + 512) / 1024;
+	}
+	snprintf(buf, size, "%lld.%02lld %c%s",
+	    (long long) (bytes + 5) / 100,
+	    (long long) (bytes + 5) / 10 % 10,
+	    unit[i],
+	    "B");
+}
+
+static void
+format_size(char *buf, int size, off_t bytes)
+{
+	int i;
+
+	for (i = 0; bytes >= 10000 && unit[i] != 'T'; i++)
+		bytes = (bytes + 512) / 1024;
+	snprintf(buf, size, "%4lld%c%s",
+	    (long long) bytes,
+	    unit[i],
+	    i ? "B" : " ");
+}
+
+void
+refresh_progress_meter(void)
+{
+	char buf[MAX_WINSIZE + 1];
+	const char *dot = "";
+	time_t now;
+	off_t transferred, bytes_left;
+	double elapsed;
+	int len, cur_speed, hours, minutes, seconds, barlength, i;
+	int percent, overhead = 30;
+
+	transferred = *counter - (cur_pos ? cur_pos : start_pos);
+	cur_pos = *counter;
+	now = monotime();
+	bytes_left = end_pos - cur_pos;
+
+	if (bytes_left > 0)
+		elapsed = now - last_update;
+	else {
+		elapsed = now - start;
+		/* Calculate true total speed when done */
+		transferred = end_pos - start_pos;
+		bytes_per_second = 0;
+	}
+
+	/* calculate speed */
+	if (elapsed != 0)
+		cur_speed = (transferred / elapsed);
+	else
+		cur_speed = transferred;
+
+#define AGE_FACTOR 0.9
+	if (bytes_per_second != 0) {
+		bytes_per_second = (bytes_per_second * AGE_FACTOR) +
+		    (cur_speed * (1.0 - AGE_FACTOR));
+	} else
+		bytes_per_second = cur_speed;
+
+	buf[0] = '\0';
+	/* title */
+	if (!verbose && title != NULL) {
+		len = strlen(title);
+		if (len < 7)
+			len = 7;
+		else if (len > 12) {
+			len = 12;
+			dot = "...";
+			overhead += 3;
+		}
+		snprintf(buf, sizeof buf, "\r%-*.*s%s ", len, len, title, dot);
+		overhead += len + 1;
+	} else
+		snprintf(buf, sizeof buf, "\r");
+
+	if (end_pos == 0 || cur_pos == end_pos)
+		percent = 100;
+	else
+		percent = ((float)cur_pos / end_pos) * 100;
+
+	/* filename and percent */
+	if (!verbose && filename != NULL) {
+		len = strlen(filename);
+		if (len < 12)
+			len = 12;
+		else if (len > 25) {
+			len = 22;
+			dot = "...";
+			overhead += 3;
+		}
+		snprintf(buf + strlen(buf), sizeof buf - strlen(buf),
+		    "%-*.*s%s %3d%% ", len, len, filename, dot, percent);
+		overhead += len + 1;
+	} else
+		snprintf(buf, sizeof buf, "\r%3d%% ", percent);
+
+	/* bar */
+	barlength = win_size - overhead;
+	if (barlength > 0) {
+		i = barlength * percent / 100;
+		snprintf(buf + strlen(buf), sizeof buf - strlen(buf),
+		    "|%.*s%*s| ", i,
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************"
+		    "*******************************************************",
+		    barlength - i, "");
+
+	}
+
+	/* amount transferred */
+	format_size(buf + strlen(buf), win_size - strlen(buf), cur_pos);
+	strlcat(buf, " ", win_size);
+
+	/* ETA */
+	if (!transferred)
+		stalled += elapsed;
+	else
+		stalled = 0;
+
+	if (stalled >= STALL_TIME)
+		strlcat(buf, "- stalled -", win_size);
+	else if (bytes_per_second == 0 && bytes_left)
+		strlcat(buf, "  --:-- ETA", win_size);
+	else {
+		if (bytes_left > 0)
+			seconds = bytes_left / bytes_per_second;
+		else
+			seconds = elapsed;
+
+		hours = seconds / 3600;
+		seconds -= hours * 3600;
+		minutes = seconds / 60;
+		seconds -= minutes * 60;
+
+		if (hours != 0)
+			snprintf(buf + strlen(buf), win_size - strlen(buf),
+			    "%d:%02d:%02d", hours, minutes, seconds);
+		else
+			snprintf(buf + strlen(buf), win_size - strlen(buf),
+			    "  %02d:%02d", minutes, seconds);
+
+		if (bytes_left > 0)
+			strlcat(buf, " ETA", win_size);
+		else
+			strlcat(buf, "    ", win_size);
+	}
+
+	if (progressmeter)
+		write(STDERR_FILENO, buf, strlen(buf));
+
+	last_update = now;
+}
+
+static void
+update_progress_meter(int ignore)
+{
+	int save_errno;
+
+	save_errno = errno;
+
+	if (win_resized) {
+		setscreensize();
+		win_resized = 0;
+	}
+
+	refresh_progress_meter();
+
+	signal(SIGALRM, update_progress_meter);
+	alarm(UPDATE_INTERVAL);
+	errno = save_errno;
+}
+
+void
+start_progress_meter(const char *fn, const char *t, off_t filesize, off_t *ctr)
+{
+	start = last_update = monotime();
+	start_pos = *ctr;
+	offset = *ctr;
+	cur_pos = 0;
+	end_pos = 0;
+	counter = ctr;
+	stalled = 0;
+	bytes_per_second = 0;
+	filename = fn;
+	title = t;
+
+	/*
+	 * Suppress progressmeter if filesize isn't known when
+	 * Content-Length header has bogus values.
+	 */
+	if (filesize <= 0)
+		return;
+
+	end_pos = filesize;
+	if (progressmeter)
+		setscreensize();
+
+	refresh_progress_meter();
+
+	signal(SIGALRM, update_progress_meter);
+	signal(SIGWINCH, sig_winch);
+	alarm(UPDATE_INTERVAL);
+}
+
+void
+stop_progress_meter(void)
+{
+	char	rate_str[32];
+	double	elapsed;
+
+	alarm(0);
+
+	/* Ensure we complete the progress */
+	if (end_pos && cur_pos != end_pos)
+		refresh_progress_meter();
+
+	if (progressmeter && end_pos)
+		write(STDERR_FILENO, "\n", 1);
+
+	if (!verbose)
+		return;
+
+	elapsed = monotime() - start;
+	if (end_pos == 0) {
+		if (elapsed != 0)
+			bytes_per_second = *counter / elapsed;
+		else
+			bytes_per_second = *counter;
+	}
+
+	format_rate(rate_str, sizeof rate_str, bytes_per_second);
+	log_info("%lld byte%s received in %.2f seconds (%s/s)\n",
+	    (end_pos) ? cur_pos - offset : *counter,
+	    *counter != 1 ? "s" : "",  elapsed, rate_str);
+}
+
+static void
+sig_winch(int sig)
+{
+	win_resized = 1;
+}
+
+static void
+setscreensize(void)
+{
+	struct winsize winsize;
+
+	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) != -1 &&
+	    winsize.ws_col != 0) {
+		if (winsize.ws_col > MAX_WINSIZE)
+			win_size = MAX_WINSIZE;
+		else
+			win_size = winsize.ws_col;
+	} else
+		win_size = DEFAULT_WINSIZE;
+	win_size += 1;					/* trailing \0 */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/regress/Makefile	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,3 @@
+SUBDIR+= unit-tests
+
+.include <bsd.subdir.mk>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/regress/unit-tests/Makefile	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,2 @@
+SUBDIR+= url_parse
+. include <bsd.regress.mk>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/regress/unit-tests/url_parse/Makefile	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,17 @@
+FTPREL=	../../../
+.PATH: ${.CURDIR}/${FTPREL}
+
+PROG=test_url_parse
+SRCS=test_url_parse.c
+SRCS+=file.c ftp.c http.c progressmeter.c url.c util.c xmalloc.c
+
+CFLAGS+=-I ${.CURDIR}/${FTPREL}
+LDADD+=	-lutil -ltls -lssl -lcrypto
+DPADD+=	${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO}
+
+REGRESS_TARGETS=run-regress-${PROG}
+
+run-regress-${PROG}: ${PROG}
+	env ${TEST_ENV} ./${PROG}
+
+.include <bsd.regress.mk>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/regress/unit-tests/url_parse/test_url_parse.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2020 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <err.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "ftp.h"
+
+struct url *ftp_proxy, *http_proxy;
+volatile sig_atomic_t interrupted;
+const char *useragent;
+char *oarg;
+int activemode, family, io_debug, verbose, progressmeter;
+
+int
+fd_request(char *path, int flags, off_t *offset)
+{
+	/* dummy */
+	return 0;
+}
+
+static struct {
+	const char	*str;
+	struct url	 url;
+	int		 noparse;
+} testcases[] = {
+	{ "http://google.com/index.html", {
+	    S_HTTP, 0, "google.com", "80", "/index.html" } },
+	{ "https://google.com:", {
+	    S_HTTPS, 0, "google.com", "443" } },
+	{ "file:.", {
+	    S_FILE, 0, NULL, NULL, "." } },
+	{ "http://[::1]:/index.html", {
+	    S_HTTP, 1, "::1", "80", "/index.html" } },
+	{ "http://[::1]:1234/", {
+	    S_HTTP, 1, "::1", "1234", "/" } },
+	{ "foo.bar", {}, 1 },
+	{ "http://[::1:1234", {}, 1 },
+	{ "http://[1::2::3]:1234", {
+	    S_HTTP, 0, "1::2::3", "1234" } },
+	{ "http://foo.com:bar", {
+	    S_HTTP, 0, "foo.com", "bar" } },
+	{ "http:/foo.com", {}, 1 },
+	{ "http://foo:bar@baz.com", {
+	    S_HTTP, 0, "baz.com", "80" } },
+	{ "http://[::1]abcd/", {}, 1 },
+	{ "    http://localhost:8080", {
+	    S_HTTP, 0, "localhost", "8080" } },
+	{ "ftps://localhost:21", {}, 1 },
+	{ "http://marc.info/?l=openbsd-tech&m=151790635206581&q=raw", {
+	    S_HTTP, 0, "marc.info", "80", "/?l=openbsd-tech&m=151790635206581&q=raw" } },
+	{ "file://disklabel.template", {
+	    S_FILE, 0, NULL, NULL, "/disklabel.template" } },
+	{ "file:/disklabel.template", {
+	    S_FILE, 0, NULL, NULL, "/disklabel.template" } },
+	{ "file:///disklabel.template", {
+	    S_FILE, 0, NULL, NULL, "/disklabel.template" } },
+};
+
+static int
+ptr_null_cmp(void *a, void *b)
+{
+	if ((a && b == NULL) || (a == NULL && b))
+		return 1;
+
+	return 0;
+}
+
+static int
+url_cmp(struct url *a, struct url *b)
+{
+	if (ptr_null_cmp(a, b) ||
+	    ptr_null_cmp(a->host, b->host) ||
+	    ptr_null_cmp(a->port, b->port) ||
+	    ptr_null_cmp(a->path, b->path))
+		return 1;
+
+	if (a->scheme != b->scheme ||
+	    (a->host && strcmp(a->host, b->host)) ||
+	    (a->port && strcmp(a->port, b->port)) ||
+	    (a->path && strcmp(a->path, b->path)))
+		return 1;
+
+	return 0;
+}
+
+int
+main(void)
+{
+	struct url	*url, *eurl;
+	size_t		 i;
+
+	if (freopen("/dev/null", "w", stderr) == NULL)
+		err(1, "freopen");
+
+	for (i = 0; i < nitems(testcases); i++) {
+		url = url_parse(testcases[i].str);
+		if (testcases[i].noparse) {
+			if (url != NULL)
+				goto bad;
+
+			continue;
+		}
+
+		if (url_cmp(url, &testcases[i].url) != 0)
+			goto bad;
+	}
+
+	return 0;
+
+ bad:
+	printf("%s\n", testcases[i].str);
+	eurl = &testcases[i].url;
+	printf("Expected: scheme = %s, host = %s, port = %s, path = %s\n",
+	    url_scheme_str(eurl->scheme), eurl->host, eurl->port, eurl->path);
+	printf("Got: scheme = %s, host = %s, port = %s, path = %s\n",
+	    url_scheme_str(url->scheme), url->host, url->port, url->path);
+	return 1;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/url.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,424 @@
+/*
+ * Copyright (c) 2017 Sunil Nimmagadda <sunil@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.
+ */
+
+/*-
+ * 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.
+ */
+#include <sys/types.h>
+
+#include <netinet/in.h>
+#include <resolv.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+#define BASICAUTH_LEN	1024
+
+static void	authority_parse(const char *, char **, char **, char **);
+static int	ipv6_parse(const char *, char **, char **);
+static int	unsafe_char(const char *);
+
+#ifndef NOSSL
+const char	*scheme_str[] = { "http:", "ftp:", "file:", "https:" };
+const char	*port_str[] = { "80", "21", NULL, "443" };
+#else
+const char	*scheme_str[] = { "http:", "ftp:", "file:" };
+const char	*port_str[] = { "80", "21", NULL };
+#endif /* NOSSL */
+
+int
+url_scheme_lookup(const char *str)
+{
+	size_t	i;
+
+#ifdef NOSSL
+	if (strncasecmp(str, "https:", 6) == 0)
+		errx(1, "No HTTPS support.");
+#endif /* NOSSL */
+
+	for (i = 0; i < nitems(scheme_str); i++)
+		if (strncasecmp(str, scheme_str[i], strlen(scheme_str[i])) == 0)
+			return i;
+
+	return -1;
+}
+
+static int
+ipv6_parse(const char *str, char **host, char **port)
+{
+	char	*p;
+
+	if ((p = strchr(str, ']')) == NULL) {
+		warnx("%s: invalid IPv6 address: %s", __func__, str);
+		return 1;
+	}
+
+	*p++ = '\0';
+	if (strlen(str + 1) > 0)
+		*host = xstrdup(str + 1);
+
+	if (*p == '\0')
+		return 0;
+
+	if (*p++ != ':') {
+		warnx("%s: invalid port: %s", __func__, p);
+		free(*host);
+		return 1;
+	}
+
+	if (strlen(p) > 0)
+		*port = xstrdup(p);
+
+	return 0;
+}
+
+static void
+authority_parse(const char *str, char **host, char **port, char **basic_auth)
+{
+	char	*p;
+
+	if ((p = strchr(str, '@')) != NULL) {
+		*basic_auth = xcalloc(1, BASICAUTH_LEN);
+		if (b64_ntop((unsigned char *)str, p - str,
+		    *basic_auth, BASICAUTH_LEN) == -1)
+			errx(1, "base64 encode failed");
+
+		str = ++p;
+	}
+
+	if ((p = strchr(str, ':')) != NULL) {
+		*p++ = '\0';
+		if (strlen(p) > 0)
+			*port = xstrdup(p);
+	}
+
+	if (strlen(str) > 0)
+		*host = xstrdup(str);
+}
+
+struct url *
+xurl_parse(const char *str)
+{
+	struct url	*url;
+
+	if ((url = url_parse(str)) == NULL)
+		exit(1);
+
+	return url;
+}
+
+struct url *
+url_parse(const char *str)
+{
+	struct url	*url;
+	const char	*p, *q;
+	char		*basic_auth, *host, *port, *path, *s;
+	size_t		 len;
+	int		 ip_literal, scheme;
+
+	p = str;
+	ip_literal = 0;
+	host = port = path = basic_auth = NULL;
+	while (isblank((unsigned char)*p))
+		p++;
+
+	if ((q = strchr(p, ':')) == NULL) {
+		warnx("%s: scheme missing: %s", __func__, str);
+		return NULL;
+	}
+
+	if ((scheme = url_scheme_lookup(p)) == -1) {
+		warnx("%s: invalid scheme: %s", __func__, p);
+		return NULL;
+	}
+
+	p = ++q;
+	if (strncmp(p, "//", 2) != 0) {
+		if (scheme == S_FILE)
+			goto done;
+		else {
+			warnx("%s: invalid url: %s", __func__, str);
+			return NULL;
+		}
+	}
+
+	p += 2;
+
+	/*
+	 * quirk to parse file:// which isn't valid but required for
+	 * backwards compatibility.
+	 */
+	if (scheme == S_FILE) {
+		q = (*p == '/') ? p : p - 1;
+		goto done;
+	}
+
+	len = strlen(p);
+	/* Authority terminated by a '/' if present */
+	if ((q = strchr(p, '/')) != NULL)
+		len = q - p;
+
+	s = xstrndup(p, len);
+	if (*p == '[') {
+		if (ipv6_parse(s, &host, &port) != 0) {
+			free(s);
+			return NULL;
+		}
+		ip_literal = 1;
+	} else
+		authority_parse(s, &host, &port, &basic_auth);
+
+	free(s);
+	if (port == NULL && scheme != S_FILE)
+		port = xstrdup(port_str[scheme]);
+
+ done:
+	if (q != NULL)
+		path = xstrdup(q);
+
+	if (io_debug) {
+		fprintf(stderr,
+		    "scheme: %s\nhost: %s\nport: %s\npath: %s\n",
+		    scheme_str[scheme], host, port, path);
+	}
+
+	url = xcalloc(1, sizeof *url);
+	url->scheme = scheme;
+	url->host = host;
+	url->port = port;
+	url->path = path;
+	url->basic_auth = basic_auth;
+	url->ip_literal = ip_literal;
+	return url;
+}
+
+void
+url_free(struct url *url)
+{
+	if (url == NULL)
+		return;
+
+	free(url->host);
+	free(url->port);
+	free(url->path);
+	freezero(url->basic_auth, BASICAUTH_LEN);
+	free(url);
+}
+
+void
+url_connect(struct url *url, int timeout)
+{
+	switch (url->scheme) {
+	case S_HTTP:
+	case S_HTTPS:
+		http_connect(url, timeout);
+		break;
+	case S_FTP:
+		if (ftp_proxy)
+			http_connect(url, timeout);
+		else
+			ftp_connect(url, timeout);
+		break;
+	}
+}
+
+struct url *
+url_request(struct url *url, off_t *offset, off_t *sz)
+{
+	switch (url->scheme) {
+	case S_HTTP:
+	case S_HTTPS:
+		return http_get(url, offset, sz);
+	case S_FTP:
+		if (ftp_proxy)
+			return http_get(url, offset, sz);
+
+		return ftp_get(url, offset, sz);
+	case S_FILE:
+		return file_get(url, offset, sz);
+	}
+
+	return NULL;
+}
+
+void
+url_save(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	switch (url->scheme) {
+	case S_HTTP:
+	case S_HTTPS:
+		http_save(url, dst_fp, offset);
+		break;
+	case S_FTP:
+		if (ftp_proxy)
+			http_save(url, dst_fp, offset);
+		else
+			ftp_save(url, dst_fp, offset);
+		break;
+	case S_FILE:
+		file_save(url, dst_fp, offset);
+		break;
+	}
+}
+
+void
+url_close(struct url *url)
+{
+	switch (url->scheme) {
+	case S_HTTP:
+	case S_HTTPS:
+		http_close(url);
+		break;
+	case S_FTP:
+		if (ftp_proxy)
+			http_close(url);
+		else
+			ftp_close(url);
+		break;
+	}
+}
+
+char *
+url_str(struct url *url)
+{
+	char	*host, *str;
+	int	 custom_port;
+
+	custom_port = strcmp(url->port, port_str[url->scheme]) ? 1 : 0;
+	if (url->ip_literal)
+		xasprintf(&host, "[%s]", url->host);
+	else
+		host = xstrdup(url->host);
+
+	xasprintf(&str, "%s//%s%s%s%s",
+	    scheme_str[url->scheme],
+	    host,
+	    custom_port ? ":" : "",
+	    custom_port ? url->port : "",
+	    url->path ? url->path : "/");
+
+	free(host);
+	return str;
+}
+
+const char *
+url_scheme_str(int scheme)
+{
+	return scheme_str[scheme];
+}
+
+const char *
+url_port_str(int scheme)
+{
+	return port_str[scheme];
+}
+
+/*
+ * Encode given URL, per RFC1738.
+ * Allocate and return string to the caller.
+ */
+char *
+url_encode(const char *path)
+{
+	size_t i, length, new_length;
+	char *epath, *epathp;
+
+	length = new_length = strlen(path);
+
+	/*
+	 * First pass:
+	 * Count unsafe characters, and determine length of the
+	 * final URL.
+	 */
+	for (i = 0; i < length; i++)
+		if (unsafe_char(path + i))
+			new_length += 2;
+
+	epath = epathp = xmalloc(new_length + 1);	/* One more for '\0'. */
+
+	/*
+	 * Second pass:
+	 * Encode, and copy final URL.
+	 */
+	for (i = 0; i < length; i++)
+		if (unsafe_char(path + i)) {
+			snprintf(epathp, 4, "%%" "%02x",
+			    (unsigned char)path[i]);
+			epathp += 3;
+		} else
+			*(epathp++) = path[i];
+
+	*epathp = '\0';
+	return epath;
+}
+
+/*
+ * Determine whether the character needs encoding, per RFC1738:
+ * 	- No corresponding graphic US-ASCII.
+ * 	- Unsafe characters.
+ */
+static int
+unsafe_char(const char *c0)
+{
+	const char *unsafe_chars = " <>\"#{}|\\^~[]`";
+	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) ||
+
+	    /*
+	     * Unsafe characters.
+	     * '%' is also unsafe, if is not followed by two
+	     * hexadecimal digits.
+	     */
+	    strchr(unsafe_chars, *c) != NULL ||
+	    (*c == '%' && (!isxdigit(*++c) || !isxdigit(*++c))));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@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.
+ */
+
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <netdb.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+static void	tooslow(int);
+
+/*
+ * Wait for an asynchronous connect(2) attempt to finish.
+ */
+int
+connect_wait(int s)
+{
+	struct pollfd pfd[1];
+	int error = 0;
+	socklen_t len = sizeof(error);
+
+	pfd[0].fd = s;
+	pfd[0].events = POLLOUT;
+
+	if (poll(pfd, 1, -1) == -1)
+		return -1;
+	if (getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
+		return -1;
+	if (error != 0) {
+		errno = error;
+		return -1;
+	}
+	return 0;
+}
+
+static void
+tooslow(int signo)
+{
+	dprintf(STDERR_FILENO, "%s: connect taking too long\n", getprogname());
+	_exit(2);
+}
+
+int
+tcp_connect(const char *host, const char *port, int timeout)
+{
+	struct addrinfo	 hints, *res, *res0;
+	char		 hbuf[NI_MAXHOST];
+	const char	*cause = NULL;
+	int		 error, s = -1, save_errno;
+
+	if (host == NULL) {
+		warnx("hostname missing");
+		return -1;
+	}
+
+	memset(&hints, 0, sizeof hints);
+	hints.ai_family = family;
+	hints.ai_socktype = SOCK_STREAM;
+	if ((error = getaddrinfo(host, port, &hints, &res0))) {
+		warnx("%s: %s", host, gai_strerror(error));
+		return -1;
+	}
+
+	if (timeout) {
+		(void)signal(SIGALRM, tooslow);
+		alarm(timeout);
+	}
+
+	for (res = res0; res; res = res->ai_next) {
+		if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf,
+		    sizeof hbuf, NULL, 0, NI_NUMERICHOST) != 0)
+			(void)strlcpy(hbuf, "(unknown)", sizeof hbuf);
+
+		log_info("Trying %s...\n", hbuf);
+		s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+		if (s == -1) {
+			cause = "socket";
+			continue;
+		}
+
+		for (error = connect(s, res->ai_addr, res->ai_addrlen);
+		    error != 0 && errno == EINTR; error = connect_wait(s))
+			continue;
+
+		if (error != 0) {
+			cause = "connect";
+			save_errno = errno;
+			close(s);
+			errno = save_errno;
+			s = -1;
+			continue;
+		}
+
+		break;
+	}
+
+	freeaddrinfo(res0);
+	if (s == -1) {
+		warn("%s", cause);
+		return -1;
+	}
+
+	if (timeout) {
+		signal(SIGALRM, SIG_DFL);
+		alarm(0);
+	}
+
+	return s;
+}
+
+void
+log_info(const char *fmt, ...)
+{
+	va_list	ap;
+
+	if (verbose == 0)
+		return;
+
+	va_start(ap, fmt);
+	if (oarg && strcmp(oarg, "-") == 0)
+		vfprintf(stderr, fmt, ap);
+	else
+		vprintf(fmt, ap);
+
+	va_end(ap);
+}
+
+void
+log_request(const char *prefix, struct url *url, struct url *proxy)
+{
+	char	*host;
+	int	 custom_port;
+
+	if (url->scheme == S_FILE)
+		return;
+
+	custom_port = strcmp(url->port, url_port_str(url->scheme)) ? 1 : 0;
+	if (url->ip_literal)
+		xasprintf(&host, "[%s]", url->host);
+	else
+		host = xstrdup(url->host);
+
+	if (proxy)
+		log_info("%s %s//%s%s%s%s"
+		    " (via %s//%s%s%s)\n",
+		    prefix,
+		    url_scheme_str(url->scheme),
+		    host,
+		    custom_port ? ":" : "",
+		    custom_port ? url->port : "",
+		    url->path ? url->path : "",
+
+		    /* via proxy part */
+		    (proxy->scheme == S_HTTP) ? "http" : "https",
+		    proxy->host,
+		    proxy->port ? ":" : "",
+		    proxy->port ? proxy->port : "");
+	else
+		log_info("%s %s//%s%s%s%s\n",
+		    prefix,
+		    url_scheme_str(url->scheme),
+		    host,
+		    custom_port ? ":" : "",
+		    custom_port ? url->port : "",
+		    url->path ? url->path : "");
+
+	free(host);
+}
+
+void
+copy_file(FILE *dst, FILE *src, off_t *offset)
+{
+	char	*tmp_buf;
+	size_t	 r;
+
+	tmp_buf = xmalloc(TMPBUF_LEN);
+	while ((r = fread(tmp_buf, 1, TMPBUF_LEN, src)) != 0 && !interrupted) {
+		*offset += r;
+		if (fwrite(tmp_buf, 1, r, dst) != r)
+			err(1, "%s: fwrite", __func__);
+	}
+
+	if (interrupted) {
+		free(tmp_buf);
+		return;
+	}
+
+	if (!feof(src))
+		errx(1, "%s: fread", __func__);
+
+	free(tmp_buf);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xmalloc.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,147 @@
+/* $OpenBSD: xmalloc.c,v 1.11 2016/11/17 10:06:08 nicm Exp $ */
+
+/*
+ * Author: Tatu Ylonen <ylo@cs.hut.fi>
+ * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
+ *                    All rights reserved
+ * Versions of malloc and friends that check their results, and never return
+ * failure (they call fatalx if they encounter an error).
+ *
+ * As far as I am concerned, the code I have written for this software
+ * can be used freely for any purpose.  Any derived versions of this
+ * software must be clearly marked as such, and if the derived work is
+ * incompatible with the protocol description in the RFC file, it must be
+ * called by a name other than "ssh" or "Secure Shell".
+ */
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "xmalloc.h"
+
+void *
+xmalloc(size_t size)
+{
+	void *ptr;
+
+	if (size == 0)
+		errx(1, "xmalloc: zero size");
+	ptr = malloc(size);
+	if (ptr == NULL)
+		err(1, "xmalloc: allocating %zu bytes", size);
+	return ptr;
+}
+
+void *
+xcalloc(size_t nmemb, size_t size)
+{
+	void *ptr;
+
+	if (size == 0 || nmemb == 0)
+		errx(1, "xcalloc: zero size");
+	ptr = calloc(nmemb, size);
+	if (ptr == NULL)
+		err(1, "xcalloc: allocating %zu * %zu bytes", nmemb, size);
+	return ptr;
+}
+
+void *
+xrealloc(void *ptr, size_t size)
+{
+	return xreallocarray(ptr, 1, size);
+}
+
+void *
+xreallocarray(void *ptr, size_t nmemb, size_t size)
+{
+	void *new_ptr;
+
+	if (nmemb == 0 || size == 0)
+		errx(1, "xreallocarray: zero size");
+	new_ptr = reallocarray(ptr, nmemb, size);
+	if (new_ptr == NULL)
+		err(1, "xreallocarray: allocating %zu * %zu bytes",
+		    nmemb, size);
+	return new_ptr;
+}
+
+char *
+xstrdup(const char *str)
+{
+	char *cp;
+
+	if ((cp = strdup(str)) == NULL)
+		err(1, "xstrdup");
+	return cp;
+}
+
+char *
+xstrndup(const char *str, size_t maxlen)
+{
+	char *cp;
+
+	if ((cp = strndup(str, maxlen)) == NULL)
+		err(1, "xstrndup");
+	return cp;
+}
+
+int
+xasprintf(char **ret, const char *fmt, ...)
+{
+	va_list ap;
+	int i;
+
+	va_start(ap, fmt);
+	i = xvasprintf(ret, fmt, ap);
+	va_end(ap);
+
+	return i;
+}
+
+int
+xvasprintf(char **ret, const char *fmt, va_list ap)
+{
+	int i;
+
+	i = vasprintf(ret, fmt, ap);
+
+	if (i < 0 || *ret == NULL)
+		err(1, "xasprintf");
+
+	return i;
+}
+
+int
+xsnprintf(char *str, size_t len, const char *fmt, ...)
+{
+	va_list ap;
+	int i;
+
+	va_start(ap, fmt);
+	i = xvsnprintf(str, len, fmt, ap);
+	va_end(ap);
+
+	return i;
+}
+
+int
+xvsnprintf(char *str, size_t len, const char *fmt, va_list ap)
+{
+	int i;
+
+	if (len > INT_MAX)
+		errx(1, "xsnprintf: len > INT_MAX");
+
+	i = vsnprintf(str, len, fmt, ap);
+
+	if (i < 0 || i >= (int)len)
+		errx(1, "xsnprintf: overflow");
+
+	return i;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xmalloc.h	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,41 @@
+/* $OpenBSD: xmalloc.h,v 1.2 2016/11/17 10:06:08 nicm Exp $ */
+
+/*
+ * Author: Tatu Ylonen <ylo@cs.hut.fi>
+ * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
+ *                    All rights reserved
+ * Created: Mon Mar 20 22:09:17 1995 ylo
+ *
+ * Versions of malloc and friends that check their results, and never return
+ * failure (they call fatal if they encounter an error).
+ *
+ * As far as I am concerned, the code I have written for this software
+ * can be used freely for any purpose.  Any derived versions of this
+ * software must be clearly marked as such, and if the derived work is
+ * incompatible with the protocol description in the RFC file, it must be
+ * called by a name other than "ssh" or "Secure Shell".
+ */
+
+#ifndef XMALLOC_H
+#define XMALLOC_H
+
+void	*xmalloc(size_t);
+void	*xcalloc(size_t, size_t);
+void	*xrealloc(void *, size_t);
+void	*xreallocarray(void *, size_t, size_t);
+char	*xstrdup(const char *);
+char	*xstrndup(const char *, size_t);
+int	 xasprintf(char **, const char *, ...)
+		__attribute__((__format__ (printf, 2, 3)))
+		__attribute__((__nonnull__ (2)));
+int	 xvasprintf(char **, const char *, va_list)
+		__attribute__((__nonnull__ (2)));
+int	 xsnprintf(char *, size_t, const char *, ...)
+		__attribute__((__format__ (printf, 3, 4)))
+		__attribute__((__nonnull__ (3)))
+		__attribute__((__bounded__ (__string__, 1, 2)));
+int	 xvsnprintf(char *, size_t, const char *, va_list)
+		__attribute__((__nonnull__ (3)))
+		__attribute__((__bounded__ (__string__, 1, 2)));
+
+#endif	/* XMALLOC_H */