]> git.ipfire.org Git - thirdparty/public-inbox.git/commitdiff
repobrowse: git fallback allows smart cloning
authorEric Wong <e@80x24.org>
Mon, 7 Mar 2016 08:10:55 +0000 (08:10 +0000)
committerEric Wong <e@80x24.org>
Tue, 5 Apr 2016 18:58:27 +0000 (18:58 +0000)
We can reuse the existing code for cloning ssoma repositories
to serve normal git repos for repobrowse.

Also, this finally adds a test to fallback to dumb cloning when
http.uploadPack is disabled for the git repository to save
CPU/memory on the host machine.

lib/PublicInbox/Repobrowse.pm
lib/PublicInbox/RepobrowseGitFallback.pm
t/repobrowse_common_git.perl
t/repobrowse_git_fallback.t [new file with mode: 0644]

index 82d38f903cf33ae06bf2168f9a0ae9747c5cca6e..51ec14c8ec435a058b19f28b57251c8f19b7e310 100644 (file)
@@ -68,7 +68,7 @@ sub root_index {
 
 sub run {
        my ($self, $cgi, $method) = @_;
-       return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD\z/);
+       return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD|POST\z/);
 
        # URL syntax: / repo [ / cmd [ / path ] ]
        # cmd: log | commit | diff | tree | view | blob | snapshot
index 79cc2fe4ab72d34f85ff8ec835a8830a68fbd674..696e5b944ede1e38cdd5cc41c32af92710e8a50e 100644 (file)
@@ -7,81 +7,16 @@ package PublicInbox::RepobrowseGitFallback;
 use strict;
 use warnings;
 use base qw(PublicInbox::RepobrowseBase);
-use Fcntl qw(:seek);
+use PublicInbox::GitHTTPBackend;
 
 # overrides PublicInbox::RepobrowseBase::call
 sub call {
        my ($self, undef, $req) = @_;
        my $expath = $req->{expath};
        return if index($expath, '..') >= 0; # prevent path traversal
-
        my $git = $req->{repo_info}->{git};
-       my $f = "$git->{git_dir}/$expath";
-       return unless -f $f && -r _;
-       my @st = stat(_);
-       my ($size, $mtime) = ($st[7], $st[9]);
-       # TODO: if-modified-since and last-modified...
-       open my $in, '<', $f or return;
-       my $code = 200;
-       my $len = $size;
-       my @h;
-
-       # FIXME: this is Plack-only
-       my $range = eval { $req->{cgi}->{env}->{HTTP_RANGE} };
-       if (defined $range && $range =~ /\bbytes=(\d*)-(\d*)\z/) {
-               ($code, $len) = prepare_range($req, $in, \@h, $1, $2, $size);
-       }
-
-       # we use the unsafe variant since we assume the server admin
-       # would not place untrusted HTML/JS/CSS in the git directory
-       my $type = $self->mime_type_unsafe($expath) || 'text/plain';
-       push @h, 'Content-Type', $type, 'Content-Length', $len;
-       sub {
-               my ($res) = @_; # Plack callback
-               my $fh = $res->([ $code, \@h ]);
-               my $buf;
-               my $n = 8192;
-               while ($size > 0) {
-                       $n = $size if $size < $n;
-                       my $r = read($in, $buf, $n);
-                       last if (!defined($r) || $r <= 0);
-                       $fh->write($buf);
-               }
-               $fh->close;
-       }
-}
-
-sub bad_range { [ 416, [], [] ] }
-
-sub prepare_range {
-       my ($req, $in, $h, $beg, $end, $size) = @_;
-       my $code = 200;
-       my $len = $size;
-       if ($beg eq '') {
-               if ($end ne '') { # last N bytes
-                       $beg = $size - $end;
-                       $beg = 0 if $beg < 0;
-                       $end = $size - 1;
-                       $code = 206;
-               }
-       } else {
-               if ($end eq '' || $end >= $size) {
-                       $end = $size - 1;
-                       $code = 206;
-               } elsif ($end < $size) {
-                       $code = 206;
-               }
-       }
-       if ($code == 206) {
-               $len = $end - $beg + 1;
-               seek($in, $beg, SEEK_SET) or return [ 500, [], [] ];
-               push @$h, qw(Accept-Ranges bytes),
-                               'Content-Range', "bytes $beg-$end/$size";
-
-               # FIXME: Plack::Middleware::Deflater bug?
-               $req->{cgi}->{env}->{'psgix.no-compress'} = 1;
-       }
-       ($code, $len);
+       my $cgi = $req->{cgi};
+       PublicInbox::GitHTTPBackend::serve($cgi, $git, $expath);
 }
 
 1;
index 116ae5abbd87e7f827bd8109161c976fbd4d9938..9c62a2614ce31c7bd3306abf9f13ca30a9a43bc7 100644 (file)
@@ -4,7 +4,6 @@
 use strict;
 use warnings;
 use Test::More;
-use Data::Dumper;
 use File::Temp qw/tempdir/;
 use Cwd qw/getcwd/;
 my @mods = qw(HTTP::Request::Common Plack::Request Plack::Test URI::Escape);
@@ -31,8 +30,9 @@ sub dechunk ($) {
 }
 
 use_ok $_ foreach @mods;
-my $git_dir = tempdir(CLEANUP => 1);
+my $git_dir = tempdir('repobrowse-XXXXXX', CLEANUP => 1, TMPDIR => 1);
 my $psgi = "examples/repobrowse.psgi";
+my $repobrowse_config = "$git_dir/repobrowse_config";
 my $app;
 ok(-f $psgi, 'psgi example for repobrowse.psgi found');
 {
@@ -49,7 +49,6 @@ ok(-f $psgi, 'psgi example for repobrowse.psgi found');
        }
        waitpid $pid, 0;
        is($?, 0, 'fast-import succeeded');
-       my $repobrowse_config = "$git_dir/pi_repobrowse_config";
        my $fh;
        ok((open $fh, '>', $repobrowse_config and
                print $fh '[repo "test.git"]', "\n",
@@ -64,4 +63,5 @@ bless {
        psgi => $psgi,
        git_dir => $git_dir,
        app => $app,
+       repobrowse_config => $repobrowse_config,
 }, 'Repobrowse::TestGit';
diff --git a/t/repobrowse_git_fallback.t b/t/repobrowse_git_fallback.t
new file mode 100644 (file)
index 0000000..99f2ee7
--- /dev/null
@@ -0,0 +1,87 @@
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+use Test::More;
+my $test = require './t/repobrowse_common_git.perl';
+foreach my $mod (qw(Danga::Socket HTTP::Parser::XS HTTP::Date HTTP::Status)) {
+       eval "require $mod";
+       plan skip_all => "$mod missing for repobrowse_git_fallback.t" if $@;
+}
+use File::Temp qw/tempdir/;
+use Cwd qw/getcwd/;
+use IO::Socket;
+use Fcntl qw(F_SETFD);
+use POSIX qw(dup2);
+my $tmpdir = tempdir('repobrowse_git_fallback-XXXXXX', TMPDIR => 1, CLEANUP => 1);
+my $err = "$tmpdir/stderr.log";
+my $out = "$tmpdir/stdout.log";
+my $httpd = 'blib/script/public-inbox-httpd';
+my $psgi = getcwd() . '/' . $test->{psgi};
+my %opts = (
+       LocalAddr => '127.0.0.1',
+       ReuseAddr => 1,
+       Proto => 'tcp',
+       Type => SOCK_STREAM,
+       Listen => 1024,
+);
+my $sock = IO::Socket::INET->new(%opts);
+my $pid;
+END { kill 'TERM', $pid if defined $pid };
+my $spawn_httpd = sub {
+       $pid = fork;
+       if ($pid == 0) {
+               # pretend to be systemd:
+               dup2(fileno($sock), 3) or die "dup2 failed: $!\n";
+               my $t = IO::Handle->new_from_fd(3, 'r');
+               $t->fcntl(F_SETFD, 0);
+               $ENV{REPOBROWSE_CONFIG} = $test->{repobrowse_config};
+               $ENV{LISTEN_PID} = $$;
+               $ENV{LISTEN_FDS} = 1;
+               exec $httpd, '-W0', $psgi;
+               # exec $httpd, '-W0', "--stdout=$out", "--stderr=$err", $psgi;
+               die "FAIL: $!\n";
+       }
+       ok(defined $pid, 'forked httpd process successfully');
+};
+
+$spawn_httpd->();
+
+{
+       my $host = $sock->sockhost;
+       my $port = $sock->sockport;
+       my $url = "http://$host:$port/test.git";
+       is(system(qw(git clone -q --mirror), $url, "$tmpdir/smart.git"),
+               0, 'smart clone successful');
+       is(system('git', "--git-dir=$tmpdir/smart.git", 'fsck'), 0, 'fsck OK');
+
+       is(system('git', "--git-dir=$test->{git_dir}",
+               qw(config http.uploadpack 0)), 0, 'disabled smart HTTP');
+       is(system('git', "--git-dir=$test->{git_dir}",
+               qw(update-server-info)), 0, 'enable dumb HTTP');
+       is(system(qw(git clone -q --mirror), $url, "$tmpdir/dumb.git"),
+               0, 'dumb clone successful');
+       is(system('git', "--git-dir=$tmpdir/dumb.git", 'fsck'), 0, 'fsck dumb OK');
+
+       # allow reading description file
+       my %conn = ( PeerAddr => $host, PeerPort => $port, Proto => 'tcp',
+               Type => SOCK_STREAM);
+       my $conn = IO::Socket::INET->new(%conn);
+       ok($conn, "connected for description check");
+       $conn->write("GET /test.git/description HTTP/1.0\r\n\r\n");
+       ok($conn->read(my $buf, 8192), 'read response');
+       my ($head, $body) = split(/\r\n\r\n/, $buf, 2);
+       like($head, qr!\AHTTP/1\.0 200 !s, 'got 200 response for description');
+
+       $conn = IO::Socket::INET->new(%conn);
+       ok($conn, "connected for range check");
+       $conn->write("GET /test.git/description HTTP/1.0\r\n" .
+                       "Range: bytes=5-\r\n\r\n");
+       ok($conn->read($buf, 8192), 'read partial response');
+       my ($h2, $b2) = split(/\r\n\r\n/, $buf, 2);
+       like($h2, qr!\AHTTP/1\.0 206 !s, 'got 206 response for range');
+       is($b2, substr($body, 5), 'substring matches on 206');
+}
+
+done_testing();
+1;