#!/usr/bin/perl -- # -*-Perl-*-
# $Revision: 1.6 $
#
# inncheck - by Brendan Kehoe (brendan@cygnus.com)
#
# Sanity-check the configuration of an INN system.

require 'stat.pl';

# =()<$inn'newslib = "@<_PATH_NEWSLIB>@";>()=
$inn'newslib = "/usr/local/news";
# =()<$inn'hn = "@<_PATH_INNDHOSTS>@";>()=
$inn'hn = "/usr/local/news/hosts.nntp";
# =()<$inn'ic = "@<_PATH_CONFIG>@";>()=
$inn'ic = "/usr/local/news/inn.conf";
# =()<$inn'm  = "@<_PATH_MODERATORS>@";>()=
$inn'm  = "/usr/local/news/moderators";
# =()<$inn'nf = "@<_PATH_NEWSFEEDS>@";>()=
$inn'nf = "/usr/local/news/newsfeeds";
# =()<$inn'na = "@<_PATH_NNRPACCESS>@";>()=
$inn'na = "/usr/local/news/nnrp.access";
# =()<$inn'pn = "@<_PATH_NNTPPASS>@";>()=
$inn'pn = "/usr/local/news/passwd.nntp";
# =()<$inn'cc = "@<_PATH_CONTROLCTL>@";>()=
$inn'cc = "/usr/local/news/control.ctl";
# =()<$inn'ec = "@<_PATH_EXPIRECTL>@";>()=
$inn'ec = "/usr/local/news/expire.ctl";
# =()<$inn'newsuser = "@<NEWSUSER>@";>()=
$inn'newsuser = "news";
# =()<$inn'newsgroup = "@<NEWSGROUP>@";>()=
$inn'newsgroup = "news";

$inn'ns = "$inn'newslib/nntpsend.ctl";

$inn'legal = "<ABFHINSTW";
%inn'sublegal = (
	"<", "0123456789",
	"A", "d",
	"B", "0123456789/",
	"F", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_./",
	"H", "0123456789",
	"I", "0123456789",
	"N", "mu",
	"S", "0123456789",
	"T", "cfmpx",
	"W", "*nfhgbmsDN",
);

%inn'icparts = (
	"fromhost",		0,
	"moderatormailer",	1,
	"organization",		2,
	"pathhost",		3,
	"server",		4,
	"domain",		5
);
@inn'ickeys = keys %inn'icparts;
$inn'nparts = $#inn'ickeys;

%inn'controls = (
	"all",		1,
	"newgroup",	2,
	"rmgroup",	3,
	"sendsys",	4,
	"checkgroups",	5,
	"version",	6,
	"senduuname",	7,
	"sendme",	8,
	"ihave",	9,
	"error",	10
);
%inn'ccactions = (
	"drop",		1,
	"log",		2,
	"mail",		3,
	"doit",		4
);
#

@inn'files = ( $inn'hn, $inn'ic, $inn'm, $inn'nf, $inn'na,
	       $inn'pn, $inn'cc, $inn'ec );
%inn'doem = ();

select (STDOUT); $| = 1;
if ($#ARGV >= 0) {
    for (@ARGV) {
	if (/hosts/)    { $inn'doem{$inn'hn}++; }
	elsif (/inn/)   { $inn'doem{$inn'ic}++; }
	elsif (/mod/)   { $inn'doem{$inn'm}++;  }
	elsif (/newsf/) { $inn'doem{$inn'nf}++; }
	elsif (/nnrp/)  { $inn'doem{$inn'na}++; }
	elsif (/nntps/) { $inn'doem{$inn'ns}++; }
	elsif (/pass/)  { $inn'doem{$inn'pn}++; }
	elsif (/cont/)  { $inn'doem{$inn'cc}++; }
	elsif (/exp/)   { $inn'doem{$inn'ec}++; }
	elsif (/perms/) { $inn'doem{"perms"}++; $check_perms++; }
	elsif (/fix/)   { $fix++; }
	elsif (/-v/)    { $verbose++; }
	elsif (/-ped/)  { $pedantic++; }
	else {
	    ($program = $0) =~ s,.*/,,;
	    print "Usage: $program [ -v ] [ -pedantic ] [ -perms [ -fix ] ] [ file... ]\n";
	    # XXX use a for to print the inn'files array out.
	    print " Legal files: hosts.nntp   inn.conf     moderators   newsfeeds\n";
	    print "              nnrp.access  passwd.nntp  control.ctl  expire.ctl\n";
	    print "              nntpsend.ctl\n";
	    print " (Default is to check all files).\n";
	    exit 1;
	}
    }
}

# Note we load "perms" with 1 if they do -perms, so we don't trigger the
# full checking.  Also don't spit out shell stuff unless -perms.
%inn'doem || do { for (@inn'files) { $inn'doem{$_}++; } };
$fix = 0 unless $check_perms;

$inn'doem{$inn'hn} && do {
    if (-f $inn'hn) { &hosts_nntp; }
	else { print &basename($inn'hn), ":0: file missing\n"; } };
$inn'doem{$inn'ic} && do {
    if (-f $inn'ic) { &inn_conf; }
	else { print &basename($inn'ic), ":0: file missing\n"; } };
$inn'doem{$inn'm} && do {
    if (-f $inn'm) { &moderators; }
	else { print &basename($inn'm), ":0: file missing\n"; } };
$inn'doem{$inn'nf} && do {
    if (-f $inn'nf) { &newsfeeds; }
	else { print &basename($inn'nf), ":0: file missing\n"; } };
$inn'doem{$inn'na} && do {
    if (-f $inn'na) { &nnrp_access; }
	else { print &basename($inn'na), ":0: file missing\n"; } };
$inn'doem{$inn'pn} && do {
    if (-f $inn'pn) { &passwd_nntp; }
	else { print &basename($inn'pn), ":0: file missing\n"; } };
$inn'doem{$inn'cc} && do {
    if (-f $inn'cc) { &control_ctl; }
	else { print &basename($inn'cc), ":0: file missing\n"; } };
$inn'doem{$inn'ec} && do {
    if (-f $inn'ec) { &expire_ctl; }
	else { print &basename($inn'ec), ":0: file missing\n"; } };
$inn'doem{$inn'ns} && do {
    if (-f $inn'ns) { &nntpsend_ctl; }
	else { print &basename($inn'ns), ":0: file missing\n"; } };

do &Check_All if $check_perms;
exit 0;

#
# *-* Do inn.conf
sub hosts_nntp {
local ($workfile, $line);
print "Looking at $inn'hn...\n" if $verbose;
$workfile = &basename ($inn'hn); &Perms ($inn'hn);

open(HN, $inn'hn) || return &Die ($inn'hn);
hn: while (<HN>) {
    $line++; next hn if /^#/o; next hn if /^\s*$/o; chop;

    next hn if /^[\w\.\-]+:/;
    next hn if /^[\d\.]+:/;

    print "$workfile:$line: malformed line.\n";
}
close HN;
};

#

sub inn_conf {
local ($workfile, $line, $k, $v, $w, $tmp);
local (@found[$inn'nparts]);
local ($hostname, $fqdn);

print "Looking at $inn'ic...\n" if $verbose;
$workfile = &basename ($inn'ic); &Perms ($inn'ic);
chop ($hostname = `hostname`);
$fqdn = (gethostbyname ($hostname))[0];

open(IC, $inn'ic) || return &Die ($inn'ic);
ic: while (<IC>) {
    $line++; next ic if /^#/o; next ic if /^\s*$/o; chop;

    /^\s+#/ && do {
	print "$workfile:$line: A comment must be flush left\n";
	next ic;
    };
    /^(.*):\s*(.*)$/o && do {
	$k = $1; $v = $2;
	if (($w = $inn'icparts{$1}) < 0) {
		print "$workfile:$line: Invalid field `$1'\n";
		next ic;
	}
	if ($found[$w]) {
		print "$workfile:$line: Field `$1' already specified\n";
		next ic;
	}
	$found[$w]++;
	if ($w eq 0) {
		if ($v !~ /[\w\-]+\.[\w\-]+/) {
			print "$workfile:$line: fromhost isn't a valid FQDN\n";
			next ic;
		}
	} elsif ($w eq 1) {
		if ($v !~ /[\w\-]+\.[\w\-]+/) {
			print "$workfile:$line: modmailer has bad address\n";
			next ic;
		}
	} elsif ($w eq 2) {
		if ($v eq "") {
			print "$workfile:$line: org is blank\n";
			next ic;
		}
	} elsif ($w eq 3) {
		if ($v =~ /!/) {
			print "$workfile:$line: warning: pathhost has a ! in it\n";
			next ic;
		}
	} elsif ($w eq 4) {
		if ($pedantic && ($fqdn !~ /^$v/)) {
			print "$workfile:$line: warning: server (`$v') isn't local hostname\n";
			next ic;
		}
	} else {
		if ($fqdn =~ /[^\.]+\(\..*\)/) {
		    if ($v ne $1) {
		        print "$workfile:$line: warning: domain (`$v') isn't local domain\n";
		    }
		}
		($v =~ /^\./) && do {
		    print "$workfile:$line: warning: domain should not have a leading period\n";
		};
	}
    };
}

foreach $key (@inn'ickeys) {
  $tmp = $inn'icparts{$key};
  $found[$tmp] || do {
    if ($tmp == 1) {
	printf "$workfile:$line: warning: missing $key (and no moderators file).\n"
	    unless -f $inn'm;
    } else {
	$pedantic && printf "$workfile:$line: warning: missing $key\n";
    }
  };
}

close IC;
};

#
# *-* Do moderators
sub moderators {
local ($workfile, $line, $tmp);
print "Looking at $inn'm...\n" if $verbose;
$workfile = &basename ($inn'm); &Perms ($inn'm);

open(M, $inn'm) || return &Die ($inn'm);
mod: while (<M>) {
    $line++; next mod if /^#/o; next mod if /^\s*$/o; chop;

    if (/^([\w\.\-\*]*):(.*)/o) {
	($1 eq "" || $2 eq "") && do {
	    print "$workfile:$line: missing field\n";
	};
	$tmp = $2; ($pedantic && ($tmp !~ /[@!]/)) && do {
	    print "$workfile:$line: not an email address\n";
	};
	($verbose && ($tmp =~ /^%s$/)) && do {
	    print "$workfile:$line: warning: `$1' goes to local address\n";
	};
	# XXX What if they have `%s@foo,%s@bar'?  e.g., a local addr?
	($tmp =~ /.*%s.*%s.*/) && do {
	    print "$workfile:$line: should only have one %s in an address field\n";
	};
	next mod;
    }
    print "$workfile:$line: malformed line.\n";
}
close M;
};

#
# *-* Do newsfeeds
sub newsfeeds {
local ($workfile, $line, $tmp, $l_in, $real_line, $f);
local (@funnels, $nfunnels);	# Keep track of the funnels.

print "Looking at $inn'nf...\n" if $verbose;
$workfile = &basename ($inn'nf); &Perms ($inn'nf);

open(NF, $inn'nf) || return &Die ($inn'nf);
nf: while (<NF>) {
    $line++; next nf if /^\s*$/o; chop;

    # For the newsfeeds file, if they commented out a feed, swallow
    # everything it represents.
    /^#.*\\$/o && do {
       while (/\\$/o) { $_ = <NF>; $line++; }
       do { $_ = <NF>; $line++; } if //;
       next nf;
    };
    next nf if /^#/o;

     /^\s+\#/ && do {
	 print "$workfile:$line: Comments must be flush left!\n";
	 next nf;
    };

    /^([\s\w\-\.\/,@]+)[:\\]/o && do {
        $tmp = $1; if ($tmp =~ /\s/o) {
            print "$workfile:$line: Newsfeed `$tmp' has whitespace in its name\n";
            next nf;
        }

	# Swallow all continuations (lines ending with a backslash).
	$real_line = $line;
	while (/\\\s*$/o) {
	    chop; $l_in .= $_;
	    $_ = <NF>; chop; $line++;
	    # strip out all leading whitespace
	    s/^\s+//g;
	}
	$l_in .= $_ if /[^\\]$/o;

	&Parse_entry($l_in, $real_line); $l_in = "";
	next nf;
    };

    print "$workfile:$line: malformed line.\n";
}
close NF;
# Go through and make sure all referenced funnels exist.
for (@funnels) { /`(.*)'/; print "$workfile:$_\n" unless $def{$1}; }
(!$def{"ME"} || $me_empty)
    && do { print "$workfile:0: warning: no ME restriction on validity of incoming article distributions\n"; };

print "done.\n" if $verbose;
};

#

sub nnrp_access {
local ($workfile, $line, $tmp);
print "Looking at $inn'na...\n" if $verbose;
$workfile = &basename ($inn'na); &Perms ($inn'na);

open(NA, $inn'na) || return &Die ($inn'na);
na: while (<NA>) {
    $line++; next na if /^#/o; next na if /^\s*$/o; chop;

    if (/([\w\-\.\*]+):([\w\-\s]*):.*:.*:/o) {
	$tmp = $1;
	print "$workfile:$line: access list has a / in it\n" if $tmp =~ /\//;

	$tmp = $2;
	print "$workfile:$line: illegal permissions: `$tmp'\n"
	    if $tmp =~ /[A-OQS-Z]/;

	next na;
    } else {
	print "$workfile:$line: malformed line.\n";
    }
}
close NA;
};

#
# *-* Do passwd.nntp
sub passwd_nntp {
local ($workfile, $line, $name, $pass);
print "Looking at $inn'pn...\n" if $verbose;
$workfile = &basename ($inn'pn); &Perms ($inn'pn);

open(PN, $inn'pn) || return &Die ($inn'pn);
pn: while (<PN>) {
    $line++; next pn if /^#/o; next pn if /^\s*$/o; chop;

    if (/[\w\-\.]+:(.*):(.*)(:.*)?/
	|| /[\w\-\.]+:$/) {
	$name = $1; $pass = $2;
	if (($name eq "" && $pass ne "")
	    || ($name ne "" && $pass eq "")) {
	    print "$workfile:$line: username/password must both be blank or non-blank\n";
	}
    } else {
	print "$workfile:$line: malformed line.\n";
    }
}
close PN;
};

#

sub control_ctl {
local ($workfile, $line, $tmp);
print "Looking at $inn'cc...\n" if $verbose;
$workfile = &basename ($inn'cc); &Perms ($inn'cc);

open(CC, $inn'cc) || return &Die ($inn'cc);
cc: while (<CC>) {
    $line++; next cc if /^#/o; next cc if /^\s*$/o; chop;

    /^([^:]+):([^:]+):([^:]+):([^:=]+)/o && do {
	if ($inn'controls{$1} eq 0) {
	    print "$workfile:$line: illegal control message `$1'.\n";
	    next cc;
	}
	if ($1 eq "error" && $4 eq "doit") {
	    print "$workfile:$line: action for erroneous control messages set to `doit'.\n";
	}
	if ($2 ne "*") {
	    $tmp = $2;
	    if ($2 eq "") {
 		print "$workfile:$line: empty from field for `$1'.\n";
		next cc;
	    } elsif ($tmp !~ /[@!]/) {
		print "$workfile:$line: warning: doesn't resemble an email address.\n";
	    }
	}
	# We could sanity-check this for conflicting rules, or warn about
	# the last-match rule possibly tripping us up, but we'll see.
	if ($3 ne "*") {
	    $tmp = $3; if ($tmp !~ /\./) {
		print "$workfile:$line: warning: may not match groups properly.\n";
	    }
	}
	$tmp = $4; $tmp =~ /([^=]+)(=.+)?/;
	if ($inn'ccactions{$1} eq 0) {
	    print "$workfile:$line: illegal action `$tmp'\n";
	} elsif ($2 ne "") {
	    if ($1 ne "doit" && $1 ne "mail") {
		print "$workfile:$line: `$1' doesn't take an argument.\n";
		next cc;
	    }
	}
	next cc;
    };
    print "$workfile:$line: malformed line.\n";
}
close CC;
};

#

sub expire_ctl {
local ($workfile, $line, $rem, $tmp);
print "Looking at $inn'ec...\n" if $verbose;
$workfile = &basename ($inn'ec); &Perms ($inn'ec);

open(EC, $inn'ec) || return &Die ($inn'ec);
ec: while (<EC>) {
    $line++; next ec if /^#/o; next ec if /^\s*$/o; chop;

    /^\/remember\/:(.+)/ && do {
	print "$workfile:$line: more than one /remember/ line.\n" if $rem;
	# Still warn about dupes even if the predecessors had problems.
	$rem++;

	$tmp = $1;
	# These are arbitrary magic numbers.
	if ($1 > 60.0 || $1 < 5.0) {
	    print "$workfile:$line: warning: are you sure about your value for remember?\n";
	} elsif ($tmp !~ /[\d\.]+/) {
	    print "$workfile:$line: illegal value `$1' for remember.\n";
        }
	next ec;
     };

     # This could also be beefed up to check for errors with more than
     # one expire for each of m, u, and a, and other things.
     /^[^:]+:([^:]+):([\d\.]+|never):([\d\.]+|never):([\d\.]+|never)/o && do {
	if ($1 !~ /[mMuUaA]/) {
	    print "$workfile:$line: illegal modflag `$1' (must be M, U, or A)\n";
	}
	if ($4 ne "never" && $3 > $4) {
	    print "$workfile:$line: purge `$4' set younger than default `$3'.\n";
	}
	if ($3 ne "never" && $2 ne "never" && $2 > $3) {
	    print "$workfile:$line: default `$3' set younger than keep `$2'.\n";
	}
	next ec;
    };
    print "$workfile:$line: malformed line.\n";
  }
close EC;
};

#

sub nntpsend_ctl {
local ($workfile, $line, $tmp, $site);
print "Looking at $inn'ns...\n" if $verbose;
$workfile = &basename ($inn'ns); &Perms ($inn'ns);

open(NS, $inn'ns) || return &Die ($inn'ns);
ns: while (<NS>) {
    $line++; next ns if /^#/o; next ns if /^\s*$/o; chop;

    # Ignore the size info for now.
    /^([\s\w\-\.\/,@]+):(.*):.*:(.*)/o && do {
	$tmp =~ /\s/o
	    && print "$workfile:$line: sitename `$tmp' has whitespace in its name\n";
	$2 eq "" && print "$workfile:$line: FQDN is empty for `$1'\n";
	$site = $1;
	unless ($3 eq "") {
	    for (split(/ /, $3)) {
		if (/^[-]?([adrtTpS])(.*)/o) {
		    if ($1 eq "t" || $1 eq "T") {
			$tmp = $2; if ($tmp !~ /\d+/) {
			    print "$workfile:$line: illegal argument to option `$1': $3\n";
			}
		    }
		} else {
		    print "$workfile:$line: illegal arg format for `$site'\n";
		}
	    }
	}
	next ns;
    };
	    

    print "$workfile:$line: malformed line.\n";
}
close HN;
};

#

# Parse a newsfeeds entry
sub Parse_entry {
    local($l, $pline) = @_;
    local($funnel, $file, $me);

    if ($l !~ /^.*(:.*){3}.*$/o) {
	print "$workfile:$pline: too few fields (`$l').\n";
	return;
    }

    $l =~ /^([^\/,:]+)([,\/][^:]*)*:/o; $feed = $1;
    if ($def{$feed} && $pedantic) {
	print "$workfile:$pline: warning: feed $feed already appeared\n";
	return;
    }

    print "$feed, " if $verbose;
    $def{$feed}++;
    $feed eq "ME" && $me++;

    if ($2 ne "") {
	$exclusions = $2;
	if ($exclusions =~ /^\/.*\//o) {
	    print "$workfile:$pline: Multiple slashes in exclusions for `$1'\n";
	    return;
	}
    }

    $l =~ /^[^:]*:([^\/:]*)([\/]?[^:]+[,]?)*:(.*):(.*)/o;
    $groups = $1;
    if ($2 ne "") {
	$dists = $2;
	if ($dists =~ /^\/.*\//o) {
	    print "$workfile:$pline: Multiple slashes in groups/dists for `$feed'\n";
	    return;
	}
    } elsif ($me) {
	# Warn if the dists field for ME is empty.  Don't bother giving the
	# warning now, since there are two occasions when it's useful.
	$me_empty = 1;
    }

    if (($groups =~ /[^\w!]all/)
	&& ($groups !~ /!junk/)
	&& ($groups !~ /!control/)) {
	# If we don't have !junk,!control, give a helpful warning.
	print "$workfile:$pline: warning: consider adding !junk (and possibly !control) to $feed\n";
    }

    $file = $funnel = 0;
    foreach $flag (split (/,/, $3)) {
	($main, @chars) = split ((), $flag);
	if (index($inn'legal, $main) < 0) {
	    print "$workfile:$pline: illegal flag `$flag'\n";
	    return;
	}
	for (@chars) {
	    if (index($inn'sublegal{$main}, $_) < 0) {
	        print "$workfile:$pline: illegal flag `$flag'\n";
	        return;
	    }
	    # Track funnels
	    if ($main eq "T") {
		$_ eq "m" && ($funnel = 1);
		$_ eq "f" && ($file = 1);
	    }
	}
    }

    $tmp = $4; $tmp =~ /^[^\/]/o
        && $pedantic && !$funnel && !$file
	&& print "$workfile:$pline: warning: relative path for $feed\n";

    # Mark funnels for later checking (but only check it if we haven't
    # seen its definition yet---no need to look for something you know).
    $funnel && $tmp =~ /^[^\/]/o && !$def{$tmp}
	&& ($funnels[$nfunnels++] = "$pline: undefined funnel `$tmp'");
};

# Check the perms for each of the basic files we normally check.
sub Perms {
    local ($file) = @_;
    local (@sb, $mode, $owner);
    local (%modes) = (
		$inn'hn, 0440,
		$inn'nf, 0444,
		$inn'na, 0440,
		$inn'cc, 0440,
		$inn'ec, 0440,
		$inn'ic, 0444,
		$inn'm,  0444,
		$inn'pn, 0440,
		$inn'ns, 0440
      );

    $workfile = &basename ($file);
    &Check ($file, $modes{$file}, $inn'newsuser, $inn'newsgroup);
};

# Check the permissions of nearly every file in an INN installation.
sub Check_All {
    local ($workfile);
    # =()<    local ($inn'ctlprogs) = "@<_PATH_CONTROLPROGS>@";>()=
    local ($inn'ctlprogs) = "/usr/local/news/bin/control";
    # =()<    local ($inn'inews) = "@<_PATH_INEWS>@";>()=
    local ($inn'inews) = "/usr/local/news/inews";
    # =()<    local ($inn'innd) = "@<_PATH_INND>@";>()=
    local ($inn'innd) = "/usr/local/etc/innd";
    # =()<    local ($inn'inndstart) = "@<_PATH_INNDSTART>@";>()=
    local ($inn'inndstart) = "/usr/local/etc/inndstart";
    # =()<    local ($inn'newsbin) = "@<_PATH_NEWSBIN>@";>()=
    local ($inn'newsbin) = "/usr/local/news/bin";
    # =()<    local ($inn'newsboot) = "@<_PATH_NEWSBOOT>@";>()=
    local ($inn'newsboot) = "/usr/local/etc/rc.news";
    # =()<    local ($inn'nnrpd) = "@<_PATH_NNRPD>@";>()=
    local ($inn'nnrpd) = "/usr/local/etc/in.nnrpd";
    # =()<    local ($inn'parsectl) = "@<_PATH_PARSECTL>@";>()=
    local ($inn'parsectl) = "/usr/local/news/parsecontrol";
    # =()<    local ($inn'relaynews) = "@<_PATH_RELAYNEWS>@";>()=
    local ($inn'relaynews) = "/usr/local/news/relaynews";
    # =()<    local ($inn'rnews) = "@<_PATH_RNEWS>@";>()=
    local ($inn'rnews) = "/usr/local/news/rnews";
    # =()<    local ($inn'rnewsprogs) = "@<_PATH_RNEWSPROGS>@";>()=
    local ($inn'rnewsprogs) = "/usr/local/news/bin/rnews";
    # =()<    local ($inn'spool) = "@<_PATH_SPOOL>@";>()=
    local ($inn'spool) = "/var/spool/news";
    # =()<    local ($inn'archivedir) = "@<_PATH_ARCHIVEDIR>@";>()=
    local ($inn'archivedir) = "/var/spool/news/news.archive";
    # =()<    local ($inn'batchdir) = "@<_PATH_BATCHDIR>@";>()=
    local ($inn'batchdir) = "/var/spool/news/out.going";
    # =()<    local ($inn'most_logs) = "@<_PATH_MOST_LOGS>@";>()=
    local ($inn'most_logs) = "/var/log/news";
    # =()<    local ($inn'spoolnews) = "@<_PATH_SPOOLNEWS>@";>()=
    local ($inn'spoolnews) = "/var/spool/rnews";
    # =()<    local ($inn'badnews) = "@<_PATH_BADNEWS>@";>()=
    local ($inn'badnews) = "/var/spool/rnews/bad";
    # =()<    local ($inn'tmp) = "@<_PATH_SPOOLTEMP>@";>()=
    local ($inn'tmp) = "/var/spool/rnews/tmp";
    # =()<    local ($inn'innddir) = "@<_PATH_INNDDIR>@";>()=
    local ($inn'innddir) = "/usr/local/news/innd";
    
    # Note: These aren't local cuz you lose if you try to squeeze all of
    #       this onto the stack in some situations.

    # All of the control messages.
    @inn'control = ( "checkgroups", "default", "docheckgroups",
	"newgroup", "rmgroup", "sendme", "sendsys", "sendme", "senduuname",
	"version" );

    # Programs in RNEWSPROGS are all mode 555.
    @inn'rnewsp = ( "c7unbatch", "decode", "encode" );

    # Programs in NEWSBIN are either 555 or 550.
    @inn'newsbin_public = ( "archive", "batcher", "buffchan", "convdate",
	   "cvtbatch", "expire", "filechan", "getlist", "grephistory",
	   "innconfval", "innxmit", "makeactive", "makehistory", "newsrequeue",
	   "nntpget", "prunehistory", "shlock", "shrinkfile" );
    @inn'newsbin_private = ( "ctlinnd", "ctlrun", "inncheck",
	   "innstat",  "innwatch", "makegroup", "news.daily",
       	   "nntpsend", "scanlogs", "sendbatch", "tally.control",
	   "tally.unwanted", "writelog" );
    @inn'newslib_private = ( "send-ihave", "send-nntp", "send-uucp" );
    @inn'newslib_private_read = ( "innlog.awk" );
    @inn'dirs = ( $inn'spool, $inn'archivedir, $inn'batchdir,
	   $inn'most_logs, $inn'most_logs . "/OLD", $inn'spoolnews,
	   $inn'badnews, $inn'tmp, $inn'newslib, $inn'newsbin,
	   $inn'ctlprogs, $inn'rnewsprogs );

    # The modes for the various programs.
    %prog_modes = (
	      $inn'inews,      02555,
	      $inn'newsboot,   0550,
	      $inn'nnrpd,      0555,
	      $inn'innd,       0555,
	      $inn'parsectl,   0550,
	      $inn'relaynews,  0555,
	      $inn'rnews,      02555 );
    @programs = keys %prog_modes;

    for (@inn'files) { &Perms ($_); }

    for (@inn'newslib_private) {
	$workfile = $_;
	&Check ("$inn'newslib/$_", 0550, $inn'newsuser, $inn'newsgroup);
    }
    for (@inn'newslib_private_read) {
	$workfile = $_;
	&Check ("$inn'newslib/$_", 0440, $inn'newsuser, $inn'newsgroup);
    }
    for (@inn'newsbin_private) {
	$workfile = $_;
	&Check ("$inn'newsbin/$_", 0550, $inn'newsuser, $inn'newsgroup);
    }
    for (@inn'newsbin_public) {
	$workfile = $_;
	&Check ("$inn'newsbin/$_", 0555, $inn'newsuser, $inn'newsgroup);
    }
    for (@inn'control) {
	$workfile = $_;
	&Check ("$inn'ctlprogs/$_", 0550, $inn'newsuser, $inn'newsgroup);
    }
    for (@inn'rnewsp) {
	$workfile = $_;
	&Check ("$inn'rnewsprogs/$_", 0555,
	        $inn'newsuser, $inn'newsgroup);
    }

    # Also make sure that @inn'rnewsp are the *only* programs in there;
    # anything else is probably someone trying to spoof rnews into being bad.
    &Intersect ($inn'rnewsprogs, @inn'rnewsp);

    for (@programs) {
	$workfile = &basename ($_);
	&Check ($_, $prog_modes{$_}, $inn'newsuser, $inn'newsgroup);
    }
    $workfile = &basename ($inn'inndstart);
    &Check ($inn'inndstart, 0555, "root", "bin");

    for (@inn'dirs) {
	$workfile = $_;
 	&Check ($_, 0775, $inn'newsuser, $inn'newsgroup);
    }
    $workfile = $inn'innddir;
    &Check ($inn'innddir, 0770, $inn'newsuser, $inn'newsgroup);

    # don't touch this for now:
    return 1;
};

# Given a file F, check its mode to be M, and its ownership to be by the
# user U in the group G.
sub Check {
    local ($f, $m, $u, $g) = @_;
    local (@sb, $owner, $group, $mode);

    ! -e $f && do { $fix && print "# "; print ":0: missing $f\n"; return -1; };
    @sb = stat ($f);
    $owner = (getpwuid (@sb[$ST_UID]))[0];
    $group = (getgrgid (@sb[$ST_GID]))[0];
    $mode  = @sb[$ST_MODE] & ~0770000;

    # If it's a directory, ignore the set-gid bit.
    -d $f && ($mode &= ~0777000);

    ($owner ne $u) && do {
	$fix && print "# ";
	print "$workfile:0: warning: owned by $owner, should be $u\n";
	$fix && print "chown $u $f\n";
    };
    ($group ne $g) && do {
	$fix && print "# ";
	print "$workfile:0: warning: in group $group, should be $g\n";
	$fix && print "chgrp $g $f\n";
    };
    ($mode ne $m) && do {
	$fix  && print "# ";
	printf "$workfile:0: warning: mode %o, should be %o\n", $mode, $m;
	$fix && printf "chmod %o %s\n", $m, $f;
    };
};

# return 1 if the Intersection of the files in the DIR and FILES is empty.
# otherwise, report an error for each illegal file, and return 0.
sub Intersect {
    local ($dir, @files) = @_;
    local (@in) = &ls ($dir);
    local (%dummy, @foo);
    
    return 1 if $#in eq -1;

    grep ($dummy{$_}++, @files);
    return 1 unless @foo = grep (!$dummy{$_}, @in);

    for (@foo) {
	$fix && print "# ";
	print ":0: ERROR: illegal file `$_' in $dir\n";
    }
    return 0;
};

sub ls {
    local ($dir) = @_;
    local (*DIR);
    local (@list) = ();
    
    unless (opendir(DIR, $dir)) {
	$fix && print "# ";
	print ":0: warning: can't open $dir directory\n";
    } else {
	# this assumes we'll not get a dir of './foo'
	@list = grep(!/^\./, readdir(DIR));
    }
    closedir DIR;
    return @list;
};

sub Die {
    $fix && print "# ";
    print &basename (@_), ":0: couldn't open: $!\n";
};

sub basename { local ($tmp) = @_; $tmp =~ s,.*/,,; return $tmp; };
