diff --git a/.gitignore b/.gitignore index c6f5e9e..262144f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ mod_vroot.h *.lo *.log *Tests*.log +*.a *.o *~ diff --git a/mod_vroot.c b/mod_vroot.c index 741d522..4208923 100644 --- a/mod_vroot.c +++ b/mod_vroot.c @@ -1,7 +1,7 @@ /* * ProFTPD: mod_vroot -- a module implementing a virtual chroot capability * via the FSIO API - * Copyright (c) 2002-2019 TJ Saunders + * Copyright (c) 2002-2021 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 @@ -41,6 +41,7 @@ unsigned int vroot_opts = 0; module vroot_module; static int vroot_engine = FALSE; +static const char *trace_channel = "vroot"; #if PROFTPD_VERSION_NUMBER >= 0x0001030407 static int vroot_use_mkdtemp = FALSE; @@ -242,19 +243,20 @@ MODRET set_vrootserverroot(cmd_rec *cmd) { /* Command handlers */ -MODRET vroot_log_retr(cmd_rec *cmd) { - const char *key, *path; - - if (vroot_engine == FALSE || - session.chroot_path == NULL) { - return PR_DECLINED(cmd); - } - - key = "mod_xfer.retr-path"; +static const char *vroot_cmd_fixup_path(cmd_rec *cmd, const char *key, + int use_best_path) { + const char *path; + char *real_path = NULL; path = pr_table_get(cmd->notes, key, NULL); if (path != NULL) { - char *real_path; + if (use_best_path == TRUE) { + /* Only needed for mod_sftp sessions, to do what mod_xfer does for FTP + * commands, but in a way that does not require mod_sftp changes. + * Probably too clever. + */ + path = dir_best_path(cmd->pool, path); + } if (*path == '/') { const char *base_path; @@ -267,43 +269,209 @@ MODRET vroot_log_retr(cmd_rec *cmd) { real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH); } + pr_trace_msg(trace_channel, 17, + "fixed up '%s' path in command %s; was '%s', now '%s'", key, + (char *) cmd->argv[0], path, real_path); pr_table_set(cmd->notes, key, real_path, 0); } + return real_path; +} + +MODRET vroot_pre_scp_retr(cmd_rec *cmd) { + const char *key, *proto, *real_path; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + /* As a PRE_CMD handler, we only run for SCP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "scp") != 0) { + return PR_DECLINED(cmd); + } + + /* Unlike SFTP sessions, mod_sftp does NOT set these cmd->notes for SCP + * sessions before doing the PRE_CMD dispatching. So we do it ourselves, + * pre-emptively, before using our other machinery. + */ + key = "mod_xfer.retr-path"; + (void) pr_table_add(cmd->notes, key, pstrdup(cmd->pool, cmd->arg), 0); + + real_path = vroot_cmd_fixup_path(cmd, key, TRUE); + if (real_path != NULL) { + /* In addition, for SCP sessions, we modify cmd->arg as well, for + * mod_sftp's benefit. + */ + cmd->arg = (char *) real_path; + } + return PR_DECLINED(cmd); } -MODRET vroot_log_stor(cmd_rec *cmd) { - const char *key, *path; +MODRET vroot_pre_sftp_retr(cmd_rec *cmd) { + const char *key, *proto, *real_path; if (vroot_engine == FALSE || session.chroot_path == NULL) { return PR_DECLINED(cmd); } - key = "mod_xfer.store-path"; + /* As a PRE_CMD handler, we only run for SFTP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "sftp") != 0) { + return PR_DECLINED(cmd); + } + key = "mod_xfer.retr-path"; + real_path = vroot_cmd_fixup_path(cmd, key, TRUE); + if (real_path != NULL) { + /* In addition, for SFTP sessions, we modify cmd->arg as well, for + * mod_sftp's benefit. + */ + cmd->arg = (char *) real_path; + } + + return PR_DECLINED(cmd); +} + +MODRET vroot_post_sftp_retr(cmd_rec *cmd) { + const char *key, *path, *proto; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + /* As a POST_CMD handler, we only run for SFTP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "sftp") != 0) { + return PR_DECLINED(cmd); + } + + key = "mod_xfer.retr-path"; path = pr_table_get(cmd->notes, key, NULL); if (path != NULL) { - char *real_path; + /* In addition, for SFTP sessions, we modify session.xfer.path as well, + * for mod_xfer's benefit in TransferLog entries. + */ + session.xfer.path = pstrdup(session.xfer.p, path); + } - if (*path == '/') { - const char *base_path; + return PR_DECLINED(cmd); +} - base_path = vroot_path_get_base(cmd->tmp_pool, NULL); - real_path = pdircat(cmd->pool, base_path, path, NULL); - vroot_path_clean(real_path); +MODRET vroot_log_retr(cmd_rec *cmd) { + const char *key; - } else { - real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH); - } + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } - pr_table_set(cmd->notes, key, real_path, 0); + key = "mod_xfer.retr-path"; + (void) vroot_cmd_fixup_path(cmd, key, FALSE); + return PR_DECLINED(cmd); +} + +MODRET vroot_pre_scp_stor(cmd_rec *cmd) { + const char *key, *proto, *real_path; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + /* As a PRE_CMD handler, we only run for SCP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "scp") != 0) { + return PR_DECLINED(cmd); + } + + /* Unlike SFTP sessions, mod_sftp does NOT set these cmd->notes for SCP + * sessions before doing the PRE_CMD dispatching. So we do it ourselves, + * pre-emptively, before using our other machinery. + */ + key = "mod_xfer.store-path"; + (void) pr_table_add(cmd->notes, key, pstrdup(cmd->pool, cmd->arg), 0); + + real_path = vroot_cmd_fixup_path(cmd, key, TRUE); + if (real_path != NULL) { + /* In addition, for SCP sessions, we modify cmd->arg as well, for + * mod_sftp's benefit. + */ + cmd->arg = (char *) real_path; } return PR_DECLINED(cmd); } +MODRET vroot_pre_sftp_stor(cmd_rec *cmd) { + const char *key, *proto, *real_path; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + /* As a PRE_CMD handler, we only run for SFTP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "sftp") != 0) { + return PR_DECLINED(cmd); + } + + key = "mod_xfer.store-path"; + real_path = vroot_cmd_fixup_path(cmd, key, TRUE); + if (real_path != NULL) { + /* In addition, for SFTP sessions, we modify cmd->arg as well, for + * mod_sftp's benefit. + */ + cmd->arg = (char *) real_path; + } + + return PR_DECLINED(cmd); +} + +MODRET vroot_post_sftp_stor(cmd_rec *cmd) { + const char *key, *path, *proto; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + /* As a POST_CMD handler, we only run for SFTP sessions. */ + proto = pr_session_get_protocol(0); + if (strcmp(proto, "sftp") != 0) { + return PR_DECLINED(cmd); + } + + key = "mod_xfer.store-path"; + path = pr_table_get(cmd->notes, key, NULL); + if (path != NULL) { + /* In addition, for SFTP sessions, we modify session.xfer.path as well, + * for mod_xfer's benefit in TransferLog entries. + */ + session.xfer.path = pstrdup(session.xfer.p, path); + } + + return PR_DECLINED(cmd); +} + +MODRET vroot_log_stor(cmd_rec *cmd) { + const char *key; + + if (vroot_engine == FALSE || + session.chroot_path == NULL) { + return PR_DECLINED(cmd); + } + + key = "mod_xfer.store-path"; + (void) vroot_cmd_fixup_path(cmd, key, FALSE); + return PR_DECLINED(cmd); +} + MODRET vroot_pre_mkd(cmd_rec *cmd) { if (vroot_engine == FALSE || session.chroot_path == NULL) { @@ -527,12 +695,13 @@ static cmdtable vroot_cmdtab[] = { /* These command handlers are for manipulating cmd->notes, to get * paths properly logged. * - * Ideally these would be LOG_CMD/LOG_CMD_ERR phase handlers. HOWEVER, - * we need to transform things before the cmd is dispatched to mod_log, - * and mod_log uses a C_ANY handler for logging. And when dispatching, - * C_ANY handlers are run before named handlers. This means that using - * LOG_CMD/LOG_CMD_ERR handlers would be run AFTER mod_log's handler, - * even though we appear BEFORE mod_log in the module load order. + * Ideally these POST_CMD handlers would be LOG_CMD/LOG_CMD_ERR phase + * handlers. HOWEVER, we need to transform things before the cmd is + * dispatched to mod_log, and mod_log uses a C_ANY handler for logging. + * And when dispatching, C_ANY handlers are run before named handlers. + * This means that using * LOG_CMD/LOG_CMD_ERR handlers would be run AFTER + * mod_log's handler, even though we appear BEFORE mod_log in the module + * load order. * * Thus to do the transformation, we actually use CMD/POST_CMD_ERR phase * handlers here. The reason to use CMD, rather than POST_CMD, is the @@ -549,6 +718,22 @@ static cmdtable vroot_cmdtab[] = { { CMD, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE, CL_WRITE }, { POST_CMD_ERR, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE }, + /* To make this more complicated, we DO actually want these handlers to + * run as PRE_CMD handlers, but only for mod_sftp sessions. Why? The + * mod_sftp module does not use the normal CMD handlers; it handles + * dispatching on its own. And we do still want mod_vroot to fix up + * the paths properly for SFTP/SCP sessions, too. + */ + { PRE_CMD, C_APPE, G_NONE, vroot_pre_sftp_stor, FALSE, FALSE, CL_WRITE }, + { POST_CMD, C_APPE, G_NONE, vroot_post_sftp_stor, FALSE, FALSE }, + { PRE_CMD, C_RETR, G_NONE, vroot_pre_sftp_retr, FALSE, FALSE, CL_READ }, + { POST_CMD, C_RETR, G_NONE, vroot_post_sftp_retr, FALSE, FALSE }, + { PRE_CMD, C_STOR, G_NONE, vroot_pre_sftp_stor, FALSE, FALSE, CL_WRITE }, + { POST_CMD, C_STOR, G_NONE, vroot_post_sftp_stor, FALSE, FALSE }, + + { PRE_CMD, C_RETR, G_NONE, vroot_pre_scp_retr, FALSE, FALSE, CL_READ }, + { PRE_CMD, C_STOR, G_NONE, vroot_pre_scp_stor, FALSE, FALSE, CL_WRITE }, + { 0, NULL } }; diff --git a/mod_vroot.h.in b/mod_vroot.h.in index 6f05c1f..266af0e 100644 --- a/mod_vroot.h.in +++ b/mod_vroot.h.in @@ -27,7 +27,7 @@ #include "conf.h" -#define MOD_VROOT_VERSION "mod_vroot/0.9.8" +#define MOD_VROOT_VERSION "mod_vroot/0.9.9" /* Make sure the version of proftpd is as necessary. */ #if PROFTPD_VERSION_NUMBER < 0x0001030602 diff --git a/t/lib/ProFTPD/Tests/Modules/mod_vroot.pm b/t/lib/ProFTPD/Tests/Modules/mod_vroot.pm index 00f81f2..4396455 100644 --- a/t/lib/ProFTPD/Tests/Modules/mod_vroot.pm +++ b/t/lib/ProFTPD/Tests/Modules/mod_vroot.pm @@ -10313,7 +10313,6 @@ sub vroot_log_extlog_stor { auth_group_write($auth_group_file, $group, $gid, $user); my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); - my $ext_log = File::Spec->rel2abs("$tmpdir/custom.log"); my $config = { @@ -10321,7 +10320,7 @@ sub vroot_log_extlog_stor { ScoreboardFile => $scoreboard_file, SystemLog => $log_file, TraceLog => $log_file, - Trace => 'fsio:10', + Trace => 'fsio:10 jot:20 vroot:20', AuthUserFile => $auth_user_file, AuthGroupFile => $auth_group_file, diff --git a/t/lib/ProFTPD/Tests/Modules/mod_vroot/sftp.pm b/t/lib/ProFTPD/Tests/Modules/mod_vroot/sftp.pm index b818537..03c99fc 100644 --- a/t/lib/ProFTPD/Tests/Modules/mod_vroot/sftp.pm +++ b/t/lib/ProFTPD/Tests/Modules/mod_vroot/sftp.pm @@ -99,16 +99,31 @@ my $TESTS = { test_class => [qw(bug forking mod_sftp sftp)], }, + vroot_sftp_log_extlog_stor => { + order => ++$order, + test_class => [qw(bug forking mod_sftp sftp)], + }, + vroot_sftp_log_xferlog_stor => { order => ++$order, test_class => [qw(bug forking mod_sftp sftp)], }, + vroot_scp_log_extlog_retr => { + order => ++$order, + test_class => [qw(bug forking mod_sftp scp)], + }, + vroot_scp_log_xferlog_retr => { order => ++$order, test_class => [qw(bug forking mod_sftp scp)], }, + vroot_scp_log_extlog_stor => { + order => ++$order, + test_class => [qw(bug forking mod_sftp scp)], + }, + vroot_scp_log_xferlog_stor => { order => ++$order, test_class => [qw(bug forking mod_sftp scp)], @@ -2650,12 +2665,12 @@ sub vroot_sftp_log_extlog_retr { ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, - Trace => 'command:20 event:20 extlog:20 fsio:10 jot:20 sftp:20 scp:20', + Trace => 'command:20 event:20 extlog:20 fsio:10 jot:20 sftp:20 scp:20 vroot:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, - LogFormat => 'custom "%m: %f"', + LogFormat => 'custom "%f"', ExtendedLog => "$ext_log READ custom", IfModules => { @@ -2760,27 +2775,22 @@ sub vroot_sftp_log_extlog_retr { eval { if (open(my $fh, "< $ext_log")) { - my $ok = 0; - - while (my $line = <$fh>) { - chomp($line); - - if ($ENV{TEST_VERBOSE}) { - print STDERR "$line\n"; - } + my $line = <$fh>; + close($fh); + chomp($line); - if ($line =~ /^RETR/) { - $ok = 1; - last; - } + if ($ENV{TEST_VERBOSE}) { + print STDERR "$line\n"; } - close($fh); - - unless ($ok) { - die("Did not see expected ExtendedLog line with RETR"); + if ($^O eq 'darwin') { + # MacOSX-specific hack, due to how it handles tmp files + $test_file = ('/private' . $test_file); } + $self->assert($test_file eq $line, + test_msg("Expected '$test_file', got '$line'")); + } else { die("Can't read $ext_log: $!"); } @@ -2983,6 +2993,153 @@ sub vroot_sftp_log_xferlog_retr { test_cleanup($setup->{log_file}, $ex); } +sub vroot_sftp_log_extlog_stor { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'vroot'); + + my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); + my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); + my $ext_log = File::Spec->rel2abs("$tmpdir/ext.log"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'fsio:10 jot:20 sftp:20 scp:20 vroot:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + LogFormat => 'custom "%f"', + ExtendedLog => "$ext_log WRITE custom", + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPHostKey $dsa_host_key", + ], + + 'mod_vroot.c' => { + VRootEngine => 'on', + VRootLog => $setup->{log_file}, + DefaultRoot => '~', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + require Net::SSH2; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + my $ssh2 = Net::SSH2->new(); + + sleep(1); + + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + my $sftp = $ssh2->sftp(); + unless ($sftp) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str"); + } + + my $fh = $sftp->open('test.txt', O_WRONLY|O_CREAT, 0644); + unless ($fh) { + my ($err_code, $err_name) = $sftp->error(); + die("Can't open test.txt: [$err_name] ($err_code)"); + } + + my $buf = "Hello, World!\n"; + print $fh $buf; + + # To issue the FXP_CLOSE, we have to explicit destroy the filehandle + $fh = undef; + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $ext_log")) { + my $line = <$fh>; + close($fh); + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($^O eq 'darwin') { + # MacOSX-specific hack, due to how it handles tmp files + $test_file = ('/private' . $test_file); + } + + $self->assert($test_file eq $line, + test_msg("Expected '$test_file', got '$line'")); + + } else { + die("Can't read $ext_log: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + sub vroot_sftp_log_xferlog_stor { my $self = shift; my $tmpdir = $self->{tmpdir}; @@ -2999,7 +3156,7 @@ sub vroot_sftp_log_xferlog_stor { ScoreboardFile => $setup->{scoreboard_file}, SystemLog => $setup->{log_file}, TraceLog => $setup->{log_file}, - Trace => 'fsio:10 sftp:20 scp:20', + Trace => 'fsio:10 jot:20 sftp:20 scp:20 vroot:20', AuthUserFile => $setup->{auth_user_file}, AuthGroupFile => $setup->{auth_group_file}, @@ -3158,6 +3315,150 @@ sub vroot_sftp_log_xferlog_stor { test_cleanup($setup->{log_file}, $ex); } +sub vroot_scp_log_extlog_retr { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'vroot'); + + my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); + my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); + if (open(my $fh, "> $test_file")) { + print $fh "Hello, World!\n"; + unless (close($fh)) { + die("Can't write $test_file: $!"); + } + + } else { + die("Can't open $test_file: $!"); + } + + my $ext_log = File::Spec->rel2abs("$tmpdir/ext.log"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'fsio:10 sftp:20 scp:20 vroot:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + LogFormat => 'custom "%f"', + ExtendedLog => "$ext_log READ custom", + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPHostKey $dsa_host_key", + ], + + 'mod_vroot.c' => { + VRootEngine => 'on', + VRootLog => $setup->{log_file}, + DefaultRoot => '~', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + require Net::SSH2; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + my $ssh2 = Net::SSH2->new(); + + sleep(1); + + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->scp_get('test.txt', '/dev/null')) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't download 'test.txt' from server: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $ext_log")) { + my $line = <$fh>; + chomp($line); + close($fh); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "$line\n"; + } + + if ($^O eq 'darwin') { + # MacOSX-specific hack, due to how it handles tmp files + $test_file = ('/private' . $test_file); + } + + $self->assert($test_file eq $line, + test_msg("Expected '$test_file', got '$line'")); + + } else { + die("Can't read $ext_log: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + sub vroot_scp_log_xferlog_retr { my $self = shift; my $tmpdir = $self->{tmpdir}; @@ -3330,6 +3631,140 @@ sub vroot_scp_log_xferlog_retr { test_cleanup($setup->{log_file}, $ex); } +sub vroot_scp_log_extlog_stor { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'vroot'); + + my $rsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_rsa_key"); + my $dsa_host_key = File::Spec->rel2abs("$ENV{PROFTPD_TEST_DIR}/t/etc/modules/mod_sftp/ssh_host_dsa_key"); + + my $test_file = File::Spec->rel2abs("$tmpdir/test.txt"); + my $ext_log = File::Spec->rel2abs("$tmpdir/ext.log"); + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'fsio:10 sftp:20 scp:20 vroot:20', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + + LogFormat => 'custom "%f"', + ExtendedLog => "$ext_log WRITE custom", + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPHostKey $dsa_host_key", + ], + + 'mod_vroot.c' => { + VRootEngine => 'on', + VRootLog => $setup->{log_file}, + DefaultRoot => '~', + }, + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + my $ex; + require Net::SSH2; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + my $ssh2 = Net::SSH2->new(); + + sleep(1); + + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->scp_put($setup->{config_file}, 'test.txt')) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't upload $setup->{config_file} to server: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $ext_log")) { + my $line = <$fh>; + chomp($line); + close($fh); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "$line\n"; + } + + if ($^O eq 'darwin') { + # MacOSX-specific hack, due to how it handles tmp files + $test_file = ('/private' . $test_file); + } + + $self->assert($test_file eq $line, + test_msg("Expected '$test_file', got '$line'")); + + } else { + die("Can't read $ext_log: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + sub vroot_scp_log_xferlog_stor { my $self = shift; my $tmpdir = $self->{tmpdir};