use strict; use warnings FATAL => 'all'; use Apache::Test; use Apache::TestRequest; use Apache::TestUtil qw(t_write_file t_start_error_log_watch t_finish_error_log_watch); use File::Spec; # test ap_expr Apache::TestRequest::user_agent(keep_alive => 1); my $file_foo = Apache::Test::vars('serverroot') . '/htdocs/expr/index.html'; my $dir_foo = Apache::Test::vars('serverroot') . '/htdocs/expr'; my $file_notexist = Apache::Test::vars('serverroot') . '/htdocs/expr/none'; my $file_zero = Apache::Test::vars('serverroot') . '/htdocs/expr/zero'; my $url_foo = '/apache/'; my $url_notexist = '/apache/expr/none'; my @test_cases = ( [ 'true' => 1 ], [ 'false' => 0 ], [ 'foo' => undef ], # integer comparison [ '1 -eq 01' => 1 ], [ '1 -eq 2' => 0 ], [ '1 -ne 2' => 1 ], [ '1 -ne 1' => 0 ], [ '1 -lt 02' => 1 ], [ '1 -lt 1' => 0 ], [ '1 -le 2' => 1 ], [ '1 -le 1' => 1 ], [ '2 -gt 1' => 1 ], [ '1 -gt 1' => 0 ], [ '2 -ge 1' => 1 ], [ '1 -ge 1' => 1 ], [ '1 -gt -1' => 1 ], # string comparison [ q{'aa' == 'aa'} => 1 ], [ q{'aa' == 'b'} => 0 ], [ q{'aa' = 'aa'} => 1 ], [ q{'aa' = 'b'} => 0 ], [ q{'aa' != 'b'} => 1 ], [ q{'aa' != 'aa'} => 0 ], [ q{'aa' < 'b'} => 1 ], [ q{'aa' < 'aa'} => 0 ], [ q{'aa' <= 'b'} => 1 ], [ q{'aa' <= 'aa'} => 1 ], [ q{'b' > 'aa'} => 1 ], [ q{'aa' > 'aa'} => 0 ], [ q{'b' >= 'aa'} => 1 ], [ q{'aa' >= 'aa'} => 1 ], # string operations/whitespace handling [ q{'a' . 'b' . 'c' = 'abc'} => 1 ], [ q{'a' .'b'. 'c' = 'abc'} => 1 ], [ q{ 'a' .'b'. 'c'='abc' } => 1 ], [ q{'a1c' = 'a'. 1. 'c'} => 1 ], [ q{req('foo') . 'bar' = 'bar'} => 1 ], [ q[%{req:foo} . 'bar' = 'bar'] => 1 ], [ q['x'.%{req:foo} . 'bar' = 'xbar'] => 1 ], [ q[%{req:User-Agent} . 'bar' != 'bar'] => 1 ], [ q['%{req:User-Agent}' . 'bar' != 'bar'] => 1 ], [ q['%{TIME}' . 'bar' != 'bar'] => 1 ], [ q[%{TIME} != ''] => 1 ], # string lists [ q{'a' -in { 'b', 'a' } } => 1 ], [ q{'a' -in { 'b', 'c' } } => 0 ], # regexps [ q[ 'abc' =~ /bc/ ] => 1 ], [ q[ 'abc' =~ /BC/i ] => 1 ], [ q[ 'abc' !~ m!bc! ] => 0 ], [ q[ 'abc' !~ m!BC!i ] => 0 ], [ q[ $0 == '' ] => 1 ], [ q[ $1 == '' ] => 1 ], [ q[ $9 == '' ] => 1 ], [ q[ '$0' == '' ] => 1 ], [ q[ 'abc' =~ /(bc)/ && $0 == 'bc' ] => 1 ], [ q[ 'abc' =~ /(bc)/ && $1 == 'bc' ] => 1 ], [ q[ 'abc' =~ /b(.)/ && $1 == 'c' ] => 1 ], # $0 .. $9 are only populated if there are capturing parens [ q[ 'abc' =~ /bc/ && $0 == '' ] => 1 ], [ q[ 'abc' =~ /(bc)/ && 'xy' =~ /x/ && $0 == 'bc' ] => 1 ], # Attempt to blow up when more matches are present than 'typical' $0 .. $9 [ q[ 'abcdefghijklm' =~ /(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)/ && $2 == 'c' ] => 1 ], # variables [ q[%{TIME_YEAR} =~ /^\d{4}$/] => 1 ], [ q[%{TIME_YEAR} =~ /^\d{3}$/] => 0 ], [ q[%{TIME_MON} -gt 0 && %{TIME_MON} -le 12 ] => 1 ], [ q[%{TIME_DAY} -gt 0 && %{TIME_DAY} -le 31 ] => 1 ], [ q[%{TIME_HOUR} -ge 0 && %{TIME_HOUR} -lt 24 ] => 1 ], [ q[%{TIME_MIN} -ge 0 && %{TIME_MIN} -lt 60 ] => 1 ], [ q[%{TIME_SEC} -ge 0 && %{TIME_SEC} -lt 60 ] => 1 ], [ q[%{TIME} =~ /^\d{14}$/] => 1 ], [ q[%{API_VERSION} -gt 20101001 ] => 1 ], [ q[%{REQUEST_METHOD} == 'GET' ] => 1 ], [ q['x%{REQUEST_METHOD}' == 'xGET' ] => 1 ], [ q['x%{REQUEST_METHOD}y' == 'xGETy' ] => 1 ], [ q[%{REQUEST_SCHEME} == 'http' ] => 1 ], [ q[%{HTTPS} == 'off' ] => 1 ], [ q[%{REQUEST_URI} == '/apache/expr/index.html' ] => 1 ], # request headers [ q[%{req:referer} = 'SomeReferer' ] => 1 ], [ q[req('Referer') = 'SomeReferer' ] => 1 ], [ q[http('Referer') = 'SomeReferer' ] => 1 ], [ q[%{HTTP_REFERER} = 'SomeReferer' ] => 1 ], [ q[req('User-Agent') = 'SomeAgent' ] => 1 ], [ q[%{HTTP_USER_AGENT} = 'SomeAgent' ] => 1 ], [ q[req('SomeHeader') = 'SomeValue' ] => 1 ], [ q[req('SomeHeader2') = 'SomeValue' ] => 0 ], # functions [ q[toupper('abC12d') = 'ABC12D' ] => 1 ], [ q[tolower('abC12d') = 'abc12d' ] => 1 ], [ q[escape('?') = '%3f' ] => 1 ], [ q[unescape('%3f') = '?' ] => 1 ], [ q[toupper(escape('?')) = '%3F' ] => 1 ], [ q[tolower(toupper(escape('?'))) = '%3f' ] => 1 ], [ q[%{toupper:%{escape:?}} = '%3F' ] => 1 ], [ q[file('] . $file_foo . q[') = 'foo\n' ] => 1 ], # unary operators [ q[-n ''] => 0 ], [ q[-z ''] => 1 ], [ q[-n '1'] => 1 ], [ q[-z '1'] => 0 ], # IP match [ q[-R 'abc'] => undef ], [ q[-R %{REMOTE_ADDR}] => undef ], [ q[-R '240.0.0.0'] => 0 ], [ q[-R '240.0.0.0/8'] => 0 ], [ q[-R 'ff::/8'] => 0 ], [ q[-R '127.0.0.1' || -R '::1'] => 1 ], [ q['127.0.0.1' -ipmatch 'abc'] => undef ], [ q['127.0.0.1' -ipmatch %{REMOTE_ADDR}] => undef ], [ q['127.0.0.1' -ipmatch '240.0.0.0'] => 0 ], [ q['127.0.0.1' -ipmatch '240.0.0.0/8'] => 0 ], [ q['127.0.0.1' -ipmatch 'ff::/8'] => 0 ], [ q['127.0.0.1' -ipmatch '127.0.0.0/8'] => 1 ], # fn/strmatch [ q['foo' -strmatch '*o'] => 1 ], [ q['fo/o' -strmatch 'f*'] => 1 ], [ q['foo' -strmatch 'F*'] => 0 ], [ q['foo' -strcmatch 'F*'] => 1 ], [ q['foo' -strmatch 'g*'] => 0 ], [ q['foo' -strcmatch 'g*'] => 0 ], [ q['a/b' -fnmatch 'a*'] => 0 ], [ q['a/b' -fnmatch 'a/*'] => 1 ], # error handling [ q['%{foo:User-Agent}' != 'bar'] => undef ], [ q[%{foo:User-Agent} != 'bar'] => undef ], [ q[foo('bar') = 'bar'] => undef ], [ q[%{FOO} != 'bar'] => undef ], [ q['bar' = bar] => undef ], ); # # Bool logic: # Test all combinations with 0 to 2 '||' or '&&' operators # my @bool_base = ( [ q[true] => 1 ], ); push @bool_base, ( [ q[-z ''] => 1 ], [ q[-n 'x'] => 1 ], [ q[false] => 0 ], [ q[-n ''] => 0 ], [ q[-z 'x'] => 0 ], ) if 0; # This produces an exessive number of tests for normal operation # negation function: perl's "!" returns undef for false, but we need 0 sub neg { return (shift) ? 0 : 1; } # also test combinations with '!' operator before each operand @bool_base = (@bool_base, map { ["!$_->[0]" => neg($_->[1]) ] } @bool_base); # now create the test cases my @bool_test_cases; foreach my $ex1 (@bool_base) { my ($e1, $r1) = @$ex1; push @bool_test_cases, [ $e1 => $r1 ]; foreach my $ex2 (@bool_base) { my ($e2, $r2) = @$ex2; push @bool_test_cases, [ "$e1 && $e2" => ($r1 && $r2) ]; push @bool_test_cases, [ "$e1 || $e2" => ($r1 || $r2) ]; foreach my $ex3 (@bool_base) { my ($e3, $r3) = @$ex3; foreach my $op1 ("||", "&&") { foreach my $op2 ("||", "&&") { my $r = eval "$r1 $op1 $r2 $op2 $r3"; push @bool_test_cases, [ "$e1 $op1 $e2 $op2 $e3" => $r]; } } } } } push @test_cases, @bool_test_cases; # also test combinations with '!' operator before the whole expression push @test_cases, map { ["!($_->[0])" => neg($_->[1]) ] } @bool_test_cases; if (have_min_apache_version("2.3.13")) { push(@test_cases, ( # functions [ q[filesize('] . $file_foo . q[') = 4 ] => 1 ], [ q[filesize('] . $file_notexist . q[') = 0 ] => 1 ], [ q[filesize('] . $file_zero . q[') = 0 ] => 1 ], # unary operators [ qq[-d '$file_foo' ] => 0 ], [ qq[-e '$file_foo' ] => 1 ], [ qq[-f '$file_foo' ] => 1 ], [ qq[-s '$file_foo' ] => 1 ], [ qq[-d '$file_zero' ] => 0 ], [ qq[-e '$file_zero' ] => 1 ], [ qq[-f '$file_zero' ] => 1 ], [ qq[-s '$file_zero' ] => 0 ], [ qq[-d '$dir_foo' ] => 1 ], [ qq[-e '$dir_foo' ] => 1 ], [ qq[-f '$dir_foo' ] => 0 ], [ qq[-s '$dir_foo' ] => 0 ], [ qq[-d '$file_notexist' ] => 0 ], [ qq[-e '$file_notexist' ] => 0 ], [ qq[-f '$file_notexist' ] => 0 ], [ qq[-s '$file_notexist' ] => 0 ], [ qq[-F '$file_foo' ] => 1 ], [ qq[-F '$file_notexist' ] => 0 ], [ qq[-U '$url_foo' ] => 1 ], [ qq[-U '$url_notexist' ] => 0 ], )); } if (have_min_apache_version("2.4.5")) { push(@test_cases, ( [ qq[sha1('foo') = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' ] => 1 ], [ qq[md5('foo') = 'acbd18db4cc2f85cedef654fccc4a4d8' ] => 1 ], [ qq[base64('foo') = 'Zm9v' ] => 1 ], [ qq[unbase64('Zm9vMg==') = 'foo2' ] => 1 ], )); } if (have_min_apache_version("2.5")) { my $SAN_one = "email:, email:, " . "IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, " . "IP Address:192.168.169.170"; my $SAN_tuple = "'email:', 'email:', " . "'IP Address:127.0.0.1', 'IP Address:0:0:0:0:0:0:0:1', " . "'IP Address:192.168.169.170'"; my $SAN_list_one = "{ '$SAN_one' }"; my $SAN_list_tuple = "{ $SAN_tuple }"; my $SAN_split = '.*?IP Address:([^,]+)'; push(@test_cases, ( [ "join {'a', 'b', 'c'} == 'abc'" => 1 ], [ "join($SAN_list_tuple, ', ') == " . "'email:, email:, " . "IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, " . "IP Address:192.168.169.170'" => 1 ], [ "join($SAN_list_tuple, ', ') == join $SAN_list_one" => 1 ], [ "join(split(s/$SAN_split/\$1/, $SAN_list_tuple), ', ') == " . "'email:, email:, " . "127.0.0.1, 0:0:0:0:0:0:0:1, 192.168.169.170'" => 1 ], [ "join(split(s/$SAN_split/\$1/, $SAN_list_one), ', ') == " . "'127.0.0.1, 0:0:0:0:0:0:0:1, 192.168.169.170'" => 1 ], [ "'IP Address:192.168.169.170' -in $SAN_list_tuple" => 1 ], [ "'192.168.169.170' -in split s/$SAN_split/\$1/, $SAN_list_tuple" => 1 ], [ "'0:0:0:0:0:0:0:1' -in split s/$SAN_split/\$1/, $SAN_list_one" => 1 ], [ "%{REMOTE_ADDR} -in split s/$SAN_split/\$1/, $SAN_list_one" => 1 ], [ "'email:' -in split s/$SAN_split/\$1/, $SAN_list_tuple" => 1 ], [ "'email:' -in split s/$SAN_split/\$1/, $SAN_list_one" => 0 ], [ "'IP Address:%{REMOTE_ADDR}' -in split/, /, join $SAN_list_one" => 1 ], )); } plan tests => scalar(@test_cases) + 1, need need_lwp, need_module('mod_authz_core'), need_min_apache_version('2.3.9'); t_start_error_log_watch(); my %rc_map = ( 500 => 'parse error', 403 => 'true', 200 => 'false'); foreach my $t (@test_cases) { my ($expr, $expect) = @{$t}; write_htaccess($expr); my $response = GET('/apache/expr/index.html', 'SomeHeader' => 'SomeValue', 'User-Agent' => 'SomeAgent', 'Referer' => 'SomeReferer'); my $rc = $response->code; if (!defined $expect) { print qq{Should get parse error for "$expr", got $rc_map{$rc}\n}; ok($rc == 500); } elsif ($expect) { print qq{"$expr" should evaluate to true, got $rc_map{$rc}\n}; ok($rc == 403); } else { print qq{"$expr" should evaluate to false, got $rc_map{$rc}\n}; ok($rc == 200); } } my @loglines = t_finish_error_log_watch(); my @evalerrors = grep { /internal evaluation error/i } @loglines; my $num_errors = scalar @evalerrors; print "Error log should not have 'Internal evaluation error' entries, found $num_errors\n"; ok($num_errors == 0); exit 0; ### sub routines sub write_htaccess { my $expr = shift; my $file = File::Spec->catfile(Apache::Test::vars('serverroot'), 'htdocs', 'apache', 'expr', '.htaccess'); t_write_file($file, << "EOF" ); Require all denied EOF }