3 ## Sanity-check the configuration of an INN system
4 ## by Brendan Kehoe <brendan@cygnus.com> and Rich $alz.
6 require "@LIBDIR@/innshellvars.pl" ;
12 $newsuser = '@NEWSUSER@';
13 $newsgroup = '@NEWSGRP@';
15 ## We use simple names, mapping them to the real filenames only when
16 ## we actually need a filename.
18 'active', "$inn::pathdb/active",
19 'archive', "$inn::patharchive",
20 'badnews', "$inn::pathincoming/bad",
21 'batchdir', "$inn::pathoutgoing",
22 'control.ctl', "$inn::pathetc/control.ctl",
23 'ctlprogs', "$inn::pathcontrol",
24 'expire.ctl', "$inn::pathetc/expire.ctl",
25 'history', "$inn::pathdb/history",
26 'incoming.conf', "$inn::pathetc/incoming.conf",
27 'inews', "$inn::pathbin/inews",
28 'inn.conf', "$inn::pathetc/inn.conf",
29 'innd', "$inn::pathbin/innd",
30 'innddir', "$inn::pathrun",
31 'inndstart', "$inn::pathbin/inndstart",
32 'moderators', "$inn::pathetc/moderators",
33 'most_logs', "$inn::pathlog",
34 'newsbin', "$inn::pathbin",
35 'newsboot', "$inn::pathbin/rc.news",
36 'newsfeeds', "$inn::pathetc/newsfeeds",
37 'overview.fmt', "$inn::pathetc/overview.fmt",
38 'newsetc', "$inn::pathetc",
39 'newslib', "@LIBDIR@",
40 'nnrpd', "$inn::pathbin/nnrpd",
41 'nntpsend.ctl', "$inn::pathetc/nntpsend.ctl",
42 'oldlogs', "$inn::pathlog/OLD",
43 'passwd.nntp', "$inn::pathetc/passwd.nntp",
44 'readers.conf', "$inn::pathetc/readers.conf",
45 'rnews', "$inn::pathbin/rnews",
46 'rnewsprogs', "$inn::pathbin/rnews.libexec",
47 'spooltemp', "$inn::pathtmp",
48 'spool', "$inn::patharticles",
49 'spoolnews', "$inn::pathincoming"
52 ## The sub's that check the config files.
55 'control.ctl', 'control_ctl',
56 'expire.ctl', 'expire_ctl',
57 'incoming.conf', 'incoming_conf',
58 'inn.conf', 'inn_conf',
59 'moderators', 'moderators',
60 'newsfeeds', 'newsfeeds',
61 'overview.fmt', 'overview_fmt',
62 'nntpsend.ctl', 'nntpsend_ctl',
63 'passwd.nntp', 'passwd_nntp',
64 'readers.conf', 'readers_conf'
67 ## The modes of the config files we can check.
72 'incoming.conf', 0640,
93 print "$file:$line: starts with whitespace\n";
96 print "$file:$line: ends with whitespace\n";
103 ## These are the functions that verify each individual file, called
104 ## from the main code. Each function gets <IN> as the open file, $line
105 ## as the linecount, and $file as the name of the file.
115 local ($group, $hi, $lo, $f, $alias, %groups, %aliases);
117 input: while ( <IN> ) {
119 unless ( ($group, $hi, $lo, $f) = /^([^ ]+) (\d+) (\d+) (.+)\n$/ ) {
120 print "$file:$line: malformed line.\n";
124 print "$file:$line: group `$group' already appeared\n"
125 if $groups{$group}++;
126 print "$file:$line: `$hi' < '$lo'.\n"
127 if $hi < $lo && $lo != $hi + 1;
129 next input if $f =~ /^[jmynx]$/;
130 unless ( ($alias) = $f =~ /^=(.*)$/ ) {
131 print "$file:$line: bad flag `$f'.\n";
135 print "$file:$line: empty alias.\n";
138 $aliases{$alias} = $line
139 unless defined $groups{$alias};
141 foreach $key ( keys %aliases ) {
142 print "$file:$aliases{$group} aliased to unknown group `$key'.\n"
143 unless defined $groups{$key};
152 %control'messages = (
175 local ($msg, $from, $ng, $act);
177 input: while ( <IN> ) {
178 next input if &spacious($file, ++$line);
180 unless ( ($msg, $from, $ng, $act) =
181 /^([^:]+):([^:]+):([^:]+):(.+)$/ ) {
182 print "$file:$line: malformed line.\n";
185 if ( !defined $control'messages{$msg} ) {
186 print "$file:$line: unknown control message `$msg'.\n";
189 print "$file:$line: action for unknown control messages is `doit'.\n"
190 if $msg eq "default" && $act eq "doit";
191 print "$file:$line: empty from field.\n"
193 print "$file:$line: bad email address.\n"
194 if $from ne "*" && $from !~ /[@!]/;
196 ## Perhaps check for conflicting rules, or warn about the last-match
197 ## rule? Maybe later...
198 print "$file:$line: may not match groups properly.\n"
199 if $ng ne "*" && $ng !~ /\./;
200 if ( $act !~ /([^=]+)(=.+)?/ ) {
201 print "$file:$line: malformed line.\n";
205 $act = "verify" if ($act =~ /^verify-.+/) ;
206 print "$file:$line: unknown action `$act'\n"
207 if !defined $control'actions{$act};
219 local ($rem, $v, $def, $class, $pat, $flag, $keep, $default, $purge, $groupbaseexpiry);
221 $groupbaseexpiry = $inn::groupbaseexpiry;
222 $groupbaseexpiry =~ tr/A-Z/a-z/;
223 input: while ( <IN> ) {
224 next input if &spacious($file, ++$line);
226 if ( ($v) = m@/remember/:(.+)@ ) {
227 print "$file:$line: more than one /remember/ line.\n"
229 if ( $v !~ /[\d\.]+/ ) {
230 print "$file:$line: illegal value `$v' for remember.\n";
233 print "$file:$line: are you sure about your /remember/ value?\n"
234 ## These are arbitrary "sane" values.
235 if $v != 0 && ($v > 60.0 || $v < 5.0);
239 ## Could check for conflicting lines, but that's hard.
240 if ($groupbaseexpiry =~ /^true$/ || $groupbaseexpiry =~ /^yes$/ ||
241 $groupbaseexpiry =~ /^on$/) {
242 unless ( ($pat, $flag, $keep, $default, $purge) =
243 /^([^:])+:([^:]+):([\d\.]+|never):([\d\.]+|never):([\d\.]+|never)$/ ) {
244 print "$file:$line: malformed line.\n";
247 print "$file:$line: duplicate default line\n"
248 if $pat eq "*" && $flag eq "a" && $def++;
249 print "$file:$line: unknown modflag `$flag'\n"
250 if $flag !~ /[mMuUaAxX]/;
252 unless ( ($class, $keep, $default, $purge) =
253 /^(\d+):([\d\.]+|never):([\d\.]+|never):([\d\.]+|never)$/ ) {
254 print "$file:$line: malformed line.\n";
257 print "$file:$line: invalid class\n"
260 print "$file:$line: purge `$purge' younger than default `$default'.\n"
261 if $purge ne "never" && $default > $purge;
262 print "$file:$line: default `$default' younger than keep `$keep'.\n"
263 if $default ne "never" && $keep ne "never" && $keep > $default;
285 system ("$inn::pathbin/innconfval", '-C');
287 # if ( $k eq "domain" ) {
288 # print "$file:$line: domain (`$v') isn't local domain\n"
289 # if $fqdn =~ /[^\.]+\(\..*\)/ && $v ne $1;
290 # print "$file:$line: domain should not have a leading period\n"
292 # } elsif ( $k eq "fromhost" ) {
293 # print "$file:$line: fromhost isn't a valid FQDN\n"
294 # if $v !~ /[\w\-]+\.[\w\-]+/;
295 # } elsif ( $k eq "moderatormailer" ) {
296 # # FIXME: shouldn't warn about blank lines if the
297 # # moderators file exists
298 # print "$file:$line: moderatormailer has bad address\n"
299 # if $v !~ /[\w\-]+\.[\w\-]+/ && $v ne "%s";
300 # } elsif ( $k eq "organization" ) {
301 # print "$file:$line: org is blank\n"
303 # } elsif ( $k eq "pathhost" ) {
304 # print "$file:$line: pathhost has a ! in it\n"
306 # } elsif ( $k eq "pathalias" ) {
307 # print "$file:$line: pathalias has a ! in it\n"
309 # } elsif ( $k eq "pathcluster" ) {
310 # print "$file:$line: pathcluster has a ! in it\n"
312 # } elsif ( $k eq "server" ) {
313 # print "$file:$line: server (`$v') isn't local hostname\n"
314 # if $pedantic && $fqdn !~ /^$v/;
317 # if ( $key eq "moderatormailer" ) {
318 # printf "$file:$line: missing $key and no moderators file.\n"
319 # if ! -f $paths{"moderators"};
334 input: while ( <IN> ) {
335 next input if &spacious($file, ++$line);
337 unless ( ($k, $v) = /^([^:]+):(.+)$/ ) {
338 print "$file:$line: malformed line.\n";
342 if ( $k eq "" || $v eq "" ) {
343 print "$file:$line: missing field\n";
346 print "$file:$line: not an email address\n"
347 if $pedantic && $v !~ /[@!]/;
348 print "$file:$line: `$v' goes to local address\n"
349 if $pedantic && $v eq "%s";
350 print "$file:$line: more than one %s in address field\n"
373 'Q', '^@?\d+(-\d+)?/\d+(_\d+)?$',
376 'W', '^[befghmnpst*DGHNPOR]*$',
382 local ($next, $start, $me_empty, @muxes, %sites);
383 local ($site, $pats, $dists, $flags, $param, $type, $k, $v, $defsub);
384 local ($bang, $nobang, $prog, $dir);
386 input: while ( <IN> ) {
390 print "$file:$line: starts with whitespace\n"
393 ## Read continuation lines.
403 print "$file:$line: ends with whitespace\n"
406 # Catch a variable setting.
407 if ( /^\$([A-Za-z0-9]+)=/ ) {
408 print "$file:$line: variable name too long\n"
413 unless ( ($site, $pats, $flags, $param) =
414 /^([^:]+):([^:]*):([^:]*):(.*)$/ ) {
415 print "$file:$line: malformed line.\n";
419 print "$file:$line: Newsfeed `$site' has whitespace in its name\n"
421 print "$file:$line: comma-space in site name\n"
423 print "$file:$line: comma-space in subscription list\n"
425 print "$file:$line: comma-space in flags\n"
428 print "$file:$start: ME has exclusions\n"
430 print "$file:$start: multiple slashes in exclusions for `$site'\n"
432 $site =~ s@([^/]*)/.*@$1@;
436 if ( $site eq "ME" ) {
438 $defsub =~ s@(.*)/.*@$1@;
439 } elsif ( $defsub ne "" ) {
440 $pats = "$defsub,$pats";
442 print "$file:$start: Multiple slashes in distribution for `$site'\n"
445 if ( $site eq "ME" ) {
446 print "$file:$start: ME flags should be empty\n"
448 print "$file:$start: ME param should be empty\n"
454 ## If we don't have !junk,!control, give a helpful warning.
455 # if ( $site ne "ME" && $pats =~ /!\*,/ ) {
456 # print "$file:$start: consider adding !junk to $site\n"
457 # if $pats !~ /!junk/;
458 # print "$file:$start: consider adding !control to $site\n"
459 # if $pats !~ /!control/;
462 ## Check distributions.
463 if ( ($dists) = $pats =~ m@.*/(.*)@ ) {
465 dist: foreach $d ( split(/,/, $dists) ) {
472 print "$file:$start: questionable distribution `$d'\n"
473 if $d !~ /^!?[a-z0-9-]+$/;
475 print "$file:$start: both ! and non-! distributions\n"
479 flag: foreach $flag ( split(/,/, $flags) ) {
480 ($k, $v) = $flag =~ /(.)(.*)/;
481 if ( !defined $newsfeeds'flags{$k} ) {
482 print "$file:$start: unknown flag `$flag'\n";
485 if ( $v !~ /$newsfeeds'flags{$k}/ ) {
486 print "$file:$start: bad value `$v' for flag `$k'\n";
493 ## Warn about multiple feeds.
494 if ( !defined $sites{$site} ) {
495 $sites{$site} = $type;
496 } elsif ( $sites{$site} ne $type ) {
497 print "$file:$start: feed $site multiple conflicting feeds\n";
500 if ( $type =~ /[cpx]/ ) {
503 print "$file:$start: relative path for $site\n"
505 print "$file:$start: `$prog' is not executable for $site\n"
508 if ( $type eq "f" && $param =~ m@/@ ) {
510 $dir =~ s@(.*)/.*@$1@;
511 $dir = $paths{'batchdir'} . "/" . $dir
512 unless $dir =~ m@^/@;
513 print "$file:$start: directory `$dir' does not exist for $site\n"
517 ## If multiplex target not known, add to multiplex list.
518 push(@muxes, "$start: undefined multiplex `$param'")
519 if $type eq "m" && !defined $sites{$param};
522 ## Go through and make sure all referenced multiplex exist.
525 if /`(.*)'/ && !defined $sites{$1};
527 print "$file:0: warning you accept all incoming article distributions\n"
528 if !defined $sites{"ME"} || $me_empty;
539 #%overview_fmtheaders = (
561 local ($header, $mode, $sawfull);
564 input: while ( <IN> ) {
565 next input if &spacious($file, ++$line);
567 unless ( ($header, $mode) = /^([^:]+):([^:]*)$/ ) {
568 print "$file:$line: malformed line.\n";
572 #print "$file:$line: unknown header `$header'\n"
573 # if !defined $overview_fmtheaders{$header};
574 if ( $mode eq "full" ) {
576 } elsif ( $mode eq "" ) {
577 print "$file:$line: short header `$header' appears after full one\n"
580 print "$file:$line: unknown mode `$mode'\n";
593 local ($site, $fqdn, $flags, $f, $v);
595 input: while ( <IN> ) {
596 next input if &spacious($file, ++$line);
598 ## Ignore the size info for now.
599 unless ( ($site, $fqdn, $flags) =
600 /^([\w\-\.]+):([^:]*):[^:]*:([^:]*)$/ ) {
601 print "$file:$line: malformed line.\n";
604 print "$file:$line: FQDN is empty for `$site'\n"
607 next input if $flags eq "";
608 flag: foreach (split(/ /, $flags)) {
609 unless ( ($f, $v) = /^-([adrvtTpSP])(.*)$/ ) {
610 print "$file:$line: unknown argument for `$site'\n";
613 print "$file:$line: unknown argument to option `$f': $flags\n"
614 if ( $f eq "t" || $f eq "T" || $f eq "P") && $v !~ /\d+/;
627 local ($name, $pass);
629 input: while ( <IN> ) {
630 next input if &spacious($file, ++$line);
632 unless ( ($name, $pass) = /[\w\-\.]+:(.*):(.*)(:authinfo)?$/ ) {
634 print "$file:$line: malformed line.\n";
636 print "$file:$line: username/password must both be blank or non-blank\n"
637 if ( $name eq "" && $pass ne "" ) || ($name ne "" && $pass eq "");
654 ## Routines to check permissions
657 ## Given a file F, check its mode to be M, and its ownership to be by the
658 ## user U in the group G. U and G have defaults.
662 local ($f, $m, $u, $g) = ( @_, $newsuser, $newsgroup);
663 local (@sb, $owner, $group, $mode);
665 die "Internal error, undefined name in perm from ", (caller(0))[2], "\n"
667 die "Internal error, undefined mode in perm from ", (caller(0))[2], "\n"
671 print "$pfx$f:0: missing\n";
675 $owner = (getpwuid($sb[$ST_UID]))[0];
676 $group = (getgrgid($sb[$ST_GID]))[0];
677 $mode = $sb[$ST_MODE] & ~0770000;
679 ## Ignore setgid bit on directories.
683 if ( $owner ne $u ) {
684 print "$pfx$f:0: owned by $owner, should be $u\n";
685 print "chown $u $f\n"
688 if ( $group ne $g ) {
689 print "$pfx$f:0: in group $group, should be $g\n";
690 print "chgrp $g $f\n"
694 printf "$pfx$f:0: mode %o, should be %o\n", $mode, $m;
695 printf "chmod %o $f\n", $m
701 ## Return 1 if the Intersection of the files in the DIR and FILES is empty.
702 ## Otherwise, report an error for each illegal file, and return 0.
706 local ($dir, @files) = @_;
707 local (@in, %dummy, $i);
709 if ( !opendir(DH, $dir) ) {
710 print "$pfx$dir:0: can't open directory\n";
713 @in = grep($_ ne "." && $_ ne "..", readdir(DH));
722 foreach ( grep ($dummy{$_} == 0, @in) ) {
723 print "$pfx$dir:0: ERROR: illegal file `$_' in directory\n";
731 'archive', 'badnews', 'batchdir', 'ctlprogs', 'most_logs', 'newsbin',
732 'newsetc', 'newslib', 'oldlogs', 'rnewsprogs', 'spooltemp', 'spool', 'spoolnews'
735 'c7unbatch', 'decode', 'encode', 'gunbatch'
738 'archive', 'batcher', 'buffchan', 'convdate', 'cvtbatch', 'expire',
739 'filechan', 'getlist', 'grephistory', 'innconfval', 'innxmit',
740 'makehistory', 'nntpget', 'overchan', 'prunehistory', 'shlock',
744 'ctlinnd', 'expirerm', 'inncheck', 'innstat', 'innwatch',
745 'news.daily', 'nntpsend', 'scanlogs', 'sendbatch',
746 'tally.control', 'writelog',
747 'send-ihave', 'send-nntp', 'send-uucp'
749 #@newslib_private_read = (
753 ## The modes for the various programs.
755 'inews', @INEWSMODE@,
759 'rnews', @RNEWSMODE@,
762 ## Check the permissions of nearly every file in an INN installation.
766 local ($rnewsprogs) = $paths{'rnewsprogs'};
767 local ($newsbin) = $paths{'newsbin'};
768 local ($newslib) = $paths{'newslib'};
770 foreach ( @directories ) {
771 &checkperm($paths{$_}, 0755);
773 &checkperm($paths{'innddir'}, 0750);
774 foreach ( keys %prog_modes ) {
775 &checkperm($paths{$_}, $prog_modes{$_});
777 &checkperm($paths{'inndstart'}, 04550, 'root', $newsgroup);
778 foreach ( keys %paths ) {
779 &checkperm($paths{$_}, $modes{$_})
780 if defined $modes{$_};
782 &checkperm($paths{'history'}, 0644);
783 # Commented out for now since it depends on the history type.
784 #&checkperm($paths{'history'} . ".dir", 0644);
785 #&checkperm($paths{'history'} . ".index", 0644);
786 #&checkperm($paths{'history'} . ".hash", 0644);
787 #foreach ( @newslib_private_read ) {
788 # &checkperm("$newslib/$_", 0440);
790 foreach ( @newsbin_private ) {
791 &checkperm("$newsbin/$_", 0550);
793 foreach ( @newsbin_public ) {
794 &checkperm("$newsbin/$_", 0555);
796 foreach ( @rnews_programs ) {
797 &checkperm("$rnewsprogs/$_", 0555);
800 ## Also make sure that @rnews_programs are the *only* programs in there;
801 ## anything else is probably someone trying to spoof rnews into being bad.
802 &intersect($rnewsprogs, @rnews_programs);
809 ## Parsing, main routine.
817 print "Usage error: @_.\n";
820 $program [-v] [-noperm] [-pedantic] [-perms [-fix] ] [-a|file...]
821 File to check may be followed by \"=path\" to use the specified path. All
822 files are checked if -a is used or if -perms is not used. Files that may
824 foreach ( sort(keys %checklist) ) {
847 arg: foreach ( @ARGV ) {
873 &Usage("Unknown flag `$_'");
875 if ( ($k, $v) = /(.*)=(.*)/ ) {
876 &Usage("Can't check `$k'")
877 if !defined $checklist{$k};
882 &Usage("Can't check `$_'")
883 if !defined $checklist{$_};
887 &Usage("Can't use `-fix' without `-perm'")
889 &Usage("Can't use `-noperm' with `-perm'")
890 if $noperms && $perms;
891 $pfx = $fix ? '# ' : '';
893 @todo = grep(defined $checklist{$_}, sort(keys %paths))
894 if $all || (scalar(@todo) == 0 && ! $perms);
902 action: foreach $workfile ( @todo ) {
903 $file = $paths{$workfile};
905 print "$file:0: file missing\n";
908 print "Looking at $file...\n"
910 if ( !open(IN, $file) ) {
911 print "$pfx$workfile:0: can't open $!\n";
914 &checkperm($file, $modes{$workfile})
915 if $noperms == 0 && !$perms && defined $modes{$workfile};
917 eval "&$checklist{$workfile}" || warn "$@";