You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by fe...@apache.org on 2006/07/22 20:21:43 UTC

svn commit: r424614 - /spamassassin/trunk/sa-update.raw

Author: felicity
Date: Sat Jul 22 11:21:43 2006
New Revision: 424614

URL: http://svn.apache.org/viewvc?rev=424614&view=rev
Log:
more work on sa-update, use File::Spec->catfile() where appropriate, only look for gpg once, etc.

Modified:
    spamassassin/trunk/sa-update.raw

Modified: spamassassin/trunk/sa-update.raw
URL: http://svn.apache.org/viewvc/spamassassin/trunk/sa-update.raw?rev=424614&r1=424613&r2=424614&view=diff
==============================================================================
--- spamassassin/trunk/sa-update.raw (original)
+++ spamassassin/trunk/sa-update.raw Sat Jul 22 11:21:43 2006
@@ -101,6 +101,8 @@
 # Clean up PATH appropriately
 Mail::SpamAssassin::Util::clean_path_in_taint_mode();
 
+##############################################################################
+
 # Default list of GPG keys allowed to sign update releases
 #
 # pub   1024D/265FA05B 2003-06-09
@@ -126,6 +128,8 @@
 #
 my @channels = ( 'updates.spamassassin.org' );
 
+##############################################################################
+
 use constant MIRBY_DOWNLOADED => -1;
 
 my %opt = ();
@@ -229,9 +233,10 @@
   $opt{$optkey} = $1;
 }
 
-my $GPGPath;
+##############################################################################
+
+# Deal with gpg-related options
 
-# deal with gpg-related options
 if (@{$opt{'gpgkey'}}) {
   $GPG_ENABLED = 1;
   foreach my $key (@{$opt{'gpgkey'}}) {
@@ -244,6 +249,7 @@
     $valid_GPG{$key} = 1;
   }
 }
+
 if (defined $opt{'gpgkeyfile'}) {
   $GPG_ENABLED = 1;
   unless (open(GPG, $opt{'gpgkeyfile'})) {
@@ -262,25 +268,46 @@
   }
   close(GPG);
 }
-if ( $opt{'import'} ) {
-  my $ex = import_gpg_key($opt{'import'});
-  exit $ex;
-}
 
-# does the sa-update keyring exist?  if not, import it
-if ($GPG_ENABLED) {
+# At this point, we need to know where GPG is ...
+my $GPGPath;
+if ($GPG_ENABLED || $opt{'import'}) {
+  # find GPG in the PATH
+  # bug 4958: for *NIX it's "gpg", in Windows it's "gpg.exe"
+  $GPGPath = 'gpg' . $Config{_exe};
+  dbg("gpg: Searching for '$GPGPath'");
+
+  if ($GPGPath = Mail::SpamAssassin::Util::find_executable_in_env_path($GPGPath)) {
+    dbg("gpg: found $GPGPath");
+  }
+  else {
+    die "error: gpg required but not found!\n";
+  }
+
+  # GPG was found, and we've been asked to import a key only
+  if ( $opt{'import'} ) {
+    my $ex = import_gpg_key($opt{'import'});
+    exit $ex;
+  }
+
+  # does the sa-update keyring exist?  if not, import it
   if(!-f File::Spec->catfile($opt{'gpghomedir'}, "secring.gpg")) {
     import_default_keyring();
     # attempt to continue even if this fails, anyway
   }
-}
 
-# convert fingerprint gpg ids to keyids
-foreach (keys %valid_GPG) {
-  my $id = substr $_, -8;
-  $valid_GPG{$id} = 1;
+  # specify which keys are trusted
+  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
+
+  # convert fingerprint gpg ids to keyids
+  foreach (keys %valid_GPG) {
+    my $id = substr $_, -8;
+    $valid_GPG{$id} = 1;
+  }
 }
 
+##############################################################################
+
 # Deal with channel-related options
 if (defined $opt{'channel'} && scalar @{$opt{'channel'}} > 0) {
   @channels = @{$opt{'channel'}};
@@ -311,13 +338,6 @@
   }
 }
 
-# find GPG in the PATH
-if ($GPG_ENABLED) {
-  $GPGPath = find_gpg_path();
-  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
-}
-
-
 my $res = Net::DNS::Resolver->new();
 
 my $ua = LWP::UserAgent->new();
@@ -335,9 +355,8 @@
 # by default, exit code is 1, to indicate no updates occurred
 my $exit = 1;
 
-# Set the temp directory that we'll use
-my $UPDTmp = Mail::SpamAssassin::Util::secure_tmpdir();
-dbg("channel: update tmp directory $UPDTmp");
+# Use a temporary directory for all update channels
+my $UPDTmp;
 
 # Go ahead and loop through all of the channels
 foreach my $channel (@channels) {
@@ -347,7 +366,7 @@
   my $nicechannel = $channel;
   $nicechannel =~ tr/A-Za-z0-9-/_/cs;
 
-  my $UPDDir = "$opt{'updatedir'}/$nicechannel";
+  my $UPDDir = File::Spec->catfile($opt{'updatedir'}, $nicechannel);
   my $CFFile = "$UPDDir.cf";
   my $PREFile = "$UPDDir.pre";
 
@@ -356,6 +375,7 @@
   dbg("channel: channel pre file $PREFile");
 
   my($mirby, $mirby_time);
+  my $mirby_path = File::Spec->catfile($UPDDir, "MIRRORED.BY");
 
   # try to read metadata from channel.cf file
   my $currentV = -1;
@@ -399,12 +419,12 @@
   }
 
   # Read in the MIRRORED.BY file if it exists
-  if (open(MIRBY, "$UPDDir/MIRRORED.BY")) {
+  if (open(MIRBY, $mirby_path)) {
     local $/ = undef;
     $mirby = <MIRBY>;
     close(MIRBY);
 
-    $mirby_time = (stat "$UPDDir/MIRRORED.BY")[9];
+    $mirby_time = (stat $mirby_path)[9];
   }
   else {
     # We don't currently have the list of mirrors, so go grab it.
@@ -502,7 +522,6 @@
   }
 
   unless ($content && $SHA1 && (!$GPG_ENABLED || $GPG)) {
-    warn "error: channel $channel has no working mirrors\n";
     channel_failed("channel: could not find working mirror");
     next;
   }
@@ -513,10 +532,9 @@
   $SHA1 =~ /^([a-fA-F0-9]{40})/;
   $SHA1 = $1 || 'INVALID';
   my $digest = sha1_hex($content);
-  dbg("sha1: verification expected: $SHA1");
-  dbg("sha1: verification got     : $digest");
+  dbg("sha1: verification wanted: $SHA1");
+  dbg("sha1: verification result: $digest");
   unless ($digest eq $SHA1) {
-    warn "error: can't verify SHA1 signature\n";
     channel_failed("channel: SHA1 verification failed");
     next;
   }
@@ -638,8 +656,12 @@
   # OK, we're all validated at this point, install the new version
   dbg("channel: file verification passed, testing update");
 
-  dbg("channel: cleaning out temp directory");
-  if (!clean_update_dir($UPDTmp)) {
+  dbg("channel: preparing temp directory for new channel");
+  if (!$UPDTmp) {
+    $UPDTmp = Mail::SpamAssassin::Util::secure_tmpdir();
+    dbg("generic: update tmp directory $UPDTmp");
+  }
+  elsif (!clean_update_dir($UPDTmp)) {
     die "channel: attempt to clean update dir failed, aborting";
   }
 
@@ -664,9 +686,15 @@
     # blown away the old ruleset.
     dbg("channel: point of no return for existing $UPDDir");
 
-    # clean out the "real" update dir
+    # clean out the previous channel files
+    if (! unlink $PREFile ) {
+      warn("channel: attempt to rm channel pre file failed, attempting to continue anyway");
+    }
+    if (! unlink $CFFile ) {
+      warn("channel: attempt to rm channel cf file failed, attempting to continue anyway");
+    }
     if (!clean_update_dir($UPDDir)) {
-      warn("channel: attempt to rm contents failed, attempting to continue anyway");
+      warn("channel: attempt to rm channel directory failed, attempting to continue anyway");
     }
   }
   else {
@@ -685,9 +713,26 @@
     dbg("channel: point of no return for new $UPDDir");
   }
 
-  # Write out the mirby file
+  # extract the files again for the last time
+  if (!taint_safe_archive_extract($UPDDir, $content_file)) {
+    channel_failed("channel: archive extraction failed");
+
+    # bug 4941: try to get rid of the empty directories to avoid leaving SA
+    # with no rules.
+    if (!clean_update_dir($UPDDir)) {
+      warn "channel: attempt to clean up failed extraction also failed!\n";
+    }
+    else {
+      rmdir $UPDDir;
+      rmdir $opt{'updatedir'};
+    }
+
+    next;
+  }
+
+  # Write out the mirby file, not fatal if it doesn't work
   dbg("channel: creating MIRRORED.BY file");
-  if (open(MBY, ">$UPDDir/MIRRORED.BY")) {
+  if (open(MBY, ">$mirby_path")) {
     print MBY $mirby;
     close(MBY);
   }
@@ -695,17 +740,12 @@
     warn "error: can't write new MIRRORED.BY file: $!\n";
   }
 
-  # extract the files again
-  if (!taint_safe_archive_extract($UPDDir, $content_file)) {
-    channel_failed("channel: archive extraction failed");
-    next;
-  }
-
-  # OK, lint passed. now create the update config file
+  # the last step is to create the .cf and .pre files to include the
+  # channel files
   my @CF = ();
   my @PRE = ();
 
-  dbg("channel: creating update config file");
+  dbg("channel: creating update cf/pre files");
 
   # Put in whatever metadata we need
   push(@CF, "# UPDATE version $newV\n");
@@ -715,9 +755,10 @@
     die "fatal: can't access $UPDDir: $!\n";
   }
   while(my $file = readdir(DIR)) {
-    $file =~ /^([^\/]+)$/;       # untaint
+    $file =~ /^(.+)$/;       # untaint
     $file = $1;
-    next unless (-f "$UPDDir/$file");   # shouldn't ever happen
+    my $path = File::Spec->catfile($UPDDir, $file);
+    next unless (-f $path);   # shouldn't ever happen
 
     if ($file =~ /\.cf$/) {
       push(@CF, "include $nicechannel/$file\n");
@@ -748,24 +789,36 @@
   dbg("channel: update complete");
 }
 
+##############################################################################
+
 # clean out the temp dir
-if (!clean_update_dir($UPDTmp)) {
-  warn "error: unable to clean out the files in $UPDTmp\n";
+if ($UPDTmp) {
+  dbg("generic: cleaning up temporary directory/files");
+  if (!clean_update_dir($UPDTmp)) {
+    warn "error: unable to clean out the files in $UPDTmp\n";
+  }
 }
 
 # clear out the temp files if they still exist
 foreach ( $content_file, $UPDTmp ) {
-  if (-d $_) {
+  next unless (defined $_ && -e $_);
+
+  if (-d _) {
     rmdir $_ || warn "error: can't remove directory $_: $!\n";
   }
-  elsif (-e _) {
+  elsif (-f _) {
     unlink $_ || warn "error: can't remove file $_: $!\n";
   }
+  else {
+    warn "error: '$_' isn't a file nor a directory, skipping\n";
+  }
 }
 
 dbg("diag: updates complete, exiting with code $exit");
 exit $exit;
 
+##############################################################################
+
 sub write_channel_file {
   my ($filename, $contents) = @_;
 
@@ -780,6 +833,8 @@
   return 0;
 }
 
+##############################################################################
+
 sub channel_failed {
   my $reason = shift;
   warn("$reason, channel failed\n");
@@ -790,6 +845,8 @@
   }
 }
 
+##############################################################################
+
 sub taint_safe_archive_extract {
   my $todir = shift;
   my $input = shift;
@@ -811,7 +868,7 @@
     $file =~ /^([-\.\,\/a-zA-Z0-9_]+)$/;
     my $outfname = $1;
     $outfname =~ s/\.\.\//__\//gs;      # avoid "../" dir traversal attacks
-    $outfname = "$todir/$outfname";
+    $outfname = File::Spec->catfile($todir, $outfname);
 
     dbg "extracting: $outfname";
     if (open OUT, ">".$outfname) {
@@ -839,6 +896,8 @@
   return;       # undef = failure
 }
 
+##############################################################################
+
 # Do a generic TXT query
 sub do_txt_query {
   my($query) = shift;
@@ -865,6 +924,8 @@
   return $result;
 }
 
+##############################################################################
+
 # Do a GET request via HTTP for a certain URL
 # Use the optional time_t value to do an IMS GET
 sub http_get {
@@ -915,6 +976,8 @@
   return;
 }
 
+##############################################################################
+
 # choose a random integer between 0 and the total weight of all mirrors
 # loop through the mirrors from largest to smallest weight
 # if random number is < largest weight, use it
@@ -952,11 +1015,15 @@
   return $mirrors[0];
 }
 
+##############################################################################
+
 sub print_version {
   print "sa-update version $VERSION\n"
       . "  running on Perl version " . join(".", map { $_||=0; $_*1 } ($] =~ /(\d)\.(\d{3})(\d{3})?/ )) . "\n";
 }
 
+##############################################################################
+
 sub print_usage_and_exit {
   my ( $message, $exitval ) = @_;
   $exitval ||= 64;
@@ -972,24 +1039,15 @@
   );
 }
 
+##############################################################################
+
 sub usage {
   my ( $verbose, $message ) = @_;
   print "sa-update version $VERSION\n";
   pod2usage( -verbose => $verbose, -message => $message, -exitval => 64 );
 }
 
-sub find_gpg_path {
-  # bug 4958: for *NIX it's "gpg", in Windows it's "gpg.exe"
-  my $gpg = 'gpg' . $Config{_exe};
-
-  dbg("gpg: Searching for '$gpg'");
-
-  my $path = Mail::SpamAssassin::Util::find_executable_in_env_path($gpg) ||
-    die "fatal: couldn't find GPG\n";
-
-  dbg("gpg: found $path");
-  return $path;
-}
+##############################################################################
 
 sub interpolate_gpghomedir {
   my $gpghome = '';
@@ -1007,10 +1065,11 @@
   return $gpghome;
 }
 
+##############################################################################
+
 sub import_gpg_key {
   my $keyfile = shift;
 
-  $GPGPath = find_gpg_path();
   my $gpghome = interpolate_gpghomedir();
 
   my $CMD = "$GPGPath $gpghome --batch ".
@@ -1030,7 +1089,7 @@
     }
 
     if ($GNUPG =~ /^IMPORTED /) {
-      print "sa-update --import: success. $GNUPG\n";
+      dbg("gpg: gpg key imported successfully");
     }
   }
 
@@ -1038,46 +1097,60 @@
   return ($? >> 8);
 }
 
+##############################################################################
+
 sub import_default_keyring {
   my $defkey = File::Spec->catfile ($DEF_RULES_DIR, "sa-update-pubkey.txt");
-  return unless (-f $defkey);
+  unless (-f $defkey) {
+    dbg("gpg: import of default keyring failed, couldn't find sa-update-pubkey.txt");
+    return;
+  }
 
-  print "sa-update: importing default keyring to '".$opt{gpghomedir}."'...\n";
+  dbg("gpg: importing default keyring to '".$opt{gpghomedir});
   unless (-d $opt{gpghomedir}) {
     # use 0700 to avoid "unsafe permissions" warning
-    mkdir ($opt{gpghomedir}, 0700) or die "cannot mkdir $opt{gpghomedir}: $!";
+    mkpath([$opt{'gpghomedir'}], 0, 0700) or die "cannot mkpath $opt{gpghomedir}: $!";
   } 
   import_gpg_key($defkey);
 }
 
+##############################################################################
+
 sub is_valid_gpg_key_id {
   # either a keyid (8 bytes) or a fingerprint (40 bytes)
   return ($_[0] =~ /^[a-fA-F0-9]+$/ && (length $_[0] == 8 || length $_[0] == 40));
 }
 
+##############################################################################
+
 sub clean_update_dir {
   my $dir = shift;
 
   unless (opendir(DIR, $dir)) {
     warn "error: can't readdir $dir: $!\n";
-    dbg("channel: attempt to readdir failed, channel failed");
-    return 0;
+    dbg("generic: attempt to readdir ($dir) failed");
+    return;
   }
   while(my $file = readdir(DIR)) {
-    next unless (-f "$dir/$file");
-    dbg("channel: unlinking $file");
-    $file =~ /^([^\/]+)$/;       # untaint
+    $file =~ /^(.+)$/;       # untaint
     $file = $1;
-    if (!unlink "$dir/$file") {
-      warn "error: can't remove file $dir/$file: $!\n";
+
+    my $path = File::Spec->catfile($dir, $file);
+    next unless (-f $path);
+
+    dbg("generic: unlinking $file");
+    if (!unlink $path) {
+      warn "error: can't remove file $path: $!\n";
       closedir(DIR);
-      return 0;
+      return;
     }
   }
   closedir(DIR);
   return 1;
 }
 
+##############################################################################
+
 sub lint_check_dir {
   my $dir = shift;
 
@@ -1086,8 +1159,8 @@
   # "config" or otherwise be more terse. :(
   my $spamtest = new Mail::SpamAssassin( {
     rules_filename      => $dir,
-    site_rules_filename => "$dir/doesnotexist",
-    userprefs_filename  => "$dir/doesnotexist",
+    site_rules_filename => File::Spec->catfile($dir, "doesnotexist"),
+    userprefs_filename  => File::Spec->catfile($dir, "doesnotexist"),
 
     local_tests_only    => 1,
     dont_copy_prefs     => 1,
@@ -1108,7 +1181,9 @@
   return $res == 0;
 }
 
-# ---------------------------------------------------------------------------
+##############################################################################
+
+=cut
 
 =head1 NAME
 
@@ -1263,6 +1338,7 @@
 Mail::SpamAssassin::Conf(3)
 spamassassin(1)
 spamd(1)
+<http://wiki.apache.org/spamassassin/RuleUpdates>
 
 =head1 PREREQUESITES