2 # dgit-repos-push-receiver
5 # .../dgit-repos-push-receiver KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
6 # .../dgit-repos-push-receiver 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 # KEYRING-AUTH-SPEC is a :-separated list of
15 # KEYRING.GPG,AUTH-SPEC
16 # where AUTH-SPEC is one of
23 # - extract the destination repo name
24 # - make a hardlink clone of the destination repo
25 # - provide the destination with a stunt pre-receive hook
26 # - run actual git-receive-pack with that new destination
27 # as a result of this the stunt pre-receive hook runs; it does this
28 # + understand what refs we are allegedly updating and
29 # check some correspondences:
30 # * we are updating only refs/tags/debian/* and refs/dgit/*
31 # * and only one of each
32 # * and the tag does not already exist
34 # * recovering the suite name from the destination refs/dgit/ ref
35 # + disassemble the signed tag into its various fields and signature
37 # * parsing the first line of the tag message to recover
38 # the package name, version and suite
39 # * checking that the package name corresponds to the dest repo name
40 # * checking that the suite name is as recovered above
41 # + verify the signature on the signed tag
42 # and if necessary check that the keyid and package are listed in dm.txt
43 # + check various correspondences:
44 # * the suite is one of those permitted
45 # * the signed tag must refer to a commit
46 # * the signed tag commit must be the refs/dgit value
47 # * the name in the signed tag must correspond to its ref name
48 # * the tag name must be debian/<version> (massaged as needed)
49 # * the signed tag has a suitable name
50 # * the commit is a fast forward
51 # + push the signed tag and new dgit branch to the actual repo
53 # If the destination repo does not already exist, we need to make
54 # sure that we create it reasonably atomically, and also that
55 # we don't every have a destination repo containing no refs at all
56 # (because such a thing causes git-fetch-pack to barf). So then we
57 # do as above, except:
58 # - before starting, we take out our own lock for the destination repo
59 # - we don't make a hardline clone of the destination repo; instead
60 # we make a copy (not a hardlink clone) of _template
61 # - we set up a post-receive hook as well, which does the following:
62 # + check that exactly two refs were updated
63 # + delete the two stunt hooks
64 # + rename the working repo into place as the destination repo
69 our $package_re = '[0-9a-z][-+.0-9a-z]+';
77 sub acquirelock ($$) {
78 my ($lock, $must) = @_;
80 my $fh = new IO::File, ">", $lock or die "open $lock: $!";
81 my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
84 die "flock $lock: $!";
90 my $want = (stat _)[1];
92 my $got = (stat _)[1];
93 return $fh if $got == $want;
97 sub makeworkingclone () {
98 $workrepo = "$dgitrepos/_tmp/${pkg}_incoming$$";
99 my $lock = "$workrepo.lock";
100 my $lockfh = acquirelock($lock, 1);
101 if (!stat $destrepo) {
102 $! == ENOENT or die "stat dest repo $destrepo: $!";
103 mkdir $workrepo or die "create work repo $workrepo: $!";
104 runcmd qw(git init --bare), $workrepo;
106 runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
110 sub setupstunthook () {
111 my $prerecv = "$workrepo/hooks/pre-receive";
112 my $fh = new IO::File, $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
113 or die "$prerecv: $!";
114 print $fh <<END or die "$prerecv: $!";
117 exec $0 --pre-receive-hook $pkg
119 close $fh or die "$prerecv: $!";
120 $ENV{'DGIT_RPR_WORK'}= $workrepo;
121 $ENV{'DGIT_RPR_DEST'}= $destrepo;
124 #----- stunt post-receive hook -----
126 our ($tagname, $tagval, $suite, $oldcommit, $commit);
127 our ($version, %tagh);
131 m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
132 my ($old, $sha1, $refname) = ($1, $2, $3);
133 if ($refname =~ m{^refs/tags/(?=debian/)}) {
134 die if defined $tagname;
137 reject "tag $tagname already exists -".
138 " not replacing previously-pushed version"
140 } elsif ($refname =~ m{^refs/dgit/}) {
141 die if defined $suite;
149 STDIN->error and die $!;
151 die unless defined $refname;
152 die unless defined $branchname;
156 open PT, ">dgit-tmp/plaintext" or die $!;
157 open DS, ">dgit-tmp/plaintext.asc" or die $!;
158 open T, "-|", qw(git cat-file tag), $tagval or die $!;
160 $!=0; $_=<T>; defined or die $!;
162 if (m/^(\S+) (.*)/) {
163 push @{ $tagh{$1} }, $2;
170 $!=0; $_=<T>; defined or die $!;
171 m/^($package_re) release (\S+) for (\S+) \[dgit\]$/ or die;
173 die unless $1 eq $pkg;
175 die unless $3 eq $suite;
179 $!=0; $_=<T>; defined or die $!;
180 last if m/^-----BEGIN PGP/;
192 sub checksig_keyring ($) {
193 my ($keyringfile) = @_;
194 # returns primary-keyid if signed by a key in this keyring
196 # or dies on other errors
200 open P, "-|", (qw(gpgv --status-fd=1),
201 map { '--keyring', $_ }, @keyrings,
202 qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext))
206 next unless s/^\[GNUPG:\]: //;
208 my @l = split / /, $_;
209 if ($l[0] eq 'NO_PUBKEY') {
211 } elsif ($l[0] eq 'VALIDSIG') {
213 $sigtype eq '00' or reject "signature is not of type 00!";
215 die unless defined $ok;
224 sub dm_txt_check ($$) {
225 my ($keyid, $dmtxtfn) = @_;
226 open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
228 m/^fingerprint:\s+$keyid$/oi
231 or reject "key $keyid missing Allow section in permissions!";
236 or reject "package $package not allowed for key $keyid";
240 foreach my $p (split /\s+/) {
241 return if $p eq $package; # yay!
244 DT->error and die $!;
246 reject "key $keyid not in permissions list although in keyring!";
250 foreach my $kas (split /:/, $keyrings) {
251 $kas =~ s/^([^,]+),// or die;
252 my $keyid = checksig_keyring $1;
253 if (defined $keyid) {
254 if ($kas =~ m/^a$/) {
256 } elsif ($kas =~ m/^m([^,]+)$/) {
257 dm_txt_check($keyid, $1);
264 reject "key not found in keyrings";
268 fixme check the suite against the approved list
269 tagh1('type') eq 'commit' or die;
270 tagh1('object') eq $commit or die;
271 tagh1('tag') eq $tagname or die;
275 $tagname eq "debian/$v" or die;
277 # check that our ref is being fast-forwarded
278 if ($oldcommit =~ m/[^0]/) {
279 $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
281 $mb eq $oldcommit or reject "not fast forward on dgit branch";
287 my $r = system (qw(git send-pack),
289 "$commit:refs/dgit/$suite",
290 "$tagval:refs/tags/$tagname");
291 !$r or die "onward push failed: $r $!";
295 chdir $workrepo or die "chdir $workrepo: $!";
296 mkdir "dgit-tmp" or $!==EEXIST or die $!;
304 #----- arg parsing and main program -----
309 if ($ARGV[0] eq '--pre-receive-hook') {
313 defined($workrepo = $ENV{'DGIT_RPR_WORK'}) or die;
314 defined($destrepo = $ENV{'DGIT_RPR_DEST'}) or die;
315 defined($keyrings = $ENV{'DGIT_RPR_KEYRINGS'}) or die $!;
316 open STDOUT, ">&STDERR" or die $!;
323 die if $ARGV[0] =~ m/^-/;
324 $ENV{'DGIT_RPR_KEYRINGS'} = shift @ARGV;
325 die if $ARGV[0] =~ m/^-/;
326 $dgitrepos = shift @ARGV;
329 if ($ARGV[0] != m/^-/) {
332 } elsif ($ARGV[0] eq '--ssh') {
335 my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
339 (git-receive-pack|git-upload-pack)
345 or die "requested command $cmd not understood";
350 $func = $main::{"main__$func"};
356 $destrepo = "$dgitrepos/$pkg.git";
359 sub main__git_receive_pack () {
364 runcmd qw(git receive-pack), $destdir;