proftpd-mod-tar-0.4/0000755000175000017500000000000013063310535013347 5ustar hillehilleproftpd-mod-tar-0.4/t/0000755000175000017500000000000013063310535013612 5ustar hillehilleproftpd-mod-tar-0.4/t/etc/0000755000175000017500000000000013063310535014365 5ustar hillehilleproftpd-mod-tar-0.4/t/etc/modules/0000755000175000017500000000000013063310535016035 5ustar hillehilleproftpd-mod-tar-0.4/t/etc/modules/mod_tar/0000755000175000017500000000000013063310535017462 5ustar hillehilleproftpd-mod-tar-0.4/t/etc/modules/mod_tar/subdir.tar0000644000175000017500000002400013063310535021456 0ustar hillehillesubdir/0000755000175000017500000000000011726212312010157 5ustar tjtjsubdir/test.txt0000644000175000017500000000001611726212312011674 0ustar tjtjHello, World! proftpd-mod-tar-0.4/t/lib/0000755000175000017500000000000013063310535014360 5ustar hillehilleproftpd-mod-tar-0.4/t/lib/ProFTPD/0000755000175000017500000000000013063310535015576 5ustar hillehilleproftpd-mod-tar-0.4/t/lib/ProFTPD/Tests/0000755000175000017500000000000013063310535016700 5ustar hillehilleproftpd-mod-tar-0.4/t/lib/ProFTPD/Tests/Modules/0000755000175000017500000000000013063310535020310 5ustar hillehilleproftpd-mod-tar-0.4/t/lib/ProFTPD/Tests/Modules/mod_tar.pm0000644000175000017500000021424513063310535022303 0ustar hillehillepackage 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; proftpd-mod-tar-0.4/t/modules/0000755000175000017500000000000013063310535015262 5ustar hillehilleproftpd-mod-tar-0.4/t/modules/mod_tar.t0000644000175000017500000000026313063310535017075 0ustar hillehille#!/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"); proftpd-mod-tar-0.4/.travis.yml0000644000175000017500000000167713063310535015473 0ustar hillehillelanguage: 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 proftpd-mod-tar-0.4/doc/0000755000175000017500000000000013063310535014114 5ustar hillehilleproftpd-mod-tar-0.4/doc/NOTES.libarchive0000644000175000017500000001060713063310535017042 0ustar hillehille 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. proftpd-mod-tar-0.4/.gitattributes0000644000175000017500000000006213063310535016240 0ustar hillehille*.pl linguist-language=C *.pm linguist-language=C proftpd-mod-tar-0.4/mod_tar.html0000644000175000017500000002210413063310535015661 0ustar hillehille ProFTPD module mod_tar

ProFTPD module mod_tar



The mod_tar module supports on-the-fly creation of tar files. Whenever a client attempts to download a directory as a tar file, the mod_tar module will automatically create a tar file of that directory.

To provide this feature, the mod_tar module uses the libarchive library; see:

  http://libarchive.github.com/

This module is contained in the mod_tar file for ProFTPD 1.3.x, and is not compiled by default. Installation instructions are discussed here. More examples of mod_tar usage can be found here.

The most current version of mod_tar can be found at:

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

Author

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

Directives


TarEnable

Syntax: TarEnable on|off
Default: None
Context: <Directory>, .ftpaccess Module: mod_tar
Compatibility: 1.3.1rc1 and later

The TarEnable directive can be used to block or prevent a directory from being turned into a tar file by mod_tar.


TarEngine

Syntax: TarEngine on|off
Default: off
Context: "server config", <VirtualHost>, <Global>, <Anonymous>
Module: mod_tar
Compatibility: 1.3.1rc1 and later

The TarEngine directive enables or disables the module's support for on-the-fly tar file creation.


TarLog

Syntax: TarLog file
Default: None
Context: server config, <VirtualHost>, <Global>
Module: mod_tar
Compatibility: 1.3.1rc1 and later

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

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


TarOptions

Syntax: TarOptions opt1 ...
Default: None
Context: "server config", <VirtualHost>, <Global>, <Anonymous>
Module: mod_tar
Compatibility: 1.3.1rc1 and later

The TarOptions directive is used to configure various optional behavior of mod_tar, usually pertaining to how the .tar files are constructed.

Example:

  TarOptions FollowSymlinks

The currently implemented options are:


TarTempPath

Syntax: TarTempPath path
Default: "./"
Context: "server config", <VirtualHost>, <Global>, <Anonymous>
Module: mod_tar
Compatibility: 1.3.1rc1 and later

The TarTempPath directive controls the directory where mod_tar will writes its temporary .tar files. Keep in mind that the TarTempPath is subject to any chroot (i.e. use of DefaultRoot or <Anonymous>).

The default TarTempPath is "./", which means that the temporary .tar files are written in the current directory of the FTP session.


Installation

To install mod_tar, copy the mod_tar.c file into:
  proftpd-dir/contrib/
after unpacking the latest proftpd-1.3.x source code. For including mod_tar as a staticly linked module:
  ./configure --with-modules=mod_tar
To build mod_tar as a DSO module:
  ./configure --enable-dso --with-shared=mod_tar
Then follow the usual steps:
  make
  make install

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

  # prxs -c -i -d -I /path/to/libarchive/include -L /path/to/libarchive/lib mod_tar.c

Note that in order to support gzip and bzip2 compression, the mod_tar module requires linking with the zlib (-lz) and bzip2 lib (-lbz2) libraries. You may need to install these packages on your system in order to build mod_tar.


Usage

The mod_tar module works by watching all download requests (i.e. RETR commands), looking specifically for requests like:

 RETR $dir.tar.gz
The following extensions will trigger mod_tar to attempt on-the-fly tar file creation:

If the requested tar file already exists, then mod_tar does nothing, and lets the download proceed normally. If the requested tar file is not for a directory, then mod_tar does nothing.

Next, the mod_tar module checks for the existence of a "$dir/.notar" file. If this file is present, then mod_tar does nothing. (This provides feature compatibility with wu-ftpd's on-the-fly tar file creation feature.)

The mod_tar module then checks to see if TarEnable has been configured for the requested directory. For example, you can block certain directories from being bundled up by mod_tar by using:

  <Directory $dir>
    TarEnable off
  </Directory>

Once these checks have passed, a randomly generated unique filename is generated for the tar file to be created; the tar file is created in the session's current working directory (although this can be changed using the TarTempPath directive), and is deleted after the download finishes. This means that the client will need write privileges in that directory in order for the tar file to be created.

No external commands are used for creating the tar file. Searches for on-the-fly tar file creation will turn up reports of vulnerabilities and issues with the tar file feature in wu-ftpd. The problem there was that wu-ftpd used external commands, such as /bin/tar, to create the tar files. These commands take a range of command-line options; malicious FTP clients could exploit those command-line options, and wu-ftpd's on-the-fly tar file implementation, to attack the server. By contrast, the mod_tar module does not use any external commands; it uses the libtar library for creating the tar file. And mod_tar ensures that the requested path is indeed a directory and that that directory path is treated as-is, with no special interpolation or interpretation.

Example configuration:

  <IfModule mod_tar.c>
    TarEngine on
    TarLog /var/ftpd/tar.log
  </IfModule>



Author: $Author: tj $
Last Updated: $Date: 2009/08/20 17:07:14 $


© Copyright 2009-2012 TJ Saunders
All Rights Reserved


proftpd-mod-tar-0.4/README.md0000644000175000017500000000112113063310535014621 0ustar hillehilleproftpd-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). proftpd-mod-tar-0.4/mod_tar.c0000644000175000017500000006736313063310535015157 0ustar hillehille/* * ProFTPD - mod_tar * 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 * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 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. * * $Libraries: -larchive -lz -lbz2$ */ #include "conf.h" #include "privs.h" #include #include #define MOD_TAR_VERSION "mod_tar/0.4" /* Make sure the version of proftpd is as necessary. */ #if PROFTPD_VERSION_NUMBER < 0x0001030101 # error "ProFTPD 1.3.1rc1 or later required" #endif 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 #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; struct archive_data { const char *path; pr_fh_t *fh; }; static const char *trace_channel = "tar"; 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; fh = pr_fsio_open(path, O_RDONLY); if (fh == NULL) { int xerrno = errno; pr_trace_msg(trace_channel, 3, "unable to read '%s': %s", path, strerror(xerrno)); errno = xerrno; return -1; } 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; } if (S_ISDIR(pst.st_mode)) { int xerrno = EISDIR; pr_trace_msg(trace_channel, 3, "unable to use '%s': %s", path, strerror(xerrno)); pr_fsio_close(fh); errno = xerrno; return -1; } tmp_pool = make_sub_pool(p); /* Use a buffer size based on the filesystem blocksize, for better IO. */ buflen = st->st_blksize; buf = palloc(tmp_pool, buflen); res = pr_fsio_read(fh, buf, buflen); while (res > 0) { pr_signals_handle(); if (archive_write_data(tar, buf, res) < 0) { int xerrno; xerrno = archive_errno(tar); pr_trace_msg(trace_channel, 3, "error adding data to archive: %s", archive_error_string(tar)); destroy_pool(tmp_pool); pr_fsio_close(fh); errno = xerrno; return -1; } res = pr_fsio_read(fh, buf, buflen); } 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; if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) { res = pr_fsio_lstat(real_name, &st); } else { res = pr_fsio_stat(real_name, &st); } if (res < 0) { int xerrno = errno; pr_trace_msg(trace_channel, 9, "error stat'ing '%s': %s", real_name, strerror(xerrno)); errno = xerrno; return -1; } 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) { i = PR_TUNABLE_PATH_MAX - 1; } path[i] = '\0'; 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; xerrno = archive_errno(tar); pr_trace_msg(trace_channel, 3, "error writing archive entry header: %s", archive_error_string(tar)); errno = xerrno; return -1; } /* 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(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; DIR *dirh; struct stat st; int res; 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) { return 0; } return -1; } while ((dent = readdir(dirh)) != NULL) { pr_signals_handle(); 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); if (save_dir) { memset(save_path, '\0', sizeof(save_path)); snprintf(save_path, sizeof(save_path)-1, "%s/%s", save_dir, dent->d_name); } if (!(tar_opts & TAR_OPT_DEREF_SYMLINKS)) { res = pr_fsio_lstat(real_path, &st); } else { res = pr_fsio_stat(real_path, &st); } if (res < 0) { int xerrno = errno; (void) closedir(dirh); errno = xerrno; return -1; } if (S_ISDIR(st.st_mode)) { 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(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_archive_open_cb(struct archive *tar, void *user_data) { struct archive_data *tar_data; pr_fh_t *fh; tar_data = user_data; fh = pr_fsio_open(tar_data->path, O_WRONLY|O_CREAT); if (fh == NULL) { return ARCHIVE_FATAL; } /* 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)); } tar_data->fh = fh; return ARCHIVE_OK; } static ssize_t tar_archive_write_cb(struct archive *tar, void *user_data, const void *buf, size_t buflen) { struct archive_data *tar_data; 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; } tar_data->fh = NULL; return ARCHIVE_OK; } 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; tar = archive_write_new(); if (tar == NULL) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "error allocating new archive handle: %s", archive_error_string(tar)); errno = archive_errno(tar); return -1; } /* 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 configuring archive handle for ustar format: %s", archive_error_string(tar)); errno = archive_errno(tar); return -1; } } 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; } } 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; } } 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. */ 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); 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; xerrno = archive_errno(tar); (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "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 (append_tree(p, tar, entry, 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)); archive_entry_free(entry); (void) archive_write_close(tar); archive_write_free(tar); (void) unlink(dst_file); errno = xerrno; return -1; } 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, "error writing tar file: %s", archive_error_string(tar)); archive_write_free(tar); (void) unlink(dst_file); errno = xerrno; return -1; } archive_write_free(tar); return 0; } static char *tar_get_ext_tar(char *path, size_t path_len) { if (path_len < 4) { return NULL; } if (path[path_len-4] == '.') { if ((path[path_len-3] == 'T' || path[path_len-3] == 't') && (path[path_len-2] == 'A' || path[path_len-2] == 'a') && (path[path_len-1] == 'R' || path[path_len-1] == 'r')) { return &path[path_len-4]; } } return NULL; } static char *tar_get_ext_tgz(char *path, size_t path_len) { if (path_len < 4) { return NULL; } if (path[path_len-4] == '.') { if ((path[path_len-3] == 'T' || path[path_len-3] == 't') && (path[path_len-2] == 'G' || path[path_len-2] == 'g') && (path[path_len-1] == 'Z' || path[path_len-1] == 'z')) { return &path[path_len-4]; } } return NULL; } static char *tar_get_ext_targz(char *path, size_t path_len) { if (path_len < 7) { return NULL; } if (path[path_len-7] == '.') { if ((path[path_len-6] == 'T' || path[path_len-6] == 't') && (path[path_len-5] == 'A' || path[path_len-5] == 'a') && (path[path_len-4] == 'R' || path[path_len-4] == 'r') && path[path_len-3] == '.' && (path[path_len-2] == 'G' || path[path_len-2] == 'g') && (path[path_len-1] == 'z' || path[path_len-1] == 'z')) { return &path[path_len-7]; } return NULL; } 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; } if (path[path_len-8] == '.') { if ((path[path_len-7] == 'T' || path[path_len-7] == 't') && (path[path_len-6] == 'A' || path[path_len-6] == 'a') && (path[path_len-5] == 'R' || path[path_len-5] == 'r') && path[path_len-4] == '.' && (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-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 */ /* usage: TarEnable on|off */ MODRET set_tarenable(cmd_rec *cmd) { int bool = -1; config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_DIR|CONF_DYNDIR); bool = get_boolean(cmd, 1); if (bool == -1) CONF_ERROR(cmd, "expected Boolean parameter"); c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = palloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = bool; return PR_HANDLED(cmd); } /* usage: TarEngine on|off */ MODRET set_tarengine(cmd_rec *cmd) { int bool = -1; config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON); bool = get_boolean(cmd, 1); if (bool == -1) CONF_ERROR(cmd, "expected Boolean parameter"); c = add_config_param(cmd->argv[0], 1, NULL); c->argv[0] = palloc(c->pool, sizeof(int)); *((int *) c->argv[0]) = bool; c->flags |= CF_MERGEDOWN; return PR_HANDLED(cmd); } /* usage: TarLog path|"none" */ MODRET set_tarlog(cmd_rec *cmd) { CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL); add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); return PR_HANDLED(cmd); } /* usage: TarOptions opt1 opt2 ... */ MODRET set_taroptions(cmd_rec *cmd) { config_rec *c = NULL; register unsigned int i = 0; unsigned long opts = 0UL; if (cmd->argc-1 == 0) CONF_ERROR(cmd, "wrong number of parameters"); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON); c = add_config_param(cmd->argv[0], 1, NULL); for (i = 1; i < cmd->argc; i++) { if (strcmp(cmd->argv[i], "FollowSymlinks") == 0 || strcmp(cmd->argv[i], "dereference") == 0) { opts |= TAR_OPT_DEREF_SYMLINKS; } else { CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown TarOption '", cmd->argv[i], "'", NULL)); } } c->argv[0] = pcalloc(c->pool, sizeof(unsigned long)); *((unsigned long *) c->argv[0]) = opts; c->flags |= CF_MERGEDOWN; return PR_HANDLED(cmd); } /* usage: TarTempPath path */ MODRET set_tartemppath(cmd_rec *cmd) { config_rec *c; CHECK_ARGS(cmd, 1); CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL|CONF_ANON); c = add_config_param_str(cmd->argv[0], 1, cmd->argv[1]); c->flags |= CF_MERGEDOWN; return PR_HANDLED(cmd); } /* Command handlers */ MODRET tar_post_pass(cmd_rec *cmd) { config_rec *c; c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarEngine", FALSE); if (c) { int enable_tar = *((int *) c->argv[0]); if (enable_tar) { tar_engine = TRUE; } } if (tar_engine) { pr_event_register(&tar_module, "core.exit", tar_exit_ev, NULL); c = find_config(TOPLEVEL_CONF, CONF_PARAM, "TarOptions", FALSE); 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); if (c) { tar_tmp_path = dir_canonical_path(session.pool, c->argv[0]); if (session.chroot_path) { size_t chroot_len; chroot_len = strlen(session.chroot_path); if (strncmp(tar_tmp_path, session.chroot_path, chroot_len) == 0) { tar_tmp_path += chroot_len; } } (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "using '%s' as the staging directory for temporary .tar files", tar_tmp_path); } } return PR_DECLINED(cmd); } MODRET tar_pre_retr(cmd_rec *cmd) { char *path, *tmp; size_t path_len; if (tar_engine == FALSE) { return PR_DECLINED(cmd); } if (cmd->argc < 2) { return PR_DECLINED(cmd); } path = pr_fs_decode_path(cmd->tmp_pool, cmd->arg); /* If dir_realpath() returns non-NULL here, then the requested path * exists; we should not try to handle it in that case. */ tmp = dir_realpath(cmd->tmp_pool, path); if (tmp != NULL) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "path '%s' already exists, skipping", tmp); return PR_DECLINED(cmd); } /* Check if the requested path ends in ".tar", ".tar.gz", ".tgz", * or ".tar.bz2". If it does, check if the name leading up to that * extension is a directory. If both of these conditions are met, we can * proceed. */ path_len = strlen(path); if (path_len > 4) { char *dir, *notar_file, *ptr, *tar_file; int fd, res; struct stat st; config_rec *d; unsigned long flags = 0UL; 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); return PR_DECLINED(cmd); } *ptr = '\0'; path = dir_realpath(cmd->tmp_pool, path); res = dir_exists(path); if (res == 0) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "'%s' is not a directory, ignoring", path); *ptr = '.'; return PR_DECLINED(cmd); } /* Check for a "$dir/.notar" file, for backward compatibility with * wu-ftpd. */ notar_file = pdircat(cmd->tmp_pool, path, ".notar", NULL); if (file_exists(notar_file)) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "'%s' present, skipping tar file of '%s' directory", notar_file, path); *ptr = '.'; return PR_DECLINED(cmd); } /* Check for "TarEnable off" for this directory. Make sure we check * for any possible .ftpaccess files in the target directory which * may contain a TarEnable configuration. */ if (pr_fsio_lstat(path, &st) == 0) { build_dyn_config(cmd->pool, path, &st, TRUE); } d = dir_match_path(cmd->tmp_pool, path); if (d) { config_rec *c; c = find_config(d->subset, CONF_PARAM, "TarEnable", FALSE); if (c) { int tar_enable; tar_enable = *((int *) c->argv[0]); if (tar_enable == FALSE) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "'TarEnable off' found, skipping tar file of '%s' directory", path); *ptr = '.'; return PR_DECLINED(cmd); } } } else { pr_trace_msg(trace_channel, 9, "no match found for '%s'", path); } dir = strrchr(path, '/'); if (dir == NULL) { dir = path; } else { dir++; } tar_file = pdircat(cmd->pool, tar_tmp_path, "XXXXXX", NULL); 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(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. */ 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(xerrno)); *ptr = '.'; return PR_DECLINED(cmd); } /* Stash this temporary filename. */ if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "mod_tar.tar-file"), tar_file, 0) < 0) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "error stashing tar file in notes: %s", strerror(errno)); *ptr = '.'; return PR_DECLINED(cmd); } /* We also make a copy of the temporary filename elsewhere, in case * the client dies unexpectedly. */ tar_tmp_file = pstrdup(session.pool, tar_file); /* And stash a copy of the originally requested directory. */ if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "mod_tar.orig-path"), pstrdup(cmd->pool, path), 0) < 0) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "error stashing original path in notes: %s", strerror(errno)); *ptr = '.'; return PR_DECLINED(cmd); } *ptr = '.'; (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "replaced 'RETR %s' with 'RETR %s'", cmd->arg, tar_file); cmd->arg = tar_file; } return PR_DECLINED(cmd); } MODRET tar_log_retr(cmd_rec *cmd) { char *path; if (tar_engine == FALSE) { return PR_DECLINED(cmd); } path = pr_table_get(cmd->notes, "mod_tar.tar-file", NULL); if (path != NULL) { if (unlink(path) < 0) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "error deleting '%s': %s", path, strerror(errno)); } else { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "deleted tar file '%s'", path); tar_tmp_file = NULL; } } path = pr_table_get(cmd->notes, "mod_tar.orig-path", NULL); 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. */ session.xfer.path = path; } return PR_DECLINED(cmd); } /* Event handlers */ static void tar_exit_ev(const void *event_data, void *user_data) { /* Clean up any temporary tar files which we might have left around because * the client exited uncleanly. */ if (tar_tmp_file != NULL) { if (unlink(tar_tmp_file) < 0) { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "error deleting '%s': %s", tar_tmp_file, strerror(errno)); } else { (void) pr_log_writefile(tar_logfd, MOD_TAR_VERSION, "deleted tar file '%s'", tar_tmp_file); tar_tmp_file = NULL; } } } /* Initialization functions */ static int tar_sess_init(void) { config_rec *c; c = find_config(main_server->conf, CONF_PARAM, "TarLog", FALSE); if (c && 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) { case 0: break; case -1: pr_log_debug(DEBUG1, MOD_TAR_VERSION ": unable to open TarLog '%s': %s", path, strerror(xerrno)); break; case PR_LOG_SYMLINK: pr_log_debug(DEBUG1, MOD_TAR_VERSION ": unable to open TarLog '%s': %s", path, "is a symlink"); break; case PR_LOG_WRITABLE_DIR: pr_log_debug(DEBUG1, MOD_TAR_VERSION ": unable to open TarLog '%s': %s", path, "parent directory is world-writable"); break; } } return 0; } static int tar_init(void) { pr_log_debug(DEBUG0, MOD_TAR_VERSION ": using libarchive %s", archive_version_string()); return 0; } /* Module API tables */ static conftable tar_conftab[] = { { "TarEnable", set_tarenable, NULL }, { "TarEngine", set_tarengine, NULL }, { "TarLog", set_tarlog, NULL }, { "TarOptions", set_taroptions, NULL }, { "TarTempPath", set_tartemppath, NULL }, { NULL } }; static cmdtable tar_cmdtab[] = { { POST_CMD, C_PASS, G_NONE, tar_post_pass, FALSE, FALSE }, { PRE_CMD, C_RETR, G_NONE, tar_pre_retr, FALSE, FALSE }, { LOG_CMD, C_RETR, G_NONE, tar_log_retr, FALSE, FALSE }, { LOG_CMD_ERR, C_RETR, G_NONE, tar_log_retr, FALSE, FALSE }, }; module tar_module = { NULL, NULL, /* Module API version 2.0 */ 0x20, /* Module name */ "tar", /* Module config handler table */ tar_conftab, /* Module command handler table */ tar_cmdtab, /* Module auth handler table */ NULL, /* Module init function */ tar_init, /* Session init function */ tar_sess_init, /* Module version */ MOD_TAR_VERSION };