X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=t%2Fhttpd-corner.t;h=8a0337c2b5f38c34d910dead87118d379a0acefd;hb=956ede734d7c2e8d0a3003c6e2d554114586643e;hp=40692086ad7cb8218e6d9d735f85843e76e89f67;hpb=aeaa38f620cf880a073b3a37463f0c577188df46;p=public-inbox.git diff --git a/t/httpd-corner.t b/t/httpd-corner.t index 40692086..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,10 +17,11 @@ 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); use POSIX qw(dup2 mkfifo :sys_wait_h); -my $tmpdir = tempdir(CLEANUP => 1); +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"; @@ -34,20 +36,32 @@ 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 }; 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) { # 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; + $ENV{LISTEN_FDS} = 2; exec $httpd, @args, "--stdout=$out", "--stderr=$err", $psgi; die "FAIL: $!\n"; } @@ -63,6 +77,82 @@ my $spawn_httpd = sub { $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 { my ($sock, $msg) = @_; my $conn = IO::Socket::INET->new( @@ -76,6 +166,16 @@ 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'); @@ -143,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; @@ -155,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); @@ -199,6 +298,18 @@ 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"); @@ -401,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;