2 # dgit-repos-push-receiver
5 # .../dgit-repos-push-receiver SUITES KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
6 # .../dgit-repos-push-receiver SUITES KEYRING-AUTH-SPEC DGIT-REPOS-DIR PACKAGE
8 # .../dgit-repos-push-receiver --pre-receive-hook PACKAGE
10 # Invoked as the ssh restricted command
12 # Works like git-receive-pack
14 # SUITES is the name of a file which lists the permissible suites
15 # one per line (#-comments and blank lines ignored)
17 # KEYRING-AUTH-SPEC is a :-separated list of
18 # KEYRING.GPG,AUTH-SPEC
19 # where AUTH-SPEC is one of
26 # - extract the destination repo name
27 # - make a hardlink clone of the destination repo
28 # - provide the destination with a stunt pre-receive hook
29 # - run actual git-receive-pack with that new destination
30 # as a result of this the stunt pre-receive hook runs; it does this
31 # + understand what refs we are allegedly updating and
32 # check some correspondences:
33 # * we are updating only refs/tags/debian/* and refs/dgit/*
34 # * and only one of each
35 # * and the tag does not already exist
37 # * recovering the suite name from the destination refs/dgit/ ref
38 # + disassemble the signed tag into its various fields and signature
40 # * parsing the first line of the tag message to recover
41 # the package name, version and suite
42 # * checking that the package name corresponds to the dest repo name
43 # * checking that the suite name is as recovered above
44 # + verify the signature on the signed tag
45 # and if necessary check that the keyid and package are listed in dm.txt
46 # + check various correspondences:
47 # * the suite is one of those permitted
48 # * the signed tag must refer to a commit
49 # * the signed tag commit must be the refs/dgit value
50 # * the name in the signed tag must correspond to its ref name
51 # * the tag name must be debian/<version> (massaged as needed)
52 # * the signed tag has a suitable name
53 # * the commit is a fast forward
54 # + push the signed tag and new dgit branch to the actual repo
56 # If the destination repo does not already exist, we need to make
57 # sure that we create it reasonably atomically, and also that
58 # we don't every have a destination repo containing no refs at all
59 # (because such a thing causes git-fetch-pack to barf). So then we
60 # do as above, except:
61 # - before starting, we take out our own lock for the destination repo
62 # - we create a prospective new destination repo by making a copy
64 # - we use the prospective new destination repo instead of the
65 # actual new destination repo (since the latter doesn't exist)
66 # - we set up a post-receive hook as well, which
67 # + checks that exactly two refs were updated
68 # + touches a stamp file
69 # - after git-receive-pack exits, we rename the prospective
70 # destination repo into place
74 # - Temporary working trees and their locks are cleaned up
75 # opportunistically by a program which tries to take each lock and
76 # if successful deletes both the tree and the lockfile
77 # - Prospective working trees and their locks are cleaned up by
78 # a program which tries to take each lock and if successful
79 # deletes any prospective working tree and the lock (but not
80 # of course any actual tree)
81 # - It is forbidden to _remove_ the lockfile without removing
82 # the corresponding temporary tree, as the lockfile is also
83 # a stampfile whose presence indicates that there may be
89 our $package_re = '[0-9a-z][-+.0-9a-z]+';
97 sub acquirelock ($$) {
98 my ($lock, $must) = @_;
100 my $fh = new IO::File, ">", $lock or die "open $lock: $!";
101 my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
104 die "flock $lock: $!";
107 next if $! == ENOENT;
108 die "stat $lock: $!";
110 my $want = (stat _)[1];
112 my $got = (stat _)[1];
113 return $fh if $got == $want;
117 sub makeworkingclone () {
118 $workrepo = "$dgitrepos/_tmp/${pkg}_incoming$$";
119 my $lock = "$workrepo.lock";
120 my $lockfh = acquirelock($lock, 1);
121 if (!stat $destrepo) {
122 $! == ENOENT or die "stat dest repo $destrepo: $!";
123 mkdir $workrepo or die "create work repo $workrepo: $!";
124 runcmd qw(git init --bare), $workrepo;
126 runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
130 sub setupstunthook () {
131 my $prerecv = "$workrepo/hooks/pre-receive";
132 my $fh = new IO::File, $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
133 or die "$prerecv: $!";
134 print $fh <<END or die "$prerecv: $!";
137 exec $0 --pre-receive-hook $pkg
139 close $fh or die "$prerecv: $!";
140 $ENV{'DGIT_RPR_WORK'}= $workrepo;
141 $ENV{'DGIT_RPR_DEST'}= $destrepo;
144 #----- stunt post-receive hook -----
146 our ($tagname, $tagval, $suite, $oldcommit, $commit);
147 our ($version, %tagh);
151 m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
152 my ($old, $sha1, $refname) = ($1, $2, $3);
153 if ($refname =~ m{^refs/tags/(?=debian/)}) {
154 die if defined $tagname;
157 reject "tag $tagname already exists -".
158 " not replacing previously-pushed version"
160 } elsif ($refname =~ m{^refs/dgit/}) {
161 die if defined $suite;
169 STDIN->error and die $!;
171 die unless defined $refname;
172 die unless defined $branchname;
176 open PT, ">dgit-tmp/plaintext" or die $!;
177 open DS, ">dgit-tmp/plaintext.asc" or die $!;
178 open T, "-|", qw(git cat-file tag), $tagval or die $!;
180 $!=0; $_=<T>; defined or die $!;
182 if (m/^(\S+) (.*)/) {
183 push @{ $tagh{$1} }, $2;
190 $!=0; $_=<T>; defined or die $!;
191 m/^($package_re) release (\S+) for (\S+) \[dgit\]$/ or die;
193 die unless $1 eq $pkg;
195 die unless $3 eq $suite;
199 $!=0; $_=<T>; defined or die $!;
200 last if m/^-----BEGIN PGP/;
212 sub checksig_keyring ($) {
213 my ($keyringfile) = @_;
214 # returns primary-keyid if signed by a key in this keyring
216 # or dies on other errors
220 open P, "-|", (qw(gpgv --status-fd=1),
221 map { '--keyring', $_ }, @keyrings,
222 qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext))
226 next unless s/^\[GNUPG:\]: //;
228 my @l = split / /, $_;
229 if ($l[0] eq 'NO_PUBKEY') {
231 } elsif ($l[0] eq 'VALIDSIG') {
233 $sigtype eq '00' or reject "signature is not of type 00!";
235 die unless defined $ok;
244 sub dm_txt_check ($$) {
245 my ($keyid, $dmtxtfn) = @_;
246 open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
248 m/^fingerprint:\s+$keyid$/oi
251 or reject "key $keyid missing Allow section in permissions!";
256 or reject "package $package not allowed for key $keyid";
260 foreach my $p (split /\s+/) {
261 return if $p eq $package; # yay!
264 DT->error and die $!;
266 reject "key $keyid not in permissions list although in keyring!";
270 foreach my $kas (split /:/, $keyrings) {
271 $kas =~ s/^([^,]+),// or die;
272 my $keyid = checksig_keyring $1;
273 if (defined $keyid) {
274 if ($kas =~ m/^a$/) {
276 } elsif ($kas =~ m/^m([^,]+)$/) {
277 dm_txt_check($keyid, $1);
284 reject "key not found in keyrings";
288 fixme check the suite against the approved list
289 tagh1('type') eq 'commit' or die;
290 tagh1('object') eq $commit or die;
291 tagh1('tag') eq $tagname or die;
295 $tagname eq "debian/$v" or die;
297 # check that our ref is being fast-forwarded
298 if ($oldcommit =~ m/[^0]/) {
299 $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
301 $mb eq $oldcommit or reject "not fast forward on dgit branch";
307 my $r = system (qw(git send-pack),
309 "$commit:refs/dgit/$suite",
310 "$tagval:refs/tags/$tagname");
311 !$r or die "onward push failed: $r $!";
315 chdir $workrepo or die "chdir $workrepo: $!";
316 mkdir "dgit-tmp" or $!==EEXIST or die $!;
324 #----- arg parsing and main program -----
329 if ($ARGV[0] eq '--pre-receive-hook') {
333 defined($workrepo = $ENV{'DGIT_RPR_WORK'}) or die;
334 defined($destrepo = $ENV{'DGIT_RPR_DEST'}) or die;
335 defined($keyrings = $ENV{'DGIT_RPR_KEYRINGS'}) or die $!;
336 open STDOUT, ">&STDERR" or die $!;
343 die if $ARGV[0] =~ m/^-/;
344 $suitesfile = shift @ARGV;
346 die if $ARGV[0] =~ m/^-/;
347 $ENV{'DGIT_RPR_KEYRINGS'} = shift @ARGV;
349 die if $ARGV[0] =~ m/^-/;
350 $dgitrepos = shift @ARGV;
353 if ($ARGV[0] != m/^-/) {
356 } elsif ($ARGV[0] eq '--ssh') {
359 my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
363 (git-receive-pack|git-upload-pack)
369 or die "requested command $cmd not understood";
374 $func = $main::{"main__$func"};
380 $destrepo = "$dgitrepos/$pkg.git";
383 sub main__git_receive_pack () {
388 runcmd qw(git receive-pack), $destdir;