]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/GitHTTPBackend.pm
git-http-backend: simplify dumb serving
[public-inbox.git] / lib / PublicInbox / GitHTTPBackend.pm
index 2c81d4c8d8ecf864d76cc8ceb1b4820d54ef7b1d..97d96d526121752060ff3274dbcdfee57cfebc31 100644 (file)
@@ -9,6 +9,7 @@ use warnings;
 use Fcntl qw(:seek);
 use IO::File;
 use PublicInbox::Spawn qw(spawn);
+use HTTP::Date qw(time2str);
 
 # n.b. serving "description" and "cloneurl" should be innocuous enough to
 # not cause problems.  serving "config" might...
@@ -25,62 +26,100 @@ our $ANY = join('|', @binary, @text);
 my $BIN = join('|', @binary);
 my $TEXT = join('|', @text);
 
-sub r {
-       [ $_[0] , [qw(Content-Type text/plain Content-Length 0) ], [] ]
+my @no_cache = ('Expires', 'Fri, 01 Jan 1980 00:00:00 GMT',
+               'Pragma', 'no-cache',
+               'Cache-Control', 'no-cache, max-age=0, must-revalidate');
+
+my $nextq;
+sub do_next () {
+       my $q = $nextq;
+       $nextq = undef;
+       while (my $cb = shift @$q) {
+               $cb->(); # this may redefine nextq
+       }
+}
+
+sub r ($) {
+       my ($s) = @_;
+       [ $s, [qw(Content-Type text/plain Content-Length 0), @no_cache ], [] ]
 }
 
 sub serve {
        my ($cgi, $git, $path) = @_;
+
        my $service = $cgi->param('service') || '';
        if ($service =~ /\Agit-\w+-pack\z/ || $path =~ /\Agit-\w+-pack\z/) {
                my $ok = serve_smart($cgi, $git, $path);
                return $ok if $ok;
        }
 
+       serve_dumb($cgi, $git, $path);
+}
+
+sub err ($@) {
+       my ($env, @msg) = @_;
+       $env->{'psgi.errors'}->print(@msg, "\n");
+}
+
+sub drop_client ($) {
+       if (my $io = $_[0]->{'psgix.io'}) {
+               $io->close; # this is Danga::Socket::close
+       }
+}
+
+sub serve_dumb {
+       my ($cgi, $git, $path) = @_;
+
+       my @h;
        my $type;
        if ($path =~ /\A(?:$BIN)\z/o) {
                $type = 'application/octet-stream';
+               push @h, 'Expires', time2str(time + 31536000);
+               push @h, 'Cache-Control', 'public, max-age=31536000';
        } elsif ($path =~ /\A(?:$TEXT)\z/o) {
                $type = 'text/plain';
+               push @h, @no_cache;
        } else {
                return r(404);
        }
+
        my $f = "$git->{git_dir}/$path";
-       return r(404) unless -f $f && -r _;
+       return r(404) unless -f $f && -r _; # just in case it's a FIFO :P
        my @st = stat(_);
        my $size = $st[7];
+       my $env = $cgi->{env};
 
-       # TODO: If-Modified-Since and Last-Modified
+       # TODO: If-Modified-Since and Last-Modified?
        open my $in, '<', $f or return r(404);
-       my $code = 200;
        my $len = $size;
-       my @h;
-
-       my $env = $cgi->{env};
-       my $range = $env->{HTTP_RANGE};
-       if (defined $range && $range =~ /\bbytes=(\d*)-(\d*)\z/) {
+       my $code = 200;
+       push @h, 'Content-Type', $type;
+       if (($env->{HTTP_RANGE} || '') =~ /\bbytes=(\d*)-(\d*)\z/) {
                ($code, $len) = prepare_range($cgi, $in, \@h, $1, $2, $size);
                if ($code == 416) {
                        push @h, 'Content-Range', "bytes */$size";
                        return [ 416, \@h, [] ];
                }
        }
-
-       push @h, 'Content-Type', $type, 'Content-Length', $len;
-       sub {
-               my ($res) = @_; # Plack callback
-               my $fh = $res->([ $code, \@h ]);
-               my $buf;
-               my $n = 8192;
-               while ($len > 0) {
+       push @h, 'Content-Length', $len;
+       my $n = 65536;
+       [ $code, \@h, Plack::Util::inline_object(close => sub { close $in },
+               getline => sub {
+                       return if $len == 0;
                        $n = $len if $len < $n;
-                       my $r = sysread($in, $buf, $n);
-                       last if (!defined($r) || $r <= 0);
-                       $len -= $r;
-                       $fh->write($buf);
-               }
-               $fh->close;
-       }
+                       my $r = sysread($in, my $buf, $n);
+                       if (!defined $r) {
+                               err($env, "$f read error: $!");
+                       } elsif ($r <= 0) {
+                               err($env, "$f EOF with $len bytes left");
+                       } else {
+                               $len -= $r;
+                               $n = 8192;
+                               return $buf;
+                       }
+                       drop_client($env);
+                       return;
+               })]
 }
 
 sub prepare_range {
@@ -113,7 +152,7 @@ sub prepare_range {
                if ($len <= 0) {
                        $code = 416;
                } else {
-                       seek($in, $beg, SEEK_SET) or return [ 500, [], [] ];
+                       sysseek($in, $beg, SEEK_SET) or return [ 500, [], [] ];
                        push @$h, qw(Accept-Ranges bytes Content-Range);
                        push @$h, "bytes $beg-$end/$size";
 
@@ -132,7 +171,6 @@ sub serve_smart {
        my $input = $env->{'psgi.input'};
        my $buf;
        my $in;
-       my $err = $env->{'psgi.errors'};
        my $fd = eval { fileno($input) };
        if (defined $fd && $fd >= 0) {
                $in = $input;
@@ -141,11 +179,11 @@ sub serve_smart {
        }
        my ($rpipe, $wpipe);
        unless (pipe($rpipe, $wpipe)) {
-               $err->print("error creating pipe: $!\n");
-               return r(500);
+               err($env, "error creating pipe: $! - going static");
+               return;
        }
        my %env = %ENV;
-       # GIT_HTTP_EXPORT_ALL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL
+       # GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL
        # may be set in the server-process and are passed as-is
        foreach my $name (qw(QUERY_STRING
                                REMOTE_USER REMOTE_ADDR
@@ -162,47 +200,60 @@ sub serve_smart {
        my %rdr = ( 0 => fileno($in), 1 => fileno($wpipe) );
        my $pid = spawn([qw(git http-backend)], \%env, \%rdr);
        unless (defined $pid) {
-               $err->print("error spawning: $!\n");
-               return r(500);
+               err($env, "error spawning: $! - going static");
+               return;
        }
        $wpipe = $in = undef;
        $buf = '';
        my ($vin, $fh, $res);
+
+       # Danga::Socket users, we queue up the read_enable callback to
+       # fire after pending writes are complete:
+       my $pi_http = $env->{'psgix.io'};
+       my $read_enable = sub { $rpipe->watch_read(1) };
+       my $read_disable = sub {
+               $rpipe->watch_read(0);
+               $pi_http->write($read_enable);
+       };
+
        my $end = sub {
                if ($fh) {
                        $fh->close;
                        $fh = undef;
-               } else {
-                       $res->(r(500)) if $res;
                }
                if ($rpipe) {
-                       $rpipe->close; # _may_ be Danga::Socket::close
+                       # _may_ be Danga::Socket::close via
+                       # PublicInbox::HTTPD::Async::close:
+                       $rpipe->close;
                        $rpipe = undef;
                }
                if (defined $pid) {
-                       my $wpid = $pid;
-                       $pid = undef;
-                       return if $wpid == waitpid($wpid, 0);
-                       $err->print("git http-backend ($git_dir): $?\n");
+                       my $e = $pid == waitpid($pid, 0) ?
+                               $? : "PID:$pid still running?";
+                       err($env, "git http-backend ($git_dir): $e") if $e;
                }
+               return unless $res;
+               my $dumb = serve_dumb($cgi, $git, $path);
+               ref($dumb) eq 'ARRAY' ? $res->($dumb) : $dumb->($res);
        };
        my $fail = sub {
-               my ($e) = @_;
-               if ($e eq 'EAGAIN') {
+               if ($!{EAGAIN} || $!{EINTR}) {
                        select($vin, undef, undef, undef) if defined $vin;
                        # $vin is undef on async, so this is a noop on EAGAIN
                        return;
                }
+               my $e = $!;
                $end->();
-               $err->print("git http-backend ($git_dir): $e\n");
+               err($env, "git http-backend ($git_dir): $e\n");
        };
        my $cb = sub { # read git-http-backend output and stream to client
                my $r = $rpipe ? $rpipe->sysread($buf, 8192, length($buf)) : 0;
-               return $fail->($!{EAGAIN} ? 'EAGAIN' : $!) unless defined $r;
+               return $fail->() unless defined $r;
                return $end->() if $r == 0; # EOF
                if ($fh) { # stream body from git-http-backend to HTTP client
                        $fh->write($buf);
                        $buf = '';
+                       $read_disable->() if $read_disable;
                } elsif ($buf =~ s/\A(.*?)\r\n\r\n//s) { # parse headers
                        my $h = $1;
                        my $code = 200;
@@ -215,17 +266,23 @@ sub serve_smart {
                                        push @h, $k, $v;
                                }
                        }
-                       # write response header:
-                       $fh = $res->([ $code, \@h ]);
-                       $res = undef;
-                       $fh->write($buf);
+                       if ($code == 403) {
+                               # smart cloning disabled, serve dumbly
+                               # in $end since we never undef $res in here
+                       } else { # write response header:
+                               $fh = $res->([ $code, \@h ]);
+                               $res = undef;
+                               $fh->write($buf);
+                       }
                        $buf = '';
                } # else { keep reading ... }
        };
        if (my $async = $env->{'pi-httpd.async'}) {
+               # $async is PublicInbox::HTTPD::Async->new($rpipe, $cb)
                $rpipe = $async->($rpipe, $cb);
                sub { ($res) = @_ } # let Danga::Socket handle the rest.
        } else { # synchronous loop for other PSGI servers
+               $read_enable = $read_disable = undef;
                $vin = '';
                vec($vin, fileno($rpipe), 1) = 1;
                sub {
@@ -243,8 +300,7 @@ sub input_to_file {
        while (1) {
                my $r = $input->read($buf, 8192);
                unless (defined $r) {
-                       my $err = $env->{'psgi.errors'};
-                       $err->print("error reading input: $!\n");
+                       err($env, "error reading input: $!");
                        return;
                }
                last if ($r == 0);