proftpd-mod-vroot-0.9.4/000077500000000000000000000000001302754346600151405ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/README.md000066400000000000000000000001221302754346600164120ustar00rootroot00000000000000proftpd-mod_vroot ================= ProFTPD module that creates "virtual" chrootsproftpd-mod-vroot-0.9.4/mod_vroot.c000066400000000000000000001353461302754346600173300ustar00rootroot00000000000000/* * ProFTPD: mod_vroot -- a module implementing a virtual chroot capability * via the FSIO API * * Copyright (c) 2002-2014 TJ Saunders * * 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. * * As a special exemption, TJ Saunders and other respective copyright holders * give permission to link this program with OpenSSL, and distribute the * resulting executable, without including the source code for OpenSSL in the * source distribution. * * This is mod_vroot, contrib software for proftpd 1.2 and above. * For more information contact TJ Saunders . * * $Id: mod_vroot.c,v 1.24 2011/01/11 02:41:10 tj Exp tj $ */ #include "conf.h" #include "privs.h" #define MOD_VROOT_VERSION "mod_vroot/0.9.4" /* Make sure the version of proftpd is as necessary. */ #if PROFTPD_VERSION_NUMBER < 0x0001030406 # error "ProFTPD 1.3.4b or later required" #endif static const char *vroot_log = NULL; static int vroot_logfd = -1; static char vroot_cwd[PR_TUNABLE_PATH_MAX + 1]; static char vroot_base[PR_TUNABLE_PATH_MAX + 1]; static size_t vroot_baselen = 0; static unsigned char vroot_engine = FALSE; static pool *vroot_alias_pool = NULL; static pr_table_t *vroot_aliastab = NULL; static pool *vroot_dir_pool = NULL; static pr_table_t *vroot_dirtab = NULL; #if PROFTPD_VERSION_NUMBER >= 0x0001030407 static int vroot_use_mkdtemp = FALSE; #endif /* ProFTPD 1.3.4c or later */ static unsigned int vroot_opts = 0; #define VROOT_OPT_ALLOW_SYMLINKS 0x0001 /* vroot_realpath() flags */ #define VROOT_REALPATH_FL_ABS_PATH 0x001 /* vroot_lookup_path() flags */ #define VROOT_LOOKUP_FL_NO_ALIASES 0x0001 static const char *trace_channel = "vroot"; static int vroot_is_alias(const char *); /* Support routines note: some of these support functions are borrowed from * pure-ftpd. */ static void strmove(register char *dst, register const char *src) { if (dst == NULL || src == NULL) { return; } while (*src != 0) { *dst++ = *src++; } *dst = 0; } static void vroot_clean_path(char *path) { char *p = NULL; if (path == NULL || *path == 0) { return; } while ((p = strstr(path, "//")) != NULL) { strmove(p, p + 1); } while ((p = strstr(path, "/./")) != NULL) { strmove(p, p + 2); } while (strncmp(path, "../", 3) == 0) { path += 3; } p = strstr(path, "/../"); if (p != NULL) { if (p == path) { while (strncmp(path, "/../", 4) == 0) { strmove(path, path + 3); } p = strstr(path, "/../"); } while (p != NULL) { char *next_elem = p + 4; if (p != path && *p == '/') { p--; } while (p != path && *p != '/') { p--; } if (*p == '/') { p++; } strmove(p, next_elem); p = strstr(path, "/../"); } } p = path; if (*p == '.') { p++; if (*p == '\0') { return; } if (*p == '/') { while (*p == '/') { p++; } strmove(path, p); } } if (*p == '\0') { return; } p = path + strlen(path) - 1; if (*p != '.' || p == path) { return; } p--; if (*p == '/' || p == path) { p[1] = '\0'; return; } if (*p != '.' || p == path) { return; } p--; if (*p != '/') { return; } *p = '\0'; p = strrchr(path, '/'); if (p == NULL) { *path = '/'; path[1] = '\0'; return; } p[1] = '\0'; } static char *vroot_realpath(pool *p, const char *path, int flags) { char *real_path = NULL; size_t real_pathlen; if (flags & VROOT_REALPATH_FL_ABS_PATH) { /* If not an absolute path, prepend the current location. */ if (*path != '/') { real_path = pdircat(p, pr_fs_getvwd(), path, NULL); } else { real_path = pstrdup(p, path); } } else { real_path = pstrdup(p, path); } vroot_clean_path(real_path); /* If the given path ends in a slash, remove it. The handling of * VRootAliases is sensitive to such things. */ real_pathlen = strlen(real_path); if (real_pathlen > 1 && real_path[real_pathlen-1] == '/') { real_path[real_pathlen-1] = '\0'; real_pathlen--; } return real_path; } static int vroot_lookup_path(pool *p, char *path, size_t pathlen, const char *dir, int flags, char **alias_path) { char buf[PR_TUNABLE_PATH_MAX + 1], *bufp = NULL; memset(buf, '\0', sizeof(buf)); memset(path, '\0', pathlen); if (strcmp(dir, ".") != 0) { sstrncpy(buf, dir, sizeof(buf)); } else { sstrncpy(buf, pr_fs_getcwd(), sizeof(buf)); } vroot_clean_path(buf); bufp = buf; if (strncmp(bufp, vroot_base, vroot_baselen) == 0) { bufp += vroot_baselen; } loop: pr_signals_handle(); if (bufp[0] == '.' && bufp[1] == '.' && (bufp[2] == '\0' || bufp[2] == '/')) { char *tmp = NULL; tmp = strrchr(path, '/'); if (tmp != NULL) { *tmp = '\0'; } else { *path = '\0'; } if (strncmp(path, vroot_base, vroot_baselen) == 0 || path[vroot_baselen] != '/') { snprintf(path, pathlen, "%s/", vroot_base); } if (bufp[0] == '.' && bufp[1] == '.' && bufp[2] == '/') { bufp += 3; goto loop; } } else if (*bufp == '/') { snprintf(path, pathlen, "%s/", vroot_base); bufp += 1; goto loop; } else if (*bufp != '\0') { size_t buflen, tmplen; char *ptr = NULL; ptr = strstr(bufp, ".."); if (ptr != NULL) { size_t ptrlen; /* We need to watch for path components/filenames which legitimately * contain two or more periods in addition to other characters. */ ptrlen = strlen(ptr); if (ptrlen >= 3) { /* If this ".." occurrence is the start of the buffer AND the next * character after the ".." is a slash, then deny it. */ if (ptr == bufp && ptr[2] == '/') { errno = EPERM; return -1; } /* If this ".." occurrence is NOT the start of the buffer AND the * characters preceeding and following the ".." are slashes, then * deny it. */ if (ptr != bufp && ptr[-1] == '/' && ptr[2] == '/') { errno = EPERM; return -1; } } } buflen = strlen(bufp) + 1; tmplen = strlen(path); if (tmplen + buflen >= pathlen) { errno = ENAMETOOLONG; return -1; } path[tmplen] = '/'; memcpy(path + tmplen + 1, bufp, buflen); } /* Clean any unnecessary characters added by the above processing. */ vroot_clean_path(path); if (!(flags & VROOT_LOOKUP_FL_NO_ALIASES)) { /* Check to see if this path is an alias; if so, return the real path. */ if (vroot_aliastab != NULL) { char *start_ptr = NULL, *end_ptr = NULL, *src_path = NULL; /* buf is used here for storing the "suffix", to be appended later when * aliases are found. */ bufp = buf; start_ptr = path; while (start_ptr != NULL) { char *ptr = NULL; pr_signals_handle(); pr_trace_msg(trace_channel, 15, "checking for alias for '%s'", start_ptr); src_path = pr_table_get(vroot_aliastab, start_ptr, NULL); if (src_path != NULL) { pr_trace_msg(trace_channel, 15, "found '%s' for alias '%s'", src_path, start_ptr); /* If the caller provided a pointer for wanting to know the full * alias path (not the true path), then fill that pointer. */ if (alias_path != NULL) { if (end_ptr != NULL) { *alias_path = pdircat(p, start_ptr, end_ptr + 1, NULL); } else { *alias_path = pstrdup(p, start_ptr); } pr_trace_msg(trace_channel, 19, "using alias path '%s' for '%s'", *alias_path, start_ptr); } sstrncpy(path, src_path, pathlen); if (end_ptr != NULL) { /* Now tack on our suffix from the scratchpad. */ sstrcat(path, bufp, pathlen); } break; } ptr = strrchr(start_ptr, '/'); if (end_ptr != NULL) { *end_ptr = '/'; } if (ptr == NULL) { break; } /* If this is the start of the path, we're done. */ if (ptr == start_ptr) { break; } /* Store the suffix in the buf scratchpad. */ sstrncpy(buf, ptr, sizeof(buf)); end_ptr = ptr; *end_ptr = '\0'; } } } return 0; } static int vroot_is_alias(const char *path) { if (pr_table_get(vroot_aliastab, path, 0) != NULL) { return 0; } errno = ENOENT; return -1; } static int handle_vroot_alias(void) { config_rec *c; pool *tmp_pool = NULL; /* Handle any VRootAlias settings. */ tmp_pool = make_sub_pool(session.pool); c = find_config(main_server->conf, CONF_PARAM, "VRootAlias", FALSE); while (c) { char src_path[PR_TUNABLE_PATH_MAX+1], dst_path[PR_TUNABLE_PATH_MAX+1], *ptr; pr_signals_handle(); /* XXX Note that by using vroot_lookup_path(), we assume a POST_CMD * invocation. Looks like VRootAlias might end up being incompatible * with VRootServerRoot. */ memset(src_path, '\0', sizeof(src_path)); ptr = c->argv[0]; /* Check for any expandable variables. */ ptr = path_subst_uservar(tmp_pool, &ptr); sstrncpy(src_path, ptr, sizeof(src_path)-1); vroot_clean_path(src_path); ptr = c->argv[1]; /* Check for any expandable variables. */ ptr = path_subst_uservar(tmp_pool, &ptr); ptr = dir_best_path(tmp_pool, ptr); vroot_lookup_path(NULL, dst_path, sizeof(dst_path)-1, ptr, VROOT_LOOKUP_FL_NO_ALIASES, NULL); if (vroot_alias_pool == NULL) { vroot_alias_pool = make_sub_pool(session.pool); pr_pool_tag(vroot_alias_pool, "VRoot Alias Pool"); vroot_aliastab = pr_table_alloc(vroot_alias_pool, 0); } if (pr_table_add(vroot_aliastab, pstrdup(vroot_alias_pool, dst_path), pstrdup(vroot_alias_pool, src_path), 0) < 0) { /* Make a slightly better log message when there is an alias collision. */ if (errno == EEXIST) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "VRootAlias already configured for '%s', ignoring bad alias", (char *) c->argv[1]); } else { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "error stashing VRootAlias '%s': %s", dst_path, strerror(errno)); } } else { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "aliased '%s' to real path '%s'", dst_path, src_path); } c = find_config_next(c, c->next, CONF_PARAM, "VRootAlias", FALSE); } destroy_pool(tmp_pool); return 0; } /* FS callbacks */ static int vroot_stat(pr_fs_t *fs, const char *stat_path, struct stat *st) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *path = NULL; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ return stat(stat_path, st); } tmp_pool = make_sub_pool(session.pool); path = vroot_realpath(tmp_pool, stat_path, 0); if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) { destroy_pool(tmp_pool); return -1; } res = stat(vpath, st); destroy_pool(tmp_pool); return res; } static int vroot_lstat(pr_fs_t *fs, const char *orig_path, struct stat *st) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *path = NULL; size_t pathlen = 0; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ return lstat(orig_path, st); } tmp_pool = make_sub_pool(session.pool); path = pstrdup(tmp_pool, orig_path); vroot_clean_path(path); /* If the given path ends in a slash, remove it. The handling of * VRootAliases is sensitive to such things. */ pathlen = strlen(path); if (pathlen > 1 && path[pathlen-1] == '/') { path[pathlen-1] = '\0'; pathlen--; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) { destroy_pool(tmp_pool); return -1; } if ((vroot_opts & VROOT_OPT_ALLOW_SYMLINKS) || vroot_is_alias(path) == 0) { res = lstat(vpath, st); if (res < 0) { destroy_pool(tmp_pool); return -1; } res = stat(vpath, st); destroy_pool(tmp_pool); return res; } res = lstat(vpath, st); destroy_pool(tmp_pool); return res; } static int vroot_rename(pr_fs_t *fs, const char *rnfm, const char *rnto) { int res; char vpath1[PR_TUNABLE_PATH_MAX + 1], vpath2[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = rename(rnfm, rnto); return res; } if (vroot_lookup_path(NULL, vpath1, sizeof(vpath1)-1, rnfm, 0, NULL) < 0) { return -1; } if (vroot_lookup_path(NULL, vpath2, sizeof(vpath2)-1, rnto, 0, NULL) < 0) { return -1; } res = rename(vpath1, vpath2); return res; } static int vroot_unlink(pr_fs_t *fs, const char *path) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (vroot_base[0] == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = unlink(path); return res; } /* Do not allow deleting of aliased files/directories; the aliases may only * exist for this user/group. */ if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, VROOT_LOOKUP_FL_NO_ALIASES, NULL) < 0) { return -1; } if (vroot_is_alias(vpath) == 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "denying delete of '%s' because it is a VRootAlias", vpath); errno = EACCES; return -1; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) { return -1; } res = unlink(vpath); return res; } static int vroot_open(pr_fh_t *fh, const char *path, int flags) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = open(path, flags, PR_OPEN_MODE); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = open(vpath, flags, PR_OPEN_MODE); return res; } static int vroot_creat(pr_fh_t *fh, const char *path, mode_t mode) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = creat(path, mode); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = creat(vpath, mode); return res; } static int vroot_link(pr_fs_t *fs, const char *path1, const char *path2) { int res; char vpath1[PR_TUNABLE_PATH_MAX + 1], vpath2[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = link(path1, path2); return res; } if (vroot_lookup_path(NULL, vpath1, sizeof(vpath1)-1, path1, 0, NULL) < 0) { return -1; } if (vroot_lookup_path(NULL, vpath2, sizeof(vpath2)-1, path2, 0, NULL) < 0) { return -1; } res = link(vpath1, vpath2); return res; } static int vroot_symlink(pr_fs_t *fs, const char *path1, const char *path2) { int res; char vpath1[PR_TUNABLE_PATH_MAX + 1], vpath2[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = symlink(path1, path2); return res; } if (vroot_lookup_path(NULL, vpath1, sizeof(vpath1)-1, path1, 0, NULL) < 0) { return -1; } if (vroot_lookup_path(NULL, vpath2, sizeof(vpath2)-1, path2, 0, NULL) < 0) { return -1; } res = symlink(vpath1, vpath2); return res; } static int vroot_readlink(pr_fs_t *fs, const char *readlink_path, char *buf, size_t max) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *path = NULL, *alias_path = NULL; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ return readlink(readlink_path, buf, max); } /* In order to find any VRootAlias paths, we need to use the full path. * However, if we do NOT find any VRootAlias, then we do NOT want to use * the full path. */ tmp_pool = make_sub_pool(session.pool); path = vroot_realpath(tmp_pool, readlink_path, VROOT_REALPATH_FL_ABS_PATH); if (vroot_lookup_path(tmp_pool, vpath, sizeof(vpath)-1, path, 0, &alias_path) < 0) { destroy_pool(tmp_pool); return -1; } if (alias_path == NULL) { if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, readlink_path, 0, NULL) < 0) { destroy_pool(tmp_pool); return -1; } } res = readlink(vpath, buf, max); destroy_pool(tmp_pool); return res; } static int vroot_truncate(pr_fs_t *fs, const char *path, off_t length) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = truncate(path, length); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = truncate(vpath, length); return res; } static int vroot_chmod(pr_fs_t *fs, const char *path, mode_t mode) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = chmod(path, mode); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = chmod(vpath, mode); return res; } static int vroot_chown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = chown(path, uid, gid); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = chown(vpath, uid, gid); return res; } #if PROFTPD_VERSION_NUMBER >= 0x0001030407 static int vroot_lchown(pr_fs_t *fs, const char *path, uid_t uid, gid_t gid) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = lchown(path, uid, gid); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = lchown(vpath, uid, gid); return res; } #endif /* ProFTPD 1.3.4c or later */ static int vroot_chroot(pr_fs_t *fs, const char *path) { char *chroot_path = "/", *tmp = NULL; config_rec *c; if (!path || *path == '\0') { errno = EINVAL; return -1; } memset(vroot_base, '\0', sizeof(vroot_base)); if (path[0] == '/' && path[1] == '\0') { /* chrooting to '/', nothing needs to be done. */ return 0; } c = find_config(main_server->conf, CONF_PARAM, "VRootServerRoot", FALSE); if (c) { int res; char *server_root, *ptr = NULL; server_root = c->argv[0]; /* If the last character in the configured path is a slash, remove * it temporarily. */ if (server_root[strlen(server_root)-1] == '/') { ptr = &(server_root[strlen(server_root)-1]); *ptr = '\0'; } /* Now, make sure that the given path is below the configured * VRootServerRoot. If so, then we perform a real chroot to the * VRootServerRoot directory, then use vroots from there. */ res = strncmp(path, server_root, strlen(server_root)); if (ptr != NULL) { *ptr = '/'; } if (res == 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "chroot path '%s' within VRootServerRoot '%s', " "chrooting to VRootServerRoot", path, server_root); if (chroot(server_root) < 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "error chrooting to VRootServerRoot '%s': %s", server_root, strerror(errno)); return -1; } pr_fs_clean_path(path + strlen(server_root), vroot_base, sizeof(vroot_base)); chroot_path = server_root; } else { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "chroot path '%s' is not within VRootServerRoot '%s', " "not chrooting to VRootServerRoot", path, server_root); pr_fs_clean_path(path, vroot_base, sizeof(vroot_base)); } } else { pr_fs_clean_path(path, vroot_base, sizeof(vroot_base)); } tmp = vroot_base; /* Advance to the end of the path. */ while (*tmp != '\0') { tmp++; } for (;;) { tmp--; if (tmp == vroot_base || *tmp != '/') { break; } *tmp = '\0'; } vroot_baselen = strlen(vroot_base); if (vroot_baselen >= sizeof(vroot_cwd)) { errno = ENAMETOOLONG; return -1; } session.chroot_path = pstrdup(session.pool, chroot_path); return 0; } static int vroot_chdir(pr_fs_t *fs, const char *path) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *vpathp = NULL, *alias_path = NULL; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = chdir(path); return res; } tmp_pool = make_sub_pool(session.pool); if (vroot_lookup_path(tmp_pool, vpath, sizeof(vpath)-1, path, 0, &alias_path) < 0) { destroy_pool(tmp_pool); return -1; } res = chdir(vpath); if (res < 0) { destroy_pool(tmp_pool); return -1; } if (alias_path != NULL) { vpathp = alias_path; } else { vpathp = vpath; } if (strncmp(vpathp, vroot_base, vroot_baselen) == 0) { pr_trace_msg(trace_channel, 19, "adjusting vpath '%s' to account for vroot base '%s' (%lu)", vpathp, vroot_base, (unsigned long) vroot_baselen); vpathp += vroot_baselen; } pr_trace_msg(trace_channel, 19, "setting current working directory to '%s'", vpathp); /* pr_fs_setcwd() makes a copy of the argument path, so we can safely * destroy our temporary pool. */ pr_fs_setcwd(vpathp); destroy_pool(tmp_pool); return 0; } static int vroot_utimes(pr_fs_t *fs, const char *utimes_path, struct timeval *tvs) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *path = NULL; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = utimes(utimes_path, tvs); return res; } tmp_pool = make_sub_pool(session.pool); path = vroot_realpath(tmp_pool, utimes_path, VROOT_REALPATH_FL_ABS_PATH); if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) { destroy_pool(tmp_pool); return -1; } res = utimes(vpath, tvs); destroy_pool(tmp_pool); return res; } static struct dirent *vroot_dent = NULL; static size_t vroot_dentsz = 0; /* On most systems, dirent.d_name is an array into which we can copy the * name we want. * * However, on other systems (e.g. Solaris 2), dirent.d_name is an array size * of 1. This approach makes use of the fact that the d_name member is the * last member of the struct, meaning that the actual size is variable. * * We need to Do The Right Thing(tm) in either case. */ static size_t vroot_dent_namesz = 0; static array_header *vroot_dir_aliases = NULL; static int vroot_dir_idx = -1; static int vroot_alias_dirscan(const void *key_data, size_t key_datasz, void *value_data, size_t value_datasz, void *user_data) { const char *alias_path = NULL, *dir_path = NULL, *real_path = NULL; char *ptr = NULL; size_t dir_pathlen; alias_path = key_data; real_path = value_data; dir_path = user_data; ptr = strrchr(alias_path, '/'); if (ptr == NULL) { /* This is not likely to happen, but if it does, simply move to the * next item in the table. */ return 0; } /* If the dir path and the real path are the same, skip this alias. * Otherwise we end up with an extraneous entry in the directory listing. */ if (strcmp(real_path, dir_path) == 0) { return 0; } dir_pathlen = strlen(dir_path); if (strncmp(dir_path, alias_path, dir_pathlen) == 0) { pr_trace_msg(trace_channel, 17, "adding VRootAlias '%s' to list of aliases contained in '%s'", alias_path, dir_path); *((char **) push_array(vroot_dir_aliases)) = pstrdup(vroot_dir_pool, ptr + 1); } return 0; } static int vroot_dirtab_keycmp_cb(const void *key1, size_t keysz1, const void *key2, size_t keysz2) { unsigned long k1, k2; memcpy(&k1, key1, sizeof(k1)); memcpy(&k2, key2, sizeof(k2)); return (k1 == k2 ? 0 : 1); } static unsigned int vroot_dirtab_hash_cb(const void *key, size_t keysz) { unsigned long h; memcpy(&h, key, sizeof(h)); return h; } static void *vroot_opendir(pr_fs_t *fs, const char *orig_path) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1], *path = NULL; void *dirh = NULL; struct stat st; size_t pathlen = 0; pool *tmp_pool = NULL; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ dirh = opendir(orig_path); return dirh; } tmp_pool = make_sub_pool(session.pool); /* If the given path ends in a slash, remove it. The handling of * VRootAliases is sensitive to trailing slashes. */ path = pstrdup(tmp_pool, orig_path); vroot_clean_path(path); /* If the given path ends in a slash, remove it. The handling of * VRootAliases is sensitive to such things. */ pathlen = strlen(path); if (pathlen > 1 && path[pathlen-1] == '/') { path[pathlen-1] = '\0'; pathlen--; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) { destroy_pool(tmp_pool); return NULL; } /* Check if the looked-up vpath is a symlink; we may need to resolve any * links ourselves, rather than assuming that the system opendir(3) can * handle it. */ res = vroot_lstat(fs, vpath, &st); while (res == 0 && S_ISLNK(st.st_mode)) { char data[PR_TUNABLE_PATH_MAX + 1]; pr_signals_handle(); memset(data, '\0', sizeof(data)); res = vroot_readlink(fs, vpath, data, sizeof(data)-1); if (res < 0) break; data[res] = '\0'; sstrncpy(vpath, data, sizeof(vpath)); res = vroot_lstat(fs, vpath, &st); } dirh = opendir(vpath); if (dirh == NULL) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "error opening virtualized directory '%s' (from '%s'): %s", vpath, path, strerror(errno)); destroy_pool(tmp_pool); return NULL; } if (vroot_aliastab != NULL) { unsigned long *cache_dirh = NULL; if (vroot_dirtab == NULL) { vroot_dir_pool = make_sub_pool(session.pool); pr_pool_tag(vroot_dir_pool, "VRoot Directory Pool"); vroot_dirtab = pr_table_alloc(vroot_dir_pool, 0); /* Since this table will use DIR pointers as keys, we want to override * the default hashing and key comparison functions used. */ pr_table_ctl(vroot_dirtab, PR_TABLE_CTL_SET_KEY_HASH, vroot_dirtab_hash_cb); pr_table_ctl(vroot_dirtab, PR_TABLE_CTL_SET_KEY_CMP, vroot_dirtab_keycmp_cb); } cache_dirh = palloc(vroot_dir_pool, sizeof(unsigned long)); *cache_dirh = (unsigned long) dirh; if (pr_table_kadd(vroot_dirtab, cache_dirh, sizeof(unsigned long), pstrdup(vroot_dir_pool, vpath), strlen(vpath) + 1) < 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "error stashing path '%s' (key %p) in directory table: %s", vpath, dirh, strerror(errno)); } else { vroot_dir_aliases = make_array(vroot_dir_pool, 0, sizeof(char *)); res = pr_table_do(vroot_aliastab, vroot_alias_dirscan, vpath, PR_TABLE_DO_FL_ALL); if (res < 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "error doing dirscan on aliases table: %s", strerror(errno)); } else { register unsigned int i; (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "found %d %s in directory '%s'", vroot_dir_aliases->nelts, vroot_dir_aliases->nelts != 1 ? "VRootAliases" : "VRootAlias", vpath); vroot_dir_idx = 0; for (i = 0; i < vroot_dir_aliases->nelts; i++) { char **elts = vroot_dir_aliases->elts; (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "'%s' aliases: [%u] %s", vpath, i, elts[i]); } } } } destroy_pool(tmp_pool); return dirh; } static struct dirent *vroot_readdir(pr_fs_t *fs, void *dirh) { struct dirent *dent = NULL; next_dent: dent = readdir((DIR *) dirh); if (vroot_dir_aliases != NULL) { char **elts; elts = vroot_dir_aliases->elts; if (dent != NULL) { register unsigned int i; /* If this dent has the same name as an alias, the alias wins. * This is similar to a mounted filesystem, which hides any directories * underneath the mount point for the duration of the mount. */ /* Yes, this is a linear scan; it assumes that the number of configured * aliases for a site will be relatively few. Should this assumption * not be borne out by reality, then we should switch to using a * table, not an array_header, for storing the aliased paths. */ for (i = 0; i < vroot_dir_aliases->nelts; i++) { if (strcmp(dent->d_name, elts[i]) == 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "skipping directory entry '%s', as it is aliased", dent->d_name); goto next_dent; } } } else { if (vroot_dir_idx < 0 || vroot_dir_idx >= vroot_dir_aliases->nelts) { return NULL; } memset(vroot_dent, 0, vroot_dentsz); if (vroot_dent_namesz == 0) { sstrncpy(vroot_dent->d_name, elts[vroot_dir_idx++], sizeof(vroot_dent->d_name)); } else { sstrncpy(vroot_dent->d_name, elts[vroot_dir_idx++], vroot_dent_namesz); } return vroot_dent; } } return dent; } static int vroot_closedir(pr_fs_t *fs, void *dirh) { int res; res = closedir((DIR *) dirh); if (vroot_dirtab != NULL) { unsigned long lookup_dirh; int count; lookup_dirh = (unsigned long) dirh; (void) pr_table_kremove(vroot_dirtab, &lookup_dirh, sizeof(unsigned long), NULL); /* If the dirtab table is empty, destroy the table. */ count = pr_table_count(vroot_dirtab); if (count == 0) { pr_table_empty(vroot_dirtab); destroy_pool(vroot_dir_pool); vroot_dir_pool = NULL; vroot_dirtab = NULL; vroot_dir_aliases = NULL; vroot_dir_idx = -1; } } return res; } static int vroot_mkdir(pr_fs_t *fs, const char *path, mode_t mode) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = mkdir(path, mode); return res; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = mkdir(vpath, mode); return res; } static int vroot_rmdir(pr_fs_t *fs, const char *path) { int res; char vpath[PR_TUNABLE_PATH_MAX + 1]; if (session.curr_phase == LOG_CMD || session.curr_phase == LOG_CMD_ERR || (session.sf_flags & SF_ABORT) || *vroot_base == '\0') { /* NOTE: once stackable FS modules are supported, have this fall through * to the next module in the stack. */ res = rmdir(path); return res; } /* Do not allow deleting of aliased files/directories; the aliases may only * exist for this user/group. */ if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, VROOT_LOOKUP_FL_NO_ALIASES, NULL) < 0) { return -1; } if (vroot_is_alias(vpath) == 0) { (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION, "denying delete of '%s' because it is a VRootAlias", vpath); errno = EACCES; return -1; } if (vroot_lookup_path(NULL, vpath, sizeof(vpath)-1, path, 0, NULL) < 0) return -1; res = rmdir(vpath); return res; } /* Configuration handlers */ /* usage: VRootAlias src-path dst-path */ MODRET set_vrootalias(cmd_rec *cmd) { config_rec *c; CHECK_ARGS(cmd, 2); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (pr_fs_valid_path(cmd->argv[1]) < 0) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "source path '", cmd->argv[1], "' is not an absolute path", NULL)); } c = add_config_param_str(cmd->argv[0], 2, cmd->argv[1], cmd->argv[2]); /* Set this flag in order to allow mod_ifsession to work properly with * multiple VRootAlias directives. */ c->flags |= CF_MERGEDOWN_MULTI; return PR_HANDLED(cmd); } /* usage: VRootEngine on|off */ MODRET set_vrootengine(cmd_rec *cmd) { int bool = -1; config_rec *c = NULL; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); bool = get_boolean(cmd, 1); if (bool == -1) CONF_ERROR(cmd, "expected Boolean parameter"); c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(unsigned char)); *((unsigned char *) c->argv[0]) = bool; return PR_HANDLED(cmd); } /* usage: VRootLog path|"none" */ MODRET set_vrootlog(cmd_rec *cmd) { CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (pr_fs_valid_path(cmd->argv[1]) < 0) CONF_ERROR(cmd, "must be an absolute path"); (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); return PR_HANDLED(cmd); } /* usage: VRootOptions opt1 opt2 ... optN */ MODRET set_vrootoptions(cmd_rec *cmd) { config_rec *c = NULL; register unsigned int i; unsigned int opts = 0U; if (cmd->argc-1 == 0) CONF_ERROR(cmd, "wrong number of parameters"); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); c = add_config_param(cmd->argv[0], 1, NULL); for (i = 1; i < cmd->argc; i++) { if (strcmp(cmd->argv[i], "allowSymlinks") == 0) { opts |= VROOT_OPT_ALLOW_SYMLINKS; } else { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown VRootOption: '", cmd->argv[i], "'", NULL)); } } c->argv[0] = pcalloc(c->pool, sizeof(unsigned int)); *((unsigned int *) c->argv[0]) = opts; return PR_HANDLED(cmd); } /* usage: VRootServerRoot path */ MODRET set_vrootserverroot(cmd_rec *cmd) { struct stat st; config_rec *c; size_t pathlen; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); if (pr_fs_valid_path(cmd->argv[1]) < 0) CONF_ERROR(cmd, "must be an absolute path"); if (stat(cmd->argv[1], &st) < 0) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", cmd->argv[1], "': ", strerror(errno), NULL)); } if (!S_ISDIR(st.st_mode)) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", cmd->argv[1], "' is not a directory", NULL)); } c = add_config_param(cmd->argv[0], 1, NULL); /* Make sure the configured path has a trailing path separater ('/'). * This is important. */ pathlen = strlen(cmd->argv[1]); if (cmd->argv[1][pathlen - 1] != '/') { c->argv[0] = pstrcat(c->pool, cmd->argv[1], "/", NULL); } else { c->argv[0] = pstrdup(c->pool, cmd->argv[1]); } return PR_HANDLED(cmd); } /* Command handlers */ MODRET vroot_log_retr(cmd_rec *cmd) { const char *key; char *path; if (vroot_engine == FALSE || session.chroot_path == NULL) { return PR_DECLINED(cmd); } key = "mod_xfer.retr-path"; path = pr_table_get(cmd->notes, key, NULL); if (path != NULL) { char *real_path; if (*path == '/') { real_path = pdircat(cmd->pool, vroot_base, path, NULL); vroot_clean_path(real_path); } else { real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH); } pr_table_set(cmd->notes, key, real_path, 0); } return PR_DECLINED(cmd); } MODRET vroot_log_stor(cmd_rec *cmd) { const char *key; char *path; if (vroot_engine == FALSE || session.chroot_path == NULL) { return PR_DECLINED(cmd); } key = "mod_xfer.store-path"; path = pr_table_get(cmd->notes, key, NULL); if (path != NULL) { char *real_path; if (*path == '/') { real_path = pdircat(cmd->pool, vroot_base, path, NULL); vroot_clean_path(real_path); } else { real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH); } pr_table_set(cmd->notes, key, real_path, 0); } return PR_DECLINED(cmd); } MODRET vroot_pre_mkd(cmd_rec *cmd) { if (vroot_engine == FALSE || session.chroot_path == NULL) { return PR_DECLINED(cmd); } #if PROFTPD_VERSION_NUMBER >= 0x0001030407 vroot_use_mkdtemp = pr_fsio_set_use_mkdtemp(FALSE); #endif /* ProFTPD 1.3.4c or later */ return PR_DECLINED(cmd); } MODRET vroot_post_mkd(cmd_rec *cmd) { if (vroot_engine == FALSE || session.chroot_path == NULL) { return PR_DECLINED(cmd); } #if PROFTPD_VERSION_NUMBER >= 0x0001030407 pr_fsio_set_use_mkdtemp(vroot_use_mkdtemp); #endif /* ProFTPD 1.3.4c or later */ return PR_DECLINED(cmd); } MODRET vroot_pre_pass(cmd_rec *cmd) { pr_fs_t *fs = NULL; unsigned char *use_vroot = NULL; use_vroot = get_param_ptr(main_server->conf, "VRootEngine", FALSE); if (!use_vroot || *use_vroot == FALSE) { vroot_engine = FALSE; return PR_DECLINED(cmd); } /* First, make sure that we have not already registered our FS object. */ fs = pr_unmount_fs("/", "vroot"); if (fs) { destroy_pool(fs->fs_pool); } fs = pr_register_fs(main_server->pool, "vroot", "/"); if (fs == NULL) { pr_log_debug(DEBUG3, MOD_VROOT_VERSION ": error registering fs: %s", strerror(errno)); return PR_DECLINED(cmd); } pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot registered"); /* Add the module's custom FS callbacks here. This module does not * provide callbacks for the following (as they are unnecessary): * close(), read(), write(), and lseek(). */ fs->stat = vroot_stat; fs->lstat = vroot_lstat; fs->rename = vroot_rename; fs->unlink = vroot_unlink; fs->open = vroot_open; fs->creat = vroot_creat; fs->link = vroot_link; fs->readlink = vroot_readlink; fs->symlink = vroot_symlink; fs->truncate = vroot_truncate; fs->chmod = vroot_chmod; fs->chown = vroot_chown; #if PROFTPD_VERSION_NUMBER >= 0x0001030407 fs->lchown = vroot_lchown; #endif /* ProFTPD 1.3.4c or later */ fs->chdir = vroot_chdir; fs->chroot = vroot_chroot; fs->utimes = vroot_utimes; fs->opendir = vroot_opendir; fs->readdir = vroot_readdir; fs->closedir = vroot_closedir; fs->mkdir = vroot_mkdir; fs->rmdir = vroot_rmdir; vroot_engine = TRUE; return PR_DECLINED(cmd); } MODRET vroot_post_pass(cmd_rec *cmd) { if (vroot_engine) { /* If not chrooted, unregister vroot. */ if (!session.chroot_path) { if (pr_unregister_fs("/") < 0) { pr_log_debug(DEBUG2, MOD_VROOT_VERSION ": error unregistering vroot: %s", strerror(errno)); } else { pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered"); pr_fs_setcwd(pr_fs_getvwd()); pr_fs_clear_cache(); } } else { config_rec *c; /* Otherwise, lookup and process any VRootOptions. */ c = find_config(main_server->conf, CONF_PARAM, "VRootOptions", FALSE); if (c) { vroot_opts = *((unsigned int *) c->argv[0]); } /* XXX This needs to be in the PRE_CMD PASS handler, as when * VRootServer is used, so that a real chroot(2) occurs. */ handle_vroot_alias(); } } return PR_DECLINED(cmd); } MODRET vroot_post_pass_err(cmd_rec *cmd) { if (vroot_engine) { /* If not chrooted, unregister vroot. */ if (session.chroot_path == NULL) { if (pr_unregister_fs("/") < 0) { pr_log_debug(DEBUG2, MOD_VROOT_VERSION ": error unregistering vroot: %s", strerror(errno)); } else { pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered"); } } if (vroot_aliastab) { pr_table_empty(vroot_aliastab); destroy_pool(vroot_alias_pool); vroot_alias_pool = NULL; vroot_aliastab = NULL; } } return PR_DECLINED(cmd); } /* Initialization routines */ static int vroot_sess_init(void) { config_rec *c; struct dirent dent; c = find_config(main_server->conf, CONF_PARAM, "VRootLog", FALSE); if (c) { vroot_log = c->argv[0]; } if (vroot_log && strcasecmp(vroot_log, "none") != 0) { int res; PRIVS_ROOT res = pr_log_openfile(vroot_log, &vroot_logfd, 0660); PRIVS_RELINQUISH switch (res) { case 0: break; case -1: pr_log_debug(DEBUG1, MOD_VROOT_VERSION ": unable to open VRootLog '%s': %s", vroot_log, strerror(errno)); break; case PR_LOG_SYMLINK: pr_log_debug(DEBUG1, MOD_VROOT_VERSION ": unable to open VRootLog '%s': %s", vroot_log, "is a symlink"); break; case PR_LOG_WRITABLE_DIR: pr_log_debug(DEBUG1, MOD_VROOT_VERSION ": unable to open VRootLog '%s': %s", vroot_log, "parent directory is world-writable"); break; } } /* Allocate the memory for the static struct dirent that we use, including * determining the necessary sizes. */ vroot_dentsz = sizeof(dent); if (sizeof(dent.d_name) == 1) { /* Allocate extra space for the dent path name. */ vroot_dent_namesz = PR_TUNABLE_PATH_MAX; } vroot_dentsz += vroot_dent_namesz; vroot_dent = palloc(session.pool, vroot_dentsz); return 0; } /* Module API tables */ static conftable vroot_conftab[] = { { "VRootAlias", set_vrootalias, NULL }, { "VRootEngine", set_vrootengine, NULL }, { "VRootLog", set_vrootlog, NULL }, { "VRootOptions", set_vrootoptions, NULL }, { "VRootServerRoot", set_vrootserverroot, NULL }, { NULL } }; static cmdtable vroot_cmdtab[] = { { PRE_CMD, C_PASS, G_NONE, vroot_pre_pass, FALSE, FALSE }, { POST_CMD, C_PASS, G_NONE, vroot_post_pass, FALSE, FALSE }, { POST_CMD_ERR, C_PASS, G_NONE, vroot_post_pass_err, FALSE, FALSE }, { PRE_CMD, C_MKD, G_NONE, vroot_pre_mkd, FALSE, FALSE }, { POST_CMD, C_MKD, G_NONE, vroot_post_mkd, FALSE, FALSE }, { POST_CMD_ERR, C_MKD, G_NONE, vroot_post_mkd, FALSE, FALSE }, { PRE_CMD, C_XMKD, G_NONE, vroot_pre_mkd, FALSE, FALSE }, { POST_CMD, C_XMKD, G_NONE, vroot_post_mkd, FALSE, FALSE }, { POST_CMD_ERR, C_XMKD, G_NONE, vroot_post_mkd, FALSE, FALSE }, /* These command handlers are for manipulating cmd->notes, to get * paths properly logged. * * Ideally these would be LOG_CMD/LOG_CMD_ERR phase handlers. HOWEVER, * we need to transform things before the cmd is dispatched to mod_log, * and mod_log uses a C_ANY handler for logging. And when dispatching, * C_ANY handlers are run before named handlers. This means that using * LOG_CMD/LOG_CMD_ERR handlers would be run AFTER mod_log's handler, * even though we appear BEFORE mod_log in the module load order. * * Thus to do the transformation, we actually use POST_CMD/POST_CMD_ERR * phase handlers here. */ { POST_CMD, C_APPE, G_NONE, vroot_log_stor, FALSE, FALSE }, { POST_CMD_ERR, C_APPE, G_NONE, vroot_log_stor, FALSE, FALSE }, { POST_CMD, C_RETR, G_NONE, vroot_log_retr, FALSE, FALSE }, { POST_CMD_ERR, C_RETR, G_NONE, vroot_log_retr, FALSE, FALSE }, { POST_CMD, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE }, { POST_CMD_ERR, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE }, { 0, NULL } }; module vroot_module = { NULL, NULL, /* Module API version 2.0 */ 0x20, /* Module name */ "vroot", /* Module configuration handler table */ vroot_conftab, /* Module command handler table */ vroot_cmdtab, /* Module authentication handler table */ NULL, /* Module initialization function */ NULL, /* Session initialization function */ vroot_sess_init, /* Module version */ MOD_VROOT_VERSION }; proftpd-mod-vroot-0.9.4/mod_vroot.html000066400000000000000000000164331302754346600200450ustar00rootroot00000000000000 ProFTPD module mod_vroot

ProFTPD module mod_vroot



This module is contained in the mod_vroot.c file for ProFTPD 1.3.x, and is not compiled by default. Installation instructions are discussed here.

The purpose of this module to is to implement a virtual chroot capability that does not require root privileges. The mod_vroot module provides this capability by using ProFTPD's FS API, available as of 1.2.8rc1.

The most current version of mod_vroot can be found at:

  https://github.com/Castaglia/proftpd-mod_vroot.git

Author

Please contact TJ Saunders <tj at castaglia.org> with any questions, concerns, or suggestions regarding this module.

Thanks

2003-08-26: Thanks to Oskar Liljeblad for the elegant patch that added symlink support.

Directives


VRootAlias

Syntax: VRootAlias src-path dst-path
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_vroot
Compatibility: 1.3.2 and later

The VRootAlias directive is used to create an "alias" of a directory outside of the chroot area into the chroot. The dst-path parameter is a relative path, relative to the chroot area (i.e. the directory in which the session starts). The src-path parameter, on the other hand, is an absolute path, and may be to a file or directory.

For example, you might map a shared upload directory into a user's home directory using:

  <IfModule mod_vroot.c>
    VRootEngine on

    DefaultRoot ~
    VRootAlias /var/ftp/upload ~/upload
  </IfModule>
This will automatically create an "upload" directory to appear in the chroot area (in this case, the user's home directory).

The VRootAlias directive is only needed for files/directories that are going to be accessed by remote clients. It is not needed for configuration files (e.g. PAM configuration files like pam_env.conf) needed by libraries. Using the VRootAlias for such library configuration files is pointless and wasteful.

Note that this directive will not work if the VRootServerRoot is used.


VRootEngine

Syntax: VRootEngine on|off
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_vroot
Compatibility: 1.2.8rc1 and later

The VRootEngine directive enables the virtual chroot engine implemented by mod_vroot. If enabled, the virtual chroot will be used in place of the operating system's chroot(2). This directive affects any DefaultRoot directives and any <Anonymous> contexts within the server context in which the VRootEngine directive appears.


VRootLog

Syntax: VRootLog file
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_vroot
Compatibility: 1.3.0rc1 and later

The VRootLog directive is used to specify a log file for mod_vroot's reporting on a per-server basis. The file parameter given must be the full path to the file to use for logging.


VRootOptions

Syntax: VRootOptions opt1 ...
Default: None
Context: "server config" <VirtualHost>, <Global>
Module: mod_vroot
Compatibility: 1.2.9rc2 and later

The VRootOptions directive is used to configure various optional behavior of mod_vroot.

Example:

  VRootOptions allowSymlinks

The currently implemented options are:


VRootServerRoot

Syntax: VRootServerRoot path
Default: None
Context: "server config" <VirtualHost>, <Global>
Module: mod_vroot
Compatibility: 1.3.2rc1 and later

The VRootServerRoot directive is used to configure a directory to which the mod_vroot module will perform a real chroot. The idea is that each <VirtualHost> can have its own directory to which a real chroot(2) system call is made; the user-specific home directories will be virtual roots underneath this directory. Thus some measure of security, via the chroot(2) system call, is provided by the kernel, while still allowing symlinked shared folders among users of this <VirtualHost>.

For example:

  <VirtualHost a.b.c.d>
    VRootEngine on
    VRootServerRoot /etc/ftpd/a.b.c.d/
    VRootOptions allowSymlinks
    DefaultRoot ~
    ...

  </VirtualHost>

See also: VRootOptions


Installation

After unpacking and patching the latest proftpd-1.3.x source code, copy the mod_vroot.c file into:
  proftpd-dir/contrib/
Then follow the normal steps for using third-party modules in proftpd:
  ./configure --with-modules=mod_vroot
To build mod_vroot as a DSO module:
  ./configure --enable-dso --with-shared=mod_vroot
Then follow the usual steps:
  make
  make install

For those with an existing ProFTPD installation, you can use the prxs tool to add mod_vroot, as a DSO module, to your existing server:

  # prxs -c -i -d mod_vroot.c


Author: $Author: tj $
Last Updated: $Date: 2009/10/19 16:30:18 $

© Copyright 2000-2013 TJ Saunders
All Rights Reserved

proftpd-mod-vroot-0.9.4/t/000077500000000000000000000000001302754346600154035ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/000077500000000000000000000000001302754346600161515ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/000077500000000000000000000000001302754346600173675ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/Tests/000077500000000000000000000000001302754346600204715ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/Tests/Modules/000077500000000000000000000000001302754346600221015ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/Tests/Modules/mod_vroot.pm000066400000000000000000011226641302754346600244630ustar00rootroot00000000000000package ProFTPD::Tests::Modules::mod_vroot; use lib qw(t/lib); use base qw(ProFTPD::TestSuite::Child); use strict; use Cwd; use Digest::MD5; use File::Path qw(mkpath rmtree); use File::Spec; use IO::Handle; use POSIX qw(:fcntl_h); use ProFTPD::TestSuite::FTP; use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); $| = 1; my $order = 0; my $TESTS = { vroot_engine => { order => ++$order, test_class => [qw(forking)], }, vroot_anon => { order => ++$order, test_class => [qw(forking)], }, vroot_anon_limit_write_allow_stor => { order => ++$order, test_class => [qw(forking)], }, vroot_symlink => { order => ++$order, test_class => [qw(forking)], }, vroot_symlink_eloop => { order => ++$order, test_class => [qw(forking)], }, vroot_opt_allow_symlinks_file => { order => ++$order, test_class => [qw(forking)], }, vroot_opt_allow_symlinks_dir_retr => { order => ++$order, test_class => [qw(forking)], }, vroot_opt_allow_symlinks_dir_stor_no_overwrite => { order => ++$order, test_class => [qw(forking)], }, vroot_opt_allow_symlinks_dir_stor => { order => ++$order, test_class => [qw(forking)], }, vroot_opt_allow_symlinks_dir_cwd => { order => ++$order, test_class => [qw(forking)], }, vroot_dir_mkd => { order => ++$order, test_class => [qw(forking)], }, vroot_server_root => { order => ++$order, test_class => [qw(forking rootprivs)], }, vroot_server_root_mkd => { order => ++$order, test_class => [qw(bug forking rootprivs)], }, vroot_alias_file_list => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_list_multi => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_retr => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_stor_no_overwrite => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_stor => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_dele => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_file_mlsd => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_file_mlst => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_dup_same_name => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dup_colliding_aliases => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_delete_source => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_no_source => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_list_no_trailing_slash => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_list_with_trailing_slash => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_list_from_above => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_cwd_list => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_cwd_stor => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_cwd_cdup => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_mkd => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_rmd => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_dir_cwd_mlsd => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_dir_outside_root_cwd_mlsd => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_dir_outside_root_cwd_mlsd_cwd_ls => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_dir_mlsd_from_above => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_dir_mlst => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_symlink_list => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_symlink_retr => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_symlink_stor_no_overwrite => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_symlink_stor => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_symlink_mlsd => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_symlink_mlst => { order => ++$order, test_class => [qw(forking mod_facts)], }, vroot_alias_ifuser => { order => ++$order, test_class => [qw(forking mod_ifsession)], }, vroot_alias_ifgroup => { order => ++$order, test_class => [qw(forking mod_ifsession)], }, vroot_alias_ifgroup_list_stor => { order => ++$order, test_class => [qw(forking mod_ifsession)], }, vroot_alias_ifclass => { order => ++$order, test_class => [qw(forking mod_ifsession)], }, vroot_showsymlinks_on => { order => ++$order, test_class => [qw(bug forking)], }, vroot_hiddenstores_on_double_dot => { order => ++$order, test_class => [qw(bug forking)], }, vroot_mfmt => { order => ++$order, test_class => [qw(bug forking)], }, vroot_log_extlog_retr => { order => ++$order, test_class => [qw(forking)], }, vroot_log_extlog_stor => { order => ++$order, test_class => [qw(forking)], }, # XXX Currently does not work, since TransferLog logging can't be filtered # vroot_log_xferlog_retr => { # order => ++$order, # test_class => [qw(inprogress forking)], # }, # XXX Currently does not work, since TransferLog logging can't be filtered # vroot_log_xferlog_stor => { # order => ++$order, # test_class => [qw(inprogress forking)], # }, # XXX Currently does not work due to matching logic, and to # mod_vroot's session.chroot_path machinations. # vroot_config_limit_write => { # order => ++$order, # test_class => [qw(bug forking)], # }, vroot_config_deleteabortedstores_conn_aborted => { order => ++$order, test_class => [qw(bug forking)], }, vroot_config_deleteabortedstores_cmd_aborted => { order => ++$order, test_class => [qw(bug forking)], }, vroot_alias_var_u_file => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_var_u_dir => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_var_u_dir_with_stor_mff => { order => ++$order, test_class => [qw(forking)], }, vroot_alias_var_u_symlink_dir => { order => ++$order, test_class => [qw(forking)], }, # See: # https://github.com/Castaglia/proftpd-mod_vroot/issues/4 vroot_alias_bad_src_dst_check_bug4 => { order => ++$order, test_class => [qw(bug forking)], }, # See: # https://github.com/Castaglia/proftpd-mod_vroot/issues/5 vroot_alias_bad_alias_dirscan_bug5 => { order => ++$order, test_class => [qw(bug forking)], }, # See: # https://github.com/proftpd/proftpd/issues/59 vroot_alias_enametoolong_bug59 => { order => ++$order, test_class => [qw(bug forking)], }, }; sub new { return shift()->SUPER::new(@_); } sub tear_down { rmtree('/tmp/vroot.d'); } sub list_tests { return testsuite_get_runnable_tests($TESTS); # XXX test file aliases where the alias includes directories which do not # exist. Should we allow traversal of these kinds of aliases (if so, what # real directory do we use for perms, ownership? What would a CWD into # such a path component mean?), or only allow retrieval/storage to that # alias but not traversal? } sub vroot_engine { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to change up and out of the vroot ($resp_code, $resp_msg) = $client->quote("CWD", ".."); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CWD command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; $ok = 1; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } ($resp_code, $resp_msg) = $client->cdup(); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CDUP command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; $ok = 1; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_anon { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my ($user, $group) = config_get_identity(); my $passwd = 'test'; my $anon_dir = File::Spec->rel2abs($tmpdir); my $uid = $<; my $gid = $(; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $anon_dir)) { die("Can't set perms on $anon_dir to 0755: $!"); } unless (chown($uid, $gid, $anon_dir)) { die("Can't set owner of $anon_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, '/tmp', '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < User $user Group $group EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to change up and out of the vroot ($resp_code, $resp_msg) = $client->quote("CWD", ".."); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CWD command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; $ok = 1; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } ($resp_code, $resp_msg) = $client->cdup(); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CDUP command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; $ok = 1; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_anon_limit_write_allow_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my ($user, $group) = config_get_identity(); my $passwd = 'test'; my $anon_dir = File::Spec->rel2abs($tmpdir); my $uid = $<; my $gid = $(; my $uploads_dir = File::Spec->rel2abs("$anon_dir/uploads"); mkpath($uploads_dir); my $test_file = File::Spec->rel2abs("$uploads_dir/test.txt"); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $anon_dir, $uploads_dir)) { die("Can't set perms on $anon_dir to 0755: $!"); } unless (chown($uid, $gid, $anon_dir, $uploads_dir)) { die("Can't set owner of $anon_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, '/tmp', '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Test this config: # # # # DenyAll # # # # # AllowAll # # # my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < User $user Group $group RequireValidShell off DenyAll # Ideally there would be no leading slash here, but because of how # mod_vroot alters things (see Issue #1), the leading slash makes # the test succeed. AllowAll EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->stor_raw('uploads/test.txt'); unless ($conn) { die("STOR uploads/test.txt failed:" . $client->response_code() . " " . $client->response_msg()); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf), 15); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); $self->assert(-f $test_file, test_msg("File $test_file does not exist as expected")); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_symlink { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a file that is outside of the vroot my $test_file = File::Spec->rel2abs("$tmpdir/bar.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../bar.txt", "foo.txt")) { die("Can't symlink '../bar.txt' to 'foo.txt': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { sleep(1); my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } if (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly not empty"); } # Try to download from the symlink my $conn = $client->retr_raw('foo.txt'); if ($conn) { die("RETR test.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "foo.txt: No such file or directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh, 15) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_symlink_eloop { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a file that is outside of the vroot my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../test.txt", "test.txt")) { die("Can't symlink '../test.txt' to 'test.txt': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } if (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly not empty"); } # Try to download from the symlink my $conn = $client->retr_raw('test.txt'); if ($conn) { die("RETR test.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); # We expect this because the "../test.txt" -> "test.txt" symlink # causes mod_vroot to handle "../test.txt" as "test.txt", which is # a symlink -- hence the loop. $expected = "test.txt: Too many levels of symbolic links"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_opt_allow_symlinks_file { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a file that is outside of the vroot my $test_file = File::Spec->rel2abs("$tmpdir/bar.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../bar.txt", "foo.txt")) { die("Can't symlink '../bar.txt' to 'foo.txt': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'foo.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to download from the symlink my $conn = $client->retr_raw('foo.txt'); unless ($conn) { die("RETR foo.txt failed: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_opt_allow_symlinks_dir_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $public_dir = File::Spec->rel2abs("$tmpdir/public"); mkpath($public_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $public_dir)) { die("Can't set perms on $home_dir, $public_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $public_dir)) { die("Can't set owner of $home_dir, $public_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a directory that is outside of the vroot my $test_file = File::Spec->rel2abs("$public_dir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../public", "public")) { die("Can't symlink '../public' to 'public': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'public' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to download from the symlink my $conn = $client->retr_raw('public/test.txt'); unless ($conn) { die("RETR public/test.txt failed: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_opt_allow_symlinks_dir_stor_no_overwrite { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $public_dir = File::Spec->rel2abs("$tmpdir/public"); mkpath($public_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $public_dir)) { die("Can't set perms on $home_dir, $public_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $public_dir)) { die("Can't set owner of $home_dir, $public_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a directory that is outside of the vroot my $test_file = File::Spec->rel2abs("$public_dir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../public", "public")) { die("Can't symlink '../public' to 'public': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'public' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to upload to the symlink my $conn = $client->stor_raw('public/test.txt'); if ($conn) { die("STOR public/test.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "public/test.txt: Overwrite permission denied"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_opt_allow_symlinks_dir_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $public_dir = File::Spec->rel2abs("$tmpdir/public"); mkpath($public_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $public_dir)) { die("Can't set perms on $home_dir, $public_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $public_dir)) { die("Can't set owner of $home_dir, $public_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a directory that is outside of the vroot my $test_file = File::Spec->rel2abs("$public_dir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../public", "public")) { die("Can't symlink '../public' to 'public': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'public' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to upload to the symlink my $conn = $client->stor_raw('public/test.txt'); unless ($conn) { die("STOR public/test.txt failed: " . $client->response_code() . " " . $client->response_msg()); } my $buf = "Farewell cruel world!"; $conn->write($buf, length($buf), 25); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_opt_allow_symlinks_dir_cwd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $public_dir = File::Spec->rel2abs("$tmpdir/public"); mkpath($public_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $public_dir)) { die("Can't set perms on $home_dir, $public_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $public_dir)) { die("Can't set owner of $home_dir, $public_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); # Create a symlink to a directory that is outside of the vroot my $test_file = File::Spec->rel2abs("$public_dir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../public", "public")) { die("Can't symlink '../public' to 'public': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'public' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to change into the symlink'd directory my ($resp_code, $resp_msg) = $client->cwd('public'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'test.txt' => 1, }; $ok = 1; $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/public" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); # Go up to the parent directory ($resp_code, $resp_msg) = $client->cdup(); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CDUP command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_dir_mkd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $sub_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($sub_dir); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, Directory => { # BUG: This should be $sub_dir. But due to how mod_vroot currently # works, the path has to be modified to match the # mod_vroot. (I.e. for the purposes of this test, just '/foo.d'). # Sigh. # '/foo.d' => { $sub_dir => { # Test the UserOwner directive in the setting UserOwner => 'root', }, }, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); $client->cwd('foo.d'); $client->mkd('bar.d'); my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected response code $expected, got $resp_code")); $expected = "\"/foo.d/bar.d\" - Directory successfully created"; $self->assert($expected eq $resp_msg, test_msg("Expected response message '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); eval { if (open(my $fh, "< $log_file")) { # Look for the 'smkdir' fsio channel trace message; it will tell us # whether the UserOwner directive from the section was # successfully found. my $have_smkdir_line = 0; my $line; while ($line = <$fh>) { chomp($line); if ($line =~ /smkdir/) { $have_smkdir_line = 1; last; } } close($fh); $self->assert($have_smkdir_line, test_msg("Did not find expected 'fsio' channel TraceLog line in $log_file")); if ($line =~ /UID (\d+)/) { my $smkdir_uid = $1; if ($< == 0) { $self->assert($smkdir_uid == 0, test_msg("Expected UID 0, got $smkdir_uid")); } } else { die("Unexpectedly formatted 'fsio' channel TraceLog line '$line'"); } } else { die("Can't read $log_file: $!"); } }; if ($@) { $ex = $@; } if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_server_root { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $abs_tmpdir = File::Spec->rel2abs($tmpdir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootServerRoot => $abs_tmpdir, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } if (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly not empty"); } # Try to change up and out of the vroot ($resp_code, $resp_msg) = $client->quote("CWD", ".."); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CWD command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } if (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly not empty"); } ($resp_code, $resp_msg) = $client->cdup(); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CDUP command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } if (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly not empty"); } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_server_root_mkd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); my $abs_tmpdir = File::Spec->rel2abs($tmpdir); my $test_dir = File::Spec->rel2abs("$home_dir/test.d"); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootServerRoot => $abs_tmpdir, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->mkd('test.d'); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/test.d\" - Directory successfully created"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->cwd('test.d'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CWD command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/test.d\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_list { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_list_multi { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file1 = '~/bar.txt'; my $dst_file2 = 'baz.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => [ 'VRootEngine on', "VRootLog $log_file", 'DefaultRoot ~', "VRootAlias $src_file $dst_file1", "VRootAlias $src_file $dst_file2", "VRootAlias $src_file /tmp/foo/bar/baz", ], 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, 'baz.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_dir = File::Spec->rel2abs("$tmpdir/test.d"); mkpath($test_dir); my $src_file = File::Spec->rel2abs("$test_dir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to download the aliased file my $conn = $client->retr_raw('bar.txt'); unless ($conn) { die("RETR bar.txt failed: " . $client->response_code() . " " . $client->response_msg()); } my $buf; my $count = $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); my $expected = 14; $self->assert($expected == $count, test_msg("Expected $expected, got $count")); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_stor_no_overwrite { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to upload to the aliased file my $conn = $client->stor_raw('bar.txt'); if ($conn) { die("STOR bar.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'bar.txt: Overwrite permission denied'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to upload to the aliased file my $conn = $client->stor_raw('bar.txt'); unless ($conn) { die("STOR bar.txt failed: " . $client->response_code() . ' ' . $client->response_msg()); } my $buf = "Farewell, cruel world"; $conn->write($buf, length($buf), 25); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_dele { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to delete the aliased file eval { $client->dele('bar.txt') }; unless ($@) { die("DELE bar.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'bar.txt: Permission denied'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_mlsd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = 'bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->mlsd_raw('bar.txt'); if ($conn) { die("MLSD bar.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "'bar.txt' is not a directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_file_mlst { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = 'bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->mlst('bar.txt'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/bar.txt$'; $self->assert(qr/$expected/, $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dup_same_name { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/foo.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dup_colliding_aliases { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => [ 'VRootEngine on', "VRootLog $log_file", 'DefaultRoot ~', "VRootAlias $src_file $dst_file", "VRootAlias $auth_user_file $dst_file", ], 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_delete_source { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); # Delete the source of the alias, and make sure that neither source # nor alias appear in the directory listing. ($resp_code, $resp_msg) = $client->dele('foo.txt'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "DELE command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_no_source { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_list_no_trailing_slash { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.d' => 1, 'bar.d' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_list_with_trailing_slash { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir/ $dst_dir/", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.d' => 1, 'bar.d' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_list_from_above { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw('bar.d'); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_cwd_list { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->cwd('bar.d'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/bar.d\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_cwd_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/sub1.d/sub2.d/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->cwd('bar.d'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/bar.d\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->stor_raw('test.txt'); unless ($conn) { die("Failed to STOR: " . $client->response_code() . " " . $client->response_msg()); } my $buf = "Hello, World!"; $conn->write($buf, length($buf), 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_cwd_cdup { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->cwd('bar.d'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/bar.d\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->cdup(); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CDUP command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_mkd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = 'bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); eval { $client->mkd('bar.d') }; unless ($@) { die("MKD bar.d succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "bar.d: File exists"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_rmd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = 'bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); eval { $client->rmd('bar.d') }; unless ($@) { die("RMD bar.d succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "bar.d: Permission denied"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_cwd_mlsd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->cwd('bar.d'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->mlsd_raw(); unless ($conn) { die("Failed to MLSD: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("MLSD data unexpectedly empty"); } my $expected = { '.' => 1, '..' => 1, 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in MLSD data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_mlsd_from_above { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->mlsd_raw('bar.d'); unless ($conn) { die("Failed to MLSD bar.d: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("MLSD data unexpectedly empty"); } my $expected = { '.' => 1, '..' => 1, 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in MLSD data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_outside_root_cwd_mlsd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/baz"); mkpath($home_dir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 table:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->cwd('bar.d'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->mlsd_raw(); unless ($conn) { die("Failed to MLSD: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("MLSD data unexpectedly empty"); } my $expected = { '.' => 1, '..' => 1, 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in MLSD data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_outside_root_cwd_mlsd_cwd_ls { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/baz"); mkpath($home_dir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $sub_dir = File::Spec->rel2abs("$tmpdir/foo.d/subdir"); mkpath($sub_dir); my $test_file4 = File::Spec->rel2abs("$tmpdir/foo.d/subdir/test.txt"); if (open(my $fh, "> $test_file4")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file4: $!"); } } else { die("Can't open $test_file4: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 table:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->cwd('bar.d'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->mlsd_raw(); unless ($conn) { die("Failed to MLSD: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("MLSD data unexpectedly empty"); } my $expected = { '.' => 1, '..' => 1, 'subdir' => 1, 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in MLSD data") } ($resp_code, $resp_msg) = $client->cwd('subdir'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "CWD command successful"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $buf = ''; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'test.txt' => 1, }; $ok = 1; $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } ($resp_code, $resp_msg) = $client->pwd(); $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = '"/bar.d/subdir" is the current directory'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_dir_mlst { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = 'bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->mlst('bar.d'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'modify=\d+;perm=flcdmpe;type=dir;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/bar\.d$'; $self->assert(qr/$expected/, $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_list { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'foo.lnk' => 1, 'bar.lnk' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to download the aliased file my $conn = $client->retr_raw('bar.lnk'); unless ($conn) { die("RETR bar.txt failed: " . $client->response_code() . " " . $client->response_msg()); } my $buf; my $count = $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); my $expected = 14; $self->assert($expected == $count, test_msg("Expected $expected, got $count")); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_stor_no_overwrite { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to upload to the aliased symlink my $conn = $client->stor_raw('bar.lnk'); if ($conn) { die("STOR bar.lnk succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'bar.lnk: Overwrite permission denied'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to upload to the aliased symlink my $conn = $client->stor_raw('bar.lnk'); unless ($conn) { die("STOR bar.lnk failed: " . $client->response_code() . ' ' . $client->response_msg()); } my $buf = "Farewell, cruel world"; $conn->write($buf, length($buf), 25); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_mlsd { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->mlsd_raw('bar.lnk'); if ($conn) { die("MLSD bar.lnk succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "'bar.lnk' is not a directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_symlink_mlst { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->mlst('bar.lnk'); my $expected; $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'modify=\d+;perm=adfr(w)?;size=\d+;type=file;unique=\S+;UNIX.group=\d+;UNIX.mode=\d+;UNIX.owner=\d+; \/bar\.lnk$'; $self->assert(qr/$expected/, $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_ifuser { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file1 = '~/bar.txt'; my $dst_file2 = '~/baz.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < VRootAlias $src_file $dst_file1 VRootAlias $src_file $dst_file2 EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_ifgroup { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file1 = '~/bar.txt'; my $dst_file2 = '~/baz.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < VRootAlias $src_file $dst_file1 VRootAlias $src_file $dst_file2 EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_ifgroup_list_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/home/$user"); mkpath($home_dir); my $uid = 500; my $gid = 500; my $shared_dir = File::Spec->rel2abs("$tmpdir/shared"); mkpath($shared_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $shared_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $shared_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$shared_dir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < VRootAlias $shared_dir ~/shared EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'shared' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } $self->assert($ok, test_msg("Unexpected name '$mismatch' appeared in LIST data")); ($resp_code, $resp_msg) = $client->cwd('shared'); $expected = 250; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = 'CWD command successful'; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn = $client->list_raw('-al'); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } $buf = ''; $conn->read($buf, 8192, 5); eval { $conn->close() }; $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. $res = {}; $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { '.' => 1, '..' => 1, 'test.txt' => 1, }; $ok = 1; $mismatch = undef; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } $self->assert($ok, test_msg("Unexpected name '$mismatch' appeared in LIST data")); $conn = $client->stor_raw('bar.txt'); unless ($conn) { die("Failed to STOR bar.txt: " . $client->response_code() . " " . $client->response_msg()); } $buf = "Farewell, cruel world!\n"; $conn->write($buf, length($buf), 5); eval { $conn->close() }; $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_ifclass { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file1 = '~/bar.txt'; my $dst_file2 = '~/baz.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < From 127.0.0.1 VRootAlias $src_file $dst_file1 VRootAlias $src_file $dst_file2 EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'foo.txt' => 1, 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_showsymlinks_on { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs("$tmpdir/home"); my $uid = 500; my $gid = 500; mkpath($home_dir); # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); # See: # # http://forums.proftpd.org/smf/index.php/topic,5207.0.html # Create a symlink to a file that is outside of the vroot my $outside_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $outside_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $outside_file: $!"); } } else { die("Can't open $outside_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("../foo.txt", "foo.lnk")) { die("Can't symlink '../foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } # Now create a symlink which points inside the vroot my $inside_file = File::Spec->rel2abs("$tmpdir/home/bar.txt"); if (open(my $fh, "> $inside_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $inside_file: $!"); } } else { die("Can't open $inside_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./bar.txt", "bar.lnk")) { die("Can't symlink './bar.txt' to 'bar.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, # ShowSymlinks is on by default, but explicitly list it here for # completeness ShowSymlinks => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => $home_dir, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^\S+\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } my $expected = { 'bar.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } # Try to download from the symlink my $conn = $client->retr_raw('bar.txt'); unless ($conn) { die("RETR bar.txt failed: " . $client->response_code() . " " . $client->response_msg()); } $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_hiddenstores_on_double_dot { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', HiddenStores => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); # Try to upload a file whose name starts with a period my $conn = $client->stor_raw('.foo'); unless ($conn) { die("STOR .foo failed: " . $client->response_code() . ' ' . $client->response_msg()); } my $buf = "Farewell, cruel world"; $conn->write($buf, length($buf), 25); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_mfmt { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); # First try MFMT using relative paths my $path = './test.txt'; ($resp_code, $resp_msg) = $client->mfmt('20020717210715', $path); my $expected; $expected = 213; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Modify=20020717210715; $path"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $path = "test.txt"; ($resp_code, $resp_msg) = $client->mfmt('20020717210715', $path); $expected = 213; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Modify=20020717210715; $path"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); # Next try an absolute path (from client perspective) $path = "/test.txt"; ($resp_code, $resp_msg) = $client->mfmt('20020717210715', $path); $expected = 213; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Modify=20020717210715; $path"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); # Last try the real absolute path $path = $test_file; eval { $client->mfmt('20020717210715', $path) }; unless ($@) { die("MFMT $path succeeded unexpectedly"); } $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$path: No such file or directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_log_extlog_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, LogFormat => 'custom "%f"', ExtendedLog => "$ext_log READ custom", IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->retr_raw('test.txt'); unless ($conn) { die("Failed to RETR test.txt: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); # Now, read in the ExtendedLog, and see whether the %f variable was # properly written out. if (open(my $fh, "< $ext_log")) { my $line = <$fh>; chomp($line); close($fh); if ($^O eq 'darwin') { # MacOSX-specific hack, due to how it handles tmp files $test_file = ('/private' . $test_file); } $self->assert($test_file eq $line, test_msg("Expected '$test_file', got '$line'")); } else { die("Can't read $ext_log: $!"); } if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_log_extlog_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, LogFormat => 'custom "%f"', ExtendedLog => "$ext_log WRITE custom", IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->stor_raw('test.txt'); unless ($conn) { die("Failed to STOR test.txt: " . $client->response_code() . " " . $client->response_msg()); } my $buf = "Hello, World!"; $conn->write($buf, length($buf), 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); # Now, read in the ExtendedLog, and see whether the %f variable was # properly written out. if (open(my $fh, "< $ext_log")) { my $line = <$fh>; chomp($line); close($fh); if ($^O eq 'darwin') { # MacOSX-specific hack, due to how it handles tmp files $test_file = ('/private' . $test_file); } $self->assert($test_file eq $line, test_msg("Expected '$test_file', got '$line'")); } else { die("Can't read $ext_log: $!"); } if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_log_xferlog_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); if (open(my $fh, "> $test_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $test_file: $!"); } } else { die("Can't open $test_file: $!"); } my $xfer_log = File::Spec->rel2abs("$tmpdir/xfer.log"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, TransferLog => $xfer_log, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); $client->type('binary'); my $conn = $client->retr_raw('test.txt'); unless ($conn) { die("Failed to RETR test.txt: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if (open(my $fh, "< $xfer_log")) { my $line = <$fh>; chomp($line); close($fh); my $expected = '^\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+\s+\d+\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+_\s+o\s+r\s+(\S+)\s+ftp\s+0\s+\*\s+c$'; $self->assert(qr/$expected/, $line, test_msg("Expected '$expected', got '$line'")); if ($line =~ /$expected/) { my $remote_host = $1; my $filesz = $2; my $filename = $3; my $xfer_type = $4; my $user_name = $5; if ($^O eq 'darwin') { # MacOSX-specific hack, due to how it handles tmp files $test_file = ('/private' . $test_file); } $expected = '127.0.0.1'; $self->assert($expected eq $remote_host, test_msg("Expected '$expected', got '$remote_host'")); $expected = -s $test_file; $self->assert($expected == $filesz, test_msg("Expected '$expected', got '$filesz'")); $expected = $test_file; $self->assert($expected eq $filename, test_msg("Expected '$expected', got '$filename'")); $expected = 'b'; $self->assert($expected eq $xfer_type, test_msg("Expected '$expected', got '$xfer_type'")); $expected = $user; $self->assert($expected eq $user_name, test_msg("Expected '$expected', got '$user_name'")); } } else { die("Can't read $xfer_log: $!"); } if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_log_xferlog_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); my $xfer_log = File::Spec->rel2abs("$tmpdir/xfer.log"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, TransferLog => $xfer_log, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); $client->type('binary'); my $conn = $client->stor_raw('test.txt'); unless ($conn) { die("Failed to STOR test.txt: " . $client->response_code() . " " . $client->response_msg()); } my $buf = "Hello, World!"; $conn->write($buf, length($buf), 5); eval { $conn->close() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if (open(my $fh, "< $xfer_log")) { my $line = <$fh>; chomp($line); close($fh); my $expected = '^\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+\s+\d+\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+_\s+i\s+r\s+(\S+)\s+ftp\s+0\s+\*\s+c$'; $self->assert(qr/$expected/, $line, test_msg("Expected '$expected', got '$line'")); if ($line =~ /$expected/) { my $remote_host = $1; my $filesz = $2; my $filename = $3; my $xfer_type = $4; my $user_name = $5; if ($^O eq 'darwin') { # MacOSX-specific hack, due to how it handles tmp files $test_file = ('/private' . $test_file); } $expected = '127.0.0.1'; $self->assert($expected eq $remote_host, test_msg("Expected '$expected', got '$remote_host'")); $expected = -s $test_file; $self->assert($expected == $filesz, test_msg("Expected '$expected', got '$filesz'")); $expected = $test_file; $self->assert($expected eq $filename, test_msg("Expected '$expected', got '$filename'")); $expected = 'b'; $self->assert($expected eq $xfer_type, test_msg("Expected '$expected', got '$xfer_type'")); $expected = $user; $self->assert($expected eq $user_name, test_msg("Expected '$expected', got '$user_name'")); } } else { die("Can't read $xfer_log: $!"); } if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_config_limit_write { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'directory:20 fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < DenyAll EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->stor_raw('test.txt'); if ($conn) { eval { $conn->close() }; die("STOR test.txt succeeded unexpectedly"); } my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); my $expected; $expected = 550; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "test.txt: Permission denied"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_config_deleteabortedstores_conn_aborted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt."); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, HiddenStores => 'on', DeleteAbortedStores => 'on', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->stor_raw('test.txt'); unless ($conn) { die("Failed to STOR test.txt: " . $client->response_code() . ' ' . $client->response_msg()); } my $buf = "Hello, World!"; $conn->write($buf, length($buf), 5); unless (-f $hidden_file) { die("File $hidden_file does not exist as expected"); } eval { $conn->abort() }; my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg, 1); if (-f $test_file) { die("File $test_file exists unexpectedly"); } if (-f $hidden_file) { die("File $hidden_file exists unexpectedly"); } }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_config_deleteabortedstores_cmd_aborted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $hidden_file = File::Spec->rel2abs("$tmpdir/.in.test.txt."); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, HiddenStores => 'on', DeleteAbortedStores => 'on', TimeoutLinger => 1, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my $conn = $client->stor_raw('test.txt'); unless ($conn) { die("Failed to STOR test.txt: " . $client->response_code() . ' ' . $client->response_msg()); } my $buf = "Hello, World!"; $conn->write($buf, length($buf), 5); unless (-f $hidden_file) { die("File $hidden_file does not exist as expected"); } $client->quote('ABOR'); my $resp_code = $client->response_code(); my $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg, 1); eval { $conn->close() }; $client->quit(); if (-f $test_file) { die("File $test_file exists unexpectedly"); } if (-f $hidden_file) { die("File $hidden_file exists unexpectedly"); } }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_var_u_file { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $sub_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($sub_dir); my $src_path = File::Spec->rel2abs("$sub_dir/foo.txt"); if (open(my $fh, "> $src_path")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_path: $!"); } } else { die("Can't open $src_path: $!"); } my $src_file = File::Spec->rel2abs($tmpdir) . '/%u/foo.txt'; my $dst_file = '~/bar.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'bar.txt' => 1, 'proftpd' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = '-rw-r--r--'; $self->assert($mode eq $res->{'bar.txt'}, test_msg("Expected '$mode' for 'bar.txt', got '$res->{'bar.txt'}'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_var_u_dir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $sub_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($sub_dir); my $src_path = File::Spec->rel2abs("$sub_dir/foo.d"); mkpath($src_path); my $src_file = File::Spec->rel2abs($tmpdir) . '/%u/foo.d'; my $dst_file = '~/bar.d'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'bar.d' => 1, 'proftpd' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = 'drwxr-xr-x'; $self->assert($mode eq $res->{'bar.d'}, test_msg("Expected '$mode' for 'bar.d', got '$res->{'bar.d'}'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_var_u_dir_with_stor_mff { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $sub_dir = File::Spec->rel2abs("$tmpdir/sub.d"); mkpath($sub_dir); my $src_path = File::Spec->rel2abs("$sub_dir/foo.d"); mkpath($src_path); my $src_file = File::Spec->rel2abs($tmpdir) . '/sub.d/foo.d/'; my $dst_file = '/%u'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'DEFAULT:10 vroot:20 sql:10 fileperms:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; sleep(1); $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'sub.d' => 1, 'proftpd' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = 'drwxr-xr-x'; $self->assert($mode eq $res->{'proftpd'}, test_msg("Expected '$mode' for 'proftpd', got '$res->{'proftpd'}'")); # Now change into the aliased directory, and upload a file there ($resp_code, $resp_msg) = $client->cwd('proftpd'); sleep(1); my $file = 'test.txt'; $conn = $client->stor_raw($file); unless ($conn) { die("STOR $file failed: " . $client->response_code() . " " . $client->response_msg()); } $buf = "Hello, World!\n"; $conn->write($buf, length($buf), 25); eval { $conn->close() }; sleep(1); $resp_code = $client->response_code(); $resp_msg = $client->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); my $facts = 'Modify=20020717210715;Create=20120807064710'; ($resp_code, $resp_msg) = $client->mff($facts, $file); $expected = 213; $self->assert($expected == $resp_code, test_msg("Expected response code $expected, got $resp_code")); $expected = "$facts $file"; $self->assert($expected eq $resp_msg, test_msg("Expected response message '$expected', got '$resp_msg'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh, 45) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_var_u_symlink_dir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $sub_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($sub_dir); my $src_path = File::Spec->rel2abs("$sub_dir/foo.d"); mkpath($src_path); my $cwd = getcwd(); unless (chdir($sub_dir)) { die("Can't chdir to $sub_dir: $!"); } unless (symlink('foo.d', "./foo.lnk")) { die("Can't symlink 'foo.d' to './foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_file = File::Spec->rel2abs($tmpdir) . '/%u/foo.lnk'; my $dst_file = '/%u.lnk'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, ShowSymlinks => 'off', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg); ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.conf' => 1, 'vroot.group' => 1, 'vroot.passwd' => 1, 'vroot.pid' => 1, 'vroot.scoreboard' => 1, 'vroot.scoreboard.lck' => 1, 'proftpd.lnk' => 1, 'proftpd' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = 'drwxr-xr-x'; $self->assert($mode eq $res->{'proftpd.lnk'}, test_msg("Expected '$mode' for 'proftpd.lnk', got '$res->{'proftpd.lnk'}'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_bad_src_dst_check_bug4 { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($home_dir); my $uid = 500; my $gid = 500; # In order for the real /tmp/vroot.d directory to be visible, via # VRootAlias, within the vroot, the leading /tmp directory needs to # actually exist with the vroot. In other words, the path needs to be # real, even if the leaf is virtual. my $user_tmpdir = File::Spec->rel2abs("$home_dir/tmp"); mkpath($user_tmpdir); my $test_dir = File::Spec->rel2abs("/tmp/vroot.d"); mkpath($test_dir); my $test_file = File::Spec->rel2abs("$test_dir/test.txt"); if (open(my $fh, "> $test_file")) { close($fh); } else { die("Can't open $test_file: $!"); } # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $user_tmpdir, $test_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $user_tmpdir, $test_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, ShowSymlinks => 'off', IfModules => { 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$test_dir ~/tmp/vroot.d", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->cwd('/tmp/vroot.d'); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'test.txt' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = '-rw-r--r--'; $self->assert($mode eq $res->{'test.txt'}, test_msg("Expected '$mode' for 'test.txt', got '$res->{'test.txt'}'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_bad_alias_dirscan_bug5 { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($home_dir); my $uid = 500; my $gid = 500; # In order for the real /tmp/vroot.d directory to be visible, via # VRootAlias, within the vroot, the leading /tmp directory needs to # actually exist with the vroot. In other words, the path needs to be # real, even if the leaf is virtual. my $user_tmpdir = File::Spec->rel2abs("$home_dir/tmp"); mkpath($user_tmpdir); my $test_dir = File::Spec->rel2abs("/tmp/vroot.d"); mkpath($test_dir); my $test_file = File::Spec->rel2abs("$test_dir/test.txt"); if (open(my $fh, "> $test_file")) { close($fh); } else { die("Can't open $test_file: $!"); } # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $user_tmpdir, $test_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $user_tmpdir, $test_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, ShowSymlinks => 'off', IfModules => { 'mod_vroot.c' => [ 'VRootEngine on', "VRootLog $log_file", 'DefaultRoot ~', "VRootAlias $test_dir ~/vroot.d", "VRootAlias $test_dir ~/tmp/vroot.d", ], 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client->cwd('/tmp'); my $conn = $client->list_raw(); unless ($conn) { die("Failed to LIST: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 5); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^(\S+)\s+\d+\s+\S+\s+\S+\s+.*?\s+(\S+)$/) { $res->{$2} = $1; } } unless (scalar(keys(%$res)) > 0) { die("LIST data unexpectedly empty"); } $expected = { 'vroot.d' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in LIST data") } my $mode = 'drwxr-xr-x'; $self->assert($mode eq $res->{'vroot.d'}, test_msg("Expected '$mode' for 'vroot.d', got '$res->{'vroot.d'}'")); $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } sub vroot_alias_enametoolong_bug59 { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = test_get_logfile(); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $group = 'ftpd'; my $home_dir = File::Spec->rel2abs("$tmpdir/$user"); mkpath($home_dir); my $uid = 500; my $gid = 500; my $test_dir = File::Spec->rel2abs("/tmp/vroot.d/0001a/10001a/encoding/input"); mkpath($test_dir); my $test_file = File::Spec->rel2abs("$test_dir/asgard"); if (open(my $fh, "> $test_file")) { close($fh); } else { die("Can't open $test_file: $!"); } # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir, $test_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $test_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, $group, $gid, $user); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, ShowSymlinks => 'off', IfModules => { 'mod_vroot.c' => [ 'VRootEngine on', "VRootLog $log_file", 'DefaultRoot ~', "VRootAlias /tmp ~/tmp-vroot-alias", "VRootAlias $test_dir ~/0001a-input", ], 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } my $ex; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client->login($user, $passwd); my ($resp_code, $resp_msg) = $client->pwd(); my $expected; $expected = 257; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "\"/\" is the current directory"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); my $conn = $client->mlsd_raw('0001a-input'); unless ($conn) { die("Failed to MLSD: " . $client->response_code() . " " . $client->response_msg()); } my $buf; $conn->read($buf, 8192, 30); eval { $conn->close() }; # We have to be careful of the fact that readdir returns directory # entries in an unordered fashion. my $res = {}; my $lines = [split(/\n/, $buf)]; foreach my $line (@$lines) { if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) { $res->{$1} = 1; } } unless (scalar(keys(%$res)) > 0) { die("MLSD data unexpectedly empty"); } $expected = { '.' => 1, '..' => 1, 'asgard' => 1, }; my $ok = 1; my $mismatch; foreach my $name (keys(%$res)) { unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in MLSD data") } $client->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { test_append_logfile($log_file, $ex); unlink($log_file); die($ex); } unlink($log_file); } 1; proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/Tests/Modules/mod_vroot/000077500000000000000000000000001302754346600241115ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/lib/ProFTPD/Tests/Modules/mod_vroot/sftp.pm000066400000000000000000001770121302754346600254330ustar00rootroot00000000000000package ProFTPD::Tests::Modules::mod_vroot::sftp; use lib qw(t/lib); use base qw(ProFTPD::TestSuite::Child); use strict; use Cwd; use Digest::MD5; use File::Path qw(mkpath rmtree); use File::Spec; use IO::Handle; use POSIX qw(:fcntl_h); use ProFTPD::TestSuite::FTP; use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); $| = 1; my $order = 0; my $TESTS = { vroot_alias_file_sftp_read => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_write_no_overwrite => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_write => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_stat => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_lstat => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_realpath => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_sftp_remove => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_dir_sftp_readdir => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_dir_sftp_rmdir => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_symlink_sftp_stat => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_symlink_sftp_lstat => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_symlink_sftp_realpath => { order => ++$order, test_class => [qw(forking mod_sftp sftp)], }, vroot_alias_file_scp_download => { order => ++$order, test_class => [qw(forking mod_sftp scp)], }, vroot_alias_file_scp_upload => { order => ++$order, test_class => [qw(forking mod_sftp scp)], }, }; sub new { return shift()->SUPER::new(@_); } sub list_tests { return testsuite_get_runnable_tests($TESTS); # XXX test file aliases where the alias includes directories which do not # exist. Should we allow traversal of these kinds of aliases (if so, what # real directory do we use for perms, ownership? What would a CWD into # such a path component mean?), or only allow retrieval/storage to that # alias but not traversal? } sub set_up { my $self = shift; $self->SUPER::set_up(@_); # Make sure that mod_sftp does not complain about permissions on the hostkey # files. my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); unless (chmod(0400, $rsa_host_key, $dsa_host_key)) { die("Can't set perms on $rsa_host_key, $dsa_host_key: $!"); } } sub vroot_alias_file_sftp_read { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $fh = $sftp->open('bar.txt', O_RDONLY); unless ($fh) { my ($err_code, $err_name) = $sftp->error(); die("Can't open bar.txt: [$err_name] ($err_code)"); } my $buf; my $size = 0; my $res = $fh->read($buf, 8192, 30); while ($res) { $size += $res; $res = $fh->read($buf, 8192, 30); } # To issue the FXP_CLOSE, we have to explicit destroy the filehandle $fh = undef; my $expected = 14; $self->assert($expected == $size, test_msg("Expected $expected, got $size")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_write_no_overwrite { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $fh = $sftp->open('bar.txt', O_WRONLY|O_TRUNC, 0644); if ($fh) { die("OPEN bar.txt succeeded unexpectedly"); } my ($err_code, $err_name) = $sftp->error(); my $expected = 'SSH_FX_PERMISSION_DENIED'; $self->assert($expected eq $err_name, test_msg("Expected '$expected', got '$err_name'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_write { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $fh = $sftp->open('bar.txt', O_WRONLY|O_TRUNC, 0644); unless ($fh) { my ($err_code, $err_name) = $sftp->error(); die("Can't open bar.txt: [$err_name] ($err_code)"); } my $count = 20; for (my $i = 0; $i < $count; $i++) { print $fh "ABCD" x 4096; } # To issue the FXP_CLOSE, we have to explicit destroy the filehandle $fh = undef; $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_stat { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $attrs = $sftp->stat('bar.txt', 1); unless ($attrs) { my ($err_code, $err_name) = $sftp->error(); die("FXP_STAT bar.txt failed: [$err_name] ($err_code)"); } my $expected = 14; my $file_size = $attrs->{size}; $self->assert($expected == $file_size, test_msg("Expected $expected, got $file_size")); $expected = $<; my $file_uid = $attrs->{uid}; $self->assert($expected == $file_uid, test_msg("Expected '$expected', got '$file_uid'")); $expected = $(; my $file_gid = $attrs->{gid}; $self->assert($expected == $file_gid, test_msg("Expected '$expected', got '$file_gid'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_lstat { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $attrs = $sftp->stat('bar.txt', 0); unless ($attrs) { my ($err_code, $err_name) = $sftp->error(); die("FXP_STAT bar.txt failed: [$err_name] ($err_code)"); } my $expected = 14; my $file_size = $attrs->{size}; $self->assert($expected == $file_size, test_msg("Expected $expected, got $file_size")); $expected = $<; my $file_uid = $attrs->{uid}; $self->assert($expected == $file_uid, test_msg("Expected '$expected', got '$file_uid'")); $expected = $(; my $file_gid = $attrs->{gid}; $self->assert($expected == $file_gid, test_msg("Expected '$expected', got '$file_gid'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_realpath { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $real_path = $sftp->realpath('bar.txt'); unless ($real_path) { my ($err_code, $err_name) = $sftp->error(); die("Can't get real path for 'bar.txt': [$err_name] ($err_code)"); } my $expected; $expected = '/bar.txt'; $self->assert($expected eq $real_path, test_msg("Expected '$expected', got '$real_path'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_sftp_remove { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $res = $sftp->unlink('bar.txt'); if ($res) { die("REMOVE bar.txt succeeded unexpectedly"); } my ($err_code, $err_name) = $sftp->error(); my $expected = 'SSH_FX_PERMISSION_DENIED'; $self->assert($expected eq $err_name, test_msg("Expected '$expected', got '$err_name'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_dir_sftp_readdir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $test_file1 = File::Spec->rel2abs("$tmpdir/foo.d/a.txt"); if (open(my $fh, "> $test_file1")) { close($fh); } else { die("Can't open $test_file1: $!"); } my $test_file2 = File::Spec->rel2abs("$tmpdir/foo.d/b.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $test_file3 = File::Spec->rel2abs("$tmpdir/foo.d/c.txt"); if (open(my $fh, "> $test_file3")) { close($fh); } else { die("Can't open $test_file3: $!"); } my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $dir = $sftp->opendir('bar.d'); unless ($dir) { my ($err_code, $err_name) = $sftp->error(); die("Can't open directory 'bar.d': [$err_name] ($err_code)"); } my $res = {}; my $file = $dir->read(); while ($file) { $res->{$file->{name}} = $file; $file = $dir->read(); } my $expected = { '.' => 1, '..' => 1, 'a.txt' => 1, 'b.txt' => 1, 'c.txt' => 1, }; # To issue the FXP_CLOSE, we have to explicit destroy the dirhandle $dir = undef; $ssh2->disconnect(); my $ok = 1; my $mismatch; my $seen = []; foreach my $name (keys(%$res)) { push(@$seen, $name); unless (defined($expected->{$name})) { $mismatch = $name; $ok = 0; last; } } unless ($ok) { die("Unexpected name '$mismatch' appeared in READDIR data") } # Now remove from $expected all of the paths we saw; if there are # any entries remaining in $expected, something went wrong. foreach my $name (@$seen) { delete($expected->{$name}); } my $remaining = scalar(keys(%$expected)); $self->assert(0 == $remaining, test_msg("Expected 0, got $remaining")); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_dir_sftp_rmdir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_dir = File::Spec->rel2abs("$tmpdir/foo.d"); mkpath($src_dir); my $dst_dir = '~/bar.d'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_dir $dst_dir", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $res = $sftp->rmdir('/bar.d'); if ($res) { die("RMDIR bar.d succeeded unexpectedly"); } my ($err_code, $err_name) = $sftp->error(); my $expected = 'SSH_FX_PERMISSION_DENIED'; $self->assert($expected eq $err_name, test_msg("Expected '$expected', got '$err_name'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_symlink_sftp_stat { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $attrs = $sftp->stat('bar.lnk', 1); unless ($attrs) { my ($err_code, $err_name) = $sftp->error(); die("FXP_STAT bar.txt failed: [$err_name] ($err_code)"); } my $expected = 14; my $file_size = $attrs->{size}; $self->assert($expected == $file_size, test_msg("Expected $expected, got $file_size")); $expected = $<; my $file_uid = $attrs->{uid}; $self->assert($expected == $file_uid, test_msg("Expected '$expected', got '$file_uid'")); $expected = $(; my $file_gid = $attrs->{gid}; $self->assert($expected == $file_gid, test_msg("Expected '$expected', got '$file_gid'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_symlink_sftp_lstat { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $attrs = $sftp->stat('bar.lnk', 0); unless ($attrs) { my ($err_code, $err_name) = $sftp->error(); die("FXP_STAT bar.txt failed: [$err_name] ($err_code)"); } my $expected = 14; my $file_size = $attrs->{size}; $self->assert($expected == $file_size, test_msg("Expected $expected, got $file_size")); $expected = $<; my $file_uid = $attrs->{uid}; $self->assert($expected == $file_uid, test_msg("Expected '$expected', got '$file_uid'")); $expected = $(; my $file_gid = $attrs->{gid}; $self->assert($expected == $file_gid, test_msg("Expected '$expected', got '$file_gid'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_symlink_sftp_realpath { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $cwd = getcwd(); unless (chdir($home_dir)) { die("Can't chdir to $home_dir: $!"); } unless (symlink("./foo.txt", "foo.lnk")) { die("Can't symlink './foo.txt' to 'foo.lnk': $!"); } unless (chdir($cwd)) { die("Can't chdir to $cwd: $!"); } my $src_symlink = File::Spec->rel2abs("$tmpdir/foo.lnk"); my $dst_file = '~/bar.lnk'; my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, VRootOptions => 'allowSymlinks', DefaultRoot => '~', VRootAlias => "$src_symlink $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $sftp = $ssh2->sftp(); unless ($sftp) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); } my $real_path = $sftp->realpath('bar.lnk'); unless ($real_path) { my ($err_code, $err_name) = $sftp->error(); die("FXP_REALPATH bar.lnk failed: [$err_name] ($err_code)"); } my $expected = '/bar.lnk'; $self->assert($expected eq $real_path, test_msg("Expected '$expected', got '$real_path'")); $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_scp_download { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $test_file = File::Spec->rel2abs("tmpdir/test.txt"); my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $res = $ssh2->scp_get('/bar.txt', $test_file); unless ($res) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't download bar.txt from server: [$err_name] ($err_code) $err_str"); } $ssh2->disconnect(); unless (-f $test_file) { die("$test_file file does not exist as expected"); } }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } sub vroot_alias_file_scp_upload { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/vroot.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/vroot.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/vroot.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/vroot.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/vroot.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; # Make sure that, if we're running as root, that the home directory has # permissions/privs set for the account we create if ($< == 0) { unless (chmod(0755, $home_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir, '/bin/bash'); auth_group_write($auth_group_file, 'ftpd', $gid, $user); my $src_file = File::Spec->rel2abs("$tmpdir/foo.txt"); if (open(my $fh, "> $src_file")) { print $fh "Hello, World!\n"; unless (close($fh)) { die("Can't write $src_file: $!"); } } else { die("Can't open $src_file: $!"); } my $dst_file = '~/bar.txt'; my $test_file = File::Spec->rel2abs("tmpdir/test.txt"); my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'fsio:10', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_sftp.c' => [ "SFTPEngine on", "SFTPLog $log_file", "SFTPHostKey $rsa_host_key", "SFTPHostKey $dsa_host_key", ], 'mod_vroot.c' => { VRootEngine => 'on', VRootLog => $log_file, DefaultRoot => '~', VRootAlias => "$src_file $dst_file", }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); # Open pipes, for use between the parent and child processes. Specifically, # the child will indicate when it's done with its test by writing a message # to the parent. my ($rfh, $wfh); unless (pipe($rfh, $wfh)) { die("Can't open pipe: $!"); } require Net::SSH2; my $ex; # Ignore SIGPIPE local $SIG{PIPE} = sub { }; # Fork child $self->handle_sigchld(); defined(my $pid = fork()) or die("Can't fork: $!"); if ($pid) { eval { my $ssh2 = Net::SSH2->new(); sleep(1); unless ($ssh2->connect('127.0.0.1', $port)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); } unless ($ssh2->auth_password($user, $passwd)) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); } my $res = $ssh2->scp_put($config_file, '/bar.txt'); unless ($res) { my ($err_code, $err_name, $err_str) = $ssh2->error(); die("Can't upload bar.txt to server: [$err_name] ($err_code) $err_str"); } $ssh2->disconnect(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($config_file, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($pid_file); $self->assert_child_ok($pid); if ($ex) { die($ex); } unlink($log_file); } 1; proftpd-mod-vroot-0.9.4/t/modules/000077500000000000000000000000001302754346600170535ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/modules/mod_vroot.t000066400000000000000000000002651302754346600212530ustar00rootroot00000000000000#!/usr/bin/env perl use lib qw(t/lib); use strict; use Test::Unit::HarnessUnit; $| = 1; my $r = Test::Unit::HarnessUnit->new(); $r->start("ProFTPD::Tests::Modules::mod_vroot"); proftpd-mod-vroot-0.9.4/t/modules/mod_vroot/000077500000000000000000000000001302754346600210635ustar00rootroot00000000000000proftpd-mod-vroot-0.9.4/t/modules/mod_vroot/sftp.t000066400000000000000000000002731302754346600222260ustar00rootroot00000000000000#!/usr/bin/env perl use lib qw(t/lib); use strict; use Test::Unit::HarnessUnit; $| = 1; my $r = Test::Unit::HarnessUnit->new(); $r->start("ProFTPD::Tests::Modules::mod_vroot::sftp");