#!/usr/bin/perl -w
-# dgit-repos-push-receiver
+# dgit-repos-server
#
# usages:
-# .../dgit-repos-push-receiver KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
-# .../dgit-repos-push-receiver KEYRING-AUTH-SPEC DGIT-REPOS-DIR PACKAGE
+# .../dgit-repos-server SUITES KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
# internal usage:
-# .../dgit-repos-push-receiver --pre-receive-hook PACKAGE
+# .../dgit-repos-server --pre-receive-hook PACKAGE
#
# Invoked as the ssh restricted command
#
# Works like git-receive-pack
#
+# SUITES is the name of a file which lists the permissible suites
+# one per line (#-comments and blank lines ignored)
+#
# KEYRING-AUTH-SPEC is a :-separated list of
# KEYRING.GPG,AUTH-SPEC
# where AUTH-SPEC is one of
use strict;
# What we do is this:
-# - extract the destination repo name somehow
+# - extract the destination repo name
# - make a hardlink clone of the destination repo
# - provide the destination with a stunt pre-receive hook
# - run actual git-receive-pack with that new destination
-# as a result of this the stunt pre-receive hook runs; it does this
-# find the keyring(s) to use for verification
-# verify the signed tag
-# check that the signed tag has a suitable name
-# parse the signed tag body to extract the intended
-# distro and suite
-# check that the distro is right
-# check that the suite is the same as the branch we are
-# supposed to update
-# check that the signed tag refers to the same commit
-# as the new suite
-# check that the signer was correct
-# push the signed tag to the actual repo
-# push the new dgit branch head to the actual repo
+# as a result of this the stunt pre-receive hook runs; it does this:
+# + understand what refs we are allegedly updating and
+# check some correspondences:
+# * we are updating only refs/tags/debian/* and refs/dgit/*
+# * and only one of each
+# * and the tag does not already exist
+# and
+# * recover the suite name from the destination refs/dgit/ ref
+# + disassemble the signed tag into its various fields and signature
+# including:
+# * parsing the first line of the tag message to recover
+# the package name, version and suite
+# * checking that the package name corresponds to the dest repo name
+# * checking that the suite name is as recovered above
+# + verify the signature on the signed tag
+# and if necessary check that the keyid and package are listed in dm.txt
+# + check various correspondences:
+# * the suite is one of those permitted
+# * the signed tag must refer to a commit
+# * the signed tag commit must be the refs/dgit value
+# * the name in the signed tag must correspond to its ref name
+# * the tag name must be debian/<version> (massaged as needed)
+# * the signed tag has a suitable name
+# * the commit is a fast forward
+# + push the signed tag and new dgit branch to the actual repo
+#
+# If the destination repo does not already exist, we need to make
+# sure that we create it reasonably atomically, and also that
+# we don't every have a destination repo containing no refs at all
+# (because such a thing causes git-fetch-pack to barf). So then we
+# do as above, except:
+# - before starting, we take out our own lock for the destination repo
+# - we create a prospective new destination repo by making a copy
+# of _template
+# - we use the prospective new destination repo instead of the
+# actual new destination repo (since the latter doesn't exist)
+# - we set up a post-receive hook as well, which
+# + touches a stamp file
+# - after git-receive-pack exits, we
+# + check that the prospective repo contains a tag and head
+# + rename the prospective destination repo into place
+#
+# Cleanup strategy:
+# - We are crash-only
+# - Temporary working trees and their locks are cleaned up
+# opportunistically by a program which tries to take each lock and
+# if successful deletes both the tree and the lockfile
+# - Prospective working trees and their locks are cleaned up by
+# a program which tries to take each lock and if successful
+# deletes any prospective working tree and the lock (but not
+# of course any actual tree)
+# - It is forbidden to _remove_ the lockfile without removing
+# the corresponding temporary tree, as the lockfile is also
+# a stampfile whose presence indicates that there may be
+# cleanup to do
use POSIX;
use Fcntl qw(:flock);
+use File::Path qw(rmtree);
our $package_re = '[0-9a-z][-+.0-9a-z]+';
+our $func;
our $dgitrepos;
-our $pkg;
+our $package;
+our $suitesfile;
+our $realdestrepo;
our $destrepo;
our $workrepo;
-our @keyrings;
+our $keyrings;
+our @lockfhs;
+
+#----- utilities -----
sub acquirelock ($$) {
my ($lock, $must) = @_;
+ my $fh;
for (;;) {
- my $fh = new IO::File, ">", $lock or die "open $lock: $!";
+ close $fh if $fh;
+ $fh = new IO::File $lock, ">" or die "open $lock: $!";
my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
if (!$ok) {
- return unless $must;
+ return undef unless $must;
die "flock $lock: $!";
}
if (!stat $lock) {
my $want = (stat _)[1];
stat $fh or die $!;
my $got = (stat _)[1];
- return $fh if $got == $want;
+ last if $got == $want;
}
+ return $fh;
}
-sub makeworkingclone () {
- $workrepo = "$dgitrepos/_tmp/${pkg}_incoming$$";
- my $lock = "$workrepo.lock";
- my $lockfh = acquirelock($lock, 1);
- if (!stat $destrepo) {
- $! == ENOENT or die "stat dest repo $destrepo: $!";
- mkdir $workrepo or die "create work repo $workrepo: $!";
- runcmd qw(git init --bare), $workrepo;
- } else {
- runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
+sub acquiretree ($$) {
+ my ($tree, $must) = @_;
+ my $fh = acquirelock("$tree.lock", $must);
+ if ($fh) {
+ push @lockfhs, $fh;
+ rmtree $tree;
}
+ return $fh;
+}
+
+sub mkrepotmp () {
+ my $tmpdir = "$dgitrepos/_tmp";
+ return if mkdir $tmpdir;
+ return if $! == EEXIST;
+ die $!;
+}
+
+sub reject ($) {
+ die "dgit-repos-server: reject: $_[0]\n";
+}
+
+sub runcmd {
+ $!=0; $?=0;
+ my $r = system @_;
+ die "@_ $? $!" if $r;
+}
+
+#----- git-receive-pack -----
+
+sub fixmissing__git_receive_pack () {
+ mkrepotmp();
+ $destrepo = "$dgitrepos/_tmp/${package}_prospective";
+ acquiretree($destrepo, 1);
+ my $r = system qw(cp -a --), "$dgitrepos/_template", "$destrepo";
+ !$r or die "create new repo failed failed: $r $!";
+}
+
+sub makeworkingclone () {
+ mkrepotmp();
+ $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
+ acquiretree($workrepo, 1);
+ runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
}
sub setupstunthook () {
my $prerecv = "$workrepo/hooks/pre-receive";
- my $fh = new IO::File, $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
+ my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
or die "$prerecv: $!";
print $fh <<END or die "$prerecv: $!";
#!/bin/sh
set -e
-exec $0 --pre-receive-hook $pkg
+exec $0 --pre-receive-hook $package
END
close $fh or die "$prerecv: $!";
$ENV{'DGIT_RPR_WORK'}= $workrepo;
$ENV{'DGIT_RPR_DEST'}= $destrepo;
}
+sub maybeinstallprospective () {
+ return if $destrepo eq $realdestrepo;
+
+ my $child = open SR, "-|";
+ defined $child or die $!;
+ if (!$child) {
+ chdir $destrepo or die $!;
+ exec qw(git show-ref);
+ die $!;
+ }
+ my %got = qw(tag 0 head 0);
+ while (<SR>) {
+ chomp or die;
+ s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
+ my $wh =
+ m{^refs/tags/} ? 'tag' :
+ m{^refs/dgit/} ? 'head' :
+ die;
+ die if $got{$wh}++;
+ }
+ die if grep { !$_ } values %got;
+ $!=0; $?=0; close SR or die "$? $!";
+
+ rename $destrepo, $realdestrepo or die $!;
+ remove "$destrepo.lock" or die $!;
+}
+
+sub main__git_receive_pack () {
+ makeworkingclone();
+ setupstunthook();
+ runcmd qw(git receive-pack), $destrepo;
+ maybeinstallprospective();
+}
+
#----- stunt post-receive hook -----
our ($tagname, $tagval, $suite, $oldcommit, $commit);
}
STDIN->error and die $!;
- die unless defined $refname;
- die unless defined $branchname;
+ die unless defined $tagname;
+ die unless defined $suite;
}
sub parsetag () {
open PT, ">dgit-tmp/plaintext" or die $!;
open DS, ">dgit-tmp/plaintext.asc" or die $!;
open T, "-|", qw(git cat-file tag), $tagval or die $!;
- my %tagh;
for (;;) {
$!=0; $_=<T>; defined or die $!;
print PT or die $!;
$!=0; $_=<T>; defined or die $!;
m/^($package_re) release (\S+) for (\S+) \[dgit\]$/ or die;
- die unless $1 eq $pkg;
+ die unless $1 eq $package;
$version = $2;
die unless $3 eq $suite;
my $ok = undef;
- open P, "-|", (qw(gpgv --status-fd=1),
- map { '--keyring', $_ }, @keyrings,
+ open P, "-|", (qw(gpgv --status-fd=1 --keyring),
+ $keyringfile,
qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext))
or die $!;
reject "key not found in keyrings";
}
-sub checktag () {
- tagh1('object') eq $branchval or die;
+sub checksuite () {
+ open SUITES, "<", $suitesfile or die $!;
+ while (<SUITES>) {
+ chomp;
+ next unless m/\S/;
+ next if m/^\#/;
+ s/\s+$//;
+ return if $_ eq $suite;
+ }
+ die $! if SUITES->error;
+ reject "unknown suite";
+}
+
+sub checks () {
+ checksuite();
tagh1('type') eq 'commit' or die;
+ tagh1('object') eq $commit or die;
tagh1('tag') eq $tagname or die;
my $v = $version;
$v =~ y/~:/_%/;
$tagname eq "debian/$v" or die;
- check fast forward;
+ # check that our ref is being fast-forwarded
+ if ($oldcommit =~ m/[^0]/) {
+ $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
+ chomp $mb;
+ $mb eq $oldcommit or reject "not fast forward on dgit branch";
+ }
}
+sub onwardpush () {
+ $!=0;
+ my $r = system (qw(git send-pack),
+ $destrepo,
+ "$commit:refs/dgit/$suite",
+ "$tagval:refs/tags/$tagname");
+ !$r or die "onward push failed: $r $!";
+}
sub stunthook () {
chdir $workrepo or die "chdir $workrepo: $!";
readupdates();
parsetag();
verifytag();
- checktag();
-... ...
+ checks();
+ onwardpush();
+}
+
+#----- git-upload-pack -----
+
+sub fixmissing__git_upload_pack () {
+ $destrepo = "$dgitrepos/_empty";
+ my $lfh = acquiretree($destrepo,1);
+ return if stat $destrepo;
+ die $! unless $!==ENOENT;
+ rmtree "$destrepo.new";
+ umask 022;
+ runcmd qw(git init --bare --quiet), "$destrepo.new";
+ rename "$destrepo.new", $destrepo or die $!;
+ unlink "$destrepo.lock" or die $!;
+ close $lfh;
+}
+
+sub main__git_upload_pack () {
+ runcmd qw(git upload-pack), $destrepo;
}
#----- arg parsing and main program -----
-sub parseargs () {
+sub argval () {
+ die unless @ARGV;
+ my $v = shift @ARGV;
+ die if $v =~ m/^-/;
+ return $v;
+}
+
+sub parseargsdispatch () {
die unless @ARGV;
if ($ARGV[0] eq '--pre-receive-hook') {
shift @ARGV;
@ARGV == 1 or die;
- $pkg = shift @ARGV;
+ $package = shift @ARGV;
+ defined($suitesfile = $ENV{'DGIT_RPR_SUITES'}) or die;
defined($workrepo = $ENV{'DGIT_RPR_WORK'}) or die;
defined($destrepo = $ENV{'DGIT_RPR_DEST'}) or die;
defined($keyrings = $ENV{'DGIT_RPR_KEYRINGS'}) or die $!;
exit 0;
}
- die unless @ARGV>=2;
-
- die if $ARGV[0] =~ m/^-/;
- $ENV{'DGIT_RPR_KEYRINGS'} = shift @ARGV;
- die if $ARGV[0] =~ m/^-/;
- $dgitrepos = shift @ARGV;
-
- die unless @ARGV;
- if ($ARGV[0] != m/^-/) {
- @ARGV == 1 or die;
- $pkg = shift @ARGV;
- } elsif ($ARGV[0] eq '--ssh') {
- shift @ARGV;
- !@ARGV or die;
- my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
- $cmd =~ m{
- ^
- (?:\S*/)?
- (git-receive-pack|git-upload-pack)
- \s+
- (?:\S*/)?
- ($package_re)\.git
- $
- }ox
- or die "requested command $cmd not understood";
- $method = $1;
- $pkg = $2;
+ $ENV{'DGIT_RPR_SUITES'} = argval();
+ $ENV{'DGIT_RPR_KEYRINGS'} = argval();
+ $dgitrepos = argval();
+
+ die unless @ARGV==1 && $ARGV[0] eq '--ssh';
+
+ my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
+ $cmd =~ m{
+ ^
+ (?: \S* / )?
+ ( [-0-9a-z]+ )
+ \s+
+ (?: \S* / )?
+ ($package_re) \.git
+ $
+ }ox
+ or reject "command string not understood";
+ my $method = $1;
+ $package = $2;
+ $realdestrepo = "$dgitrepos/$package.git";
+
+ my $funcn = $method;
+ $funcn =~ y/-/_/;
+ my $mainfunc = $main::{"main__$funcn"};
+
+ reject "unknown method" unless $mainfunc;
+
+ if (stat $realdestrepo) {
+ $destrepo = $realdestrepo;
} else {
- die;
+ $! == ENOENT or die "stat dest repo $destrepo: $!";
+ my $fixfunc = $main::{"fixmissing__$funcn"};
+ &$fixfunc;
}
- $destrepo = "$dgitrepos/$pkg.git";
+ &$mainfunc;
}
-sub main () {
- parseargs();
-fixme check method;
- makeworkingclone();
- setupstunthook();
- runcmd qw(git receive-pack), $destdir;
+sub unlockall () {
+ while (my $fh = pop @lockfhs) { close $fh; }
+}
+
+sub cleanup () {
+ unlockall();
+ if (!chdir "$dgitrepos/_tmp") {
+ $!==ENOENT or die $!;
+ return;
+ }
+ foreach my $lf (<*.lock>) {
+ my $tree = $lf;
+ $tree =~ s/\.lock$//;
+ next unless acquiretree($tree, 0);
+ remove $lf or warn $!;
+ unlockall();
+ }
}
+
+parseargsdispatch();
+cleanup();