proftpd-mod_counter-0.6.2/0000775000175000001440000000000013623576613014543 5ustar hilleusersproftpd-mod_counter-0.6.2/mod_counter.c0000664000175000001440000010234213302554542017216 0ustar hilleusers/* * ProFTPD: mod_counter -- a module for using counters to enforce per-file usage * Copyright (c) 2004-2018 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_counter, contrib software for proftpd 1.3.x and above. * For more information contact TJ Saunders . */ #include "conf.h" #include "privs.h" #include "mod_ctrls.h" #include #include #define MOD_COUNTER_VERSION "mod_counter/0.6.2" #if PROFTPD_VERSION_NUMBER < 0x0001030201 # error "ProFTPD 1.3.2rc1 or later required" #endif #define COUNTER_PROJ_ID 247 #define COUNTER_NSEMS 3 #define COUNTER_READER_SEMNO 0 #define COUNTER_WRITER_SEMNO 1 #define COUNTER_NPROCS_SEMNO 2 #define COUNTER_DEFAULT_MAX_READERS 0 #define COUNTER_DEFAULT_MAX_WRITERS 1 module counter_module; struct counter_fh { struct counter_fh *next, *prev; const char *area; size_t arealen; int isglob; pr_fh_t *fh; }; static pool *counter_pool = NULL; static xaset_t *counter_fhs = NULL; static const char *counter_chroot_path = NULL; static const char *counter_curr_path = NULL; static int counter_curr_semid = -1; static int counter_engine = FALSE; static int counter_max_readers = COUNTER_DEFAULT_MAX_READERS; static int counter_max_writers = COUNTER_DEFAULT_MAX_WRITERS; static int counter_logfd = -1; static int counter_pending = 0; #define COUNTER_HAVE_READER 0x01 #define COUNTER_HAVE_WRITER 0x02 #if (defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)) || \ defined(DARWIN9) || defined(DARWIN10) || defined(DARWIN11) || \ defined(DARWIN12) || defined(DARWIN13) || defined(DARWIN14) || \ defined(DARWIN15) || defined(DARWIN16) || defined(DARWIN17) #else union semun { int val; struct semid_ds *buf; unsigned short int *array; struct seminfo *__buf; }; #endif #ifndef HAVE_FLOCK # define LOCK_SH 1 # define LOCK_EX 2 # define LOCK_UN 8 # define LOCK_NB 4 #endif /* HAVE_FLOCK */ static int counter_file_lock(pr_fh_t *, int); static array_header *counter_file_read(pr_fh_t *); static int counter_file_write(pr_fh_t *, array_header *); static int counter_set_procs(int); static int counter_set_readers(int); static int counter_set_writers(int); /* Support routines */ static int counter_add_reader(int semid) { struct sembuf s[2]; s[0].sem_num = COUNTER_READER_SEMNO; s[0].sem_op = -1; s[0].sem_flg = IPC_NOWAIT|SEM_UNDO; s[1].sem_num = COUNTER_NPROCS_SEMNO; s[1].sem_op = -1; s[1].sem_flg = IPC_NOWAIT|SEM_UNDO; return semop(semid, s, 2); } static int counter_add_writer(int semid) { struct sembuf s[2]; s[0].sem_num = COUNTER_WRITER_SEMNO; s[0].sem_op = -1; s[0].sem_flg = IPC_NOWAIT|SEM_UNDO; s[1].sem_num = COUNTER_NPROCS_SEMNO; s[1].sem_op = -1; s[1].sem_flg = IPC_NOWAIT|SEM_UNDO; return semop(semid, s, 2); } static int counter_file_add_id(pr_fh_t *fh, int semid) { int res; array_header *ids; if (counter_file_lock(fh, LOCK_EX) < 0) { return -1; } ids = counter_file_read(fh); if (ids == NULL) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return -1; } *((int *) push_array(ids)) = semid; res = counter_file_write(fh, ids); counter_file_lock(fh, LOCK_UN); return res; } static int counter_file_lock(pr_fh_t *fh, int op) { static int counter_have_lock = FALSE; #ifdef HAVE_FLOCK int res; #else int flag; struct flock lock; #endif /* HAVE_FLOCK */ if (counter_have_lock && ((op & LOCK_SH) || (op & LOCK_EX))) { return 0; } if (!counter_have_lock && (op & LOCK_UN)) { return 0; } #ifdef HAVE_FLOCK res = flock(fh->fh_fd, op); if (res == 0) { if ((op & LOCK_SH) || (op & LOCK_EX)) { counter_have_lock = TRUE; } else if (op & LOCK_UN) { counter_have_lock = FALSE; } } return res; #else flag = F_SETLKW; lock.l_whence = 0; lock.l_start = lock.l_len = 0; if (op & LOCK_SH) { lock.l_type = F_RDLCK; } else if (op & LOCK_EX) { lock.l_type = F_WRLCK; } else if (op & LOCK_UN) { lock.l_type= F_UNLCK; } else { errno = EINVAL; return -1; } if (op & LOCK_NB) { flag = F_SETLK; } while (fcntl(fh->fh_fd, flag, &lock) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } return -1; } if ((op & LOCK_SH) || (op & LOCK_EX)) { counter_have_lock = TRUE; } else if (op & LOCK_UN) { counter_have_lock = FALSE; } return 0; #endif /* HAVE_FLOCK */ } static array_header *counter_file_read(pr_fh_t *fh) { char buf[PR_TUNABLE_BUFFER_SIZE]; array_header *ids = make_array(counter_pool, 0, sizeof(int)); /* Read the list of IDs in the CounterFile into an array. */ if (counter_file_lock(fh, LOCK_SH) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error read-locking CounterFile '%s': %s", fh->fh_path, strerror(errno)); } if (pr_fsio_lseek(fh, 0, SEEK_SET) < 0) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return NULL; } memset(buf, '\0', sizeof(buf)); while (pr_fsio_gets(buf, sizeof(buf), fh) != NULL) { int id; pr_signals_handle(); id = atoi(buf); if (id < 0) { continue; } *((int *) push_array(ids)) = id; } if (counter_file_lock(fh, LOCK_UN) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error unlocking CounterFile '%s': %s", fh->fh_path, strerror(errno)); } return ids; } static int counter_file_remove_id(pr_fh_t *fh, int semid) { register unsigned int i; int res; array_header *ids; int *semids; if (counter_file_lock(fh, LOCK_EX) < 0) { return -1; } ids = counter_file_read(fh); if (ids == NULL) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return -1; } semids = (int *) ids->elts; for (i = 0; i < ids->nelts; i++) { if (semids[i] == semid) { semids[i] = -1; break; } } res = counter_file_write(fh, ids); counter_file_lock(fh, LOCK_UN); return res; } static int counter_file_write(pr_fh_t *fh, array_header *ids) { register unsigned int i; int *elts; /* Write the list of IDs in the given array to the CounterFile, * overwriting any previous values. */ if (counter_file_lock(fh, LOCK_EX) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error write-locking CounterFile '%s': %s", fh->fh_path, strerror(errno)); } if (pr_fsio_lseek(fh, 0, SEEK_SET) < 0) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return -1; } elts = (int *) ids->elts; for (i = 0; i < ids->nelts; i++) { char buf[32]; pr_signals_handle(); /* Skip any negative IDs. This small hack allows for IDs to be * effectively removed from the list. */ if (elts[i] < 0) { continue; } memset(buf, '\0', sizeof(buf)); snprintf(buf, sizeof(buf), "%d\n", elts[i]); buf[sizeof(buf)-1] = '\0'; buf[strlen(buf)-1] = '\0'; if (pr_fsio_puts(buf, fh) < 0) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return -1; } } if (pr_fsio_ftruncate(fh, 0) < 0) { int xerrno = errno; counter_file_lock(fh, LOCK_UN); errno = xerrno; return -1; } if (counter_file_lock(fh, LOCK_SH) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error unlocking CounterFile '%s': %s", fh->fh_path, strerror(errno)); } return 0; } static char *counter_abs_path(pool *p, const char *path, int interpolate) { const char *chroot_path; char *abs_path; chroot_path = session.chroot_path; if (counter_chroot_path != NULL) { session.chroot_path = counter_chroot_path; } abs_path = dir_abs_path(p, path, interpolate); session.chroot_path = chroot_path; return abs_path; } static pr_fh_t *counter_get_fh(pool *p, const char *path) { struct counter_fh *iter, *cfh = NULL; const char *abs_path; /* Find the CounterFile handle to use for the given path, if any. */ if (counter_fhs == NULL) { errno = ENOENT; return NULL; } if (session.chroot_path) { abs_path = counter_abs_path(p, path, FALSE); } else { abs_path = path; } /* In order to handle globs, we do two passes. On the first pass, * we look for the closest-matching glob area. On the second pass, * we look for any closest-matching non-glob area. This means that * exact matches override glob matches (as they should). */ for (iter = (struct counter_fh *) counter_fhs->xas_list; iter; iter = iter->next) { pr_signals_handle(); if (!iter->isglob) { continue; } if (cfh == NULL) { /* Haven't found anything matching yet. */ if (pr_fnmatch(iter->area, abs_path, 0) == 0) { cfh = iter; } } else { /* Have a previous match. Is this a closer matching area? */ if (iter->arealen > cfh->arealen && pr_fnmatch(iter->area, abs_path, 0) == 0) { cfh = iter; } } } for (iter = (struct counter_fh *) counter_fhs->xas_list; iter; iter = iter->next) { if (iter->isglob) { continue; } if (cfh == NULL) { /* Haven't found anything matching yet. */ if (strncmp(iter->area, abs_path, iter->arealen) == 0) { cfh = iter; } } else { /* Have a previous match. Is this a closer matching area? */ if (iter->arealen > cfh->arealen && strncmp(iter->area, abs_path, iter->arealen) == 0) { cfh = iter; } } } if (cfh != NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "using CounterFile '%s' covering area '%s' for path '%s'", cfh->fh->fh_path, cfh->area, path); return cfh->fh; } errno = ENOENT; return NULL; } /* Use Perl's hashing algorithm by default. * * Here's a good article about this hashing algorithm, and about hashing * functions in general: * * http://www.perl.com/pub/2002/10/01/hashes.html */ static unsigned int key_hash(const void *key, size_t keysz) { unsigned int i = 0; size_t sz = !keysz ? strlen((const char *) key) : keysz; while (sz--) { const char *k = key; unsigned int c; c = k[sz]; /* Always handle signals in potentially long-running while loops. */ pr_signals_handle(); i = (i * 33) + c; } return i; } static key_t counter_get_key(const char *path) { size_t pathlen; unsigned int h; key_t key; /* ftok() uses stat(2) on the given path, which means that it needs to exist. * So we AVOID using ftok(3), and instead generate the key value ourselves. */ pathlen = strlen(path); h = key_hash(path, pathlen); key = h & COUNTER_PROJ_ID; return key; } static int counter_get_sem(pr_fh_t *fh, const char *path) { int semid; key_t key; /* Obtain a key for this path. */ key = counter_get_key(path); if (key == (key_t) -1) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get key for '%s': %s", path, strerror(errno)); return -1; } /* Try first using IPC_CREAT|IPC_EXCL, to check if there is an existing * semaphore set for this key. If there is, try again, using a flag of * zero. */ semid = semget(key, COUNTER_NSEMS, IPC_CREAT|IPC_EXCL|0666); if (semid < 0) { if (errno == EEXIST) { semid = semget(key, 0, 0); } else { return -1; } } else { /* Set the values of the newly created semaphore to the configured * CounterMaxReaders and CounterMaxWriters. */ if (counter_set_readers(semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error setting readers (semaphore ID %d): %s", semid, strerror(errno)); } if (counter_set_writers(semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error setting writers (semaphore ID %d): %s", semid, strerror(errno)); } if (counter_set_procs(semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error setting procs (semaphore ID %d): %s", semid, strerror(errno)); } /* Record the ID of the created semaphore in the CounterFile. */ if (counter_file_add_id(fh, semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error recording semaphore (semaphore ID %d) in CounterFile '%s': %s", semid, fh->fh_path, strerror(errno)); } } return semid; } static int counter_remove_reader(pr_fh_t *fh, int semid) { struct sembuf s[2]; s[0].sem_num = COUNTER_READER_SEMNO; s[0].sem_op = 1; s[0].sem_flg = IPC_NOWAIT|SEM_UNDO; s[1].sem_num = COUNTER_NPROCS_SEMNO; s[1].sem_op = 1; s[1].sem_flg = IPC_NOWAIT|SEM_UNDO; if (semop(semid, s, 2) < 0) { return -1; } if (semctl(semid, 0, IPC_RMID, s) < 0) { return -1; } return counter_file_remove_id(fh, semid); } static int counter_remove_writer(pr_fh_t *fh, int semid) { struct sembuf s[2]; s[0].sem_num = COUNTER_WRITER_SEMNO; s[0].sem_op = 1; s[0].sem_flg = IPC_NOWAIT|SEM_UNDO; s[1].sem_num = COUNTER_NPROCS_SEMNO; s[1].sem_op = 1; s[1].sem_flg = IPC_NOWAIT|SEM_UNDO; if (semop(semid, s, 2) < 0) { return -1; } if (semctl(semid, 0, IPC_RMID, s) < 0) { return -1; } return counter_file_remove_id(fh, semid); } static int counter_set_procs(int semid) { union semun arg; arg.val = counter_max_readers + counter_max_writers; return semctl(semid, COUNTER_NPROCS_SEMNO, SETVAL, arg); } static int counter_set_readers(int semid) { union semun arg; arg.val = counter_max_readers; return semctl(semid, COUNTER_READER_SEMNO, SETVAL, arg); } static int counter_set_writers(int semid) { union semun arg; arg.val = counter_max_writers; return semctl(semid, COUNTER_WRITER_SEMNO, SETVAL, arg); } /* Configuration handlers */ /* usage: CounterEngine on|off */ MODRET set_counterengine(cmd_rec *cmd) { int engine = -1; config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); engine = get_boolean(cmd, 1); if (engine == -1) { CONF_ERROR(cmd, "expected Boolean parameter"); } c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = engine; return PR_HANDLED(cmd); } /* usage: CounterFile path */ MODRET set_counterfile(cmd_rec *cmd) { config_rec *c; const char *path; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR); path = cmd->argv[1]; if (*path != '/') { CONF_ERROR(cmd, "must be an absolute path"); } /* In theory, we could open a filehandle on the configured path right * here, and fail if the file couldn't be created/opened. Then we * could just stash that filehandle in the cmd_rec. Easy. * * However, that would mean that we would have open descriptors for * vhosts to which the client may not connect. We would also need to * use pr_fs_get_usable_fd() so that these filehandles don't use the wrong * fds. Instead, then, we wait to open the filehandles in sess_init(), * where we know vhost to which the client connected. */ c = add_config_param_str(cmd->argv[0], 1, path); c->flags |= CF_MERGEDOWN; return PR_HANDLED(cmd); } /* usage: CounterLog path|"none" */ MODRET set_counterlog(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"); } add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); return PR_HANDLED(cmd); } /* usage: * CounterMaxReaders max * CounterMaxWriters max */ MODRET set_countermaxreaderswriters(cmd_rec *cmd) { int count; config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON|CONF_DIR); /* A count of zero means that an unlimited number of readers (or writers), * as is the default without this module, is in effect. */ count = atoi(cmd->argv[1]); if (count < 0 || count > INT_MAX) { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "invalid number: ", cmd->argv[1], NULL)); } c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = pcalloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = count; c->flags |= CF_MERGEDOWN; return PR_HANDLED(cmd); } /* Command handlers */ MODRET counter_retr(cmd_rec *cmd) { config_rec *c; int res; pr_fh_t *fh; const char *path; if (counter_engine == FALSE) { return PR_DECLINED(cmd); } c = find_config(CURRENT_CONF, CONF_PARAM, "CounterMaxReaders", FALSE); counter_max_readers = c ? *((int *) c->argv[0]) : COUNTER_DEFAULT_MAX_READERS; if (counter_max_readers == 0) { return PR_DECLINED(cmd); } path = pr_table_get(cmd->notes, "mod_xfer.retr-path", NULL); if (path == NULL) { return PR_DECLINED(cmd); } /* Note that for purposes of our semaphores, we need to use the absolute * path. */ counter_curr_path = counter_abs_path(cmd->pool, path, FALSE); fh = counter_get_fh(cmd->tmp_pool, counter_curr_path); if (fh == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: no CounterFile found for path '%s'", (char *) cmd->argv[0], counter_curr_path); /* No CounterFile configured/available for this path. */ return PR_DECLINED(cmd); } counter_curr_semid = counter_get_sem(fh, counter_curr_path); if (counter_curr_semid < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get semaphore for '%s': %s", counter_curr_path, strerror(errno)); return PR_DECLINED(cmd); } /* Add a reader to this file by decrementing the reader counter value. * This functions as a sort of "lock". */ res = counter_add_reader(counter_curr_semid); if (res < 0 && errno == EAGAIN) { /* The lock acquisition failed, which means the file is busy. * The download should be failed. */ (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: max number of readers (%d) reached for '%s'", (char *) cmd->argv[0], counter_max_readers, counter_curr_path); pr_response_add_err(R_450, _("%s: File busy"), cmd->arg); return PR_ERROR(cmd); } counter_pending |= COUNTER_HAVE_READER; (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: added reader counter for '%s' (semaphore ID %d)", (char *) cmd->argv[0], counter_curr_path, counter_curr_semid); return PR_DECLINED(cmd); } /* Handles the DELE, RNFR, and RNTO commands. */ MODRET counter_alter(cmd_rec *cmd) { config_rec *c; int res; pr_fh_t *fh; const char *path; if (counter_engine == FALSE) { return PR_DECLINED(cmd); } c = find_config(CURRENT_CONF, CONF_PARAM, "CounterMaxWriters", FALSE); counter_max_writers = c ? *((int *) c->argv[0]) : COUNTER_DEFAULT_MAX_WRITERS; if (counter_max_writers == 0) { return PR_DECLINED(cmd); } path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg); if (!exists((char *) path)) { return PR_DECLINED(cmd); } /* The semaphores operate using dir_best_path(). */ path = dir_best_path(cmd->tmp_pool, path); if (path == NULL) { return PR_DECLINED(cmd); } counter_curr_path = path; fh = counter_get_fh(cmd->tmp_pool, counter_curr_path); if (fh == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: no CounterFile found for path '%s'", (char *) cmd->argv[0], counter_curr_path); /* No CounterFile configured/available for this path. */ return PR_DECLINED(cmd); } counter_curr_semid = counter_get_sem(fh, counter_curr_path); if (counter_curr_semid < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get semaphore for '%s': %s", counter_curr_path, strerror(errno)); return PR_DECLINED(cmd); } /* Add a writer to this file by decrementing the writer counter value. * This functions as a sort of "lock". */ res = counter_add_writer(counter_curr_semid); if (res < 0 && errno == EAGAIN) { /* The lock acquisition failed, which means the file is busy. * The upload should be failed. */ (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: max number of writers (%d) reached for '%s'", (char *) cmd->argv[0], counter_max_writers, counter_curr_path); pr_response_add_err(R_450, _("%s: File busy"), cmd->arg); return PR_ERROR(cmd); } counter_pending |= COUNTER_HAVE_WRITER; (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: added writer counter for '%s' (semaphore ID %d)", (char *) cmd->argv[0], counter_curr_path, counter_curr_semid); return PR_DECLINED(cmd); } MODRET counter_stor(cmd_rec *cmd) { config_rec *c; int res; pr_fh_t *fh; const char *path; if (counter_engine == FALSE) { return PR_DECLINED(cmd); } c = find_config(CURRENT_CONF, CONF_PARAM, "CounterMaxWriters", FALSE); counter_max_writers = c ? *((int *) c->argv[0]) : COUNTER_DEFAULT_MAX_WRITERS; if (counter_max_writers == 0) { return PR_DECLINED(cmd); } path = pr_table_get(cmd->notes, "mod_xfer.store-path", NULL); if (path == NULL) { return PR_DECLINED(cmd); } /* Note that for purposes of our semaphores, we need to use the absolute * path. */ counter_curr_path = counter_abs_path(cmd->pool, path, FALSE); fh = counter_get_fh(cmd->tmp_pool, counter_curr_path); if (fh == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: no CounterFile found for path '%s'", (char *) cmd->argv[0], counter_curr_path); /* No CounterFile configured/available for this path. */ return PR_DECLINED(cmd); } counter_curr_semid = counter_get_sem(fh, counter_curr_path); if (counter_curr_semid < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get semaphore for '%s': %s", counter_curr_path, strerror(errno)); return PR_DECLINED(cmd); } /* Add a writer to this file by decrementing the writer counter value. * This functions as a sort of "lock". */ res = counter_add_writer(counter_curr_semid); if (res < 0 && errno == EAGAIN) { /* The lock acquisition failed, which means the file is busy. * The upload should be failed. */ (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: max number of writers (%d) reached for '%s'", (char *) cmd->argv[0], counter_max_writers, counter_curr_path); pr_response_add_err(R_450, _("%s: File busy"), cmd->arg); return PR_ERROR(cmd); } counter_pending |= COUNTER_HAVE_WRITER; (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: added writer counter for '%s' (semaphore ID %d)", (char *) cmd->argv[0], counter_curr_path, counter_curr_semid); return PR_DECLINED(cmd); } MODRET counter_reader_done(cmd_rec *cmd) { pr_fh_t *fh; if (counter_engine == FALSE) { return PR_DECLINED(cmd); } if (!(counter_pending & COUNTER_HAVE_READER)) { return PR_DECLINED(cmd); } fh = counter_get_fh(cmd->tmp_pool, counter_curr_path); if (fh == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: no CounterFile found for path '%s'", (char *) cmd->argv[0], counter_curr_path); /* No CounterFile configured/available for this path. */ return PR_DECLINED(cmd); } if (counter_curr_semid == -1) { counter_curr_semid = counter_get_sem(fh, counter_curr_path); if (counter_curr_semid < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get semaphore for '%s': %s", counter_curr_path, strerror(errno)); return PR_DECLINED(cmd); } } if (counter_remove_reader(fh, counter_curr_semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error removing reader for '%s': %s", counter_curr_path, strerror(errno)); } else { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "removed reader counter for '%s' (semaphore ID %d)", counter_curr_path, counter_curr_semid); counter_curr_path = NULL; counter_curr_semid = -1; counter_pending &= ~COUNTER_HAVE_READER; } return PR_DECLINED(cmd); } MODRET counter_writer_done(cmd_rec *cmd) { pr_fh_t *fh; if (counter_engine == FALSE) { return PR_DECLINED(cmd); } if (!(counter_pending & COUNTER_HAVE_WRITER)) { return PR_DECLINED(cmd); } fh = counter_get_fh(cmd->tmp_pool, counter_curr_path); if (fh == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "%s: no CounterFile found for path '%s'", (char *) cmd->argv[0], counter_curr_path); /* No CounterFile configured/available for this path. */ return PR_DECLINED(cmd); } if (counter_curr_semid == -1) { counter_curr_semid = counter_get_sem(fh, counter_curr_path); if (counter_curr_semid < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unable to get semaphore for '%s': %s", counter_curr_path, strerror(errno)); return PR_DECLINED(cmd); } } if (counter_remove_writer(fh, counter_curr_semid) < 0) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "error removing writer for '%s': %s", counter_curr_path, strerror(errno)); } else { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "removed writer counter for '%s' (semaphore ID %d)", counter_curr_path, counter_curr_semid); counter_curr_path = NULL; counter_curr_semid = -1; counter_pending &= ~COUNTER_HAVE_WRITER; } return PR_DECLINED(cmd); } /* Event handlers */ #if defined(PR_SHARED_MODULE) static void counter_mod_unload_ev(const void *event_data, void *user_data) { if (strcmp("mod_counter.c", (const char *) event_data) == 0) { pr_event_unregister(&counter_module, NULL, NULL); if (counter_pool) { destroy_pool(counter_pool); } } } #endif static void counter_chroot_ev(const void *event_data, void *user_data) { counter_chroot_path = pstrdup(counter_pool, event_data); } static void counter_exit_ev(const void *event_data, void *user_data) { pr_fh_t *fh; if (counter_engine == FALSE) { return; } if (counter_curr_semid != -1 && (counter_pending & COUNTER_HAVE_READER)) { fh = counter_get_fh(counter_pool, counter_curr_path); if (fh != NULL) { counter_remove_reader(fh, counter_curr_semid); } } if (counter_curr_semid != -1 && (counter_pending & COUNTER_HAVE_WRITER)) { if (fh == NULL) { fh = counter_get_fh(counter_pool, counter_curr_path); } if (fh != NULL) { counter_remove_writer(fh, counter_curr_semid); } } } static void counter_restart_ev(const void *event_data, void *user_data) { if (counter_pool) { destroy_pool(counter_pool); } counter_pool = make_sub_pool(permanent_pool); pr_pool_tag(counter_pool, MOD_COUNTER_VERSION); } /* Initialization functions */ static int counter_init(void) { counter_pool = make_sub_pool(permanent_pool); pr_pool_tag(counter_pool, MOD_COUNTER_VERSION); #if defined(PR_SHARED_MODULE) pr_event_register(&counter_module, "core.module-unload", counter_mod_unload_ev, NULL); #endif pr_event_register(&counter_module, "core.restart", counter_restart_ev, NULL); return 0; } static int counter_sess_init(void) { config_rec *c; c = find_config(main_server->conf, CONF_PARAM, "CounterEngine", FALSE); if (c != NULL) { counter_engine = *((int *) c->argv[0]); } if (counter_engine == FALSE) { return 0; } c = find_config(main_server->conf, CONF_PARAM, "CounterLog", FALSE); if (c != NULL) { const char *path = c->argv[0]; if (strcasecmp(path, "none") != 0) { int res, xerrno; PRIVS_ROOT res = pr_log_openfile(path, &counter_logfd, 0660); xerrno = errno; PRIVS_RELINQUISH; if (res < 0) { pr_log_debug(DEBUG2, MOD_COUNTER_VERSION ": error opening CounterLog '%s': %s", path, strerror(xerrno)); counter_logfd = -1; } } } /* Find all CounterFile directives for this vhost, and make sure they * have open handles. We need to do this here, and not in a POST_CMD * PASS handler because of the need to open handles that may be outside * of a chroot. */ c = find_config(main_server->conf, CONF_PARAM, "CounterFile", TRUE); while (c != NULL) { int xerrno = 0; const char *area = NULL, *path; pr_fh_t *fh; struct counter_fh *cfh; pr_signals_handle(); path = c->argv[0]; if (c->parent != NULL) { if (c->parent->config_type == CONF_ANON || c->parent->config_type == CONF_DIR) { area = c->parent->name; } else { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "unhandled configuration parent type (%d) for CounterFile, skipping", c->parent->config_type); c = find_config_next(c, c->next, CONF_PARAM, "CounterFile", TRUE); continue; } } else { /* Toplevel CounterFile directive, in "server config" or * sections. */ area = "/"; } PRIVS_ROOT fh = pr_fsio_open(path, O_RDWR|O_CREAT); xerrno = errno; PRIVS_RELINQUISH if (fh == NULL) { pr_log_debug(DEBUG1, MOD_COUNTER_VERSION ": error opening CounterFile '%s': %s", path, strerror(xerrno)); counter_engine = FALSE; if (counter_fhs != NULL) { for (cfh = (struct counter_fh *) counter_fhs->xas_list; cfh; cfh = cfh->next) { (void) pr_fsio_close(cfh->fh); } } return 0; } (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "opened CounterFile '%s'", path); if (counter_fhs == NULL) { counter_fhs = xaset_create(counter_pool, NULL); } cfh = pcalloc(counter_pool, sizeof(struct counter_fh)); /* Ignore any trailing slash. */ cfh->arealen = strlen(area); if (cfh->arealen > 1 && area[cfh->arealen-1] == '/') { cfh->arealen--; } cfh->area = pstrndup(counter_pool, area, cfh->arealen); /* Mark any areas that use glob(3) characters. */ if (strpbrk(cfh->area, "[*?") != NULL) { cfh->isglob = TRUE; } cfh->fh = fh; xaset_insert(counter_fhs, (xasetmember_t *) cfh); c = find_config_next(c, c->next, CONF_PARAM, "CounterFile", TRUE); } if (counter_fhs == NULL) { (void) pr_log_writefile(counter_logfd, MOD_COUNTER_VERSION, "no CounterFiles configured, disabling module"); counter_engine = FALSE; return 0; } pr_event_register(&counter_module, "core.exit", counter_exit_ev, NULL); /* If mod_vroot is present, we need to do a little more magic to counter * the mod_vroot magic. */ if (pr_module_exists("mod_vroot.c") == TRUE) { pr_event_register(&counter_module, "core.chroot", counter_chroot_ev, NULL); } return 0; } /* Module API tables */ static conftable counter_conftab[] = { { "CounterEngine", set_counterengine, NULL }, { "CounterFile", set_counterfile, NULL }, { "CounterLog", set_counterlog, NULL }, { "CounterMaxReaders", set_countermaxreaderswriters, NULL }, { "CounterMaxWriters", set_countermaxreaderswriters, NULL }, { NULL } }; static cmdtable counter_cmdtab[] = { { CMD, C_RETR, G_NONE, counter_retr, FALSE, FALSE }, { CMD, C_APPE, G_NONE, counter_stor, FALSE, FALSE }, { CMD, C_DELE, G_NONE, counter_alter, FALSE, FALSE }, { CMD, C_RNFR, G_NONE, counter_alter, FALSE, FALSE }, { CMD, C_RNTO, G_NONE, counter_alter, FALSE, FALSE }, { CMD, C_STOR, G_NONE, counter_stor, FALSE, FALSE }, { LOG_CMD, C_RETR, G_NONE, counter_reader_done, FALSE, FALSE }, { LOG_CMD_ERR,C_RETR, G_NONE, counter_reader_done, FALSE, FALSE }, { LOG_CMD, C_APPE, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD_ERR,C_APPE, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD, C_DELE, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD_ERR,C_DELE, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD, C_RNTO, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD_ERR,C_RNTO, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD, C_STOR, G_NONE, counter_writer_done, FALSE, FALSE }, { LOG_CMD_ERR,C_STOR, G_NONE, counter_writer_done, FALSE, FALSE }, { 0, NULL } }; module counter_module = { NULL, NULL, /* Module API version 2.0 */ 0x20, /* Module name */ "counter", /* Module configuration handler table */ counter_conftab, /* Module command handler table */ counter_cmdtab, /* Module authentication handler table */ NULL, /* Module initialization function */ counter_init, /* Session initialization function */ counter_sess_init, /* Module version */ MOD_COUNTER_VERSION }; proftpd-mod_counter-0.6.2/mod_counter.html0000664000175000001440000001636113302554542017745 0ustar hilleusers ProFTPD module mod_counter

ProFTPD module mod_counter



The mod_counter module is designed to allow a sort of "locking" to be enforced when the same file is being uploaded or downloaded by multiple clients at the same time.

The mod_counter works by creating a SysV semaphore for a file being read/written, and placing a number of reader or writer "counters" in that semaphore. When the configured maximum number of counters is reached, the FTP command which seeks to add another reader/writer counter will be denied. This allows site to configure the maximum number of clients which can be reading/writing any file at one time.

This module is contained in the mod_counter.c file for ProFTPD 1.2.x/1.3.x, and is not compiled by default. Installation instructions are discussed here. Example configurations and further details are discussed in the usage section.

The most current version of mod_counter can be found at:

  http://www.castaglia.org/proftpd/

Author

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

Directives


CounterEngine

Syntax: CounterEngine on|off
Default: off
Context: server config, <VirtualHost>, <Global>
Module: mod_counter
Compatibility: 1.2.10rc1 and later

The CounterEngine directive enables or disables the module's runtime counter engine. If it is set to off this module does no "locking". Use this directive to disable the module instead of commenting out all mod_counter directives.


CounterFile

Syntax: CounterFile path
Default: None
Context: server config, <VirtualHost>, <Global>, <Anonymous>, <Directory>
Module: mod_counter
Compatibility: 1.3.2rc1 and later

The CounterFile directive configures a file that mod_counter uses for tracking the semaphores it creates. This directive is required for mod_counter, if enabled, to function.


CounterLog

Syntax: CounterLog path|"none"
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_counter
Compatibility: 1.2.10rc1 and later

The CounterLog directive is used to a specify a log file for mod_counter reporting and debugging, and can be done a per-server basis. The path parameter must be the full path to the file to use for logging. Note that this path must not be to a world-writeable directory and, unless AllowLogSymlinks is explicitly set to on (generally a bad idea), the path must not be a symbolic link.

If path is "none", no logging will be done at all; this setting can be used to override a CounterLog setting inherited from a <Global> context.


CounterMaxReaders

Syntax: CounterMaxReaders max
Default: 0
Context: server config, <VirtualHost>, <Global>, <Anonymous>, <Directory>
Module: mod_counter
Compatibility: 1.2.10rc1 and later

The CounterMaxReaders directive specifies the maximum number of clients allowed to be reading to the same file at the same time. By default, all clients are allowed to read the same file at one time by mod_counter.


CounterMaxWriters

Syntax: CounterMaxWriters max
Default: 1
Context: server config, <VirtualHost>, <Global>, <Anonymous>, <Directory>
Module: mod_counter
Compatibility: 1.2.10rc1 and later

The CounterMaxWriters directive specifies the maximum number of clients allowed to be writing to the same file at the same time. By default, only one client is allowed to write to the same file at one time by mod_counter.


Installation

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

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

  # prxs -c -i -d mod_counter.c


Usage

The mod_counter module pays attention to the following FTP commands:
  • APPE
  • DELE
  • RETR
  • RNFR
  • RNTO
  • STOR
This means, for example, that you can use mod_counter to prevent an uploaded file from being deleted or renamed before the uploading client has finished the upload by using:
  <IfModule mod_counter.c>
    CounterEngine on
    CounterFile /var/proftpd/counter.txt

    # Allow only one client at a time to be writing (including deletes and renames) to a given file.
    CounterMaxWriters 1
  </IfModule>

Likewise, if for some reason you need to limit the number of clients which can be downloading a given file at the same time, you would use the CounterMaxReaders directive:

  <IfModule mod_counter.c>
    CounterEngine on
    CounterFile /var/proftpd/counter.txt

    # Allow only three clients at a time to be reading the same file
    CounterMaxReaders 3
  </IfModule>



© Copyright 2004-2017 TJ Saunders
All Rights Reserved


proftpd-mod_counter-0.6.2/t/0000775000175000001440000000000013302554542014775 5ustar hilleusersproftpd-mod_counter-0.6.2/t/lib/0000775000175000001440000000000013302554542015543 5ustar hilleusersproftpd-mod_counter-0.6.2/t/lib/ProFTPD/0000775000175000001440000000000013302554542016761 5ustar hilleusersproftpd-mod_counter-0.6.2/t/lib/ProFTPD/Tests/0000775000175000001440000000000013302554542020063 5ustar hilleusersproftpd-mod_counter-0.6.2/t/lib/ProFTPD/Tests/Modules/0000775000175000001440000000000013302554542021473 5ustar hilleusersproftpd-mod_counter-0.6.2/t/lib/ProFTPD/Tests/Modules/mod_counter.pm0000664000175000001440000031766313302554542024367 0ustar hilleuserspackage ProFTPD::Tests::Modules::mod_counter; use lib qw(t/lib); use base qw(ProFTPD::TestSuite::Child); use strict; use File::Copy; use File::Path qw(mkpath rmtree); use File::Spec; use IO::Handle; use ProFTPD::TestSuite::FTP; use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite); $| = 1; my $order = 0; my $TESTS = { counter_retr_max_readers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_stor_max_writers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_stor_max_writers_exceeded_hidden_stores_issue6 => { order => ++$order, test_class => [qw(forking)], }, counter_appe_max_writers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_dele_max_writers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_rnfr_max_writers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_rnto_max_writers_exceeded => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_toplevel => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_toplevel_chrooted => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_midlevel => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_midlevel_chrooted => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_bottomlevel => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_bottomlevel_chrooted => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_none => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_none_chrooted => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_using_vhost => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_using_anon => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_using_anon_subdir => { order => ++$order, test_class => [qw(forking rootprivs)], }, counter_closest_matching_file_using_globs => { order => ++$order, test_class => [qw(forking)], }, counter_closest_matching_file_using_globs_and_exact => { order => ++$order, test_class => [qw(forking)], }, counter_vroot_retr_max_readers_exceeded => { order => ++$order, test_class => [qw(forking mod_vroot)], }, counter_vroot_retr_max_readers_exceeded_in_subdir => { order => ++$order, test_class => [qw(forking mod_vroot)], }, counter_vroot_stor_max_writers_exceeded => { order => ++$order, test_class => [qw(forking mod_vroot)], }, counter_vroot_stor_max_writers_exceeded_in_subdir => { order => ++$order, test_class => [qw(forking mod_vroot)], }, }; sub new { return shift()->SUPER::new(@_); } sub list_tests { return testsuite_get_runnable_tests($TESTS); } sub counter_retr_max_readers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterFile => $counter_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{config_file}, $config); if (open(my $fh, ">> $setup->{config_file}")) { print $fh < CounterMaxReaders 1 EOC unless (close($fh)) { die("Can't write $setup->{config_file}: $!"); } } else { die("Can't open $setup->{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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } sub counter_stor_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->stor_raw($test_file); if ($conn2) { die("STOR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_stor_max_writers_exceeded_hidden_stores_issue6 { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, Trace => 'counter:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', HiddenStores => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->stor_raw($test_file); if ($conn2) { die("STOR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 550; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: Temporary hidden file"; $self->assert(qr/$expected/, $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_appe_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', AllowStoreRestart => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->appe_raw($test_file); if ($conn2) { die("APPE $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_dele_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', AllowStoreRestart => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to delete the same file eval { $client2->dele($test_file) }; unless ($@) { die("DELE $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_rnfr_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', AllowStoreRestart => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to rename the same file eval { $client2->rnfr($test_file) }; unless ($@) { die("RNFR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_rnto_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.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 $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.txt'; my $test_file2 = File::Spec->rel2abs("$tmpdir/test2.txt"); if (open(my $fh, "> $test_file2")) { close($fh); } else { die("Can't open $test_file2: $!"); } my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, AllowOverwrite => 'on', AllowStoreRestart => 'on', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterFile => $counter_file, CounterMaxWriters => 1, }, '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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR $test_file: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to rename another file # to file being uploaded $client2->rnfr($test_file2); eval { $client2->rnto($test_file) }; unless ($@) { die("RNTO $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf = "Hello, World!\n"; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_toplevel { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_toplevel_chrooted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_midlevel { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } unless (copy($config_file, "$sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); $client1->cwd("foo"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); $client2->cwd("foo"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_midlevel_chrooted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } unless (copy($config_file, "$sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); $client1->cwd("foo"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); $client2->cwd("foo"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_bottomlevel { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } unless (copy($config_file, "$sub_sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); $client1->cwd("foo/bar"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); $client2->cwd("foo/bar"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_bottomlevel_chrooted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subdir_tab CounterFile $subsubdir_tab EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } unless (copy($config_file, "$sub_sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); $client1->cwd("foo/bar"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); $client2->cwd("foo/bar"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_none { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subsubdir_tab 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); unless ($conn2) { die("RETR $test_file failed unexpectedly: " . $client2->response_code() . " " . $client2->response_msg()); } my $buf; $conn2->read($buf, 8192); $conn2->close(); $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_none_chrooted { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { print $fh < CounterFile $subsubdir_tab 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); unless ($conn2) { die("RETR $test_file failed unexpectedly: " . $client2->response_code() . " " . $client2->response_msg()); } my $buf; $conn2->read($buf, 8192); $conn2->close(); $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_using_vhost { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, Port => '0', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); $port = ProFTPD::TestSuite::Utils::get_high_numbered_port(); if (open(my $fh, ">> $config_file")) { print $fh < ServerName \"TJ's VirtualHost Server\" Port $port AuthUserFile $auth_user_file AuthGroupFile $auth_group_file CounterEngine on CounterLog $log_file CounterMaxReaders 1 CounterFile $toplevel_tab
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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_using_anon { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my ($config_user, $config_group) = config_get_identity(); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $config_user, $passwd, $uid, $gid, '/tmp', '/bin/bash'); auth_group_write($auth_group_file, $config_group, $gid, $user); my $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, Anonymous => { $home_dir => { User => $config_user, Group => $config_group, UserAlias => "anonymous $config_user", RequireValidShell => 'off', CounterFile => $toplevel_tab, CounterMaxReaders => 1, }, }, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my $port; ($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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login('anonymous', 'ftp@nospam.org'); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login('anonymous', 'ftp@nospam.org'); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_using_anon_subdir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my ($config_user, $config_group) = config_get_identity(); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_dir)) { die("Can't set owner of $home_dir to $uid/$gid: $!"); } } auth_user_write($auth_user_file, $config_user, $passwd, $uid, $gid, '/tmp', '/bin/bash'); auth_group_write($auth_group_file, $config_group, $gid, $user); my $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, Anonymous => { $home_dir => { User => $config_user, Group => $config_group, UserAlias => "anonymous $config_user", RequireValidShell => 'off', Directory => { $sub_dir => { CounterMaxReaders => 1, CounterFile => $subdir_tab, }, }, }, }, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my $port; ($port, $config_user, $config_group) = config_write($config_file, $config); unless (copy($config_file, "$sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login('anonymous', 'ftp@nospam.org'); $client1->cwd('foo'); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login('anonymous', 'ftp@nospam.org'); $client2->cwd('foo'); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); } my $buf; $conn->read($buf, 8192); $conn->close(); $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $expected = 226; $self->assert($expected == $resp_code, test_msg("Expected $expected, got $resp_code")); $expected = "Transfer complete"; $self->assert($expected eq $resp_msg, test_msg("Expected '$expected', got '$resp_msg'")); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_closest_matching_file_using_globs { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $sub_dir = File::Spec->rel2abs("$tmpdir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$tmpdir/foo/bar"); mkpath($sub_sub_dir); if ($< == 0) { unless (chmod(0755, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $sub_dir to 0755: $!"); } unless (chown($setup->{uid}, $setup->{gid}, $sub_dir, $sub_sub_dir)) { die("Can't set owner of $sub_dir to $setup->{uid}/$setup->{gid}: $!"); } } my $toplevel_tab = File::Spec->rel2abs("$tmpdir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{config_file}, $config); if (open(my $fh, ">> $setup->{config_file}")) { my $config_sub_dir = $sub_dir; my $config_subsubdir_tab = $subsubdir_tab; if ($^O eq 'darwin') { $config_sub_dir = '/private' . $config_sub_dir; $config_subsubdir_tab = '/private' . $config_subsubdir_tab; } print $fh < CounterFile $config_subsubdir_tab EOC unless (close($fh)) { die("Can't write $setup->{config_file}: $!"); } } else { die("Can't open $setup->{config_file}: $!"); } unless (copy($setup->{config_file}, "$sub_sub_dir/counter.conf")) { die("Can't copy $setup->{config_file} to '$sub_sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); $client1->cwd("foo/bar"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); $client2->cwd("foo/bar"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } sub counter_closest_matching_file_using_globs_and_exact { my $self = shift; my $tmpdir = $self->{tmpdir}; my $config_file = "$tmpdir/counter.conf"; my $pid_file = File::Spec->rel2abs("$tmpdir/counter.pid"); my $scoreboard_file = File::Spec->rel2abs("$tmpdir/counter.scoreboard"); my $log_file = File::Spec->rel2abs('tests.log'); my $auth_user_file = File::Spec->rel2abs("$tmpdir/counter.passwd"); my $auth_group_file = File::Spec->rel2abs("$tmpdir/counter.group"); my $user = 'proftpd'; my $passwd = 'test'; my $home_dir = File::Spec->rel2abs($tmpdir); my $uid = 500; my $gid = 500; my $sub_dir = File::Spec->rel2abs("$home_dir/foo"); mkpath($sub_dir); my $sub_sub_dir = File::Spec->rel2abs("$home_dir/foo/bar"); mkpath($sub_sub_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, $sub_dir, $sub_sub_dir)) { die("Can't set perms on $home_dir to 0755: $!"); } unless (chown($uid, $gid, $home_dir, $sub_dir, $sub_sub_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 $toplevel_tab = File::Spec->rel2abs("$home_dir/counter.tab"); my $subdir_tab = File::Spec->rel2abs("$sub_dir/counter.tab"); my $subsubdir_tab = File::Spec->rel2abs("$sub_sub_dir/counter.tab"); my $test_file = 'counter.conf'; my $config = { PidFile => $pid_file, ScoreboardFile => $scoreboard_file, SystemLog => $log_file, AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $log_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, }, }; my ($port, $config_user, $config_group) = config_write($config_file, $config); if (open(my $fh, ">> $config_file")) { my $config_sub_dir = $sub_dir; my $config_subdir_tab = $subdir_tab; my $config_subsub_dir = $sub_sub_dir; my $config_subsubdir_tab = $subsubdir_tab; if ($^O eq 'darwin') { $config_sub_dir = '/private' . $config_sub_dir; $config_subdir_tab = '/private' . $config_subdir_tab; $config_subsub_dir = '/private' . $config_subsub_dir; $config_subsubdir_tab = '/private' . $config_subsubdir_tab; } print $fh < CounterFile $config_subdir_tab CounterFile $config_subsubdir_tab EOC unless (close($fh)) { die("Can't write $config_file: $!"); } } else { die("Can't open $config_file: $!"); } unless (copy($config_file, "$sub_sub_dir/counter.conf")) { die("Can't copy $config_file to '$sub_sub_dir/counter.conf': $!"); } # 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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client1->login($user, $passwd); $client1->cwd("foo/bar"); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port); $client2->login($user, $passwd); $client2->cwd("foo/bar"); my $conn = $client1->retr_raw($test_file); unless ($conn) { die("Failed to RETR: " . $client1->response_code() . " " . $client1->response_msg()); } my ($resp_code, $resp_msg); my $expected; # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw($test_file); if ($conn2) { die("RETR $test_file succeeded unexpectedly"); } else { $resp_code = $client2->response_code(); $resp_msg = $client2->response_msg(); $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); } my $buf; $conn->read($buf, 8192); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->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) { die($ex); } unlink($log_file); } sub counter_vroot_retr_max_readers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = File::Spec->rel2abs("$tmpdir/test.dat"); 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 => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20 vroot:20 vroot.fsio:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, AllowOverwrite => 'on', DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterFile => $counter_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, 'mod_vroot.c' => { VRootEngine => 'on', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); my $conn = $client1->retr_raw('test.dat'); unless ($conn) { die("Failed to RETR test.d: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw('test.dat'); if ($conn2) { die("RETR test.dat succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "test.dat: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf; $conn->read($buf, 8192, 15); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } sub counter_vroot_retr_max_readers_exceeded_in_subdir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d"); mkpath($sub_dir); my $test_file = File::Spec->rel2abs("$sub_dir/test.dat"); 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 => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20 vroot:20 vroot.fsio:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, AllowOverwrite => 'on', DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterFile => $counter_file, CounterMaxReaders => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, 'mod_vroot.c' => { VRootEngine => 'on', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); my $conn = $client1->retr_raw('test.d/test.dat'); unless ($conn) { die("Failed to RETR test.d/test.d: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->retr_raw('test.d/test.dat'); if ($conn2) { die("RETR test.d/test.dat succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "test.d/test.dat: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf; $conn->read($buf, 8192, 15); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } sub counter_vroot_stor_max_writers_exceeded { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $test_file = 'test.dat'; my $config = { PidFile => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20 vroot:20 vroot.fsio:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, AllowOverwrite => 'on', DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterFile => $counter_file, CounterMaxWriters => 1, }, 'mod_delay.c' => { DelayEngine => 'off', }, 'mod_vroot.c' => { VRootEngine => 'on', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->stor_raw($test_file); if ($conn2) { die("STOR $test_file succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf = 'Hello, World!\n'; $conn->write($buf, length($buf)); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } sub counter_vroot_stor_max_writers_exceeded_in_subdir { my $self = shift; my $tmpdir = $self->{tmpdir}; my $setup = test_setup($tmpdir, 'counter'); my $counter_file = File::Spec->rel2abs("$tmpdir/counter.tab"); my $sub_dir = File::Spec->rel2abs("$tmpdir/test.d"); mkpath($sub_dir); my $test_file = 'test.d/test.dat'; my $config = { PidFile => $setup->{pid_file}, ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, Trace => 'counter:20 vroot:20 vroot.fsio:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, AllowOverwrite => 'on', DefaultRoot => '~', IfModules => { 'mod_counter.c' => { CounterEngine => 'on', CounterLog => $setup->{log_file}, CounterFile => $counter_file, }, 'mod_delay.c' => { DelayEngine => 'off', }, 'mod_vroot.c' => { VRootEngine => 'on', }, }, }; my ($port, $config_user, $config_group) = config_write($setup->{config_file}, $config); if (open(my $fh, ">> $setup->{config_file}")) { print $fh < CounterMaxWriters 1 EOC unless (close($fh)) { die("Can't write $setup->{config_file}: $!"); } } else { die("Can't open $setup->{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 $client1 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client1->login($setup->{user}, $setup->{passwd}); my $client2 = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0, 1); $client2->login($setup->{user}, $setup->{passwd}); my $conn = $client1->stor_raw($test_file); unless ($conn) { die("Failed to STOR: " . $client1->response_code() . " " . $client1->response_msg()); } # Now, before we close this data connection, try to open another # data connection for the same file. my $conn2 = $client2->stor_raw($test_file); if ($conn2) { die("STOR $test_file succeeded unexpectedly"); } my $resp_code = $client2->response_code(); my $resp_msg = $client2->response_msg(); my $expected = 450; $self->assert($expected == $resp_code, "Expected response code $expected, got $resp_code"); $expected = "$test_file: File busy"; $self->assert($expected eq $resp_msg, "Expected response message '$expected', got '$resp_msg'"); my $buf = "Hello, World!\n"; $conn->write($buf, length($buf), 15); eval { $conn->close() }; $resp_code = $client1->response_code(); $resp_msg = $client1->response_msg(); $self->assert_transfer_ok($resp_code, $resp_msg); $client2->quit(); $client1->quit(); }; if ($@) { $ex = $@; } $wfh->print("done\n"); $wfh->flush(); } else { eval { server_wait($setup->{config_file}, $rfh) }; if ($@) { warn($@); exit 1; } exit 0; } # Stop server server_stop($setup->{pid_file}); $self->assert_child_ok($pid); test_cleanup($setup->{log_file}, $ex); } 1; proftpd-mod_counter-0.6.2/t/modules/0000775000175000001440000000000013302554542016445 5ustar hilleusersproftpd-mod_counter-0.6.2/t/modules/mod_counter.t0000664000175000001440000000026713302554542021155 0ustar hilleusers#!/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_counter"); proftpd-mod_counter-0.6.2/README.md0000664000175000001440000000116013302554542016007 0ustar hilleusersproftpd-mod_counter =================== Status ------ [![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_counter.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_counter) [![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg) Synopsis -------- The `mod_counter` module for ProFTPD provides limits on the number of concurrent uploads/downloads for files. For further module documentation, see [mod_counter.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_counter/blob/master/mod_counter.html).