chiark / gitweb /
Infra: ssh-wrap: export PERLLIB
[dgit.git] / infra / dgit-repos-server
1 #!/usr/bin/perl -w
2 # dgit-repos-server
3 #
4 # usages:
5 #   dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --ssh
6 #   dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --cron
7 # settings
8 #   --repos=GIT-REPOS-DIR      default DISTRO-DIR/repos/
9 #   --suites=SUITES-FILE       default DISTRO-DIR/suites
10 #   --policy-hook=POLICY-HOOK  default DISTRO-DIR/policy-hook
11 #   --dgit-live=DGIT-LIVE-DIR  default DISTRO-DIR/dgit-live
12 # (DISTRO-DIR is not used other than as default and to pass to policy hook)
13 # internal usage:
14 #  .../dgit-repos-server --pre-receive-hook PACKAGE
15 #
16 # Invoked as the ssh restricted command
17 #
18 # Works like git-receive-pack
19 #
20 # SUITES is the name of a file which lists the permissible suites
21 # one per line (#-comments and blank lines ignored)
22 #
23 # AUTH-SPEC is a :-separated list of
24 #   KEYRING.GPG,AUTH-SPEC
25 # where AUTH-SPEC is one of
26 #   a
27 #   mDM.TXT
28 # (With --cron AUTH-SPEC is not used and may be the empty string.)
29
30 use strict;
31 $SIG{__WARN__} = sub { die $_[0]; };
32
33 # DGIT-REPOS-DIR contains:
34 # git tree (or other object)      lock (in acquisition order, outer first)
35 #
36 #  _tmp/PACKAGE_prospective       ! } SAME.lock, held during receive-pack
37 #
38 #  _tmp/PACKAGE_incoming$$        ! } SAME.lock, held during receive-pack
39 #  _tmp/PACKAGE_incoming$$_fresh  ! }
40 #
41 #  PACKAGE.git                      } PACKAGE.git.lock
42 #  PACKAGE_garbage                  }   (also covers executions of
43 #  PACKAGE_garbage-old              }    policy hook script for PACKAGE)
44 #  PACKAGE_garbage-tmp              }
45 #  policy*                          } (for policy hook script, covered by
46 #                                   }  lock only when invoked for a package)
47 #
48 # leaf locks, held during brief operaton only:
49 #
50 #  _empty                           } SAME.lock
51 #  _empty.new                       }
52 #
53 #  _template                        } SAME.lock
54 #
55 # locks marked ! may be held during client data transfer
56
57 # What we do on push is this:
58 #  - extract the destination repo name
59 #  - make a hardlink clone of the destination repo
60 #  - provide the destination with a stunt pre-receive hook
61 #  - run actual git-receive-pack with that new destination
62 #   as a result of this the stunt pre-receive hook runs; it does this:
63 #    + understand what refs we are allegedly updating and
64 #      check some correspondences:
65 #        * we are updating only refs/tags/debian/* and refs/dgit/*
66 #        * and only one of each
67 #        * and the tag does not already exist
68 #      and
69 #        * recover the suite name from the destination refs/dgit/ ref
70 #    + disassemble the signed tag into its various fields and signature
71 #      including:
72 #        * parsing the first line of the tag message to recover
73 #          the package name, version and suite
74 #        * checking that the package name corresponds to the dest repo name
75 #        * checking that the suite name is as recovered above
76 #    + verify the signature on the signed tag
77 #      and if necessary check that the keyid and package are listed in dm.txt
78 #    + check various correspondences:
79 #        * the signed tag must refer to a commit
80 #        * the signed tag commit must be the refs/dgit value
81 #        * the name in the signed tag must correspond to its ref name
82 #        * the tag name must be debian/<version> (massaged as needed)
83 #        * the suite is one of those permitted
84 #        * the signed tag has a suitable name
85 #        * run the "push" policy hook
86 #        * replay prevention for --deliberately-not-fast-forward
87 #        * check the commit is a fast forward
88 #        * handle a request from the policy hook for a fresh repo
89 #    + push the signed tag and new dgit branch to the actual repo
90 #
91 # If the destination repo does not already exist, we need to make
92 # sure that we create it reasonably atomically, and also that
93 # we don't every have a destination repo containing no refs at all
94 # (because such a thing causes git-fetch-pack to barf).  So then we
95 # do as above, except:
96 #  - before starting, we take out our own lock for the destination repo
97 #  - we create a prospective new destination repo by making a copy
98 #    of _template
99 #  - we use the prospective new destination repo instead of the
100 #    actual new destination repo (since the latter doesn't exist)
101 #  - after git-receive-pack exits, we
102 #    + check that the prospective repo contains a tag and head
103 #    + rename the prospective destination repo into place
104 #
105 # Cleanup strategy:
106 #  - We are crash-only
107 #  - Temporary working trees and their locks are cleaned up
108 #    opportunistically by a program which tries to take each lock and
109 #    if successful deletes both the tree and the lockfile
110 #  - Prospective working trees and their locks are cleaned up by
111 #    a program which tries to take each lock and if successful
112 #    deletes any prospective working tree and the lock (but not
113 #    of course any actual tree)
114 #  - It is forbidden to _remove_ the lockfile without removing
115 #    the corresponding temporary tree, as the lockfile is also
116 #    a stampfile whose presence indicates that there may be
117 #    cleanup to do
118 #
119 # Policy hook script is invoked like this:
120 #   POLICY-HOOK-SCRIPT DISTRO DGIT-REPOS-DIR DGIT-LIVE-DIR DISTRO-DIR ACTION...
121 # ie.
122 #   POLICY-HOOK-SCRIPT ... check-list [...]
123 #   POLICY-HOOK-SCRIPT ... check-package PACKAGE [...]
124 #   POLICY-HOOK-SCRIPT ... push PACKAGE \
125 #         VERSION SUITE TAGNAME DELIBERATELIES [...]
126 #   POLICY-HOOK-SCRIPT ... push-confirm PACKAGE \
127 #         VERSION SUITE TAGNAME DELIBERATELIES FRESH-REPO|'' [...]
128 #
129 # DELIBERATELIES is like this: --deliberately-foo,--deliberately-bar,...
130 #
131 # Exit status is a bitmask.  Bit weight constants are defined in Dgit.pm.
132 #    NOFFCHECK   (2)
133 #         suppress dgit-repos-server's fast-forward check ("push" only)
134 #    FRESHREPO   (4)
135 #         blow away repo right away (ie, as if before push or fetch)
136 #         ("check-package" and "push" only)
137 # any unexpected bits mean failure, and then known set bits are ignored
138 # if no unexpected bits set, operation continues (subject to meaning
139 # of any expected bits set).  So, eg, exit 0 means "continue normally"
140 # and would be appropriate for an unknown action.
141 #
142 # cwd for push and push-confirm is a temporary repo where the incoming
143 # objects have been received; TAGNAME is the version-based tag.
144 #
145 # FRESH-REPO is '' iff the repo for this package already existed, or
146 # the pathname of the newly-created repo which will be renamed into
147 # place if everything goes well.  (NB that this is generally not the
148 # same repo as the cwd, because the objects are first received into a
149 # temporary repo so they can be examined.)  In this case FRESH-REPO
150 # contains exactly the objects and refs that will appear in the
151 # destination if push-confirm approves.
152
153 # if push requested FRESHREPO, push-confirm happens in the old working
154 # repo and FRESH-REPO is guaranteed not to be ''.
155 #
156 # policy hook for a particular package will be invoked only once at
157 # a time - (see comments about DGIT-REPOS-DIR, above)
158 #
159 # check-list and check-package are invoked via the --cron option.
160 # First, without any locking, check-list is called.  It should produce
161 # a list of package names (one per line).  Then check-package will be
162 # invoked for each named package, in each case after taking an
163 # appropriate lock.
164 #
165 # If policy hook wants to run dgit (or something else in the dgit
166 # package), it should use DGIT-LIVE-DIR/dgit (etc.), or if that is
167 # ENOENT, use the installed version.
168
169
170 use POSIX;
171 use Fcntl qw(:flock);
172 use File::Path qw(rmtree);
173 use File::Temp qw(tempfile);
174
175 use Debian::Dgit qw(:DEFAULT :policyflags);
176
177 initdebug('');
178
179 our $func;
180 our $dgitrepos;
181 our $package;
182 our $distro;
183 our $suitesfile;
184 our $policyhook;
185 our $dgitlive;
186 our $distrodir;
187 our $destrepo;
188 our $workrepo;
189 our $keyrings;
190 our @lockfhs;
191
192 our @deliberatelies;
193 our %previously;
194 our $policy;
195 our @policy_args;
196
197 #----- utilities -----
198
199 sub realdestrepo () { "$dgitrepos/$package.git"; }
200
201 sub acquirelock ($$) {
202     my ($lock, $must) = @_;
203     my $fh;
204     printdebug sprintf "locking %s %d\n", $lock, $must;
205     for (;;) {
206         close $fh if $fh;
207         $fh = new IO::File $lock, ">" or die "open $lock: $!";
208         my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
209         if (!$ok) {
210             die "flock $lock: $!" if $must;
211             printdebug " locking $lock failed\n";
212             return undef;
213         }
214         next unless stat_exists $lock;
215         my $want = (stat _)[1];
216         stat $fh or die $!;
217         my $got = (stat _)[1];
218         last if $got == $want;
219     }
220     return $fh;
221 }
222
223 sub acquirermtree ($$) {
224     my ($tree, $must) = @_;
225     my $fh = acquirelock("$tree.lock", $must);
226     if ($fh) {
227         push @lockfhs, $fh;
228         rmtree $tree;
229     }
230     return $fh;
231 }
232
233 sub locksometree ($) {
234     my ($tree) = @_;
235     acquirelock("$tree.lock", 1);
236 }
237
238 sub lockrealtree () {
239     locksometree(realdestrepo);
240 }
241
242 sub mkrepotmp () { ensuredir "$dgitrepos/_tmp" };
243
244 sub removedtagsfile () { "$dgitrepos/_removed-tags/$package"; }
245
246 sub recorderror ($) {
247     my ($why) = @_;
248     my $w = $ENV{'DGIT_DRS_WORK'}; # we are in stunthook
249     if (defined $w) {
250         chomp $why;
251         open ERR, ">", "$w/drs-error" or die $!;
252         print ERR $why, "\n" or die $!;
253         close ERR or die $!;
254         return 1;
255     }
256     return 0;
257 }
258
259 sub reject ($) {
260     my ($why) = @_;
261     recorderror "reject: $why";
262     die "dgit-repos-server: reject: $why\n";
263 }
264
265 sub runcmd {
266     debugcmd '+',@_;
267     $!=0; $?=0;
268     my $r = system @_;
269     die (shellquote @_)." $? $!" if $r;
270 }
271
272 sub policyhook {
273     my ($policyallowbits, @polargs) = @_;
274     # => ($exitstatuspolicybitmap);
275     die if $policyallowbits & ~0x3e;
276     my @cmd = ($policyhook,$distro,$dgitrepos,$dgitlive,$distrodir,@polargs);
277     debugcmd '+',@cmd;
278     my $r = system @cmd;
279     die "system: $!" if $r < 0;
280     die "dgit-repos-server: policy hook failed (or rejected) ($?)\n"
281         if $r & ~($policyallowbits << 8);
282     printdebug sprintf "hook => %#x\n", $r;
283     return $r >> 8;
284 }
285
286 sub mkemptyrepo ($$) {
287     my ($dir,$sharedperm) = @_;
288     runcmd qw(git init --bare --quiet), "--shared=$sharedperm", $dir;
289 }
290
291 sub mkrepo_fromtemplate ($) {
292     my ($dir) = @_;
293     my $template = "$dgitrepos/_template";
294     locksometree($template);
295     printdebug "copy template $template -> $dir\n";
296     my $r = system qw(cp -a --), $template, $dir;
297     !$r or die "create new repo $dir failed: $r $!";
298 }
299
300 sub movetogarbage () {
301     # realdestrepo must have been locked
302
303     my $real = realdestrepo;
304     return unless stat_exists $real;
305
306     my $garbagerepo = "$dgitrepos/${package}_garbage";
307     # We arrange to always keep at least one old tree, for recovery
308     # from mistakes.  This is either $garbage or $garbage-old.
309     if (stat_exists "$garbagerepo") {
310         printdebug "movetogarbage: rmtree $garbagerepo-tmp\n";
311         rmtree "$garbagerepo-tmp";
312         if (rename "$garbagerepo-old", "$garbagerepo-tmp") {
313             printdebug "movetogarbage: $garbagerepo-old -> -tmp, rmtree\n";
314             rmtree "$garbagerepo-tmp";
315         } else {
316             die "$garbagerepo $!" unless $!==ENOENT;
317             printdebug "movetogarbage: $garbagerepo-old -> -tmp\n";
318         }
319         printdebug "movetogarbage: $garbagerepo -> -old\n";
320         rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!";
321     }
322
323     ensuredir "$dgitrepos/_removed-tags";
324     open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!";
325     git_for_each_ref('refs/tags/'.debiantag('*'), sub {
326         my ($objid,$objtype,$fullrefname,$reftail) = @_;
327         print PREVIOUS "\n$objid $reftail .\n" or die $!;
328     }, $real);
329     close PREVIOUS or die $!;
330
331     printdebug "movetogarbage: $real -> $garbagerepo\n";
332     rename $real, $garbagerepo
333         or $! == ENOENT
334         or die "$garbagerepo $!";
335 }
336
337 sub policy_checkpackage () {
338     my $lfh = lockrealtree();
339
340     $policy = policyhook(FRESHREPO,'check-package',$package);
341     if ($policy & FRESHREPO) {
342         movetogarbage();
343     }
344
345     close $lfh;
346 }
347
348 #----- git-receive-pack -----
349
350 sub fixmissing__git_receive_pack () {
351     mkrepotmp();
352     $destrepo = "$dgitrepos/_tmp/${package}_prospective";
353     acquirermtree($destrepo, 1);
354     mkrepo_fromtemplate($destrepo);
355 }
356
357 sub makeworkingclone () {
358     mkrepotmp();
359     $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
360     acquirermtree($workrepo, 1);
361     my $lfh = lockrealtree();
362     runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
363     close $lfh;
364     rmtree "${workrepo}_fresh";
365 }
366
367 sub setupstunthook () {
368     my $prerecv = "$workrepo/hooks/pre-receive";
369     my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
370         or die "$prerecv: $!";
371     print $fh <<END or die "$prerecv: $!";
372 #!/bin/sh
373 set -e
374 exec $0 --pre-receive-hook $package
375 END
376     close $fh or die "$prerecv: $!";
377     $ENV{'DGIT_DRS_WORK'}= $workrepo;
378     $ENV{'DGIT_DRS_DEST'}= $destrepo;
379     printdebug " stunt hook set up $prerecv\n";
380 }
381
382 sub dealwithfreshrepo () {
383     my $freshrepo = "${workrepo}_fresh";
384     return unless stat_exists $freshrepo;
385     $destrepo = $freshrepo;
386 }
387
388 sub maybeinstallprospective () {
389     return if $destrepo eq realdestrepo;
390
391     if (open REJ, "<", "$workrepo/drs-error") {
392         local $/ = undef;
393         my $msg = <REJ>;
394         REJ->error and die $!;
395         print STDERR $msg;
396         exit 1;
397     } else {
398         $!==&ENOENT or die $!;
399     }
400
401     printdebug " show-ref ($destrepo) ...\n";
402
403     my $child = open SR, "-|";
404     defined $child or die $!;
405     if (!$child) {
406         chdir $destrepo or die $!;
407         exec qw(git show-ref);
408         die $!;
409     }
410     my %got = qw(tag 0 head 0);
411     while (<SR>) {
412         chomp or die;
413         printdebug " show-refs| $_\n";
414         s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
415         my $wh =
416             m{^refs/tags/} ? 'tag' :
417             m{^refs/dgit/} ? 'head' :
418             die;
419         die if $got{$wh}++;
420     }
421     $!=0; $?=0; close SR or $?==256 or die "$? $!";
422
423     printdebug "installprospective ?\n";
424     die Dumper(\%got)." -- missing refs in new repo"
425         if grep { !$_ } values %got;
426
427     lockrealtree();
428
429     if ($destrepo eq "${workrepo}_fresh") {
430         movetogarbage;
431     }
432
433     printdebug "install $destrepo => ".realdestrepo."\n";
434     rename $destrepo, realdestrepo or die $!;
435     remove realdestrepo.".lock" or die $!;
436 }
437
438 sub main__git_receive_pack () {
439     makeworkingclone();
440     setupstunthook();
441     runcmd qw(git receive-pack), $workrepo;
442     dealwithfreshrepo();
443     maybeinstallprospective();
444 }
445
446 #----- stunt post-receive hook -----
447
448 our ($tagname, $tagval, $suite, $oldcommit, $commit);
449 our ($version, %tagh);
450
451 our ($tagexists_error);
452
453 sub readupdates () {
454     printdebug " updates ...\n";
455     while (<STDIN>) {
456         chomp or die;
457         printdebug " upd.| $_\n";
458         m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
459         my ($old, $sha1, $refname) = ($1, $2, $3);
460         if ($refname =~ m{^refs/tags/(?=debian/)}) {
461             reject "pushing multiple tags!" if defined $tagname;
462             $tagname = $'; #';
463             $tagval = $sha1;
464             $tagexists_error= "tag $tagname already exists -".
465                 " not replacing previously-pushed version"
466                 if $old =~ m/[^0]/;
467         } elsif ($refname =~ m{^refs/dgit/}) {
468             reject "pushing multiple heads!" if defined $suite;
469             $suite = $'; #';
470             $oldcommit = $old;
471             $commit = $sha1;
472         } else {
473             reject "pushing unexpected ref!";
474         }
475     }
476     STDIN->error and die $!;
477
478     reject "push is missing tag ref update" unless defined $tagname;
479     reject "push is missing head ref update" unless defined $suite;
480     printdebug " updates ok.\n";
481 }
482
483 sub parsetag () {
484     printdebug " parsetag...\n";
485     open PT, ">dgit-tmp/plaintext" or die $!;
486     open DS, ">dgit-tmp/plaintext.asc" or die $!;
487     open T, "-|", qw(git cat-file tag), $tagval or die $!;
488     for (;;) {
489         $!=0; $_=<T>; defined or die $!;
490         print PT or die $!;
491         if (m/^(\S+) (.*)/) {
492             push @{ $tagh{$1} }, $2;
493         } elsif (!m/\S/) {
494             last;
495         } else {
496             die;
497         }
498     }
499     $!=0; $_=<T>; defined or die $!;
500     m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
501         reject "tag message not in expected format";
502
503     die unless $1 eq $package;
504     $version = $2;
505     die "$3 != $suite " unless $3 eq $suite;
506
507     my $copyl = $_;
508     for (;;) {
509         print PT $copyl or die $!;
510         $!=0; $_=<T>; defined or die "missing signature? $!";
511         $copyl = $_;
512         if (m/^\[dgit ([^"].*)\]$/) { # [dgit "something"] is for future
513             $_ = $1." ";
514             while (length) {
515                 if (s/^distro\=(\S+) //) {
516                     die "$1 != $distro" unless $1 eq $distro;
517                 } elsif (s/^(--deliberately-$deliberately_re) //) {
518                     push @deliberatelies, $1;
519                 } elsif (s/^previously:(\S+)=(\w+) //) {
520                     die "previously $1 twice" if defined $previously{$1};
521                     $previously{$1} = $2;
522                 } elsif (s/^[-+.=0-9a-z]\S* //) {
523                 } else {
524                     die "unknown dgit info in tag ($_)";
525                 }
526             }
527             next;
528         }
529         last if m/^-----BEGIN PGP/;
530     }
531     $_ = $copyl;
532     for (;;) {
533         print DS or die $!;
534         $!=0; $_=<T>;
535         last if !defined;
536     }
537     T->error and die $!;
538     close PT or die $!;
539     close DS or die $!;
540     printdebug " parsetag ok.\n";
541 }
542
543 sub checksig_keyring ($) {
544     my ($keyringfile) = @_;
545     # returns primary-keyid if signed by a key in this keyring
546     # or undef if not
547     # or dies on other errors
548
549     my $ok = undef;
550
551     printdebug " checksig keyring $keyringfile...\n";
552
553     our @cmd = (qw(gpgv --status-fd=1 --keyring),
554                    $keyringfile,
555                    qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
556     debugcmd '|',@cmd;
557
558     open P, "-|", @cmd
559         or die $!;
560
561     while (<P>) {
562         next unless s/^\[GNUPG:\] //;
563         chomp or die;
564         printdebug " checksig| $_\n";
565         my @l = split / /, $_;
566         if ($l[0] eq 'NO_PUBKEY') {
567             last;
568         } elsif ($l[0] eq 'VALIDSIG') {
569             my $sigtype = $l[9];
570             $sigtype eq '00' or reject "signature is not of type 00!";
571             $ok = $l[10];
572             die unless defined $ok;
573             last;
574         }
575     }
576     close P;
577
578     printdebug sprintf " checksig ok=%d\n", !!$ok;
579
580     return $ok;
581 }
582
583 sub dm_txt_check ($$) {
584     my ($keyid, $dmtxtfn) = @_;
585     printdebug " dm_txt_check $keyid $dmtxtfn\n";
586     open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
587     while (<DT>) {
588         m/^fingerprint:\s+$keyid$/oi
589             ..0 or next;
590         if (s/^allow:/ /i..0) {
591         } else {
592             m/^./
593                 or reject "key $keyid missing Allow section in permissions!";
594             next;
595         }
596         # in right stanza...
597         s/^[ \t]+//
598             or reject "package $package not allowed for key $keyid";
599         # in allow field...
600         s/\([^()]+\)//;
601         s/\,//;
602         chomp or die;
603         printdebug " dm_txt_check allow| $_\n";
604         foreach my $p (split /\s+/) {
605             if ($p eq $package) {
606                 # yay!
607                 printdebug " dm_txt_check ok\n";
608                 return;
609             }
610         }
611     }
612     DT->error and die $!;
613     close DT or die $!;
614     reject "key $keyid not in permissions list although in keyring!";
615 }
616
617 sub verifytag () {
618     foreach my $kas (split /:/, $keyrings) {
619         printdebug "verifytag $kas...\n";
620         $kas =~ s/^([^,]+),// or die;
621         my $keyid = checksig_keyring $1;
622         if (defined $keyid) {
623             if ($kas =~ m/^a$/) {
624                 printdebug "verifytag a ok\n";
625                 return; # yay
626             } elsif ($kas =~ m/^m([^,]+)$/) {
627                 dm_txt_check($keyid, $1);
628                 printdebug "verifytag m ok\n";
629                 return;
630             } else {
631                 die;
632             }
633         }   
634     }
635     reject "key not found in keyrings";
636 }
637
638 sub checksuite () {
639     printdebug "checksuite ($suitesfile)\n";
640     open SUITES, "<", $suitesfile or die $!;
641     while (<SUITES>) {
642         chomp;
643         next unless m/\S/;
644         next if m/^\#/;
645         s/\s+$//;
646         return if $_ eq $suite;
647     }
648     die $! if SUITES->error;
649     reject "unknown suite";
650 }
651
652 sub checktagnoreplay () {
653     # We need to prevent a replay attack using an earlier signed tag.
654     # We also want to archive in the history the object ids of
655     # anything we remove, even if we get rid of the actual objects.
656     #
657     # So, we check that the signed tag mentions the name and tag
658     # object id of:
659     #
660     # (a) In the case of FRESHREPO: all tags and refs/heads/* in
661     #     the repo.  That is, effectively, all the things we are
662     #     deleting.
663     #
664     #     This prevents any tag implying a FRESHREPO push
665     #     being replayed into a different state of the repo.
666     #
667     #     There is still the folowing risk: If a non-ff push is of a
668     #     head which is an ancestor of a previous ff-only push, the
669     #     previous push can be replayed.
670     #
671     #     So we keep a separate list, as a file in the repo, of all
672     #     the tag object ids we have ever seen and removed.  Any such
673     #     tag object id will be rejected even for ff-only pushes.
674     #
675     # (b) In the case of just NOFFCHECK: all tags referring to the
676     #     current head for the suite (there must be at least one).
677     #
678     #     This prevents any tag implying a NOFFCHECK push being
679     #     replayed to rewind from a different head.
680     #
681     #     The possibility of an earlier ff-only push being replayed is
682     #     eliminated as follows: the tag from such a push would still
683     #     be in our repo, and therefore the replayed push would be
684     #     rejected because the set of refs being updated would be
685     #     wrong.
686
687     if (!open PREVIOUS, "<", removedtagsfile) {
688         die removedtagsfile." $!" unless $!==ENOENT;
689     } else {
690         # Protocol for updating this file is to append to it, not
691         # write-new-and-rename.  So all updates are prefixed with \n
692         # and suffixed with " .\n" so that partial writes can be
693         # ignored.
694         while (<PREVIOUS>) {
695             next unless m/^(\w+) (.*) \.\n/;
696             next unless $1 eq $tagval;
697             reject "Replay of previously-rewound upload ($tagval $2)";
698         }
699         die removedtagsfile." $!" if PREVIOUS->error;
700         close PREVIOUS;
701     }
702
703     return unless $policy & (FRESHREPO|NOFFCHECK);
704
705     my $garbagerepo = "$dgitrepos/${package}_garbage";
706     lockrealtree();
707
708     my $nchecked = 0;
709     my @problems;
710
711     my $check_ref_previously= sub {
712         my ($objid,$objtype,$fullrefname,$reftail) = @_;
713         my $supkey = $fullrefname;
714         $supkey =~ s{^refs/}{} or die "$supkey $objid ?";
715         my $supobjid = $previously{$supkey};
716         if (!defined $supobjid) {
717             printdebug "checktagnoreply - missing\n";
718             push @problems, "does not declare previously $supkey";
719         } elsif ($supobjid ne $objid) {
720             push @problems, "declared previously $supkey=$supobjid".
721                 " but actually previously $supkey=$objid";
722         } else {
723             $nchecked++;
724         }
725     };
726
727     if ($policy & FRESHREPO) {
728         foreach my $kind (qw(tags heads)) {
729             git_for_each_ref("refs/$kind", $check_ref_previously);
730         }
731     } else {
732         my $branch= server_branch($suite);
733         my $branchhead= git_get_ref(server_ref($suite));
734         if (!length $branchhead) {
735             # No such branch - NOFFCHECK was unnecessary.  Oh well.
736             printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n";
737         } else {
738             printdebug "checktagnoreplay - not FRESHREPO,".
739                 " checking for overwriting refs/$branch=$branchhead\n";
740             git_for_each_tag_referring($branchhead, sub {
741                 my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_;
742                 $check_ref_previously->($tagobjid,undef,$fullrefname,undef);
743             });
744             printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked";
745             push @problems, "does not declare previously any tag".
746                 " referring to branch head $branch=$branchhead"
747                 unless $nchecked;
748         }
749     }
750
751     if (@problems) {
752         reject "replay attack prevention check failed:".
753             " signed tag for $version: ".
754             join("; ", @problems).
755             "\n";
756     }
757     printdebug "checktagnoreplay - all ok ($tagval)\n"
758 }
759
760 sub tagh1 ($) {
761     my ($tag) = @_;
762     my $vals = $tagh{$tag};
763     reject "missing header $tag in signed tag object" unless $vals;
764     reject "multiple headers $tag in signed tag object" unless @$vals == 1;
765     return $vals->[0];
766 }
767
768 sub checks () {
769     printdebug "checks\n";
770
771     tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
772     tagh1('object') eq $commit or reject "tag refers to wrong commit";
773     tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
774
775     my $v = $version;
776     $v =~ y/~:/_%/;
777
778     printdebug "translated version $v\n";
779     $tagname eq "debian/$v" or die;
780
781     lockrealtree();
782
783     @policy_args = ($package,$version,$suite,$tagname,
784                     join(",",@deliberatelies));
785     $policy = policyhook(NOFFCHECK|FRESHREPO, 'push', @policy_args);
786
787     if (defined $tagexists_error) {
788         if ($policy & FRESHREPO) {
789             printdebug "ignoring tagexists_error: $tagexists_error\n";
790         } else {
791             reject $tagexists_error;
792         }
793     }
794
795     checktagnoreplay();
796     checksuite();
797
798     # check that our ref is being fast-forwarded
799     printdebug "oldcommit $oldcommit\n";
800     if (!($policy & NOFFCHECK) && $oldcommit =~ m/[^0]/) {
801         $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
802         chomp $mb;
803         $mb eq $oldcommit or reject "not fast forward on dgit branch";
804     }
805     if ($policy & FRESHREPO) {
806         # It's a bit late to be discovering this here, isn't it ?
807         #
808         # What we do is: Generate a fresh destination repo right now,
809         # and arrange to treat it from now on as if it were a
810         # prospective repo.
811         #
812         # The presence of this fresh destination repo is detected by
813         # the parent, which responds by making a fresh master repo
814         # from the template.  (If the repo didn't already exist then
815         # $destrepo was _prospective, and we change it here.  This is
816         # OK because the parent's check for _fresh persuades it not to
817         # use _prospective.)
818         #
819         $destrepo = "${workrepo}_fresh"; # workrepo lock covers
820         mkrepo_fromtemplate $destrepo;
821     }
822 }
823
824 sub onwardpush () {
825     my @cmd = (qw(git send-pack), $destrepo);
826     push @cmd, qw(--force) if $policy & NOFFCHECK;
827     push @cmd, "$commit:refs/dgit/$suite",
828                "$tagval:refs/tags/$tagname";
829     debugcmd '+',@cmd;
830     $!=0;
831     my $r = system @cmd;
832     !$r or die "onward push to $destrepo failed: $r $!";
833 }
834
835 sub finalisepush () {
836     if ($destrepo eq realdestrepo) {
837         policyhook(0, 'push-confirm', @policy_args, '');
838         onwardpush();
839     } else {
840         # We are to receive the push into a new repo (perhaps
841         # because the policy push hook asked us to with FRESHREPO, or
842         # perhaps because the repo didn't exist before).
843         #
844         # We want to provide the policy push-confirm hook with a repo
845         # which looks like the one which is going to be installed.
846         # The working repo is no good because it might contain
847         # previous history.
848         #
849         # So we push the objects into the prospective new repo right
850         # away.  If the hook declines, we decline, and the prospective
851         # repo is never installed.
852         onwardpush();
853         policyhook(0, 'push-confirm', @policy_args, $destrepo);
854     }
855 }
856
857 sub stunthook () {
858     printdebug "stunthook in $workrepo\n";
859     chdir $workrepo or die "chdir $workrepo: $!";
860     mkdir "dgit-tmp" or $!==EEXIST or die $!;
861     readupdates();
862     parsetag();
863     verifytag();
864     checks();
865     finalisepush();
866     printdebug "stunthook done.\n";
867 }
868
869 #----- git-upload-pack -----
870
871 sub fixmissing__git_upload_pack () {
872     $destrepo = "$dgitrepos/_empty";
873     my $lfh = locksometree($destrepo);
874     return if stat_exists $destrepo;
875     rmtree "$destrepo.new";
876     mkemptyrepo "$destrepo.new", "0644";
877     rename "$destrepo.new", $destrepo or die $!;
878     unlink "$destrepo.lock" or die $!;
879     close $lfh;
880 }
881
882 sub main__git_upload_pack () {
883     my $lfh = locksometree($destrepo);
884     printdebug "git-upload-pack in $destrepo\n";
885     chdir $destrepo or die "$destrepo: $!";
886     close $lfh;
887     runcmd qw(git upload-pack), ".";
888 }
889
890 #----- arg parsing and main program -----
891
892 sub argval () {
893     die unless @ARGV;
894     my $v = shift @ARGV;
895     die if $v =~ m/^-/;
896     return $v;
897 }
898
899 our %indistrodir = (
900     # keys are used for DGIT_DRS_XXX too
901     'repos' => \$dgitrepos,
902     'suites' => \$suitesfile,
903     'policy-hook' => \$policyhook,
904     'dgit-live' => \$dgitlive,
905     );
906
907 our @hookenvs = qw(distro suitesfile policyhook
908                    dgitlive keyrings dgitrepos distrodir);
909
910 # workrepo and destrepo handled ad-hoc
911
912 sub mode_ssh () {
913     die if @ARGV;
914
915     my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
916     $cmd =~ m{
917         ^
918         (?: \S* / )?
919         ( [-0-9a-z]+ )
920         \s+
921         '? (?: \S* / )?
922         ($package_re) \.git
923         '?$
924     }ox 
925     or reject "command string not understood";
926     my $method = $1;
927     $package = $2;
928
929     my $funcn = $method;
930     $funcn =~ y/-/_/;
931     my $mainfunc = $main::{"main__$funcn"};
932
933     reject "unknown method" unless $mainfunc;
934
935     policy_checkpackage();
936
937     if (stat_exists realdestrepo) {
938         $destrepo = realdestrepo;
939     } else {
940         printdebug " fixmissing $funcn\n";
941         my $fixfunc = $main::{"fixmissing__$funcn"};
942         &$fixfunc;
943     }
944
945     printdebug " running main $funcn\n";
946     &$mainfunc;
947 }
948
949 sub mode_cron () {
950     die if @ARGV;
951
952     my $listfh = tempfile();
953     open STDOUT, ">&", $listfh or die $!;
954     policyhook(0,'check-list');
955     open STDOUT, ">&STDERR" or die $!;
956
957     seek $listfh, 0, 0 or die $!;
958     while (<$listfh>) {
959         chomp or die;
960         next if m/^\s*\#/;
961         next unless m/\S/;
962         die unless m/^($package_re)$/;
963         
964         $package = $1;
965         policy_checkpackage();
966     }
967     die $! if $listfh->error;
968 }    
969
970 sub parseargsdispatch () {
971     die unless @ARGV;
972
973     delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
974     delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
975
976     if ($ENV{'DGIT_DRS_DEBUG'}) {
977         enabledebug();
978     }
979
980     if ($ARGV[0] eq '--pre-receive-hook') {
981         if ($debuglevel) {
982             $debugprefix.="=";
983             printdebug "in stunthook ".(shellquote @ARGV)."\n";
984             foreach my $k (sort keys %ENV) {
985                 printdebug "$k=$ENV{$k}\n" if $k =~  m/^DGIT/;
986             }
987         }
988         shift @ARGV;
989         @ARGV == 1 or die;
990         $package = shift @ARGV;
991         ${ $main::{$_} } = $ENV{"DGIT_DRS_\U$_"} foreach @hookenvs;
992         defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
993         defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
994         open STDOUT, ">&STDERR" or die $!;
995         eval {
996             stunthook();
997         };
998         if ($@) {
999             recorderror "$@" or die;
1000             die $@;
1001         }
1002         exit 0;
1003     }
1004
1005     $distro    = argval();
1006     $distrodir = argval();
1007     $keyrings  = argval();
1008
1009     foreach my $dk (keys %indistrodir) {
1010         ${ $indistrodir{$dk} } = "$distrodir/$dk";
1011     }
1012
1013     while (@ARGV && $ARGV[0] =~ m/^--([-0-9a-z]+)=/ && $indistrodir{$1}) {
1014         ${ $indistrodir{$1} } = $'; #';
1015         shift @ARGV;
1016     }
1017
1018     $ENV{"DGIT_DRS_\U$_"} = ${ $main::{$_} } foreach @hookenvs;
1019
1020     die unless @ARGV==1;
1021
1022     my $mode = shift @ARGV;
1023     die unless $mode =~ m/^--(\w+)$/;
1024     my $fn = ${*::}{"mode_$1"};
1025     die unless $fn;
1026     $fn->();
1027 }
1028
1029 sub unlockall () {
1030     while (my $fh = pop @lockfhs) { close $fh; }
1031 }
1032
1033 sub cleanup () {
1034     unlockall();
1035     if (!chdir "$dgitrepos/_tmp") {
1036         $!==ENOENT or die $!;
1037         return;
1038     }
1039     foreach my $lf (<*.lock>) {
1040         my $tree = $lf;
1041         $tree =~ s/\.lock$//;
1042         next unless acquirermtree($tree, 0);
1043         remove $lf or warn $!;
1044         unlockall();
1045     }
1046 }
1047
1048 parseargsdispatch();
1049 cleanup();