main.c
author Sunil Nimmagadda <sunil@nimmagadda.net>
Tue, 06 Dec 2022 13:51:55 +0000
changeset 0 1d0ce1ebbc72
permissions -rw-r--r--
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);
}