/* ** Copyright 2000-2004 University of Illinois Board of Trustees ** Copyright 2000-2004 Mark D. Roth ** All rights reserved. ** ** ftplist.c - FTP directory parsing code ** ** Mark D. Roth */ #include #include #include #include #include #include #ifdef STDC_HEADERS # include # include #endif /* needed for strcasecmp() on some platforms */ #include #ifdef DEBUG static void _print_file_info(file_info_t *fip) { printf("fi_filename\t\t= \"%s\"\n", fip->fi_filename); printf("fi_linkto\t\t= \"%s\"\n", fip->fi_linkto); printf("fs_mode\t\t\t= %04o\n", (unsigned int)fip->fi_stat.fs_mode); printf("fs_nlink\t\t= %hu\n", (unsigned short)fip->fi_stat.fs_nlink); printf("fs_username\t\t= \"%s\"\n", fip->fi_stat.fs_username); printf("fs_groupname\t\t= \"%s\"\n", fip->fi_stat.fs_groupname); printf("fs_size\t\t\t= %lu\n", (unsigned long)fip->fi_stat.fs_size); printf("fs_mtime\t\t= %s", asctime(localtime(&fip->fi_stat.fs_mtime))); } #endif /* ** _ftp_list_parse_choose() - choose a list-parsing function based on ** the first line returned by the server ** arguments: ** ftp FTP handle ** buf line to be parsed */ static void _ftp_list_parse_choose(FTP *ftp, char *buf) { int i; #ifdef DEBUG printf("==> _ftp_list_parse_choose(\"%s\")\n", buf); #endif /* ** for MLST, we know what to use, ** so we don't even bother looking at buf */ if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST) && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST)) { ftp->ftp_list_parse_function = _ftp_list_parse_mlsd; #ifdef DEBUG printf("<== _ftp_list_parse_choose(): using MLST parsing\n"); #endif return; } /* if it starts with a '+', assume EPLF */ if (buf[0] == '+') { ftp->ftp_list_parse_function = _ftp_list_parse_eplf; #ifdef DEBUG printf("<== _ftp_list_parse_choose(): using EPLF parsing\n"); #endif return; } /* if there's no whitespace, assume it's a filename-only listing */ if (strpbrk(buf, " \t") == NULL) { ftp->ftp_list_parse_function = _ftp_list_parse_dummy; #ifdef DEBUG printf("<== _ftp_list_parse_choose(): using dummy parsing\n"); #endif return; } /* if it starts with a date in the form MM-DD-YY, assume NT */ if (sscanf(buf, "%2d-%2d-%2d", &i, &i, &i) == 3) { ftp->ftp_list_parse_function = _ftp_list_parse_nt; #ifdef DEBUG printf("<== _ftp_list_parse_choose(): using WinNT parsing\n"); #endif return; } /* ** if we haven't identified it as any other type, ** assume UNIX-style directories by default */ ftp->ftp_list_parse_function = _ftp_list_parse_unix; #ifdef DEBUG printf("<== _ftp_list_parse_choose(): using UNIX parsing\n"); #endif } /* ** _ftp_list() - request, read, and parse a directory list from the server ** Returns: ** (number of entries) success ** -1 (and sets errno) failure */ int _ftp_list(FTP *ftp, char *path, fget_hash_t *files_h, enum dcent_types preferred_type, unsigned long flags) { char buf[FTPBUFSIZE]; int retval = 0, code = 0, numignored = 0, save_errno; unsigned short in_desired_cwd = 0; char *new_cwd, *cp, *cp2; char orig_path[MAXPATHLEN] = ""; file_info_t *fip; #ifdef DEBUG printf("==> _ftp_list(ftp=0x%lx (%s), path=\"%s\", " "files_h=0x%lx, preferred_type=%d, flags=%lu)\n", ftp, ftp->ftp_host, path, files_h, preferred_type, flags); #endif /* ** for MLSD or MLST, we don't need to mess with CWD */ if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST) && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST)) { /* ** make sure the server knows which facts we want returned */ if (_ftp_opts_mlsd(ftp) == -1) return -1; /* ** for MLSD, set up the command and skip down ** to where we open the ftp-data connection */ if (preferred_type == DCENT_DIR) { snprintf(buf, sizeof(buf), "MLSD %s", path); goto open_data_connection; } /* ** MLST is handled differently, because there's no ** ftp-data connection */ if (_ftp_send_command(ftp, "MLST %s", path) == -1) { #ifdef DEBUG printf(" _ftp_list(): _ftp_send_command(\"%s\"): " "%s\n", buf, strerror(errno)); #endif return -1; } if (_ftp_get_response(ftp, &code, buf, sizeof(buf)) == -1) { #ifdef DEBUG printf(" _ftp_list(): _ftp_get_response(): %s\n", strerror(errno)); #endif return -1; } if (code != 250) { errno = ENOENT; return -1; } /* find line starting with a space */ cp = strstr(buf, "\n "); if (cp == NULL) { errno = EINVAL; return -1; } /* skip to beginning of MLST data */ cp += 2; /* find next newline */ cp2 = strchr(cp, '\n'); if (cp2 == NULL) { errno = EINVAL; return -1; } *cp2 = '\0'; fip = (file_info_t *)calloc(1, sizeof(file_info_t)); if (fip == NULL) return -1; /* parse the entry */ switch (_ftp_list_parse_mlsd(ftp, cp, fip)) { /* ** _ftp_list_parse_mlsd() never returns FLP_ERROR */ case FLP_IGNORE: default: /* ignore line */ free(fip); errno = EINVAL; return -1; case FLP_VALID: /* normal file */ fget_hash_add(files_h, fip); } /* return number of entries, which is always 1 */ return 1; } /* ** not using MLSx, so construct LIST request */ /* ** if the path is "/", always treat it as a DCENT_DIR request, ** since there is no parent directory to check for a DCENT_FILE */ if (strcmp(path, "/") == 0) preferred_type = DCENT_DIR; /* ** for files, change to the parent directory ** for directories, change to the directory itself */ if (preferred_type == DCENT_FILE) new_cwd = dirname(path); else new_cwd = path; /* ** if we're not already in the desired directory, ** save original path and send CWD request */ if (strcmp(ftp_getcwd(ftp), new_cwd) == 0) in_desired_cwd = 1; else { strlcpy(orig_path, ftp_getcwd(ftp), sizeof(orig_path)); if (ftp_chdir(ftp, new_cwd) == -1) { #ifdef DEBUG printf(" _ftp_list(): ftp_chdir(\"%s\"): %s\n", new_cwd, strerror(errno)); #endif /* ** if we're looking for a directory but we can't ** CWD to the specified path, then fail ** ** FIXME: is this too much of an assumption? ** is it possible for CWD to fail for a directory, ** but still have LIST succeed? */ if (preferred_type == DCENT_DIR) return -1; /* ** also fail immediately if we got ECONNRESET */ if (errno == ECONNRESET) return -1; /* ** it could be EACCES or ENOENT ** (and if we got ENOENT, it might really be an ** ENOTDIR situation) ** either way, we'll try passing the path name to ** the LIST request */ /* ** clear buffer so that we know not to CWD back below */ orig_path[0] = '\0'; } else in_desired_cwd = 1; } /* ** construct list command */ strlcpy(buf, "LIST", sizeof(buf)); /* ** add appropriate ls(1) options, ** unless server doesn't grok them */ if (! BIT_ISSET(flags, FL_TRY_NO_LS_OPTS) && ! BIT_ISSET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS)) strlcat(buf, (preferred_type == DCENT_FILE ? " -ld" : " -la"), sizeof(buf)); /* ** add the pathname to be listed */ if (preferred_type == DCENT_FILE) { strlcat(buf, " ", sizeof(buf)); /* ** if we're already in the parent directory, just use ** the basename; otherwise, use the full path */ if (in_desired_cwd) strlcat(buf, basename(path), sizeof(buf)); else strlcat(buf, path, sizeof(buf)); } else /* preferred_type == DCENT_DIR */ { /* ** if we're not already in the directory to be listed, ** use the full path; otherwise, don't use any argument */ if (! in_desired_cwd) { strlcat(buf, " ", sizeof(buf)); strlcat(buf, path, sizeof(buf)); } } open_data_connection: /* for MLSD or LIST requests, use a data connection */ if (_ftp_data_connect(ftp, "%s", buf) == -1) { retval = -1; goto done; } /* ** if there are send and receive hooks set for the control ** connection, use them for the file listing as well */ fget_netio_set_options(ftp->ftp_data, NETIO_OPT_SEND_HOOK, ftp->ftp_send_hook, NETIO_OPT_RECV_HOOK, ftp->ftp_recv_hook, NETIO_OPT_HOOK_HANDLE, ftp, NETIO_OPT_HOOK_DATA, ftp->ftp_hook_data, 0); /* read each line */ while (fget_netio_read_line(ftp->ftp_data, ftp->ftp_io_timeout, buf, sizeof(buf)) > 0) { /* choose a list parsing function, if needed */ if (ftp->ftp_list_parse_function == NULL) _ftp_list_parse_choose(ftp, buf); /* allocate structure for new entry */ fip = (file_info_t *)calloc(1, sizeof(file_info_t)); if (fip == NULL) return -1; /* ** call the parsing function to generate ** an ftpstat structure */ switch ((*(ftp->ftp_list_parse_function))(ftp, buf, fip)) { case FLP_ERROR: /* error (sets errno) */ save_errno = errno; free(fip); _ftp_data_close(ftp); errno = save_errno; retval = -1; goto done; case FLP_IGNORE: default: /* ignore line */ free(fip); numignored++; break; case FLP_VALID: /* normal file */ fget_hash_add(files_h, fip); } } if (_ftp_data_close(ftp) == -1) { /* if something other than 226 was returned, return ENOENT */ if (errno == EINVAL) errno = ENOENT; retval = -1; goto done; } /* no lines returned, which means ENOENT */ if ((fget_hash_nents(files_h) + numignored) == 0) { errno = ENOENT; retval = -1; goto done; } /* no errors, so return number of entries found */ retval = fget_hash_nents(files_h); done: /* ** for MLSD, return immediately ** (no need to try again with ls(1) options ** or CWD back to where we belong) */ if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST) && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST)) return retval; /* ** if we failed with ls(1) options, try again without them */ if (! BIT_ISSET(flags, FL_TRY_NO_LS_OPTS) && ! BIT_ISSET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS) && retval == -1 && errno != ECONNRESET) { retval = _ftp_list(ftp, path, files_h, preferred_type, flags|FL_TRY_NO_LS_OPTS); /* if it succeeded, don't use ls(1) options from now on */ if (retval == 0) BIT_SET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS); } /* ** change back to original working directory */ if (orig_path[0] != '\0' && (retval != -1 || errno != ECONNRESET) && ftp_chdir(ftp, orig_path) == -1) return -1; #ifdef DEBUG printf("<== _ftp_list(): returning %d\n", retval); #endif return retval; }