/* Unix-specific ftpconn hooks Copyright (C) 1997 Free Software Foundation, Inc. Written by Miles Bader This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include #include #include #include #include #include #include #include #include #ifdef HAVE_HURD_HURD_TYPES_H #include #endif #include /* Uid/gid to use when we don't know about a particular user name. */ #define DEFAULT_UID 65535 #define DEFAULT_GID 65535 struct ftp_conn_syshooks ftp_conn_unix_syshooks = { ftp_conn_unix_pasv_addr, ftp_conn_unix_interp_err, ftp_conn_unix_start_get_stats, ftp_conn_unix_cont_get_stats }; /* Try to get an internet address out of the reply to a PASV command. Unfortunately, the format of the reply such isn't standardized. */ error_t ftp_conn_unix_pasv_addr (struct ftp_conn *conn, const char *txt, struct sockaddr **addr) { unsigned a0, a1, a2, a3; /* Parts of the inet address */ unsigned p0, p1; /* Parts of the prot (msb, lsb) */ if (sscanf (txt, "%*[^0-9]%d,%d,%d,%d,%d,%d", &a0,&a1,&a2,&a3, &p0,&p1) != 6) return EGRATUITOUS; else { unsigned char *a, *p; *addr = malloc (sizeof (struct sockaddr_in)); if (! *addr) return ENOMEM; (*addr)->sa_len = sizeof (struct sockaddr_in); (*addr)->sa_family = AF_INET; a = (unsigned char *)&((struct sockaddr_in *)*addr)->sin_addr.s_addr; a[0] = a0 & 0xff; a[1] = a1 & 0xff; a[2] = a2 & 0xff; a[3] = a3 & 0xff; p = (unsigned char *)&((struct sockaddr_in *)*addr)->sin_port; p[0] = p0 & 0xff; p[1] = p1 & 0xff; return 0; } } /* Compare strings P & Q in a most forgiving manner, ignoring case and everything but alphanumeric characters. */ static int strlaxcmp (const char *p, const char *q) { for (;;) { int ch1, ch2; while (*p && !isalnum (*p)) p++; while (*q && !isalnum (*q)) q++; if (!*p || !*q) break; ch1 = tolower (*p); ch2 = tolower (*q); if (ch1 != ch2) break; p++; q++; } return *p - *q; } /* Try to convert an error message in TXT into an error code. POSS_ERRS contains a list of likely errors to try; if no other clue is found, the first thing in poss_errs is returned. */ error_t ftp_conn_unix_interp_err (struct ftp_conn *conn, const char *txt, const error_t *poss_errs) { const char *p; const error_t *e; if (!poss_errs || !poss_errs[0]) return EIO; /* ignore everything before the last colon. */ p = strrchr (txt, ':'); if (p) p++; else p = txt; /* Now, for each possible error, do a string compare ignoring case and anything non-alphanumberic. */ for (e = poss_errs; *e; e++) if (strlaxcmp (p, strerror (*e)) == 0) return *e; return poss_errs[0]; } struct get_stats_state { char *name; /* Last read (maybe partial) name. */ size_t name_len; /* Valid length of NAME, *not including* '\0'. */ size_t name_alloced; /* Allocated size of NAME (>= NAME_LEN). */ int name_partial; /* True if NAME isn't complete. */ int contents; /* Are we looking for directory contents? */ int added_slash; /* Did we prefix the name with `./'? */ struct stat stat; /* Last read stat info. */ int start; /* True if at beginning of output. */ size_t buf_len; /* Length of contents in BUF. */ char buf[7000]; }; /* Start an operation to get a list of file-stat structures for NAME (this is often similar to ftp_conn_start_dir, but with OS-specific flags), and return a file-descriptor for reading on, and a state structure in STATE suitable for passing to cont_get_stats. If CONTENTS is true, NAME must refer to a directory, and the contents will be returned, otherwise, the (single) result will refer to NAME. */ error_t ftp_conn_unix_start_get_stats (struct ftp_conn *conn, const char *name, int contents, int *fd, void **state) { error_t err; size_t req_len; char *req; struct get_stats_state *s = malloc (sizeof (struct get_stats_state)); const char *flags = contents ? "-A" : "-Ad"; const char *slash = strchr (name, '/'); if (! s) return ENOMEM; if (strcspn (name, "*? \t\n{}$`\\\"'") < strlen (name)) /* NAME contains some metacharacters, which makes the behavior of various ftp servers unpredictable, so punt. */ { free (s); return EINVAL; } /* We pack the ls options and the name into the list argument, in REQ, which will do the right thing on most unix ftp servers. */ /* Space needed for REQ. */ req_len = strlen (flags) + 1 + strlen (name) + 1; /* If NAME doesn't contain a slash, we prepend `./' to it so that we can tell from the results whether it's a directory or not. */ if (! slash) req_len += 2; req = malloc (req_len); if (! req) return ENOMEM; snprintf (req, req_len, "%s %s%s", flags, slash ? "" : "./", name); /* Make the actual request. */ err = ftp_conn_start_dir (conn, req, fd); free (req); if (err) free (s); else { s->contents = contents; s->added_slash = !slash; s->name = 0; s->name_len = s->name_alloced = 0; s->name_partial = 0; s->buf_len = 0; s->start = 1; *state = s; } return err; } static char *months[] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", 0 }; /* Translate the information in the ls output in *LINE as best we can into STAT, and update *LINE to point to the filename at the end of the line. If *LINE should be ignored, EAGAIN is returned. */ static error_t parse_dir_entry (char **line, struct stat *stat) { char **m; struct tm tm; char *p = *line, *e; /* drwxrwxrwt 3 root wheel 1024 May 1 16:58 /tmp drwxrwxrwt 5 root daemon 4096 May 1 17:15 /tmp drwxrwxrwt 4 root 0 1024 May 1 14:34 /tmp drwxrwxrwt 6 root wheel 284 May 1 12:46 /tmp drwxrwxrwt 4 sys sys 482 May 1 17:11 /tmp drwxrwxrwt 7 34 archive 512 May 1 14:28 /tmp */ if (strncasecmp (p, "total ", 6) == 0) return EAGAIN; bzero (stat, sizeof *stat); #ifdef FSTYPE_FTP stat->st_fstype = FSTYPE_FTP; #endif /* File format (S_IFMT) bits. */ switch (*p++) { case '-': stat->st_mode |= S_IFREG; break; case 'd': stat->st_mode |= S_IFDIR; break; case 'c': stat->st_mode |= S_IFCHR; break; case 'b': stat->st_mode |= S_IFBLK; break; case 'l': stat->st_mode |= S_IFLNK; break; case 's': stat->st_mode |= S_IFSOCK; break; case 'p': stat->st_mode |= S_IFIFO; break; default: return EGRATUITOUS; } /* User perm bits. */ switch (*p++) { case '-': break; case 'r': stat->st_mode |= S_IRUSR; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'w': stat->st_mode |= S_IWUSR; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'x': stat->st_mode |= S_IXUSR; break; case 's': stat->st_mode |= S_IXUSR | S_ISUID; break; case 'S': stat->st_mode |= S_ISUID; break; default: return EGRATUITOUS; } /* Group perm bits. */ switch (*p++) { case '-': break; case 'r': stat->st_mode |= S_IRGRP; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'w': stat->st_mode |= S_IWGRP; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'x': stat->st_mode |= S_IXGRP; break; case 's': stat->st_mode |= S_IXGRP | S_ISGID; break; case 'S': stat->st_mode |= S_ISGID; break; default: return EGRATUITOUS; } /* `Other' perm bits. */ switch (*p++) { case '-': break; case 'r': stat->st_mode |= S_IROTH; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'w': stat->st_mode |= S_IWOTH; break; default: return EGRATUITOUS; } switch (*p++) { case '-': break; case 'x': stat->st_mode |= S_IXOTH; break; case 't': stat->st_mode |= S_IXOTH | S_ISVTX; break; case 'T': stat->st_mode |= S_ISVTX; break; default: return EGRATUITOUS; } #define SKIP_WS() \ while (isspace (*p)) p++; #define PARSE_INT() ({ \ unsigned u = strtoul (p, &e, 10); \ if (e == p || isalnum (*e)) \ return EGRATUITOUS; \ p = e; \ u; \ }) /* Link count. */ SKIP_WS (); stat->st_nlink = PARSE_INT (); /* File owner. */ SKIP_WS (); if (isdigit (*p)) stat->st_uid = PARSE_INT (); else { struct passwd *pw; e = p + strcspn (p, " \t\n"); *e++ = '\0'; pw = getpwnam (p); if (pw) stat->st_uid = pw->pw_uid; else stat->st_uid = DEFAULT_UID; p = e; } #ifdef HAVE_STAT_ST_AUTHOR stat->st_author = stat->st_uid; #endif /* File group. */ SKIP_WS (); if (isdigit (*p)) stat->st_gid = PARSE_INT (); else { struct group *gr; e = p + strcspn (p, " \t\n"); *e++ = '\0'; gr = getgrnam (p); if (gr) stat->st_gid = gr->gr_gid; else stat->st_gid = DEFAULT_GID; p = e; } /* File size / device numbers. */ SKIP_WS (); if (S_ISCHR (stat->st_mode) || S_ISBLK (stat->st_mode)) /* Block and character devices show the block params instead of the file size. */ { stat->st_dev = PARSE_INT (); if (*p != ',') return EGRATUITOUS; stat->st_dev = (stat->st_dev << 8) | PARSE_INT (); stat->st_size = 0; } else /* File size. */ stat->st_size = PARSE_INT (); stat->st_blocks = stat->st_size >> 9; /* Date. Ick. */ /* Formats: MONTH DAY HH:MM and MONTH DAY YEAR */ bzero (&tm, sizeof tm); SKIP_WS (); e = p + strcspn (p, " \t\n"); for (m = months; *m; m++) if (strncasecmp (*m, p, e - p) == 0) { tm.tm_mon = m - months; break; } if (! *m) return EGRATUITOUS; p = e; SKIP_WS (); tm.tm_mday = PARSE_INT (); SKIP_WS (); if (p[1] == ':' || p[2] == ':') { struct tm *now_tm; struct timeval now_tv; tm.tm_hour = PARSE_INT (); p++; tm.tm_min = PARSE_INT (); if (gettimeofday (&now_tv, 0) != 0) return errno; now_tm = localtime (&now_tv.tv_sec); if (now_tm->tm_mon < tm.tm_mon) tm.tm_year = now_tm->tm_year - 1; else tm.tm_year = now_tm->tm_year; } else tm.tm_year = PARSE_INT () - 1900; stat->st_mtime = mktime (&tm); if (stat->st_mtime == (time_t)-1) return EGRATUITOUS; /* atime and ctime are the same as mtime. */ stat->st_atime = stat->st_ctime = stat->st_mtime; /* Update *LINE to point to the filename. */ SKIP_WS (); *line = p; return 0; } /* Read stats information from FD, calling ADD_STAT for each new stat (HOOK is passed to ADD_STAT). FD and STATE should be returned from start_get_stats. If this function returns EAGAIN, then it should be called again to finish the job (possibly after calling select on FD); if it returns 0, then it is finishe,d and FD and STATE are deallocated. */ error_t ftp_conn_unix_cont_get_stats (struct ftp_conn *conn, int fd, void *state, ftp_conn_add_stat_fun_t add_stat, void *hook) { char *p, *nl; ssize_t rd; size_t name_len; error_t err = 0; struct get_stats_state *s = state; /* We always consume full lines, so we know that we have to read more when we first get called. */ rd = read (fd, s->buf + s->buf_len, sizeof (s->buf) - s->buf_len); if (rd < 0) { err = errno; goto finished; } if (rd == 0) /* EOF */ if (s->buf_len == 0) /* We're done! Clean up and return the result in STATS. */ { if (s->start) /* No output at all. From many ftp servers, this means that the specified file wasn't found. */ err = ENOENT; goto finished; } else /* Partial line at end of file? */ nl = s->buf + s->buf_len; else /* Look for a new line in what we read (we know that there weren't any in the buffer before that). */ { nl = memchr (s->buf + s->buf_len, '\n', rd); s->buf_len += rd; } s->start = 0; /* We've read past the start. */ if (!nl && s->buf_len < sizeof (s->buf)) /* We didn't find any newlines (which implies we didn't hit EOF), and we still have room to grow the buffer, so just wait until next time to do anything. */ return EAGAIN; /* Where we start parsing. */ p = s->buf; do { if (! s->name_partial) /* We aren't continuing to read an overflowed name from the previous call, so we know that we are at the start of a line, and can parse the info here as a directory entry. */ { /* Parse the directory entry info, updating P to point to the beginning of the name. */ err = parse_dir_entry (&p, &s->stat); if (err == EAGAIN) /* This line isn't a real entry and should be ignored. */ goto skip_line; if (err) goto finished; } /* Now fill in S->last_stat's name field, possibly extending it from a previous buffer. */ name_len = (nl ? nl - p : s->buf + s->buf_len - p); if (name_len > 0 && p[name_len - 1] == '\r') name_len--; if (name_len > 0) /* Extending s->name. */ { size_t old_len = s->name_len; size_t total_len = old_len + name_len + 1; if (total_len > s->name_alloced) { char *new_name = realloc (s->name, total_len); if (! new_name) goto enomem; s->name = new_name; s->name_alloced = total_len; } strncpy (s->name + old_len, p, name_len); s->name[old_len + name_len] = '\0'; s->name_len = total_len - 1; } if (nl) { char *name = s->name; char *symlink_target = 0; if (S_ISLNK (s->stat.st_mode)) /* A symlink, see if we can find the link target. */ { symlink_target = strstr (name, " -> "); if (symlink_target) { *symlink_target = '\0'; symlink_target += 4; } } if (strchr (name, '/')) if (s->contents) /* We know that the name originally request had a slash in it (because we added one if necessary), so if a name in the listing has one too, it can't be the contents of a directory; if this is the case and we wanted the contents, this must not be a directory. */ { err = ENOTDIR; goto finished; } else if (s->added_slash) /* S->name must be the same name we passed; if we added a `./' prefix, removed it so the client gets back what it passed. */ name += 2; /* Call the callback function to process the current entry; it is responsible for freeing S->name and SYMLINK_TARGET. */ err = (*add_stat) (name, &s->stat, symlink_target, hook); if (err) goto finished; s->name_len = 0; s->name_partial = 0; skip_line: p = nl + 1; nl = memchr (p, '\n', s->buf + s->buf_len - p); } else /* We found no newline, so the name extends past what we read; we'll try to read more next time. */ { s->name_partial = 1; /* Skip over the partial name for the next iteration. */ p += name_len; } } while (nl); /* Move any remaining characters in the buffer to the beginning for the next call. */ s->buf_len -= (p - s->buf); if (s->buf_len > 0) memmove (s->buf, p, s->buf_len); /* Try again later. */ return EAGAIN; enomem: /* Some memory allocation failed. */ err = ENOMEM; finished: /* We're finished (with an error if ERR != 0), deallocate everything & return. */ if (s->name) free (s->name); free (s); close (fd); if (err && rd > 0) ftp_conn_abort (conn); else if (err) ftp_conn_finish_transfer (conn); else err = ftp_conn_finish_transfer (conn); return err; }