| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 | #!/usr/bin/env perl# This script is used to test Civetweb web serveruse IO::Socket;use File::Path;use Cwd;use strict;use warnings;#use diagnostics;sub on_windows { $^O =~ /win32/i; }my $port = 23456;my $pid = undef;my $num_requests;my $dir_separator = on_windows() ? '\\' : '/';my $copy_cmd = on_windows() ? 'copy' : 'cp';my $test_dir_uri = "test_dir";my $root = 'test';my $test_dir = $root . $dir_separator. $test_dir_uri;my $config = 'civetweb.conf';my $exe_ext = on_windows() ? '.exe' : '';my $civetweb_exe = '.' . $dir_separator . 'civetweb' . $exe_ext;my $embed_exe = '.' . $dir_separator . 'embed' . $exe_ext;my $unit_test_exe = '.' . $dir_separator . 'unit_test' . $exe_ext;my $exit_code = 0;my @files_to_delete = ('debug.log', 'access.log', $config, "$root/a/put.txt",  "$root/a+.txt", "$root/.htpasswd", "$root/binary_file", "$root/a",  "$root/myperl", $embed_exe, $unit_test_exe);END {  unlink @files_to_delete;  kill_spawned_child();  File::Path::rmtree($test_dir);  exit $exit_code;}sub fail {  print "FAILED: @_\n";  $exit_code = 1;  exit 1;}sub get_num_of_log_entries {  open FD, "access.log" or return 0;  my @lines = (<FD>);  close FD;  return scalar @lines;}# Send the request to the 127.0.0.1:$port and return the replysub req {  my ($request, $inc, $timeout) = @_;  my $sock = IO::Socket::INET->new(Proto => 6,    PeerAddr => '127.0.0.1', PeerPort => $port);  fail("Cannot connect to http://127.0.0.1:$port : $!") unless $sock;  $sock->autoflush(1);  foreach my $byte (split //, $request) {    last unless print $sock $byte;    select undef, undef, undef, .001 if length($request) < 256;  }  my ($out, $buf) = ('', '');  eval {    alarm $timeout if $timeout;    $out .= $buf while (sysread($sock, $buf, 1024) > 0);    alarm 0 if $timeout;  };  close $sock;  $num_requests += defined($inc) ? $inc : 1;  my $num_logs = get_num_of_log_entries();  unless ($num_requests == $num_logs) {    fail("Request has not been logged: [$request], output: [$out]");  }  return $out;}# Send the request. Compare with the expected reply. Fail if no matchsub o {  my ($request, $expected_reply, $message, $num_logs) = @_;  print "==> $message ... ";  my $reply = req($request, $num_logs);  if ($reply =~ /$expected_reply/s) {    print "OK\n";  } else {#fail("Requested: [$request]\nExpected: [$expected_reply], got: [$reply]");    fail("Expected: [$expected_reply], got: [$reply]");  }}# Spawn a server listening on specified portsub spawn {  my ($cmdline) = @_;  print 'Executing: ', @_, "\n";  if (on_windows()) {    my @args = split /\s+/, $cmdline;    my $executable = $args[0];    Win32::Spawn($executable, $cmdline, $pid);    die "Cannot spawn @_: $!" unless $pid;  } else {    unless ($pid = fork()) {      exec $cmdline;      die "cannot exec [$cmdline]: $!\n";    }  }  sleep 1;}sub write_file {  open FD, ">$_[0]" or fail "Cannot open $_[0]: $!";  binmode FD;  print FD $_[1];  close FD;}sub read_file {  open FD, $_[0] or fail "Cannot open $_[0]: $!";  my @lines = <FD>;  close FD;  return join '', @lines;}sub kill_spawned_child {  if (defined($pid)) {    kill(9, $pid);    waitpid($pid, 0);  }}####################################################### ENTRY POINTunlink @files_to_delete;$SIG{PIPE} = 'IGNORE';$SIG{ALRM} = sub { die "timeout\n" };#local $| =1;# Make sure we export only symbols that start with "mg_", and keep local# symbols static.if ($^O =~ /darwin|bsd|linux/) {  my $out = `(cc -c src/civetweb.c && nm src/civetweb.o) | grep ' T '`;  foreach (split /\n/, $out) {    /T\s+_?mg_.+/ or fail("Exported symbol $_")  }}if (scalar(@ARGV) > 0 and $ARGV[0] eq 'unit') {  do_unit_test();  exit 0;}# Make sure we load config file if no options are given.# Command line options override config files settingswrite_file($config, "access_log_file access.log\n" .           "listening_ports 127.0.0.1:12345\n");spawn("$civetweb_exe -listening_ports 127.0.0.1:$port");o("GET /test/hello.txt HTTP/1.0\n\n", 'HTTP/1.1 200 OK', 'Loading config file');unlink $config;kill_spawned_child();# Spawn the server on port $portmy $cmd = "$civetweb_exe ".  "-listening_ports 127.0.0.1:$port ".  "-access_log_file access.log ".  "-error_log_file debug.log ".  "-cgi_environment CGI_FOO=foo,CGI_BAR=bar,CGI_BAZ=baz " .  "-extra_mime_types .bar=foo/bar,.tar.gz=blah,.baz=foo " .  '-put_delete_auth_file test/passfile ' .  '-access_control_list -0.0.0.0/0,+127.0.0.1 ' .  "-document_root $root ".  "-hide_files_patterns **exploit.PL ".  "-enable_keep_alive yes ".  "-url_rewrite_patterns /aiased=/etc/,/ta=$test_dir";$cmd .= ' -cgi_interpreter perl' if on_windows();spawn($cmd);o("GET /hello.txt HTTP/1.1\nConnection: close\nRange: bytes=3-50\r\n\r\n",  'Content-Length: 15\s', 'Range past the file end');o("GET /hello.txt HTTP/1.1\n\n   GET /hello.txt HTTP/1.0\n\n",  'HTTP/1.1 200.+keep-alive.+HTTP/1.1 200.+close',  'Request pipelining', 2);my $x = 'x=' . 'A' x (200 * 1024);my $len = length($x);o("POST /env.cgi HTTP/1.0\r\nContent-Length: $len\r\n\r\n$x",  '^HTTP/1.1 200 OK', 'Long POST');# Try to overflow: Send very long requestreq('POST ' . '/..' x 100 . 'ABCD' x 3000 . "\n\n", 0); # don't log this oneo("GET /hello.txt HTTP/1.0\n\n", 'HTTP/1.1 200 OK', 'GET regular file');o("GET /hello.txt HTTP/1.0\nContent-Length: -2147483648\n\n",  'HTTP/1.1 200 OK', 'Negative content length');o("GET /hello.txt HTTP/1.0\n\n", 'Content-Length: 17\s',  'GET regular file Content-Length');o("GET /%68%65%6c%6c%6f%2e%74%78%74 HTTP/1.0\n\n",  'HTTP/1.1 200 OK', 'URL-decoding');# Break CGI reading after 1 second. We must get full output.# Since CGI script does sleep, we sleep as well and increase request count# manually.my $slow_cgi_reply;print "==> Slow CGI output ... ";fail('Slow CGI output forward reply=', $slow_cgi_reply) unless  ($slow_cgi_reply = req("GET /timeout.cgi HTTP/1.0\r\n\r\n", 0, 1)) =~ /Some data/s;print "OK\n";sleep 3;$num_requests++;# '+' in URI must not be URL-decoded to spacewrite_file("$root/a+.txt", '');o("GET /a+.txt HTTP/1.0\n\n", 'HTTP/1.1 200 OK', 'URL-decoding, + in URI');# Test HTTP version parsingo("GET / HTTPX/1.0\r\n\r\n", '^HTTP/1.1 500', 'Bad HTTP Version', 0);o("GET / HTTP/x.1\r\n\r\n", '^HTTP/1.1 505', 'Bad HTTP maj Version', 0);o("GET / HTTP/1.1z\r\n\r\n", '^HTTP/1.1 505', 'Bad HTTP min Version', 0);o("GET / HTTP/02.0\r\n\r\n", '^HTTP/1.1 505', 'HTTP Version >1.1', 0);# File with leading single doto("GET /.leading.dot.txt HTTP/1.0\n\n", 'abc123', 'Leading dot 1');o("GET /...leading.dot.txt HTTP/1.0\n\n", 'abc123', 'Leading dot 2');o("GET /../\\\\/.//...leading.dot.txt HTTP/1.0\n\n", 'abc123', 'Leading dot 3')  if on_windows();o("GET .. HTTP/1.0\n\n", '400 Bad Request', 'Leading dot 4', 0);mkdir $test_dir unless -d $test_dir;o("GET /$test_dir_uri/not_exist HTTP/1.0\n\n",  'HTTP/1.1 404', 'PATH_INFO loop problem');o("GET /$test_dir_uri HTTP/1.0\n\n", 'HTTP/1.1 301', 'Directory redirection');o("GET /$test_dir_uri/ HTTP/1.0\n\n", 'Modified', 'Directory listing');write_file("$test_dir/index.html", "tralala");o("GET /$test_dir_uri/ HTTP/1.0\n\n", 'tralala', 'Index substitution');o("GET / HTTP/1.0\n\n", 'embed.c', 'Directory listing - file name');o("GET /ta/ HTTP/1.0\n\n", 'Modified', 'Aliases');o("GET /not-exist HTTP/1.0\r\n\n", 'HTTP/1.1 404', 'Not existent file');mkdir $test_dir . $dir_separator . 'x';my $path = $test_dir . $dir_separator . 'x' . $dir_separator . 'index.cgi';write_file($path, read_file($root . $dir_separator . 'env.cgi'));chmod(0755, $path);o("GET /$test_dir_uri/x/ HTTP/1.0\n\n", "Content-Type: text/html\r\n\r\n",  'index.cgi execution');my $cwd = getcwd();o("GET /$test_dir_uri/x/ HTTP/1.0\n\n",  "SCRIPT_FILENAME=$cwd/test/test_dir/x/index.cgi", 'SCRIPT_FILENAME');o("GET /ta/x/ HTTP/1.0\n\n", "SCRIPT_NAME=/ta/x/index.cgi",  'Aliases SCRIPT_NAME');o("GET /hello.txt HTTP/1.1\nConnection: close\n\n", 'Connection: close',  'No keep-alive');$path = $test_dir . $dir_separator . 'x' . $dir_separator . 'a.cgi';system("ln -s `which perl` $root/myperl") == 0 or fail("Can't symlink perl");write_file($path, "#!../../myperl\n" .           "print \"Content-Type: text/plain\\n\\nhi\";");chmod(0755, $path);o("GET /$test_dir_uri/x/a.cgi HTTP/1.0\n\n", "hi", 'Relative CGI interp path');o("GET * HTTP/1.0\n\n", "^HTTP/1.1 404", '* URI');my $mime_types = {  html => 'text/html',  htm => 'text/html',  txt => 'text/plain',  unknown_extension => 'text/plain',  js => 'application/x-javascript',  css => 'text/css',  jpg => 'image/jpeg',  c => 'text/plain',  'tar.gz' => 'blah',  bar => 'foo/bar',  baz => 'foo',};foreach my $key (keys %$mime_types) {  my $filename = "_mime_file_test.$key";  write_file("$root/$filename", '');  o("GET /$filename HTTP/1.0\n\n",    "Content-Type: $mime_types->{$key}", ".$key mime type");  unlink "$root/$filename";}# Get binary file and check the integritymy $binary_file = 'binary_file';my $f2 = '';foreach (0..123456) { $f2 .= chr(int(rand() * 255)); }write_file("$root/$binary_file", $f2);my $f1 = req("GET /$binary_file HTTP/1.0\r\n\n");while ($f1 =~ /^.*\r\n/) { $f1 =~ s/^.*\r\n// }$f1 eq $f2 or fail("Integrity check for downloaded binary file");my $range_request = "GET /hello.txt HTTP/1.1\nConnection: close\n"."Range: bytes=3-5\r\n\r\n";o($range_request, '206 Partial Content', 'Range: 206 status code');o($range_request, 'Content-Length: 3\s', 'Range: Content-Length');o($range_request, 'Content-Range: bytes 3-5/17', 'Range: Content-Range');o($range_request, '\nple$', 'Range: body content');# Test directory sorting. Sleep between file creation for 1.1 seconds,# to make sure modification time are different.mkdir "$test_dir/sort";write_file("$test_dir/sort/11", 'xx');select undef, undef, undef, 1.1;write_file("$test_dir/sort/aa", 'xxxx');select undef, undef, undef, 1.1;write_file("$test_dir/sort/bb", 'xxx');select undef, undef, undef, 1.1;write_file("$test_dir/sort/22", 'x');o("GET /$test_dir_uri/sort/?n HTTP/1.0\n\n",  '200 OK.+>11<.+>22<.+>aa<.+>bb<',  'Directory listing (name, ascending)');o("GET /$test_dir_uri/sort/?nd HTTP/1.0\n\n",  '200 OK.+>bb<.+>aa<.+>22<.+>11<',  'Directory listing (name, descending)');o("GET /$test_dir_uri/sort/?s HTTP/1.0\n\n",  '200 OK.+>22<.+>11<.+>bb<.+>aa<',  'Directory listing (size, ascending)');o("GET /$test_dir_uri/sort/?sd HTTP/1.0\n\n",  '200 OK.+>aa<.+>bb<.+>11<.+>22<',  'Directory listing (size, descending)');o("GET /$test_dir_uri/sort/?d HTTP/1.0\n\n",  '200 OK.+>11<.+>aa<.+>bb<.+>22<',  'Directory listing (modification time, ascending)');o("GET /$test_dir_uri/sort/?dd HTTP/1.0\n\n",  '200 OK.+>22<.+>bb<.+>aa<.+>11<',  'Directory listing (modification time, descending)');unless (scalar(@ARGV) > 0 and $ARGV[0] eq "basic_tests") {  # Check that .htpasswd file existence trigger authorization  write_file("$root/.htpasswd", 'user with space, " and comma:mydomain.com:5deda12442309cbdcdffc6b2737a894f');  o("GET /hello.txt HTTP/1.1\n\n", '401 Unauthorized',    '.htpasswd - triggering auth on file request');  o("GET / HTTP/1.1\n\n", '401 Unauthorized',    '.htpasswd - triggering auth on directory request');  # Test various funky things in an authentication header.  o("GET /hello.txt HTTP/1.0\nAuthorization: Digest   eq== empty=\"\", empty2=, quoted=\"blah foo bar, baz\\\"\\\" more\\\"\", unterminatedquoted=\" doesn't stop\n\n",    '401 Unauthorized', 'weird auth values should not cause crashes');  my $auth_header = "Digest username=\"user with space, \\\" and comma\", ".    "realm=\"mydomain.com\", nonce=\"1291376417\", uri=\"/\",".    "response=\"e8dec0c2a1a0c8a7e9a97b4b5ea6a6e6\", qop=auth, nc=00000001, cnonce=\"1a49b53a47a66e82\"";  o("GET /hello.txt HTTP/1.0\nAuthorization: $auth_header\n\n", 'HTTP/1.1 200 OK', 'GET regular file with auth');  o("GET / HTTP/1.0\nAuthorization: $auth_header\n\n", '^(.(?!(.htpasswd)))*$',    '.htpasswd is hidden from the directory list');  o("GET / HTTP/1.0\nAuthorization: $auth_header\n\n", '^(.(?!(exploit.pl)))*$',    'hidden file is hidden from the directory list');  o("GET /.htpasswd HTTP/1.0\nAuthorization: $auth_header\n\n",    '^HTTP/1.1 404 ', '.htpasswd must not be shown');  o("GET /exploit.pl HTTP/1.0\nAuthorization: $auth_header\n\n",    '^HTTP/1.1 404', 'hidden files must not be shown');  unlink "$root/.htpasswd";  o("GET /dir%20with%20spaces/hello.cgi HTTP/1.0\n\r\n",      'HTTP/1.1 200 OK.+hello', 'CGI script with spaces in path');  o("GET /env.cgi HTTP/1.0\n\r\n", 'HTTP/1.1 200 OK', 'GET CGI file');  o("GET /bad2.cgi HTTP/1.0\n\n", "HTTP/1.1 123 Please pass me to the client\r",    'CGI Status code text');  o("GET /sh.cgi HTTP/1.0\n\r\n", 'shell script CGI',    'GET sh CGI file') unless on_windows();  o("GET /env.cgi?var=HELLO HTTP/1.0\n\n", 'QUERY_STRING=var=HELLO',    'QUERY_STRING wrong');  o("POST /env.cgi HTTP/1.0\r\nContent-Length: 9\r\n\r\nvar=HELLO",    'var=HELLO', 'CGI POST wrong');  o("POST /env.cgi HTTP/1.0\r\nContent-Length: 9\r\n\r\nvar=HELLO",    '\x0aCONTENT_LENGTH=9', 'Content-Length not being passed to CGI');  o("GET /env.cgi HTTP/1.0\nMy-HdR: abc\n\r\n",    'HTTP_MY_HDR=abc', 'HTTP_* env');  o("GET /env.cgi HTTP/1.0\n\r\nSOME_TRAILING_DATA_HERE",    'HTTP/1.1 200 OK', 'GET CGI with trailing data');  o("GET /env.cgi%20 HTTP/1.0\n\r\n",    'HTTP/1.1 404', 'CGI Win32 code disclosure (%20)');  o("GET /env.cgi%ff HTTP/1.0\n\r\n",    'HTTP/1.1 404', 'CGI Win32 code disclosure (%ff)');  o("GET /env.cgi%2e HTTP/1.0\n\r\n",    'HTTP/1.1 404', 'CGI Win32 code disclosure (%2e)');  o("GET /env.cgi%2b HTTP/1.0\n\r\n",    'HTTP/1.1 404', 'CGI Win32 code disclosure (%2b)');  o("GET /env.cgi HTTP/1.0\n\r\n", '\nHTTPS=off\n', 'CGI HTTPS');  o("GET /env.cgi HTTP/1.0\n\r\n", '\nCGI_FOO=foo\n', '-cgi_env 1');  o("GET /env.cgi HTTP/1.0\n\r\n", '\nCGI_BAR=bar\n', '-cgi_env 2');  o("GET /env.cgi HTTP/1.0\n\r\n", '\nCGI_BAZ=baz\n', '-cgi_env 3');  o("GET /env.cgi/a/b/98 HTTP/1.0\n\r\n", 'PATH_INFO=/a/b/98\n', 'PATH_INFO');  o("GET /env.cgi/a/b/9 HTTP/1.0\n\r\n", 'PATH_INFO=/a/b/9\n', 'PATH_INFO');  # Check that CGI's current directory is set to script's directory  my $copy_cmd = on_windows() ? 'copy' : 'cp';  system("$copy_cmd $root" . $dir_separator .  "env.cgi $test_dir" .    $dir_separator . 'env.cgi');  o("GET /$test_dir_uri/env.cgi HTTP/1.0\n\n",    "CURRENT_DIR=.*$root/$test_dir_uri", "CGI chdir()");  # SSI tests  o("GET /ssi1.shtml HTTP/1.0\n\n",    'ssi_begin.+CFLAGS.+ssi_end', 'SSI #include file=');  o("GET /ssi2.shtml HTTP/1.0\n\n",    'ssi_begin.+Unit test.+ssi_end', 'SSI #include virtual=');  my $ssi_exec = on_windows() ? 'ssi4.shtml' : 'ssi3.shtml';  o("GET /$ssi_exec HTTP/1.0\n\n",    'ssi_begin.+Makefile.+ssi_end', 'SSI #exec');  my $abs_path = on_windows() ? 'ssi6.shtml' : 'ssi5.shtml';  my $word = on_windows() ? 'boot loader' : 'root';  o("GET /$abs_path HTTP/1.0\n\n",    "ssi_begin.+$word.+ssi_end", 'SSI #include abspath');  o("GET /ssi7.shtml HTTP/1.0\n\n",    'ssi_begin.+Unit test.+ssi_end', 'SSI #include "..."');  o("GET /ssi8.shtml HTTP/1.0\n\n",    'ssi_begin.+CFLAGS.+ssi_end', 'SSI nested #includes');  # Manipulate the passwords file  my $path = 'test_htpasswd';  unlink $path;  system("$civetweb_exe -A $path a b c") == 0    or fail("Cannot add user in a passwd file");  system("$civetweb_exe -A $path a b c2") == 0    or fail("Cannot edit user in a passwd file");  my $content = read_file($path);  $content =~ /^b:a:\w+$/gs or fail("Bad content of the passwd file");  unlink $path;  do_PUT_test();  kill_spawned_child();  do_unit_test();}sub do_PUT_test {  # This only works because civetweb currently doesn't look at the nonce.  # It should really be rejected...  my $auth_header = "Authorization: Digest  username=guest, ".  "realm=mydomain.com, nonce=1145872809, uri=/put.txt, ".  "response=896327350763836180c61d87578037d9, qop=auth, ".  "nc=00000002, cnonce=53eddd3be4e26a98\n";  o("PUT /a/put.txt HTTP/1.0\nContent-Length: 7\n$auth_header\n1234567",    "HTTP/1.1 201 OK", 'PUT file, status 201');  fail("PUT content mismatch")  unless read_file("$root/a/put.txt") eq '1234567';  o("PUT /a/put.txt HTTP/1.0\nContent-Length: 4\n$auth_header\nabcd",    "HTTP/1.1 200 OK", 'PUT file, status 200');  fail("PUT content mismatch")  unless read_file("$root/a/put.txt") eq 'abcd';  o("PUT /a/put.txt HTTP/1.0\n$auth_header\nabcd",    "HTTP/1.1 411 Length Required", 'PUT 411 error');  o("PUT /a/put.txt HTTP/1.0\nExpect: blah\nContent-Length: 1\n".    "$auth_header\nabcd",    "HTTP/1.1 417 Expectation Failed", 'PUT 417 error');  o("PUT /a/put.txt HTTP/1.0\nExpect: 100-continue\nContent-Length: 4\n".    "$auth_header\nabcd",    "HTTP/1.1 100 Continue.+HTTP/1.1 200", 'PUT 100-Continue');}sub do_unit_test {  my $target = on_windows() ? 'wi' : 'un';  system("make $target") == 0 or fail("Unit test failed!");}print "SUCCESS! All tests passed.\n";
 |