diff -r 000000000000 -r 1d0ce1ebbc72 ftp.c --- /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 + * + * 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 + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +}