]> git.decadent.org.uk Git - dak.git/blob - tools/debianqueued-0.9/debianqueued
debianqueued: add support for *.dak-commands
[dak.git] / tools / debianqueued-0.9 / debianqueued
1 #!/usr/bin/perl -w
2 #
3 # debianqueued -- daemon for managing Debian upload queues
4 #
5 # Copyright (C) 1997 Roman Hodek <Roman.Hodek@informatik.uni-erlangen.de>
6 # Copyright (C) 2001-2007 Ryan Murray <rmurray@debian.org>
7 # Copyright (C) 2008 Thomas Viehmann <tv@beamnet.de>
8 #
9 # This program is free software.  You can redistribute it and/or
10 # modify it under the terms of the GNU General Public License as
11 # published by the Free Software Foundation: either version 2 or
12 # (at your option) any later version.
13 # This program comes with ABSOLUTELY NO WARRANTY!
14 #
15
16 require 5.002;
17 use strict;
18 use POSIX;
19 use POSIX qw( strftime sys_stat_h sys_wait_h signal_h );
20 use Net::Ping;
21 use Net::FTP;
22 use Socket qw( PF_INET AF_INET SOCK_STREAM );
23 use Config;
24 use Sys::Hostname;
25 use File::Copy;
26
27 setlocale(&POSIX::LC_ALL, "C");
28
29 # ---------------------------------------------------------------------------
30 #                                                               configuration
31 # ---------------------------------------------------------------------------
32
33 package conf;
34 ( $conf::queued_dir = ( ( $0 !~ m,^/, ) ? POSIX::getcwd() . "/" : "" ) . $0 )
35   =~ s,/[^/]+$,,;
36 require "$conf::queued_dir/config";
37 my $junk = $conf::debug;    # avoid spurious warnings about unused vars
38 $junk = $conf::ssh_key_file;
39 $junk = $conf::stray_remove_timeout;
40 $junk = $conf::problem_report_timeout;
41 $junk = $conf::queue_delay;
42 $junk = $conf::keep_files;
43 $junk = $conf::valid_files;
44 $junk = $conf::max_upload_retries;
45 $junk = $conf::upload_delay_1;
46 $junk = $conf::upload_delay_2;
47 $junk = $conf::ar;
48 $junk = $conf::gzip;
49 $junk = $conf::cp;
50 $junk = $conf::check_md5sum;
51
52 #$junk = $conf::ls;
53 $junk         = $conf::chmod;
54 $junk         = $conf::ftpdebug;
55 $junk         = $conf::ftptimeout;
56 $junk         = $conf::no_changes_timeout;
57 $junk         = @conf::nonus_packages;
58 $junk         = @conf::test_binaries;
59 $junk         = @conf::maintainer_mail;
60 $junk         = @conf::targetdir_delayed;
61 $junk         = $conf::mail ||= '/usr/sbin/sendmail';
62 $junk         = $conf::overridemail;
63 $conf::target = "localhost" if $conf::upload_method eq "copy";
64
65 package main;
66
67 ( $main::progname = $0 ) =~ s,.*/,,;
68
69 ($main::hostname, undef, undef, undef, undef) = gethostbyname(hostname());
70
71 my %packages = ();
72
73 # extract -r and -k args
74 $main::arg = "";
75 if ( @ARGV == 1 && $ARGV[0] =~ /^-[rk]$/ ) {
76   $main::arg = ( $ARGV[0] eq '-k' ) ? "kill" : "restart";
77   shift @ARGV;
78 }
79
80 # test for another instance of the queued already running
81 my ( $pid, $delayed_dirs, $adelayedcore );
82 if ( open( PIDFILE, "<$conf::pidfile" ) ) {
83   chomp( $pid = <PIDFILE> );
84   close(PIDFILE);
85   if ( !$pid ) {
86
87     # remove stale pid file
88     unlink($conf::pidfile);
89   } elsif ($main::arg) {
90     local ($|) = 1;
91     print "Killing running daemon (pid $pid) ...";
92     kill( 15, $pid );
93     my $cnt = 20;
94     while ( kill( 0, $pid ) && $cnt-- > 0 ) {
95       sleep 1;
96       print ".";
97     }
98     if ( kill( 0, $pid ) ) {
99       print " failed!\nProcess $pid still running.\n";
100       exit 1;
101     }
102     print "ok\n";
103     if ( -e "$conf::incoming/core" ) {
104       unlink("$conf::incoming/core");
105       print "(Removed core file)\n";
106     }
107     for ( $delayed_dirs = 0 ;
108           $delayed_dirs <= $conf::max_delayed ;
109           $delayed_dirs++ )
110     {
111       $adelayedcore =
112         sprintf( "$conf::incoming_delayed/core", $delayed_dirs );
113       if ( -e $adelayedcore ) {
114         unlink($adelayedcore);
115         print "(Removed core file)\n";
116       }
117     } ## end for ( $delayed_dirs = 0...
118     exit 0 if $main::arg eq "kill";
119   } else {
120     die "Another $main::progname is already running (pid $pid)\n"
121       if $pid && kill( 0, $pid );
122   }
123 } elsif ( $main::arg eq "kill" ) {
124   die "No daemon running\n";
125 } elsif ( $main::arg eq "restart" ) {
126   print "(No daemon running; starting anyway)\n";
127 }
128
129 # if started without arguments (initial invocation), then fork
130 if ( !@ARGV ) {
131
132   # now go to background
133   die "$main::progname: fork failed: $!\n"
134     unless defined( $pid = fork );
135   if ($pid) {
136
137     # parent: wait for signal from child (SIGCHLD or SIGUSR1) and exit
138     my $sigset = POSIX::SigSet->new();
139     $sigset->emptyset();
140     $SIG{"CHLD"} = sub { };
141     $SIG{"USR1"} = sub { };
142     POSIX::sigsuspend($sigset);
143     waitpid( $pid, WNOHANG );
144     if ( kill( 0, $pid ) ) {
145       print "Daemon (on $main::hostname) started in background (pid $pid)\n";
146       exit 0;
147     } else {
148       exit 1;
149     }
150   } else {
151
152     # child
153     setsid;
154     if ( $conf::upload_method eq "ssh" ) {
155
156       # exec an ssh-agent that starts us again
157       # force shell to be /bin/sh, ssh-agent may base its decision
158       # whether to use a fd or a Unix socket on the shell...
159       $ENV{"SHELL"} = "/bin/sh";
160       exec $conf::ssh_agent, $0, "startup", getppid();
161       die "$main::progname: Could not exec $conf::ssh_agent: $!\n";
162     } else {
163
164       # no need to exec, just set up @ARGV as expected below
165       @ARGV = ( "startup", getppid() );
166     }
167   } ## end else [ if ($pid)
168 } ## end if ( !@ARGV )
169 die "Please start without any arguments.\n"
170   if @ARGV != 2 || $ARGV[0] ne "startup";
171 my $parent_pid = $ARGV[1];
172
173 do {
174   my $version;
175   ( $version = 'Release: 0.95' ) =~ s/\$ ?//g;
176   print "debianqueued $version\n";
177 };
178
179 # check if all programs exist
180 my $prg;
181 foreach $prg ( $conf::gpg, $conf::ssh, $conf::scp, $conf::ssh_agent,
182                $conf::ssh_add, $conf::md5sum, $conf::mail, $conf::mkfifo )
183 {
184   die "Required program $prg doesn't exist or isn't executable\n"
185     if !-x $prg;
186
187   # check for correct upload method
188   die "Bad upload method '$conf::upload_method'.\n"
189     if $conf::upload_method ne "ssh"
190       && $conf::upload_method ne "ftp"
191       && $conf::upload_method ne "copy";
192   die "No keyrings\n" if !@conf::keyrings;
193
194 } ## end foreach $prg ( $conf::gpg, ...
195 die "statusfile path must be absolute."
196   if $conf::statusfile !~ m,^/,;
197 die "upload and target queue paths must be absolute."
198   if $conf::incoming !~ m,^/,
199     || $conf::incoming_delayed !~ m,^/,
200     || $conf::targetdir !~ m,^/,
201     || $conf::targetdir_delayed !~ m,^/,;
202
203 # ---------------------------------------------------------------------------
204 #                                                          initializations
205 # ---------------------------------------------------------------------------
206
207 # prototypes
208 sub calc_delta();
209 sub check_dir();
210 sub get_filelist_from_known_good_changes($);
211 sub age_delayed_queues();
212 sub process_changes($\@);
213 sub process_commands($);
214 sub age_delayed_queues();
215 sub is_on_target($\@);
216 sub copy_to_target(@);
217 sub pgp_check($);
218 sub check_alive(;$);
219 sub check_incoming_writable();
220 sub fork_statusd();
221 sub write_status_file();
222 sub print_status($$$$$$);
223 sub format_status_num(\$$);
224 sub format_status_str(\$$);
225 sub send_status();
226 sub ftp_open();
227 sub ftp_cmd($@);
228 sub ftp_close();
229 sub ftp_response();
230 sub ftp_code();
231 sub ftp_error();
232 sub ssh_cmd($);
233 sub scp_cmd(@);
234 sub local_cmd($;$);
235 sub check_alive(;$);
236 sub check_incoming_writable();
237 sub rm(@);
238 sub md5sum($);
239 sub is_debian_file($);
240 sub get_maintainer($);
241 sub debian_file_stem($);
242 sub msg($@);
243 sub debug(@);
244 sub init_mail(;$);
245 sub finish_mail();
246 sub send_mail($$$);
247 sub try_to_get_mail_addr($$);
248 sub format_time();
249 sub print_time($);
250 sub block_signals();
251 sub unblock_signals();
252 sub close_log($);
253 sub kid_died($);
254 sub restart_statusd();
255 sub fatal_signal($);
256
257 $ENV{"PATH"} = "/bin:/usr/bin";
258 $ENV{"IFS"} = "" if defined( $ENV{"IFS"} && $ENV{"IFS"} ne "" );
259
260 # constants for stat
261 sub ST_DEV()   { 0 }
262 sub ST_INO()   { 1 }
263 sub ST_MODE()  { 2 }
264 sub ST_NLINK() { 3 }
265 sub ST_UID()   { 4 }
266 sub ST_GID()   { 5 }
267 sub ST_RDEV()  { 6 }
268 sub ST_SIZE()  { 7 }
269 sub ST_ATIME() { 8 }
270 sub ST_MTIME() { 9 }
271 sub ST_CTIME() { 10 }
272
273 # fixed lengths of data items passed over status pipe
274 sub STATNUM_LEN() { 30 }
275 sub STATSTR_LEN() { 128 }
276
277 # init list of signals
278 defined $Config{sig_name}
279   or die "$main::progname: No signal list defined!\n";
280 my $i = 0;
281 my $name;
282 foreach $name ( split( ' ', $Config{sig_name} ) ) {
283   $main::signo{$name} = $i++;
284 }
285
286 @main::fatal_signals = qw( INT QUIT ILL TRAP ABRT BUS FPE USR2 SEGV PIPE
287   TERM XCPU XFSZ PWR );
288
289 $main::block_sigset = POSIX::SigSet->new;
290 $main::block_sigset->addset( $main::signo{"INT"} );
291 $main::block_sigset->addset( $main::signo{"TERM"} );
292
293 # some constant net stuff
294 $main::tcp_proto = ( getprotobyname('tcp') )[2]
295   or die "Cannot get protocol number for 'tcp'\n";
296 my $used_service = ( $conf::upload_method eq "ssh" ) ? "ssh" : "ftp";
297 $main::echo_port = ( getservbyname( $used_service, 'tcp' ) )[2]
298   or die "Cannot get port number for service '$used_service'\n";
299
300 # clear queue of stored mails
301 @main::stored_mails = ();
302
303 # run ssh-add to bring the key into the agent (will use stdin/stdout)
304 if ( $conf::upload_method eq "ssh" ) {
305   system "$conf::ssh_add $conf::ssh_key_file"
306     and die "$main::progname: Running $conf::ssh_add failed "
307     . "(exit status ", $? >> 8, ")\n";
308 }
309
310 # change to queue dir
311 chdir($conf::incoming)
312   or die "$main::progname: cannot cd to $conf::incoming: $!\n";
313
314 # needed before /dev/null redirects, some system send a SIGHUP when loosing
315 # the controlling tty
316 $SIG{"HUP"} = "IGNORE";
317
318 # open logfile, make it unbuffered
319 open( LOG, ">>$conf::logfile" )
320   or die "Cannot open my logfile $conf::logfile: $!\n";
321 chmod( 0644, $conf::logfile )
322   or die "Cannot set modes of $conf::logfile: $!\n";
323 select( ( select(LOG), $| = 1 )[0] );
324
325 sleep(1);
326 $SIG{"HUP"} = \&close_log;
327
328 # redirect stdin, ... to /dev/null
329 open( STDIN, "</dev/null" )
330   or die "$main::progname: Can't redirect stdin to /dev/null: $!\n";
331 open( STDOUT, ">&LOG" )
332   or die "$main::progname: Can't redirect stdout to $conf::logfile: $!\n";
333 open( STDERR, ">&LOG" )
334   or die "$main::progname: Can't redirect stderr to $conf::logfile: $!\n";
335
336 # ok, from this point usually no "die" anymore, stderr is gone!
337 msg( "log", "daemon (pid $$) (on $main::hostname) started\n" );
338
339 # initialize variables used by send_status before launching the status daemon
340 $main::dstat = "i";
341 format_status_num( $main::next_run, time + 10 );
342 format_status_str( $main::current_changes, "" );
343 check_alive();
344 $main::incoming_writable = 1;    # assume this for now
345
346 # start the daemon watching the 'status' FIFO
347 if ( $conf::statusfile && $conf::statusdelay == 0 ) {
348   $main::statusd_pid = fork_statusd();
349   $SIG{"CHLD"}       = \&kid_died;       # watch out for dead status daemon
350                                          # SIGUSR1 triggers status info
351   $SIG{"USR1"}       = \&send_status;
352 } ## end if ( $conf::statusfile...
353 $main::maind_pid = $$;
354
355 END {
356   kill( $main::signo{"ABRT"}, $$ )
357     if defined $main::signo{"ABRT"};
358 }
359
360 # write the pid file
361 open( PIDFILE, ">$conf::pidfile" )
362   or msg( "log", "Can't open $conf::pidfile: $!\n" );
363 printf PIDFILE "%5d\n", $$;
364 close(PIDFILE);
365 chmod( 0644, $conf::pidfile )
366   or die "Cannot set modes of $conf::pidfile: $!\n";
367
368 # other signals will just log an error and exit
369 foreach (@main::fatal_signals) {
370   $SIG{$_} = \&fatal_signal;
371 }
372
373 # send signal to user-started process that we're ready and it can exit
374 kill( $main::signo{"USR1"}, $parent_pid );
375
376 # ---------------------------------------------------------------------------
377 #                                                                the mainloop
378 # ---------------------------------------------------------------------------
379
380 # default to classical incoming/target
381 $main::current_incoming  = $conf::incoming;
382 $main::current_targetdir = $conf::targetdir;
383
384 $main::dstat = "i";
385 write_status_file() if $conf::statusdelay;
386 while (1) {
387
388   # ping target only if there is the possibility that we'll contact it (but
389   # also don't wait too long).
390   my @have_changes = <*.changes *.commands *.dak-commands>;
391   for ( my $delayed_dirs = 0 ;
392         $delayed_dirs <= $conf::max_delayed ;
393         $delayed_dirs++ )
394   {
395     my $adelayeddir = sprintf( "$conf::incoming_delayed", $delayed_dirs );
396     push( @have_changes, <$adelayeddir/*.changes> );
397   } ## end for ( my $delayed_dirs ...
398   check_alive()
399     if @have_changes || ( time - $main::last_ping_time ) > 8 * 60 * 60;
400
401   if ( @have_changes && $main::target_up ) {
402     check_incoming_writable if !$main::incoming_writable;
403     check_dir() if $main::incoming_writable;
404   }
405   $main::dstat = "i";
406   write_status_file() if $conf::statusdelay;
407
408   if ( $conf::upload_method eq "copy" ) {
409     age_delayed_queues();
410   }
411
412   # sleep() returns if we received a signal (SIGUSR1 for status FIFO), so
413   # calculate the end time once and wait for it being reached.
414   format_status_num( $main::next_run, time + $conf::queue_delay );
415   my $delta;
416   while ( ( $delta = calc_delta() ) > 0 ) {
417     debug("mainloop sleeping $delta secs");
418     sleep($delta);
419
420     # check if statusd died, if using status FIFO, or update status file
421     if ($conf::statusdelay) {
422       write_status_file();
423     } else {
424       restart_statusd();
425     }
426   } ## end while ( ( $delta = calc_delta...
427 } ## end while (1)
428
429 sub calc_delta() {
430   my $delta;
431
432   $delta = $main::next_run - time;
433   $delta = $conf::statusdelay
434     if $conf::statusdelay && $conf::statusdelay < $delta;
435   return $delta;
436 } ## end sub calc_delta()
437
438 # ---------------------------------------------------------------------------
439 #                                                       main working functions
440 # ---------------------------------------------------------------------------
441
442 #
443 # main function for checking the incoming dir
444 #
445 sub check_dir() {
446   my ( @files, @changes, @keep_files, @this_keep_files, @stats, $file,
447        $adelay );
448
449   debug("starting checkdir");
450   $main::dstat = "c";
451   write_status_file() if $conf::statusdelay;
452
453   # test if needed binaries are available; this is if they're on maybe
454   # slow-mounted NFS filesystems
455   foreach (@conf::test_binaries) {
456     next if -f $_;
457
458     # maybe the mount succeeds now
459     sleep 5;
460     next if -f $_;
461     msg( "log", "binary test failed for $_; delaying queue run\n" );
462     goto end_run;
463   } ## end foreach (@conf::test_binaries)
464
465   for ( $adelay = -1 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
466     if ( $adelay == -1 ) {
467       $main::current_incoming       = $conf::incoming;
468       $main::current_incoming_short = "";
469       $main::current_targetdir      = $conf::targetdir;
470     } else {
471       $main::current_incoming = sprintf( $conf::incoming_delayed, $adelay );
472       $main::current_incoming_short = sprintf( "DELAYED/%d-day", $adelay );
473       $main::current_targetdir = sprintf( $conf::targetdir_delayed, $adelay );
474     }
475
476     # need to clear directory specific variables
477     undef(@keep_files);
478     undef(@this_keep_files);
479
480     chdir($main::current_incoming)
481       or (
482            msg(
483                 "log",
484                 "Cannot change to dir "
485                   . "${main::current_incoming_short}: $!\n"
486               ),
487            return
488          );
489
490     # look for *.commands and *.dak-commands files but not in delayed queues
491     if ( $adelay == -1 ) {
492       foreach $file (<*.commands>) {
493         init_mail($file);
494         block_signals();
495         process_commands($file);
496         unblock_signals();
497         $main::dstat = "c";
498         write_status_file() if $conf::statusdelay;
499         finish_mail();
500       } ## end foreach $file (<*.commands>)
501           foreach $file (<*.dak-commands>) {
502                 init_mail($file);
503                 block_signals();
504                 process_dak_commands($file);
505                 unblock_signals();
506                 $main::dstat = "c";
507                 write_status_file() if $conf::statusdelay;
508                 finish_mail();
509           }
510     } ## end if ( $adelay == -1 )
511     opendir( INC, "." )
512       or (
513            msg(
514                 "log", "Cannot open dir ${main::current_incoming_short}: $!\n"
515               ),
516            return
517          );
518     @files = readdir(INC);
519     closedir(INC);
520
521     # process all .changes files found
522     @changes = grep /\.changes$/, @files;
523     push( @keep_files, @changes );    # .changes files aren't stray
524     foreach $file (@changes) {
525       init_mail($file);
526
527       # wrap in an eval to allow jumpbacks to here with die in case
528       # of errors
529       block_signals();
530       eval { process_changes( $file, @this_keep_files ); };
531       unblock_signals();
532       msg( "log,mail", $@ ) if $@;
533       $main::dstat = "c";
534       write_status_file() if $conf::statusdelay;
535
536       # files which are ok in conjunction with this .changes
537       debug("$file tells to keep @this_keep_files");
538       push( @keep_files, @this_keep_files );
539       finish_mail();
540
541       # break out of this loop if the incoming dir has become unwritable
542       goto end_run if !$main::incoming_writable;
543     } ## end foreach $file (@changes)
544     ftp_close() if $conf::upload_method eq "ftp";
545
546     # find files which aren't related to any .changes
547     foreach $file (@files) {
548
549       # filter out files we never want to delete
550       next if !-f $file ||    # may have disappeared in the meantime
551              $file eq "."
552           || $file eq ".."
553           || ( grep { $_ eq $file } @keep_files )
554           || $file =~ /$conf::keep_files/;
555
556       # Delete such files if they're older than
557       # $stray_remove_timeout; they could be part of an
558       # yet-incomplete upload, with the .changes still missing.
559       # Cannot send any notification, since owner unknown.
560       next if !( @stats = stat($file) );
561       my $age = time - $stats[ST_MTIME];
562       my ( $maint, $pattern, @job_files );
563       if (    $file =~ /^junk-for-writable-test/
564            || $file !~ m,$conf::valid_files,
565            || $age >= $conf::stray_remove_timeout )
566       {
567         msg( "log",
568              "Deleted stray file ${main::current_incoming_short}/$file\n" )
569           if rm($file);
570       } elsif (
571         $age > $conf::no_changes_timeout
572         && is_debian_file($file)
573         &&
574
575         # not already reported
576           !( $stats[ST_MODE] & S_ISGID )
577         && ( $pattern   = debian_file_stem($file) )
578         && ( @job_files = glob($pattern) )
579         &&
580
581         # If a .changes is in the list, it has the same stem as the
582         # found file (probably a .orig.tar.gz). Don't report in this
583         # case.
584         !( grep( /\.changes$/, @job_files ) )
585               )
586       {
587         $maint = get_maintainer($file);
588
589         # Don't send a mail if this looks like the recompilation of a
590         # package for a non-i386 arch. For those, the maintainer field is
591         # useless :-(
592         if ( !grep( /(\.dsc|_(i386|all)\.deb)$/, @job_files ) ) {
593           msg( "log", "Found an upload without .changes and with no ",
594                ".dsc file\n" );
595           msg( "log",
596                "Not sending a report, because probably ",
597                "recompilation job\n" );
598         } elsif ($maint) {
599           init_mail();
600           $main::mail_addr = $maint;
601           $main::mail_addr = $1 if $main::mail_addr =~ /<([^>]*)>/;
602           $main::mail_subject =
603             "Incomplete upload found in " . "Debian upload queue";
604           msg(
605                "mail",
606                "Probably you are the uploader of the following "
607                  . "file(s) in\n"
608              );
609           msg( "mail", "the Debian upload queue directory:\n  " );
610           msg( "mail", join( "\n  ", @job_files ), "\n" );
611           msg(
612                "mail",
613                "This looks like an upload, but a .changes file "
614                  . "is missing, so the job\n"
615              );
616           msg( "mail", "cannot be processed.\n\n" );
617           msg(
618                "mail",
619                "If no .changes file arrives within ",
620                print_time( $conf::stray_remove_timeout - $age ),
621                ", the files will be deleted.\n\n"
622              );
623           msg(
624                "mail",
625                "If you didn't upload those files, please just "
626                  . "ignore this message.\n"
627              );
628           finish_mail();
629           msg(
630                "log",
631                "Sending problem report for an upload without a "
632                  . ".changes\n"
633              );
634           msg( "log", "Maintainer: $maint\n" );
635         } else {
636           msg(
637                "log",
638                "Found an upload without .changes, but can't "
639                  . "find a maintainer address\n"
640              );
641         } ## end else [ if ( !grep( /(\.dsc|_(i386|all)\.deb)$/...
642         msg( "log", "Files: @job_files\n" );
643
644         # remember we already have sent a mail regarding this file
645         foreach (@job_files) {
646           my @st = stat($_);
647           next if !@st;    # file may have disappeared in the meantime
648           chmod +( $st[ST_MODE] |= S_ISGID ), $_;
649         }
650       } else {
651         debug(
652 "found stray file ${main::current_incoming_short}/$file, deleting in ",
653           print_time( $conf::stray_remove_timeout - $age )
654         );
655       } ## end else [ if ( $file =~ /^junk-for-writable-test/...
656     } ## end foreach $file (@files)
657   } ## end for ( $adelay = -1 ; $adelay...
658   chdir($conf::incoming);
659
660 end_run:
661   $main::dstat = "i";
662   write_status_file() if $conf::statusdelay;
663 } ## end sub check_dir()
664
665 sub get_filelist_from_known_good_changes($) {
666   my $changes = shift;
667
668   local (*CHANGES);
669   my (@filenames);
670
671   # parse the .changes file
672   open( CHANGES, "<$changes" )
673     or die "$changes: $!\n";
674 outer_loop: while (<CHANGES>) {
675     if (/^Files:/i) {
676       while (<CHANGES>) {
677         redo outer_loop if !/^\s/;
678         my @field = split(/\s+/);
679         next if @field != 6;
680
681         # forbid shell meta chars in the name, we pass it to a
682         # subshell several times...
683         $field[5] =~ /^([a-zA-Z0-9.+_:@=%-][~a-zA-Z0-9.+_:@=%-]*)/;
684         if ( $1 ne $field[5] ) {
685           msg( "log", "found suspicious filename $field[5]\n" );
686           next;
687         }
688         push( @filenames, $field[5] );
689       } ## end while (<CHANGES>)
690     } ## end if (/^Files:/i)
691   } ## end while (<CHANGES>)
692   close(CHANGES);
693   return @filenames;
694 } ## end sub get_filelist_from_known_good_changes($)
695
696 #
697 # process one .changes file
698 #
699 sub process_changes($\@) {
700   my $changes   = shift;
701   my $keep_list = shift;
702   my (
703        $pgplines,     @files,     @filenames,  @changes_stats,
704        $failure_file, $retries,   $last_retry, $upload_time,
705        $file,         $do_report, $ls_l,       $problems_reported,
706        $errs,         $pkgname,   $signator,   $extralines
707      );
708   local (*CHANGES);
709   local (*FAILS);
710
711   format_status_str( $main::current_changes,
712                      "$main::current_incoming_short/$changes" );
713   $main::dstat = "c";
714   write_status_file() if $conf::statusdelay;
715
716   @$keep_list = ();
717   msg( "log", "processing ${main::current_incoming_short}/$changes\n" );
718
719   # parse the .changes file
720   open( CHANGES, "<$changes" )
721     or die "Cannot open ${main::current_incoming_short}/$changes: $!\n";
722   $pgplines        = 0;
723   $extralines      = 0;
724   $main::mail_addr = "";
725   @files           = ();
726 outer_loop: while (<CHANGES>) {
727     if (/^---+(BEGIN|END) PGP .*---+$/) {
728       ++$pgplines;
729       next;
730     }
731     if ( $pgplines < 1 or $pgplines >= 3 ) {
732       $extralines++ if length $_ > 1;
733       next;
734     }
735     if (/^Maintainer:\s*/i) {
736       chomp( $main::mail_addr = $' );
737       $main::mail_addr = $1 if $main::mail_addr =~ /<([^>]*)>/;
738     } elsif (/^Source:\s*/i) {
739       chomp( $pkgname = $' );
740       $pkgname =~ s/\s+$//;
741       $main::packages{$pkgname}++;
742     } elsif (/^Files:/i) {
743       while (<CHANGES>) {
744         redo outer_loop if !/^\s/;
745         my @field = split(/\s+/);
746         next if @field != 6;
747
748         # forbid shell meta chars in the name, we pass it to a
749         # subshell several times...
750         $field[5] =~ /^([a-zA-Z0-9.+_:@=%-][~a-zA-Z0-9.+_:@=%-]*)/;
751         if ( $1 ne $field[5] ) {
752           msg( "log", "found suspicious filename $field[5]\n" );
753           msg(
754             "mail",
755 "File '$field[5]' mentioned in $main::current_incoming_short/$changes\n",
756             "has bad characters in its name. Removed.\n"
757           );
758           rm( $field[5] );
759           next;
760         } ## end if ( $1 ne $field[5] )
761         push(
762               @files,
763               {
764                 md5  => $field[1],
765                 size => $field[2],
766                 name => $field[5]
767               }
768             );
769         push( @filenames, $field[5] );
770         debug( "includes file $field[5], size $field[2], ", "md5 $field[1]" );
771       } ## end while (<CHANGES>)
772     } ## end elsif (/^Files:/i)
773   } ## end while (<CHANGES>)
774   close(CHANGES);
775
776   # tell check_dir that the files mentioned in this .changes aren't stray,
777   # we know about them somehow
778   @$keep_list = @filenames;
779
780   # some consistency checks
781   if ( $extralines ) {
782     msg( "log,mail",
783 "$main::current_incoming_short/$changes contained lines outside the pgp signed "
784 ."part, cannot process\n" );
785     goto remove_only_changes;
786   } ## end if ( $extralines )
787   if ( !$main::mail_addr ) {
788     msg( "log,mail",
789 "$main::current_incoming_short/$changes doesn't contain a Maintainer: field; "
790         . "cannot process\n" );
791     goto remove_only_changes;
792   } ## end if ( !$main::mail_addr)
793   if ( $main::mail_addr !~ /^(buildd_\S+-\S+|\S+\@\S+\.\S+)/ ) {
794
795     # doesn't look like a mail address, maybe only the name
796     my ( $new_addr, @addr_list );
797     if ( $new_addr = try_to_get_mail_addr( $main::mail_addr, \@addr_list ) ) {
798
799       # substitute (unique) found addr, but give a warning
800       msg(
801            "mail",
802            "(The Maintainer: field didn't contain a proper "
803              . "mail address.\n"
804          );
805       msg(
806            "mail",
807            "Looking for `$main::mail_addr' in the Debian "
808              . "keyring gave your address\n"
809          );
810       msg( "mail", "as unique result, so I used this.)\n" );
811       msg( "log",
812            "Substituted $new_addr for malformed " . "$main::mail_addr\n" );
813       $main::mail_addr = $new_addr;
814     } else {
815
816       # not found or not unique: hold the job and inform queue maintainer
817       my $old_addr = $main::mail_addr;
818       $main::mail_addr = $conf::maintainer_mail;
819       msg(
820         "mail",
821 "The job ${main::current_incoming_short}/$changes doesn't have a correct email\n"
822       );
823       msg( "mail", "address in the Maintainer: field:\n" );
824       msg( "mail", "  $old_addr\n" );
825       msg( "mail", "A check for this in the Debian keyring gave:\n" );
826       msg( "mail",
827            @addr_list
828            ? "  " . join( ", ", @addr_list ) . "\n"
829            : "  nothing\n" );
830       msg( "mail", "Please fix this manually\n" );
831       msg(
832         "log",
833 "Bad Maintainer: field in ${main::current_incoming_short}/$changes: $old_addr\n"
834       );
835       goto remove_only_changes;
836     } ## end else [ if ( $new_addr = try_to_get_mail_addr...
837   } ## end if ( $main::mail_addr ...
838   if ( $pgplines < 3 ) {
839     msg(
840         "log,mail",
841         "$main::current_incoming_short/$changes isn't signed with PGP/GnuPG\n"
842        );
843     msg( "log", "(uploader $main::mail_addr)\n" );
844     goto remove_only_changes;
845   } ## end if ( $pgplines < 3 )
846   if ( !@files ) {
847     msg( "log,mail",
848        "$main::current_incoming_short/$changes doesn't mention any files\n" );
849     msg( "log", "(uploader $main::mail_addr)\n" );
850     goto remove_only_changes;
851   } ## end if ( !@files )
852
853   # check for packages that shouldn't be processed
854   if ( grep( $_ eq $pkgname, @conf::nonus_packages ) ) {
855     msg(
856          "log,mail",
857          "$pkgname is a package that must be uploaded "
858            . "to nonus.debian.org\n"
859        );
860     msg( "log,mail", "instead of target.\n" );
861     msg( "log,mail",
862          "Job rejected and removed all files belonging " . "to it:\n" );
863     msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
864     rm( $changes, @filenames );
865     return;
866   } ## end if ( grep( $_ eq $pkgname...
867
868   $failure_file = $changes . ".failures";
869   $retries = $last_retry = 0;
870   if ( -f $failure_file ) {
871     open( FAILS, "<$failure_file" )
872       or die "Cannot open $main::current_incoming_short/$failure_file: $!\n";
873     my $line = <FAILS>;
874     close(FAILS);
875     ( $retries, $last_retry ) = ( $1, $2 )
876       if $line =~ /^(\d+)\s+(\d+)$/;
877     push( @$keep_list, $failure_file );
878   } ## end if ( -f $failure_file )
879
880   # run PGP on the file to check the signature
881   if ( !( $signator = pgp_check($changes) ) ) {
882     msg(
883        "log,mail",
884        "$main::current_incoming_short/$changes has bad PGP/GnuPG signature!\n"
885     );
886     msg( "log", "(uploader $main::mail_addr)\n" );
887   remove_only_changes:
888     msg(
889       "log,mail",
890 "Removing $main::current_incoming_short/$changes, but keeping its associated ",
891       "files for now.\n"
892     );
893     rm($changes);
894
895     # Set SGID bit on associated files, so that the test for Debian files
896     # without a .changes doesn't consider them.
897     foreach (@filenames) {
898       my @st = stat($_);
899       next if !@st;    # file may have disappeared in the meantime
900       chmod +( $st[ST_MODE] |= S_ISGID ), $_;
901     }
902     return;
903   } elsif ( $signator eq "LOCAL ERROR" ) {
904
905     # An error has appened when starting pgp... Don't process the file,
906     # but also don't delete it
907     debug(
908 "Can't PGP/GnuPG check $main::current_incoming_short/$changes -- don't process it for now"
909     );
910     return;
911   } ## end elsif ( $signator eq "LOCAL ERROR")
912
913   die "Cannot stat ${main::current_incoming_short}/$changes (??): $!\n"
914     if !( @changes_stats = stat($changes) );
915
916   # Make $upload_time the maximum of all modification times of files
917   # related to this .changes (and the .changes it self). This is the
918   # last time something changes to these files.
919   $upload_time = $changes_stats[ST_MTIME];
920   for $file (@files) {
921     my @stats;
922     next if !( @stats = stat( $file->{"name"} ) );
923     $file->{"stats"} = \@stats;
924     $upload_time = $stats[ST_MTIME] if $stats[ST_MTIME] > $upload_time;
925   } ## end for $file (@files)
926
927   $do_report = ( time - $upload_time ) > $conf::problem_report_timeout;
928   $problems_reported = $changes_stats[ST_MODE] & S_ISGID;
929
930   # if any of the files is newer than the .changes' ctime (the time
931   # we sent a report and set the sticky bit), send new problem reports
932   if ( $problems_reported && $changes_stats[ST_CTIME] < $upload_time ) {
933     $problems_reported = 0;
934     chmod +( $changes_stats[ST_MODE] &= ~S_ISGID ), $changes;
935     debug("upload_time>changes-ctime => resetting problems reported");
936   }
937   debug("do_report=$do_report problems_reported=$problems_reported");
938
939   # now check all files for correct size and md5 sum
940   for $file (@files) {
941     my $filename = $file->{"name"};
942     if ( !defined( $file->{"stats"} ) ) {
943
944       # could be an upload that isn't complete yet, be quiet,
945       # but don't process the file;
946       msg( "log,mail", "$filename doesn't exist\n" )
947         if $do_report && !$problems_reported;
948       msg( "log", "$filename doesn't exist (ignored for now)\n" )
949         if !$do_report;
950       msg( "log", "$filename doesn't exist (already reported)\n" )
951         if $problems_reported;
952       ++$errs;
953     } elsif ( $file->{"stats"}->[ST_SIZE] < $file->{"size"}
954               && !$do_report )
955     {
956
957       # could be an upload that isn't complete yet, be quiet,
958       # but don't process the file
959       msg( "log", "$filename is too small (ignored for now)\n" );
960       ++$errs;
961     } elsif ( $file->{"stats"}->[ST_SIZE] != $file->{"size"} ) {
962       msg( "log,mail", "$filename has incorrect size; deleting it\n" );
963       rm($filename);
964       ++$errs;
965     } elsif ( md5sum($filename) ne $file->{"md5"} ) {
966       msg( "log,mail",
967            "$filename has incorrect md5 checksum; ",
968            "deleting it\n" );
969       rm($filename);
970       ++$errs;
971     } ## end elsif ( md5sum($filename)...
972   } ## end for $file (@files)
973
974   if ($errs) {
975     if ( ( time - $upload_time ) > $conf::bad_changes_timeout ) {
976
977       # if a .changes fails for a really long time (several days
978       # or so), remove it and all associated files
979       msg(
980           "log,mail",
981           "$main::current_incoming_short/$changes couldn't be processed for ",
982           int( $conf::bad_changes_timeout / ( 60 * 60 ) ),
983           " hours and is now deleted\n"
984          );
985       msg( "log,mail", "All files it mentions are also removed:\n" );
986       msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
987       rm( $changes, @filenames, $failure_file );
988     } elsif ( $do_report && !$problems_reported ) {
989
990       # otherwise, send a problem report, if not done already
991       msg(
992            "mail",
993            "Due to the errors above, the .changes file couldn't ",
994            "be processed.\n",
995            "Please fix the problems for the upload to happen.\n"
996          );
997
998       # remember we already have sent a mail regarding this file
999       debug("Sending problem report mail and setting SGID bit");
1000       my $mode = $changes_stats[ST_MODE] |= S_ISGID;
1001       msg( "log", "chmod failed: $!" )
1002         if ( chmod( $mode, $changes ) != 1 );
1003     } ## end elsif ( $do_report && !$problems_reported)
1004
1005     # else: be quiet
1006
1007     return;
1008   } ## end if ($errs)
1009
1010   # if this upload already failed earlier, wait until the delay requirement
1011   # is fulfilled
1012   if ( $retries > 0
1013        && ( time - $last_retry ) <
1014        ( $retries == 1 ? $conf::upload_delay_1 : $conf::upload_delay_2 ) )
1015   {
1016     msg( "log", "delaying retry of upload\n" );
1017     return;
1018   } ## end if ( $retries > 0 && (...
1019
1020   if ( $conf::upload_method eq "ftp" ) {
1021     return if !ftp_open();
1022   }
1023
1024   # check if the job is already present on target
1025   # (moved to here, to avoid bothering target as long as there are errors in
1026   # the job)
1027   if ( $ls_l = is_on_target( $changes, @filenames ) ) {
1028     msg(
1029       "log,mail",
1030 "$main::current_incoming_short/$changes is already present on target host:\n"
1031     );
1032     msg( "log,mail", "$ls_l\n" );
1033     msg( "mail",
1034          "Either you already uploaded it, or someone else ",
1035          "came first.\n" );
1036     msg( "log,mail", "Job $changes removed.\n" );
1037     rm( $changes, @filenames, $failure_file );
1038     return;
1039   } ## end if ( $ls_l = is_on_target...
1040
1041   # clear sgid bit before upload, scp would copy it to target. We don't need
1042   # it anymore, we know there are no problems if we come here. Also change
1043   # mode of files to 644 if this should be done locally.
1044   $changes_stats[ST_MODE] &= ~S_ISGID;
1045   if ( !$conf::chmod_on_target ) {
1046     $changes_stats[ST_MODE] &= ~0777;
1047     $changes_stats[ST_MODE] |= 0644;
1048   }
1049   chmod +( $changes_stats[ST_MODE] ), $changes;
1050
1051   # try uploading to target
1052   if ( !copy_to_target( $changes, @filenames ) ) {
1053
1054     # if the upload failed, increment the retry counter and remember the
1055     # current time; both things are written to the .failures file. Don't
1056     # increment the fail counter if the error was due to incoming
1057     # unwritable.
1058     return if !$main::incoming_writable;
1059     if ( ++$retries >= $conf::max_upload_retries ) {
1060       msg( "log,mail",
1061            "$changes couldn't be uploaded for $retries times now.\n" );
1062       msg( "log,mail",
1063            "Giving up and removing it and its associated files:\n" );
1064       msg( "log,mail", "  ", join( ", ", @filenames ), "\n" );
1065       rm( $changes, @filenames, $failure_file );
1066     } else {
1067       $last_retry = time;
1068       if ( open( FAILS, ">$failure_file" ) ) {
1069         print FAILS "$retries $last_retry\n";
1070         close(FAILS);
1071         chmod( 0600, $failure_file )
1072           or die "Cannot set modes of $failure_file: $!\n";
1073       } ## end if ( open( FAILS, ">$failure_file"...
1074       push( @$keep_list, $failure_file );
1075       debug("now $retries failed uploads");
1076       msg(
1077            "mail",
1078            "The upload will be retried in ",
1079            print_time(
1080                          $retries == 1
1081                        ? $conf::upload_delay_1
1082                        : $conf::upload_delay_2
1083                      ),
1084            "\n"
1085          );
1086     } ## end else [ if ( ++$retries >= $conf::max_upload_retries)
1087     return;
1088   } ## end if ( !copy_to_target( ...
1089
1090   # If the files were uploaded ok, remove them
1091   rm( $changes, @filenames, $failure_file );
1092
1093   msg( "mail", "$changes uploaded successfully to $conf::target\n" );
1094   msg( "mail", "along with the files:\n  ", join( "\n  ", @filenames ),
1095        "\n" );
1096   msg( "log",
1097        "$changes processed successfully (uploader $main::mail_addr)\n" );
1098
1099   # Check for files that have the same stem as the .changes (and weren't
1100   # mentioned there) and delete them. It happens often enough that people
1101   # upload a .orig.tar.gz where it isn't needed and also not in the
1102   # .changes. Explicitly deleting it (and not waiting for the
1103   # $stray_remove_timeout) reduces clutter in the queue dir and maybe also
1104   # educates uploaders :-)
1105
1106   #     my $pattern = debian_file_stem( $changes );
1107   #     my $spattern = substr( $pattern, 0, -1 ); # strip off '*' at end
1108   #     my @other_files = glob($pattern);
1109   # filter out files that have a Debian revision at all and a different
1110   # revision. Those belong to a different upload.
1111   #     if ($changes =~ /^\Q$spattern\E-([\d.+-]+)/) {
1112   #             my $this_rev = $1;
1113   #             @other_files = grep( !/^\Q$spattern\E-([\d.+-]+)/ || $1 eq $this_rev,
1114   #                                                      @other_files);
1115   #}
1116   # Also do not remove those files if a .changes is among them. Then there
1117   # is probably a second upload for another version or another architecture.
1118   #     if (@other_files && !grep( /\.changes$/, @other_files )) {
1119   #             rm( @other_files );
1120   #             msg( "mail", "\nThe following file(s) seemed to belong to the same ".
1121   #                                      "upload, but weren't listed\n" );
1122   #             msg( "mail", "in the .changes file:\n  " );
1123   #             msg( "mail", join( "\n  ", @other_files ), "\n" );
1124   #             msg( "mail", "They have been deleted.\n" );
1125   #             msg( "log", "Deleted files in upload not in $changes: @other_files\n" );
1126   #}
1127 } ## end sub process_changes($\@)
1128
1129 #
1130 # process one .dak-commands file
1131 #
1132 sub process_dak_commands {
1133   my $commands = shift;
1134
1135   # TODO: get mail address from signed contents
1136   # and NOT implement a third parser for armored PGP...
1137   $main::mail_addr = undef;
1138
1139   # check signature
1140   my $signator = pgp_check($commands);
1141   if (!$signator) {
1142         msg("log,mail",
1143             "$main::current_incoming_short/$commands has bad PGP/GnuPG signature!\n");
1144         msg("log,mail",
1145                 "Removing $main::current_incoming_short/$commands\n");
1146         rm($commands);
1147         return;
1148   }
1149   elsif ($signator eq 'LOCAL ERROR') {
1150         debug("Can't check signature for $main::current_incoming_short/$commands -- don't process it for now");
1151         return;
1152   }
1153   msg("log,mail", "(PGP/GnuPG signature by $signator)\n");
1154
1155   # check target
1156   my @filenames = ($commands);
1157   if (my $ls_l = is_on_target($commands, @filenames)) {
1158         msg("log,mail", "$main::current_incoming_short/$commands is already present on target host:\n");
1159         msg("log,mail", "$ls_l\n");
1160         msg("log,mail", "Job $commands removed.\n");
1161         rm($commands);
1162         return;
1163   }
1164
1165   if (!copy_to_target($commands)) {
1166         msg("log,mail", "$commands couldn't be uploaded to target.\n");
1167         msg("log,mail", "Giving up and removing it.\n");
1168         rm($commands);
1169         return;
1170   }
1171
1172   rm($commands);
1173   msg("mail", "$commands uploaded successfully to $conf::target\n");
1174 }
1175
1176 #
1177 # process one .commands file
1178 #
1179 sub process_commands($) {
1180   my $commands = shift;
1181   my ( @cmds, $cmd, $pgplines, $signator );
1182   local (*COMMANDS);
1183   my ( @files, $file, @removed, $target_delay );
1184
1185   format_status_str( $main::current_changes, $commands );
1186   $main::dstat = "c";
1187   write_status_file() if $conf::statusdelay;
1188
1189   msg( "log", "processing $main::current_incoming_short/$commands\n" );
1190
1191   # parse the .commands file
1192   if ( !open( COMMANDS, "<$commands" ) ) {
1193     msg( "log", "Cannot open $main::current_incoming_short/$commands: $!\n" );
1194     return;
1195   }
1196   $pgplines        = 0;
1197   $main::mail_addr = "";
1198   @cmds            = ();
1199 outer_loop: while (<COMMANDS>) {
1200     if (/^---+(BEGIN|END) PGP .*---+$/) {
1201       ++$pgplines;
1202     } elsif (/^Uploader:\s*/i) {
1203       chomp( $main::mail_addr = $' );
1204       $main::mail_addr = $1 if $main::mail_addr =~ /<([^>]*)>/;
1205     } elsif (/^Commands:/i) {
1206       $_ = $';
1207       for ( ; ; ) {
1208         s/^\s*(.*)\s*$/$1/;    # delete whitespace at both ends
1209         if ( !/^\s*$/ ) {
1210           push( @cmds, $_ );
1211           debug("includes cmd $_");
1212         }
1213         last outer_loop if !defined( $_ = scalar(<COMMANDS>) );
1214         chomp;
1215         redo outer_loop if !/^\s/ || /^$/;
1216       } ## end for ( ; ; )
1217     } ## end elsif (/^Commands:/i)
1218   } ## end while (<COMMANDS>)
1219   close(COMMANDS);
1220
1221   # some consistency checks
1222   if ( !$main::mail_addr || $main::mail_addr !~ /^\S+\@\S+\.\S+/ ) {
1223     msg( "log,mail",
1224 "$main::current_incoming_short/$commands contains no or bad Uploader: field: "
1225         . "$main::mail_addr\n" );
1226     msg( "log,mail",
1227          "cannot process $main::current_incoming_short/$commands\n" );
1228     $main::mail_addr = "";
1229     goto remove;
1230   } ## end if ( !$main::mail_addr...
1231   msg( "log", "(command uploader $main::mail_addr)\n" );
1232
1233   if ( $pgplines < 3 ) {
1234     msg(
1235        "log,mail",
1236        "$main::current_incoming_short/$commands isn't signed with PGP/GnuPG\n"
1237     );
1238     msg(
1239       "mail",
1240       "or the uploaded file is broken. Make sure to transfer in binary mode\n"
1241     );
1242     msg( "mail", "or better yet - use dcut for commands files\n" );
1243     goto remove;
1244   } ## end if ( $pgplines < 3 )
1245
1246   # run PGP on the file to check the signature
1247   if ( !( $signator = pgp_check($commands) ) ) {
1248     msg(
1249       "log,mail",
1250       "$main::current_incoming_short/$commands has bad PGP/GnuPG signature!\n"
1251     );
1252   remove:
1253     msg( "log,mail", "Removing $main::current_incoming_short/$commands\n" );
1254     rm($commands);
1255     return;
1256   } elsif ( $signator eq "LOCAL ERROR" ) {
1257
1258     # An error has appened when starting pgp... Don't process the file,
1259     # but also don't delete it
1260     debug(
1261 "Can't PGP/GnuPG check $main::current_incoming_short/$commands -- don't process it for now"
1262     );
1263     return;
1264   } ## end elsif ( $signator eq "LOCAL ERROR")
1265   msg( "log", "(PGP/GnuPG signature by $signator)\n" );
1266
1267   # now process commands
1268   msg(
1269     "mail",
1270 "Log of processing your commands file $main::current_incoming_short/$commands:\n\n"
1271   );
1272   foreach $cmd (@cmds) {
1273     my @word = split( /\s+/, $cmd );
1274     msg( "mail,log", "> @word\n" );
1275     my $selecteddelayed = -1;
1276     next if @word < 1;
1277
1278     if ( $word[0] eq "rm" ) {
1279       foreach ( @word[ 1 .. $#word ] ) {
1280         my $origword = $_;
1281         if (m,^DELAYED/([0-9]+)-day/,) {
1282           $selecteddelayed = $1;
1283           s,^DELAYED/[0-9]+-day/,,;
1284         }
1285         if (m,(^|/)\*,) {
1286           msg("mail,log", "$_: filename component cannot start with a wildcard\n");
1287         } elsif ( $origword eq "--searchdirs" ) {
1288           $selecteddelayed = -2;
1289         } elsif (m,/,) {
1290           msg(
1291             "mail,log",
1292 "$_: filename may not contain slashes except for DELAYED/#-day/ prefixes\n"
1293           );
1294         } else {
1295
1296           # process wildcards but also plain names
1297           my (@thesefiles);
1298           my $pat = quotemeta($_);
1299           $pat =~ s/\\\*/.*/g;
1300           $pat =~ s/\\\?/.?/g;
1301           $pat =~ s/\\([][])/$1/g;
1302
1303           if ( $selecteddelayed < 0 ) {    # scanning or explicitly incoming
1304             opendir( DIR, "." );
1305             push( @thesefiles, grep /^$pat$/, readdir(DIR) );
1306             closedir(DIR);
1307           }
1308           if ( $selecteddelayed >= 0 ) {
1309             my $dir = sprintf( $conf::incoming_delayed, $selecteddelayed );
1310             opendir( DIR, $dir );
1311             push( @thesefiles,
1312                   map ( "$dir/$_", grep /^$pat$/, readdir(DIR) ) );
1313             closedir(DIR);
1314           } elsif ( $selecteddelayed == -2 ) {
1315             for ( my ($adelay) = 0 ;
1316                   ( !@thesefiles ) && $adelay <= $conf::max_delayed ;
1317                   $adelay++ )
1318             {
1319               my $dir = sprintf( $conf::incoming_delayed, $adelay );
1320               opendir( DIR, $dir );
1321               push( @thesefiles,
1322                     map ( "$dir/$_", grep /^$pat$/, readdir(DIR) ) );
1323               closedir(DIR);
1324             } ## end for ( my ($adelay) = 0 ...
1325           } ## end elsif ( $selecteddelayed ...
1326           push( @files, @thesefiles );
1327           if ( !@thesefiles ) {
1328             msg( "mail,log", "$origword did not match anything\n" );
1329           }
1330         } ## end else [ if ( $origword eq "--searchdirs")
1331       } ## end foreach ( @word[ 1 .. $#word...
1332       if ( !@files ) {
1333         msg( "mail,log", "No files to delete\n" );
1334       } else {
1335         @removed = ();
1336         foreach $file (@files) {
1337           if ( !-f $file ) {
1338             msg( "mail,log", "$file: no such file\n" );
1339           } elsif ( $file =~ /$conf::keep_files/ ) {
1340             msg( "mail,log", "$file is protected, cannot " . "remove\n" );
1341           } elsif ( !unlink($file) ) {
1342             msg( "mail,log", "$file: rm: $!\n" );
1343           } else {
1344             $file =~ s,$conf::incoming/?,,;
1345             push( @removed, $file );
1346           }
1347         } ## end foreach $file (@files)
1348         msg( "mail,log", "Files removed: @removed\n" ) if @removed;
1349       } ## end else [ if ( !@files )
1350     } elsif ( $word[0] eq "reschedule" ) {
1351       if ( @word != 3 ) {
1352         msg( "mail,log", "Wrong number of arguments\n" );
1353       } elsif ( $conf::upload_method ne "copy" ) {
1354         msg( "mail,log", "reschedule not available\n" );
1355       } elsif ( $word[1] =~ m,/, || $word[1] !~ m/\.changes/ ) {
1356         msg(
1357            "mail,log",
1358            "$word[1]: filename may not contain slashes and must be .changes\n"
1359         );
1360       } elsif ( !( ($target_delay) = $word[2] =~ m,^([0-9]+)-day$, )
1361                 || $target_delay > $conf::max_delayed )
1362       {
1363         msg(
1364           "mail,log",
1365 "$word[2]: rescheduling target must be #-day with # between 0 and $conf::max_delayed (in particular, no '/' allowed)\n"
1366         );
1367       } elsif ( $word[1] =~ /$conf::keep_files/ ) {
1368         msg( "mail,log", "$word[1] is protected, cannot do stuff with it\n" );
1369       } else {
1370         my ($adelay);
1371         for ( $adelay = 0 ;
1372             $adelay <= $conf::max_delayed
1373             && !-f (
1374               sprintf( "$conf::targetdir_delayed", $adelay ) . "/$word[1]" ) ;
1375             $adelay++ )
1376         {
1377         } ## end for ( $adelay = 0 ; $adelay...
1378         if ( $adelay > $conf::max_delayed ) {
1379           msg( "mail,log", "$word[1] not found\n" );
1380         } elsif ( $adelay == $target_delay ) {
1381           msg( "mail,log", "$word[1] already is in $word[2]\n" );
1382         } else {
1383           my (@thesefiles);
1384           my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
1385           my ($target_dir) =
1386             sprintf( "$conf::targetdir_delayed", $target_delay );
1387           push( @thesefiles, $word[1] );
1388           push( @thesefiles,
1389                 get_filelist_from_known_good_changes("$dir/$word[1]") );
1390           for my $afile (@thesefiles) {
1391             if ( $afile =~ m/\.changes$/ ) {
1392               utime undef, undef, ("$dir/$afile");
1393             }
1394             if ( !move("$dir/$afile", "$target_dir/$afile") ) {
1395               msg( "mail,log", "move: $!\n" );
1396             } else {
1397               msg( "mail,log", "$afile moved to $target_delay-day\n" );
1398             }
1399           } ## end for my $afile (@thesefiles)
1400         } ## end else [ if ( $adelay > $conf::max_delayed)
1401       } ## end else [ if ( @word != 3 )
1402     } elsif ( $word[0] eq "cancel" ) {
1403       if ( @word != 2 ) {
1404         msg( "mail,log", "Wrong number of arguments\n" );
1405       } elsif ( $conf::upload_method ne "copy" ) {
1406         msg( "mail,log", "cancel not available\n" );
1407       } elsif (
1408           $word[1] !~ m,^[a-zA-Z0-9.+_:@=%-][~a-zA-Z0-9.+_:@=%-]*\.changes$, )
1409       {
1410         msg( "mail,log",
1411           "argument to cancel must be one .changes filename without path\n" );
1412       } ## end elsif ( $word[1] !~ ...
1413       my (@files) = ();
1414       for ( my ($adelay) = 0 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
1415         my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
1416         if ( -f "$dir/$word[1]" ) {
1417           @removed = ();
1418           push( @files, "$word[1]" );
1419           push( @files,
1420                 get_filelist_from_known_good_changes("$dir/$word[1]") );
1421           foreach $file (@files) {
1422             if ( !-f "$dir/$file" ) {
1423               msg( "mail,log", "$dir/$file: no such file\n" );
1424             } elsif ( "$dir/$file" =~ /$conf::keep_files/ ) {
1425               msg( "mail,log",
1426                    "$dir/$file is protected, cannot " . "remove\n" );
1427             } elsif ( !unlink("$dir/$file") ) {
1428               msg( "mail,log", "$dir/$file: rm: $!\n" );
1429             } else {
1430               push( @removed, $file );
1431             }
1432           } ## end foreach $file (@files)
1433           msg( "mail,log", "Files removed from $adelay-day: @removed\n" )
1434             if @removed;
1435         } ## end if ( -f "$dir/$word[1]")
1436       } ## end for ( my ($adelay) = 0 ...
1437       if ( !@files ) {
1438         msg( "mail,log", "No upload found: $word[1]\n" );
1439       }
1440     } else {
1441       msg( "mail,log", "unknown command $word[0]\n" );
1442     }
1443   } ## end foreach $cmd (@cmds)
1444   rm($commands);
1445   msg( "log",
1446        "-- End of $main::current_incoming_short/$commands processing\n" );
1447 } ## end sub process_commands($)
1448
1449 sub age_delayed_queues() {
1450   for ( my ($adelay) = 0 ; $adelay <= $conf::max_delayed ; $adelay++ ) {
1451     my ($dir) = sprintf( "$conf::targetdir_delayed", $adelay );
1452     my ($target_dir);
1453     if ( $adelay == 0 ) {
1454       $target_dir = $conf::targetdir;
1455     } else {
1456       $target_dir = sprintf( "$conf::targetdir_delayed", $adelay - 1 );
1457     }
1458     for my $achanges (<$dir/*.changes>) {
1459       my $mtime = ( stat($achanges) )[9];
1460       if ( $mtime + 24 * 60 * 60 <= time || $adelay == 0 ) {
1461         utime undef, undef, ($achanges);
1462         my @thesefiles = ( $achanges =~ m,.*/([^/]*), );
1463         push( @thesefiles, get_filelist_from_known_good_changes($achanges) );
1464         for my $afile (@thesefiles) {
1465           if ( !move("$dir/$afile", "$target_dir/$afile") ) {
1466             msg( "log", "move: $!\n" );
1467           } else {
1468             msg( "log", "$afile moved to $target_dir\n" );
1469           }
1470         } ## end for my $afile (@thesefiles)
1471       } ## end if ( $mtime + 24 * 60 ...
1472     } ## end for my $achanges (<$dir/*.changes>)
1473   } ## end for ( my ($adelay) = 0 ...
1474 } ## end sub age_delayed_queues()
1475
1476 #
1477 # check if a file is already on target
1478 #
1479 sub is_on_target($\@) {
1480   my $file     = shift;
1481   my $filelist = shift;
1482   my $msg;
1483   my $stat;
1484
1485   if ( $conf::upload_method eq "ssh" ) {
1486     ( $msg, $stat ) = ssh_cmd("ls -l $file");
1487   } elsif ( $conf::upload_method eq "ftp" ) {
1488     my $err;
1489     ( $msg, $err ) = ftp_cmd( "dir", $file );
1490     if ($err) {
1491       $stat = 1;
1492       $msg  = $err;
1493     } elsif ( !$msg ) {
1494       $stat = 1;
1495       $msg  = "ls: no such file\n";
1496     } else {
1497       $stat = 0;
1498       $msg = join( "\n", @$msg );
1499     }
1500   } else {
1501     my @allfiles = ($file);
1502     push( @allfiles, @$filelist );
1503     $stat = 1;
1504     $msg  = "no such file";
1505     for my $afile (@allfiles) {
1506       if ( -f "$conf::targetdir/$afile" ) {
1507         $stat = 0;
1508         $msg  = "$afile";
1509       }
1510     } ## end for my $afile (@allfiles)
1511     for ( my ($adelay) = 0 ;
1512           $adelay <= $conf::max_delayed && $stat ;
1513           $adelay++ )
1514     {
1515       for my $afile (@allfiles) {
1516         if (
1517            -f ( sprintf( "$conf::targetdir_delayed", $adelay ) . "/$afile" ) )
1518         {
1519           $stat = 0;
1520           $msg = sprintf( "%d-day", $adelay ) . "/$afile";
1521         } ## end if ( -f ( sprintf( "$conf::targetdir_delayed"...
1522       } ## end for my $afile (@allfiles)
1523     } ## end for ( my ($adelay) = 0 ...
1524   } ## end else [ if ( $conf::upload_method...
1525   chomp($msg);
1526   debug("exit status: $stat, output was: $msg");
1527
1528   return "" if $stat && $msg =~ /no such file/i;    # file not present
1529   msg( "log", "strange ls -l output on target:\n", $msg ), return ""
1530     if $stat || $@;    # some other error, but still try to upload
1531
1532   # ls -l returned 0 -> file already there
1533   $msg =~ s/\s\s+/ /g;    # make multiple spaces into one, to save space
1534   return $msg;
1535 } ## end sub is_on_target($\@)
1536
1537 #
1538 # copy a list of files to target
1539 #
1540 sub copy_to_target(@) {
1541   my @files = @_;
1542   my ( @md5sum, @expected_files, $sum, $name, $msgs, $stat );
1543
1544   $main::dstat = "u";
1545   write_status_file() if $conf::statusdelay;
1546
1547   # copy the files
1548   if ( $conf::upload_method eq "ssh" ) {
1549     ( $msgs, $stat ) = scp_cmd(@files);
1550     goto err if $stat;
1551   } elsif ( $conf::upload_method eq "ftp" ) {
1552     my ( $rv, $file );
1553     if ( !$main::FTP_chan->cwd($main::current_targetdir) ) {
1554       msg( "log,mail",
1555            "Can't cd to $main::current_targetdir on $conf::target\n" );
1556       goto err;
1557     }
1558     foreach $file (@files) {
1559       ( $rv, $msgs ) = ftp_cmd( "put", $file );
1560       goto err if !$rv;
1561     }
1562   } else {
1563     ( $msgs, $stat ) =
1564       local_cmd( "$conf::cp @files $main::current_targetdir", 'NOCD' );
1565     goto err if $stat;
1566   }
1567
1568   # check md5sums or sizes on target against our own
1569   my $have_md5sums = 1;
1570   if ($conf::check_md5sum) {
1571     if ( $conf::upload_method eq "ssh" ) {
1572       ( $msgs, $stat ) = ssh_cmd("md5sum @files");
1573       goto err if $stat;
1574       @md5sum = split( "\n", $msgs );
1575     } elsif ( $conf::upload_method eq "ftp" ) {
1576       my ( $rv, $err, $file );
1577       foreach $file (@files) {
1578         ( $rv, $err ) = ftp_cmd( "quot", "site", "md5sum", $file );
1579         if ($err) {
1580           next if ftp_code() == 550;    # file not found
1581           if ( ftp_code() == 500 ) {    # unimplemented
1582             $have_md5sums = 0;
1583             goto get_sizes_instead;
1584           }
1585           $msgs = $err;
1586           goto err;
1587         } ## end if ($err)
1588         chomp( my $t = ftp_response() );
1589         push( @md5sum, $t );
1590       } ## end foreach $file (@files)
1591       if ( !$have_md5sums ) {
1592       get_sizes_instead:
1593         foreach $file (@files) {
1594           ( $rv, $err ) = ftp_cmd( "size", $file );
1595           if ($err) {
1596             next if ftp_code() == 550;    # file not found
1597             $msgs = $err;
1598             goto err;
1599           }
1600           push( @md5sum, "$rv $file" );
1601         } ## end foreach $file (@files)
1602       } ## end if ( !$have_md5sums )
1603     } else {
1604       ( $msgs, $stat ) = local_cmd("$conf::md5sum @files");
1605       goto err if $stat;
1606       @md5sum = split( "\n", $msgs );
1607     }
1608
1609     @expected_files = @files;
1610     foreach (@md5sum) {
1611       chomp;
1612       ( $sum, $name ) = split;
1613       next if !grep { $_ eq $name } @files;    # a file we didn't upload??
1614       next if $sum eq "md5sum:";               # looks like an error message
1615       if (    ( $have_md5sums && $sum ne md5sum($name) )
1616            || ( !$have_md5sums && $sum != ( -s $name ) ) )
1617       {
1618         msg(
1619              "log,mail",
1620              "Upload of $name to $conf::target failed ",
1621              "(" . ( $have_md5sums ? "md5sum" : "size" ) . " mismatch)\n"
1622            );
1623         goto err;
1624       } ## end if ( ( $have_md5sums &&...
1625
1626       # seen that file, remove it from expect list
1627       @expected_files = map { $_ eq $name ? () : $_ } @expected_files;
1628     } ## end foreach (@md5sum)
1629     if (@expected_files) {
1630       msg( "log,mail", "Failed to upload the files\n" );
1631       msg( "log,mail", "  ", join( ", ", @expected_files ), "\n" );
1632       msg( "log,mail", "(Not present on target after upload)\n" );
1633       goto err;
1634     } ## end if (@expected_files)
1635   } ## end if ($conf::check_md5sum)
1636
1637   if ($conf::chmod_on_target) {
1638
1639     # change file's mode explicitly to 644 on target
1640     if ( $conf::upload_method eq "ssh" ) {
1641       ( $msgs, $stat ) = ssh_cmd("chmod 644 @files");
1642       goto err if $stat;
1643     } elsif ( $conf::upload_method eq "ftp" ) {
1644       my ( $rv, $file );
1645       foreach $file (@files) {
1646         ( $rv, $msgs ) = ftp_cmd( "quot", "site", "chmod", "644", $file );
1647         msg( "log", "Can't chmod $file on target:\n$msgs" )
1648           if $msgs;
1649         goto err if !$rv;
1650       } ## end foreach $file (@files)
1651     } else {
1652       ( $msgs, $stat ) = local_cmd("$conf::chmod 644 @files");
1653       goto err if $stat;
1654     }
1655   } ## end if ($conf::chmod_on_target)
1656
1657   $main::dstat = "c";
1658   write_status_file() if $conf::statusdelay;
1659   return 1;
1660
1661 err:
1662   msg( "log,mail",
1663        "Upload to $conf::target failed",
1664        $? ? ", last exit status " . sprintf( "%s", $? >> 8 ) : "", "\n" );
1665   msg( "log,mail", "Error messages:\n", $msgs )
1666     if $msgs;
1667
1668   # If "permission denied" was among the errors, test if the incoming is
1669   # writable at all.
1670   if ( $msgs =~ /(permission denied|read-?only file)/i ) {
1671     if ( !check_incoming_writable() ) {
1672       msg( "log,mail", "(The incoming directory seems to be ",
1673            "unwritable.)\n" );
1674     }
1675   } ## end if ( $msgs =~ /(permission denied|read-?only file)/i)
1676
1677   # remove bad files or an incomplete upload on target
1678   if ( $conf::upload_method eq "ssh" ) {
1679     ssh_cmd("rm -f @files");
1680   } elsif ( $conf::upload_method eq "ftp" ) {
1681     my $file;
1682     foreach $file (@files) {
1683       my ( $rv, $err );
1684       ( $rv, $err ) = ftp_cmd( "delete", $file );
1685       msg( "log", "Can't delete $file on target:\n$err" )
1686         if $err;
1687     } ## end foreach $file (@files)
1688   } else {
1689     my @tfiles = map { "$main::current_targetdir/$_" } @files;
1690     debug("executing unlink(@tfiles)");
1691     rm(@tfiles);
1692   }
1693   $main::dstat = "c";
1694   write_status_file() if $conf::statusdelay;
1695   return 0;
1696 } ## end sub copy_to_target(@)
1697
1698 #
1699 # check if a file is correctly signed with PGP
1700 #
1701 sub pgp_check($) {
1702   my $file   = shift;
1703   my $output = "";
1704   my $signator;
1705   my $found = 0;
1706   my $stat;
1707   local (*PIPE);
1708
1709   $stat = 1;
1710   if ( -x $conf::gpg ) {
1711     debug(   "executing $conf::gpg --no-options --batch "
1712            . "--no-default-keyring --always-trust "
1713            . "--keyring "
1714            . join( " --keyring ", @conf::keyrings )
1715            . " --verify '$file'" );
1716     if (
1717          !open( PIPE,
1718                     "$conf::gpg --no-options --batch "
1719                   . "--no-default-keyring --always-trust "
1720                   . "--keyring "
1721                   . join( " --keyring ", @conf::keyrings )
1722                   . " --verify '$file'"
1723                   . " 2>&1 |"
1724               )
1725        )
1726     {
1727       msg( "log", "Can't open pipe to $conf::gpg: $!\n" );
1728       return "LOCAL ERROR";
1729     } ## end if ( !open( PIPE, "$conf::gpg --no-options --batch "...
1730     $output .= $_ while (<PIPE>);
1731     close(PIPE);
1732     $stat = $?;
1733   } ## end if ( -x $conf::gpg )
1734
1735   if ($stat) {
1736     msg( "log,mail", "GnuPG signature check failed on $file\n" );
1737     msg( "mail",     $output );
1738     msg( "log,mail", "(Exit status ", $stat >> 8, ")\n" );
1739     return "";
1740   } ## end if ($stat)
1741
1742   $output =~ /^(gpg: )?good signature from (user )?"(.*)"\.?$/im;
1743   ( $signator = $3 ) ||= "unknown signator";
1744   if ($conf::debug) {
1745     debug("GnuPG signature ok (by $signator)");
1746   }
1747   return $signator;
1748 } ## end sub pgp_check($)
1749
1750 # ---------------------------------------------------------------------------
1751 #                                                         the status daemon
1752 # ---------------------------------------------------------------------------
1753
1754 #
1755 # fork a subprocess that watches the 'status' FIFO
1756 #
1757 # that process blocks until someone opens the FIFO, then sends a
1758 # signal (SIGUSR1) to the main process, expects
1759 #
1760 sub fork_statusd() {
1761   my $statusd_pid;
1762   my $main_pid = $$;
1763   my $errs;
1764   local (*STATFIFO);
1765
1766   $statusd_pid = open( STATUSD, "|-" );
1767   die "cannot fork: $!\n" if !defined($statusd_pid);
1768
1769   # parent just returns
1770   if ($statusd_pid) {
1771     msg( "log", "forked status daemon (pid $statusd_pid)\n" );
1772     return $statusd_pid;
1773   }
1774
1775   # child: the status FIFO daemon
1776
1777   # ignore SIGPIPE here, in case some closes the FIFO without completely
1778   # reading it
1779   $SIG{"PIPE"} = "IGNORE";
1780
1781   # also ignore SIGCLD, we don't want to inherit the restart-statusd handler
1782   # from our parent
1783   $SIG{"CHLD"} = "DEFAULT";
1784
1785   rm($conf::statusfile);
1786   $errs = `$conf::mkfifo $conf::statusfile`;
1787   die "$main::progname: cannot create named pipe $conf::statusfile: $errs"
1788     if $?;
1789   chmod( 0644, $conf::statusfile )
1790     or die "Cannot set modes of $conf::statusfile: $!\n";
1791
1792   # close log file, so that log rotating works
1793   close(LOG);
1794   close(STDOUT);
1795   close(STDERR);
1796
1797   while (1) {
1798     my ( $status, $mup, $incw, $ds, $next_run, $last_ping, $currch, $l );
1799
1800     # open the FIFO for writing; this blocks until someone (probably ftpd)
1801     # opens it for reading
1802     open( STATFIFO, ">$conf::statusfile" )
1803       or die "Cannot open $conf::statusfile\n";
1804     select(STATFIFO);
1805
1806     # tell main daemon to send us status infos
1807     kill( $main::signo{"USR1"}, $main_pid );
1808
1809     # get the infos from stdin; must loop until enough bytes received!
1810     my $expect_len = 3 + 2 * STATNUM_LEN + STATSTR_LEN;
1811     for ( $status = "" ; ( $l = length($status) ) < $expect_len ; ) {
1812       sysread( STDIN, $status, $expect_len - $l, $l );
1813     }
1814
1815     # disassemble the status byte stream
1816     my $pos = 0;
1817     foreach (
1818               [ mup       => 1 ],
1819               [ incw      => 1 ],
1820               [ ds        => 1 ],
1821               [ next_run  => STATNUM_LEN ],
1822               [ last_ping => STATNUM_LEN ],
1823               [ currch    => STATSTR_LEN ]
1824             )
1825     {
1826       eval "\$$_->[0] = substr( \$status, $pos, $_->[1] );";
1827       $pos += $_->[1];
1828     } ## end foreach ( [ mup => 1 ], [ incw...
1829     $currch =~ s/\n+//g;
1830
1831     print_status( $mup, $incw, $ds, $next_run, $last_ping, $currch );
1832     close(STATFIFO);
1833
1834     # This sleep is necessary so that we can't reopen the FIFO
1835     # immediately, in case the reader hasn't closed it yet if we get to
1836     # the open again. Is there a better solution for this??
1837     sleep 1;
1838   } ## end while (1)
1839 } ## end sub fork_statusd()
1840
1841 #
1842 # update the status file, in case we use a plain file and not a FIFO
1843 #
1844 sub write_status_file() {
1845
1846   return if !$conf::statusfile;
1847
1848   open( STATFILE, ">$conf::statusfile" )
1849     or ( msg( "log", "Could not open $conf::statusfile: $!\n" ), return );
1850   my $oldsel = select(STATFILE);
1851
1852   print_status(
1853                 $main::target_up,      $main::incoming_writable,
1854                 $main::dstat,          $main::next_run,
1855                 $main::last_ping_time, $main::current_changes
1856               );
1857
1858   select($oldsel);
1859   close(STATFILE);
1860 } ## end sub write_status_file()
1861
1862 sub print_status($$$$$$) {
1863   my $mup       = shift;
1864   my $incw      = shift;
1865   my $ds        = shift;
1866   my $next_run  = shift;
1867   my $last_ping = shift;
1868   my $currch    = shift;
1869   my $approx;
1870   my $version;
1871
1872   ( $version = 'Release: 0.9 $Revision: 1.51 $' ) =~ s/\$ ?//g;
1873   print "debianqueued $version\n";
1874
1875   $approx = $conf::statusdelay ? "approx. " : "";
1876
1877   if ( $mup eq "0" ) {
1878     print "$conf::target is down, queue pausing\n";
1879     return;
1880   } elsif ( $conf::upload_method ne "copy" ) {
1881     print "$conf::target seems to be up, last ping $approx",
1882       print_time( time - $last_ping ), " ago\n";
1883   }
1884
1885   if ( $incw eq "0" ) {
1886     print "The incoming directory is not writable, queue pausing\n";
1887     return;
1888   }
1889
1890   if ( $ds eq "i" ) {
1891     print "Next queue check in $approx", print_time( $next_run - time ), "\n";
1892     return;
1893   } elsif ( $ds eq "c" ) {
1894     print "Checking queue directory\n";
1895   } elsif ( $ds eq "u" ) {
1896     print "Uploading to $conf::target\n";
1897   } else {
1898     print "Bad status data from daemon: \"$mup$incw$ds\"\n";
1899     return;
1900   }
1901
1902   print "Current job is $currch\n" if $currch;
1903 } ## end sub print_status($$$$$$)
1904
1905 #
1906 # format a number for sending to statusd (fixed length STATNUM_LEN)
1907 #
1908 sub format_status_num(\$$) {
1909   my $varref = shift;
1910   my $num    = shift;
1911
1912   $$varref = sprintf "%" . STATNUM_LEN . "d", $num;
1913 } ## end sub format_status_num(\$$)
1914
1915 #
1916 # format a string for sending to statusd (fixed length STATSTR_LEN)
1917 #
1918 sub format_status_str(\$$) {
1919   my $varref = shift;
1920   my $str    = shift;
1921
1922   $$varref = substr( $str, 0, STATSTR_LEN );
1923   $$varref .= "\n" x ( STATSTR_LEN - length($$varref) );
1924 } ## end sub format_status_str(\$$)
1925
1926 #
1927 # send a status string to the status daemon
1928 #
1929 # Avoid all operations that could call malloc() here! Most libc
1930 # implementations aren't reentrant, so we may not call it from a
1931 # signal handler. So use only already-defined variables.
1932 #
1933 sub send_status() {
1934   local $! = 0;    # preserve errno
1935
1936   # re-setup handler, in case we have broken SysV signals
1937   $SIG{"USR1"} = \&send_status;
1938
1939   syswrite( STATUSD, $main::target_up,         1 );
1940   syswrite( STATUSD, $main::incoming_writable, 1 );
1941   syswrite( STATUSD, $main::dstat,             1 );
1942   syswrite( STATUSD, $main::next_run,          STATNUM_LEN );
1943   syswrite( STATUSD, $main::last_ping_time,    STATNUM_LEN );
1944   syswrite( STATUSD, $main::current_changes,   STATSTR_LEN );
1945 } ## end sub send_status()
1946
1947 # ---------------------------------------------------------------------------
1948 #                                                           FTP functions
1949 # ---------------------------------------------------------------------------
1950
1951 #
1952 # open FTP connection to target host if not already open
1953 #
1954 sub ftp_open() {
1955
1956   if ($main::FTP_chan) {
1957
1958     # is already open, but might have timed out; test with a cwd
1959     return $main::FTP_chan
1960       if $main::FTP_chan->cwd($main::current_targetdir);
1961
1962     # cwd didn't work, channel is closed, try to reopen it
1963     $main::FTP_chan = undef;
1964   } ## end if ($main::FTP_chan)
1965
1966   if (
1967        !(
1968           $main::FTP_chan =
1969           Net::FTP->new(
1970                          $conf::target,
1971                          Debug   => $conf::ftpdebug,
1972                          Timeout => $conf::ftptimeout,
1973                          Passive => 1,
1974                        )
1975         )
1976      )
1977   {
1978     msg( "log,mail", "Cannot open FTP server $conf::target\n" );
1979     goto err;
1980   } ## end if ( !( $main::FTP_chan...
1981   if ( !$main::FTP_chan->login() ) {
1982     msg( "log,mail", "Anonymous login on FTP server $conf::target failed\n" );
1983     goto err;
1984   }
1985   if ( !$main::FTP_chan->binary() ) {
1986     msg( "log,mail", "Can't set binary FTP mode on $conf::target\n" );
1987     goto err;
1988   }
1989   if ( !$main::FTP_chan->cwd($main::current_targetdir) ) {
1990     msg( "log,mail",
1991          "Can't cd to $main::current_targetdir on $conf::target\n" );
1992     goto err;
1993   }
1994   debug("opened FTP channel to $conf::target");
1995   return 1;
1996
1997 err:
1998   $main::FTP_chan = undef;
1999   return 0;
2000 } ## end sub ftp_open()
2001
2002 sub ftp_cmd($@) {
2003   my $cmd = shift;
2004   my ( $rv, $err );
2005   my $direct_resp_cmd = ( $cmd eq "quot" );
2006
2007   debug( "executing FTP::$cmd(" . join( ", ", @_ ) . ")" );
2008   $SIG{"ALRM"} = sub { die "timeout in FTP::$cmd\n" };
2009   alarm($conf::remote_timeout);
2010   eval { $rv = $main::FTP_chan->$cmd(@_); };
2011   alarm(0);
2012   $err = "";
2013   $rv = ( ftp_code() =~ /^2/ ) ? 1 : 0 if $direct_resp_cmd;
2014   if ($@) {
2015     $err = $@;
2016     undef $rv;
2017   } elsif ( !$rv ) {
2018     $err = ftp_response();
2019   }
2020   return ( $rv, $err );
2021 } ## end sub ftp_cmd($@)
2022
2023 sub ftp_close() {
2024   if ($main::FTP_chan) {
2025     $main::FTP_chan->quit();
2026     $main::FTP_chan = undef;
2027   }
2028   return 1;
2029 } ## end sub ftp_close()
2030
2031 sub ftp_response() {
2032   return join( '', @{ ${*$main::FTP_chan}{'net_cmd_resp'} } );
2033 }
2034
2035 sub ftp_code() {
2036   return ${*$main::FTP_chan}{'net_cmd_code'};
2037 }
2038
2039 sub ftp_error() {
2040   my $code = ftp_code();
2041   return ( $code =~ /^[45]/ ) ? 1 : 0;
2042 }
2043
2044 # ---------------------------------------------------------------------------
2045 #                                                         utility functions
2046 # ---------------------------------------------------------------------------
2047
2048 sub ssh_cmd($) {
2049   my $cmd = shift;
2050   my ( $msg, $stat );
2051
2052   my $ecmd = "$conf::ssh $conf::ssh_options $conf::target "
2053     . "-l $conf::targetlogin \'cd $main::current_targetdir; $cmd\'";
2054   debug("executing $ecmd");
2055   $SIG{"ALRM"} = sub { die "timeout in ssh command\n" };
2056   alarm($conf::remote_timeout);
2057   eval { $msg = `$ecmd 2>&1`; };
2058   alarm(0);
2059   if ($@) {
2060     $msg  = $@;
2061     $stat = 1;
2062   } else {
2063     $stat = $?;
2064   }
2065   return ( $msg, $stat );
2066 } ## end sub ssh_cmd($)
2067
2068 sub scp_cmd(@) {
2069   my ( $msg, $stat );
2070
2071   my $ecmd = "$conf::scp $conf::ssh_options @_ "
2072     . "$conf::targetlogin\@$conf::target:$main::current_targetdir";
2073   debug("executing $ecmd");
2074   $SIG{"ALRM"} = sub { die "timeout in scp\n" };
2075   alarm($conf::remote_timeout);
2076   eval { $msg = `$ecmd 2>&1`; };
2077   alarm(0);
2078   if ($@) {
2079     $msg  = $@;
2080     $stat = 1;
2081   } else {
2082     $stat = $?;
2083   }
2084   return ( $msg, $stat );
2085 } ## end sub scp_cmd(@)
2086
2087 sub local_cmd($;$) {
2088   my $cmd  = shift;
2089   my $nocd = shift;
2090   my ( $msg, $stat );
2091
2092   my $ecmd = ( $nocd ? "" : "cd $main::current_targetdir; " ) . $cmd;
2093   debug("executing $ecmd");
2094   $msg  = `($ecmd) 2>&1`;
2095   $stat = $?;
2096   return ( $msg, $stat );
2097
2098 } ## end sub local_cmd($;$)
2099
2100 #
2101 # check if target is alive (code stolen from Net::Ping.pm)
2102 #
2103 sub check_alive(;$) {
2104   my $timeout = shift;
2105   my ( $saddr, $ret, $target_ip );
2106   local (*PINGSOCK);
2107
2108   if ( $conf::upload_method eq "copy" ) {
2109     format_status_num( $main::last_ping_time, time );
2110     $main::target_up = 1;
2111     return;
2112   }
2113
2114   $timeout ||= 30;
2115
2116   if ( !( $target_ip = ( gethostbyname($conf::target) )[4] ) ) {
2117     msg( "log", "Cannot get IP address of $conf::target\n" );
2118     $ret = 0;
2119     goto out;
2120   }
2121   $saddr = pack( 'S n a4 x8', AF_INET, $main::echo_port, $target_ip );
2122   $SIG{'ALRM'} = sub { die };
2123   alarm($timeout);
2124
2125   $ret = $main::tcp_proto;    # avoid warnings about unused variable
2126   $ret = 0;
2127   eval <<'EOM' ;
2128     return unless socket( PINGSOCK, PF_INET, SOCK_STREAM, $main::tcp_proto );
2129     return unless connect( PINGSOCK, $saddr );
2130     $ret = 1;
2131 EOM
2132   alarm(0);
2133   close(PINGSOCK);
2134   msg( "log", "pinging $conf::target: " . ( $ret ? "ok" : "down" ) . "\n" );
2135 out:
2136   $main::target_up = $ret ? "1" : "0";
2137   format_status_num( $main::last_ping_time, time );
2138   write_status_file() if $conf::statusdelay;
2139 } ## end sub check_alive(;$)
2140
2141 #
2142 # check if incoming dir on target is writable
2143 #
2144 sub check_incoming_writable() {
2145   my $testfile = ".debianqueued-testfile";
2146   my ( $msg, $stat );
2147
2148   if ( $conf::upload_method eq "ssh" ) {
2149     ( $msg, $stat ) =
2150       ssh_cmd( "rm -f $testfile; touch $testfile; " . "rm -f $testfile" );
2151   } elsif ( $conf::upload_method eq "ftp" ) {
2152     my $file = "junk-for-writable-test-" . format_time();
2153     $file =~ s/[ :.]/-/g;
2154     local (*F);
2155     open( F, ">$file" );
2156     close(F);
2157     my $rv;
2158     ( $rv, $msg ) = ftp_cmd( "put", $file );
2159     $stat = 0;
2160     $msg = "" if !defined $msg;
2161     unlink $file;
2162     ftp_cmd( "delete", $file );
2163   } elsif ( $conf::upload_method eq "copy" ) {
2164     ( $msg, $stat ) =
2165       local_cmd( "rm -f $testfile; touch $testfile; " . "rm -f $testfile" );
2166   }
2167   chomp($msg);
2168   debug("exit status: $stat, output was: $msg");
2169
2170   if ( !$stat ) {
2171
2172     # change incoming_writable only if ssh didn't return an error
2173     $main::incoming_writable =
2174       ( $msg =~ /(permission denied|read-?only file|cannot create)/i )
2175       ? "0"
2176       : "1";
2177   } else {
2178     debug("local error, keeping old status");
2179   }
2180   debug("incoming_writable = $main::incoming_writable");
2181   write_status_file() if $conf::statusdelay;
2182   return $main::incoming_writable;
2183 } ## end sub check_incoming_writable()
2184
2185 #
2186 # remove a list of files, log failing ones
2187 #
2188 sub rm(@) {
2189   my $done = 0;
2190
2191   foreach (@_) {
2192     ( unlink $_ and ++$done )
2193       or $! == ENOENT
2194       or msg( "log", "Could not delete $_: $!\n" );
2195   }
2196   return $done;
2197 } ## end sub rm(@)
2198
2199 #
2200 # get md5 checksum of a file
2201 #
2202 sub md5sum($) {
2203   my $file = shift;
2204   my $line;
2205
2206   chomp( $line = `$conf::md5sum $file` );
2207   debug( "md5sum($file): ",
2208            $? ? "exit status $?"
2209          : $line =~ /^(\S+)/ ? $1
2210          :                     "match failed" );
2211   return $? ? "" : $line =~ /^(\S+)/ ? $1 : "";
2212 } ## end sub md5sum($)
2213
2214 #
2215 # check if a file probably belongs to a Debian upload
2216 #
2217 sub is_debian_file($) {
2218   my $file = shift;
2219   return $file =~ /\.(deb|dsc|(diff|tar)\.gz)$/
2220     && $file !~ /\.orig\.tar\.gz/;
2221 }
2222
2223 #
2224 # try to extract maintainer email address from some a non-.changes file
2225 # return "" if not possible
2226 #
2227 sub get_maintainer($) {
2228   my $file       = shift;
2229   my $maintainer = "";
2230   local (*F);
2231
2232   if ( $file =~ /\.diff\.gz$/ ) {
2233
2234     # parse a diff
2235     open( F, "$conf::gzip -dc '$file' 2>/dev/null |" ) or return "";
2236     while (<F>) {
2237
2238       # look for header line of a file */debian/control
2239       last if m,^\+\+\+\s+[^/]+/debian/control(\s+|$),;
2240     }
2241     while (<F>) {
2242       last if /^---/;   # end of control file patch, no Maintainer: found
2243                         # inside control file patch look for Maintainer: field
2244       $maintainer = $1, last if /^\+Maintainer:\s*(.*)$/i;
2245     }
2246     while (<F>) { }     # read to end of file to avoid broken pipe
2247     close(F) or return "";
2248   } elsif ( $file =~ /\.(deb|dsc|tar\.gz)$/ ) {
2249     if ( $file =~ /\.deb$/ && $conf::ar ) {
2250
2251       # extract control.tar.gz from .deb with ar, then let tar extract
2252       # the control file itself
2253       open( F,
2254                 "($conf::ar p '$file' control.tar.gz | "
2255               . "$conf::tar -xOf - "
2256               . "--use-compress-program $conf::gzip "
2257               . "control) 2>/dev/null |"
2258           ) or return "";
2259     } elsif ( $file =~ /\.dsc$/ ) {
2260
2261       # just do a plain grep
2262       debug("get_maint: .dsc, no cmd");
2263       open( F, "<$file" ) or return "";
2264     } elsif ( $file =~ /\.tar\.gz$/ ) {
2265
2266       # let tar extract a file */debian/control
2267       open( F,
2268                 "$conf::tar -xOf '$file' "
2269               . "--use-compress-program $conf::gzip "
2270               . "\\*/debian/control 2>&1 |"
2271           ) or return "";
2272     } else {
2273       return "";
2274     }
2275     while (<F>) {
2276       $maintainer = $1, last if /^Maintainer:\s*(.*)$/i;
2277     }
2278     close(F) or return "";
2279   } ## end elsif ( $file =~ /\.(deb|dsc|tar\.gz)$/)
2280
2281   return $maintainer;
2282 } ## end sub get_maintainer($)
2283
2284 #
2285 # return a pattern that matches all files that probably belong to one job
2286 #
2287 sub debian_file_stem($) {
2288   my $file = shift;
2289   my ( $pkg, $version );
2290
2291   # strip file suffix
2292   $file =~ s,\.(deb|dsc|changes|(orig\.)?tar\.gz|diff\.gz)$,,;
2293
2294   # if not is *_* (name_version), can't derive a stem and return just
2295   # the file's name
2296   return $file if !( $file =~ /^([^_]+)_([^_]+)/ );
2297   ( $pkg, $version ) = ( $1, $2 );
2298
2299   # strip Debian revision from version
2300   $version =~ s/^(.*)-[\d.+-]+$/$1/;
2301
2302   return "${pkg}_${version}*";
2303 } ## end sub debian_file_stem($)
2304
2305 #
2306 # output a messages to several destinations
2307 #
2308 # first arg is a comma-separated list of destinations; valid are "log"
2309 # and "mail"; rest is stuff to be printed, just as with print
2310 #
2311 sub msg($@) {
2312   my @dest = split( ',', shift );
2313
2314   if ( grep /log/, @dest ) {
2315     my $now = format_time();
2316     print LOG "$now ", @_;
2317   }
2318
2319   if ( grep /mail/, @dest ) {
2320     $main::mail_text .= join( '', @_ );
2321   }
2322 } ## end sub msg($@)
2323
2324 #
2325 # print a debug messages, if $debug is true
2326 #
2327 sub debug(@) {
2328   return if !$conf::debug;
2329   my $now = format_time();
2330   print LOG "$now DEBUG ", @_, "\n";
2331 }
2332
2333 #
2334 # intialize the "mail" destination of msg() (this clears text,
2335 # address, subject, ...)
2336 #
2337 sub init_mail(;$) {
2338   my $file = shift;
2339
2340   $main::mail_addr    = "";
2341   $main::mail_text    = "";
2342   %main::packages     = ();
2343   $main::mail_subject = $file ? "Processing of $file" : "";
2344 } ## end sub init_mail(;$)
2345
2346 #
2347 # finalize mail to be sent from msg(): check if something present, and
2348 # then send out
2349 #
2350 sub finish_mail() {
2351
2352   debug("No mail for $main::mail_addr")
2353     if $main::mail_addr && !$main::mail_text;
2354   return unless $main::mail_addr && $main::mail_text;
2355
2356   if ( !send_mail( $main::mail_addr, $main::mail_subject, $main::mail_text ) )
2357   {
2358
2359     # store this mail in memory so it isn't lost if executing sendmail
2360     # failed.
2361     push(
2362           @main::stored_mails,
2363           {
2364             addr    => $main::mail_addr,
2365             subject => $main::mail_subject,
2366             text    => $main::mail_text
2367           }
2368         );
2369   } ## end if ( !send_mail( $main::mail_addr...
2370   init_mail();
2371
2372   # try to send out stored mails
2373   my $mailref;
2374   while ( $mailref = shift(@main::stored_mails) ) {
2375     if (
2376          !send_mail( $mailref->{'addr'}, $mailref->{'subject'},
2377                      $mailref->{'text'} )
2378        )
2379     {
2380       unshift( @main::stored_mails, $mailref );
2381       last;
2382     } ## end if ( !send_mail( $mailref...
2383   } ## end while ( $mailref = shift(...
2384 } ## end sub finish_mail()
2385
2386 #
2387 # send one mail
2388 #
2389 sub send_mail($$$) {
2390   my $addr    = shift;
2391   my $subject = shift;
2392   my $text    = shift;
2393
2394   my $package =
2395     keys %main::packages ? join( ' ', keys %main::packages ) : "";
2396
2397   use Email::Send;
2398
2399   unless ( defined($Email::Send::Sendmail::SENDMAIL) ) {
2400     $Email::Send::Sendmail::SENDMAIL = $conf::mail;
2401   }
2402
2403   if ($conf::overridemail) {
2404         $addr = $conf::overridemail;
2405   }
2406
2407   my $date = sprintf "%s",
2408     strftime( "%a, %d %b %Y %T %z", ( localtime(time) ) );
2409   my $message = <<__MESSAGE__;
2410 To: $addr
2411 From: Debian FTP Masters <ftpmaster\@ftp-master.debian.org>
2412 Subject: $subject
2413 Date: $date
2414 X-Debian: DAK
2415 X-DAK: DAK
2416 __MESSAGE__
2417
2418   if ( length $package ) {
2419     $message .= "X-Debian-Package: $package\n";
2420   }
2421
2422   $message .= "\n$text";
2423   $message .= "\nGreetings,\n\n\tYour Debian queue daemon (running on host $main::hostname)\n";
2424
2425   my $mail = Email::Send->new;
2426   for (qw[Sendmail SMTP]) {
2427     $mail->mailer($_) and last if $mail->mailer_available($_);
2428   }
2429
2430   my $ret = $mail->send($message);
2431   if ( $ret && $ret !~ /Message sent|success/ ) {
2432     return 0;
2433   }
2434
2435   return 1;
2436 } ## end sub send_mail($$$)
2437
2438 #
2439 # try to find a mail address for a name in the keyrings
2440 #
2441 sub try_to_get_mail_addr($$) {
2442   my $name    = shift;
2443   my $listref = shift;
2444
2445   @$listref = ();
2446   open( F,
2447             "$conf::gpg --no-options --batch --no-default-keyring "
2448           . "--always-trust --keyring "
2449           . join( " --keyring ", @conf::keyrings )
2450           . " --list-keys |"
2451       ) or return "";
2452   while (<F>) {
2453     if ( /^pub / && / $name / ) {
2454       /<([^>]*)>/;
2455       push( @$listref, $1 );
2456     }
2457   } ## end while (<F>)
2458   close(F);
2459
2460   return ( @$listref >= 1 ) ? $listref->[0] : "";
2461 } ## end sub try_to_get_mail_addr($$)
2462
2463 #
2464 # return current time as string
2465 #
2466 sub format_time() {
2467   my $t;
2468
2469   # omit weekday and year for brevity
2470   ( $t = localtime ) =~ /^\w+\s(.*)\s\d+$/;
2471   return $1;
2472 } ## end sub format_time()
2473
2474 sub print_time($) {
2475   my $secs = shift;
2476   my $hours = int( $secs / ( 60 * 60 ) );
2477
2478   $secs -= $hours * 60 * 60;
2479   return sprintf "%d:%02d:%02d", $hours, int( $secs / 60 ), $secs % 60;
2480 } ## end sub print_time($)
2481
2482 #
2483 # block some signals during queue processing
2484 #
2485 # This is just to avoid data inconsistency or uploads being aborted in the
2486 # middle. Only "soft" signals are blocked, i.e. SIGINT and SIGTERM, try harder
2487 # ones if you really want to kill the daemon at once.
2488 #
2489 sub block_signals() {
2490   POSIX::sigprocmask( SIG_BLOCK, $main::block_sigset );
2491 }
2492
2493 sub unblock_signals() {
2494   POSIX::sigprocmask( SIG_UNBLOCK, $main::block_sigset );
2495 }
2496
2497 #
2498 # process SIGHUP: close log file and reopen it (for logfile cycling)
2499 #
2500 sub close_log($) {
2501   close(LOG);
2502   close(STDOUT);
2503   close(STDERR);
2504
2505   open( LOG, ">>$conf::logfile" )
2506     or die "Cannot open my logfile $conf::logfile: $!\n";
2507   chmod( 0644, $conf::logfile )
2508     or msg( "log", "Cannot set modes of $conf::logfile: $!\n" );
2509   select( ( select(LOG), $| = 1 )[0] );
2510
2511   open( STDOUT, ">&LOG" )
2512     or msg( "log",
2513       "$main::progname: Can't redirect stdout to " . "$conf::logfile: $!\n" );
2514   open( STDERR, ">&LOG" )
2515     or msg( "log",
2516       "$main::progname: Can't redirect stderr to " . "$conf::logfile: $!\n" );
2517   msg( "log", "Restart after SIGHUP\n" );
2518 } ## end sub close_log($)
2519
2520 #
2521 # process SIGCHLD: check if it was our statusd process
2522 #
2523 sub kid_died($) {
2524   my $pid;
2525
2526   # reap statusd, so that it's no zombie when we try to kill(0) it
2527   waitpid( $main::statusd_pid, WNOHANG );
2528
2529   # Uncomment the following line if your Perl uses unreliable System V signal
2530   # (i.e. if handlers reset to default if the signal is delivered).
2531   # (Unfortunately, the re-setup can't be done in any case, since on some
2532   # systems this will cause the SIGCHLD to be delivered again if there are
2533   # still unreaped children :-(( )
2534
2535   #      $SIG{"CHLD"} = \&kid_died; # resetup handler for SysV
2536 } ## end sub kid_died($)
2537
2538 sub restart_statusd() {
2539
2540   # restart statusd if it died
2541   if ( !kill( 0, $main::statusd_pid ) ) {
2542     close(STATUSD);    # close out pipe end
2543     $main::statusd_pid = fork_statusd();
2544   }
2545 } ## end sub restart_statusd()
2546
2547 #
2548 # process a fatal signal: cleanup and exit
2549 #
2550 sub fatal_signal($) {
2551   my $signame = shift;
2552   my $sig;
2553
2554   # avoid recursions of fatal_signal in case of BSD signals
2555   foreach $sig (qw( ILL ABRT BUS FPE SEGV PIPE )) {
2556     $SIG{$sig} = "DEFAULT";
2557   }
2558
2559   if ( $$ == $main::maind_pid ) {
2560
2561     # only the main daemon should do this
2562     kill( $main::signo{"TERM"}, $main::statusd_pid )
2563       if defined $main::statusd_pid;
2564     unlink( $conf::statusfile, $conf::pidfile );
2565   } ## end if ( $$ == $main::maind_pid)
2566   msg( "log", "Caught SIG$signame -- exiting (pid $$)\n" );
2567   exit 1;
2568 } ## end sub fatal_signal($)
2569
2570 # Local Variables:
2571 #  tab-width: 4
2572 #  fill-column: 78
2573 # End: