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 {