/* 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 #include #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. */ int name_partial; /* True if NAME isn't complete. */ struct stat stat; /* Last read stat info. */ size_t buf_len; /* Length of contents in BUF. */ int start; /* True if at beginning of output. */ 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. FORCE_DIR controls what happens if NAME refers to a directory: if FORCE_DIR is false, STATS will contain entries for all files *in* NAME, and if FORCE_DIR is true, it will contain just a single entry for NAME itself (or an error will be returned when this isn't possible). */ error_t ftp_conn_unix_start_get_stats (struct ftp_conn *conn, const char *name, int force_dir, int *fd, void **state) { error_t err; struct get_stats_state *s = malloc (sizeof (struct get_stats_state)); if (! s) return ENOMEM; if (force_dir) { char *dir_op; if (asprintf (&dir_op, "-d %s", name) <= 0) return errno; err = ftp_conn_start_dir (conn, dir_op, fd); free (dir_op); } else err = ftp_conn_start_dir (conn, name, fd); if (err) free (s); else { s->name = 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); stat->st_fstype = FSTYPE_FTP; /* 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; } stat->st_author = stat->st_uid; /* 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) if (s->name) /* Extending s->name. */ { size_t old_len = strlen (s->name); char *new_name = realloc (s->name, old_len + name_len + 1); if (! new_name) goto enomem; s->name = new_name; strncpy (new_name + old_len, p, name_len); new_name[old_len + name_len] = '\0'; } else /* A new name. */ { s->name = malloc (name_len + 1); if (! s->name) goto enomem; strncpy (s->name, p, name_len); s->name[name_len] = '\0'; } if (nl) { char *symlink_target = 0; if (S_ISLNK (s->stat.st_mode)) /* A symlink, see if we can find the link target. */ { char *lsep = strstr (s->name, " -> "); if (lsep) { *lsep = '\0'; lsep += 4; symlink_target = strdup (lsep); s->name = realloc (s->name, (lsep - 3) - s->name); } } /* Call the callback function to process the current entry; it is responsible for freeing S->name and SYMLINK_TARGET. */ err = (*add_stat) (s->name, &s->stat, symlink_target, hook); if (err) goto finished; s->name = 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); return err; }