--- src/lib/libfetch/ftp.c 2003/12/01 22:58:44 1.3 +++ src/lib/libfetch/ftp.c 2007/08/05 21:48:12 1.4 @@ -1,5 +1,5 @@ /*- - * Copyright (c) 1998 Dag-Erling Coïdan Smørgrav + * Copyright (c) 1998-2004 Dag-Erling Coïdan Smørgrav * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,7 +25,7 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * $FreeBSD: src/lib/libfetch/ftp.c,v 1.16.2.31 2003/06/06 06:45:25 des Exp $ + * $FreeBSD: src/lib/libfetch/ftp.c,v 1.96 2007/04/22 22:33:29 njl Exp $ * $DragonFly$ */ @@ -66,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +90,9 @@ #define FTP_EPASSIVE_MODE 229 #define FTP_LOGGED_IN 230 #define FTP_FILE_ACTION_OK 250 +#define FTP_DIRECTORY_CREATED 257 /* multiple meanings */ +#define FTP_FILE_CREATED 257 /* multiple meanings */ +#define FTP_WORKING_DIRECTORY 257 /* multiple meanings */ #define FTP_NEED_PASSWORD 331 #define FTP_NEED_ACCOUNT 332 #define FTP_FILE_OK 350 @@ -198,14 +202,58 @@ _ftp_cmd(conn_t *conn, const char *fmt, * Return a pointer to the filename part of a path */ static const char * -_ftp_filename(const char *file) +_ftp_filename(const char *file, int *len, int *type) { - char *s; + const char *s; if ((s = strrchr(file, '/')) == NULL) - return (file); + s = file; else - return (s + 1); + s = s + 1; + *len = strlen(s); + if (*len > 7 && strncmp(s + *len - 7, ";type=", 6) == 0) { + *type = s[*len - 1]; + *len -= 7; + } else { + *type = '\0'; + } + return (s); +} + +/* + * Get current working directory from the reply to a CWD, PWD or CDUP + * command. + */ +static int +_ftp_pwd(conn_t *conn, char *pwd, size_t pwdlen) +{ + char *src, *dst, *end; + int q; + + if (conn->err != FTP_WORKING_DIRECTORY && + conn->err != FTP_FILE_ACTION_OK) + return (FTP_PROTOCOL_ERROR); + end = conn->buf + conn->buflen; + src = conn->buf + 4; + if (src >= end || *src++ != '"') + return (FTP_PROTOCOL_ERROR); + for (q = 0, dst = pwd; src < end && pwdlen--; ++src) { + if (!q && *src == '"') + q = 1; + else if (q && *src != '"') + break; + else if (q) + *dst++ = '"', q = 0; + else + *dst++ = *src; + } + if (!pwdlen) + return (FTP_PROTOCOL_ERROR); + *dst = '\0'; +#if 0 + DEBUG(fprintf(stderr, "pwd: [%s]\n", pwd)); +#endif + return (FTP_OK); } /* @@ -215,29 +263,138 @@ _ftp_filename(const char *file) static int _ftp_cwd(conn_t *conn, const char *file) { - char *s; - int e; + const char *beg, *end; + char pwd[PATH_MAX]; + int e, i, len; - if ((s = strrchr(file, '/')) == NULL || s == file) { - e = _ftp_cmd(conn, "CWD /"); - } else { - e = _ftp_cmd(conn, "CWD %.*s", s - file, file); - } - if (e != FTP_FILE_ACTION_OK) { + /* If no slashes in name, no need to change dirs. */ + if ((end = strrchr(file, '/')) == NULL) + return (0); + if ((e = _ftp_cmd(conn, "PWD")) != FTP_WORKING_DIRECTORY || + (e = _ftp_pwd(conn, pwd, sizeof(pwd))) != FTP_OK) { _ftp_seterr(e); return (-1); } + for (;;) { + len = strlen(pwd); + + /* Look for a common prefix between PWD and dir to fetch. */ + for (i = 0; i <= len && i <= end - file; ++i) + if (pwd[i] != file[i]) + break; +#if 0 + DEBUG(fprintf(stderr, "have: [%.*s|%s]\n", i, pwd, pwd + i)); + DEBUG(fprintf(stderr, "want: [%.*s|%s]\n", i, file, file + i)); +#endif + /* Keep going up a dir until we have a matching prefix. */ + if (pwd[i] == '\0' && (file[i - 1] == '/' || file[i] == '/')) + break; + if ((e = _ftp_cmd(conn, "CDUP")) != FTP_FILE_ACTION_OK || + (e = _ftp_cmd(conn, "PWD")) != FTP_WORKING_DIRECTORY || + (e = _ftp_pwd(conn, pwd, sizeof(pwd))) != FTP_OK) { + _ftp_seterr(e); + return (-1); + } + } + +#ifdef FTP_COMBINE_CWDS + /* Skip leading slashes, even "////". */ + for (beg = file + i; beg < end && *beg == '/'; ++beg, ++i) + /* nothing */ ; + + /* If there is no trailing dir, we're already there. */ + if (beg >= end) + return (0); + + /* Change to the directory all in one chunk (e.g., foo/bar/baz). */ + e = _ftp_cmd(conn, "CWD %.*s", (int)(end - beg), beg); + if (e == FTP_FILE_ACTION_OK) + return (0); +#endif /* FTP_COMBINE_CWDS */ + + /* That didn't work so go back to legacy behavior (multiple CWDs). */ + for (beg = file + i; beg < end; beg = file + i + 1) { + while (*beg == '/') + ++beg, ++i; + for (++i; file + i < end && file[i] != '/'; ++i) + /* nothing */ ; + e = _ftp_cmd(conn, "CWD %.*s", file + i - beg, beg); + if (e != FTP_FILE_ACTION_OK) { + _ftp_seterr(e); + return (-1); + } + } return (0); } /* + * Set transfer mode and data type + */ +static int +_ftp_mode_type(conn_t *conn, int mode, int type) +{ + int e; + + switch (mode) { + case 0: + case 's': + mode = 'S'; + case 'S': + break; + default: + return (FTP_PROTOCOL_ERROR); + } + if ((e = _ftp_cmd(conn, "MODE %c", mode)) != FTP_OK) { + if (mode == 'S') { + /* + * Stream mode is supposed to be the default - so + * much so that some servers not only do not + * support any other mode, but do not support the + * MODE command at all. + * + * If "MODE S" fails, it is unlikely that we + * previously succeeded in setting a different + * mode. Therefore, we simply hope that the + * server is already in the correct mode, and + * silently ignore the failure. + */ + } else { + return (e); + } + } + + switch (type) { + case 0: + case 'i': + type = 'I'; + case 'I': + break; + case 'a': + type = 'A'; + case 'A': + break; + case 'd': + type = 'D'; + case 'D': + /* can't handle yet */ + default: + return (FTP_PROTOCOL_ERROR); + } + if ((e = _ftp_cmd(conn, "TYPE %c", type)) != FTP_OK) + return (e); + + return (FTP_OK); +} + +/* * Request and parse file stats */ static int _ftp_stat(conn_t *conn, const char *file, struct url_stat *us) { char *ln; - const char *s; + const char *filename; + int filenamelen, type; struct tm tm; time_t t; int e; @@ -245,12 +402,15 @@ _ftp_stat(conn_t *conn, const char *file us->size = -1; us->atime = us->mtime = 0; - if ((s = strrchr(file, '/')) == NULL) - s = file; - else - ++s; + filename = _ftp_filename(file, &filenamelen, &type); + + if ((e = _ftp_mode_type(conn, 0, type)) != FTP_OK) { + _ftp_seterr(e); + return (-1); + } - if ((e = _ftp_cmd(conn, "SIZE %s", s)) != FTP_FILE_STATUS) { + e = _ftp_cmd(conn, "SIZE %.*s", filenamelen, filename); + if (e != FTP_FILE_STATUS) { _ftp_seterr(e); return (-1); } @@ -267,7 +427,8 @@ _ftp_stat(conn_t *conn, const char *file us->size = -1; DEBUG(fprintf(stderr, "size: [%lld]\n", (long long)us->size)); - if ((e = _ftp_cmd(conn, "MDTM %s", s)) != FTP_FILE_STATUS) { + e = _ftp_cmd(conn, "MDTM %.*s", filenamelen, filename); + if (e != FTP_FILE_STATUS) { _ftp_seterr(e); return (-1); } @@ -455,6 +616,9 @@ _ftp_transfer(conn_t *conn, const char * struct sockaddr_storage sa; struct sockaddr_in6 *sin6; struct sockaddr_in *sin4; + const char *bindaddr; + const char *filename; + int filenamelen, type; int low, pasv, verbose; int e, sd = -1; socklen_t l; @@ -471,6 +635,13 @@ _ftp_transfer(conn_t *conn, const char * pasv = ((s = getenv("FTP_PASSIVE_MODE")) != NULL && strncasecmp(s, "no", 2) != 0); + /* isolate filename */ + filename = _ftp_filename(file, &filenamelen, &type); + + /* set transfer mode and data type */ + if ((e = _ftp_mode_type(conn, 0, type)) != FTP_OK) + goto ouch; + /* find our own address, bind, and listen */ l = sizeof(sa); if (getsockname(conn->sd, (struct sockaddr *)&sa, &l) == -1) @@ -590,13 +761,17 @@ _ftp_transfer(conn_t *conn, const char * /* connect to data port */ if (verbose) _fetch_info("opening data connection"); + bindaddr = getenv("FETCH_BIND_ADDRESS"); + if (bindaddr != NULL && *bindaddr != '\0' && + _fetch_bind(sd, sa.ss_family, bindaddr) != 0) + goto sysouch; if (connect(sd, (struct sockaddr *)&sa, sa.ss_len) == -1) goto sysouch; /* make the server initiate the transfer */ if (verbose) _fetch_info("initiating transfer"); - e = _ftp_cmd(conn, "%s %s", oper, _ftp_filename(file)); + e = _ftp_cmd(conn, "%s %.*s", oper, filenamelen, filename); if (e != FTP_CONNECTION_ALREADY_OPEN && e != FTP_OPEN_DATA_CONNECTION) goto ouch; @@ -681,15 +856,14 @@ _ftp_transfer(conn_t *conn, const char * /* seek to required offset */ if (offset) - if (_ftp_cmd(conn, "REST %llu", - (unsigned long long)offset) != FTP_FILE_OK) + if (_ftp_cmd(conn, "REST %ju", (uintmax_t)offset) != FTP_FILE_OK) goto sysouch; /* make the server initiate the transfer */ if (verbose) _fetch_info("initiating transfer"); - e = _ftp_cmd(conn, "%s %s", oper, _ftp_filename(file)); - if (e != FTP_OPEN_DATA_CONNECTION) + e = _ftp_cmd(conn, "%s %.*s", oper, filenamelen, filename); + if (e != FTP_CONNECTION_ALREADY_OPEN && e != FTP_OPEN_DATA_CONNECTION) goto ouch; /* accept the incoming connection and go to town */ @@ -812,13 +986,7 @@ _ftp_connect(struct url *url, struct url if ((e = _ftp_authenticate(conn, url, purl)) != FTP_LOGGED_IN) goto fouch; - /* might as well select mode and type at once */ -#ifdef FTP_FORCE_STREAM_MODE - if ((e = _ftp_cmd(conn, "MODE S")) != FTP_OK) /* default is S */ - goto fouch; -#endif - if ((e = _ftp_cmd(conn, "TYPE I")) != FTP_OK) /* default is A */ - goto fouch; + /* TODO: Request extended features supported, if any (RFC 3659). */ /* done */ return (conn);