An HTTP(S), FTP client.
Found a copy of some old OpenBSD days hacking stashed somewhere in
the backups. This version saw the light of the day as official
OpenBSD ftp(1) for a grand total of 1 day :-)
/*
* 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);
}