summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile14
-rw-r--r--authors1
-rw-r--r--cmd.c637
-rw-r--r--file.c53
-rw-r--r--ftp.1411
-rw-r--r--ftp.c445
-rw-r--r--ftp.h115
-rw-r--r--http.c833
-rw-r--r--main.c526
-rw-r--r--progressmeter.c358
-rw-r--r--regress/Makefile3
-rw-r--r--regress/unit-tests/Makefile2
-rw-r--r--regress/unit-tests/url_parse/Makefile17
-rw-r--r--regress/unit-tests/url_parse/test_url_parse.c134
-rw-r--r--url.c424
-rw-r--r--util.c215
-rw-r--r--xmalloc.c147
-rw-r--r--xmalloc.h41
18 files changed, 4376 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d8d5906
--- /dev/null
+++ b/Makefile
@@ -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>
diff --git a/authors b/authors
new file mode 100644
index 0000000..417bc71
--- /dev/null
+++ b/authors
@@ -0,0 +1 @@
+Sunil Nimmagadda <sunil@nimmagadda.net>
diff --git a/cmd.c b/cmd.c
new file mode 100644
index 0000000..faf7949
--- /dev/null
+++ b/cmd.c
@@ -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);
+ }
+}
diff --git a/file.c b/file.c
new file mode 100644
index 0000000..f60f223
--- /dev/null
+++ b/file.c
@@ -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);
+}
diff --git a/ftp.1 b/ftp.1
new file mode 100644
index 0000000..f610a80
--- /dev/null
+++ b/ftp.1
@@ -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.
diff --git a/ftp.c b/ftp.c
new file mode 100644
index 0000000..c0d9c89
--- /dev/null
+++ b/ftp.c
@@ -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;
+}
diff --git a/ftp.h b/ftp.h
new file mode 100644
index 0000000..909465b
--- /dev/null
+++ b/ftp.h
@@ -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 */
diff --git a/http.c b/http.c
new file mode 100644
index 0000000..9cbc59f
--- /dev/null
+++ b/http.c
@@ -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 */
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..a3d9f67
--- /dev/null
+++ b/main.c
@@ -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);
+}
diff --git a/progressmeter.c b/progressmeter.c
new file mode 100644
index 0000000..ec9b8ee
--- /dev/null
+++ b/progressmeter.c
@@ -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 */
+}
diff --git a/regress/Makefile b/regress/Makefile
new file mode 100644
index 0000000..5301289
--- /dev/null
+++ b/regress/Makefile
@@ -0,0 +1,3 @@
+SUBDIR+= unit-tests
+
+.include <bsd.subdir.mk>
diff --git a/regress/unit-tests/Makefile b/regress/unit-tests/Makefile
new file mode 100644
index 0000000..e67c6f6
--- /dev/null
+++ b/regress/unit-tests/Makefile
@@ -0,0 +1,2 @@
+SUBDIR+= url_parse
+. include <bsd.regress.mk>
diff --git a/regress/unit-tests/url_parse/Makefile b/regress/unit-tests/url_parse/Makefile
new file mode 100644
index 0000000..608eb81
--- /dev/null
+++ b/regress/unit-tests/url_parse/Makefile
@@ -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>
diff --git a/regress/unit-tests/url_parse/test_url_parse.c b/regress/unit-tests/url_parse/test_url_parse.c
new file mode 100644
index 0000000..5c15d35
--- /dev/null
+++ b/regress/unit-tests/url_parse/test_url_parse.c
@@ -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;
+}
diff --git a/url.c b/url.c
new file mode 100644
index 0000000..546d448
--- /dev/null
+++ b/url.c
@@ -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))));
+}
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..463fb8b
--- /dev/null
+++ b/util.c
@@ -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);
+}
diff --git a/xmalloc.c b/xmalloc.c
new file mode 100644
index 0000000..cd85939
--- /dev/null
+++ b/xmalloc.c
@@ -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;
+}
diff --git a/xmalloc.h b/xmalloc.h
new file mode 100644
index 0000000..76d93a2
--- /dev/null
+++ b/xmalloc.h
@@ -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 */