X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=t%2Fhttpd-corner.t;h=8a0337c2b5f38c34d910dead87118d379a0acefd;hb=5bde05b66f084c7c9dcf7389079aab6925aef328;hp=366e56cb5d3f04b2f00b5e9ee3e6ae983a6d55f0;hpb=4b644ab063e1390ec09dd85e3c9a019ad86682e2;p=public-inbox.git diff --git a/t/httpd-corner.t b/t/httpd-corner.t index 366e56cb..8a0337c2 100644 --- a/t/httpd-corner.t +++ b/t/httpd-corner.t @@ -5,9 +5,10 @@ use strict; use warnings; use Test::More; +use Time::HiRes qw(gettimeofday tv_interval); -foreach my $mod (qw(Plack::Util Plack::Request Plack::Builder Danga::Socket - HTTP::Parser::XS HTTP::Date HTTP::Status)) { +foreach my $mod (qw(Plack::Util Plack::Builder Danga::Socket + HTTP::Date HTTP::Status)) { eval "require $mod"; plan skip_all => "$mod missing for httpd-corner.t" if $@; } @@ -16,9 +17,13 @@ use Digest::SHA qw(sha1_hex); use File::Temp qw/tempdir/; use Cwd qw/getcwd/; use IO::Socket; +use IO::Socket::UNIX; use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD :seek); use Socket qw(SO_KEEPALIVE IPPROTO_TCP TCP_NODELAY); -my $tmpdir = tempdir(CLEANUP => 1); +use POSIX qw(dup2 mkfifo :sys_wait_h); +my $tmpdir = tempdir('httpd-corner-XXXXXX', TMPDIR => 1, CLEANUP => 1); +my $fifo = "$tmpdir/fifo"; +ok(defined mkfifo($fifo, 0777), 'created FIFO'); my $err = "$tmpdir/stderr.log"; my $out = "$tmpdir/stdout.log"; my $httpd = 'blib/script/public-inbox-httpd'; @@ -31,29 +36,121 @@ my %opts = ( Listen => 1024, ); my $sock = IO::Socket::INET->new(%opts); +my $upath = "$tmpdir/s"; +my $unix = IO::Socket::UNIX->new( + Listen => 1024, + Type => SOCK_STREAM, + Local => $upath +); +ok($unix, 'UNIX socket created'); my $pid; END { kill 'TERM', $pid if defined $pid }; -{ - ok($sock, 'sock created'); +my $spawn_httpd = sub { + my (@args) = @_; $! = 0; my $fl = fcntl($sock, F_GETFD, 0); ok(! $!, 'no error from fcntl(F_GETFD)'); is($fl, FD_CLOEXEC, 'cloexec set by default (Perl behavior)'); $pid = fork; if ($pid == 0) { - use POSIX qw(dup2); # pretend to be systemd - fcntl($sock, F_SETFD, $fl &= ~FD_CLOEXEC); dup2(fileno($sock), 3) or die "dup2 failed: $!\n"; + dup2(fileno($unix), 4) or die "dup2 failed: $!\n"; + my $t = IO::Handle->new_from_fd(3, 'r'); + $t->fcntl(F_SETFD, 0); + my $u = IO::Handle->new_from_fd(4, 'r'); + $u->fcntl(F_SETFD, 0); $ENV{LISTEN_PID} = $$; - $ENV{LISTEN_FDS} = 1; - exec $httpd, '-W0', "--stdout=$out", "--stderr=$err", $psgi; + $ENV{LISTEN_FDS} = 2; + exec $httpd, @args, "--stdout=$out", "--stderr=$err", $psgi; die "FAIL: $!\n"; } ok(defined $pid, 'forked httpd process successfully'); +}; + +{ + ok($sock, 'sock created'); $! = 0; - fcntl($sock, F_SETFD, $fl |= FD_CLOEXEC); - ok(! $!, 'no error from fcntl(F_SETFD)'); + my $fl = fcntl($sock, F_GETFD, 0); + ok(! $!, 'no error from fcntl(F_GETFD)'); + is($fl, FD_CLOEXEC, 'cloexec set by default (Perl behavior)'); + $spawn_httpd->('-W0'); +} + +{ + my $conn = conn_for($sock, 'streaming callback'); + $conn->write("GET /callback HTTP/1.0\r\n\r\n"); + ok($conn->read(my $buf, 8192), 'read response'); + my ($head, $body) = split(/\r\n\r\n/, $buf); + is($body, "hello world\n", 'callback body matches expected'); +} + +{ + my $conn = conn_for($sock, 'getline-die'); + $conn->write("GET /getline-die HTTP/1.1\r\nHost: example.com\r\n\r\n"); + ok($conn->read(my $buf, 8192), 'read some response'); + like($buf, qr!HTTP/1\.1 200\b[^\r]*\r\n!, 'got some sort of header'); + is($conn->read(my $nil, 8192), 0, 'read EOF'); + $conn = undef; + my $after = capture($err); + is(scalar(grep(/GETLINE FAIL/, @$after)), 1, 'failure logged'); + is(scalar(grep(/CLOSE FAIL/, @$after)), 1, 'body->close not called'); +} + +{ + my $conn = conn_for($sock, 'close-die'); + $conn->write("GET /close-die HTTP/1.1\r\nHost: example.com\r\n\r\n"); + ok($conn->read(my $buf, 8192), 'read some response'); + like($buf, qr!HTTP/1\.1 200\b[^\r]*\r\n!, 'got some sort of header'); + is($conn->read(my $nil, 8192), 0, 'read EOF'); + $conn = undef; + my $after = capture($err); + is(scalar(grep(/GETLINE FAIL/, @$after)), 0, 'getline not failed'); + is(scalar(grep(/CLOSE FAIL/, @$after)), 1, 'body->close not called'); +} + +{ + my $conn = conn_for($sock, 'excessive header'); + $SIG{PIPE} = 'IGNORE'; + $conn->write("GET /callback HTTP/1.0\r\n"); + foreach my $i (1..500000) { + last unless $conn->write("X-xxxxxJunk-$i: omg\r\n"); + } + ok(!$conn->write("\r\n"), 'broken request'); + ok($conn->read(my $buf, 8192), 'read response'); + my ($head, $body) = split(/\r\n\r\n/, $buf); + like($head, qr/\b400\b/, 'got 400 response'); +} + +{ + my $conn = conn_for($sock, 'excessive body Content-Length'); + $SIG{PIPE} = 'IGNORE'; + my $n = (10 * 1024 * 1024) + 1; + $conn->write("PUT /sha1 HTTP/1.0\r\nContent-Length: $n\r\n\r\n"); + ok($conn->read(my $buf, 8192), 'read response'); + my ($head, $body) = split(/\r\n\r\n/, $buf); + like($head, qr/\b413\b/, 'got 413 response'); +} + +{ + my $conn = conn_for($sock, 'excessive body chunked'); + $SIG{PIPE} = 'IGNORE'; + my $n = (10 * 1024 * 1024) + 1; + $conn->write("PUT /sha1 HTTP/1.1\r\nTransfer-Encoding: chunked\r\n"); + $conn->write("\r\n".sprintf("%x\r\n", $n)); + ok($conn->read(my $buf, 8192), 'read response'); + my ($head, $body) = split(/\r\n\r\n/, $buf); + like($head, qr/\b413\b/, 'got 413 response'); +} + +# Unix domain sockets +{ + my $u = IO::Socket::UNIX->new(Type => SOCK_STREAM, Peer => $upath); + ok($u, 'unix socket connected'); + $u->write("GET /host-port HTTP/1.0\r\n\r\n"); + $u->read(my $buf, 4096); + like($buf, qr!\r\n\r\n127\.0\.0\.1:0\z!, + 'set REMOTE_ADDR and REMOTE_PORT for Unix socket'); } sub conn_for { @@ -69,6 +166,68 @@ sub conn_for { return $conn; } +{ + my $conn = conn_for($sock, 'host-port'); + $conn->write("GET /host-port HTTP/1.0\r\n\r\n"); + $conn->read(my $buf, 4096); + my ($head, $body) = split(/\r\n\r\n/, $buf); + my ($addr, $port) = split(/:/, $body); + is($addr, $conn->sockhost, 'host matches addr'); + is($port, $conn->sockport, 'port matches'); +} + +# graceful termination +{ + my $conn = conn_for($sock, 'graceful termination via slow header'); + $conn->write("GET /slow-header HTTP/1.0\r\n" . + "X-Check-Fifo: $fifo\r\n\r\n"); + open my $f, '>', $fifo or die "open $fifo: $!\n"; + $f->autoflush(1); + ok(print($f "hello\n"), 'wrote something to fifo'); + my $kpid = $pid; + $pid = undef; + is(kill('TERM', $kpid), 1, 'started graceful shutdown'); + ok(print($f "world\n"), 'wrote else to fifo'); + close $f or die "close fifo: $!\n"; + $conn->read(my $buf, 8192); + my ($head, $body) = split(/\r\n\r\n/, $buf, 2); + like($head, qr!\AHTTP/1\.[01] 200 OK!, 'got 200 for slow-header'); + is($body, "hello\nworld\n", 'read expected body'); + is(waitpid($kpid, 0), $kpid, 'reaped httpd'); + is($?, 0, 'no error'); + $spawn_httpd->('-W0'); +} + +{ + my $conn = conn_for($sock, 'graceful termination via slow-body'); + $conn->write("GET /slow-body HTTP/1.0\r\n" . + "X-Check-Fifo: $fifo\r\n\r\n"); + open my $f, '>', $fifo or die "open $fifo: $!\n"; + $f->autoflush(1); + my $buf; + $conn->sysread($buf, 8192); + like($buf, qr!\AHTTP/1\.[01] 200 OK!, 'got 200 for slow-body'); + like($buf, qr!\r\n\r\n!, 'finished HTTP response header'); + + foreach my $c ('a'..'c') { + $c .= "\n"; + ok(print($f $c), 'wrote line to fifo'); + $conn->sysread($buf, 8192); + is($buf, $c, 'got trickle for reading'); + } + my $kpid = $pid; + $pid = undef; + is(kill('TERM', $kpid), 1, 'started graceful shutdown'); + ok(print($f "world\n"), 'wrote else to fifo'); + close $f or die "close fifo: $!\n"; + $conn->sysread($buf, 8192); + is($buf, "world\n", 'read expected body'); + is($conn->sysread($buf, 8192), 0, 'got EOF from server'); + is(waitpid($kpid, 0), $kpid, 'reaped httpd'); + is($?, 0, 'no error'); + $spawn_httpd->('-W0'); +} + sub delay { select(undef, undef, undef, shift || rand(0.02)) } my $str = 'abcdefghijklmnopqrstuvwxyz'; @@ -84,7 +243,6 @@ my $check_self = sub { SKIP: { use POSIX qw(dup2); - use IO::File; my $have_curl = 0; foreach my $p (split(':', $ENV{PATH})) { -x "$p/curl" or next; @@ -96,7 +254,7 @@ SKIP: { my $url = 'http://' . $sock->sockhost . ':' . $sock->sockport . '/sha1'; my ($r, $w); pipe($r, $w) or die "pipe: $!"; - my $tout = IO::File->new_tmpfile or die "new_tmpfile: $!"; + open(my $tout, '+>', undef) or die "open temporary file: $!"; my $pid = fork; defined $pid or die "fork: $!"; my @cmd = (qw(curl --tcp-nodelay --no-buffer -T- -HExpect: -sS), $url); @@ -140,6 +298,40 @@ SKIP: { } } +{ + my $conn = conn_for($sock, 'no TCP_CORK on empty body'); + $conn->write("GET /empty HTTP/1.1\r\nHost:example.com\r\n\r\n"); + my $buf = ''; + my $t0 = [ gettimeofday ]; + until ($buf =~ /\r\n\r\n/s) { + $conn->sysread($buf, 4096, length($buf)); + } + my $elapsed = tv_interval($t0, [ gettimeofday ]); + ok($elapsed < 0.190, 'no 200ms TCP cork delay on empty body'); +} + +{ + my $conn = conn_for($sock, 'graceful termination during slow request'); + $conn->write("PUT /sha1 HTTP/1.0\r\n"); + delay(); + $conn->write("Content-Length: $len\r\n"); + delay(); + $conn->write("\r\n"); + my $kpid = $pid; + $pid = undef; + is(kill('TERM', $kpid), 1, 'started graceful shutdown'); + delay(); + my $n = 0; + foreach my $c ('a'..'z') { + $n += $conn->write($c); + } + is($n, $len, 'wrote alphabet'); + $check_self->($conn); + is(waitpid($kpid, 0), $kpid, 'reaped httpd'); + is($?, 0, 'no error'); + $spawn_httpd->('-W0'); +} + # various DoS attacks against the chunk parser: { local $SIG{PIPE} = 'IGNORE'; @@ -320,4 +512,13 @@ SKIP: { done_testing(); +sub capture { + my ($f) = @_; + open my $fh, '+<', $f or die "failed to open $f: $!\n"; + local $/ = "\n"; + my @r = <$fh>; + truncate($fh, 0) or die "truncate failed on $f: $!\n"; + \@r +} + 1;