[Git][debian-proftpd-team/proftpd-mod-tar][master] New upstream release: mod_tar 0.4.
Hilmar Preuße
gitlab at salsa.debian.org
Tue May 1 22:00:23 BST 2018
Hilmar Preuße pushed to branch master at Debian ProFTPD Team / proftpd-mod-tar
Commits:
51b160e7 by Hilmar Preusse at 2018-05-01T22:57:55+02:00
New upstream release: mod_tar 0.4.
- - - - -
11 changed files:
- + .gitattributes
- + .travis.yml
- + README.md
- debian/changelog
- debian/control
- + doc/NOTES.libarchive
- mod_tar.c
- mod_tar.html
- + t/etc/modules/mod_tar/subdir.tar
- + t/lib/ProFTPD/Tests/Modules/mod_tar.pm
- + t/modules/mod_tar.t
Changes:
=====================================
.gitattributes
=====================================
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.pl linguist-language=C
+*.pm linguist-language=C
=====================================
.travis.yml
=====================================
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,35 @@
+language: c
+
+compiler:
+ - gcc
+ - clang
+
+install:
+ - sudo apt-get update -qq
+ # for libarchive
+ - sudo apt-get install -y libarchive-dev
+ # for libbz2
+ - sudo apt-get install -y libbz2-dev
+ # for unit tests
+ - sudo apt-get install -y check
+ # for static code analysis
+ - sudo apt-get install -y cppcheck rats
+ # for test code coverage
+ - sudo apt-get install -y lcov
+ - gem install coveralls-lcov
+
+before_script:
+ - cd ${TRAVIS_BUILD_DIR}
+ - lcov --directory . --zerocounters
+
+script:
+ # - find . -type f -name "*.c" -print | grep -v t\/ | xargs cppcheck 2>&1
+ # - find . -type f -name "*.c" -print | grep -v t\/ | xargs rats --language=c
+ - git clone https://github.com/proftpd/proftpd.git
+ - cp mod_tar.c proftpd/contrib/
+ - cd proftpd
+ - ./configure --enable-devel=coverage --enable-dso --enable-tests --with-shared=mod_tar
+ - make
+ - make clean
+ - ./configure --enable-devel=coverage --enable-tests --with-modules=mod_tar
+ - make
=====================================
README.md
=====================================
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+proftpd-mod_tar
+===============
+
+Status
+------
+[![Build Status](https://travis-ci.org/Castaglia/proftpd-mod_tar.svg?branch=master)](https://travis-ci.org/Castaglia/proftpd-mod_tar)
+[![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg)
+
+
+Synopsis
+--------
+The `mod_tar` module for ProFTPD provides for on-the-fly creation of
+tarballs for downloaded directories.
+
+For further module documentation, see [mod_tar.html](https://htmlpreview.github.io/?https://github.com/Castaglia/proftpd-mod_tar/blob/master/mod_tar.html).
=====================================
debian/changelog
=====================================
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+proftpd-mod-tar (0.4-1) UNRELEASED; urgency=medium
+
+ * New upstream release
+ - Fixed FTBFS on Hurd (Closes: #897383).
+ - Uses libarchive-dev instead of libtar-dev.
+
+ -- Francesco Paolo Lovergine <frankie at debian.org> Tue, 01 May 2018 22:45:51 +0200
+
proftpd-mod-tar (0.3.3-2) unstable; urgency=low
* Removing libacl1-dev as BD and increasing proftpd-dev to (>= 1.3.4~rc3-2~)
=====================================
debian/control
=====================================
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,7 @@ Section: net
Priority: optional
Maintainer: ProFTPD Maintainance Team <pkg-proftpd-maintainers at lists.alioth.debian.org>
Uploaders: Francesco Paolo Lovergine <frankie at debian.org>
-Build-Depends: debhelper (>= 9~), libtar-dev, proftpd-dev (>= 1.3.4~rc3-2~), libbz2-dev, zlib1g-dev
+Build-Depends: debhelper (>= 9~), proftpd-dev (>= 1.3.4~rc3-2~), libbz2-dev, zlib1g-dev, libarchive-dev
Standards-Version: 4.1.4
Homepage: http://www.castaglia.org/proftpd/modules/mod_tar.html
Vcs-Browser: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar
=====================================
doc/NOTES.libarchive
=====================================
--- /dev/null
+++ b/doc/NOTES.libarchive
@@ -0,0 +1,131 @@
+
+ struct archive *a;
+
+ a = archive_write_new();
+
+ # Set compression: none, bzip2, or gzip
+ archive_write_set_compression_none(a);
+ archive_write_set_compression_bzip2(a);
+ archive_write_set_compression_gzip(a);
+
+ # Set format
+ archive_write_set_format_ustar(a);
+
+ OR
+
+ archive_write_set_format_pax_restricted(a); // Note 1
+
+ /* Libarchive's "pax restricted" format is a tar format that uses pax
+ * extensions only when absolutely necessary. Most of the time, it will
+ * write plain ustar entries. This is the recommended tar format for most
+ * uses. You should explicitly use ustar format only when you have to
+ * create archives that will be readable on older systems; you should
+ * explicitly request pax format only when you need to preserve as many
+ * attributes as possible.
+ */
+
+ archive_write_set_format_zip(a);
+ // ? See caveats about ZIP64 format, 64-bit platforms:
+ // libarchive/archive_write_set_format_zip.c
+
+Reading data from disk:
+
+ use archive_read_disk_set_gname_lookup(),
+ archive_read_disk_set_uname_lookup()
+
+ to set callbacks that libarchive will use to resolve UID/GID to names.
+ E.g. set pr_auth_get_pwuid(), pr_auth_get_grgid().
+
+ use archive_read_disk_set_symlink_logical() (follows symlinks) or
+ archive_read_disk_set_symlink_physical() (does not follow symlinks)
+
+ use archive_read_disk_entry_from_file()?
+
+Example (from libarchive_read_disk(3)). Note that the libarchive_write_open()
+man page has a better example:
+
+ struct archive *a;
+
+ a = archive_write_new();
+ if (a == NULL) {
+ ...
+ }
+
+ archive_write_open(a, custom_data, open_cb, write_cb, close_cb)
+
+ archive_write_open_filename(a, outname, 10240);
+
+ void add_file_to_archive(struct archive *a, const char *path) {
+
+ char buf[8K];
+ size_t nread;
+ struct archive *lookup;
+ struct archive_entry *entry;
+ int fd;
+
+ /* Create a lookup archive; set the callbacks we want to use. */
+ lookup = archive_read_disk_new();
+ archive_read_disk_set_standard_lookup(lookup);
+
+ entry = archive_entry_new();
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ /* cleanup */
+ }
+
+ archive_entry_copy_pathname(entry, path);
+
+ /* The last argument is a struct stat *. If we provide that,
+ * then read_disk_entry_from_file() just copies the stat info
+ * it needs. We can use this, do the pr_fsio_fstat() ourselves.
+ */
+ archive_read_disk_entry_from_file(lookup, entry fd, NULL);
+ archive_write_header(a, entry);
+
+ /* XXX If a regular file, copy the file contents */
+ nread = read(fd, buf, sizeof(buf));
+ while (nread > 0) {
+ /* Handle signals */
+
+ archive_write_data(a, buf, nread);
+ nread = read(fd, buf, sizeof(buf));
+ }
+
+ archive_write_finish_entry(a);
+
+ archive_entry_free(entry);
+ archive_read_free(lookup);
+
+ close(fd);
+ }
+
+ archive_write_close(a); // Note 4
+ archive_write_free(a); // Note 5
+
+This example creates a fresh archive_entry object for each file. For better
+performance, you can reuse the same archive_entry object by using
+`archive_entry_clear()` to erase it after each use.
+
+Note 3: Size, file type, and pathname are all required attributes here. You
+can also use `archive_entry_copy_stat()` to copy all information from the
+`struct stat` to the archive entry, including file type. To get even more
+complete information, look at the `archive_read_disk` API, which provides an
+easy way to get more extensive file metadata---including ACLs and extended
+attributes on some systems---than using the system `stat()` system call. It
+also works on platforms such as Windows where `stat()` either doesn't exist or
+is broken.
+
+ This suggests that mod_tar should use archive_entry_copy_stat(), and
+ have a TarOptions option for enabling the recording of extended attributes
+ (and would switch to using archive_read_disk()).
+
+Note 4: The free/finish call will implicitly call `archive_write_close()` if
+necessary. However, the close call returns an error code and the free/finish
+call does not, so if you rely on the implicit close, you won't be able to
+detect any errors that happen with the final writes.
+
+Note 5: Beginning with libarchive 3.0, this function is called
+`archive_write_free()`. The previous name was `archive_write_finish()`. If you
+want to write software compatible with libarchive 2.x and libarchive 3.x, you
+should use the old name, but be aware that it will be removed when
+libarchive 4.x is released.
=====================================
mod_tar.c
=====================================
--- a/mod_tar.c
+++ b/mod_tar.c
@@ -1,6 +1,6 @@
/*
* ProFTPD - mod_tar
- * Copyright (c) 2009-2010 TJ Saunders
+ * Copyright (c) 2009-2017 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
@@ -14,25 +14,23 @@
*
* 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.
+ * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, 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.
*
- * $Id: mod_tar.c,v 1.7 2009/10/01 15:30:57 tj Exp tj $
- * $Libraries: -ltar -lz -lbz2 $
+ * $Libraries: -larchive -lz -lbz2$
*/
#include "conf.h"
#include "privs.h"
-#include <libtar.h>
-#include <zlib.h>
-#include <bzlib.h>
+#include <archive.h>
+#include <archive_entry.h>
-#define MOD_TAR_VERSION "mod_tar/0.3.3"
+#define MOD_TAR_VERSION "mod_tar/0.4"
/* Make sure the version of proftpd is as necessary. */
#if PROFTPD_VERSION_NUMBER < 0x0001030101
@@ -41,142 +39,187 @@
module tar_module;
+/* Necessary prototype. */
+static void tar_exit_ev(const void *, void *);
+
static int tar_engine = FALSE;
static int tar_logfd = -1;
static unsigned long tar_opts = 0UL;
#define TAR_OPT_DEREF_SYMLINKS 0x001
-static const char *tar_tmp_path = "./";
+#define TAR_ARCHIVE_FL_USE_GZIP 0x001
+#define TAR_ARCHIVE_FL_USE_BZIP2 0x002
+#define TAR_ARCHIVE_FL_USE_USTAR 0x004
+#define TAR_ARCHIVE_FL_USE_PAX 0x008
+#define TAR_ARCHIVE_FL_USE_ZIP 0x010
+static const char *tar_tmp_path = "./";
static char *tar_tmp_file = NULL;
-/* These are re-implementation of the tar_append_file() and tar_append_tree()
- * functions found in libtar. We needed to implement them ourselves in order
- * to support options such as "deference", so that mod_tar's .tar files
- * follow symlinks (libtar's default behavior, hardcoded, is to NOT follow
- * symlink).
- */
-
-struct tar_dev {
- dev_t td_dev;
- libtar_hash_t *td_h;
+struct archive_data {
+ const char *path;
+ pr_fh_t *fh;
};
-typedef struct tar_dev tar_dev_t;
-struct tar_ino {
- ino_t ti_ino;
- char ti_name[MAXPATHLEN];
-};
-typedef struct tar_ino tar_ino_t;
+static const char *trace_channel = "tar";
-/* Necessary prototype */
-static void tar_exit_ev(const void *, void *);
+static int append_data(pool *p, struct archive *tar,
+ struct archive_entry *entry, char *path, struct stat *st) {
+ pool *tmp_pool;
+ pr_fh_t *fh;
+ void *buf;
+ size_t buflen;
+ int res;
+ struct stat pst;
-static int append_file(TAR *t, char *real_name, char *save_name) {
- struct stat st;
- int i, res;
- libtar_hashptr_t hp;
- tar_dev_t *td = NULL;
- tar_ino_t *ti = NULL;
- char path[PR_TUNABLE_PATH_MAX+1];
+ fh = pr_fsio_open(path, O_RDONLY);
+ if (fh == NULL) {
+ int xerrno = errno;
- if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
- res = lstat(real_name, &st);
+ pr_trace_msg(trace_channel, 3, "unable to read '%s': %s", path,
+ strerror(xerrno));
- } else {
- res = stat(real_name, &st);
+ errno = xerrno;
+ return -1;
}
- if (res != 0)
+ res = pr_fsio_fstat(fh, &pst);
+ if (res < 0) {
+ int xerrno = errno;
+
+ pr_trace_msg(trace_channel, 3, "unable to stat '%s': %s", path,
+ strerror(xerrno));
+
+ pr_fsio_close(fh);
+ errno = xerrno;
return -1;
+ }
- /* set header block */
- memset(&(t->th_buf), 0, sizeof(struct tar_header));
- th_set_from_stat(t, &st);
+ if (S_ISDIR(pst.st_mode)) {
+ int xerrno = EISDIR;
+
+ pr_trace_msg(trace_channel, 3, "unable to use '%s': %s", path,
+ strerror(xerrno));
- /* set the header path */
- th_set_path(t, (save_name ? save_name : real_name));
+ pr_fsio_close(fh);
+ errno = xerrno;
+ return -1;
+ }
- /* check if it's a hardlink */
- libtar_hashptr_reset(&hp);
+ tmp_pool = make_sub_pool(p);
- res = libtar_hash_getkey(t->h, &hp, &(st.st_dev),
- (libtar_matchfunc_t) dev_match);
- if (res != 0) {
- td = (tar_dev_t *) libtar_hashptr_data(&hp);
+ /* Use a buffer size based on the filesystem blocksize, for better IO. */
+ buflen = st->st_blksize;
+ buf = palloc(tmp_pool, buflen);
- } else {
- td = (tar_dev_t *) calloc(1, sizeof(tar_dev_t));
- if (td == NULL)
- return -1;
+ res = pr_fsio_read(fh, buf, buflen);
+ while (res > 0) {
+ pr_signals_handle();
+ if (archive_write_data(tar, buf, res) < 0) {
+ int xerrno;
- td->td_dev = st.st_dev;
- td->td_h = libtar_hash_new(256, (libtar_hashfunc_t) ino_hash);
+ xerrno = archive_errno(tar);
+ pr_trace_msg(trace_channel, 3, "error adding data to archive: %s",
+ archive_error_string(tar));
- if (td->td_h == NULL) {
- free(td);
- return -1;
- }
+ destroy_pool(tmp_pool);
+ pr_fsio_close(fh);
- if (libtar_hash_add(t->h, td) == -1) {
- libtar_hash_free(td->td_h, free);
- free(td);
+ errno = xerrno;
return -1;
}
+
+ res = pr_fsio_read(fh, buf, buflen);
}
- libtar_hashptr_reset(&hp);
+ destroy_pool(tmp_pool);
+ pr_fsio_close(fh);
+
+ return 0;
+}
+
+static int append_file(pool *p, struct archive *tar,
+ struct archive_entry *entry, char *real_name, char *save_name) {
+ struct stat st;
+ int res;
- res = libtar_hash_getkey(td->td_h, &hp, &(st.st_ino),
- (libtar_matchfunc_t) ino_match);
- if (res != 0) {
- ti = (tar_ino_t *) libtar_hashptr_data(&hp);
- t->th_buf.typeflag = LNKTYPE;
- th_set_link(t, ti->ti_name);
+ if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
+ res = pr_fsio_lstat(real_name, &st);
} else {
- ti = (tar_ino_t *)calloc(1, sizeof(tar_ino_t));
- if (ti == NULL)
- return -1;
+ res = pr_fsio_stat(real_name, &st);
+ }
+
+ if (res < 0) {
+ int xerrno = errno;
- ti->ti_ino = st.st_ino;
- snprintf(ti->ti_name, sizeof(ti->ti_name), "%s",
- save_name ? save_name : real_name);
- libtar_hash_add(td->td_h, ti);
+ pr_trace_msg(trace_channel, 9, "error stat'ing '%s': %s", real_name,
+ strerror(xerrno));
+
+ errno = xerrno;
+ return -1;
}
- /* check if it's a symlink */
- if (TH_ISSYM(t)) {
- i = readlink(real_name, path, sizeof(path));
+ archive_entry_clear(entry);
+ archive_entry_copy_stat(entry, &st);
+ archive_entry_copy_pathname(entry, save_name);
+
+ if (S_ISLNK(st.st_mode)) {
+ int i;
+ char path[PR_TUNABLE_PATH_MAX+1];
+
+ i = readlink(real_name, path, sizeof(path)-1);
if (i == -1)
return -1;
- if (i >= PR_TUNABLE_PATH_MAX)
+ if (i >= PR_TUNABLE_PATH_MAX) {
i = PR_TUNABLE_PATH_MAX - 1;
+ }
path[i] = '\0';
- th_set_link(t, path);
+
+ pr_trace_msg(trace_channel, 15,
+ "setting destination path '%s' for symlink '%s'", path, real_name);
+ archive_entry_set_symlink(entry, path);
}
+
+ res = archive_write_header(tar, entry);
+ if (res != ARCHIVE_OK) {
+ int xerrno;
- /* print file info */
- if (t->options & TAR_VERBOSE)
- th_print_long_ls(t);
+ xerrno = archive_errno(tar);
+ pr_trace_msg(trace_channel, 3, "error writing archive entry header: %s",
+ archive_error_string(tar));
- /* write header */
- res = th_write(t);
- if (res != 0)
+ errno = xerrno;
return -1;
+ }
- /* if it's a regular file, write the contents as well */
- if (TH_ISREG(t) &&
- tar_append_regfile(t, real_name) != 0)
+ /* If it's a regular file, write the contents as well */
+ if (S_ISREG(st.st_mode)) {
+ if (append_data(p, tar, entry, real_name, &st) < 0) {
+ return -1;
+ }
+ }
+
+ res = archive_write_finish_entry(tar);
+ if (res != ARCHIVE_OK) {
+ int xerrno;
+
+ xerrno = archive_errno(tar);
+ pr_trace_msg(trace_channel, 3, "error finishing archive entry: %s",
+ archive_error_string(tar));
+
+ errno = xerrno;
return -1;
+ }
return 0;
}
-static int append_tree(TAR *t, char *real_dir, char *save_dir) {
+static int append_tree(pool *p, struct archive *tar,
+ struct archive_entry *entry, char *real_dir, char *save_dir) {
char real_path[PR_TUNABLE_PATH_MAX+1];
char save_path[PR_TUNABLE_PATH_MAX+1];
struct dirent *dent;
@@ -184,14 +227,16 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
struct stat st;
int res;
- res = append_file(t, real_dir, save_dir);
- if (res != 0)
+ res = append_file(p, tar, entry, real_dir, save_dir);
+ if (res < 0) {
return -1;
+ }
dirh = opendir(real_dir);
if (dirh == NULL) {
- if (errno == ENOTDIR)
+ if (errno == ENOTDIR) {
return 0;
+ }
return -1;
}
@@ -199,9 +244,10 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
while ((dent = readdir(dirh)) != NULL) {
pr_signals_handle();
- if (strcmp(dent->d_name, ".") == 0 ||
- strcmp(dent->d_name, "..") == 0)
+ if (strncmp(dent->d_name, ".", 2) == 0 ||
+ strncmp(dent->d_name, "..", 3) == 0) {
continue;
+ }
memset(real_path, '\0', sizeof(real_path));
snprintf(real_path, sizeof(real_path)-1, "%s/%s", real_dir, dent->d_name);
@@ -212,145 +258,251 @@ static int append_tree(TAR *t, char *real_dir, char *save_dir) {
}
if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) {
- res = lstat(real_path, &st);
+ res = pr_fsio_lstat(real_path, &st);
} else {
- res = stat(real_path, &st);
+ res = pr_fsio_stat(real_path, &st);
}
- if (res != 0)
+ if (res < 0) {
+ int xerrno = errno;
+
+ (void) closedir(dirh);
+
+ errno = xerrno;
return -1;
+ }
if (S_ISDIR(st.st_mode)) {
- res = append_tree(t, real_path, (save_dir ? save_path : NULL));
- if (res != 0)
+ res = append_tree(p, tar, entry, real_path,
+ (save_dir ? save_path : NULL));
+ if (res < 0) {
+ int xerrno = errno;
+
+ (void) closedir(dirh);
+
+ errno = xerrno;
return -1;
+ }
continue;
}
- res = append_file(t, real_path, (save_dir ? save_path : NULL));
- if (res != 0)
+ res = append_file(p, tar, entry, real_path, (save_dir ? save_path : NULL));
+ if (res < 0) {
+ int xerrno = errno;
+
+ (void) closedir(dirh);
+
+ errno = xerrno;
return -1;
+ }
}
closedir(dirh);
return 0;
}
-static int tar_create_tar(tartype_t *type, char *dst_file, char *src_path,
- char *src_dir) {
- TAR *tar;
+static int tar_archive_open_cb(struct archive *tar, void *user_data) {
+ struct archive_data *tar_data;
+ pr_fh_t *fh;
- if (tar_open(&tar, dst_file, type, O_WRONLY|O_CREAT, 0644, 0) < 0) {
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "unable to open '%s' as tar file: %s", dst_file, strerror(errno));
- return -1;
- }
+ tar_data = user_data;
- if (append_tree(tar, src_path, src_dir) < 0) {
- int xerrno = errno;
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "error appending '%s' to tar file: %s", src_path, strerror(xerrno));
- tar_close(tar);
+ fh = pr_fsio_open(tar_data->path, O_WRONLY|O_CREAT);
+ if (fh == NULL) {
+ return ARCHIVE_FATAL;
+ }
- errno = xerrno;
- return -1;
+ /* Override the default 0666 mode that pr_fsio_open() uses. */
+ if (pr_fsio_fchmod(fh, 0644) < 0) {
+ pr_trace_msg(trace_channel, 3, "error setting mode on '%s' to 0644: %s",
+ tar_data->path, strerror(errno));
}
- if (tar_append_eof(tar) < 0) {
- int xerrno = errno;
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "error appending EOF to tar file: %s", strerror(xerrno));
- tar_close(tar);
+ tar_data->fh = fh;
+ return ARCHIVE_OK;
+}
- errno = xerrno;
- return -1;
- }
+static ssize_t tar_archive_write_cb(struct archive *tar, void *user_data,
+ const void *buf, size_t buflen) {
+ struct archive_data *tar_data;
- if (tar_close(tar) < 0) {
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "error writing tar file: %s", strerror(errno));
- return -1;
+ tar_data = user_data;
+ return pr_fsio_write(tar_data->fh, buf, buflen);
+}
+
+static int tar_archive_close_cb(struct archive *tar, void *user_data) {
+ struct archive_data *tar_data;
+ int res;
+
+ tar_data = user_data;
+
+ res = pr_fsio_close(tar_data->fh);
+ if (res < 0) {
+ return ARCHIVE_FATAL;
}
- return 0;
+ tar_data->fh = NULL;
+ return ARCHIVE_OK;
}
-static int tar_bzopen(const char *path, int flags, mode_t mode) {
- int fd;
- BZFILE *bzf;
+static int tar_create_archive(pool *p, char *dst_file, unsigned long blksize,
+ char *src_path, char *src_dir, unsigned long flags) {
+ struct archive_data *tar_data;
+ struct archive *tar;
+ struct archive_entry *entry;
+ int res;
- fd = open(path, flags, mode);
- if (fd < 0) {
+ tar = archive_write_new();
+ if (tar == NULL) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "unable to open '%s': %s", path, strerror(errno));
+ "error allocating new archive handle: %s", archive_error_string(tar));
+ errno = archive_errno(tar);
return -1;
}
- if (flags & O_CREAT) {
- if (fchmod(fd, mode) < 0) {
- int xerrno = errno;
+ /* Call archive_write_set_bytes_per_block() there, so that the optimal
+ * block size for writing data out to the archive file is used.
+ *
+ * Sadly, the libarchive API uses an int for the block size, not an
+ * unsigned long, size_t, or off_t. Why even allow a signed data type
+ * for that parameter?
+ *
+ * NOTE: The `tar' program provided by libarchive defaults to a value
+ * of (20 * 512) for the bytes_per_block value; perhaps we should
+ * use that, too?
+ */
+ archive_write_set_bytes_per_block(tar, blksize);
+
+ if (flags & TAR_ARCHIVE_FL_USE_USTAR) {
+ res = archive_write_set_format_ustar(tar);
+ if (res != ARCHIVE_OK) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "error setting mode %04o on '%s': %s", mode, path, strerror(xerrno));
+ "error configuring archive handle for ustar format: %s",
+ archive_error_string(tar));
+ errno = archive_errno(tar);
+ return -1;
+ }
- close(fd);
- errno = xerrno;
+ } else if (flags & TAR_ARCHIVE_FL_USE_ZIP) {
+ res = archive_write_set_format_zip(tar);
+ if (res != ARCHIVE_OK) {
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error configuring archive handle for zip format: %s",
+ archive_error_string(tar));
+ errno = archive_errno(tar);
return -1;
}
}
- bzf = BZ2_bzdopen(fd, "wb");
- if (bzf == NULL) {
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "unable to open bzlib stream on '%s': Not enough memory", path);
- close(fd);
- errno = EPERM;
- return -1;
+ if (flags & TAR_ARCHIVE_FL_USE_GZIP) {
+ res = archive_write_add_filter_gzip(tar);
+ if (res != ARCHIVE_OK) {
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error configuring archive handle for gzip compression: %s",
+ archive_error_string(tar));
+ errno = archive_errno(tar);
+ return -1;
+ }
+
+ pr_trace_msg(trace_channel, 9, "using gzip compression for '%s'", src_path);
+
+ } else if (flags & TAR_ARCHIVE_FL_USE_BZIP2) {
+ res = archive_write_add_filter_bzip2(tar);
+ if (res != ARCHIVE_OK) {
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error configuring archive handle for bzip2 compression: %s",
+ archive_error_string(tar));
+ errno = archive_errno(tar);
+ return -1;
+ }
+
+ pr_trace_msg(trace_channel, 9, "using bzip2 compression for '%s'",
+ src_path);
+
+ } else {
+ res = archive_write_add_filter_none(tar);
+ if (res != ARCHIVE_OK) {
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error configuring archive handle for no compression: %s",
+ archive_error_string(tar));
+ errno = archive_errno(tar);
+ return -1;
+ }
}
- /* XXX I don't like doing this, returning a pointer in the space of
- * an int, but unfortunately it is the interface defined by libtar.
+ tar_data = palloc(p, sizeof(struct archive_data));
+ tar_data->path = dst_file;
+
+ /* Allocate a new archive_entry to use for adding all entries to the
+ * archive. This avoid creating/destroying an archive_entry object per
+ * file.
*/
- return (int) bzf;
-}
+ entry = archive_entry_new();
+ if (tar == NULL) {
+ int xerrno;
+
+ xerrno = archive_errno(tar);
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error allocating new archive entry handle: %s",
+ archive_error_string(tar));
+ archive_write_free(tar);
-static int tar_gzopen(const char *path, int flags, mode_t mode) {
- int fd;
- gzFile gzf;
+ errno = xerrno;
+ return -1;
+ }
+
+ res = archive_write_open(tar, tar_data, tar_archive_open_cb,
+ tar_archive_write_cb, tar_archive_close_cb);
+ if (res != ARCHIVE_OK) {
+ int xerrno;
- fd = open(path, flags, mode);
- if (fd < 0) {
+ xerrno = archive_errno(tar);
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "unable to open '%s': %s", path, strerror(errno));
+ "error opening archive handle for file '%s': %s", dst_file,
+ archive_error_string(tar));
+ archive_entry_free(entry);
+ archive_write_free(tar);
+ (void) unlink(dst_file);
+
+ errno = xerrno;
return -1;
}
- if (flags & O_CREAT) {
- if (fchmod(fd, mode) < 0) {
- int xerrno = errno;
- (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "error setting mode %04o on '%s': %s", mode, path, strerror(xerrno));
+ if (append_tree(p, tar, entry, src_path, src_dir) < 0) {
+ int xerrno = errno;
- close(fd);
- errno = xerrno;
- return -1;
- }
+ (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
+ "error appending '%s' to tar file: %s", src_path, strerror(xerrno));
+ archive_entry_free(entry);
+ (void) archive_write_close(tar);
+ archive_write_free(tar);
+ (void) unlink(dst_file);
+
+ errno = xerrno;
+ return -1;
}
- gzf = gzdopen(fd, "wb");
- if (gzf == NULL) {
+ archive_entry_free(entry);
+
+ res = archive_write_close(tar);
+ if (res < 0) {
+ int xerrno;
+
+ xerrno = archive_errno(tar);
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "unable to open zlib stream on '%s': Not enough memory", path);
- close(fd);
- errno = EPERM;
+ "error writing tar file: %s", archive_error_string(tar));
+
+ archive_write_free(tar);
+ (void) unlink(dst_file);
+
+ errno = xerrno;
return -1;
}
- /* XXX I don't like doing this, returning a pointer in the space of
- * an int, but unfortunately it is the interface defined by libtar.
- */
- return (int) gzf;
+ archive_write_free(tar);
+ return 0;
}
static char *tar_get_ext_tar(char *path, size_t path_len) {
@@ -365,8 +517,6 @@ static char *tar_get_ext_tar(char *path, size_t path_len) {
return &path[path_len-4];
}
-
- return NULL;
}
return NULL;
@@ -384,8 +534,6 @@ static char *tar_get_ext_tgz(char *path, size_t path_len) {
return &path[path_len-4];
}
-
- return NULL;
}
return NULL;
@@ -413,6 +561,24 @@ static char *tar_get_ext_targz(char *path, size_t path_len) {
return NULL;
}
+static char *tar_get_ext_tbz2(char *path, size_t path_len) {
+ if (path_len < 5) {
+ return NULL;
+ }
+
+ if (path[path_len-5] == '.') {
+ if ((path[path_len-4] == 'T' || path[path_len-4] == 't') &&
+ (path[path_len-3] == 'B' || path[path_len-3] == 'b') &&
+ (path[path_len-2] == 'Z' || path[path_len-2] == 'z') &&
+ path[path_len-1] == '2') {
+
+ return &path[path_len-5];
+ }
+ }
+
+ return NULL;
+}
+
static char *tar_get_ext_tarbz2(char *path, size_t path_len) {
if (path_len < 8) {
return NULL;
@@ -429,13 +595,77 @@ static char *tar_get_ext_tarbz2(char *path, size_t path_len) {
return &path[path_len-8];
}
+ }
+
+ return NULL;
+}
+static char *tar_get_ext_zip(char *path, size_t path_len) {
+ if (path_len < 4) {
return NULL;
}
+ if (path[path_len-4] == '.') {
+ if ((path[path_len-3] == 'Z' || path[path_len-3] == 'z') &&
+ (path[path_len-2] == 'I' || path[path_len-2] == 'i') &&
+ (path[path_len-1] == 'P' || path[path_len-1] == 'p')) {
+
+ return &path[path_len-4];
+ }
+ }
+
return NULL;
}
+static char *tar_get_flags(char *path, size_t path_len, unsigned long *flags) {
+ char *ptr;
+
+ ptr = tar_get_ext_tar(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+
+ } else {
+ ptr = tar_get_ext_tgz(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+ *flags |= TAR_ARCHIVE_FL_USE_GZIP;
+
+ } else {
+ ptr = tar_get_ext_targz(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+ *flags |= TAR_ARCHIVE_FL_USE_GZIP;
+
+ } else {
+ ptr = tar_get_ext_tbz2(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+ *flags |= TAR_ARCHIVE_FL_USE_BZIP2;
+
+ } else {
+ ptr = tar_get_ext_tarbz2(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_USTAR;
+ *flags |= TAR_ARCHIVE_FL_USE_BZIP2;
+
+ } else {
+ ptr = tar_get_ext_zip(path, path_len);
+ if (ptr != NULL) {
+ *flags |= TAR_ARCHIVE_FL_USE_ZIP;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (*flags == 0) {
+ return NULL;
+ }
+
+ return ptr;
+}
+
/* Configuration handlers
*/
@@ -501,7 +731,8 @@ MODRET set_taroptions(cmd_rec *cmd) {
c = add_config_param(cmd->argv[0], 1, NULL);
for (i = 1; i < cmd->argc; i++) {
- if (strcmp(cmd->argv[i], "dereference") == 0) {
+ if (strcmp(cmd->argv[i], "FollowSymlinks") == 0 ||
+ strcmp(cmd->argv[i], "dereference") == 0) {
opts |= TAR_OPT_DEREF_SYMLINKS;
} else {
@@ -549,8 +780,15 @@ MODRET tar_post_pass(cmd_rec *cmd) {
pr_event_register(&tar_module, "core.exit", tar_exit_ev, NULL);
c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarOptions", FALSE);
- if (c) {
- tar_opts = *((unsigned long *) c->argv[0]);
+ while (c != NULL) {
+ unsigned long opts;
+
+ pr_signals_handle();
+
+ opts = *((unsigned long *) c->argv[0]);
+ tar_opts |= opts;
+
+ c = find_config_next(c, c->next, CONF_PARAM, "TarOptions", FALSE);
}
c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarTempPath", FALSE);
@@ -579,11 +817,13 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
char *path, *tmp;
size_t path_len;
- if (!tar_engine)
+ if (tar_engine == FALSE) {
return PR_DECLINED(cmd);
+ }
- if (cmd->argc < 2)
+ if (cmd->argc < 2) {
return PR_DECLINED(cmd);
+ }
path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg);
@@ -606,32 +846,12 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
path_len = strlen(path);
if (path_len > 4) {
char *dir, *notar_file, *ptr, *tar_file;
- int fd, res, use_tar = FALSE, use_gz = FALSE, use_bz2 = FALSE;
+ int fd, res;
struct stat st;
config_rec *d;
+ unsigned long flags = 0UL;
- ptr = tar_get_ext_tar(path, path_len);
- if (ptr)
- use_tar = TRUE;
-
- if (ptr == NULL) {
- ptr = tar_get_ext_tgz(path, path_len);
- if (ptr)
- use_gz = TRUE;
- }
-
- if (ptr == NULL) {
- ptr = tar_get_ext_targz(path, path_len);
- if (ptr)
- use_gz = TRUE;
- }
-
- if (ptr == NULL) {
- ptr = tar_get_ext_tarbz2(path, path_len);
- if (ptr)
- use_bz2 = TRUE;
- }
-
+ ptr = tar_get_flags(path, path_len, &flags);
if (ptr == NULL) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"no .tar file extension found in '%s'", path);
@@ -643,7 +863,7 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
path = dir_realpath(cmd->tmp_pool, path);
res = dir_exists(path);
- if (!res) {
+ if (res == 0) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"'%s' is not a directory, ignoring", path);
*ptr = '.';
@@ -680,13 +900,17 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
int tar_enable;
tar_enable = *((int *) c->argv[0]);
- if (!tar_enable) {
+ if (tar_enable == FALSE) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
- "TarEnable off found, skipping tar file of '%s' directory", path);
+ "'TarEnable off' found, skipping tar file of '%s' directory", path);
*ptr = '.';
return PR_DECLINED(cmd);
}
}
+
+ } else {
+ pr_trace_msg(trace_channel, 9,
+ "no <Directory> match found for '%s'", path);
}
dir = strrchr(path, '/');
@@ -701,47 +925,30 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
fd = mkstemp(tar_file);
if (fd < 0) {
+ int xerrno = errno;
+
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"error creating temporary filename using mkstemp: %s",
- strerror(errno));
+ strerror(xerrno));
*ptr = '.';
return PR_DECLINED(cmd);
}
+ (void) fstat(fd, &st);
close(fd);
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"writing temporary .tar file to '%s'", tar_file);
/* Create the tar file. */
- if (use_tar) {
- res = tar_create_tar(NULL, tar_file, path, dir);
-
- } else if (use_gz) {
- tartype_t gz_type = {
- (openfunc_t) tar_gzopen,
- (closefunc_t) gzclose,
- (readfunc_t) gzread,
- (writefunc_t) gzwrite
- };
-
- res = tar_create_tar(&gz_type, tar_file, path, dir);
-
- } else if (use_bz2) {
- tartype_t bz2_type = {
- (openfunc_t) tar_bzopen,
- (closefunc_t) BZ2_bzclose,
- (readfunc_t) BZ2_bzread,
- (writefunc_t) BZ2_bzwrite
- };
-
- res = tar_create_tar(&bz2_type, tar_file, path, dir);
- }
-
+ res = tar_create_archive(cmd->tmp_pool, tar_file,
+ (unsigned long) st.st_blksize, path, dir, flags);
if (res < 0) {
+ int xerrno = errno;
+
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"error creating tar file '%s' from directory '%s': %s",
- tar_file, path, strerror(errno));
+ tar_file, path, strerror(xerrno));
*ptr = '.';
return PR_DECLINED(cmd);
}
@@ -782,11 +989,12 @@ MODRET tar_pre_retr(cmd_rec *cmd) {
MODRET tar_log_retr(cmd_rec *cmd) {
char *path;
- if (!tar_engine)
+ if (tar_engine == FALSE) {
return PR_DECLINED(cmd);
+ }
path = pr_table_get(cmd->notes, "mod_tar.tar-file", NULL);
- if (path) {
+ if (path != NULL) {
if (unlink(path) < 0) {
(void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION,
"error deleting '%s': %s", path, strerror(errno));
@@ -799,7 +1007,7 @@ MODRET tar_log_retr(cmd_rec *cmd) {
}
path = pr_table_get(cmd->notes, "mod_tar.orig-path", NULL);
- if (path) {
+ if (path != NULL) {
/* Replace session.xfer.path, so that the TransferLog/ExtendedLogs
* show the originally requested path, not the temporary filename
* generated by mod_tar.
@@ -830,16 +1038,6 @@ static void tar_exit_ev(const void *event_data, void *user_data) {
}
}
-#if defined(PR_SHARED_MODULE)
-static void tar_mod_unload_ev(const void *event_data, void *user_data) {
- if (strcmp("mod_tar.c", (const char *) event_data) == 0) {
- pr_event_unregister(&tar_module, NULL, NULL);
- close(tar_logfd);
- tar_logfd = -1;
- }
-}
-#endif /* !PR_SHARED_MODULE */
-
/* Initialization functions
*/
@@ -848,14 +1046,15 @@ static int tar_sess_init(void) {
c = find_config(main_server->conf, CONF_PARAM, "TarLog", FALSE);
if (c &&
- strcasecmp((char *) c->argv[0], "none") != 0) {
- int res;
+ strncasecmp((char *) c->argv[0], "none", 5) != 0) {
+ int res, xerrno;
char *path;
path = c->argv[0];
PRIVS_ROOT
res = pr_log_openfile(path, &tar_logfd, 0660);
+ xerrno = errno;
PRIVS_RELINQUISH
switch (res) {
@@ -864,7 +1063,7 @@ static int tar_sess_init(void) {
case -1:
pr_log_debug(DEBUG1, MOD_TAR_VERSION ": unable to open TarLog '%s': %s",
- path, strerror(errno));
+ path, strerror(xerrno));
break;
case PR_LOG_SYMLINK:
@@ -883,11 +1082,8 @@ static int tar_sess_init(void) {
}
static int tar_init(void) {
-#if defined(PR_SHARED_MODULE)
- pr_event_register(&tar_module, "core.module-unload", tar_mod_unload_ev, NULL);
-#endif
-
- pr_log_debug(DEBUG0, MOD_TAR_VERSION ": using libtar %s", libtar_version);
+ pr_log_debug(DEBUG0, MOD_TAR_VERSION ": using libarchive %s",
+ archive_version_string());
return 0;
}
=====================================
mod_tar.html
=====================================
--- a/mod_tar.html
+++ b/mod_tar.html
@@ -22,9 +22,9 @@ directory.
<p>
To provide this feature, the <code>mod_tar</code> module uses the
-<code>libtar</code> library; see:
+<code>libarchive</code> library; see:
<pre>
- <a href="http://www.feep.net/libtar/">http://www.feep.net/libtar/</a>
+ <a href="http://libarchive.github.com/">http://libarchive.github.com/</a>
</pre>
<p>
@@ -115,13 +115,13 @@ behavior of <code>mod_tar</code>, usually pertaining to how the
<p>
Example:
<pre>
- TarOptions dereference
+ TarOptions FollowSymlinks
</pre>
<p>
The currently implemented options are:
<ul>
- <li><code>dereference</code><br>
+ <li><code>FollowSymlinks</code><br>
<p>
Instead of creating <code>.tar</code> files which include symlinks,
include the files that the symlinks point to.
@@ -174,7 +174,7 @@ For those with an existing ProFTPD installation, you can use the
<code>prxs</code> tool to add <code>mod_tar</code>, as a DSO module, to
your existing server:
<pre>
- # prxs -c -i -d -I /path/to/libtar/include -L /path/to/libtar/lib mod_tar.c
+ # prxs -c -i -d -I /path/to/libarchive/include -L /path/to/libarchive/lib mod_tar.c
</pre>
<p>
@@ -198,9 +198,11 @@ The following extensions will trigger <code>mod_tar</code> to attempt
on-the-fly tar file creation:
<ul>
<li>.tar
+ <li>.tbz2
<li>.tgz
<li>.tar.gz
<li>.tar.bz2
+ <li>.zip
</ul>
<p>
@@ -265,7 +267,7 @@ Last Updated: <i>$Date: 2009/08/20 17:07:14 $</i><br>
<br><hr>
<font size=2><b><i>
-© Copyright 2009 TJ Saunders<br>
+© Copyright 2009-2012 TJ Saunders<br>
All Rights Reserved<br>
</i></b></font>
=====================================
t/etc/modules/mod_tar/subdir.tar
=====================================
Binary files /dev/null and b/t/etc/modules/mod_tar/subdir.tar differ
=====================================
t/lib/ProFTPD/Tests/Modules/mod_tar.pm
=====================================
--- /dev/null
+++ b/t/lib/ProFTPD/Tests/Modules/mod_tar.pm
@@ -0,0 +1,2881 @@
+package ProFTPD::Tests::Modules::mod_tar;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use Archive::Tar;
+use Archive::Tar::File;
+use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
+use Cwd;
+use Digest::MD5;
+use File::Copy;
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use IO::Zlib;
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+ tar_retr_file => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tar => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tar_already_exists => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tar_symlinks => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ # XXX tar_retr_tar_subdirs
+
+ # XXX Need test for absolute symlinks, and chrooted session
+ tar_retr_tar_symlinks_opt_dereference => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_enable_off => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_notar => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tar_gz => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tgz => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_retr_tar_bz2 => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_xferlog_retr_tar => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ tar_tmp_path_cleanup_on_abort => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+ # XXX tar_tmp_path_dev_full (on Linux), to test out-of-space handling
+
+ tar_retr_tar_2gb_single_file => {
+ order => ++$order,
+ test_class => [qw(forking slow)],
+ },
+
+ # XXX tar_retr_tar_2gb_many_files
+
+ tar_retr_zip => {
+ order => ++$order,
+ test_class => [qw(forking)],
+ },
+
+};
+
+sub new {
+ return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+ return testsuite_get_runnable_tests($TESTS);
+}
+
+sub tar_retr_file {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ # Make sure that, if we're running as root, that the home directory has
+ # permissions/privs set for the account we create
+ if ($< == 0) {
+ unless (chmod(0755, $home_dir)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$home_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("test.txt.tar");
+ if ($conn) {
+ die("RETR test.txt.tar succeeded unexpectedly");
+ }
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 550;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'test.txt.tar: No such file or directory';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $test_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $test_file: $!");
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $tar = Archive::Tar->new($conn);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/test.txt';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/test.txt'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_already_exists {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ # Make sure that, if we're running as root, that the home directory has
+ # permissions/privs set for the account we create
+ if ($< == 0) {
+ unless (chmod(0755, $home_dir)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $archive = File::Spec->rel2abs("t/etc/modules/mod_tar/subdir.tar");
+ my $test_file = File::Spec->rel2abs("$tmpdir/subdir.tar");
+ unless (copy($archive, $test_file)) {
+ die("Can't copy $archive to $test_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ $ctx->add("Hello, World!\n");
+ my $expected_md5 = $ctx->hexdigest();
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $tar = Archive::Tar->new($conn);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/test.txt';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/test.txt'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_symlinks {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($sub_dir);
+
+ # Create three files in subdir
+ for (my $i = 0; $i < 3; $i++) {
+ my $test_file = File::Spec->rel2abs("$sub_dir/test" . ($i + 1) . ".txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+ }
+
+ my $symlink_dir = File::Spec->rel2abs("$tmpdir/symlinkdir");
+ mkpath($symlink_dir);
+
+ # Create three symlinks in symlinkdir to the files in subdir (using
+ # relative paths).
+
+ my $cwd = getcwd();
+
+ unless (chdir($symlink_dir)) {
+ die("Can't chdir to $symlink_dir: $!");
+ }
+
+ # Create three files in subdir
+ for (my $i = 0; $i < 3; $i++) {
+ my $symlink_src = "../subdir/test" . ($i + 1) . ".txt";
+ my $symlink_dst = "./test" . ($i + 1) . ".lnk";
+
+ unless (symlink($symlink_src, $symlink_dst)) {
+ die("Can't symlink '$symlink_src' to '$symlink_dst': $!");
+ }
+ }
+
+ unless (chdir($cwd)) {
+ die("Can't chdir to $cwd: $!");
+ }
+
+ # Make sure that, if we're running as root, that the home directory has
+ # permissions/privs set for the account we create
+ if ($< == 0) {
+ unless (chmod(0755, $home_dir)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("symlinkdir.tar");
+ unless ($conn) {
+ die("RETR symlinkdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $tar = Archive::Tar->new($conn);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 4;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'symlinkdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ for (my $i = 0; $i < 3; $i++) {
+ $expected = "symlinkdir/test" . ($i + 1) . '.lnk';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ my $ent = ($tar->get_files($expected))[0];
+ $self->assert(defined($ent),
+ test_msg("Unable to get info for $expected from archive"));
+
+ $self->assert($ent->is_symlink(),
+ test_msg("File $expected is not a symlink as expected"));
+
+ $expected = '../subdir/test' . ($i + 1) . '.txt';
+ my $ent_linkname = $ent->linkname();
+ $self->assert($ent_linkname eq $expected,
+ test_msg("Expected linkname '$expected', got '$ent_linkname'"));
+ }
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_symlinks_opt_dereference {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($sub_dir);
+
+ # Create three files in subdir
+ for (my $i = 0; $i < 3; $i++) {
+ my $test_file = File::Spec->rel2abs("$sub_dir/test" . ($i + 1) . ".txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+ }
+
+ my $symlink_dir = File::Spec->rel2abs("$tmpdir/symlinkdir");
+ mkpath($symlink_dir);
+
+ # Create three symlinks in symlinkdir to the files in subdir (using
+ # relative paths).
+
+ my $cwd = getcwd();
+
+ unless (chdir($symlink_dir)) {
+ die("Can't chdir to $symlink_dir: $!");
+ }
+
+ # Create three files in subdir
+ for (my $i = 0; $i < 3; $i++) {
+ my $symlink_src = "../subdir/test" . ($i + 1) . ".txt";
+ my $symlink_dst = "./test" . ($i + 1) . ".lnk";
+
+ unless (symlink($symlink_src, $symlink_dst)) {
+ die("Can't symlink '$symlink_src' to '$symlink_dst': $!");
+ }
+ }
+
+ unless (chdir($cwd)) {
+ die("Can't chdir to $cwd: $!");
+ }
+
+ # Make sure that, if we're running as root, that the home directory has
+ # permissions/privs set for the account we create
+ if ($< == 0) {
+ unless (chmod(0755, $home_dir)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ TarOptions => 'dereference',
+# TarOptions => 'FollowSymlinks',
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("symlinkdir.tar");
+ unless ($conn) {
+ die("RETR symlinkdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $tar = Archive::Tar->new($conn);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 4;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'symlinkdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ for (my $i = 0; $i < 3; $i++) {
+ $expected = "symlinkdir/test" . ($i + 1) . '.lnk';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ my $ent = ($tar->get_files($expected))[0];
+ $self->assert(defined($ent),
+ test_msg("Unable to get info for $expected from archive"));
+
+ $self->assert($ent->is_file(),
+ test_msg("File $expected is not a symlink as expected"));
+
+ $expected = 14;
+ my $ent_sz = $ent->size();
+ $self->assert($ent_sz == $expected,
+ test_msg("Expected file size $expected, got $ent_sz"));
+ }
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_enable_off {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ # MacOSX-specific hack
+ if ($^O eq 'darwin') {
+ $sub_dir = ('/private' . $sub_dir);
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ Directory => {
+ $sub_dir => {
+ TarEnable => 'off',
+ },
+ },
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ # This should fail, since the file doesn't exist, and we configured
+ # "TarEnable off" in that directory.
+ my $conn = $client->retr_raw("subdir.tar");
+ if ($conn) {
+ die("RETR subdir.tar succeeded unexpectedly");
+ }
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 550;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'subdir.tar: No such file or directory';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_notar {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ my $notar_file = File::Spec->rel2abs("$sub_dir/.notar");
+ if (open(my $fh, "> $notar_file")) {
+ unless (close($fh)) {
+ die("Can't write $notar_file: $!");
+ }
+
+ } else {
+ die("Can't open $notar_file: $!");
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ # This should fail, since the file doesn't exist, and we configured
+ # a .notar file in that directory.
+ my $conn = $client->retr_raw("subdir.tar");
+ if ($conn) {
+ die("RETR subdir.tar succeeded unexpectedly");
+ }
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 550;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'subdir.tar: No such file or directory';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_gz {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $test_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $test_file: $!");
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar.gz");
+ unless ($conn) {
+ die("RETR subdir.tar.gz failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $zio = IO::Zlib->new($conn, 'rb');
+ my $tar = Archive::Tar->new($zio);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/test.txt';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/test.txt'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tgz {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $test_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $test_file: $!");
+ }
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tgz");
+ unless ($conn) {
+ die("RETR subdir.tgz failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $zio = IO::Zlib->new($conn, 'rb');
+ my $tar = Archive::Tar->new($zio);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/test.txt';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/test.txt'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_bz2 {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $src_file = File::Spec->rel2abs("$sub_dir/src.bin");
+ if (open(my $fh, "> $src_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $src_file: $!");
+ }
+
+ } else {
+ die("Can't open $src_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $src_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $src_file: $!");
+ }
+
+ my $dst_bz2_file = File::Spec->rel2abs("$tmpdir/dst.tar.bz2");
+ my $dst_tar_file = File::Spec->rel2abs("$tmpdir/dst.tar");
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar.bz2");
+ unless ($conn) {
+ die("RETR subdir.tar.bz2 failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ # Download the data to a separate bunzipped file, and let Archive::Tar
+ # work on that.
+
+ my $dstfh;
+ unless (open($dstfh, "> $dst_bz2_file")) {
+ die("Can't open $dst_bz2_file: $!");
+ }
+ binmode($dstfh);
+
+ my $buf;
+ my $buflen = 16384;
+ while ($conn->read($buf, $buflen, 25)) {
+ print $dstfh $buf;
+ }
+
+ unless (close($dstfh)) {
+ die("Can't write $dst_bz2_file: $!");
+ }
+
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ eval {
+ # Uncompress the file
+ `bunzip2 -q $dst_bz2_file`;
+
+ my $dstfh;
+ unless (open($dstfh, "< $dst_tar_file")) {
+ die("Can't read $dst_tar_file: $!");
+ }
+ binmode($dstfh);
+
+ my $tar = Archive::Tar->new($dstfh);
+ unless (defined($tar)) {
+ die("Can't read tar file from $dst_tar_file: " . $Archive::Tar::error);
+ }
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ my $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/src.bin';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/src.bin'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+
+ close($dstfh);
+ };
+ if ($@) {
+ $ex = $@;
+ }
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_xferlog_retr_tar {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $test_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $test_file: $!");
+ }
+
+ my $xfer_log = File::Spec->rel2abs("$tmpdir/xfer.log");
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ TransferLog => $xfer_log,
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $tar = Archive::Tar->new($conn);
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/test.txt';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ $ctx->add($tar->get_content('subdir/test.txt'));
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ eval {
+ # Now read in the TransferLog, make sure that the filename is the originally
+ # requested name, not the name as modified by mod_tar.
+
+ if (open(my $fh, "< $xfer_log")) {
+ my $line = <$fh>;
+ chomp($line);
+ close($fh);
+
+ my $expected = '^\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+\s+\d+\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+_\s+o\s+r\s+(\S+)\s+ftp\s+0\s+\*\s+c$';
+
+ $self->assert(qr/$expected/, $line,
+ test_msg("Expected '$expected', got '$line'"));
+
+ if ($line =~ /$expected/) {
+ my $remote_host = $1;
+ my $filesz = $2;
+ my $filename = $3;
+ my $xfer_type = $4;
+ my $user_name = $5;
+
+ $expected = '127.0.0.1';
+ $self->assert($expected eq $remote_host,
+ test_msg("Expected '$expected', got '$remote_host'"));
+
+ # The original filename, sans .tar extension
+ $expected = $sub_dir;
+
+ if ($^O eq 'darwin') {
+ # MacOSX-specific hack dealing with their tmp filesystem shenanigans
+ $expected = ('/private' . $expected);
+ }
+
+ $self->assert($expected eq $filename,
+ test_msg("Expected '$expected', got '$filename'"));
+
+ $expected = 'b';
+ $self->assert($expected eq $xfer_type,
+ test_msg("Expected '$expected', got '$xfer_type'"));
+
+ $expected = $user;
+ $self->assert($expected eq $user_name,
+ test_msg("Expected '$expected', got '$user_name'"));
+
+ } else {
+ die("Can't read $xfer_log: $!");
+ }
+ }
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_tmp_path_cleanup_on_abort {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/test.txt");
+ if (open(my $fh, "> $test_file")) {
+ print $fh "Hello, World!\n";
+ unless (close($fh)) {
+ die("Can't write $test_file: $!");
+ }
+
+ } else {
+ die("Can't open $test_file: $!");
+ }
+
+ my $tmp_path = File::Spec->rel2abs("$tmpdir/tarwork");
+ mkpath($tmp_path);
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ TarTempPath => $tmp_path,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ # Read in a couple of bytes, then abort the connection.
+ my $buf;
+ $conn->read($buf, 4, 25);
+ $conn->abort();
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Abort successful';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ # Scan the TarTempPath, make sure there are no files in there.
+ my $dirh;
+ unless (opendir($dirh, $tmp_path)) {
+ die("Can't open directory '$tmp_path': $!");
+ }
+
+ my $tmp_files = [grep { !/^\.$/ && !/^\.\.$/ } readdir($dirh)];
+ closedir($dirh);
+
+ my $nfiles = scalar(@$tmp_files);
+ $self->assert($nfiles == 0,
+ test_msg("Expected no tmp files, found $nfiles"));
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_tar_2gb_single_file {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $src_file = File::Spec->rel2abs("$sub_dir/src.bin");
+
+ # Create a file that is 2GB.
+ my $src_len = (2 ** 31);
+ if (open(my $fh, "> $src_file")) {
+
+ if ($ENV{TEST_VERBOSE}) {
+ print STDOUT "# Creating test file of $src_len bytes\n";
+ }
+
+ my $nchunks = 64;
+ my $chunklen = ($src_len / $nchunks);
+
+ for (my $i = 0; $i < $nchunks; $i++) {
+ print $fh "A" x $chunklen;
+ }
+
+ unless (close($fh)) {
+ die("Can't write $src_file: $!");
+ }
+
+ } else {
+ die("Can't open $src_file: $!");
+ }
+
+ my $dst_file = File::Spec->rel2abs("$tmpdir/dst.tar");
+
+ # This test could run for a while, since mod_tar has to read in the large
+ # file. So give the test time to run.
+ my $timeout_idle = 1800;
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ TimeoutIdle => $timeout_idle,
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 0,
+ 10, $timeout_idle);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.tar");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ # I'm not sure why, but Archive::Tar does not like reading tar data
+ # directly from the $conn if that tar data contains large files.
+ #
+ # To work around this, read the data from $conn into a local temp
+ # file, then set Archive::Tar to work on that local file.
+
+ my $dstfh;
+ unless (open($dstfh, "> $dst_file")) {
+ die("Can't write $dst_file: $!");
+ }
+ binmode($dstfh);
+
+ my $buf;
+ my $buflen = 16384;
+
+ while ($conn->read($buf, $buflen, 25)) {
+ print $dstfh $buf;
+ }
+
+ unless (close($dstfh)) {
+ die("Can't write $dst_file: $!");
+ }
+
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+
+ unless (open($dstfh, "< $dst_file")) {
+ die("Can't read $dst_file: $!");
+ }
+ binmode($dstfh);
+
+ if ($ENV{TEST_VERBOSE}) {
+ print STDOUT "# Finished downloading to $dst_file, verifying tar format\n";
+ }
+
+ my $tar = Archive::Tar->new($dstfh);
+ my $entries = { map { $_ => 1 } $tar->list_files() };
+
+ # Make sure the hashref contains the entries we expect
+ $expected = 2;
+ my $nents = scalar(keys(%$entries));
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/src.bin';
+ $self->assert(defined($entries->{$expected}),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ my $ent = ($tar->get_files($expected))[0];
+ $self->assert(defined($ent),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ my $ent_len = $ent->size();
+ $self->assert($ent_len eq $src_len,
+ test_msg("Expected file length $src_len, got $ent_len"));
+
+ close($dstfh);
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh, $timeout_idle + 10) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+sub tar_retr_zip {
+ my $self = shift;
+ my $tmpdir = $self->{tmpdir};
+
+ my $config_file = "$tmpdir/tar.conf";
+ my $pid_file = File::Spec->rel2abs("$tmpdir/tar.pid");
+ my $scoreboard_file = File::Spec->rel2abs("$tmpdir/tar.scoreboard");
+
+ my $log_file = test_get_logfile();
+
+ my $auth_user_file = File::Spec->rel2abs("$tmpdir/tar.passwd");
+ my $auth_group_file = File::Spec->rel2abs("$tmpdir/tar.group");
+
+ my $user = 'proftpd';
+ my $passwd = 'test';
+ my $group = 'ftpd';
+ my $home_dir = File::Spec->rel2abs($tmpdir);
+ my $uid = 500;
+ my $gid = 500;
+
+ my $sub_dir = File::Spec->rel2abs("$tmpdir/subdir");
+ mkpath($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)) {
+ die("Can't set perms on $home_dir to 0755: $!");
+ }
+
+ unless (chown($uid, $gid, $home_dir)) {
+ die("Can't set owner of $home_dir to $uid/$gid: $!");
+ }
+ }
+
+ auth_user_write($auth_user_file, $user, $passwd, $uid, $gid, $home_dir,
+ '/bin/bash');
+ auth_group_write($auth_group_file, $group, $gid, $user);
+
+ my $test_file = File::Spec->rel2abs("$sub_dir/src.bin");
+ 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: $!");
+ }
+
+ # Calculate the MD5 checksum of this file, for comparison with the
+ # downloaded file.
+ my $ctx = Digest::MD5->new();
+ my $expected_md5;
+
+ if (open(my $fh, "< $test_file")) {
+ binmode($fh);
+ $ctx->addfile($fh);
+ $expected_md5 = $ctx->hexdigest();
+ close($fh);
+
+ } else {
+ die("Can't read $test_file: $!");
+ }
+
+ my $dst_zip_file = File::Spec->rel2abs("$tmpdir/dst.zip");
+
+ my $config = {
+ PidFile => $pid_file,
+ ScoreboardFile => $scoreboard_file,
+ SystemLog => $log_file,
+ TraceLog => $log_file,
+ Trace => 'DEFAULT:10 tar:20',
+
+ AuthUserFile => $auth_user_file,
+ AuthGroupFile => $auth_group_file,
+
+ AllowOverwrite => 'on',
+ AllowStoreRestart => 'on',
+
+ IfModules => {
+ 'mod_tar.c' => {
+ TarEngine => 'on',
+ TarLog => $log_file,
+ },
+
+ 'mod_delay.c' => {
+ DelayEngine => 'off',
+ },
+ },
+ };
+
+ my ($port, $config_user, $config_group) = config_write($config_file, $config);
+
+ # Open pipes, for use between the parent and child processes. Specifically,
+ # the child will indicate when it's done with its test by writing a message
+ # to the parent.
+ my ($rfh, $wfh);
+ unless (pipe($rfh, $wfh)) {
+ die("Can't open pipe: $!");
+ }
+
+ my $ex;
+
+ # Fork child
+ $self->handle_sigchld();
+ defined(my $pid = fork()) or die("Can't fork: $!");
+ if ($pid) {
+ eval {
+ my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
+ $client->login($user, $passwd);
+ $client->type('binary');
+
+ my $conn = $client->retr_raw("subdir.zip");
+ unless ($conn) {
+ die("RETR subdir.tar failed: " . $client->response_code() . " " .
+ $client->response_msg());
+ }
+
+ my $dstfh;
+ unless (open($dstfh, "> $dst_zip_file")) {
+ die("Can't open $dst_zip_file: $!");
+ }
+
+ my $buf;
+ my $buflen = 16384;
+ while ($conn->read($buf, $buflen, 25)) {
+ print $dstfh $buf;
+ }
+
+ unless (close($dstfh)) {
+ die("Can't write $dst_zip_file: $!");
+ }
+
+ eval { $conn->close() };
+
+ my $resp_code = $client->response_code();
+ my $resp_msg = $client->response_msg();
+
+ my $expected;
+
+ $expected = 226;
+ $self->assert($expected == $resp_code,
+ test_msg("Expected response code $expected, got $resp_code"));
+
+ $expected = 'Transfer complete';
+ $self->assert($expected eq $resp_msg,
+ test_msg("Expected response message '$expected', got '$resp_msg'"));
+
+ $client->quit();
+ };
+
+ if ($@) {
+ $ex = $@;
+ }
+
+ $wfh->print("done\n");
+ $wfh->flush();
+
+ } else {
+ eval { server_wait($config_file, $rfh) };
+ if ($@) {
+ warn($@);
+ exit 1;
+ }
+
+ exit 0;
+ }
+
+ # Stop server
+ server_stop($pid_file);
+
+ $self->assert_child_ok($pid);
+
+ eval {
+ my $zip = Archive::Zip->new($dst_zip_file);
+
+ my $entries = [$zip->members()];
+
+ # Make sure the arrayref contains the entries we expect
+ my $expected = 2;
+ my $nents = scalar(@$entries);
+ $self->assert($nents == $expected,
+ test_msg("Expected $expected entries, found $nents"));
+
+ $expected = 'subdir/';
+ my $ent = $zip->memberNamed($expected);
+ $self->assert(defined($ent),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ $expected = 'subdir/src.bin';
+ $ent = $zip->memberNamed($expected);
+ $self->assert(defined($ent),
+ test_msg("Expected entry for '$expected', did not see one"));
+
+ # Make sure the file contents have not been corrupted in transit
+
+ $ctx = Digest::MD5->new();
+ my $data = $zip->contents('subdir/src.bin');
+ $ctx->add($data);
+
+ my $test_md5 = $ctx->hexdigest();
+
+ $self->assert($test_md5 eq $expected_md5,
+ test_msg("Expected MD5 checksum '$expected_md5', got '$test_md5'"));
+ };
+ if ($@) {
+ $ex = $@;
+ }
+
+ if ($ex) {
+ test_append_logfile($log_file, $ex);
+ unlink($log_file);
+
+ die($ex);
+ }
+
+ unlink($log_file);
+}
+
+1;
=====================================
t/modules/mod_tar.t
=====================================
--- /dev/null
+++ b/t/modules/mod_tar.t
@@ -0,0 +1,11 @@
+#!/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_tar");
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar/commit/51b160e73027ee439ad70cea7a5f3f17a7dd4013
---
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd-mod-tar/commit/51b160e73027ee439ad70cea7a5f3f17a7dd4013
You're receiving this email because of your account on salsa.debian.org.
More information about the Pkg-proftpd-maintainers
mailing list