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 |
|