]> Sergey Matveev's repositories - public-inbox.git/commitdiff
http: do not allow bad getline+close responses to kill us
authorEric Wong <e@80x24.org>
Thu, 4 Aug 2016 23:36:34 +0000 (23:36 +0000)
committerEric Wong <e@80x24.org>
Fri, 5 Aug 2016 02:26:26 +0000 (02:26 +0000)
PSGI applications (like our WWW :P) can fail unpredictability,
but lets try to avoid bringing the entire process down when this
happens.

lib/PublicInbox/HTTP.pm
t/httpd-corner.psgi
t/httpd-corner.t

index fa34b443f579b45c37a72b8678e64978062fe6a1..729d46fb20bcd3729043de518710bf50650cb342 100644 (file)
@@ -245,29 +245,49 @@ sub response_done ($$) {
        $self->write(sub { $alive ? next_request($self) : $self->close });
 }
 
+sub getline_cb ($$$) {
+       my ($self, $write, $close) = @_;
+       local $/ = \8192;
+       my $forward = $self->{forward};
+       # limit our own running time for fairness with other
+       # clients and to avoid buffering too much:
+       if ($forward) {
+               my $buf = eval { $forward->getline };
+               if (defined $buf) {
+                       $write->($buf); # may close in Danga::Socket::write
+                       unless ($self->{closed}) {
+                               my $next = $self->{pull};
+                               if ($self->{write_buf_size}) {
+                                       $self->write($next);
+                               } else {
+                                       PublicInbox::EvCleanup::asap($next);
+                               }
+                               return;
+                       }
+               } elsif ($@) {
+                       err($self, "response ->getline error: $@");
+                       $forward = undef;
+                       $self->close;
+               }
+       }
+
+       $self->{forward} = $self->{pull} = undef;
+       # avoid recursion
+       if ($forward) {
+               eval { $forward->close };
+               if ($@) {
+                       err($self, "response ->close error: $@");
+                       $self->close; # idempotent
+               }
+       }
+       $close->();
+}
+
 sub getline_response {
        my ($self, $body, $write, $close) = @_;
        $self->{forward} = $body;
        weaken($self);
-       my $pull = $self->{pull} = sub {
-               local $/ = \8192;
-               my $forward = $self->{forward};
-               # limit our own running time for fairness with other
-               # clients and to avoid buffering too much:
-               while ($forward && defined(my $buf = $forward->getline)) {
-                       $write->($buf);
-                       last if $self->{closed};
-                       if ($self->{write_buf_size}) {
-                               $self->write($self->{pull});
-                       } else {
-                               PublicInbox::EvCleanup::asap($self->{pull});
-                       }
-                       return;
-               }
-               $self->{forward} = $self->{pull} = undef;
-               $forward->close if $forward; # avoid recursion
-               $close->();
-       };
+       my $pull = $self->{pull} = sub { getline_cb($self, $write, $close) };
        $pull->();
 }
 
@@ -331,12 +351,15 @@ sub input_prepare {
 
 sub env_chunked { ($_[0]->{HTTP_TRANSFER_ENCODING} || '') =~ /\bchunked\b/i }
 
+sub err ($$) {
+       eval { $_[0]->{httpd}->{env}->{'psgi.errors'}->print($_[1]."\n") };
+}
+
 sub write_err {
        my ($self, $len) = @_;
-       my $err = $self->{httpd}->{env}->{'psgi.errors'};
        my $msg = $! || '(zero write)';
        $msg .= " ($len bytes remaining)" if defined $len;
-       $err->print("error buffering to input: $msg\n");
+       err($self, "error buffering to input: $msg");
        quit($self, 500);
 }
 
@@ -347,8 +370,7 @@ sub recv_err {
                $self->{input_left} = $len;
                return;
        }
-       my $err = $self->{httpd}->{env}->{'psgi.errors'};
-       $err->print("error reading for input: $! ($len bytes remaining)\n");
+       err($self, "error reading for input: $! ($len bytes remaining)");
        quit($self, 500);
 }
 
@@ -451,7 +473,10 @@ sub close {
        my $env = $self->{env};
        delete $env->{'psgix.io'} if $env; # prevent circular referernces
        $self->{pull} = $self->{forward} = $self->{env} = undef;
-       $forward->close if $forward;
+       if ($forward) {
+               eval { $forward->close };
+               err($self, "forward ->close error: $@") if $@;
+       }
        $self->SUPER::close(@_);
 }
 
index 222b9e01a058bd82cd102ba46a03957de92c4eef..ed1f92c0905e91e3f1c543745154407354ce5447 100644 (file)
@@ -60,6 +60,18 @@ my $app = sub {
                }
        } elsif ($path eq '/empty') {
                $code = 200;
+       } elsif ($path eq '/getline-die') {
+               $code = 200;
+               $body = Plack::Util::inline_object(
+                       getline => sub { die 'GETLINE FAIL' },
+                       close => sub { die 'CLOSE FAIL' },
+               );
+       } elsif ($path eq '/close-die') {
+               $code = 200;
+               $body = Plack::Util::inline_object(
+                       getline => sub { undef },
+                       close => sub { die 'CLOSE FAIL' },
+               );
        }
 
        [ $code, $h, $body ]
index 5ecc69b5e5862021a4808240453b04f70de3301f..1e8465c2ba6059846a2f7c7ebf2a0feb1d973f37 100644 (file)
@@ -85,6 +85,30 @@ my $spawn_httpd = sub {
        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';
@@ -489,4 +513,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;