]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/GitHTTPBackend.pm
githttpbackend: fall back to dumb if smart HTTP is off
[public-inbox.git] / lib / PublicInbox / GitHTTPBackend.pm
1 # Copyright (C) 2016 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # when no endpoints match, fallback to this and serve a static file
5 # or smart HTTP
6 package PublicInbox::GitHTTPBackend;
7 use strict;
8 use warnings;
9 use Fcntl qw(:seek);
10 use IO::File;
11 use PublicInbox::Spawn qw(spawn);
12
13 # n.b. serving "description" and "cloneurl" should be innocuous enough to
14 # not cause problems.  serving "config" might...
15 my @text = qw[HEAD info/refs
16         objects/info/(?:http-alternates|alternates|packs)
17         cloneurl description];
18
19 my @binary = qw!
20         objects/[a-f0-9]{2}/[a-f0-9]{38}
21         objects/pack/pack-[a-f0-9]{40}\.(?:pack|idx)
22         !;
23
24 our $ANY = join('|', @binary, @text);
25 my $BIN = join('|', @binary);
26 my $TEXT = join('|', @text);
27
28 sub r {
29         [ $_[0] , [qw(Content-Type text/plain Content-Length 0) ], [] ]
30 }
31
32 sub serve {
33         my ($cgi, $git, $path) = @_;
34         my $service = $cgi->param('service') || '';
35         if ($service =~ /\Agit-\w+-pack\z/ || $path =~ /\Agit-\w+-pack\z/) {
36                 my $ok = serve_smart($cgi, $git, $path);
37                 return $ok if $ok;
38         }
39
40         serve_dumb($cgi, $git, $path);
41 }
42
43 sub serve_dumb {
44         my ($cgi, $git, $path) = @_;
45
46         my $type;
47         if ($path =~ /\A(?:$BIN)\z/o) {
48                 $type = 'application/octet-stream';
49         } elsif ($path =~ /\A(?:$TEXT)\z/o) {
50                 $type = 'text/plain';
51         } else {
52                 return r(404);
53         }
54         my $f = "$git->{git_dir}/$path";
55         return r(404) unless -f $f && -r _;
56         my @st = stat(_);
57         my $size = $st[7];
58
59         # TODO: If-Modified-Since and Last-Modified
60         open my $in, '<', $f or return r(404);
61         my $code = 200;
62         my $len = $size;
63         my @h;
64
65         my $env = $cgi->{env};
66         my $range = $env->{HTTP_RANGE};
67         if (defined $range && $range =~ /\bbytes=(\d*)-(\d*)\z/) {
68                 ($code, $len) = prepare_range($cgi, $in, \@h, $1, $2, $size);
69                 if ($code == 416) {
70                         push @h, 'Content-Range', "bytes */$size";
71                         return [ 416, \@h, [] ];
72                 }
73         }
74
75         push @h, 'Content-Type', $type, 'Content-Length', $len;
76         sub {
77                 my ($res) = @_; # Plack callback
78                 my $fh = $res->([ $code, \@h ]);
79                 my $buf;
80                 my $n = 8192;
81                 while ($len > 0) {
82                         $n = $len if $len < $n;
83                         my $r = sysread($in, $buf, $n);
84                         last if (!defined($r) || $r <= 0);
85                         $len -= $r;
86                         $fh->write($buf);
87                 }
88                 $fh->close;
89         }
90 }
91
92 sub prepare_range {
93         my ($cgi, $in, $h, $beg, $end, $size) = @_;
94         my $code = 200;
95         my $len = $size;
96         if ($beg eq '') {
97                 if ($end ne '') { # "bytes=-$end" => last N bytes
98                         $beg = $size - $end;
99                         $beg = 0 if $beg < 0;
100                         $end = $size - 1;
101                         $code = 206;
102                 } else {
103                         $code = 416;
104                 }
105         } else {
106                 if ($beg > $size) {
107                         $code = 416;
108                 } elsif ($end eq '' || $end >= $size) {
109                         $end = $size - 1;
110                         $code = 206;
111                 } elsif ($end < $size) {
112                         $code = 206;
113                 } else {
114                         $code = 416;
115                 }
116         }
117         if ($code == 206) {
118                 $len = $end - $beg + 1;
119                 if ($len <= 0) {
120                         $code = 416;
121                 } else {
122                         seek($in, $beg, SEEK_SET) or return [ 500, [], [] ];
123                         push @$h, qw(Accept-Ranges bytes Content-Range);
124                         push @$h, "bytes $beg-$end/$size";
125
126                         # FIXME: Plack::Middleware::Deflater bug?
127                         $cgi->{env}->{'psgix.no-compress'} = 1;
128                 }
129         }
130         ($code, $len);
131 }
132
133 # returns undef if 403 so it falls back to dumb HTTP
134 sub serve_smart {
135         my ($cgi, $git, $path) = @_;
136         my $env = $cgi->{env};
137
138         my $input = $env->{'psgi.input'};
139         my $buf;
140         my $in;
141         my $err = $env->{'psgi.errors'};
142         my $fd = eval { fileno($input) };
143         if (defined $fd && $fd >= 0) {
144                 $in = $input;
145         } else {
146                 $in = input_to_file($env) or return r(500);
147         }
148         my ($rpipe, $wpipe);
149         unless (pipe($rpipe, $wpipe)) {
150                 $err->print("error creating pipe: $! - going static\n");
151                 return;
152         }
153         my %env = %ENV;
154         # GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL
155         # may be set in the server-process and are passed as-is
156         foreach my $name (qw(QUERY_STRING
157                                 REMOTE_USER REMOTE_ADDR
158                                 HTTP_CONTENT_ENCODING
159                                 CONTENT_TYPE
160                                 SERVER_PROTOCOL
161                                 REQUEST_METHOD)) {
162                 my $val = $env->{$name};
163                 $env{$name} = $val if defined $val;
164         }
165         my $git_dir = $git->{git_dir};
166         $env{GIT_HTTP_EXPORT_ALL} = '1';
167         $env{PATH_TRANSLATED} = "$git_dir/$path";
168         my %rdr = ( 0 => fileno($in), 1 => fileno($wpipe) );
169         my $pid = spawn([qw(git http-backend)], \%env, \%rdr);
170         unless (defined $pid) {
171                 $err->print("error spawning: $! - going static\n");
172                 return;
173         }
174         $wpipe = $in = undef;
175         $buf = '';
176         my ($vin, $fh, $res);
177         my $end = sub {
178                 if ($fh) {
179                         $fh->close;
180                         $fh = undef;
181                 }
182                 if ($rpipe) {
183                         $rpipe->close; # _may_ be Danga::Socket::close
184                         $rpipe = undef;
185                 }
186                 if (defined $pid && $pid != waitpid($pid, 0)) {
187                         $err->print("git http-backend ($git_dir): $?\n");
188                 } else {
189                         $pid = undef;
190                 }
191                 return unless $res;
192                 my $dumb = serve_dumb($cgi, $git, $path);
193                 ref($dumb) eq 'ARRAY' ? $res->($dumb) : $dumb->($res);
194         };
195         my $fail = sub {
196                 my ($e) = @_;
197                 if ($e eq 'EAGAIN') {
198                         select($vin, undef, undef, undef) if defined $vin;
199                         # $vin is undef on async, so this is a noop on EAGAIN
200                         return;
201                 }
202                 $end->();
203                 $err->print("git http-backend ($git_dir): $e\n");
204         };
205         my $cb = sub { # read git-http-backend output and stream to client
206                 my $r = $rpipe ? $rpipe->sysread($buf, 8192, length($buf)) : 0;
207                 return $fail->($!{EAGAIN} ? 'EAGAIN' : $!) unless defined $r;
208                 return $end->() if $r == 0; # EOF
209                 if ($fh) { # stream body from git-http-backend to HTTP client
210                         $fh->write($buf);
211                         $buf = '';
212                 } elsif ($buf =~ s/\A(.*?)\r\n\r\n//s) { # parse headers
213                         my $h = $1;
214                         my $code = 200;
215                         my @h;
216                         foreach my $l (split(/\r\n/, $h)) {
217                                 my ($k, $v) = split(/:\s*/, $l, 2);
218                                 if ($k =~ /\AStatus\z/i) {
219                                         ($code) = ($v =~ /\b(\d+)\b/);
220                                 } else {
221                                         push @h, $k, $v;
222                                 }
223                         }
224                         if ($code == 403) {
225                                 # smart cloning disabled, serve dumbly
226                                 # in $end since we never undef $res in here
227                         } else { # write response header:
228                                 $fh = $res->([ $code, \@h ]);
229                                 $res = undef;
230                                 $fh->write($buf);
231                         }
232                         $buf = '';
233                 } # else { keep reading ... }
234         };
235         if (my $async = $env->{'pi-httpd.async'}) {
236                 $rpipe = $async->($rpipe, $cb);
237                 sub { ($res) = @_ } # let Danga::Socket handle the rest.
238         } else { # synchronous loop for other PSGI servers
239                 $vin = '';
240                 vec($vin, fileno($rpipe), 1) = 1;
241                 sub {
242                         ($res) = @_;
243                         while ($rpipe) { $cb->() }
244                 }
245         }
246 }
247
248 sub input_to_file {
249         my ($env) = @_;
250         my $in = IO::File->new_tmpfile;
251         my $input = $env->{'psgi.input'};
252         my $buf;
253         while (1) {
254                 my $r = $input->read($buf, 8192);
255                 unless (defined $r) {
256                         my $err = $env->{'psgi.errors'};
257                         $err->print("error reading input: $!\n");
258                         return;
259                 }
260                 last if ($r == 0);
261                 $in->write($buf);
262         }
263         $in->flush;
264         $in->sysseek(0, SEEK_SET);
265         return $in;
266 }
267
268 1;