View | Details | Raw Unified | Return to bug 4941
Collapse All | Expand All

(-)lib/Mail/SpamAssassin/Util.pm (+55 lines)
Lines 925-930 Link Here
925
  return ($reportfile, $tmpfile);
925
  return ($reportfile, $tmpfile);
926
}
926
}
927
927
928
=item my ($dirpath) = secure_tmpdir();
929
930
Generates a directory for temporary files.  Creates it securely and
931
returns the path to the directory.
932
933
If it cannot create a directory after 20 tries, it returns C<undef>.
934
935
=cut
936
937
# stolen from secure_tmpfile()
938
sub secure_tmpdir {
939
  my $tmpdir = Mail::SpamAssassin::Util::untaint_file_path(File::Spec->tmpdir());
940
941
  if (!$tmpdir) {
942
    # Note: we would prefer to keep this fatal, as not being able to
943
    # find a writable tmpdir is a big deal for the calling code too.
944
    # That would be quite a psychotic case, also.
945
    warn "util: cannot find a temporary directory, set TMP or TMPDIR in environment";
946
    return;
947
  }
948
949
  my ($reportpath, $tmppath);
950
  my $umask = umask 077;
951
952
  for (my $retries = 20; $retries > 0; $retries--) {
953
    # we do not rely on the obscurity of this name for security,
954
    # we use a average-quality PRG since this is all we need
955
    my $suffix = join('', (0..9,'A'..'Z','a'..'z')[rand 62, rand 62, rand 62,
956
						   rand 62, rand 62, rand 62]);
957
    $reportpath = File::Spec->catfile($tmpdir,".spamassassin${$}${suffix}tmp");
958
959
    # instead, we require O_EXCL|O_CREAT to guarantee us proper
960
    # ownership of our file, read the open(2) man page
961
    if (mkdir $reportpath, 0700) {
962
      $tmppath = $reportpath;
963
      last;
964
    }
965
966
    if ($!{EEXIST}) {
967
      # it is acceptable if $reportpath already exists, try another
968
      next;
969
    }
970
    
971
    # error, maybe "out of quota" or "too many open files" (bug 4017)
972
    warn "util: secure_tmpdir failed to create file '$reportpath': $!\n";
973
  }
974
975
  umask $umask;
976
977
  warn "util: secure_tmpdir failed to create a directory, giving up" if (!$tmppath);
978
979
  return $tmppath;
980
}
981
982
928
###########################################################################
983
###########################################################################
929
984
930
sub uri_to_domain {
985
sub uri_to_domain {
(-)sa-update.raw (-212 / +266 lines)
Lines 40-46 Link Here
40
# Standard perl modules
40
# Standard perl modules
41
use File::Spec;
41
use File::Spec;
42
use File::Path;
42
use File::Path;
43
use File::Copy;
44
use Getopt::Long;
43
use Getopt::Long;
45
use Pod::Usage;
44
use Pod::Usage;
46
use Config;
45
use Config;
Lines 102-107 Link Here
102
# Clean up PATH appropriately
101
# Clean up PATH appropriately
103
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
102
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
104
103
104
##############################################################################
105
105
# Default list of GPG keys allowed to sign update releases
106
# Default list of GPG keys allowed to sign update releases
106
#
107
#
107
# pub   1024D/265FA05B 2003-06-09
108
# pub   1024D/265FA05B 2003-06-09
Lines 127-133 Link Here
127
#
128
#
128
my @channels = ( 'updates.spamassassin.org' );
129
my @channels = ( 'updates.spamassassin.org' );
129
130
131
##############################################################################
130
132
133
use constant MIRBY_DOWNLOADED => -1;
134
131
my %opt = ();
135
my %opt = ();
132
@{$opt{'gpgkey'}} = ();
136
@{$opt{'gpgkey'}} = ();
133
@{$opt{'channel'}} = ();
137
@{$opt{'channel'}} = ();
Lines 229-237 Link Here
229
  $opt{$optkey} = $1;
233
  $opt{$optkey} = $1;
230
}
234
}
231
235
232
my $GPGPath;
236
##############################################################################
233
237
234
# deal with gpg-related options
238
# Deal with gpg-related options
239
235
if (@{$opt{'gpgkey'}}) {
240
if (@{$opt{'gpgkey'}}) {
236
  $GPG_ENABLED = 1;
241
  $GPG_ENABLED = 1;
237
  foreach my $key (@{$opt{'gpgkey'}}) {
242
  foreach my $key (@{$opt{'gpgkey'}}) {
Lines 244-249 Link Here
244
    $valid_GPG{$key} = 1;
249
    $valid_GPG{$key} = 1;
245
  }
250
  }
246
}
251
}
252
247
if (defined $opt{'gpgkeyfile'}) {
253
if (defined $opt{'gpgkeyfile'}) {
248
  $GPG_ENABLED = 1;
254
  $GPG_ENABLED = 1;
249
  unless (open(GPG, $opt{'gpgkeyfile'})) {
255
  unless (open(GPG, $opt{'gpgkeyfile'})) {
Lines 262-286 Link Here
262
  }
268
  }
263
  close(GPG);
269
  close(GPG);
264
}
270
}
265
if ( $opt{'import'} ) {
266
  my $ex = import_gpg_key($opt{'import'});
267
  exit $ex;
268
}
269
271
270
# does the sa-update keyring exist?  if not, import it
272
# At this point, we need to know where GPG is ...
271
if ($GPG_ENABLED) {
273
my $GPGPath;
274
if ($GPG_ENABLED || $opt{'import'}) {
275
  # find GPG in the PATH
276
  # bug 4958: for *NIX it's "gpg", in Windows it's "gpg.exe"
277
  $GPGPath = 'gpg' . $Config{_exe};
278
  dbg("gpg: Searching for '$GPGPath'");
279
280
  if ($GPGPath = Mail::SpamAssassin::Util::find_executable_in_env_path($GPGPath)) {
281
    dbg("gpg: found $GPGPath");
282
  }
283
  else {
284
    die "error: gpg required but not found!\n";
285
  }
286
287
  # GPG was found, and we've been asked to import a key only
288
  if ( $opt{'import'} ) {
289
    my $ex = import_gpg_key($opt{'import'});
290
    exit $ex;
291
  }
292
293
  # does the sa-update keyring exist?  if not, import it
272
  if(!-f File::Spec->catfile($opt{'gpghomedir'}, "secring.gpg")) {
294
  if(!-f File::Spec->catfile($opt{'gpghomedir'}, "secring.gpg")) {
273
    import_default_keyring();
295
    import_default_keyring();
274
    # attempt to continue even if this fails, anyway
296
    # attempt to continue even if this fails, anyway
275
  }
297
  }
276
}
277
298
278
# convert fingerprint gpg ids to keyids
299
  # specify which keys are trusted
279
foreach (keys %valid_GPG) {
300
  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
280
  my $id = substr $_, -8;
301
281
  $valid_GPG{$id} = 1;
302
  # convert fingerprint gpg ids to keyids
303
  foreach (keys %valid_GPG) {
304
    my $id = substr $_, -8;
305
    $valid_GPG{$id} = 1;
306
  }
282
}
307
}
283
308
309
##############################################################################
310
284
# Deal with channel-related options
311
# Deal with channel-related options
285
if (defined $opt{'channel'} && scalar @{$opt{'channel'}} > 0) {
312
if (defined $opt{'channel'} && scalar @{$opt{'channel'}} > 0) {
286
  @channels = @{$opt{'channel'}};
313
  @channels = @{$opt{'channel'}};
Lines 311-323 Link Here
311
  }
338
  }
312
}
339
}
313
340
314
# find GPG in the PATH
315
if ($GPG_ENABLED) {
316
  $GPGPath = find_gpg_path();
317
  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
318
}
319
320
321
my $res = Net::DNS::Resolver->new();
341
my $res = Net::DNS::Resolver->new();
322
342
323
my $ua = LWP::UserAgent->new();
343
my $ua = LWP::UserAgent->new();
Lines 327-341 Link Here
327
347
328
# Generate a temporary file to put channel content in for later use ...
348
# Generate a temporary file to put channel content in for later use ...
329
my ($content_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
349
my ($content_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
350
if ( !defined $content_file ) {
351
  die "fatal: could not create temporary channel content file: $!\n";
352
}
330
close($tfh);
353
close($tfh);
331
354
332
# and another, for the new config file
333
my ($newcf_file, $tfh2) = Mail::SpamAssassin::Util::secure_tmpfile();
334
close($tfh2);
335
336
# by default, exit code is 1, to indicate no updates occurred
355
# by default, exit code is 1, to indicate no updates occurred
337
my $exit = 1;
356
my $exit = 1;
338
357
358
# Use a temporary directory for all update channels
359
my $UPDTmp;
360
339
# Go ahead and loop through all of the channels
361
# Go ahead and loop through all of the channels
340
foreach my $channel (@channels) {
362
foreach my $channel (@channels) {
341
  dbg("channel: attempting channel $channel");
363
  dbg("channel: attempting channel $channel");
Lines 344-359 Link Here
344
  my $nicechannel = $channel;
366
  my $nicechannel = $channel;
345
  $nicechannel =~ tr/A-Za-z0-9-/_/cs;
367
  $nicechannel =~ tr/A-Za-z0-9-/_/cs;
346
368
347
  my $UPDDir = "$opt{'updatedir'}/$nicechannel";
369
  my $UPDDir = File::Spec->catfile($opt{'updatedir'}, $nicechannel);
348
  my $UPDTmp = "$opt{'updatedir'}/$nicechannel.tmp";
349
  my $CFFile = "$UPDDir.cf";
370
  my $CFFile = "$UPDDir.cf";
350
  my $CFFTmp = $newcf_file;
371
  my $PREFile = "$UPDDir.pre";
351
372
352
  dbg("channel: update directory $UPDDir");
373
  dbg("channel: update directory $UPDDir");
353
  dbg("channel: update tmp directory $UPDTmp");
354
  dbg("channel: channel cf file $CFFile");
374
  dbg("channel: channel cf file $CFFile");
355
  dbg("channel: channel tmp cf file $CFFTmp");
375
  dbg("channel: channel pre file $PREFile");
356
376
377
  my($mirby, $mirby_time);
378
  my $mirby_path = File::Spec->catfile($UPDDir, "MIRRORED.BY");
379
357
  # try to read metadata from channel.cf file
380
  # try to read metadata from channel.cf file
358
  my $currentV = -1;
381
  my $currentV = -1;
359
  if (open(CF, $CFFile)) {
382
  if (open(CF, $CFFile)) {
Lines 395-422 Link Here
395
    next;
418
    next;
396
  }
419
  }
397
420
398
  # ensure dirs exist, upfront
421
  # Read in the MIRRORED.BY file if it exists
399
  unless (-d $UPDDir) {
422
  if (open(MIRBY, $mirby_path)) {
400
    dbg("channel: creating $UPDDir");
423
    local $/ = undef;
401
    mkpath([$UPDDir], 0, 0777) or die "fatal: can't create $UPDDir: $!\n";
424
    $mirby = <MIRBY>;
402
  }
425
    close(MIRBY);
403
  unless (-d $UPDTmp) {
404
    dbg("channel: creating $UPDTmp");
405
    mkpath([$UPDTmp], 0, 0777) or die "fatal: can't create $UPDTmp: $!\n";
406
  }
407
426
408
  # copy the MIRRORED.BY file to the tmpdir, if it exists
427
    $mirby_time = (stat $mirby_path)[9];
409
  if (-f "$UPDDir/MIRRORED.BY") {
410
    unlink("$UPDTmp/MIRRORED.BY");
411
412
    my ($x, $atime, $mtime);
413
    ($x,$x,$x,$x,$x,$x,$x,$x,$atime,$mtime,$x) = stat "$UPDDir/MIRRORED.BY";
414
415
    copy("$UPDDir/MIRRORED.BY", "$UPDTmp/MIRRORED.BY")
416
            or die "fatal: cannot copy $UPDDir/MIRRORED.BY to $UPDTmp/MIRRORED.BY";
417
418
    # ensure modtimes match
419
    utime($atime, $mtime, "$UPDTmp/MIRRORED.BY");
420
  }
428
  }
421
  else {
429
  else {
422
    # We don't currently have the list of mirrors, so go grab it.
430
    # We don't currently have the list of mirrors, so go grab it.
Lines 426-458 Link Here
426
      warn "error: no mirror data available for channel $channel\n";
434
      warn "error: no mirror data available for channel $channel\n";
427
      channel_failed("channel: MIRRORED.BY file location was not in DNS");
435
      channel_failed("channel: MIRRORED.BY file location was not in DNS");
428
    }
436
    }
429
    $mirror = http_get($mirror);
437
    $mirby = http_get($mirror);
430
    unless ($mirror) {
438
    unless ($mirby) {
431
      warn "error: no mirror data available for channel $channel\n";
439
      warn "error: no mirror data available for channel $channel\n";
432
      channel_failed("channel: MIRRORED.BY contents were missing");
440
      channel_failed("channel: MIRRORED.BY contents were missing");
433
      next;
441
      next;
434
    }
442
    }
443
    $mirby_time = MIRBY_DOWNLOADED;
435
444
436
    unless (open(MIR, ">$UPDTmp/MIRRORED.BY")) {
437
      warn "error: can't create mirrors file: $!\n";
438
      channel_failed("channel: MIRRORED.BY creation failure");
439
      next;
440
    }
441
    print MIR $mirror;
442
    close(MIR);
443
    dbg("channel: MIRRORED.BY file retrieved");
445
    dbg("channel: MIRRORED.BY file retrieved");
444
  }
446
  }
445
447
446
  # Read in the list of mirrors
448
  # Read in the list of mirrors
447
  unless (open(MIR, "$UPDTmp/MIRRORED.BY")) {
448
    warn "error: can't read mirrors file: $!\n";
449
    channel_failed("channel: MIRRORED.BY file is unreadable");
450
    next;
451
  }
452
453
  dbg("channel: reading MIRRORED.BY file");
449
  dbg("channel: reading MIRRORED.BY file");
454
  my %mirrors = ();
450
  my %mirrors = ();
455
  while(my $mirror = <MIR>) {
451
  my @mirrors = split(/^/, $mirby);
452
  while(my $mirror = shift @mirrors) {
456
    next if ($mirror =~ /^#/);  # explicitly skip comments
453
    next if ($mirror =~ /^#/);  # explicitly skip comments
457
454
458
    # We only support HTTP right now
455
    # We only support HTTP right now
Lines 474-480 Link Here
474
      $mirrors{$mirror}->{$k} = $v;
471
      $mirrors{$mirror}->{$k} = $v;
475
    }
472
    }
476
  }
473
  }
477
  close(MIR);
478
474
479
  unless (keys %mirrors) {
475
  unless (keys %mirrors) {
480
    warn "error: no mirrors available for channel $channel\n";
476
    warn "error: no mirrors available for channel $channel\n";
Lines 482-497 Link Here
482
    next;
478
    next;
483
  }
479
  }
484
480
485
  # remember the mtime of the file so we can IMS GET later on
486
  my $mirby_time = (stat("$UPDTmp/MIRRORED.BY"))[9];
487
488
489
  # Now that we've laid the foundation, go grab the appropriate files
481
  # Now that we've laid the foundation, go grab the appropriate files
490
  #
482
  #
491
  my $content;
483
  my $content;
492
  my $SHA1;
484
  my $SHA1;
493
  my $GPG;
485
  my $GPG;
494
  my $mirby;
495
486
496
  # Loop through all available mirrors, choose from them randomly
487
  # Loop through all available mirrors, choose from them randomly
497
  # if the archive get fails, choose another mirror,
488
  # if the archive get fails, choose another mirror,
Lines 519-531 Link Here
519
510
520
    # try to update our list of mirrors.
511
    # try to update our list of mirrors.
521
    # a failure here doesn't cause channel failure.
512
    # a failure here doesn't cause channel failure.
522
    $mirby = http_get("$mirror/MIRRORED.BY", $mirby_time);
513
    if ($mirby_time != MIRBY_DOWNLOADED) {
514
      my $mirby_tmp = http_get("$mirror/MIRRORED.BY", $mirby_time);
515
      if ($mirby_tmp) {
516
        $mirby = $mirby_tmp;
517
        $mirby_time = MIRBY_DOWNLOADED;
518
      }
519
    }
523
520
524
    last;
521
    last;
525
  }
522
  }
526
523
527
  unless ($content && $SHA1 && (!$GPG_ENABLED || $GPG)) {
524
  unless ($content && $SHA1 && (!$GPG_ENABLED || $GPG)) {
528
    warn "error: channel $channel has no working mirrors\n";
529
    channel_failed("channel: could not find working mirror");
525
    channel_failed("channel: could not find working mirror");
530
    next;
526
    next;
531
  }
527
  }
Lines 536-545 Link Here
536
  $SHA1 =~ /^([a-fA-F0-9]{40})/;
532
  $SHA1 =~ /^([a-fA-F0-9]{40})/;
537
  $SHA1 = $1 || 'INVALID';
533
  $SHA1 = $1 || 'INVALID';
538
  my $digest = sha1_hex($content);
534
  my $digest = sha1_hex($content);
539
  dbg("sha1: verification expected: $SHA1");
535
  dbg("sha1: verification wanted: $SHA1");
540
  dbg("sha1: verification got     : $digest");
536
  dbg("sha1: verification result: $digest");
541
  unless ($digest eq $SHA1) {
537
  unless ($digest eq $SHA1) {
542
    warn "error: can't verify SHA1 signature\n";
543
    channel_failed("channel: SHA1 verification failed");
538
    channel_failed("channel: SHA1 verification failed");
544
    next;
539
    next;
545
  }
540
  }
Lines 560-565 Link Here
560
    dbg("gpg: populating temp signature file");
555
    dbg("gpg: populating temp signature file");
561
    my $sig_file;
556
    my $sig_file;
562
    ($sig_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
557
    ($sig_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
558
    if ( !defined $sig_file ) {
559
      die "fatal: couldn't create temp file for GPG signature: $!\n";
560
    }
563
    binmode $tfh;
561
    binmode $tfh;
564
    print $tfh $GPG;
562
    print $tfh $GPG;
565
    close($tfh);
563
    close($tfh);
Lines 656-792 Link Here
656
  }
654
  }
657
655
658
  # OK, we're all validated at this point, install the new version
656
  # OK, we're all validated at this point, install the new version
659
  dbg("channel: file verification passed, installing update");
657
  dbg("channel: file verification passed, testing update");
660
658
661
  if ($mirby) {
659
  dbg("channel: preparing temp directory for new channel");
662
    dbg("channel: updating MIRRORED.BY contents");
660
  if (!$UPDTmp) {
663
    if (open(MBY, ">$UPDTmp/MIRRORED.BY")) {
661
    $UPDTmp = Mail::SpamAssassin::Util::secure_tmpdir();
664
      print MBY $mirby;
662
    dbg("generic: update tmp directory $UPDTmp");
665
      close(MBY);
666
    }
667
    else {
668
      warn "error: can't write new MIRRORED.BY file: $!\n";
669
    }
670
  }
663
  }
671
664
  elsif (!clean_update_dir($UPDTmp)) {
672
  dbg("channel: cleaning out update directory");
665
    die "channel: attempt to clean update dir failed, aborting";
673
  if (!clean_update_dir($UPDTmp)) {
674
    channel_failed("channel: attempt to clean update dir failed");
675
    next;
676
  }
666
  }
677
667
678
  unlink $CFFTmp || warn "error: can't remove file $CFFTmp: $!\n";
679
680
  $tfh = IO::Zlib->new($content_file, "rb");
681
  die "fatal: couldn't read content tmpfile $content_file: $!\n" unless $tfh;
682
683
  my $tar = Archive::Tar->new($tfh);
684
  die "fatal: couldn't create Archive::Tar object!\n" unless $tar;
685
686
  dbg("channel: extracting archive");
668
  dbg("channel: extracting archive");
687
  my $ret = taint_safe_archive_extract($UPDTmp, $tar);
669
  if (!taint_safe_archive_extract($UPDTmp, $content_file)) {
688
689
  unless ($ret) {
690
    close($tfh);
691
    warn "error: couldn't extract the tar archive!\n";
692
    channel_failed("channel: archive extraction failed");
670
    channel_failed("channel: archive extraction failed");
693
    next;
671
    next;
694
  }
672
  }
695
  close($tfh);
696
673
697
  # check --lint
674
  # check --lint
698
675
699
  if (!lint_check_dir($UPDTmp)) {
676
  if (!lint_check_dir($UPDTmp)) {
700
    warn "error: lint check of update failed!  channel failed\n";
701
    channel_failed("channel: lint check of update failed");
677
    channel_failed("channel: lint check of update failed");
702
    next;
678
    next;
703
  }
679
  }
704
680
681
  dbg("channel: lint check succeeded, extracting archive to $UPDDir...");
705
682
706
  # OK, lint passed. now create the update config file
683
  if (-d $UPDDir) {
684
    # ok that worked, too late to stop now!   At this stage, if there are
685
    # errors, we have to attempt to carry on regardless, since we've already
686
    # blown away the old ruleset.
687
    dbg("channel: point of no return for existing $UPDDir");
707
688
708
  dbg("channel: creating update config file");
689
    # clean out the previous channel files
709
  unless (open(CF, ">$CFFTmp")) {
690
    if (! unlink $PREFile ) {
710
    die "fatal: can't create new channel cf $CFFTmp: $!\n";
691
      warn("channel: attempt to rm channel pre file failed, attempting to continue anyway");
692
    }
693
    if (! unlink $CFFile ) {
694
      warn("channel: attempt to rm channel cf file failed, attempting to continue anyway");
695
    }
696
    if (!clean_update_dir($UPDDir)) {
697
      warn("channel: attempt to rm channel directory failed, attempting to continue anyway");
698
    }
711
  }
699
  }
700
  else {
701
    # create the dir, if it doesn't exist
702
    dbg("channel: creating $UPDDir");
703
    if (!mkpath([$UPDDir], 0, 0777)) {
704
      # bug 4941: try to get rid of the empty directories to avoid leaving SA
705
      # with no rules.
706
      rmdir $UPDDir;
707
      rmdir $opt{'updatedir'};
708
      die "fatal: can't create $UPDDir: $!\n";
709
    }
712
710
713
  # Put in whatever metadata we need
711
    # ok, that test worked.  it's now likely that the .cf's will
714
  print CF "# UPDATE version $newV\n";
712
    # similarly be ok to rename, too.   Too late to stop from here on
713
    dbg("channel: point of no return for new $UPDDir");
714
  }
715
715
716
  # try to figure out the relative path dir name
716
  # extract the files again for the last time
717
  my $relativeDir = $UPDDir;
717
  if (!taint_safe_archive_extract($UPDDir, $content_file)) {
718
  $UPDDir =~ m,/([^/]+)/*$,;
718
    channel_failed("channel: archive extraction failed");
719
  if ($1) {
719
720
    $relativeDir = $1;
720
    # bug 4941: try to get rid of the empty directories to avoid leaving SA
721
    # with no rules.
722
    if (!clean_update_dir($UPDDir)) {
723
      warn "channel: attempt to clean up failed extraction also failed!\n";
724
    }
725
    else {
726
      rmdir $UPDDir;
727
      rmdir $opt{'updatedir'};
728
    }
729
730
    next;
721
  }
731
  }
722
  dbg("channel: updatedir=$UPDDir relativepath=$relativeDir");
723
732
724
  my @files = ();
733
  # Write out the mirby file, not fatal if it doesn't work
725
  # now include *.cf
734
  dbg("channel: creating MIRRORED.BY file");
726
  unless (opendir(DIR, $UPDTmp)) {
735
  if (open(MBY, ">$mirby_path")) {
727
    die "fatal: can't access $UPDTmp: $!\n";
736
    print MBY $mirby;
737
    close(MBY);
728
  }
738
  }
739
  else {
740
    warn "error: can't write new MIRRORED.BY file: $!\n";
741
  }
742
743
  # the last step is to create the .cf and .pre files to include the
744
  # channel files
745
  my @CF = ();
746
  my @PRE = ();
747
748
  dbg("channel: creating update cf/pre files");
749
750
  # Put in whatever metadata we need
751
  push(@CF, "# UPDATE version $newV\n");
752
753
  # Find all of the cf and pre files
754
  unless (opendir(DIR, $UPDDir)) {
755
    die "fatal: can't access $UPDDir: $!\n";
756
  }
729
  while(my $file = readdir(DIR)) {
757
  while(my $file = readdir(DIR)) {
730
    $file =~ /^([^\/]+)$/;       # untaint
758
    $file =~ /^(.+)$/;       # untaint
731
    $file = $1;
759
    $file = $1;
732
    next unless (-f "$UPDTmp/$file");
760
    my $path = File::Spec->catfile($UPDDir, $file);
733
    next if ($file eq "MIRRORED.BY");   # handled separately
761
    next unless (-f $path);   # shouldn't ever happen
734
762
735
    dbg("channel: adding $file");
736
737
    if ($file =~ /\.cf$/) {
763
    if ($file =~ /\.cf$/) {
738
      print CF "include $relativeDir/$file\n";
764
      push(@CF, "include $nicechannel/$file\n");
739
    }
765
    }
766
    elsif ($file =~ /\.pre$/) {
767
      push(@PRE, "include $nicechannel/$file\n");
768
    }
769
    else {
770
      next;
771
    }
740
772
741
    push (@files, $file);
773
    dbg("channel: adding $file");
742
  }
774
  }
743
  closedir(DIR);
775
  closedir(DIR);
744
  if (!close(CF)) {
776
745
    warn "write to $CFFTmp failed! attempting to continue";
777
  # Finally, write out the files to include the update files
746
    channel_failed("write to $CFFTmp failed");
778
  if (!write_channel_file($PREFile, \@PRE)) {
779
    channel_failed("channel: writing of $PREFile failed");
747
    next;
780
    next;
748
  }
781
  }
749
782
  if (!write_channel_file($CFFile, \@CF)) {
750
  dbg("channel: applying changes to $UPDDir...");
783
    channel_failed("channel: writing of $CFFile failed");
751
784
    next;
752
  # too late to stop now!   At this stage, if there are errors,
753
  # we have to attempt to carry on regardless, since we've already
754
  # blown away the old ruleset.
755
756
  # clean out the "real" update dir, and copy from tmp areas
757
  if (!clean_update_dir($UPDDir)) {
758
    warn("channel: attempt to rm contents failed, attempting to continue anyway");
759
  }
785
  }
760
786
761
  foreach my $file (@files) {
762
    rename("$UPDTmp/$file", "$UPDDir/$file")
763
        or warn "rename $UPDTmp/$file $UPDDir/$file failed: $!";
764
  }
765
766
  unlink $CFFile || warn "error: can't remove file $CFFile: $!\n";
767
  cross_fs_rename($CFFTmp, $CFFile)
768
      or warn "rename $CFFTmp $CFFile failed: $!";
769
770
  unlink("$UPDDir/MIRRORED.BY");
771
  rename("$UPDTmp/MIRRORED.BY", "$UPDDir/MIRRORED.BY")
772
      or warn "error: couldn't mv $UPDTmp/MIRRORED.BY $UPDDir/MIRRORED.BY: $!\n";
773
774
  rmdir $UPDTmp;
775
  $exit = 0;            # "exit 0" means an update occurred
787
  $exit = 0;            # "exit 0" means an update occurred
776
788
777
  dbg("channel: update complete");
789
  dbg("channel: update complete");
778
}
790
}
779
791
792
##############################################################################
793
794
# clean out the temp dir
795
if ($UPDTmp) {
796
  dbg("generic: cleaning up temporary directory/files");
797
  if (!clean_update_dir($UPDTmp)) {
798
    warn "error: unable to clean out the files in $UPDTmp\n";
799
  }
800
}
801
780
# clear out the temp files if they still exist
802
# clear out the temp files if they still exist
781
foreach ( $newcf_file, $content_file ) {
803
foreach ( $content_file, $UPDTmp ) {
782
  if (-e $_) {
804
  next unless (defined $_ && -e $_);
805
806
  if (-d _) {
807
    rmdir $_ || warn "error: can't remove directory $_: $!\n";
808
  }
809
  elsif (-f _) {
783
    unlink $_ || warn "error: can't remove file $_: $!\n";
810
    unlink $_ || warn "error: can't remove file $_: $!\n";
784
  }
811
  }
812
  else {
813
    warn "error: '$_' isn't a file nor a directory, skipping\n";
814
  }
785
}
815
}
786
816
787
dbg("diag: updates complete, exiting with code $exit");
817
dbg("diag: updates complete, exiting with code $exit");
788
exit $exit;
818
exit $exit;
789
819
820
##############################################################################
821
822
sub write_channel_file {
823
  my ($filename, $contents) = @_;
824
825
  return 1 unless @{$contents};
826
827
  if (open(FILE, ">$filename")) {
828
    print FILE @{$contents};
829
    close FILE or return 0;
830
    return 1;
831
  }
832
833
  return 0;
834
}
835
836
##############################################################################
837
790
sub channel_failed {
838
sub channel_failed {
791
  my $reason = shift;
839
  my $reason = shift;
792
  warn("$reason, channel failed\n");
840
  warn("$reason, channel failed\n");
Lines 797-806 Link Here
797
  }
845
  }
798
}
846
}
799
847
848
##############################################################################
849
800
sub taint_safe_archive_extract {
850
sub taint_safe_archive_extract {
801
  my $todir = shift;
851
  my $todir = shift;
802
  my $tar = shift;
852
  my $input = shift;
803
853
854
  my $tfh = IO::Zlib->new($input, "rb");
855
  die "fatal: couldn't read content tmpfile $content_file: $!\n" unless $tfh;
856
857
  my $tar = Archive::Tar->new($tfh);
858
  die "fatal: couldn't create Archive::Tar object!\n" unless $tar;
859
804
  # stupid Archive::Tar is not natively taint-safe! duh.
860
  # stupid Archive::Tar is not natively taint-safe! duh.
805
  # return $tar->extract();
861
  # return $tar->extract();
806
  # instead, get the file list, untaint, and extract one-by-one.
862
  # instead, get the file list, untaint, and extract one-by-one.
Lines 812-818 Link Here
812
    $file =~ /^([-\.\,\/a-zA-Z0-9_]+)$/;
868
    $file =~ /^([-\.\,\/a-zA-Z0-9_]+)$/;
813
    my $outfname = $1;
869
    my $outfname = $1;
814
    $outfname =~ s/\.\.\//__\//gs;      # avoid "../" dir traversal attacks
870
    $outfname =~ s/\.\.\//__\//gs;      # avoid "../" dir traversal attacks
815
    $outfname = "$todir/$outfname";
871
    $outfname = File::Spec->catfile($todir, $outfname);
816
872
817
    dbg "extracting: $outfname";
873
    dbg "extracting: $outfname";
818
    if (open OUT, ">".$outfname) {
874
    if (open OUT, ">".$outfname) {
Lines 840-845 Link Here
840
  return;       # undef = failure
896
  return;       # undef = failure
841
}
897
}
842
898
899
##############################################################################
900
843
# Do a generic TXT query
901
# Do a generic TXT query
844
sub do_txt_query {
902
sub do_txt_query {
845
  my($query) = shift;
903
  my($query) = shift;
Lines 866-871 Link Here
866
  return $result;
924
  return $result;
867
}
925
}
868
926
927
##############################################################################
928
869
# Do a GET request via HTTP for a certain URL
929
# Do a GET request via HTTP for a certain URL
870
# Use the optional time_t value to do an IMS GET
930
# Use the optional time_t value to do an IMS GET
871
sub http_get {
931
sub http_get {
Lines 916-921 Link Here
916
  return;
976
  return;
917
}
977
}
918
978
979
##############################################################################
980
919
# choose a random integer between 0 and the total weight of all mirrors
981
# choose a random integer between 0 and the total weight of all mirrors
920
# loop through the mirrors from largest to smallest weight
982
# loop through the mirrors from largest to smallest weight
921
# if random number is < largest weight, use it
983
# if random number is < largest weight, use it
Lines 953-963 Link Here
953
  return $mirrors[0];
1015
  return $mirrors[0];
954
}
1016
}
955
1017
1018
##############################################################################
1019
956
sub print_version {
1020
sub print_version {
957
  print "sa-update version $VERSION\n"
1021
  print "sa-update version $VERSION\n"
958
      . "  running on Perl version " . join(".", map { $_||=0; $_*1 } ($] =~ /(\d)\.(\d{3})(\d{3})?/ )) . "\n";
1022
      . "  running on Perl version " . join(".", map { $_||=0; $_*1 } ($] =~ /(\d)\.(\d{3})(\d{3})?/ )) . "\n";
959
}
1023
}
960
1024
1025
##############################################################################
1026
961
sub print_usage_and_exit {
1027
sub print_usage_and_exit {
962
  my ( $message, $exitval ) = @_;
1028
  my ( $message, $exitval ) = @_;
963
  $exitval ||= 64;
1029
  $exitval ||= 64;
Lines 973-997 Link Here
973
  );
1039
  );
974
}
1040
}
975
1041
1042
##############################################################################
1043
976
sub usage {
1044
sub usage {
977
  my ( $verbose, $message ) = @_;
1045
  my ( $verbose, $message ) = @_;
978
  print "sa-update version $VERSION\n";
1046
  print "sa-update version $VERSION\n";
979
  pod2usage( -verbose => $verbose, -message => $message, -exitval => 64 );
1047
  pod2usage( -verbose => $verbose, -message => $message, -exitval => 64 );
980
}
1048
}
981
1049
982
sub find_gpg_path {
1050
##############################################################################
983
  # bug 4958: for *NIX it's "gpg", in Windows it's "gpg.exe"
984
  my $gpg = 'gpg' . $Config{_exe};
985
1051
986
  dbg("gpg: Searching for '$gpg'");
987
988
  my $path = Mail::SpamAssassin::Util::find_executable_in_env_path($gpg) ||
989
    die "fatal: couldn't find GPG\n";
990
991
  dbg("gpg: found $path");
992
  return $path;
993
}
994
995
sub interpolate_gpghomedir {
1052
sub interpolate_gpghomedir {
996
  my $gpghome = '';
1053
  my $gpghome = '';
997
  if ($opt{'gpghomedir'}) {
1054
  if ($opt{'gpghomedir'}) {
Lines 1008-1017 Link Here
1008
  return $gpghome;
1065
  return $gpghome;
1009
}
1066
}
1010
1067
1068
##############################################################################
1069
1011
sub import_gpg_key {
1070
sub import_gpg_key {
1012
  my $keyfile = shift;
1071
  my $keyfile = shift;
1013
1072
1014
  $GPGPath = find_gpg_path();
1015
  my $gpghome = interpolate_gpghomedir();
1073
  my $gpghome = interpolate_gpghomedir();
1016
1074
1017
  my $CMD = "$GPGPath $gpghome --batch ".
1075
  my $CMD = "$GPGPath $gpghome --batch ".
Lines 1031-1037 Link Here
1031
    }
1089
    }
1032
1090
1033
    if ($GNUPG =~ /^IMPORTED /) {
1091
    if ($GNUPG =~ /^IMPORTED /) {
1034
      print "sa-update --import: success. $GNUPG\n";
1092
      dbg("gpg: gpg key imported successfully");
1035
    }
1093
    }
1036
  }
1094
  }
1037
1095
Lines 1039-1083 Link Here
1039
  return ($? >> 8);
1097
  return ($? >> 8);
1040
}
1098
}
1041
1099
1100
##############################################################################
1101
1042
sub import_default_keyring {
1102
sub import_default_keyring {
1043
  my $defkey = File::Spec->catfile ($DEF_RULES_DIR, "sa-update-pubkey.txt");
1103
  my $defkey = File::Spec->catfile ($DEF_RULES_DIR, "sa-update-pubkey.txt");
1044
  return unless (-f $defkey);
1104
  unless (-f $defkey) {
1105
    dbg("gpg: import of default keyring failed, couldn't find sa-update-pubkey.txt");
1106
    return;
1107
  }
1045
1108
1046
  print "sa-update: importing default keyring to '".$opt{gpghomedir}."'...\n";
1109
  dbg("gpg: importing default keyring to '".$opt{gpghomedir});
1047
  unless (-d $opt{gpghomedir}) {
1110
  unless (-d $opt{gpghomedir}) {
1048
    # use 0700 to avoid "unsafe permissions" warning
1111
    # use 0700 to avoid "unsafe permissions" warning
1049
    mkdir ($opt{gpghomedir}, 0700) or die "cannot mkdir $opt{gpghomedir}: $!";
1112
    mkpath([$opt{'gpghomedir'}], 0, 0700) or die "cannot mkpath $opt{gpghomedir}: $!";
1050
  } 
1113
  } 
1051
  import_gpg_key($defkey);
1114
  import_gpg_key($defkey);
1052
}
1115
}
1053
1116
1117
##############################################################################
1118
1054
sub is_valid_gpg_key_id {
1119
sub is_valid_gpg_key_id {
1055
  # either a keyid (8 bytes) or a fingerprint (40 bytes)
1120
  # either a keyid (8 bytes) or a fingerprint (40 bytes)
1056
  return ($_[0] =~ /^[a-fA-F0-9]+$/ && (length $_[0] == 8 || length $_[0] == 40));
1121
  return ($_[0] =~ /^[a-fA-F0-9]+$/ && (length $_[0] == 8 || length $_[0] == 40));
1057
}
1122
}
1058
1123
1124
##############################################################################
1125
1059
sub clean_update_dir {
1126
sub clean_update_dir {
1060
  my $dir = shift;
1127
  my $dir = shift;
1128
1061
  unless (opendir(DIR, $dir)) {
1129
  unless (opendir(DIR, $dir)) {
1062
    warn "error: can't readdir $dir: $!\n";
1130
    warn "error: can't readdir $dir: $!\n";
1063
    dbg("channel: attempt to readdir failed, channel failed");
1131
    dbg("generic: attempt to readdir ($dir) failed");
1064
    return 0;
1132
    return;
1065
  }
1133
  }
1066
  while(my $file = readdir(DIR)) {
1134
  while(my $file = readdir(DIR)) {
1067
    next unless (-f "$dir/$file");
1135
    $file =~ /^(.+)$/;       # untaint
1068
    next if ($file eq 'MIRRORED.BY');
1069
    dbg("channel: unlinking $file");
1070
    $file =~ /^([^\/]+)$/;       # untaint
1071
    $file = $1;
1136
    $file = $1;
1072
    if (!unlink "$dir/$file") {
1137
1073
      warn "error: can't remove file $dir/$file: $!\n";
1138
    my $path = File::Spec->catfile($dir, $file);
1074
      return 0;
1139
    next unless (-f $path);
1140
1141
    dbg("generic: unlinking $file");
1142
    if (!unlink $path) {
1143
      warn "error: can't remove file $path: $!\n";
1144
      closedir(DIR);
1145
      return;
1075
    }
1146
    }
1076
  }
1147
  }
1077
  closedir(DIR);
1148
  closedir(DIR);
1078
  return 1;
1149
  return 1;
1079
}
1150
}
1080
1151
1152
##############################################################################
1153
1081
sub lint_check_dir {
1154
sub lint_check_dir {
1082
  my $dir = shift;
1155
  my $dir = shift;
1083
1156
Lines 1086-1093 Link Here
1086
  # "config" or otherwise be more terse. :(
1159
  # "config" or otherwise be more terse. :(
1087
  my $spamtest = new Mail::SpamAssassin( {
1160
  my $spamtest = new Mail::SpamAssassin( {
1088
    rules_filename      => $dir,
1161
    rules_filename      => $dir,
1089
    site_rules_filename => "$dir/doesnotexist",
1162
    site_rules_filename => File::Spec->catfile($dir, "doesnotexist"),
1090
    userprefs_filename  => "$dir/doesnotexist",
1163
    userprefs_filename  => File::Spec->catfile($dir, "doesnotexist"),
1091
1164
1092
    local_tests_only    => 1,
1165
    local_tests_only    => 1,
1093
    dont_copy_prefs     => 1,
1166
    dont_copy_prefs     => 1,
Lines 1108-1137 Link Here
1108
  return $res == 0;
1181
  return $res == 0;
1109
}
1182
}
1110
1183
1111
# a version of rename() that can cope with renaming files across filesystems,
1184
##############################################################################
1112
# as mv(1) can.
1113
sub cross_fs_rename {
1114
  my ($from, $to) = @_;
1115
  my $ret = rename ($from, $to);
1116
1185
1117
  if ($ret) {
1186
=cut
1118
    return $ret;        # success first time! great
1119
  }
1120
1187
1121
  # try a copy
1122
  if (!copy($from, $to)) {
1123
    # copy failed, too.  we have no further fallbacks; return the rename()
1124
    # failure code
1125
    return $ret;
1126
  }
1127
1128
  # copy succeeded, we're good; remove the source, and return success
1129
  unlink($from);
1130
  return 1;
1131
}
1132
1133
# ---------------------------------------------------------------------------
1134
1135
=head1 NAME
1188
=head1 NAME
1136
1189
1137
sa-update - automate SpamAssassin rule updates
1190
sa-update - automate SpamAssassin rule updates
Lines 1285-1290 Link Here
1285
Mail::SpamAssassin::Conf(3)
1338
Mail::SpamAssassin::Conf(3)
1286
spamassassin(1)
1339
spamassassin(1)
1287
spamd(1)
1340
spamd(1)
1341
<http://wiki.apache.org/spamassassin/RuleUpdates>
1288
1342
1289
=head1 PREREQUESITES
1343
=head1 PREREQUESITES
1290
1344

Return to bug 4941