summaryrefslogtreecommitdiff
path: root/dgit-repos-server
blob: 677e3d71f27444afb578db245267d747dbd7ef35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
#!/usr/bin/perl -w
# dgit-repos-push-receiver
#
# usages:
#  .../dgit-repos-push-receiver KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
#  .../dgit-repos-push-receiver KEYRING-AUTH-SPEC DGIT-REPOS-DIR PACKAGE
# internal usage:
#  .../dgit-repos-push-receiver --pre-receive-hook PACKAGE
#
# Invoked as the ssh restricted command
#
# Works like git-receive-pack
#
# KEYRING-AUTH-SPEC is a :-separated list of
#   KEYRING.GPG,AUTH-SPEC
# where AUTH-SPEC is one of
#   a
#   mDM.TXT

use strict;

# What we do is this:
#  - extract the destination repo name somehow
#  - 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

use POSIX;
use Fcntl qw(:flock);

our $package_re = '[0-9a-z][-+.0-9a-z]+';

our $dgitrepos;
our $pkg;
our $destrepo;
our $workrepo;
our @keyrings;

sub acquirelock ($$) {
    my ($lock, $must) = @_;
    for (;;) {
	my $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;
	    die "flock $lock: $!";
	}
	if (!stat $lock) {
	    next if $! == ENOENT;
	    die "stat $lock: $!";
	}
	my $want = (stat _)[1];
	stat $fh or die $!;
	my $got = (stat _)[1];
	return $fh if $got == $want;
    }
}

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 setupstunthook () {
    my $prerecv = "$workrepo/hooks/pre-receive";
    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
END
    close $fh or die "$prerecv: $!";
    $ENV{'DGIT_RPR_WORK'}= $workrepo;
    $ENV{'DGIT_RPR_DEST'}= $destrepo;
}

#----- stunt post-receive hook -----

our ($tagname, $tagval, $suite, $oldcommit, $commit);
our ($version, %tagh);

sub readupdates () {
    while (<STDIN>) {
	m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
	my ($old, $sha1, $refname) = ($1, $2, $3);
	if ($refname =~ m{^refs/tags/(?=debian/)}) {
	    die if defined $tagname;
	    $tagname = $'; #';
	    $tagval = $sha1;
	    reject "tag $tagname already exists -".
		" not replacing previously-pushed version"
		if $old =~ m/[^0]/;
	} elsif ($refname =~ m{^refs/dgit/}) {
	    die if defined $suite;
	    $suite = $'; #';
	    $oldcommit = $old;
	    $commit = $sha1;
	} else {
	    die;
	}
    }
    STDIN->error and die $!;

    die unless defined $refname;
    die unless defined $branchname;
}

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 $!;
	if (m/^(\S+) (.*)/) {
	    push @{ $tagh{$1} }, $2;
	} elsif (!m/\S/) {
	    last;
	} else {
	    die;
	}
    }
    $!=0; $_=<T>; defined or die $!;
    m/^($package_re) release (\S+) for (\S+) \[dgit\]$/ or die;

    die unless $1 eq $pkg;
    $version = $2;
    die unless $3 eq $suite;

    for (;;) {
	print PT or die $!;
	$!=0; $_=<T>; defined or die $!;
	last if m/^-----BEGIN PGP/;
    }
    for (;;) {
	print DS or die $!;
	$!=0; $_=<T>;
	last if !defined;
    }
    T->error and die $!;
    close PT or die $!;
    close DS or die $!;
}

sub checksig_keyring ($) {
    my ($keyringfile) = @_;
    # returns primary-keyid if signed by a key in this keyring
    # or undef if not
    # or dies on other errors

    my $ok = undef;

    open P, "-|", (qw(gpgv --status-fd=1),
		   map { '--keyring', $_ }, @keyrings,
		   qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext))
	or die $!;

    while (<P>) {
	next unless s/^\[GNUPG:\]: //;
	chomp or die;
	my @l = split / /, $_;
	if ($l[0] eq 'NO_PUBKEY') {
	    last;
	} elsif ($l[0] eq 'VALIDSIG') {
	    my $sigtype = $l[9];
	    $sigtype eq '00' or reject "signature is not of type 00!";
	    $ok = $l[10];
	    die unless defined $ok;
	    last;
	}
    }
    close P;

    return $ok;
}

sub dm_txt_check ($$) {
    my ($keyid, $dmtxtfn) = @_;
    open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
    while (<DT>) {
	m/^fingerprint:\s+$keyid$/oi
	    ..0 or next;
	m/^\S/
	    or reject "key $keyid missing Allow section in permissions!";
	# in right stanza...
	s/^allow:/ /i
	    ..0 or next;
	s/^\s+//
	    or reject "package $package not allowed for key $keyid";
	# in allow field...
	s/\([^()]+\)//;
	s/\,//;
	foreach my $p (split /\s+/) {
	    return if $p eq $package; # yay!
	}
    }
    DT->error and die $!;
    close DT or die $!;
    reject "key $keyid not in permissions list although in keyring!";
}

sub verifytag () {
    foreach my $kas (split /:/, $keyrings) {
	$kas =~ s/^([^,]+),// or die;
	my $keyid = checksig_keyring $1;
	if (defined $keyid) {
	    if ($kas =~ m/^a$/) {
		return; # yay
	    } elsif ($kas =~ m/^m([^,]+)$/) {
		dm_txt_check($keyid, $1);
		return;
	    } else {
		die;
	    }
	}   
    }
    reject "key not found in keyrings";
}

sub checktag () {
    tagh1('object') eq $commit or die;
    tagh1('type') eq 'commit' or die;
    tagh1('tag') eq $tagname or die;

    my $v = $version;
    $v =~ y/~:/_%/;
    $tagname eq "debian/$v" or die;

    # 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: $!";
    mkdir "dgit-tmp" or $!==EEXIST or die $!;
    readupdates();
    parsetag();
    verifytag();
    checktag();
    onwardpush();
}

#----- arg parsing and main program -----

sub parseargs () {
    die unless @ARGV;

    if ($ARGV[0] eq '--pre-receive-hook') {
	shift @ARGV;
	@ARGV == 1 or die;
	$pkg = shift @ARGV;
	defined($workrepo = $ENV{'DGIT_RPR_WORK'}) or die;
	defined($destrepo = $ENV{'DGIT_RPR_DEST'}) or die;
	defined($keyrings = $ENV{'DGIT_RPR_KEYRINGS'}) or die $!;
	open STDOUT, ">&STDERR" or die $!;
	stunthook();
	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;
	my $func = $method;
	$func =~ y/-/_/;
	$func = $main::{"main__$func"};
	&$func;
    } else {
	die;
    }

    $destrepo = "$dgitrepos/$pkg.git";
}

sub main__git_receive_pack () {
    parseargs();
fixme check method;
    makeworkingclone();
    setupstunthook();
    runcmd qw(git receive-pack), $destdir;
}