... | ... |
@@ -8,6 +8,7 @@ use Encode qw/encode decode/; |
8 | 8 |
use Gitprep::API; |
9 | 9 |
use Gitprep::Git; |
10 | 10 |
use Gitprep::Manager; |
11 |
+use Gitprep::SmartHTTP; |
|
11 | 12 |
use Scalar::Util 'weaken'; |
12 | 13 |
use Validator::Custom; |
13 | 14 |
|
... | ... |
@@ -24,6 +25,8 @@ has 'git'; |
24 | 25 |
has 'manager'; |
25 | 26 |
has 'validator'; |
26 | 27 |
|
28 |
+use constant BUFFER_SIZE => 8192; |
|
29 |
+ |
|
27 | 30 |
sub startup { |
28 | 31 |
my $self = shift; |
29 | 32 |
|
... | ... |
@@ -70,7 +73,7 @@ sub startup { |
70 | 73 |
$self->git($git); |
71 | 74 |
|
72 | 75 |
# Added public path |
73 |
- push @{$self->static->paths}, $rep_home; |
|
76 |
+ # push @{$self->static->paths}, $rep_home; |
|
74 | 77 |
|
75 | 78 |
# DBI |
76 | 79 |
my $db_file = $ENV{GITPREP_DB_FILE} |
... | ... |
@@ -177,7 +180,81 @@ sub startup { |
177 | 180 |
# Settings |
178 | 181 |
$r->get('/_settings' => template '/user-settings'); |
179 | 182 |
} |
180 |
- |
|
183 |
+ |
|
184 |
+ # Smart HTTP |
|
185 |
+ { |
|
186 |
+ my $r = $r->route('/(:project).git', project => $id_re); |
|
187 |
+ |
|
188 |
+ my $sh = Gitprep::SmartHTTP->new; |
|
189 |
+ |
|
190 |
+ # Fetch |
|
191 |
+ $r->get('/info/refs')->to(cb => sub { |
|
192 |
+ my $self = shift; |
|
193 |
+ |
|
194 |
+ my $service = $self->param('service') || ''; |
|
195 |
+ |
|
196 |
+ my $user = $self->param('user'); |
|
197 |
+ my $project = $self->param('project'); |
|
198 |
+ |
|
199 |
+ if ($service eq 'git-upload-pack') { |
|
200 |
+ |
|
201 |
+ my $rep = $git->rep($user, $project); |
|
202 |
+ my @cmd = $git->cmd($user, $project, 'upload-pack', '--stateless-rpc', '--advertise-refs', $rep); |
|
203 |
+ |
|
204 |
+ warn "@cmd"; |
|
205 |
+ |
|
206 |
+ use IPC::Open3 'open3'; |
|
207 |
+ use Symbol 'gensym'; |
|
208 |
+ my ($cout, $cerr) = (gensym, gensym ); |
|
209 |
+ my $pid = open3(my $cin, $cout, $cerr, @cmd ); |
|
210 |
+ close $cin; |
|
211 |
+ my ( $refs, $err, $buf ) = ( '', '', '' ); |
|
212 |
+ my $s = IO::Select->new( $cout, $cerr ); |
|
213 |
+ while (my @ready = $s->can_read) { |
|
214 |
+ for my $handle (@ready) { |
|
215 |
+ while ( sysread( $handle, $buf, BUFFER_SIZE ) ) { |
|
216 |
+ if ( $handle == $cerr ) { |
|
217 |
+ $err .= $buf; |
|
218 |
+ } |
|
219 |
+ else { |
|
220 |
+ $refs .= $buf; |
|
221 |
+ } |
|
222 |
+ } |
|
223 |
+ $s->remove($handle) if eof($handle); |
|
224 |
+ } |
|
225 |
+ } |
|
226 |
+ close $cout; |
|
227 |
+ close $cerr; |
|
228 |
+ waitpid($pid, 0); |
|
229 |
+ |
|
230 |
+ if ($err) { |
|
231 |
+ app->log->error($err); |
|
232 |
+ $self->render_exception($err); |
|
233 |
+ } |
|
234 |
+ |
|
235 |
+ $self->res->headers->content_type('application/x-git-upload-pack-advertisement'); |
|
236 |
+ |
|
237 |
+ my $data = |
|
238 |
+ $sh->pkt_write("# service=git-upload-pack\n") . $sh->pkt_flush() . $refs; |
|
239 |
+ |
|
240 |
+ $self->render(data => $data); |
|
241 |
+ } |
|
242 |
+ else { |
|
243 |
+ $sh->dumb_info_refs; |
|
244 |
+ } |
|
245 |
+ }); |
|
246 |
+ |
|
247 |
+ # $r->post('/git-upload-pack'); |
|
248 |
+ # $r->post('/git-receive-pack'); |
|
249 |
+ # $r->get('/HEAD'); |
|
250 |
+ # $r->get('/objects/info/alternates'); |
|
251 |
+ # $r->get('/objects/info/http-alternates'); |
|
252 |
+ # $r->get('/objects/info/packs'); |
|
253 |
+ # $r->get('/objects/[0-9a-f]{2}/[0-9a-f]{38}'); |
|
254 |
+ # $r->get('/objects/pack/pack-[0-9a-f]{40}\.pack'); |
|
255 |
+ # $r->get('/objects/pack/pack-[0-9a-f]{40}\.idx'); |
|
256 |
+ } |
|
257 |
+ |
|
181 | 258 |
# Project |
182 | 259 |
{ |
183 | 260 |
my $r = $r->route('/:project', project => $id_re); |
... | ... |
@@ -0,0 +1,387 @@ |
1 |
+package Gitprep::SmartHTTP; |
|
2 |
+ |
|
3 |
+use Mojo::Base -base; |
|
4 |
+ |
|
5 |
+use File::Spec::Functions; |
|
6 |
+use Symbol qw(gensym); |
|
7 |
+use IPC::Open3; |
|
8 |
+use IO::Select; |
|
9 |
+use IO::Uncompress::Gunzip qw($GunzipError); |
|
10 |
+ |
|
11 |
+use constant BUFFER_SIZE => 8192; |
|
12 |
+ |
|
13 |
+our $VERSION = '0.05'; |
|
14 |
+my @SERVICES = ( |
|
15 |
+ [ 'POST', 'service_rpc', qr{(.*?)/git-upload-pack$}, 'upload-pack' ], |
|
16 |
+ [ 'POST', 'service_rpc', qr{(.*?)/git-receive-pack$}, 'receive-pack' ], |
|
17 |
+ |
|
18 |
+ [ 'GET', 'get_info_refs', qr{(.*?)/info/refs$} ], |
|
19 |
+ [ 'GET', 'get_text_file', qr{(.*?)/HEAD$} ], |
|
20 |
+ [ 'GET', 'get_text_file', qr{(.*?)/objects/info/alternates$} ], |
|
21 |
+ [ 'GET', 'get_text_file', qr{(.*?)/objects/info/http-alternates$} ], |
|
22 |
+ [ 'GET', 'get_info_packs', qr{(.*?)/objects/info/packs$} ], |
|
23 |
+ [ 'GET', 'get_loose_object', qr{(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$} ], |
|
24 |
+ [ |
|
25 |
+ 'GET', 'get_pack_file', qr{(.*?)/objects/pack/pack-[0-9a-f]{40}\.pack$} |
|
26 |
+ ], |
|
27 |
+ [ 'GET', 'get_idx_file', qr{(.*?)/objects/pack/pack-[0-9a-f]{40}\.idx$} ], |
|
28 |
+); |
|
29 |
+ |
|
30 |
+sub get_service { |
|
31 |
+ my $self = shift; |
|
32 |
+ my $req = shift; |
|
33 |
+ |
|
34 |
+ my $service = $req->param('service'); |
|
35 |
+ return unless $service; |
|
36 |
+ return unless substr( $service, 0, 4 ) eq 'git-'; |
|
37 |
+ $service =~ s/git-//g; |
|
38 |
+ return $service; |
|
39 |
+} |
|
40 |
+ |
|
41 |
+sub match_routing { |
|
42 |
+ my $self = shift; |
|
43 |
+ my $req = shift; |
|
44 |
+ |
|
45 |
+ my ( $cmd, $path, $file, $rpc ); |
|
46 |
+ for my $s (@SERVICES) { |
|
47 |
+ my $match = $s->[2]; |
|
48 |
+ if ( $req->path_info =~ /$match/ ) { |
|
49 |
+ return ('not_allowed') if $s->[0] ne uc( $req->method ); |
|
50 |
+ $cmd = $s->[1]; |
|
51 |
+ $path = $1; |
|
52 |
+ $file = $req->path_info; |
|
53 |
+ $file =~ s|\Q$path/\E||; |
|
54 |
+ $rpc = $s->[3]; |
|
55 |
+ return ( $cmd, $path, $file, $rpc ); |
|
56 |
+ } |
|
57 |
+ } |
|
58 |
+ return (); |
|
59 |
+} |
|
60 |
+ |
|
61 |
+sub get_git_repo_dir { |
|
62 |
+ my $self = shift; |
|
63 |
+ my $path = shift; |
|
64 |
+ |
|
65 |
+ my $root = $self->root || `pwd`; |
|
66 |
+ chomp $root; |
|
67 |
+ $path = catdir( $root, $path ); |
|
68 |
+ return $path if ( -d $path ); |
|
69 |
+ return; |
|
70 |
+} |
|
71 |
+ |
|
72 |
+sub service_rpc { |
|
73 |
+ my $self = shift; |
|
74 |
+ my $args = shift; |
|
75 |
+ |
|
76 |
+ my $req = $args->{req}; |
|
77 |
+ my $rpc = $args->{rpc}; |
|
78 |
+ |
|
79 |
+ return $self->return_403 |
|
80 |
+ unless $self->has_access( $req, $rpc, 1 ); |
|
81 |
+ |
|
82 |
+ my @cmd = $self->git_command( $rpc, '--stateless-rpc', '.' ); |
|
83 |
+ |
|
84 |
+ my $input = $req->input; |
|
85 |
+ if ( exists $req->env->{HTTP_CONTENT_ENCODING} |
|
86 |
+ && $req->env->{HTTP_CONTENT_ENCODING} =~ /^(?:x-)?gzip$/ ) |
|
87 |
+ { |
|
88 |
+ $input = IO::Uncompress::Gunzip->new($input); |
|
89 |
+ unless ($input) { |
|
90 |
+ $req->env->{'psgi.errors'}->print("gunzip failed: $GunzipError"); |
|
91 |
+ return $self->return_400; |
|
92 |
+ } |
|
93 |
+ } |
|
94 |
+ my ( $cout, $cerr ) = ( gensym, gensym ); |
|
95 |
+ my $pid = open3( my $cin, $cout, $cerr, @cmd ); |
|
96 |
+ my $input_len = 0; |
|
97 |
+ while ( my $len = $input->read( my $buf, BUFFER_SIZE ) > 0 ) { |
|
98 |
+ print $cin $buf; |
|
99 |
+ $input_len += $len; |
|
100 |
+ } |
|
101 |
+ close $cin; |
|
102 |
+ if ( $input_len == 0 ) { |
|
103 |
+ close $cout; |
|
104 |
+ close $cerr; |
|
105 |
+ waitpid( $pid, 0 ); |
|
106 |
+ return $self->return_400; |
|
107 |
+ } |
|
108 |
+ |
|
109 |
+ return sub { |
|
110 |
+ my $respond = shift; |
|
111 |
+ my $writer = $respond->( |
|
112 |
+ [ |
|
113 |
+ 200, |
|
114 |
+ [ |
|
115 |
+ 'Content-Type' => |
|
116 |
+ sprintf( 'application/x-git-%s-result', $rpc ), |
|
117 |
+ ] |
|
118 |
+ ] |
|
119 |
+ ); |
|
120 |
+ |
|
121 |
+ my ( $out, $err, $buf ) = ( '', '', '' ); |
|
122 |
+ my $s = IO::Select->new( $cout, $cerr ); |
|
123 |
+ while ( my @ready = $s->can_read ) { |
|
124 |
+ for my $handle (@ready) { |
|
125 |
+ while ( sysread( $handle, $buf, BUFFER_SIZE ) ) { |
|
126 |
+ if ( $handle == $cerr ) { |
|
127 |
+ $err .= $buf; |
|
128 |
+ } |
|
129 |
+ else { |
|
130 |
+ $writer->write($buf); |
|
131 |
+ } |
|
132 |
+ } |
|
133 |
+ $s->remove($handle) if eof($handle); |
|
134 |
+ } |
|
135 |
+ } |
|
136 |
+ close $cout; |
|
137 |
+ close $cerr; |
|
138 |
+ waitpid( $pid, 0 ); |
|
139 |
+ |
|
140 |
+ if ($err) { |
|
141 |
+ $req->env->{'psgi.errors'}->print("git command failed: $err"); |
|
142 |
+ } |
|
143 |
+ $writer->close(); |
|
144 |
+ } |
|
145 |
+} |
|
146 |
+ |
|
147 |
+sub get_info_refs { |
|
148 |
+ my $self = shift; |
|
149 |
+ my $args = shift; |
|
150 |
+ |
|
151 |
+ my $req = $args->{req}; |
|
152 |
+ my $service = $self->get_service($req); |
|
153 |
+ if ( $self->has_access( $args->{req}, $service ) ) { |
|
154 |
+ my @cmd = |
|
155 |
+ $self->git_command( $service, '--stateless-rpc', '--advertise-refs', |
|
156 |
+ '.' ); |
|
157 |
+ |
|
158 |
+ my ( $cout, $cerr ) = ( gensym, gensym ); |
|
159 |
+ my $pid = open3( my $cin, $cout, $cerr, @cmd ); |
|
160 |
+ close $cin; |
|
161 |
+ my ( $refs, $err, $buf ) = ( '', '', '' ); |
|
162 |
+ my $s = IO::Select->new( $cout, $cerr ); |
|
163 |
+ while ( my @ready = $s->can_read ) { |
|
164 |
+ for my $handle (@ready) { |
|
165 |
+ while ( sysread( $handle, $buf, BUFFER_SIZE ) ) { |
|
166 |
+ if ( $handle == $cerr ) { |
|
167 |
+ $err .= $buf; |
|
168 |
+ } |
|
169 |
+ else { |
|
170 |
+ $refs .= $buf; |
|
171 |
+ } |
|
172 |
+ } |
|
173 |
+ $s->remove($handle) if eof($handle); |
|
174 |
+ } |
|
175 |
+ } |
|
176 |
+ close $cout; |
|
177 |
+ close $cerr; |
|
178 |
+ waitpid( $pid, 0 ); |
|
179 |
+ |
|
180 |
+ if ($err) { |
|
181 |
+ $req->env->{'psgi.errors'}->print("git command failed: $err"); |
|
182 |
+ return $self->return_400; |
|
183 |
+ } |
|
184 |
+ |
|
185 |
+ my $res = $req->new_response(200); |
|
186 |
+ $res->headers( |
|
187 |
+ [ |
|
188 |
+ 'Content-Type' => |
|
189 |
+ sprintf( 'application/x-git-%s-advertisement', $service ), |
|
190 |
+ ] |
|
191 |
+ ); |
|
192 |
+ my $body = |
|
193 |
+ pkt_write("# service=git-${service}\n") . pkt_flush() . $refs; |
|
194 |
+ $res->body($body); |
|
195 |
+ return $res->finalize; |
|
196 |
+ } |
|
197 |
+ else { |
|
198 |
+ return $self->dumb_info_refs($args); |
|
199 |
+ } |
|
200 |
+} |
|
201 |
+ |
|
202 |
+sub dumb_info_refs { |
|
203 |
+ my $self = shift; |
|
204 |
+ my $args = shift; |
|
205 |
+ $self->update_server_info; |
|
206 |
+ $self->send_file( $args, "text/plain; charset=utf-8" ); |
|
207 |
+} |
|
208 |
+ |
|
209 |
+sub get_info_packs { |
|
210 |
+ my $self = shift; |
|
211 |
+ my $args = shift; |
|
212 |
+ $self->send_file( $args, "text/plain; charset=utf-8" ); |
|
213 |
+} |
|
214 |
+ |
|
215 |
+sub get_loose_object { |
|
216 |
+ my $self = shift; |
|
217 |
+ my $args = shift; |
|
218 |
+ $self->send_file( $args, "application/x-git-loose-object" ); |
|
219 |
+} |
|
220 |
+ |
|
221 |
+sub get_pack_file { |
|
222 |
+ my $self = shift; |
|
223 |
+ my $args = shift; |
|
224 |
+ $self->send_file( $args, "application/x-git-packed-objects" ); |
|
225 |
+} |
|
226 |
+ |
|
227 |
+sub get_idx_file { |
|
228 |
+ my $self = shift; |
|
229 |
+ my $args = shift; |
|
230 |
+ $self->send_file( $args, "application/x-git-packed-objects-toc" ); |
|
231 |
+} |
|
232 |
+ |
|
233 |
+sub get_text_file { |
|
234 |
+ my $self = shift; |
|
235 |
+ my $args = shift; |
|
236 |
+ $self->send_file( $args, "text/plain" ); |
|
237 |
+} |
|
238 |
+ |
|
239 |
+sub update_server_info { |
|
240 |
+ my $self = shift; |
|
241 |
+ system( $self->git_command('update-server-info') ); |
|
242 |
+} |
|
243 |
+ |
|
244 |
+sub git_command { |
|
245 |
+ my $self = shift; |
|
246 |
+ my @commands = @_; |
|
247 |
+ my $git_bin = $self->git_path; |
|
248 |
+ return ( $git_bin, @commands ); |
|
249 |
+} |
|
250 |
+ |
|
251 |
+sub has_access { |
|
252 |
+ my $self = shift; |
|
253 |
+ my ( $req, $rpc, $check_content_type ) = @_; |
|
254 |
+ |
|
255 |
+ if ( $check_content_type |
|
256 |
+ && $req->content_type ne |
|
257 |
+ sprintf( "application/x-git-%s-request", $rpc ) ) |
|
258 |
+ { |
|
259 |
+ return; |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ return if !$rpc; |
|
263 |
+ return $self->received_pack if $rpc eq 'receive-pack'; |
|
264 |
+ return $self->upload_pack if $rpc eq 'upload-pack'; |
|
265 |
+ return; |
|
266 |
+} |
|
267 |
+ |
|
268 |
+sub send_file { |
|
269 |
+ my $self = shift; |
|
270 |
+ my ( $args, $content_type ) = @_; |
|
271 |
+ |
|
272 |
+ my $file = $args->{reqfile}; |
|
273 |
+ return $self->return_404 unless -e $file; |
|
274 |
+ |
|
275 |
+ my @stat = stat $file; |
|
276 |
+ my $res = $args->{req}->new_response(200); |
|
277 |
+ $res->headers( |
|
278 |
+ [ |
|
279 |
+ 'Content-Type' => $content_type, |
|
280 |
+ 'Last-Modified' => HTTP::Date::time2str( $stat[9] ), |
|
281 |
+ 'Expires' => 'Fri, 01 Jan 1980 00:00:00 GMT', |
|
282 |
+ 'Pragma' => 'no-cache', |
|
283 |
+ 'Cache-Control' => 'no-cache, max-age=0, must-revalidate', |
|
284 |
+ ] |
|
285 |
+ ); |
|
286 |
+ |
|
287 |
+ if ( $stat[7] ) { |
|
288 |
+ $res->header( 'Content-Length' => $stat[7] ); |
|
289 |
+ } |
|
290 |
+ open my $fh, "<:raw", $file |
|
291 |
+ or return $self->return_403; |
|
292 |
+ |
|
293 |
+ Plack::Util::set_io_path( $fh, Cwd::realpath($file) ); |
|
294 |
+ $res->body($fh); |
|
295 |
+ $res->finalize; |
|
296 |
+} |
|
297 |
+ |
|
298 |
+sub pkt_flush { |
|
299 |
+ my $self = shift; |
|
300 |
+ |
|
301 |
+ return '0000'; |
|
302 |
+} |
|
303 |
+ |
|
304 |
+sub pkt_write { |
|
305 |
+ my $self = shift; |
|
306 |
+ my $str = shift; |
|
307 |
+ return sprintf( '%04x', length($str) + 4 ) . $str; |
|
308 |
+} |
|
309 |
+ |
|
310 |
+sub return_not_allowed { |
|
311 |
+ my $self = shift; |
|
312 |
+ my $env = shift; |
|
313 |
+ if ( $env->{SERVER_PROTOCOL} eq 'HTTP/1.1' ) { |
|
314 |
+ return [ |
|
315 |
+ 405, [ 'Content-Type' => 'text/plain', 'Content-Length' => 18 ], |
|
316 |
+ ['Method Not Allowed'] |
|
317 |
+ ]; |
|
318 |
+ } |
|
319 |
+ else { |
|
320 |
+ return [ |
|
321 |
+ 400, [ 'Content-Type' => 'text/plain', 'Content-Length' => 11 ], |
|
322 |
+ ['Bad Request'] |
|
323 |
+ ]; |
|
324 |
+ } |
|
325 |
+} |
|
326 |
+ |
|
327 |
+sub return_403 { |
|
328 |
+ my $self = shift; |
|
329 |
+ return [ |
|
330 |
+ 403, [ 'Content-Type' => 'text/plain', 'Content-Length' => 9 ], |
|
331 |
+ ['Forbidden'] |
|
332 |
+ ]; |
|
333 |
+} |
|
334 |
+ |
|
335 |
+sub return_400 { |
|
336 |
+ my $self = shift; |
|
337 |
+ return [ |
|
338 |
+ 400, [ 'Content-Type' => 'text/plain', 'Content-Length' => 11 ], |
|
339 |
+ ['Bad Request'] |
|
340 |
+ ]; |
|
341 |
+} |
|
342 |
+ |
|
343 |
+sub return_404 { |
|
344 |
+ my $self = shift; |
|
345 |
+ return [ |
|
346 |
+ 404, [ 'Content-Type' => 'text/plain', 'Content-Length' => 9 ], |
|
347 |
+ ['Not Found'] |
|
348 |
+ ]; |
|
349 |
+} |
|
350 |
+ |
|
351 |
+1; |
|
352 |
+__END__ |
|
353 |
+ |
|
354 |
+=head1 NAME |
|
355 |
+ |
|
356 |
+ Plack::App::GitSmartHttp - Git Smart HTTP Server PSGI(Plack) Implementation |
|
357 |
+ |
|
358 |
+=head1 SYNOPSIS |
|
359 |
+ |
|
360 |
+ use Plack::App::GitSmartHttp; |
|
361 |
+ |
|
362 |
+ Plack::App::GitSmartHttp->new( |
|
363 |
+ root => '/var/git/repos', |
|
364 |
+ git_path => '/usr/bin/git', |
|
365 |
+ upload_pack => 1, |
|
366 |
+ received_pack => 1 |
|
367 |
+ )->to_app; |
|
368 |
+ |
|
369 |
+=head1 DESCRIPTION |
|
370 |
+ |
|
371 |
+ Plack::App::GitSmartHttp is Git Smart HTTP Server PSGI(Plack) Implementation. |
|
372 |
+ |
|
373 |
+=head1 AUTHOR |
|
374 |
+ |
|
375 |
+ Ryuzo Yamamoto E<lt>ryuzo.yamamoto@gmail.comE<gt> |
|
376 |
+ |
|
377 |
+=head1 SEE ALSO |
|
378 |
+ |
|
379 |
+ Smart HTTP Transport : <http://progit.org/2010/03/04/smart-http.html> |
|
380 |
+ Grack : <https://github.com/schacon/grack> |
|
381 |
+ |
|
382 |
+=head1 LICENSE |
|
383 |
+ |
|
384 |
+This library is free software; you can redistribute it and/or modify |
|
385 |
+it under the same terms as Perl itself. |
|
386 |
+ |
|
387 |
+=cut |
... | ... |
@@ -0,0 +1 @@ |
1 |
+receive-pack |
... | ... |
@@ -0,0 +1,6 @@ |
1 |
+<% |
|
2 |
+ use Data::Dumper; |
|
3 |
+ warn Dumper $self->req; |
|
4 |
+%> |
|
5 |
+ |
|
6 |
+upload-pack |