From d06a3beab47a57e30b35ef343ec0b8ea529c8394 Mon Sep 17 00:00:00 2001 From: brectanus Date: Fri, 30 May 2008 00:13:50 +0000 Subject: [PATCH] More tested regression tests. Cleaned up script. --- apache2/t/regression/action/10-logging.t | 248 ++++++++++++++++++ .../t/regression/config/10-audit-directives.t | 10 +- .../t/regression/config/10-misc-directives.t | 92 ++++++- apache2/t/regression/config/20-chroot.t | 35 +++ apache2/t/regression/rule/00-basics.t | 24 ++ apache2/t/regression/rule/00-inheritance.t | 4 + apache2/t/regression/rule/20-exceptions.t | 129 +++++++++ .../regression/server_root/conf/httpd.conf.in | 21 +- apache2/t/run-regression-tests.pl.in | 196 ++++++++++---- 9 files changed, 677 insertions(+), 82 deletions(-) create mode 100644 apache2/t/regression/action/10-logging.t create mode 100644 apache2/t/regression/config/20-chroot.t create mode 100644 apache2/t/regression/rule/00-basics.t create mode 100644 apache2/t/regression/rule/00-inheritance.t create mode 100644 apache2/t/regression/rule/20-exceptions.t diff --git a/apache2/t/regression/action/10-logging.t b/apache2/t/regression/action/10-logging.t new file mode 100644 index 00000000..c94c14ca --- /dev/null +++ b/apache2/t/regression/action/10-logging.t @@ -0,0 +1,248 @@ +### Logging tests + +# log/nolog +{ + type => "action", + comment => "log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# auditlog/noauditlog +{ + type => "action", + comment => "auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# All log/nolog auditlog/noauditlog combos +{ + type => "action", + comment => "log,auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,auditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "log,noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,log,noauditlog" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,auditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,auditlog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + # ENH: No message, but should have data. Is this intended? + audit => [ qr/-H--\s+Stopwatch: /s, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "nolog,noauditlog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,nolog,noauditlog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + audit => [ qr/Message: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "auditlog,nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,auditlog,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,log", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,log" + ), + match_log => { + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "action", + comment => "noauditlog,nolog", + conf => qq( + SecRuleEngine On + SecAuditEngine RelevantOnly + SecAuditLog "$ENV{AUDIT_LOG}" + SecAction "phase:1,pass,noauditlog,nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + diff --git a/apache2/t/regression/config/10-audit-directives.t b/apache2/t/regression/config/10-audit-directives.t index 6769a6a9..8a9d1663 100644 --- a/apache2/t/regression/config/10-audit-directives.t +++ b/apache2/t/regression/config/10-audit-directives.t @@ -132,18 +132,12 @@ } # Verify concurrent log contents - $LOG{$id}{fd} = new FileHandle($alogdatafn, O_RDONLY); - $LOG{$id}{fd}->blocking(0); - $LOG{$id}{buf} = ""; - my $alogdata = match_log($id, qr/^--[^-]+-A--.*$id.*-Z--$/s, 1); - if (defined $alogdata) { - $LOG{$id}{fd}->close(); - delete $LOG{$id}; + if (defined match_file($alogdatafn, qr/^--[^-]+-A--.*$id.*-Z--$/s)) { return 0; } # Error - dbg("LOGDATA: \"$alogdata\""); + dbg("LOGDATA: \"$FILE{$alogdatafn}{buf}\""); die "Audit log data did not match.\n"; }, match_response => { diff --git a/apache2/t/regression/config/10-misc-directives.t b/apache2/t/regression/config/10-misc-directives.t index 1ceae18a..2d20e033 100644 --- a/apache2/t/regression/config/10-misc-directives.t +++ b/apache2/t/regression/config/10-misc-directives.t @@ -2,13 +2,31 @@ ### TODO: # SecTmpDir -# SecUploadDir # SecUploadKeepFiles # SecWebAppId -# SecDataDir # SecChrootDir # SecGuardianLog +# SecDefaultAction +{ + type => "config", + comment => "SecDefaultAction", + conf => qq( + SecRuleEngine on + SecDefaultAction "phase:1,deny,status:500" + SecRule REQUEST_URI "test.txt" + ), + match_log => { + error => [ qr/ModSecurity: Access denied with code 500 \(phase 1\)/, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + # SecServerSignature { type => "config", @@ -28,22 +46,78 @@ ), }, -# SecDefaultAction +# SecDataDir { type => "config", - comment => "SecServerSignature On", + comment => "SecDataDir", conf => qq( - SecRuleEngine on - SecDefaultAction "phase:1,deny,status:500" - SecRule REQUEST_URI "test.txt" + SecRuleEngine On + SecDataDir "$ENV{DATA_DIR}" + SecAction initcol:ip=%{REMOTE_ADDR},setvar:ip.dummy=1,pass ), match_log => { - error => [ qr/ModSecurity: Access denied with code 500 \(phase 1\)/, 1 ], + error => [ qr/ModSecurity: Warning. Unconditional match in SecAction\./, 1 ], + }, + match_file => { + "$ENV{DATA_DIR}/ip.pag" => qr/\x00\x06dummy\x00\x00\x021\x00/, }, match_response => { - status => qr/^500$/, + status => qr/^200$/, }, request => new HTTP::Request( GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", ), }, + +# SecTmpDir/SecUploadDir/SecUploadKeepFiles +{ + type => "config", + comment => "SecTmpDir/SecUploadDir/SecUploadKeepFiles", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecTmpDir "$ENV{TEMP_DIR}" + SecUploadKeepFiles On + SecUploadDir "$ENV{UPLOAD_DIR}" + ), + test => sub { + # Get the filename and make sure the file exists + my $fn = match_log(debug => qr/Moved file from .* to ".*"\./, 5); + die "Failed to determine uploaded filename\n" unless (defined $fn); + + $fn =~ s/Moved file from .* to "(.*)"\..*/$1/; + die "File does not exist: $fn\n" unless (-e $fn); + + # Check the contents of the file + return 0 if (match_file($fn, qr/^TESTFILE$/m)); + + msg("Failed to match contents of uploaded file: $fn"); + return 1; + }, + match_log => { + debug => [ qr/Created temporary file: $ENV{TEMP_DIR}/, 1 ], + -debug => [ qr/Failed to /, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------19813181771830765643996187206", + ], + q(-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="upload-file"; filename="test" +Content-Type: application/octet-stream + +TESTFILE +-----------------------------19813181771830765643996187206 +Content-Disposition: form-data; name="file" + +Upload File +-----------------------------19813181771830765643996187206--), + ), +}, + diff --git a/apache2/t/regression/config/20-chroot.t b/apache2/t/regression/config/20-chroot.t new file mode 100644 index 00000000..2147d2b6 --- /dev/null +++ b/apache2/t/regression/config/20-chroot.t @@ -0,0 +1,35 @@ +### SecChroot tests +# TODO: Will not work as we need root access + +#{ +# type => "config", +# comment => "SecChroot", +# httpd_opts => qw( +# -DCHROOT +# ), +# conf => qq( +# # These will be in the chroot +# PidFile /logs/httpd.pid +# ScoreBoardFile /logs/httpd.scoreboard +# User nobody +# Group nogroup +# +# SecAuditEngine On +# SecDebugLog $ENV{DEBUG_LOG} +# SecDebugLogLevel 9 +# SecAuditLog $ENV{AUDIT_LOG} +# SecAuditLogStorageDir "/logs/audit" +# SecAuditLogType Concurrent +# SecChrootDir "$ENV{TEST_SERVER_ROOT}" +# ), +# match_log => { +# debug => [ qr/./, 1 ], +# audit => [ qr/./, 1 ], +# }, +# match_response => { +# status => qr/^200$/, +# }, +# request => new HTTP::Request( +# GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", +# ), +#}, diff --git a/apache2/t/regression/rule/00-basics.t b/apache2/t/regression/rule/00-basics.t new file mode 100644 index 00000000..3c7e8c04 --- /dev/null +++ b/apache2/t/regression/rule/00-basics.t @@ -0,0 +1,24 @@ +### Tests for basic rule components + +# SecAction +{ + type => "config", + comment => "SecAction (override default)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 4 + SecAction "nolog" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Warning\. Unconditional match in SecAction\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/rule/00-inheritance.t b/apache2/t/regression/rule/00-inheritance.t new file mode 100644 index 00000000..c661c28e --- /dev/null +++ b/apache2/t/regression/rule/00-inheritance.t @@ -0,0 +1,4 @@ +### Tests for rule inheritance + +### TODO: +# SecRuleInheritance diff --git a/apache2/t/regression/rule/20-exceptions.t b/apache2/t/regression/rule/20-exceptions.t new file mode 100644 index 00000000..43ccf215 --- /dev/null +++ b/apache2/t/regression/rule/20-exceptions.t @@ -0,0 +1,129 @@ +### Tests for rule exceptions + +# SecRuleRemoveByMsg + +# SecRuleRemoveById +{ + type => "config", + comment => "SecRuleRemoveById (single)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRuleRemoveById 1 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (multiple)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRuleRemoveById 1 2 3 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRuleRemoveById 1-3 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, +{ + type => "config", + comment => "SecRuleRemoveById (multiple + range)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:2" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:3" + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:4" + SecRuleRemoveById 1 2-4 + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, + +# SecRuleRemoveByMsg +{ + type => "config", + comment => "SecRuleRemoveByMsg", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_URI "test" "phase:1,deny,status:500,id:1,msg:'testing rule'" + SecRuleRemoveByMsg "testing rule" + ), + match_log => { + -error => [ qr/ModSecurity: /, 1 ], + -audit => [ qr/./, 1 ], + debug => [ qr/Starting phase REQUEST_HEADERS\..*This phase consists of 0 rule.*Starting phase RESPONSE_HEADERS\./s, 1 ], + -debug => [ qr/Access denied/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + GET => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + ), +}, diff --git a/apache2/t/regression/server_root/conf/httpd.conf.in b/apache2/t/regression/server_root/conf/httpd.conf.in index 5be7a1da..7d45cdbf 100644 --- a/apache2/t/regression/server_root/conf/httpd.conf.in +++ b/apache2/t/regression/server_root/conf/httpd.conf.in @@ -1,8 +1,10 @@ ### Base configuration for starting Apache httpd -# File locations -PidFile @MSC_REGRESSION_LOGS_DIR@/httpd.pid -ScoreBoardFile @MSC_REGRESSION_LOGS_DIR@/httpd.scoreboard + + # File locations + PidFile @MSC_REGRESSION_LOGS_DIR@/httpd.pid + ScoreBoardFile @MSC_REGRESSION_LOGS_DIR@/httpd.scoreboard + LoadModule proxy_module modules/mod_proxy.so @@ -24,9 +26,10 @@ ServerName localhost LogLevel debug ErrorLog @MSC_REGRESSION_LOGS_DIR@/error.log -DocumentRoot @MSC_REGRESSION_DOCROOT_DIR@ - - Options Indexes FollowSymLinks - AllowOverride None - - + + DocumentRoot @MSC_REGRESSION_DOCROOT_DIR@ + + Options Indexes FollowSymLinks + AllowOverride None + + diff --git a/apache2/t/run-regression-tests.pl.in b/apache2/t/run-regression-tests.pl.in index 3db0c92d..3a6b6674 100755 --- a/apache2/t/run-regression-tests.pl.in +++ b/apache2/t/run-regression-tests.pl.in @@ -26,14 +26,17 @@ my $SCRIPT = basename($0); my $SCRIPT_DIR = File::Spec->rel2abs(dirname($0)); my $REG_DIR = "$SCRIPT_DIR/regression"; my $SROOT_DIR = "$REG_DIR/server_root"; +my $DATA_DIR = "$SROOT_DIR/data"; +my $TEMP_DIR = "$SROOT_DIR/tmp"; +my $UPLOAD_DIR = "$SROOT_DIR/upload"; my $CONF_DIR = "$SROOT_DIR/conf"; -my $LOGS_DIR = "$SROOT_DIR/logs"; -my $PID_FILE = "$LOGS_DIR/httpd.pid"; +my $FILES_DIR = "$SROOT_DIR/logs"; +my $PID_FILE = "$FILES_DIR/httpd.pid"; my $HTTPD = q(@APXS_HTTPD@); my $PASSED = 0; my $TOTAL = 0; my %C = (); -my %LOG = (); +my %FILE = (); my $UA_NAME = "ModSecurity Regression Tests/1.2.3"; my $UA = LWP::UserAgent->new; $UA->agent($UA_NAME); @@ -89,9 +92,9 @@ else { usage("Invalid Apache startup script: $HTTPD\n") unless (-e $HTTPD); ### Defaults -$opt{A} = "$LOGS_DIR/modsec_audit.log" unless (defined $opt{A}); -$opt{D} = "$LOGS_DIR/modsec_debug.log" unless (defined $opt{D}); -$opt{E} = "$LOGS_DIR/error.log" unless (defined $opt{E}); +$opt{A} = "$FILES_DIR/modsec_audit.log" unless (defined $opt{A}); +$opt{D} = "$FILES_DIR/modsec_debug.log" unless (defined $opt{D}); +$opt{E} = "$FILES_DIR/error.log" unless (defined $opt{E}); $opt{C} = "$CONF_DIR/httpd.conf" unless (defined $opt{C}); $opt{H} = "$SROOT_DIR/htdocs" unless (defined $opt{H}); $opt{p} = 8088 unless (defined $opt{p}); @@ -107,8 +110,11 @@ unless (defined $opt{S}) { SERVER_PORT => $opt{p}, SERVER_NAME => "localhost", TEST_SERVER_ROOT => $SROOT_DIR, + DATA_DIR => $DATA_DIR, + TEMP_DIR => $TEMP_DIR, + UPLOAD_DIR => $UPLOAD_DIR, CONF_DIR => $CONF_DIR, - LOGS_DIR => $LOGS_DIR, + LOGS_DIR => $FILES_DIR, SCRIPT_DIR => $SCRIPT_DIR, REGRESSION_DIR => $REG_DIR, DIST_ROOT => File::Spec->rel2abs(dirname("$SCRIPT_DIR/../../..")), @@ -120,7 +126,7 @@ unless (defined $opt{S}) { USER_AGENT => $UA_NAME, ); -dbg("OPTIONS: ", \%opt); +#dbg("OPTIONS: ", \%opt); if (-e "$PID_FILE") { msg("Shutting down previous instance: $PID_FILE"); @@ -193,10 +199,10 @@ sub runfile { print CONF (ref $t{conf} eq "CODE" ? eval { &{$t{conf}} } : $t{conf}); msg("$@") if ($@); close CONF; - $httpd_up = httpd_start("Include $conf_fn") ? 0 : 1; + $httpd_up = httpd_start(\%t, "Include $conf_fn") ? 0 : 1; } else { - $httpd_up = httpd_start() ? 0 : 1; + $httpd_up = httpd_start(\%t) ? 0 : 1; } # Run any prerun setup @@ -229,7 +235,7 @@ sub runfile { } elsif (!$neg and !defined $match) { $rc = 1; - msg("response $mtype no match: $m"); + msg("response $mtype failed to match: $m"); dbg($resp); last; } @@ -239,13 +245,13 @@ sub runfile { # Run any arbitrary perl tests if ($rc == 0 and exists $t{test} and defined $t{test}) { - dbg("Executing perl test(s)..."); + #dbg("Executing perl test(s)..."); $rc = eval { &{$t{test}} }; if (! defined $rc) { msg("Error running test: $@"); $rc = -1; } - dbg("Perl tests returned: $rc"); + #dbg("Perl tests returned: $rc"); } # Search for all log matches @@ -257,14 +263,36 @@ sub runfile { if ($neg and defined $match) { $rc = 1; msg("$mtype log matched: $m->[0]"); - dbg("$LOG{$mtype}{buf}"); - + msg("Log: $FILE{$mtype}{fn}"); + dbg(escape("$FILE{$mtype}{buf}")); last; } elsif (!$neg and !defined $match) { $rc = 1; - msg("$mtype log no match: $m->[0]"); - dbg("$LOG{$mtype}{buf}"); + msg("$mtype log failed to match: $m->[0]"); + msg("Log: $FILE{$mtype}{fn}"); + dbg(escape("$FILE{$mtype}{buf}")); + last; + } + } + } + + # Search for all file matches + if ($rc == 0 and exists $t{match_file} and defined $t{match_file}) { + for my $key (keys %{ $t{match_file} || {}}) { + my($neg,$fn) = ($key =~ m/^(-?)(.*)$/); + my $m = $t{match_file}{$key}; + my $match = match_file($fn, $m); + if ($neg and defined $match) { + $rc = 1; + msg("$fn file matched: $m"); + dbg(escape("$FILE{$fn}{buf}")); + last; + } + elsif (!$neg and !defined $match) { + $rc = 1; + msg("$fn file failed match: $m"); + dbg(escape("$FILE{$fn}{buf}")); last; } } @@ -285,7 +313,7 @@ sub runfile { msg(sprintf("%s) %s%s: %s%s", $id, $t{type}, (exists($t{comment}) ? " - $t{comment}" : ""), ($rc ? "failed" : "passed"), ((defined($out) && $out ne "")? " ($out)" : ""))); if ($httpd_up) { - $httpd_up = httpd_stop() ? 0 : 1; + $httpd_up = httpd_stop(\%t) ? 0 : 1; } } @@ -362,7 +390,7 @@ sub match_response { sub match_log { my($name, $re, $timeout) = @_; my $t0 = gettimeofday; - my($fh,$rbuf) = ($LOG{$name}{fd}, \$LOG{$name}{buf}); + my($fh,$rbuf) = ($FILE{$name}{fd}, \$FILE{$name}{buf}); my $n = length($$rbuf); msg("Warning: Empty regular expression.") if (!defined $re or $re eq ""); @@ -380,10 +408,29 @@ sub match_log { return; } +sub match_file { + my($neg,$fn) = ($_[0] =~ m/^(-?)(.*)$/); + unless (exists $FILE{$fn}) { + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDWR|O_CREAT); + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{buf} = ""; + } + return match_log($_[0], $_[1]); # timeout makes no sense +} + +sub quote_shell { + my($s) = @_; + return $s unless ($s =~ m|[^\w!%+,\-./:@^]|); + $s =~ s/(['\\])/\\$1/g; + return "'$s'"; +} + sub escape { my @new = (); for my $c (split(//, $_[0])) { - push @new, ((ord($c) >= 0x20 and ord($c) <= 0x7e) ? $c : sprintf("\\x%02x", ord($c))); + my $oc = ord($c); + push @new, ((($oc >= 0x20 and $oc <= 0x7e) or $oc == 0x0a or $oc == 0x0d) ? $c : sprintf("\\x%02x", ord($c))); } join('', @new); } @@ -432,7 +479,8 @@ sub done { } sub httpd_start { - httpd_reset_logs(); + my $t = shift; + httpd_reset_fd($t); my @p = ( $HTTPD, -d => $opt{S}, @@ -441,9 +489,6 @@ sub httpd_start { -k => "start", ); -# dbg("EXEC: ", \@p); -# dbg("Httpd start"); - my $httpd_out; my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); my $out = join("\\n", split(/\n/, <$httpd_out>)); @@ -453,7 +498,7 @@ sub httpd_start { my $rc = $?; if ( WIFEXITED($rc) ) { $rc = WEXITSTATUS($rc); -# dbg("Httpd start returned with $rc."); + dbg("Httpd start returned with $rc.") if ($rc); } elsif( WIFSIGNALED($rc) ) { msg("Httpd start failed with signal " . WTERMSIG($rc) . "."); @@ -465,20 +510,24 @@ sub httpd_start { } if (defined $out and $out ne "") { + dbg(join(" ", map { quote_shell($_) } @p)); msg("Httpd start failed with error messages:\n$out"); return -1 } # Look for startup msg unless (defined match_log("error", qr/resuming normal operations/, 10)) { - quit(1, "Httpd server failed to start."); + dbg(join(" ", map { quote_shell($_) } @p)); + dbg(match_log("error", qr/(^.*ModSecurity: .*)/sm, 10)); + msg("Httpd server failed to start."); + return -1; } return $rc; } sub httpd_stop { - httpd_reset_logs(); + my $t = shift; my @p = ( $HTTPD, -d => $opt{S}, @@ -487,9 +536,6 @@ sub httpd_stop { -k => "stop", ); - #dbg("EXEC: ", \@p); -# dbg("Httpd stop"); - my $httpd_out; my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); my $out = join("\\n", split(/\n/, <$httpd_out>)); @@ -504,7 +550,7 @@ sub httpd_stop { my $rc = $?; if ( WIFEXITED($rc) ) { $rc = WEXITSTATUS($rc); -# dbg("Httpd stop returned with $rc."); + dbg("Httpd stop returned with $rc.") if ($rc); } elsif( WIFSIGNALED($rc) ) { msg("Httpd stop failed with signal " . WTERMSIG($rc) . "."); @@ -517,14 +563,17 @@ sub httpd_stop { # Look for startup msg unless (defined match_log("error", qr/caught SIG[A-Z]+, shutting down/, 10)) { - quit(1, "Httpd server failed to shutdown."); + dbg(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to shutdown."); + return -1; } return $rc; } sub httpd_reload { - httpd_reset_logs(); + my $t = shift; + httpd_reset_fd($t); my @p = ( $HTTPD, -d => $opt{S}, @@ -533,9 +582,6 @@ sub httpd_reload { -k => "graceful", ); -# dbg("EXEC: ", join(' ', map { "'$_'" } @p)); -# dbg("Httpd reload"); - my $httpd_out; my $httpd_pid = open3(undef, $httpd_out, undef, @p) or quit(1); my $out = join("\\n", split(/\n/, <$httpd_out>)); @@ -550,7 +596,7 @@ sub httpd_reload { my $rc = $?; if ( WIFEXITED($rc) ) { $rc = WEXITSTATUS($rc); -# dbg("Httpd reload returned with $rc."); + dbg("Httpd reload returned with $rc.") if ($rc); } elsif( WIFSIGNALED($rc) ) { msg("Httpd reload failed with signal " . WTERMSIG($rc) . "."); @@ -563,36 +609,74 @@ sub httpd_reload { # Look for startup msg unless (defined match_log("error", qr/resuming normal operations/, 10)) { - quit(1, "Httpd server failed to reload."); + dbg(join(" ", map { quote_shell($_) } @p)); + msg("Httpd server failed to reload."); + return -1; } return $rc; } -sub httpd_reset_logs { - # Error - if (!defined $LOG{error}{fd}) { - $LOG{error}{fd} = new FileHandle($opt{E}, O_RDWR|O_CREAT) +sub httpd_reset_fd { + my($t) = @_; + + # Cleanup + for my $key (keys %FILE) { + if (exists $FILE{$key}{fd} and defined $FILE{$key}{fd}) { + $FILE{$key}{fd}->close(); + } + delete $FILE{$key}; } - $LOG{error}{fd}->blocking(0); - $LOG{error}{fd}->sysseek(0, 2); - $LOG{error}{buf} = ""; + + # Error + $FILE{error}{fn} = $opt{E}; + $FILE{error}{fd} = new FileHandle($opt{E}, O_RDWR|O_CREAT); + $FILE{error}{fd}->blocking(0); + $FILE{error}{fd}->sysseek(0, 2); + $FILE{error}{buf} = ""; # Audit - if (!defined $LOG{audit}{fd}) { - $LOG{audit}{fd} = new FileHandle($opt{A}, O_RDWR|O_CREAT); - } - $LOG{audit}{fd}->blocking(0); - $LOG{audit}{fd}->sysseek(0, 2); - $LOG{audit}{buf} = ""; + $FILE{audit}{fn} = $opt{A}; + $FILE{audit}{fd} = new FileHandle($opt{A}, O_RDWR|O_CREAT); + $FILE{audit}{fd}->blocking(0); + $FILE{audit}{fd}->sysseek(0, 2); + $FILE{audit}{buf} = ""; # Debug - if (!defined $LOG{debug}{fd}) { - $LOG{debug}{fd} = new FileHandle($opt{D}, O_RDWR|O_CREAT); + $FILE{debug}{fn} = $opt{D}; + $FILE{debug}{fd} = new FileHandle($opt{D}, O_RDWR|O_CREAT); + $FILE{debug}{fd}->blocking(0); + $FILE{debug}{fd}->sysseek(0, 2); + $FILE{debug}{buf} = ""; + + # Any extras listed in "match_log" + if ($t and exists $t->{match_log}) { + for my $k (keys %{ $t->{match_log} || {} }) { + my($neg,$fn) = ($k =~ m/^(-?)(.*)$/); + next if (!$fn or exists $FILE{$fn}); + #dbg("Opening additional log: $fn"); + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDWR|O_CREAT); + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{fd}->sysseek(0, 2); + $FILE{$fn}{buf} = ""; + + } + } + + # Any extras listed in "match_file" + if ($t and exists $t->{match_file}) { + for my $k (keys %{ $t->{match_file} || {} }) { + my($neg,$fn) = ($k =~ m/^(-?)(.*)$/); + next if (!$fn or exists $FILE{$fn}); + #dbg("Opening file: $fn"); + $FILE{$fn}{fn} = $fn; + $FILE{$fn}{fd} = new FileHandle($fn, O_RDWR|O_CREAT); + $FILE{$fn}{fd}->blocking(0); + $FILE{$fn}{buf} = ""; + + } } - $LOG{debug}{fd}->blocking(0); - $LOG{debug}{fd}->sysseek(0, 2); - $LOG{debug}{buf} = ""; } sub encode_chunked {