/* list.c - twoftpd routines to handle list/nlst commands * Copyright (C) 2008 Bruce Guenter * * 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 of the License, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include "twoftpd.h" #include "backend.h" #include #include #include int list_options; static int list_long; static int list_flags; static int mode_nlst; static const char* mode2str(int mode) { static char buf[11]; buf[0] = S_ISREG(mode) ? '-' : S_ISDIR(mode) ? 'd' : S_ISCHR(mode) ? 'c' : S_ISBLK(mode) ? 'b' : S_ISFIFO(mode) ? 'p' : S_ISSOCK(mode) ? 's' : '?'; buf[1] = (mode & 0400) ? 'r' : '-'; buf[2] = (mode & 0200) ? 'w' : '-'; buf[3] = (mode & 04000) ? (mode & 0100) ? 's' : 'S' : (mode & 0100) ? 'x' : '-'; buf[4] = (mode & 0040) ? 'r' : '-'; buf[5] = (mode & 0020) ? 'w' : '-'; buf[6] = (mode & 02000) ? (mode & 0010) ? 's' : 'S' : (mode & 0010) ? 'x' : '-'; buf[7] = (mode & 0004) ? 'r' : '-'; buf[8] = (mode & 0002) ? 'w' : '-'; buf[9] = (mode & 01000) ? (mode & 0001) ? 't' : 'T' : (mode & 0001) ? 'x' : '-'; buf[10] = 0; return buf; } static int str_catmode(str* s, int mode) { return str_catb(s, mode2str(mode), 10); } static int str_catowner(str* s, uid_t owner) { const char* name; unsigned len; if (owner == uid) name = user, len = user_len; else name = "somebody", len = 8; if (!str_catb(s, name, len)) return 0; while (len++ < MAX_NAME_LEN) if (!str_catc(s, SPACE)) return 0; return 1; } static int str_catgroup(str* s, gid_t g) { const char* name; unsigned len; if (g == gid) name = group, len = group_len; else name = "somegrp", len = 7; if (!str_catb(s, name, len)) return 0; while (len++ < MAX_NAME_LEN) if (!str_catc(s, SPACE)) return 0; return 1; } static int str_cattime(str* s, time_t then) { struct tm* tm; time_t year; char buf[16]; unsigned len; year = now - 365/2*24*60*60; tm = localtime(&then); if (then > year) len = strftime(buf, sizeof buf - 1, "%b %e %H:%M", tm); else len = strftime(buf, sizeof buf - 1, "%b %e %Y", tm); return str_catb(s, buf, len); } static int str_catstat(str* s, const struct stat* st) { if (st) return str_catmode(s, st->st_mode) && str_catb(s, " 1 ", 6) && str_catowner(s, st->st_uid) && str_catc(s, SPACE) && str_catgroup(s, st->st_gid) && str_catc(s, SPACE) && str_catuwll(s, st->st_size, 8, ' ') && str_catc(s, SPACE) && str_cattime(s, st->st_mtime) && str_catc(s, SPACE); else return str_cats(s, "??????????" " ? " "???????? " "???????? " " ? " "??? ?? ????? "); } static int str_catflags(str* s, const struct stat* st) { if (st) { int ch; int mode = st->st_mode; ch = 0; if (S_ISDIR(mode)) ch = '/'; else if (S_ISFIFO(mode)) ch = '|'; else if (S_ISSOCK(mode)) ch = '='; else if (list_flags == 'F' && mode & 0111) ch = '*'; if (ch) return str_catc(s, ch); } return 1; } static int str_catfn(str* s, const char* fn, int striplen) { if (striplen < 0) { if (!str_catc(s, '/')) return 0; striplen = 0; } return str_cats(s, fn + striplen); } static char send_buf[8192]; static unsigned long send_used; static int send_line(int out, const str* s) { int result; if (send_used + s->len > sizeof send_buf) { if ((result = netwrite(out, send_buf, send_used, timeout * 1000)) != 0) return result; send_used = 0; } if (s->len > sizeof send_buf) return netwrite(out, s->s, s->len, timeout * 1000); memcpy(send_buf + send_used, s->s, s->len); send_used += s->len; return 0; } static str entries; static int list_entries(long count, int striplen) { static str line; int out; int result; unsigned long bytes_out; struct stat statbuf; struct stat* statptr; const char* filename = entries.s; int need_stat = list_long || list_flags; if (mode_nlst && count < 0) return respond(550, 1, "No such file or directory"); if ((out = make_out_connection()) == -1) return 1; send_used = 0; for (bytes_out = 0; count > 0; --count, filename += strlen(filename)+1) { statptr = 0; if (need_stat) { if (stat(filename, &statbuf) == -1) { if (errno == ENOENT) continue; } else statptr = &statbuf; } line.len = 0; if ((list_long && !str_catstat(&line, statptr)) || !str_catfn(&line, filename, striplen) || (list_flags && !str_catflags(&line, statptr)) || !str_cats(&line, CRLF)) { close(out); return respond_xferresult(-1, bytes_out, 1); } if ((result = send_line(out, &line)) != 0) return respond_xferresult(result, bytes_out, 1); bytes_out += line.len; } if (send_used > 0) { if ((result = netwrite(out, send_buf, send_used, timeout * 1000)) != 0) { socket_uncork(out); close(out); return respond_xferresult(result, bytes_out, 1); } } return respond_xferresult(close(out), bytes_out, 1); } static int list_dir() { long count; DIR* dir; direntry* entry; /* Turning "/" into "/." simplifies several other steps below. */ if (fullpath.len == 1) if (!str_copys(&fullpath, "/.")) return respond_internal_error(); if ((dir = opendir(fullpath.s+1)) == 0) return respond_syserr(550, "Could not open directory"); entries.len = 0; count = 0; while ((entry = readdir(dir)) != 0) { /* Always skip the "." and ".." entries. Skip all other ".*" * entries if nodotfiles was set. */ if (entry->d_name[0] != '.' || (!(entry->d_name[1] == 0 || (entry->d_name[1] == '.' && entry->d_name[2] == 0)) && (list_options & PATH_MATCH_DOTFILES) != 0)) { /* Add the entry to the list, skipping the "/" separator if none * is needed */ if (!str_catb(&entries, fullpath.s+1, fullpath.len-1) || !str_catc(&entries, '/') || !str_catb(&entries, entry->d_name, strlen(entry->d_name)+1)) { closedir(dir); return respond_internal_error(); } ++count; } } closedir(dir); if (!str_sort(&entries, 0, count, 0)) return respond_internal_error(); return list_entries(count, fullpath.len); } static int handle_listing(int longfmt) { struct stat statbuf; long count; int striplen; mode_nlst = !longfmt; list_long = longfmt; list_flags = 0; if (req_param) { while (*req_param == '-') { ++req_param; if (*req_param == SPACE) break; while (*req_param && *req_param != SPACE) { char tmp[2]; switch (*req_param) { case 'a': break; /* Listing all files is */ case 'A': break; /* not controlled by client */ case 'L': break; /* We already dereference symlinks */ case '1': break; /* We already list in a single column */ case 'F': case 'p': list_flags = *req_param; break; case 'l': list_long = 1; break; default: tmp[0] = *req_param; tmp[1] = 0; return respond_start(550, 1) && respond_str("Unknown list option: ") && respond_str(tmp) && respond_end(); } ++req_param; } if (*req_param == SPACE) ++req_param; } } if (!req_param || !*req_param) req_param = "."; if (path_contains(req_param, "..")) return respond(553, 1, "Paths containing '..' not allowed."); /* Prefix the requested path with CWD, and strip it after */ if (!qualify_validate(req_param)) return 1; if (fullpath.len == 1) return list_dir(); if ((count = path_match(fullpath.s+1, &entries, list_options)) == -1) return respond_internal_error(); striplen = (req_param[0] == '/') ? -1 : (cwd.len == 1) ? 0 : cwd.len; if (count <= 1) { /* If no entries were matched, try the literal path. */ if (count == 0) if (!str_copyb(&entries, fullpath.s+1, fullpath.len)) return respond_internal_error(); if (stat(entries.s, &statbuf) == -1) { if (errno == ENOENT) return respond(550, 1, "File or directory does not exist."); else return respond(550, 1, "Could not access file."); } else if (S_ISDIR(statbuf.st_mode)) { if (!str_copys(&fullpath, "/") || !str_catb(&fullpath, entries.s, entries.len-1)) return respond_internal_error(); return list_dir(); } } return list_entries(count, striplen); } int handle_list(void) { return handle_listing(1); } int handle_nlst(void) { return handle_listing(0); }