#!/usr/bin/perl ######################################################################## # Copyright (c) 2004 Art Sackett # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation; either version 2 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, but # # WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # # General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the Free Software # # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # # USA # ######################################################################## ## Configuration my $inifile = '/etc/generate_dspam_aliases.ini'; my $verbose = 0; # Note that the only fields whose names matter are user, uid, and gid: my @fields = qw(user password uid gid comment homedir shell); my $min_uid = 0; my $max_uid = 65535; ## End configuration BEGIN { use vars qw($VERSION); $VERSION = do{my@r=q$Revision: 0.04 $=~/\d+/g;sprintf '%d.'.'%02d'x$#r,@r}; } #use strict; #use warnings; use Config; use Fcntl qw(:flock); srand; my (%arg, %state); my @known_args = qw(inifile input output); sub check_config { my %required_parameters = ('Alias File' => [qw(alias-spam format program)], 'Password File' => [qw(fields)] ); my $config = config(); foreach my $section(sort keys %required_parameters) { foreach (@{$required_parameters{$section}}) { my $value = $config -> value($section, $_); if (!defined $value || !length($value)) { print STDERR "Configuration does not provide required value $section\::$_\n"; exit 3; } } } } sub config { if (!defined $state{'config'}) { $state{'config'} = new Config(verbose => defined $arg{'verbose'} ? 1 : $verbose); $state{'config'} -> file(defined $arg{'inifile'} ? $arg{'inifile'} : $inifile); } return $state{'config'}; } sub generate_aliases { my $config = config(); my $infile = $config -> value('Password File', 'file'); my $outfile = $config -> value('Alias File', 'file'); my $tempfile = int(rand(100000000)) . '.' . $$; if ($outfile =~ /\//) { my @path_parts = split(/\//, $outfile); pop @path_parts; push(@path_parts, $tempfile); $tempfile = join('/', @path_parts); if ($outfile =~ /^\//) { $tempfile = '/' . $tempfile; } } my $inifile_fields = $config -> value('Password File', 'fields'); if (defined $inifile_fields && length($inifile_fields)) { $inifile_fields =~ s/\s+/ /g; @fields = split(/ /, $inifile_fields); } $state{'position'} = {'user' => undef, 'uid' => undef, 'gid' => undef }; my $i = 0; foreach (@fields) { if (exists($state{'position'} -> {$_})) { if (defined $state{'position'} -> {$_}) { print STDERR "input file field $_ defined more than once.\n"; exit 6; } else { $state{'position'} -> {$_} = $i; } } $i++; } foreach ('user', 'uid', 'gid') { if (!defined $state{'position'} -> {$_}) { print STDERR "input file field $_ not defined in configuration.\n"; exit 6; } } my (%templates, %user_filter); my @params = qw(alias-notspam alias-spam args-falsepositive args-spam format program); foreach (@params) { my $value = $config -> value('Alias File', $_); if (defined $value && length($value)) { if (defined $templates{$_}) { print STDERR "configuration: Alias File::$_ defined twice.\n"; exit 6; } $templates{$_} = $value; } } my $create_spam_alias = defined $templates{'alias-spam'}; my $create_notspam_alias = defined $templates{'alias-notspam'}; if (!$create_spam_alias && !$create_notspam_alias) { print STDERR "configuration provides neither alias-spam nor alias-notspam; nothing to do.\n"; exit 6; } my @users_params = qw(ignore-gid ignore-uid ignore-user maximum-gid maximum-uid minimum-gid minimum-uid); foreach (@users_params) { my $config_string = $config -> value('Users', $_); if (defined $config_string && length($config_string)) { $config_string =~ s/\s+/ /g; my @values = split(/ /, $config_string); if ($_ =~ /^(maxi|mini)mum/) { if (scalar(@values) > 1) { print STDERR "configuration: Users::$_ contains more than one value.\n"; exit 6; } if ($values[0] !~ /^\d+$/ || $values[0] < $min_uid || $values[0] > $max_uid) { print STDERR "configuration: Users::$_ contains illegal value.\n"; exit 6; } $user_filter{$_} = $values[0]; } else { if ($_ =~ /uid$/) { foreach my $val(@values) { if ($val !~ /^\d+$/ || $val < $min_uid || $val > $max_uid) { print STDERR "configuration: Users::$_ contains illegal value.\n"; exit 6; } } } foreach my $val(@values) { $user_filter{$_} -> {$val} = 1; } } } } if (open(INPUT, "<$infile")) { if (open(OUTPUT, ">$tempfile")) { if (flock(OUTPUT, LOCK_EX|LOCK_NB)) { while () { chomp; s/^\s+//; s/\s+$//; if (!length($_) || $_ !~ /:/) { next; } my (%data); my @passwd = split(/:/); foreach ('user', 'uid', 'gid') { $data{$_} = $passwd[$state{'position'} -> {$_}]; if (!length($data{$_})) { print STDERR "bogus input file $infile: does not provide $_ in (at least one of many) records.\n"; close OUTPUT; unlink($tempfile); exit 6; } } if ($user_filter{'ignore-user'} -> {$data{'user'}}) { next; } elsif ($user_filter{'ignore-uid'} -> {$data{'uid'}}) { next; } elsif ($user_filter{'ignore-gid'} -> {$data{'gid'}}) { next; } elsif (defined($user_filter{'minimum-uid'}) && $data{'uid'} < $user_filter{'minimum-uid'}) { next; } elsif (defined $user_filter{'maximum-uid'} && $data{'uid'} > $user_filter{'maximum-uid'}) { next; } elsif (defined($user_filter{'minimum-gid'}) && $data{'gid'} < $user_filter{'minimum-gid'}) { next; } elsif (defined $user_filter{'maximum-gid'} && $data{'gid'} > $user_filter{'maximum-gid'}) { next; } if ($create_spam_alias) { $data{'alias'} = interpolate($templates{'alias-spam'}, \%data); if ($templates{'args-spam'}) { $data{'args'} = interpolate($templates{'args-spam'}, \%data); } else { $data{'args'} = ''; } if ($templates{'program'}) { $data{'program'} = interpolate($templates{'program'}, \%data); } my $line = interpolate($templates{'format'}, \%data); print OUTPUT $line, "\n"; foreach ('alias', 'args', 'program') { delete $data{$_}; } } if ($create_notspam_alias) { $data{'alias'} = interpolate($templates{'alias-notspam'}, \%data); if ($templates{'args-falsepositive'}) { $data{'args'} = interpolate($templates{'args-falsepositive'}, \%data); } else { $data{'args'} = ''; } if ($templates{'program'}) { $data{'program'} = interpolate($templates{'program'}, \%data); } my $line = interpolate($templates{'format'}, \%data); $line =~ s/^\s+//; $line =~ s/\s+$//; print OUTPUT $line, "\n"; foreach ('alias', 'args', 'program') { delete $data{$_}; } } } close OUTPUT; close INPUT; rename($tempfile, $outfile); } else { print STDERR "cannot lock temporary output file.\n"; close OUTPUT; unlink($tempfile); exit 6; } } else { print STDERR "cannot open $outfile for writing: $!\n"; exit 5; } } else { print STDERR "cannot open $infile for reading: $!\n"; exit 4; } } sub interpolate { my $target = shift; if (!defined $target || !length($target) || $target !~ /<|>/) { return $target; } my $source = shift; if (!defined $source || !ref($source) || ref($source) ne 'HASH') { return $target; } foreach (keys %$source) { $target =~ s/<$_>/$source->{$_}/g; } return $target; } sub merge_config_and_argv { my $config = config(); if (defined $arg{'input'}) { if (!length($arg{'input'})) { print STDERR "--input argument is invalid.\n"; exit 4; } $config -> set('Password File', 'file', $arg{'input'}); } if (defined $arg{'output'}) { if (!length($arg{'output'})) { print STDERR "--output argument is invalid.\n"; exit 5; } if ($arg{'output'} =~ /\//) { my @path_parts = split(/\//, $arg{'output'}); pop(@path_parts); my $directory = join('/', @path_parts); if (!-d $directory) { print STDERR "directory $directory does not exist.\n"; exit 5; } $config -> set('Alias File', 'file', $arg{'output'}); } } if (!$config -> basic_file_test($config -> value('Password File', 'file'), 3, 20480)) { exit 4; } my $outfile = $config -> value('Alias File', 'file'); if (-e $outfile) { if (!-w $outfile) { print STDERR "output file $outfile is not writable.\n"; exit 5; } } else { if (!open(OUTPUT, ">$outfile")) { print STDERR "cannot open output file $outfile for writing.\n"; exit 5; } close OUTPUT; unlink($outfile); } } sub parse_argv { if (@ARGV) { push(@known_args, 'verbose'); foreach (@ARGV) { if ($_ =~ /^--(help|usage)/) { usage(); exit 0; } elsif ($_ =~ /^--version/) { my $version = version(); print "This is generate_dspam_aliases version $version.\nCopyright 2004 Art Sackett\nReleased under the terms of the GNU General Public License.\n"; exit 0; } elsif ($_ =~ /^--verbose/) { $arg{'verbose'} = 1; $verbose = 1; } } my %known_arg = (); map {$known_arg{$_} = 1 } @known_args; my $last_name = ''; while (@ARGV) { my $chunk = shift(@ARGV); $chunk =~ s/^\s+|\s+$//g; my ($name, $value); if ($chunk =~ /^\-\-/) { $chunk =~ s/^\-\-//; # contains a name if (length($last_name)) { $arg{$last_name} = 1; $last_name = ''; } if ($chunk =~ /=/) { # also contains value ($name, $value) = split(/=/, $chunk, 2); if (!$known_arg{$name}) { print STDERR "Unknown argument: $name.\n"; exit 1; } $arg{$name} = $value; $last_name = ''; } else { # is just a name if (!$known_arg{$chunk}) { print STDERR "Unknown argument: $name.\n"; exit 1; } $last_name = $chunk; next; } } else { # is a value, or junk if (length($last_name)) { $arg{$last_name} = $chunk; $last_name = ''; } else { print STDERR "Junk argument: $chunk\n"; exit 1; } } } if (length($last_name)) { $arg{$last_name} = 1; } } $state{'argv_parsed'} = 1; } sub usage { my %known_arg = (inifile => 'full path to ini (configuration) file', input => 'full path to passwd file to parse for user names', output => 'full path to output (alias) file', verbose => 'cause generate_dspam_aliases to be more chatty' ); print "\nusage: generate_dspam_aliases [options]\n"; print "options:\n"; print "\t--inifile=/path/to/generate_dspam_aliases.ini\n"; print "\t--input=/path/to/input/file (e.g. /etc/passwd)\n"; print "\t--output=/path/to/output/file (e.g. /etc/mail/aliases/dspam)\n"; print "\t--verbose (useful for troubleshooting configuration problems)\n"; print "\t--version (emits version information, then exits)\n"; print "\nNo options are required if the default behavior specified in the\ninifile are acceptable.\n\n"; exit 0; } sub version { return ($VERSION); } parse_argv(); check_config(); merge_config_and_argv(); generate_aliases(); ### package Config; sub basic_file_test { my $self = shift; my ($file, $min, $max) = @_; if (!defined $file) { if ($self -> verbose()) { print "Config::basic_file_test() requires argument: name of file to test\n"; } return (undef); } $self -> trim($file); if (!length($file)) { if ($self -> verbose()) { print "Config::basic_file_test() requires argument: name of file to test.\n"; } return (undef); } if (!-e $file) { if ($self -> verbose()) { print "$file does not exist.\n"; } return (0); } elsif (!-f $file) { if ($self -> verbose()) { print "$file is not a regular file.\n"; } return (0); } elsif (!-r $file) { if ($self -> verbose()) { print "$file is not readable.\n"; } return (0); } elsif (!-T $file) { if ($self -> verbose()) { print "$file is not plain text.\n"; } return (0); } elsif (-z $file) { if ($self -> verbose()) { print "$file is empty (zero bytes).\n"; } return (0); } else { my $bytes = (-s $file); if (defined $min) { if ($min && $bytes < $min) { if ($self -> verbose()) { print "$file size ($bytes bytes) is less than specified minimum of $min bytes.\n"; } return (0); } } if (defined $max) { if ($max && $bytes > $max) { if ($self -> verbose()) { print "basic_file_test(): $file size ($bytes bytes) exceeds specified maximum of $max bytes.\n"; } return (0); } } return $bytes; } } sub file { my $self = shift; if (@_) { my $file = shift; if ($self -> basic_file_test($file)) { $self -> {'FILE'} = $file; } else { if ($self -> verbose()) { print "Cannot use $file.\n"; } $self -> {'FILE'} = undef; } } return $self -> {'FILE'}; } sub has_section { my $self = shift; my $test = shift; if (!defined $test || !length($test)) { if ($self -> verbose()) { print "Config::has_section(): requires argument.\n"; } return (undef); } if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) { my $loaded_okay = $self -> _read_parse(); if (!defined $loaded_okay || !$loaded_okay) { if ($self -> verbose()) { print "Config::has_section(): unable to load ini file.\n"; } return undef; } } if (wantarray) { my (@results); if (defined $self -> {'HASH'} -> {$test}) { push(@results, $test); } my $normalized = uc($test); foreach (@{$self -> sections()}) { if (uc($_) eq $normalized) { push(@results, $_); } } return @results; } else { if (defined $self -> {'HASH'} -> {$test}) { return (1); } } return (0); } sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; srand; %$self = @_; bless ($self, $class); while (my ($key, $value) = each %$self) { if ($self -> can($key)) { $self -> $key($value); } } return $self; } sub parameters { my $self = shift; my $section = shift; if (!defined $section || !length($section)) { return undef; } if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) { my $loaded_okay = $self -> _read_parse(); if (!defined $loaded_okay || !$loaded_okay) { if ($self -> verbose()) { print "Config::parameters(): unable to load ini file.\n"; } return undef; } } my @parameters = keys(%{$self -> {'HASH'} -> {$section}}); return \@parameters; } sub _read_parse { my $self = shift; my $file = shift; if (defined $file) { if (!$self -> basic_file_test($file)) { return undef; } } else { $file = $self -> file(); } my (@input); if (open (FILE, "<$file")) { @input = ; close FILE; } else { return undef; } if (!scalar @input) { return undef; } my ($section); foreach my $line(@input) { chomp($line); $line =~ s/\#.*$//; $self -> trim(\$line); if ($line =~ /^\[.*\]$/) { $line =~ s/^\[(.*)\]$/$1/; if (length($line)) { $section = $line; next; } else { $section = undef; } } if (!defined $section) { next; } if ($line !~ /=/) { next; } my ($param, $value) = split(/=/, $line, 2); foreach ($param, $value) { $self -> trim(\$_); } if (defined $param && length($param)) { $self -> {'HASH'} -> {$section} -> {$param} = $value; } $self -> {'FILE_SLURPED'} = 1; } return (1); } sub sections { my $self = shift; if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) { my $loaded_okay = $self -> _read_parse(); if (!defined $loaded_okay || !$loaded_okay) { if ($self -> verbose()) { print "Config::sections(): unable to load ini file.\n"; } return undef; } } my @sections = keys(%{$self -> {'HASH'}}); return \@sections; } sub set { my $self = shift; my $section = shift; my $parameter = shift; foreach ($section, $parameter) { if (!defined $_ || !length($_)) { return undef; } } $self -> {'HASH'} -> {$section} -> {$parameter} = shift; return (1); } sub trim { my $self = shift; my $stuff = shift; if (!defined $stuff) { return (undef); } elsif (ref($stuff) eq 'ARRAY') { foreach (@$stuff) { $_ =~ s/^\s+//; $_ =~ s/\s+$//; } } elsif (ref($stuff) eq 'HASH') { foreach (keys %$stuff) { $$stuff{$_} =~ s/^\s+//; $$stuff{$_} =~ s/\s+$//; } } elsif (ref($stuff) eq 'SCALAR') { $$stuff =~ s/^\s+//; $$stuff =~ s/\s+$//; } else { $stuff =~ s/^\s+//; $stuff =~ s/\s+$//; return ($stuff); } } sub value { my $self = shift; if (!defined $self -> {'HASH'} || !$self -> {'FILE_SLURPED'}) { my $loaded_okay = $self -> _read_parse(); if (!defined $loaded_okay || !$loaded_okay) { if ($self -> verbose()) { print "Config::value(): unable to load ini file.\n"; } return undef; } } my $section = shift; my $param = shift; return $self -> {'HASH'} -> {$section} -> {$param}; } sub verbose { my $self = shift; if (@_) { if ($_[0]) { $self -> {'VERBOSE'} = 1; } else { $self -> {'VERBOSE'} = 0; } } if (!defined $self -> {'VERBOSE'}) { return 0; } return $self -> {'VERBOSE'}; }