Showing 4 changed files with 473 additions and 2 deletions
+79 -2
lib/Gitprep.pm
... ...
@@ -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);
+387
lib/Gitprep/SmartHTTP.pm
... ...
@@ -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
+1
templates/receive-pack.html.ep
... ...
@@ -0,0 +1 @@
1
+receive-pack
+6
templates/upload-pack.html.ep
... ...
@@ -0,0 +1,6 @@
1
+<%
2
+  use Data::Dumper;
3
+  warn Dumper $self->req;
4
+%>
5
+
6
+upload-pack