ftp.c
changeset 0 1d0ce1ebbc72
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ftp.c	Tue Dec 06 13:51:55 2022 +0000
@@ -0,0 +1,445 @@
+/*
+ * Copyright (c) 2015 Sunil Nimmagadda <sunil@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "ftp.h"
+#include "xmalloc.h"
+
+static FILE	*ctrl_fp;
+static int	 data_fd;
+
+void
+ftp_connect(struct url *url, int timeout)
+{
+	char		*buf = NULL;
+	size_t		 n = 0;
+	int		 sock;
+
+	if ((sock = tcp_connect(url->host, url->port, timeout)) == -1)
+		exit(1);
+
+	if ((ctrl_fp = fdopen(sock, "r+")) == NULL)
+		err(1, "%s: fdopen", __func__);
+
+	/* greeting */
+	if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK) {
+		warnx("Can't connect to host `%s'", url->host);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+
+	free(buf);
+	log_info("Connected to %s\n", url->host);
+	if (ftp_auth(ctrl_fp, NULL, NULL) != P_OK) {
+		warnx("Can't login to host `%s'", url->host);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+}
+
+struct url *
+ftp_get(struct url *url, off_t *offset, off_t *sz)
+{
+	char	*buf = NULL, *dir, *file;
+
+	log_info("Using binary mode to transfer files.\n");
+	if (ftp_command(ctrl_fp, "TYPE I") != P_OK)
+		errx(1, "Failed to set mode to binary");
+
+	dir = dirname(url->path);
+	if (ftp_command(ctrl_fp, "CWD %s", dir) != P_OK)
+		errx(1, "CWD command failed");
+
+	log_info("Retrieving %s\n", url->path);
+	file = basename(url->path);
+	if (oarg && strcmp(oarg, "-") == 0)
+		log_info("remote: %s\n", file);
+	else
+		log_info("local: %s remote: %s\n", oarg ? oarg : file , file);
+
+	if (ftp_size(ctrl_fp, file, sz, &buf) != P_OK) {
+		fprintf(stderr, "%s", buf);
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+	free(buf);
+
+	if (activemode)
+		data_fd = ftp_eprt(ctrl_fp);
+	else if ((data_fd = ftp_epsv(ctrl_fp)) == -1)
+		data_fd = ftp_eprt(ctrl_fp);
+
+	if (data_fd == -1)
+		errx(1, "Failed to establish data connection");
+
+	if (*offset && ftp_command(ctrl_fp, "REST %lld", *offset) != P_INTER)
+		errx(1, "REST command failed");
+
+	if (ftp_command(ctrl_fp, "RETR %s", file) != P_PRE) {
+		ftp_command(ctrl_fp, "QUIT");
+		exit(1);
+	}
+
+	return url;
+}
+
+void
+ftp_save(struct url *url, FILE *dst_fp, off_t *offset)
+{
+	struct sockaddr_storage	 ss;
+	FILE			*data_fp;
+	socklen_t		 len;
+	int			 s;
+
+	if (activemode) {
+		len = sizeof(ss);
+		if ((s = accept(data_fd, (struct sockaddr *)&ss, &len)) == -1)
+			err(1, "%s: accept", __func__);
+
+		close(data_fd);
+		data_fd = s;
+	}
+
+	if ((data_fp = fdopen(data_fd, "r")) == NULL)
+		err(1, "%s: fdopen data_fd", __func__);
+
+	copy_file(dst_fp, data_fp, offset);
+	fclose(data_fp);
+}
+
+void
+ftp_close(struct url *url)
+{
+	char	*buf = NULL;
+	size_t	 n = 0;
+
+	/*
+	 * Reading reply here after progressmeter stops.
+	 */
+	if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK)
+		errx(1, "%s: %s", __func__, buf);
+
+	free(buf);
+	ftp_command(ctrl_fp, "QUIT");
+	fclose(ctrl_fp);
+}
+
+int
+ftp_getline(char **lineptr, size_t *n, int suppress_output, FILE *fp)
+{
+	ssize_t		 len;
+	char		*bufp, code[4];
+	const char	*errstr;
+	int		 lookup[] = { P_PRE, P_OK, P_INTER, N_TRANS, N_PERM };
+
+
+	if ((len = getline(lineptr, n, fp)) == -1)
+		err(1, "%s: getline", __func__);
+
+	bufp = *lineptr;
+	if (!suppress_output)
+		log_info("%s", bufp);
+
+	if (len < 4)
+		errx(1, "%s: line too short", __func__);
+
+	(void)strlcpy(code, bufp, sizeof code);
+	if (bufp[3] == ' ')
+		goto done;
+
+	/* multi-line reply */
+	while (!(strncmp(code, bufp, 3) == 0 && bufp[3] == ' ')) {
+		if ((len = getline(lineptr, n, fp)) == -1)
+			err(1, "%s: getline", __func__);
+
+		bufp = *lineptr;
+		if (!suppress_output)
+			log_info("%s", bufp);
+
+		if (len < 4)
+			continue;
+	}
+
+ done:
+	(void)strtonum(code, 100, 553, &errstr);
+	if (errstr)
+		errx(1, "%s: Response code is %s: %s", __func__, errstr, code);
+
+	return lookup[code[0] - '1'];
+}
+
+int
+ftp_command(FILE *fp, const char *fmt, ...)
+{
+	va_list	 ap;
+	char	*buf = NULL, *cmd;
+	size_t	 n = 0;
+	int	 r;
+
+	va_start(ap, fmt);
+	r = vasprintf(&cmd, fmt, ap);
+	va_end(ap);
+	if (r < 0)
+		errx(1, "%s: vasprintf", __func__);
+
+	if (io_debug)
+		fprintf(stderr, ">>> %s\n", cmd);
+
+	if (fprintf(fp, "%s\r\n", cmd) < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	free(cmd);
+	r = ftp_getline(&buf, &n, 0, fp);
+	free(buf);
+	return r;
+
+}
+
+int
+ftp_auth(FILE *fp, const char *user, const char *pass)
+{
+	char	*addr = NULL, hn[HOST_NAME_MAX+1], *un;
+	int	 code;
+
+	code = ftp_command(fp, "USER %s", user ? user : "anonymous");
+	if (code != P_OK && code != P_INTER)
+		return code;
+
+	if (pass == NULL) {
+		if (gethostname(hn, sizeof hn) == -1)
+			err(1, "%s: gethostname", __func__);
+
+		un = getlogin();
+		xasprintf(&addr, "%s@%s", un ? un : "anonymous", hn);
+	}
+
+	code = ftp_command(fp, "PASS %s", pass ? pass : addr);
+	free(addr);
+	return code;
+}
+
+int
+ftp_size(FILE *fp, const char *fn, off_t *sizep, char **buf)
+{
+	size_t	 n = 0;
+	off_t	 file_sz;
+	int	 code;
+
+	if (io_debug)
+		fprintf(stderr, ">>> SIZE %s\n", fn);
+
+	if (fprintf(fp, "SIZE %s\r\n", fn) < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	if ((code = ftp_getline(buf, &n, 1, fp)) != P_OK)
+		return code;
+
+	if (sscanf(*buf, "%*u %lld", &file_sz) != 1)
+		errx(1, "%s: sscanf size", __func__);
+
+	if (file_sz < 0 || file_sz > INT64_MAX)
+		errx(1, "%s: size out of bounds: %lld", __func__, file_sz);
+
+	if (sizep)
+		*sizep = file_sz;
+
+	return code;
+}
+
+int
+ftp_eprt(FILE *fp)
+{
+	struct sockaddr_storage	 ss;
+	char			 addr[NI_MAXHOST], port[NI_MAXSERV], *eprt;
+	socklen_t		 len;
+	int			 e, on, ret, sock;
+
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getsockname(fileno(fp), (struct sockaddr *)&ss, &len) == -1) {
+		warn("%s: getsockname", __func__);
+		return -1;
+	}
+
+	/* pick a free port */
+	switch (ss.ss_family) {
+	case AF_INET:
+		((struct sockaddr_in *)&ss)->sin_port = 0;
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)&ss)->sin6_port = 0;
+		break;
+	default:
+		errx(1, "%s: Invalid socket family", __func__);
+	}
+
+	if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) {
+		warn("%s: socket", __func__);
+		return -1;
+	}
+
+	switch (ss.ss_family) {
+	case AF_INET:
+		on = IP_PORTRANGE_HIGH;
+		if (setsockopt(sock, IPPROTO_IP, IP_PORTRANGE,
+		    (char *)&on, sizeof(on)) < 0)
+			warn("setsockopt IP_PORTRANGE (ignored)");
+		break;
+	case AF_INET6:
+		on = IPV6_PORTRANGE_HIGH;
+		if (setsockopt(sock, IPPROTO_IPV6, IPV6_PORTRANGE,
+		    (char *)&on, sizeof(on)) < 0)
+			warn("setsockopt IPV6_PORTRANGE (ignored)");
+		break;
+	}
+
+	if (bind(sock, (struct sockaddr *)&ss, len) == -1) {
+		close(sock);
+		warn("%s: bind", __func__);
+		return -1;
+	}
+
+	if (listen(sock, 1) == -1) {
+		close(sock);
+		warn("%s: listen", __func__);
+		return -1;
+	}
+
+	/* Find out the ephemeral port chosen */
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getsockname(sock, (struct sockaddr *)&ss, &len) == -1) {
+		close(sock);
+		warn("%s: getsockname", __func__);
+		return -1;
+	}
+
+	if ((e = getnameinfo((struct sockaddr *)&ss, len,
+	    addr, sizeof(addr), port, sizeof(port),
+	    NI_NUMERICHOST | NI_NUMERICSERV)) != 0) {
+		close(sock);
+		warn("%s: getnameinfo: %s", __func__, gai_strerror(e));
+		return -1;
+	}
+
+	xasprintf(&eprt, "EPRT |%d|%s|%s|",
+	    ss.ss_family == AF_INET ? 1 : 2, addr, port);
+
+	ret = ftp_command(fp, "%s", eprt);
+	free(eprt);
+	if (ret != P_OK) {
+		close(sock);
+		return -1;
+	}
+
+	return sock;
+}
+
+int
+ftp_epsv(FILE *fp)
+{
+	struct sockaddr_storage	 ss;
+	char			*buf = NULL, delim[4], *s, *e;
+	size_t			 n = 0;
+	socklen_t		 len;
+	int			 error, port, sock;
+
+	if (io_debug)
+		fprintf(stderr, ">>> EPSV\n");
+
+	if (fprintf(fp, "EPSV\r\n") < 0)
+		errx(1, "%s: fprintf", __func__);
+
+	(void)fflush(fp);
+	if (ftp_getline(&buf, &n, 1, fp) != P_OK) {
+		free(buf);
+		return -1;
+	}
+
+	if ((s = strchr(buf, '(')) == NULL || (e = strchr(s, ')')) == NULL) {
+		warnx("Malformed EPSV reply");
+		free(buf);
+		return -1;
+	}
+
+	s++;
+	*e = '\0';
+	if (sscanf(s, "%c%c%c%d%c", &delim[0], &delim[1], &delim[2],
+	    &port, &delim[3]) != 5) {
+		warnx("EPSV parse error");
+		free(buf);
+		return -1;
+	}
+	free(buf);
+
+	if (delim[0] != delim[1] || delim[0] != delim[2]
+	    || delim[0] != delim[3]) {
+		warnx("EPSV parse error");
+		return -1;
+	}
+
+	len = sizeof(ss);
+	memset(&ss, 0, len);
+	if (getpeername(fileno(fp), (struct sockaddr *)&ss, &len) == -1) {
+		warn("%s: getpeername", __func__);
+		return -1;
+	}
+
+	switch (ss.ss_family) {
+	case AF_INET:
+		((struct sockaddr_in *)&ss)->sin_port = htons(port);
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)&ss)->sin6_port = htons(port);
+		break;
+	default:
+		errx(1, "%s: Invalid socket family", __func__);
+	}
+
+	if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) {
+		warn("%s: socket", __func__);
+		return -1;
+	}
+
+	for (error = connect(sock, (struct sockaddr *)&ss, len);
+	     error != 0 && errno == EINTR; error = connect_wait(sock))
+		continue;
+
+	if (error != 0) {
+		warn("%s: connect", __func__);
+		return -1;
+	}
+
+	return sock;
+}