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 2005/02/14 22:48:31 UTC

svn commit: r153855 - in spamassassin/trunk: MANIFEST Makefile.PL sa-update.raw

Author: felicity
Date: Mon Feb 14 13:48:28 2005
New Revision: 153855

URL: http://svn.apache.org/viewcvs?view=rev&rev=153855
Log:
add in sa-update for automated rule updates, etc.

Added:
    spamassassin/trunk/sa-update.raw   (with props)
Modified:
    spamassassin/trunk/MANIFEST
    spamassassin/trunk/Makefile.PL

Modified: spamassassin/trunk/MANIFEST
URL: http://svn.apache.org/viewcvs/spamassassin/trunk/MANIFEST?view=diff&r1=153854&r2=153855
==============================================================================
--- spamassassin/trunk/MANIFEST (original)
+++ spamassassin/trunk/MANIFEST Mon Feb 14 13:48:28 2005
@@ -184,6 +184,7 @@
 rules/user_prefs.template
 sa-filter.raw
 sa-learn.raw
+sa-update.raw
 sample-nonspam.txt
 sample-spam.txt
 spamassassin

Modified: spamassassin/trunk/Makefile.PL
URL: http://svn.apache.org/viewcvs/spamassassin/trunk/Makefile.PL?view=diff&r1=153854&r2=153855
==============================================================================
--- spamassassin/trunk/Makefile.PL (original)
+++ spamassassin/trunk/Makefile.PL Mon Feb 14 13:48:28 2005
@@ -165,6 +165,7 @@
     'EXE_FILES' => {
       'sa-filter.raw'    => 'sa-filter',
       'sa-learn.raw'     => 'sa-learn',
+      'sa-update.raw'    => 'sa-update',
       'spamc/spamc.c'    => 'spamc/spamc$(EXE_EXT)',
       'spamd/spamd.raw'  => 'spamd/spamd',
     },
@@ -173,6 +174,7 @@
         'spamassassin'     => '$(INST_MAN1DIR)/spamassassin.$(MAN1EXT)',
         'sa-filter'        => '$(INST_MAN1DIR)/sa-filter.$(MAN1EXT)',
         'sa-learn'         => '$(INST_MAN1DIR)/sa-learn.$(MAN1EXT)',
+        'sa-update'        => '$(INST_MAN1DIR)/sa-update.$(MAN1EXT)',
         'spamc/spamc.pod'  => '$(INST_MAN1DIR)/spamc.$(MAN1EXT)',
         'spamd/spamd'      => '$(INST_MAN1DIR)/spamd.$(MAN1EXT)',
     },
@@ -211,7 +213,7 @@
     },
 
     'clean' => { FILES => join(' ' =>
-        'sa-filter', 'sa-learn',
+        'sa-filter', 'sa-learn', 'sa-update',
         
         'spamd/spamd',
 
@@ -1092,6 +1094,9 @@
 	$(PREPROCESS) $(FIXBYTES) $(FIXVARS) $(FIXBANG) -m$(PERM_RWX) -i$? -o$@
 
 sa-learn: sa-learn.raw
+	$(PREPROCESS) $(FIXBYTES) $(FIXVARS) $(FIXBANG) -m$(PERM_RWX) -i$? -o$@
+
+sa-update: sa-update.raw
 	$(PREPROCESS) $(FIXBYTES) $(FIXVARS) $(FIXBANG) -m$(PERM_RWX) -i$? -o$@
 
 spamd/spamd: spamd/spamd.raw

Added: spamassassin/trunk/sa-update.raw
URL: http://svn.apache.org/viewcvs/spamassassin/trunk/sa-update.raw?view=auto&rev=153855
==============================================================================
--- spamassassin/trunk/sa-update.raw (added)
+++ spamassassin/trunk/sa-update.raw Mon Feb 14 13:48:28 2005
@@ -0,0 +1,811 @@
+#!/usr/bin/perl -w
+
+# <@LICENSE>
+# Copyright 2004 Apache Software Foundation
+# 
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# </...@LICENSE>
+
+my $VERSION = 'svn' . (split(/\s+/,
+	'$Id$'))[2];
+
+my $PREFIX          = '@@PREFIX@@';             # substituted at 'make' time
+my $DEF_RULES_DIR   = '@@DEF_RULES_DIR@@';      # substituted at 'make' time
+my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@';    # substituted at 'make' time
+use lib '@@INSTALLSITELIB@@';                   # substituted at 'make' time
+
+# These are the non-standard required modules
+use Net::DNS;
+use LWP::UserAgent;
+use HTTP::Date qw(time2str);
+use Archive::Tar;
+use IO::Zlib;
+
+# These should already be available
+use Mail::SpamAssassin;
+use Digest::SHA1 qw/sha1_hex/;
+use File::Spec;
+use Getopt::Long;
+use Pod::Usage;
+use strict;
+use warnings;
+
+# Make the main dbg() accessible in our package w/o an extra function
+*dbg=\&Mail::SpamAssassin::dbg;
+
+# 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 SpamAssassin Signing Key <re...@spamassassin.org>
+#      Key fingerprint = 26C9 00A4 6DD4 0CD5 AD24  F6D7 DEE0 1987 265F A05B
+# sub  1024D/FC51569B 2003-08-21
+#
+my %valid_GPG = ( '265FA05B' => 1 );
+
+# Default list of channels to update against
+#
+my @channels = ( 'updates.spamassassin.org' );
+
+
+my %opt = ();
+@{$opt{'gpgkey'}} = ();
+@{$opt{'channel'}} = ();
+my $site_rules_path;
+my $GPG_ENABLED;
+
+Getopt::Long::Configure(
+  qw(bundling no_getopt_compat no_auto_abbrev no_ignore_case));
+GetOptions(
+  'debug|D:s'                           => \$opt{'debug'},
+  'version|V'                           => \$opt{'version'},
+  'help|h|?'                            => \$opt{'help'},
+
+  # allow multiple of these on the commandline
+  'gpgkey=s'				=> $opt{'gpgkey'},
+  'channel=s'				=> $opt{'channel'},
+
+  'gpgkeyfile=s'			=> \$opt{'gpgkeyfile'},
+  'channelfile=s'			=> \$opt{'channelfile'},
+  'updatedir=s'				=> \$site_rules_path,
+  'usegpg'				=> \$GPG_ENABLED,
+) or print_usage_and_exit();
+
+if ( defined $opt{'help'} ) {               
+  print_usage_and_exit("For more information read the sa-update man page.\n", 0);
+} 
+if ( defined $opt{'version'} ) {            
+  print_version();
+  exit(0);
+} 
+
+# Figure out what version of SpamAssassin we're using, and also figure out the
+# reverse of it for the DNS query.  Handle x.yyyzzz as well as x.yz.
+my $SAVersion = $Mail::SpamAssassin::VERSION;
+if ($SAVersion =~ /^(\d+)\.(\d{3})(\d{3})$/) {
+  $SAVersion = join(".", $1+0, $2+0, $3+0);
+}
+elsif ($SAVersion =~ /^(\d)\.(\d)(\d)$/) {
+  $SAVersion = "$1.$2.$3";
+}
+else {
+  die "fatal: SpamAssassin version number '$SAVersion' is in an unknown format!\n";
+}
+my $RevSAVersion = join(".", reverse split(/\./, $SAVersion));
+
+  
+# set debug areas, if any specified (only useful for command-line tools)
+$SAVersion =~ /^(\d+\.\d+)/;
+if ($1+0 > 3.0) {
+  $opt{'debug'} ||= 'all' if (defined $opt{'debug'});
+}
+else {
+  $opt{'debug'} = defined $opt{'debug'};
+}
+
+
+# Find the default site rule directory, also setup debugging and other M::SA bits
+my $SA = new Mail::SpamAssassin({
+  debug => $opt{'debug'},
+  local_tests_only => 1,
+  dont_copy_prefs => 1,
+
+  PREFIX          => $PREFIX,
+  DEF_RULES_DIR   => $DEF_RULES_DIR,
+  LOCAL_RULES_DIR => $LOCAL_RULES_DIR
+});
+$site_rules_path ||= $SA->first_existing_path(@Mail::SpamAssassin::site_rules_path);
+
+dbg("sa-update version $VERSION");
+dbg("using update directory: $site_rules_path");
+
+# doesn't really display useful things for this script,
+# but we do want a module/version listing, etc.
+#$SA->debug_diagnostics();
+
+$SA->finish();
+
+# deal with gpg-related options
+if (defined $opt{'gpgkey'}) {
+  $GPG_ENABLED = 1;
+  foreach my $key (@{$opt{'gpgkey'}}) {
+    unless ($key =~ /^[a-fA-F0-9]{8}$/) {
+      dbg("gpg: invalid gpgkey parameter $key");
+      next;
+    }
+    $key = uc $key;
+    dbg("gpg: adding key id $key");
+    $valid_GPG{$key} = 1;
+  }
+}
+if (defined $opt{'gpgkeyfile'}) {
+  $GPG_ENABLED = 1;
+  unless (open(GPG, $opt{'gpgkeyfile'})) {
+    die "Can't open ".$opt{'gpgkeyfile'}." for reading: $!\n";
+  }
+
+  dbg("gpg: reading in gpgfile ".$opt{'gpgkeyfile'});
+  while(my $key = <GPG>) {
+    unless ($key =~ /^[a-fA-F0-9]{8}$/) {
+      dbg("gpg: invalid key id $key");
+      next;
+    }
+    $key = uc $key;
+    dbg("gpg: adding key id $key");
+    $valid_GPG{$key} = 1;
+  }
+  close(GPG);
+}
+
+# Deal with channel-related options
+if (defined $opt{'channel'}) {
+  push(@channels, @{$opt{'channel'}});
+}
+if (defined $opt{'channelfile'}) {
+  unless (open(CHAN, $opt{'channelfile'})) {
+    die "Can't open ".$opt{'channelfile'}." for reading: $!\n";
+  }
+
+  dbg("channel reading in channelfile ".$opt{'channelfile'});
+  while(my $chan = <CHAN>) {
+    $chan = lc $chan;
+    dbg("channel: adding $chan");
+    push(@channels, $chan);
+  }
+  close(CHAN);
+}
+
+# find GPG in the PATH
+my $GPGPath;
+if ($GPG_ENABLED) {
+  dbg("gpg: Searching for 'gpg' in ".$ENV{'PATH'});
+  foreach my $dir (split(/:/, $ENV{'PATH'})) {
+    $dir = File::Spec->catfile($dir, 'gpg');
+    if (-x $dir) {
+      $GPGPath = $dir;
+      last;
+    }
+  }
+  die "fatal: couldn't find GPG in \$PATH\n" unless ($GPGPath);
+  dbg("gpg: found $GPGPath");
+  dbg("gpg: release trusted key id list: ".join(" ", keys %valid_GPG));
+}
+
+
+my $res = Net::DNS::Resolver->new();
+
+my $ua = LWP::UserAgent->new;
+$ua->agent("sa-update/$VERSION");
+$ua->timeout(10);
+$ua->env_proxy;
+
+# Generate a temporary file to put channel content in for later use ...
+my ($content_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
+close($tfh);
+
+# Go ahead and loop through all of the channels
+my $exit = 0;
+foreach my $channel (@channels) {
+  dbg("channel: attempting channel $channel");
+
+  # Convert the channel to a nice-for-filesystem version
+  my $nicechannel = $channel;
+  $nicechannel =~ tr/A-Za-z0-9-/_/cs;
+
+  my $UPDDir = "$site_rules_path/$nicechannel";
+  my $CFFile = "$UPDDir.cf";
+
+  dbg("channel: update directory $UPDDir");
+  dbg("channel: channel cf file $CFFile");
+
+  # try to read metadata from channel.cf file
+  my $currentV = -1;
+  if (open(CF, $CFFile)) {
+    while(<CF>) {
+      last unless /^# UPDATE\s+([A-Za-z]+)\s+(\S+)/;
+      my($type, $value) = (lc $1,$2);
+
+      dbg("channel: metadata $type = $value");
+
+      if ($type eq 'version') {
+        $value =~ /^(\d+)/;
+        $currentV = $1;
+      }
+    }
+    close(CF);
+  }
+
+  # Setup the channel version DNS query
+  my $DNSQ = "$RevSAVersion.$channel";
+
+  my $newV;
+  my $dnsV = do_txt_query($DNSQ);
+  if (defined $dnsV && $dnsV =~ /^(\d+)/) {
+    $newV = $1 if (!defined $newV || $1 > $newV);
+    dbg("dns: $DNSQ => $dnsV, parsed as $1");
+  }
+
+  # Not getting a response isn't a failure, there may just not be any updates
+  # for this SA version yet.
+  unless (defined $newV) {
+    dbg("channel: no updates available, skipping channel");
+    next;
+  }
+
+  # If this channel hasn't been installed before, or it's out of date,
+  # keep going.  Otherwise, skip it.
+  if ($currentV >= $newV) {
+    dbg("channel: current version is $currentV, new version is $newV, skipping channel");
+    next;
+  }
+
+  # We don't currently have the list of mirrors, so go grab it.
+  unless (-f "$UPDDir/MIRRORED.BY") {
+    dbg("channel: no MIRRORED.BY file available");
+    my $mirror = do_txt_query("mirrors.$channel");
+    unless ($mirror) {
+      warn "error: no mirror data available for channel $channel\n";
+      dbg("channel: MIRRORED.BY file location was not in DNS, channel failed");
+      $exit++;
+      next;
+    }
+    $mirror = http_get($mirror);
+    unless ($mirror) {
+      warn "error: no mirror data available for channel $channel\n";
+      dbg("channel: MIRRORED.BY contents were missing, channel failed");
+      $exit++;
+      next;
+    }
+
+    unless (-d $UPDDir) {
+      dbg("channel: creating $UPDDir");
+      mkdir $UPDDir || die "fatal: can't create $UPDDir: $!\n";
+    }
+
+    unless (open(MIR, ">$UPDDir/MIRRORED.BY")) {
+      warn "error: can't create mirrors file: $!\n";
+      dbg("channel: MIRRORED.BY creation failure, channel failed");
+      $exit++;
+      next;
+    }
+    print MIR $mirror;
+    close(MIR);
+    dbg("channel: MIRRORED.BY file retrieved");
+  }
+
+  # Read in the list of mirrors
+  unless (open(MIR, "$UPDDir/MIRRORED.BY")) {
+    warn "error: can't read mirrors file: $!\n";
+    dbg("channel: MIRRORED.BY file is unreadable, channel failed");
+    $exit++;
+    next;
+  }
+
+  dbg("channel: reading MIRRORED.BY file");
+  my %mirrors = ();
+  while(my $mirror = <MIR>) {
+    # We only support HTTP right now
+    if ($mirror !~ m@^http://@i) {
+      dbg("channel: skipping non-HTTP mirror: $mirror");
+      next;
+    }
+
+    chomp $mirror;
+    my @data;
+
+    dbg("channel: found mirror $mirror");
+
+    ($mirror,@data) = split(/\s+/, $mirror);
+    $mirror =~ s@/+$@@; # http://example.com/updates/ -> .../updates
+    $mirrors{$mirror}->{weight} = 1;
+    foreach (@data) {
+      my($k,$v) = split(/=/, $_, 2);
+      $mirrors{$mirror}->{$k} = $v;
+    }
+  }
+  close(MIR);
+
+  unless (keys %mirrors) {
+    warn "error: no mirrors available for channel $channel\n";
+    dbg("channel: no mirrors available, channel failed");
+    $exit++;
+    next;
+  }
+
+  # remember the mtime of the file so we can IMS GET later on
+  my $mirby_time = (stat("$UPDDir/MIRRORED.BY"))[9];
+
+
+  # Now that we've laid the foundation, go grab the appropriate files
+  #
+  my $content;
+  my $SHA1;
+  my $GPG;
+  my $mirby;
+
+  # Loop through all available mirrors, choose from them randomly
+  # if the archive get fails, choose another mirror,
+  # if the get for the sha1 or gpg signature files, the channel fails
+  while (my $mirror = choose_mirror(\%mirrors)) {
+    # Grab the data hash for this mirror, then remove it from the list
+    my $mirror_info = $mirrors{$mirror};
+    delete $mirrors{$mirror};
+
+    dbg("channel: selected mirror $mirror");
+
+    # Actual archive file
+    $content = http_get("$mirror/$newV.tar.gz");
+    next unless $content;
+
+    # SHA1 of the archive file
+    $SHA1 = http_get("$mirror/$newV.tar.gz.sha1");
+    last unless $SHA1;
+
+    # if GPG is enabled, the GPG detached signature of the archive file
+    if ($GPG_ENABLED) {
+      $GPG = http_get("$mirror/$newV.tar.gz.asc");
+      last unless $GPG;
+    }
+
+    # try to update our list of mirrors.
+    # a failure here doesn't cause channel failure.
+    $mirby = http_get("$mirror/MIRRORED.BY", $mirby_time);
+
+    last;
+  }
+
+  unless ($content && $SHA1 && (!$GPG_ENABLED || $GPG)) {
+    warn "error: channel $channel has no working mirrors\n";
+    dbg("channel: could not find working mirror, channel failed");
+    $exit++;
+    next;
+  }
+
+  # Validate the SHA1 signature before going forward with more complicated
+  # operations.
+  # The SHA1 file may be "signature filename" ala sha1sum, just use the signature
+  $SHA1 =~ /^([a-fA-F0-9]{40})/;
+  $SHA1 = $1 || 'INVALID';
+  my $digest = sha1_hex($content);
+  dbg("sha1: verification expected: $SHA1");
+  dbg("sha1: verification got     : $digest");
+  unless ($digest eq $SHA1) {
+    warn "error: can't verify SHA1 signature\n";
+    dbg("channel: SHA1 verification failed, channel failed");
+    $exit++;
+    next;
+  }
+
+  # Write the content out to a temp file for GPG/Archive::Tar interaction
+  dbg("channel: populating temp content file");
+  open(TMP, ">$content_file") || die "fatal: can't write to content temp file $content_file: $!\n";
+  binmode TMP;
+  print TMP $content;
+  close(TMP);
+
+  # to sign  : gpg -bas file
+  # to verify: gpg --verify --batch --no-tty --status-fd=1 -q --logger-fd=1 file.asc file
+  # look for : /^\[GNUPG:\] GOODSIG \S+(\S{8})
+  if ($GPG) {
+    dbg("gpg: populating temp signature file");
+    my $sig_file;
+    ($sig_file, $tfh) = Mail::SpamAssassin::Util::secure_tmpfile();
+    binmode $tfh;
+    print $tfh $GPG;
+    close($tfh);
+
+    dbg("gpg: calling gpg");
+    my $CMD = "$GPGPath --verify --batch --no-tty --status-fd=1 -q --logger-fd=1";
+    unless (open(CMD, "$CMD $sig_file $content_file|")) {
+      unlink $sig_file || warn "error: can't unlink $sig_file: $!\n";
+      die "fatal: couldn't execute $GPGPath: $!\n";
+    }
+
+    # Determine the fate of the signature
+    my $signer = '';
+    while(my $GNUPG = <CMD>) {
+      next unless ($GNUPG =~ /^\Q[GNUPG:] GOODSIG\E \S+(\S{8})/);
+      $signer = $1;
+    }
+
+    close(CMD);
+    unlink $sig_file || warn "Can't unlink $sig_file: $!\n";
+
+    if ($signer) {
+      dbg("gpg: good signature made by key id $signer");
+      if (exists $valid_GPG{$signer}) {
+	dbg("gpg: key id $signer is release trusted");
+      }
+      else {
+	dbg("gpg: key id $signer is not release trusted");
+	$signer = undef;
+      }
+    }
+
+    unless ($signer) {
+      warn "error: GPG validation failed\n";
+      dbg("channel: GPG verification failed, channel failed");
+      $exit++;
+      next;
+    }
+  }
+
+  # OK, we're all validated at this point, install the new version
+  dbg("channel: file verification passed, installing update");
+
+  if ($mirby) {
+    dbg("channel: updating MIRRORED.BY contents");
+    if (open(MBY, ">$UPDDir/MIRRORED.BY")) {
+      print MBY $mirby;
+      close(MBY);
+    }
+    else {
+      warn "error: can't write new MIRRORED.BY file: $!\n";
+    }
+  }
+
+  dbg("channel: cleaning out update directory");
+  unless (opendir(DIR, $UPDDir)) {
+    warn "error: can't readdir $UPDDir: $!\n";
+    dbg("channel: attempt to readdir failed, channel failed");
+    $exit++;
+    next;
+  }
+  while(my $file = readdir(DIR)) {
+    next unless (-f "$UPDDir/$file");
+    next if ($file eq 'MIRRORED.BY');
+    dbg("channel: unlinking $file");
+    unlink "$UPDDir/$file" || warn "error: can't remove file $UPDDir/$file: $!\n";
+  }
+  closedir(DIR);
+  unlink "$UPDDir.cf" || warn "error: can't remove file $UPDDir.cf: $!\n";
+
+  $tfh = IO::Zlib->new($content_file, "rb");
+  die "fatal: couldn't read content tmpfile $content_file: $!\n" unless $tfh;
+
+  my $tar = Archive::Tar->new($tfh);
+  die "fatal: couldn't create Archive::Tar object!\n" unless $tar;
+
+  # make sure we're doing the work in the update directory
+  unless (chdir $UPDDir) {
+    warn "error: can't chdir into $UPDDir: $!\n";
+    dbg("channel: chdir failed, channel failed");
+    $exit++;
+    next;
+  }
+
+  dbg("channel: extracting archive");
+  unless ($tar->extract()) {
+    close($tfh);
+    warn "error: couldn't extract the tar archive!\n";
+    dbg("channel: archive extraction failed, channel failed");
+    $exit++;
+    next;
+  }
+  close($tfh);
+
+
+  dbg("channel: creating update config file");
+  unless (open(CF, ">$UPDDir.cf")) {
+    die "fatal: can't create new channel cf $UPDDir.cf: $!\n";
+  }
+
+  # Put in whatever metadata we need
+  print CF "# UPDATE version $newV\n";
+
+  # now include *.cf
+  unless (opendir(DIR, $UPDDir)) {
+    die "fatal: can't access $UPDDir: $!\n";
+  }
+  while(my $file = readdir(DIR)) {
+    next unless (-f "$UPDDir/$file");
+    next unless ($file =~ /\.cf$/);
+    dbg("channel: adding $file");
+    print CF "include $UPDDir/$file\n";
+  }
+  closedir(DIR);
+  close(CF);
+
+  dbg("channel: update complete");
+}
+
+unlink $content_file || warn "error: couldn't remove tmpfile $content_file: $!\n";
+
+dbg("diag: updates complete, exiting with code $exit");
+exit $exit;
+
+# Do a generic TXT query
+sub do_txt_query {
+  my($query) = shift;
+
+  my $RR = $res->query($query, 'TXT');
+  my $result = '';
+
+  if ($RR) {
+    foreach my $rr ($RR->answer) {
+      my $text = $rr->rdatastr;
+      $text =~ /^"(.*)"$/;
+      if (length $result) {
+	$result .= " $1";
+      }
+      else {
+        $result = $1;
+      }
+    }
+  }
+  else {
+    dbg("dns: query failed: $query => " . $res->errorstring);
+  }
+
+  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 {
+  my($url, $ims) = @_;
+
+  my $request = HTTP::Request->new("GET");
+  $request->url($url);
+
+  if (defined $ims) {
+    my $str = time2str($ims);
+    $request->header('If-Modified-Since', $str);
+    dbg("http: IMS GET request, $url, $str");
+  }
+  else {
+    dbg("http: GET request, $url");
+  }
+
+  my $response = $ua->request($request);
+
+  if ($response->is_success) {
+    return $response->content;
+  }
+
+  dbg("http: request failed: " . $response->status_line);
+  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
+# otherwise, random number -= largest, remove mirror from list, try again
+# eventually, there'll just be 1 mirror left in $mirrors[0] and it'll be used
+# 
+sub choose_mirror {
+  my($mirror_list) = @_;
+
+  # Sort the mirror list by reverse weight (largest first)
+  my @mirrors = sort { $mirror_list->{$b}->{weight} <=> $mirror_list->{$a}->{weight} } keys %{$mirror_list};
+
+  return unless @mirrors;
+
+  if (keys %{$mirror_list} > 1) {
+    # Figure out the total weight
+    my $weight_total = 0;
+    foreach (@mirrors) {
+      $weight_total += $mirror_list->{$_}->{weight};
+    }
+
+    # Pick a random int
+    my $value = int(rand($weight_total));
+
+    # loop until we find the right mirror, or there's only 1 left
+    while (@mirrors > 1) {
+      if ($value < $mirror_list->{$mirrors[0]}->{weight}) {
+        last;
+      }
+      $value -= $mirror_list->{$mirrors[0]}->{weight};
+      shift @mirrors;
+    }
+  }
+
+  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;
+  
+  if ($exitval == 0) {
+    print_version();
+    print("\n");
+  }
+  pod2usage(
+    -verbose => 0,
+    -message => $message,
+    -exitval => $exitval,
+  );
+}
+
+sub usage {
+  my ( $verbose, $message ) = @_;
+  print "sa-update version $VERSION\n";
+  pod2usage( -verbose => $verbose, -message => $message, -exitval => 64 );
+}
+
+# ---------------------------------------------------------------------------
+
+=head1 NAME
+
+sa-update - automate SpamAssassin rule updates
+
+=head1 SYNOPSIS
+
+B<sa-update> [options]
+
+Options:
+
+  --updatedir path		Directory to place updates, defaults to the
+				SpamAssassin site rules directory (def:
+				/etc/mail/spamassassin)
+
+  --channel channel		Retrieve updates from this channel
+  				Use multiple times for multiple channels
+  --channelfile file		Retrieve updates from the channels in the file
+
+
+  --gpgkey key			Trust the key id to sign releases
+  				Use multiple times for multiple keys
+  --gpgkeyfile file		Trust the key ids in the file to sign releases
+  --usegpg			Use GPG to verify updates
+  				This is auto-enabled by use of the above
+				gpgkey and gpgkeyfile options.
+
+  -D, --debug [area=n,...]	Print debugging messages
+  -V, --version			Print version
+  -h, --help			Print usage message
+
+=head1 DESCRIPTION
+
+sa-update automates the process of downloading and installing new
+rules and configuration, based on channels.  The default channel
+is I<updates.spamassassin.org>, which has updated rules since the
+previous release.
+
+Update archives are verified by SHA1 hashes, and optionally GPG.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<updatedir>
+
+Typically sa-update will use whatever the default site rules directory
+SpamAssassin uses.  (usually /etc/mail/spamassassin)  If the updates should be
+stored in another location, specify it here.
+
+=item B<channel>
+
+sa-update can update multiple channels at the same time.  By default, it will
+only access "updates.spamassassin.org", but more channels can be specified via
+this option.  If there are multiple additional channels, use the option
+multiple times, once per channel.  i.e.:
+
+	sa-update --channel foo.example.com --channel bar.example.com
+
+=item B<channelfile>
+
+Similar to the B<channel> option, except specify the additional channels in a
+file instead of on the commandline.  This is extremely useful when there are a
+lot of additional channels.
+
+=item B<usegpg>
+
+sa-update only verifies update archives by use of a SHA1 checksum.  While this
+verifies whether or not the downloaded archive has been corrupted, it does not
+offer any form of security regarding whether or not the downloaded archive is
+legitimate (aka: non-modifed by evildoers).  Use this option to enable GPG
+verification of the archive to solve the problem.
+
+Note: Use of the following gpgkey-related options will automatically enable
+GPG verification.
+
+Note: Currently, only GPG itself is supported (ie: not PGP).  v1.2 has been
+tested, although later versions ought to work as well.
+
+=item B<gpgkey>
+
+sa-update has the concept of "release trusted" GPG keys.  When an archive is
+downloaded and the signature verified, sa-update requires that the signature
+be from one of these "release trusted" keys or else verification fails.  This
+prevents third parties from manipulating the files on a mirror, for instance,
+and signing with their own key.
+
+By default, sa-update trusts key id 265FA05B, which is the standard
+SpamAssassin release key.  Use this option to add more trusted keys.
+
+For multiple keys, use the option multiple times.  i.e.:
+
+	sa-update --gpgkey E580B363 --gpgkey 298BC7D0
+
+Note: use of this option automatically enables GPG verification.
+
+=item B<gpgkeyfile>
+
+Similar to the B<gpgkey> option, except specify the additional keys in a file
+instead of on the commandline.  This is extremely useful when there are a lot
+of additional keys that you wish to trust.
+
+=item B<-D> [I<area,...>], B<--debug> [I<area,...>]
+
+Produce debugging output.  If no areas are listed, all debugging information is
+printed.  Diagnostic output can also be enabled for each area individually;
+I<area> is the area of the code to instrument. For example, to produce
+diagnostic output on channel, gpg, and http, use:
+
+        sa-update -D channel,gpg,http
+
+=item B<-h>, B<--help>
+
+Print help message and exit.
+
+=item B<-V>, B<--version>
+
+Print sa-update version and exit.
+
+=back
+
+=head1 SEE ALSO
+
+Mail::SpamAssassin(3)
+Mail::SpamAssassin::Conf(3)
+spamassassin(1)
+spamd(1)
+
+=head1 PREREQUESITES
+
+C<Mail::SpamAssassin>
+
+=head1 BUGS
+
+See <http://bugzilla.spamassassin.org/>
+
+=head1 AUTHORS
+
+The SpamAssassin(tm) Project <http://spamassassin.apache.org/>
+
+=head1 COPYRIGHT
+
+SpamAssassin is distributed under the Apache License, Version 2.0, as
+described in the file C<LICENSE> included with the distribution.
+
+=cut
+

Propchange: spamassassin/trunk/sa-update.raw
------------------------------------------------------------------------------
    svn:executable = *

Propchange: spamassassin/trunk/sa-update.raw
------------------------------------------------------------------------------
    svn:keywords = id