~ubuntu-branches/ubuntu/lucid/spamassassin/lucid-proposed

« back to all changes in this revision

Viewing changes to sa-update.raw

  • Committer: Bazaar Package Importer
  • Author(s): Laurent Bigonville, Ubuntu Merge-o-Matic, Laurent Bigonville
  • Date: 2006-07-31 15:40:08 UTC
  • mfrom: (1.1.4 upstream)
  • Revision ID: james.westby@ubuntu.com-20060731154008-j37ulp5pgfkddegw
Tags: 3.1.3-1ubuntu1
[ Ubuntu Merge-o-Matic ]
* Merge from debian unstable.

[ Laurent Bigonville ]
* fix debian/control.
* drop debian/patches/40_fix_dns_issue.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/perl -w
 
1
#!/usr/bin/perl -w -T
2
2
 
3
3
# <@LICENSE>
4
4
# Copyright 2004 Apache Software Foundation
17
17
# </@LICENSE>
18
18
 
19
19
my $VERSION = 'svn' . (split(/\s+/,
20
 
        '$Id: sa-update.raw 231362 2005-08-11 00:25:28Z jm $'))[2];
 
20
        '$Id: sa-update.raw 408695 2006-05-22 15:34:10Z felicity $'))[2];
21
21
 
22
22
my $PREFIX          = '@@PREFIX@@';             # substituted at 'make' time
23
23
my $DEF_RULES_DIR   = '@@DEF_RULES_DIR@@';      # substituted at 'make' time
24
24
my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@';    # substituted at 'make' time
 
25
my $LOCAL_STATE_DIR = '@@LOCAL_STATE_DIR@@';    # substituted at 'make' time
25
26
use lib '@@INSTALLSITELIB@@';                   # substituted at 'make' time
26
27
 
27
28
# Standard perl modules
28
29
use File::Spec;
 
30
use File::Path;
 
31
use File::Copy;
29
32
use Getopt::Long;
30
33
use Pod::Usage;
31
34
use strict;
81
84
 
82
85
# Make the main dbg() accessible in our package w/o an extra function
83
86
*dbg=\&Mail::SpamAssassin::dbg;
 
87
sub dbg;
84
88
 
85
89
# Clean up PATH appropriately
86
90
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
87
91
 
88
92
# Default list of GPG keys allowed to sign update releases
89
93
#
90
 
# pub  1024D/265FA05B 2003-06-09 SpamAssassin Signing Key <release@spamassassin.org>
91
 
#      Key fingerprint = 26C9 00A4 6DD4 0CD5 AD24  F6D7 DEE0 1987 265F A05B
92
 
# sub  1024D/FC51569B 2003-08-21
93
 
#
94
 
my %valid_GPG = ( '265FA05B' => 1 );
 
94
# pub   1024D/265FA05B 2003-06-09
 
95
#       Key fingerprint = 26C9 00A4 6DD4 0CD5 AD24  F6D7 DEE0 1987 265F A05B
 
96
# uid                  SpamAssassin Signing Key <release@spamassassin.org>
 
97
# sub   1024D/FC51569B 2003-08-21
 
98
#
 
99
# pub   4096R/5244EC45 2005-12-20
 
100
#       Key fingerprint = 5E54 1DC9 59CB 8BAC 7C78  DFDC 4056 A61A 5244 EC45
 
101
# uid                  updates.spamassassin.org Signing Key <release@spamassassin.org>
 
102
# sub   4096R/24F434CE 2005-12-20
 
103
#
 
104
# note for gpg newbs: these are "long" gpg keyids.  It's common to also
 
105
# use the last 8 hex digits as a shorter keyid string.
 
106
#
 
107
my %valid_GPG = ( 
 
108
  '26C900A46DD40CD5AD24F6D7DEE01987265FA05B' => 1,
 
109
  '0C2B1D7175B852C64B3CDC716C55397824F434CE' => 1,
 
110
  '5E541DC959CB8BAC7C78DFDC4056A61A5244EC45' => 1,
 
111
);
95
112
 
96
113
# Default list of channels to update against
97
114
#
101
118
my %opt = ();
102
119
@{$opt{'gpgkey'}} = ();
103
120
@{$opt{'channel'}} = ();
104
 
my $site_rules_path;
105
 
my $GPG_ENABLED;
 
121
my $GPG_ENABLED = 1;
 
122
 
 
123
$opt{'gpghomedir'} = File::Spec->catfile($LOCAL_RULES_DIR, 'sa-update-keys');
106
124
 
107
125
Getopt::Long::Configure(
108
126
  qw(bundling no_getopt_compat no_auto_abbrev no_ignore_case));
113
131
 
114
132
  # allow multiple of these on the commandline
115
133
  'gpgkey=s'                            => $opt{'gpgkey'},
 
134
  'gpghomedir=s'                        => \$opt{'gpghomedir'},
116
135
  'channel=s'                           => $opt{'channel'},
117
136
 
 
137
  'import=s'                            => \$opt{'import'},
118
138
  'gpgkeyfile=s'                        => \$opt{'gpgkeyfile'},
119
139
  'channelfile=s'                       => \$opt{'channelfile'},
120
 
  'updatedir=s'                         => \$site_rules_path,
 
140
  'updatedir=s'                         => \$opt{'updatedir'},
 
141
  'gpg!'                                => \$GPG_ENABLED,
 
142
 
 
143
  # backward compatibility
121
144
  'usegpg'                              => \$GPG_ENABLED,
122
145
) or print_usage_and_exit();
123
146
 
162
185
 
163
186
  PREFIX          => $PREFIX,
164
187
  DEF_RULES_DIR   => $DEF_RULES_DIR,
165
 
  LOCAL_RULES_DIR => $LOCAL_RULES_DIR
 
188
  LOCAL_RULES_DIR => $LOCAL_RULES_DIR,
 
189
  LOCAL_STATE_DIR => $LOCAL_STATE_DIR,
166
190
});
167
 
$site_rules_path ||= $SA->first_existing_path(@Mail::SpamAssassin::site_rules_path);
 
191
 
 
192
if (defined $opt{'updatedir'}) {
 
193
  $opt{'updatedir'} = Mail::SpamAssassin::Util::untaint_file_path($opt{'updatedir'});
 
194
}
 
195
else {
 
196
  $opt{'updatedir'} = $SA->sed_path('__local_state_dir__/spamassassin/__version__');
 
197
}
168
198
 
169
199
dbg("generic: sa-update version $VERSION");
170
 
dbg("generic: using update directory: $site_rules_path");
 
200
dbg("generic: using update directory: $opt{'updatedir'}");
171
201
 
172
202
# doesn't really display useful things for this script, but we do want
173
203
# a module/version listing, etc. sa-update may be used for older versions
176
206
 
177
207
$SA->finish();
178
208
 
 
209
# untaint the command-line args; since the root user supplied these, and
 
210
# we're not a setuid script, we trust them
 
211
foreach my $optkey (keys %opt) {
 
212
  next if ref $opt{$optkey};
 
213
  my $untaint = $opt{$optkey};
 
214
  next unless defined $untaint;
 
215
  $untaint =~ /^(.*)$/;
 
216
  $opt{$optkey} = $1;
 
217
}
 
218
 
 
219
my $GPGPath;
 
220
 
179
221
# deal with gpg-related options
180
222
if (@{$opt{'gpgkey'}}) {
181
223
  $GPG_ENABLED = 1;
182
224
  foreach my $key (@{$opt{'gpgkey'}}) {
183
 
    unless ($key =~ /^[a-fA-F0-9]{8}$/) {
 
225
    unless (is_valid_gpg_key_id($key)) {
184
226
      dbg("gpg: invalid gpgkey parameter $key");
185
227
      next;
186
228
    }
197
239
 
198
240
  dbg("gpg: reading in gpgfile ".$opt{'gpgkeyfile'});
199
241
  while(my $key = <GPG>) {
200
 
    unless ($key =~ /^[a-fA-F0-9]{8}$/) {
 
242
    unless (is_valid_gpg_key_id($key)) {
201
243
      dbg("gpg: invalid key id $key");
202
244
      next;
203
245
    }
207
249
  }
208
250
  close(GPG);
209
251
}
 
252
if ( $opt{'import'} ) {
 
253
  my $ex = import_gpg_key($opt{'import'});
 
254
  exit $ex;
 
255
}
 
256
 
 
257
# does the sa-update keyring exist?  if not, import it
 
258
if ($GPG_ENABLED) {
 
259
  if(!-f File::Spec->catfile($opt{'gpghomedir'}, "secring.gpg")) {
 
260
    import_default_keyring();
 
261
    # attempt to continue even if this fails, anyway
 
262
  }
 
263
}
 
264
 
 
265
# convert fingerprint gpg ids to keyids
 
266
foreach (keys %valid_GPG) {
 
267
  my $id = substr $_, -8;
 
268
  $valid_GPG{$id} = 1;
 
269
}
210
270
 
211
271
# Deal with channel-related options
212
 
if (defined $opt{'channel'}) {
213
 
  push(@channels, @{$opt{'channel'}});
 
272
if (defined $opt{'channel'} && scalar @{$opt{'channel'}} > 0) {
 
273
  @channels = @{$opt{'channel'}};
214
274
}
215
275
if (defined $opt{'channelfile'}) {
216
276
  unless (open(CHAN, $opt{'channelfile'})) {
218
278
  }
219
279
 
220
280
  dbg("channel: reading in channelfile ".$opt{'channelfile'});
 
281
  @channels = ();
221
282
  while(my $chan = <CHAN>) {
 
283
    chomp $chan;
222
284
    $chan = lc $chan;
223
285
    dbg("channel: adding $chan");
224
286
    push(@channels, $chan);
226
288
  close(CHAN);
227
289
}
228
290
 
 
291
# untaint the channel listing
 
292
for(my $ind = 0; $ind < @channels; $ind++) {
 
293
  if ($channels[$ind] =~ /^([a-zA-Z0-9._-]+)$/) {
 
294
    $channels[$ind] = $1;
 
295
  }
 
296
  else {
 
297
    splice @channels, $ind, 1;
 
298
  }
 
299
}
 
300
 
229
301
# find GPG in the PATH
230
 
my $GPGPath;
231
302
if ($GPG_ENABLED) {
232
 
  dbg("gpg: Searching for 'gpg' in ".$ENV{'PATH'});
233
 
  foreach my $dir (split(/:/, $ENV{'PATH'})) {
234
 
    $dir = File::Spec->catfile($dir, 'gpg');
235
 
    if (-x $dir) {
236
 
      $GPGPath = $dir;
237
 
      last;
238
 
    }
239
 
  }
240
 
  die "fatal: couldn't find GPG in \$PATH\n" unless ($GPGPath);
241
 
  dbg("gpg: found $GPGPath");
 
303
  $GPGPath = find_gpg_path();
242
304
  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
243
305
}
244
306
 
245
307
 
246
308
my $res = Net::DNS::Resolver->new();
247
309
 
248
 
my $ua = LWP::UserAgent->new;
 
310
my $ua = LWP::UserAgent->new();
249
311
$ua->agent("sa-update/$VERSION");
250
 
$ua->timeout(10);
 
312
$ua->timeout(60);      # a good long timeout; 10 is too short for Coral!
251
313
$ua->env_proxy;
252
314
 
253
315
# Generate a temporary file to put channel content in for later use ...
254
316
my ($content_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
255
317
close($tfh);
256
318
 
 
319
# and another, for the new config file
 
320
my ($newcf_file, $tfh2) = Mail::SpamAssassin::Util::secure_tmpfile();
 
321
close($tfh2);
 
322
 
 
323
# by default, exit code is 1, to indicate no updates occurred
 
324
my $exit = 1;
 
325
 
257
326
# Go ahead and loop through all of the channels
258
 
my $exit = 0;
259
327
foreach my $channel (@channels) {
260
328
  dbg("channel: attempting channel $channel");
261
329
 
263
331
  my $nicechannel = $channel;
264
332
  $nicechannel =~ tr/A-Za-z0-9-/_/cs;
265
333
 
266
 
  my $UPDDir = "$site_rules_path/$nicechannel";
 
334
  my $UPDDir = "$opt{'updatedir'}/$nicechannel";
 
335
  my $UPDTmp = "$opt{'updatedir'}/$nicechannel.tmp";
267
336
  my $CFFile = "$UPDDir.cf";
 
337
  my $CFFTmp = $newcf_file;
268
338
 
269
339
  dbg("channel: update directory $UPDDir");
 
340
  dbg("channel: update tmp directory $UPDTmp");
270
341
  dbg("channel: channel cf file $CFFile");
 
342
  dbg("channel: channel tmp cf file $CFFTmp");
271
343
 
272
344
  # try to read metadata from channel.cf file
273
345
  my $currentV = -1;
310
382
    next;
311
383
  }
312
384
 
313
 
  # We don't currently have the list of mirrors, so go grab it.
314
 
  unless (-f "$UPDDir/MIRRORED.BY") {
 
385
  # ensure dirs exist, upfront
 
386
  unless (-d $UPDDir) {
 
387
    dbg("channel: creating $UPDDir");
 
388
    mkpath([$UPDDir], 0, 0777) or die "fatal: can't create $UPDDir: $!\n";
 
389
  }
 
390
  unless (-d $UPDTmp) {
 
391
    dbg("channel: creating $UPDTmp");
 
392
    mkpath([$UPDTmp], 0, 0777) or die "fatal: can't create $UPDTmp: $!\n";
 
393
  }
 
394
 
 
395
  # copy the MIRRORED.BY file to the tmpdir, if it exists
 
396
  if (-f "$UPDDir/MIRRORED.BY") {
 
397
    unlink("$UPDTmp/MIRRORED.BY");
 
398
 
 
399
    my ($x, $atime, $mtime);
 
400
    ($x,$x,$x,$x,$x,$x,$x,$x,$atime,$mtime,$x) = stat "$UPDDir/MIRRORED.BY";
 
401
 
 
402
    copy("$UPDDir/MIRRORED.BY", "$UPDTmp/MIRRORED.BY")
 
403
            or die "fatal: cannot copy $UPDDir/MIRRORED.BY to $UPDTmp/MIRRORED.BY";
 
404
 
 
405
    # ensure modtimes match
 
406
    utime($atime, $mtime, "$UPDTmp/MIRRORED.BY");
 
407
  }
 
408
  else {
 
409
    # We don't currently have the list of mirrors, so go grab it.
315
410
    dbg("channel: no MIRRORED.BY file available");
316
411
    my $mirror = do_txt_query("mirrors.$channel");
317
412
    unless ($mirror) {
318
413
      warn "error: no mirror data available for channel $channel\n";
319
 
      dbg("channel: MIRRORED.BY file location was not in DNS, channel failed");
320
 
      $exit++;
321
 
      next;
 
414
      channel_failed("channel: MIRRORED.BY file location was not in DNS");
322
415
    }
323
416
    $mirror = http_get($mirror);
324
417
    unless ($mirror) {
325
418
      warn "error: no mirror data available for channel $channel\n";
326
 
      dbg("channel: MIRRORED.BY contents were missing, channel failed");
327
 
      $exit++;
 
419
      channel_failed("channel: MIRRORED.BY contents were missing");
328
420
      next;
329
421
    }
330
422
 
331
 
    unless (-d $UPDDir) {
332
 
      dbg("channel: creating $UPDDir");
333
 
      mkdir $UPDDir || die "fatal: can't create $UPDDir: $!\n";
334
 
    }
335
 
 
336
 
    unless (open(MIR, ">$UPDDir/MIRRORED.BY")) {
 
423
    unless (open(MIR, ">$UPDTmp/MIRRORED.BY")) {
337
424
      warn "error: can't create mirrors file: $!\n";
338
 
      dbg("channel: MIRRORED.BY creation failure, channel failed");
339
 
      $exit++;
 
425
      channel_failed("channel: MIRRORED.BY creation failure");
340
426
      next;
341
427
    }
342
428
    print MIR $mirror;
345
431
  }
346
432
 
347
433
  # Read in the list of mirrors
348
 
  unless (open(MIR, "$UPDDir/MIRRORED.BY")) {
 
434
  unless (open(MIR, "$UPDTmp/MIRRORED.BY")) {
349
435
    warn "error: can't read mirrors file: $!\n";
350
 
    dbg("channel: MIRRORED.BY file is unreadable, channel failed");
351
 
    $exit++;
 
436
    channel_failed("channel: MIRRORED.BY file is unreadable");
352
437
    next;
353
438
  }
354
439
 
355
440
  dbg("channel: reading MIRRORED.BY file");
356
441
  my %mirrors = ();
357
442
  while(my $mirror = <MIR>) {
 
443
    next if ($mirror =~ /^#/);  # explicitly skip comments
 
444
 
358
445
    # We only support HTTP right now
359
446
    if ($mirror !~ m@^http://@i) {
360
447
      dbg("channel: skipping non-HTTP mirror: $mirror");
378
465
 
379
466
  unless (keys %mirrors) {
380
467
    warn "error: no mirrors available for channel $channel\n";
381
 
    dbg("channel: no mirrors available, channel failed");
382
 
    $exit++;
 
468
    channel_failed("channel: no mirrors available");
383
469
    next;
384
470
  }
385
471
 
386
472
  # remember the mtime of the file so we can IMS GET later on
387
 
  my $mirby_time = (stat("$UPDDir/MIRRORED.BY"))[9];
 
473
  my $mirby_time = (stat("$UPDTmp/MIRRORED.BY"))[9];
388
474
 
389
475
 
390
476
  # Now that we've laid the foundation, go grab the appropriate files
427
513
 
428
514
  unless ($content && $SHA1 && (!$GPG_ENABLED || $GPG)) {
429
515
    warn "error: channel $channel has no working mirrors\n";
430
 
    dbg("channel: could not find working mirror, channel failed");
431
 
    $exit++;
 
516
    channel_failed("channel: could not find working mirror");
432
517
    next;
433
518
  }
434
519
 
442
527
  dbg("sha1: verification got     : $digest");
443
528
  unless ($digest eq $SHA1) {
444
529
    warn "error: can't verify SHA1 signature\n";
445
 
    dbg("channel: SHA1 verification failed, channel failed");
446
 
    $exit++;
 
530
    channel_failed("channel: SHA1 verification failed");
447
531
    next;
448
532
  }
449
533
 
456
540
 
457
541
  # to sign  : gpg -bas file
458
542
  # to verify: gpg --verify --batch --no-tty --status-fd=1 -q --logger-fd=1 file.asc file
459
 
  # look for : /^\[GNUPG:\] GOODSIG \S+(\S{8})
 
543
  # look for : [GNUPG:] GOODSIG 6C55397824F434CE updates.spamassassin.org [...]
 
544
  #            [GNUPG:] VALIDSIG 0C2B1D7175B852C64B3CDC716C55397824F434CE [...]
 
545
  #            [GNUPG:] NO_PUBKEY 6C55397824F434CE
460
546
  if ($GPG) {
461
547
    dbg("gpg: populating temp signature file");
462
548
    my $sig_file;
466
552
    close($tfh);
467
553
 
468
554
    dbg("gpg: calling gpg");
469
 
    my $CMD = "$GPGPath --verify --batch --no-tty --status-fd=1 -q --logger-fd=1";
 
555
 
 
556
    my $gpghome = interpolate_gpghomedir();
 
557
 
 
558
    # TODO: we could also use "--keyserver pgp.mit.edu" or similar,
 
559
    # to autodownload missing keys...
 
560
    my $CMD = "$GPGPath $gpghome --verify --batch ".
 
561
        "--no-tty --status-fd=1 -q --logger-fd=1";
 
562
 
470
563
    unless (open(CMD, "$CMD $sig_file $content_file|")) {
471
564
      unlink $sig_file || warn "error: can't unlink $sig_file: $!\n";
472
565
      die "fatal: couldn't execute $GPGPath: $!\n";
474
567
 
475
568
    # Determine the fate of the signature
476
569
    my $signer = '';
 
570
    my $missingkeys = '';
477
571
    while(my $GNUPG = <CMD>) {
478
 
      next unless ($GNUPG =~ /^\Q[GNUPG:] GOODSIG\E \S+(\S{8})/);
479
 
      $signer = $1;
 
572
      chop $GNUPG;
 
573
      dbg ("gpg: $GNUPG");
 
574
 
 
575
      if ($GNUPG =~ /^gpg: fatal:/) {
 
576
        warn $GNUPG."\n";        # report bad news
 
577
      }
 
578
 
 
579
      if ($GNUPG =~ /^\Q[GNUPG:]\E NO_PUBKEY \S+(\S{8})$/) {
 
580
        $missingkeys .= $1." ";
 
581
      }
 
582
 
 
583
      next unless ($GNUPG =~ /^\Q[GNUPG:]\E (?:VALID|GOOD)SIG (\S{8,40})/);
 
584
      my $key = $1;
 
585
 
 
586
      # we want either a keyid (8) or a fingerprint (40)
 
587
      if (length $key > 8 && length $key < 40) {
 
588
        substr($key, 8) = '';
 
589
      }
 
590
 
 
591
      # use the longest match we can find
 
592
      $signer = $key if (length $key > length $signer);
480
593
    }
481
594
 
482
595
    close(CMD);
483
596
    unlink $sig_file || warn "Can't unlink $sig_file: $!\n";
484
597
 
485
598
    if ($signer) {
486
 
      dbg("gpg: good signature made by key id $signer");
 
599
      my $keyid = substr $signer, -8;
 
600
      dbg("gpg: found signature made by key $signer");
487
601
      if (exists $valid_GPG{$signer}) {
488
602
        dbg("gpg: key id $signer is release trusted");
489
603
      }
 
604
      elsif (exists $valid_GPG{$keyid}) {
 
605
        dbg("gpg: key id $keyid is release trusted");
 
606
      }
490
607
      else {
491
 
        dbg("gpg: key id $signer is not release trusted");
 
608
        dbg("gpg: key id $keyid is not release trusted");
492
609
        $signer = undef;
493
610
      }
494
611
    }
495
612
 
496
613
    unless ($signer) {
497
 
      warn "error: GPG validation failed\n";
498
 
      dbg("channel: GPG verification failed, channel failed");
499
 
      $exit++;
 
614
      warn "error: GPG validation failed!\n";
 
615
 
 
616
      if ($missingkeys) {
 
617
 
 
618
        warn <<ENDOFVALIDATIONERR;
 
619
The update downloaded successfully, but it was not signed with a trusted GPG
 
620
key.  Instead, it was signed with the following keys:
 
621
 
 
622
    $missingkeys
 
623
 
 
624
Perhaps you need to import the channel's GPG key?  For example:
 
625
 
 
626
    wget http://spamassassin.apache.org/updates/GPG.KEY
 
627
    gpg --import GPG.KEY
 
628
 
 
629
ENDOFVALIDATIONERR
 
630
 
 
631
      } else {
 
632
 
 
633
        warn <<ENDOFVALIDATIONERR;
 
634
The update downloaded successfully, but the GPG signature verification
 
635
failed.
 
636
ENDOFVALIDATIONERR
 
637
 
 
638
      }
 
639
 
 
640
      channel_failed("channel: GPG validation failed");
500
641
      next;
501
642
    }
502
643
  }
506
647
 
507
648
  if ($mirby) {
508
649
    dbg("channel: updating MIRRORED.BY contents");
509
 
    if (open(MBY, ">$UPDDir/MIRRORED.BY")) {
 
650
    if (open(MBY, ">$UPDTmp/MIRRORED.BY")) {
510
651
      print MBY $mirby;
511
652
      close(MBY);
512
653
    }
516
657
  }
517
658
 
518
659
  dbg("channel: cleaning out update directory");
519
 
  unless (opendir(DIR, $UPDDir)) {
520
 
    warn "error: can't readdir $UPDDir: $!\n";
521
 
    dbg("channel: attempt to readdir failed, channel failed");
522
 
    $exit++;
 
660
  if (!clean_update_dir($UPDTmp)) {
 
661
    channel_failed("channel: attempt to clean update dir failed");
523
662
    next;
524
663
  }
525
 
  while(my $file = readdir(DIR)) {
526
 
    next unless (-f "$UPDDir/$file");
527
 
    next if ($file eq 'MIRRORED.BY');
528
 
    dbg("channel: unlinking $file");
529
 
    unlink "$UPDDir/$file" || warn "error: can't remove file $UPDDir/$file: $!\n";
530
 
  }
531
 
  closedir(DIR);
532
 
  unlink "$UPDDir.cf" || warn "error: can't remove file $UPDDir.cf: $!\n";
 
664
 
 
665
  unlink $CFFTmp || warn "error: can't remove file $CFFTmp: $!\n";
533
666
 
534
667
  $tfh = IO::Zlib->new($content_file, "rb");
535
668
  die "fatal: couldn't read content tmpfile $content_file: $!\n" unless $tfh;
537
670
  my $tar = Archive::Tar->new($tfh);
538
671
  die "fatal: couldn't create Archive::Tar object!\n" unless $tar;
539
672
 
540
 
  # make sure we're doing the work in the update directory
541
 
  unless (chdir $UPDDir) {
542
 
    warn "error: can't chdir into $UPDDir: $!\n";
543
 
    dbg("channel: chdir failed, channel failed");
544
 
    $exit++;
545
 
    next;
546
 
  }
547
 
 
548
673
  dbg("channel: extracting archive");
549
 
  unless ($tar->extract()) {
 
674
  my $ret = taint_safe_archive_extract($UPDTmp, $tar);
 
675
 
 
676
  unless ($ret) {
550
677
    close($tfh);
551
678
    warn "error: couldn't extract the tar archive!\n";
552
 
    dbg("channel: archive extraction failed, channel failed");
553
 
    $exit++;
 
679
    channel_failed("channel: archive extraction failed");
554
680
    next;
555
681
  }
556
682
  close($tfh);
557
683
 
 
684
  # check --lint
 
685
 
 
686
  if (!lint_check_dir($UPDTmp)) {
 
687
    warn "error: lint check of update failed!  channel failed\n";
 
688
    channel_failed("channel: lint check of update failed");
 
689
    next;
 
690
  }
 
691
 
 
692
 
 
693
  # OK, lint passed. now create the update config file
558
694
 
559
695
  dbg("channel: creating update config file");
560
 
  unless (open(CF, ">$UPDDir.cf")) {
561
 
    die "fatal: can't create new channel cf $UPDDir.cf: $!\n";
 
696
  unless (open(CF, ">$CFFTmp")) {
 
697
    die "fatal: can't create new channel cf $CFFTmp: $!\n";
562
698
  }
563
699
 
564
700
  # Put in whatever metadata we need
565
701
  print CF "# UPDATE version $newV\n";
566
702
 
 
703
  # try to figure out the relative path dir name
 
704
  my $relativeDir = $UPDDir;
 
705
  $UPDDir =~ m,/([^/]+)/*$,;
 
706
  if ($1) {
 
707
    $relativeDir = $1;
 
708
  }
 
709
  dbg("channel: updatedir=$UPDDir relativepath=$relativeDir");
 
710
 
 
711
  my @files = ();
567
712
  # now include *.cf
568
 
  unless (opendir(DIR, $UPDDir)) {
569
 
    die "fatal: can't access $UPDDir: $!\n";
 
713
  unless (opendir(DIR, $UPDTmp)) {
 
714
    die "fatal: can't access $UPDTmp: $!\n";
570
715
  }
571
716
  while(my $file = readdir(DIR)) {
572
 
    next unless (-f "$UPDDir/$file");
573
 
    next unless ($file =~ /\.cf$/);
 
717
    $file =~ /^([^\/]+)$/;       # untaint
 
718
    $file = $1;
 
719
    next unless (-f "$UPDTmp/$file");
 
720
    next if ($file eq "MIRRORED.BY");   # handled separately
 
721
 
574
722
    dbg("channel: adding $file");
575
 
    print CF "include $UPDDir/$file\n";
 
723
 
 
724
    if ($file =~ /\.cf$/) {
 
725
      print CF "include $relativeDir/$file\n";
 
726
    }
 
727
 
 
728
    push (@files, $file);
576
729
  }
577
730
  closedir(DIR);
578
 
  close(CF);
 
731
  if (!close(CF)) {
 
732
    warn "write to $CFFTmp failed! attempting to continue";
 
733
    channel_failed("write to $CFFTmp failed");
 
734
    next;
 
735
  }
 
736
 
 
737
  dbg("channel: applying changes to $UPDDir...");
 
738
 
 
739
  # too late to stop now!   At this stage, if there are errors,
 
740
  # we have to attempt to carry on regardless, since we've already
 
741
  # blown away the old ruleset.
 
742
 
 
743
  # clean out the "real" update dir, and copy from tmp areas
 
744
  if (!clean_update_dir($UPDDir)) {
 
745
    warn("channel: attempt to rm contents failed, attempting to continue anyway");
 
746
  }
 
747
 
 
748
  foreach my $file (@files) {
 
749
    rename("$UPDTmp/$file", "$UPDDir/$file")
 
750
        or warn "rename $UPDTmp/$file $UPDDir/$file failed: $!";
 
751
  }
 
752
 
 
753
  unlink $CFFile || warn "error: can't remove file $CFFile: $!\n";
 
754
  cross_fs_rename($CFFTmp, $CFFile)
 
755
      or warn "rename $CFFTmp $CFFile failed: $!";
 
756
 
 
757
  unlink("$UPDDir/MIRRORED.BY");
 
758
  rename("$UPDTmp/MIRRORED.BY", "$UPDDir/MIRRORED.BY")
 
759
      or warn "error: couldn't mv $UPDTmp/MIRRORED.BY $UPDDir/MIRRORED.BY: $!\n";
 
760
 
 
761
  rmdir $UPDTmp;
 
762
  $exit = 0;            # "exit 0" means an update occurred
579
763
 
580
764
  dbg("channel: update complete");
581
765
}
582
766
 
583
 
unlink $content_file || warn "error: couldn't remove tmpfile $content_file: $!\n";
 
767
# clear out the temp files if they still exist
 
768
foreach ( $newcf_file, $content_file ) {
 
769
  if (-e $_) {
 
770
    unlink $_ || warn "error: can't remove file $_: $!\n";
 
771
  }
 
772
}
584
773
 
585
774
dbg("diag: updates complete, exiting with code $exit");
586
775
exit $exit;
587
776
 
 
777
sub channel_failed {
 
778
  my $reason = shift;
 
779
  warn("$reason, channel failed\n");
 
780
  if ($exit > 4) {
 
781
    $exit++;
 
782
  } else {
 
783
    $exit = 4;      # 4 or higher means channel failed
 
784
  }
 
785
}
 
786
 
 
787
sub taint_safe_archive_extract {
 
788
  my $todir = shift;
 
789
  my $tar = shift;
 
790
 
 
791
  # stupid Archive::Tar is not natively taint-safe! duh.
 
792
  # return $tar->extract();
 
793
  # instead, get the file list, untaint, and extract one-by-one.
 
794
 
 
795
  my @files = $tar->list_files();
 
796
  foreach my $file (@files) {
 
797
    next if ($file =~ /^\/$/);  # ignore dirs
 
798
 
 
799
    $file =~ /^([-\.\,\/a-zA-Z0-9_]+)$/;
 
800
    my $outfname = $1;
 
801
    $outfname =~ s/\.\.\//__\//gs;      # avoid "../" dir traversal attacks
 
802
    $outfname = "$todir/$outfname";
 
803
 
 
804
    dbg "extracting: $outfname";
 
805
    if (open OUT, ">".$outfname) {
 
806
      print OUT $tar->get_content($file);
 
807
      if (!close OUT) {
 
808
        warn "error: write failed to $outfname: $!";
 
809
        goto failed;
 
810
      }
 
811
    } else {
 
812
      warn "error: failed to open $outfname for write: $!";
 
813
      goto failed;
 
814
    }
 
815
  }
 
816
 
 
817
  return @files;
 
818
 
 
819
failed:
 
820
  return;       # undef = failure
 
821
}
 
822
 
588
823
# Do a generic TXT query
589
824
sub do_txt_query {
590
825
  my($query) = shift;
616
851
sub http_get {
617
852
  my($url, $ims) = @_;
618
853
 
619
 
  my $request = HTTP::Request->new("GET");
620
 
  $request->url($url);
621
 
 
622
 
  if (defined $ims) {
623
 
    my $str = time2str($ims);
624
 
    $request->header('If-Modified-Since', $str);
625
 
    dbg("http: IMS GET request, $url, $str");
626
 
  }
627
 
  else {
628
 
    dbg("http: GET request, $url");
629
 
  }
630
 
 
631
 
  my $response = $ua->request($request);
632
 
 
633
 
  if ($response->is_success) {
634
 
    return $response->content;
635
 
  }
636
 
 
637
 
  dbg("http: request failed: " . $response->status_line);
 
854
  my $response;
 
855
  my $text;
 
856
 
 
857
  # retry 3 times; this works better with Coral
 
858
  foreach my $retries (1 .. 3) {
 
859
    my $request = HTTP::Request->new("GET");
 
860
    $request->url($url);
 
861
 
 
862
    if (defined $ims) {
 
863
      my $str = time2str($ims);
 
864
      $request->header('If-Modified-Since', $str);
 
865
      dbg("http: IMS GET request, $url, $str");
 
866
    }
 
867
    else {
 
868
      dbg("http: GET request, $url");
 
869
    }
 
870
 
 
871
    $response = $ua->request($request);
 
872
 
 
873
    if ($response->is_success) {
 
874
      return $response->content;
 
875
    }
 
876
 
 
877
    # could be a "304 not modified" or similar.
 
878
    # TODO: should use a special return type for "not modified" here
 
879
    # instead of overloading the failure return type
 
880
    if ($ims && $response->status_line =~ /^3/) {
 
881
      return;
 
882
    }
 
883
 
 
884
    # include the text in the debug output; it's useful in some cases,
 
885
    # e.g. proxies that require authentication, diagnosing fascist
 
886
    # filtering false positives, etc.
 
887
    $text = $response->content;
 
888
    $text ||= "(no body)";
 
889
    $text =~ s/\s+/ /gs;
 
890
 
 
891
    dbg ("http: request failed, retrying: " . $response->status_line.": ".$text);
 
892
  }
 
893
 
 
894
  # this should be a user-readable warning without --debug
 
895
  warn "http: request failed: " . $response->status_line.": ".$text."\n";
638
896
  return;
639
897
}
640
898
 
701
959
  pod2usage( -verbose => $verbose, -message => $message, -exitval => 64 );
702
960
}
703
961
 
 
962
sub find_gpg_path {
 
963
  dbg("gpg: Searching for 'gpg' in ".$ENV{'PATH'});
 
964
  my $path;
 
965
  foreach my $dir (split(/:/, $ENV{'PATH'})) {
 
966
    $dir = File::Spec->catfile($dir, 'gpg');
 
967
    if (-x $dir) {
 
968
      $path = $dir;
 
969
      last;
 
970
    }
 
971
  }
 
972
  die "fatal: couldn't find GPG in \$PATH\n" unless ($path);
 
973
  dbg("gpg: found $path");
 
974
  return $path;
 
975
}
 
976
 
 
977
sub interpolate_gpghomedir {
 
978
  my $gpghome = '';
 
979
  if ($opt{'gpghomedir'}) {
 
980
    $gpghome = $opt{'gpghomedir'};
 
981
    $gpghome =~ s/\'/\\\'/gs;
 
982
    $gpghome = "--homedir='$gpghome'";
 
983
  }
 
984
  return $gpghome;
 
985
}
 
986
 
 
987
sub import_gpg_key {
 
988
  my $keyfile = shift;
 
989
 
 
990
  $GPGPath = find_gpg_path();
 
991
  my $gpghome = interpolate_gpghomedir();
 
992
 
 
993
  my $CMD = "$GPGPath $gpghome --batch ".
 
994
      "--no-tty --status-fd=1 -q --logger-fd=1 --import";
 
995
 
 
996
  unless (open(CMD, "$CMD $keyfile|")) {
 
997
    die "fatal: couldn't execute $GPGPath: $!\n";
 
998
  }
 
999
 
 
1000
  # Determine the fate of the signature
 
1001
  while(my $GNUPG = <CMD>) {
 
1002
    chop $GNUPG;
 
1003
    dbg ("gpg: $GNUPG");
 
1004
 
 
1005
    if ($GNUPG =~ /^gpg: /) {
 
1006
      warn $GNUPG."\n";        # report bad news
 
1007
    }
 
1008
 
 
1009
    if ($GNUPG =~ /^IMPORTED /) {
 
1010
      print "sa-update --import: success. $GNUPG\n";
 
1011
    }
 
1012
  }
 
1013
 
 
1014
  close(CMD);
 
1015
  return ($? >> 8);
 
1016
}
 
1017
 
 
1018
sub import_default_keyring {
 
1019
  my $defkey = File::Spec->catfile ($DEF_RULES_DIR, "sa-update-pubkey.txt");
 
1020
  return unless (-f $defkey);
 
1021
 
 
1022
  print "sa-update: importing default keyring to '".$opt{gpghomedir}."'...\n";
 
1023
  unless (-d $opt{gpghomedir}) {
 
1024
    # use 0700 to avoid "unsafe permissions" warning
 
1025
    mkdir ($opt{gpghomedir}, 0700) or die "cannot mkdir $opt{gpghomedir}: $!";
 
1026
  } 
 
1027
  import_gpg_key($defkey);
 
1028
}
 
1029
 
 
1030
sub is_valid_gpg_key_id {
 
1031
  # either a keyid (8 bytes) or a fingerprint (40 bytes)
 
1032
  return ($_[0] =~ /^[a-fA-F0-9]+$/ && (length $_[0] == 8 || length $_[0] == 40));
 
1033
}
 
1034
 
 
1035
sub clean_update_dir {
 
1036
  my $dir = shift;
 
1037
  unless (opendir(DIR, $dir)) {
 
1038
    warn "error: can't readdir $dir: $!\n";
 
1039
    dbg("channel: attempt to readdir failed, channel failed");
 
1040
    return 0;
 
1041
  }
 
1042
  while(my $file = readdir(DIR)) {
 
1043
    next unless (-f "$dir/$file");
 
1044
    next if ($file eq 'MIRRORED.BY');
 
1045
    dbg("channel: unlinking $file");
 
1046
    $file =~ /^([^\/]+)$/;       # untaint
 
1047
    $file = $1;
 
1048
    if (!unlink "$dir/$file") {
 
1049
      warn "error: can't remove file $dir/$file: $!\n";
 
1050
      return 0;
 
1051
    }
 
1052
  }
 
1053
  closedir(DIR);
 
1054
  return 1;
 
1055
}
 
1056
 
 
1057
sub lint_check_dir {
 
1058
  my $dir = shift;
 
1059
 
 
1060
  # due to the Logger module's globalness (all M::SA objects share the same
 
1061
  # Logger setup), we can't change the debug level here to only include
 
1062
  # "config" or otherwise be more terse. :(
 
1063
  my $spamtest = new Mail::SpamAssassin( {
 
1064
    rules_filename      => $dir,
 
1065
    site_rules_filename => "$dir/doesnotexist",
 
1066
    userprefs_filename  => "$dir/doesnotexist",
 
1067
 
 
1068
    local_tests_only    => 1,
 
1069
    dont_copy_prefs     => 1,
 
1070
 
 
1071
    PREFIX              => $PREFIX,
 
1072
    DEF_RULES_DIR       => $DEF_RULES_DIR,
 
1073
    LOCAL_RULES_DIR     => $LOCAL_RULES_DIR,
 
1074
    LOCAL_STATE_DIR     => $LOCAL_STATE_DIR,
 
1075
  });
 
1076
 
 
1077
  # need to kluge disabling bayes since it may try to expire the DB, and
 
1078
  # without the proper config it's not going to be good.
 
1079
  $spamtest->{conf}->{use_bayes} = 0;
 
1080
 
 
1081
  my $res = $spamtest->lint_rules();
 
1082
  $spamtest->finish();
 
1083
 
 
1084
  return $res == 0;
 
1085
}
 
1086
 
 
1087
# a version of rename() that can cope with renaming files across filesystems,
 
1088
# as mv(1) can.
 
1089
sub cross_fs_rename {
 
1090
  my ($from, $to) = @_;
 
1091
  my $ret = rename ($from, $to);
 
1092
 
 
1093
  if ($ret) {
 
1094
    return $ret;        # success first time! great
 
1095
  }
 
1096
 
 
1097
  # try a copy
 
1098
  if (!copy($from, $to)) {
 
1099
    # copy failed, too.  we have no further fallbacks; return the rename()
 
1100
    # failure code
 
1101
    return $ret;
 
1102
  }
 
1103
 
 
1104
  # copy succeeded, we're good; remove the source, and return success
 
1105
  unlink($from);
 
1106
  return 1;
 
1107
}
 
1108
 
704
1109
# ---------------------------------------------------------------------------
705
1110
 
706
1111
=head1 NAME
715
1120
 
716
1121
  --updatedir path              Directory to place updates, defaults to the
717
1122
                                SpamAssassin site rules directory (def:
718
 
                                /etc/mail/spamassassin)
 
1123
                                /var/lib/spamassassin/<version>)
719
1124
 
720
1125
  --channel channel             Retrieve updates from this channel
721
1126
                                Use multiple times for multiple channels
725
1130
  --gpgkey key                  Trust the key id to sign releases
726
1131
                                Use multiple times for multiple keys
727
1132
  --gpgkeyfile file             Trust the key ids in the file to sign releases
728
 
  --usegpg                      Use GPG to verify updates
729
 
                                This is auto-enabled by use of the above
730
 
                                gpgkey and gpgkeyfile options.
 
1133
  --gpghomedir path             Store the GPG keyring in this directory
 
1134
  --gpg and --nogpg             Use (or do not use) GPG to verify updates
 
1135
                                (--gpg is assumed by use of the above
 
1136
                                --gpgkey and --gpgkeyfile options)
731
1137
 
732
1138
  -D, --debug [area=n,...]      Print debugging messages
733
1139
  -V, --version                 Print version
735
1141
 
736
1142
=head1 DESCRIPTION
737
1143
 
738
 
sa-update automates the process of downloading and installing new
739
 
rules and configuration, based on channels.  The default channel
740
 
is I<updates.spamassassin.org>, which has updated rules since the
741
 
previous release.
 
1144
sa-update automates the process of downloading and installing new rules and
 
1145
configuration, based on channels.  The default channel is
 
1146
I<updates.spamassassin.org>, which has updated rules since the previous
 
1147
release.
742
1148
 
743
 
Update archives are verified by SHA1 hashes, and optionally GPG.
 
1149
Update archives are verified by default using SHA1 hashes and GPG signatures.
744
1150
 
745
1151
=head1 OPTIONS
746
1152
 
747
1153
=over 4
748
1154
 
749
 
=item B<updatedir>
750
 
 
751
 
Typically sa-update will use whatever the default site rules directory
752
 
SpamAssassin uses.  (usually /etc/mail/spamassassin)  If the updates should be
753
 
stored in another location, specify it here.
754
 
 
755
 
=item B<channel>
 
1155
=item B<--updatedir>
 
1156
 
 
1157
By default, C<sa-update> will use the system-wide rules update directory:
 
1158
 
 
1159
  @@LOCAL_STATE_DIR@@/spamassassin/@@VERSION@@
 
1160
 
 
1161
If the updates should be stored in another location, specify it here.
 
1162
 
 
1163
=item B<--channel>
756
1164
 
757
1165
sa-update can update multiple channels at the same time.  By default, it will
758
1166
only access "updates.spamassassin.org", but more channels can be specified via
761
1169
 
762
1170
        sa-update --channel foo.example.com --channel bar.example.com
763
1171
 
764
 
=item B<channelfile>
 
1172
=item B<--channelfile>
765
1173
 
766
 
Similar to the B<channel> option, except specify the additional channels in a
 
1174
Similar to the B<--channel> option, except specify the additional channels in a
767
1175
file instead of on the commandline.  This is extremely useful when there are a
768
1176
lot of additional channels.
769
1177
 
770
 
=item B<usegpg>
771
 
 
772
 
sa-update only verifies update archives by use of a SHA1 checksum.  While this
773
 
verifies whether or not the downloaded archive has been corrupted, it does not
774
 
offer any form of security regarding whether or not the downloaded archive is
775
 
legitimate (aka: non-modifed by evildoers).  Use this option to enable GPG
776
 
verification of the archive to solve the problem.
777
 
 
778
 
Note: Use of the following gpgkey-related options will automatically enable
779
 
GPG verification.
 
1178
=item B<--gpg>, B<--nogpg>
 
1179
 
 
1180
sa-update by default will verify update archives by use of a SHA1 checksum
 
1181
and GPG signature.  SHA1 hashes can verify whether or not the downloaded
 
1182
archive has been corrupted, but it does not offer any form of security
 
1183
regarding whether or not the downloaded archive is legitimate (aka:
 
1184
non-modifed by evildoers).  GPG verification of the archive is used to
 
1185
solve that problem.
 
1186
 
 
1187
If you wish to skip GPG verification, you can use the B<--nogpg> option
 
1188
to disable its use.  Use of the following gpgkey-related options will
 
1189
override B<--nogpg> and keep GPG verification enabled.
780
1190
 
781
1191
Note: Currently, only GPG itself is supported (ie: not PGP).  v1.2 has been
782
1192
tested, although later versions ought to work as well.
783
1193
 
784
 
=item B<gpgkey>
 
1194
=item B<--gpgkey>
785
1195
 
786
1196
sa-update has the concept of "release trusted" GPG keys.  When an archive is
787
1197
downloaded and the signature verified, sa-update requires that the signature
789
1199
prevents third parties from manipulating the files on a mirror, for instance,
790
1200
and signing with their own key.
791
1201
 
792
 
By default, sa-update trusts key id 265FA05B, which is the standard
 
1202
By default, sa-update trusts key id C<265FA05B>, which is the standard
793
1203
SpamAssassin release key.  Use this option to add more trusted keys.
794
1204
 
795
1205
For multiple keys, use the option multiple times.  i.e.:
798
1208
 
799
1209
Note: use of this option automatically enables GPG verification.
800
1210
 
801
 
=item B<gpgkeyfile>
 
1211
=item B<--gpgkeyfile>
802
1212
 
803
 
Similar to the B<gpgkey> option, except specify the additional keys in a file
 
1213
Similar to the B<--gpgkey> option, except specify the additional keys in a file
804
1214
instead of on the commandline.  This is extremely useful when there are a lot
805
1215
of additional keys that you wish to trust.
806
1216
 
 
1217
=item B<--gpghomedir>
 
1218
 
 
1219
Specify a directory path to use as a storage area for the C<sa-update> GPG
 
1220
keyring.  By default, this is
 
1221
 
 
1222
        @@LOCAL_RULES_DIR@@/sa-update-keys
 
1223
 
807
1224
=item B<-D> [I<area,...>], B<--debug> [I<area,...>]
808
1225
 
809
1226
Produce debugging output.  If no areas are listed, all debugging information is
813
1230
 
814
1231
        sa-update -D channel,gpg,http
815
1232
 
 
1233
For more information about which areas (also known as channels) are available,
 
1234
please see the documentation at:
 
1235
 
 
1236
        C<http://wiki.apache.org/spamassassin/DebugChannels>
 
1237
 
816
1238
=item B<-h>, B<--help>
817
1239
 
818
1240
Print help message and exit.
823
1245
 
824
1246
=back
825
1247
 
 
1248
=head1 EXIT CODES
 
1249
 
 
1250
An exit code of C<0> means an update was available, and was downloaded and
 
1251
installed successfully.
 
1252
 
 
1253
An exit code of C<1> means no fresh updates were available.
 
1254
 
 
1255
An exit code of C<4> or higher, indicates that errors occurred while
 
1256
attempting to download and extract updates.
 
1257
 
826
1258
=head1 SEE ALSO
827
1259
 
828
1260
Mail::SpamAssassin(3)
840
1272
 
841
1273
=head1 AUTHORS
842
1274
 
843
 
The SpamAssassin(tm) Project <http://spamassassin.apache.org/>
 
1275
The Apache SpamAssassin(tm) Project <http://spamassassin.apache.org/>
844
1276
 
845
1277
=head1 COPYRIGHT
846
1278