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