#!/usr/bin/perl use strict; use Socket; use Errno qw(EACCES EADDRNOTAVAIL EADDRINUSE); my ($verbose, $wait, $diff); my ($list_config, $list_log, $list_network, $exit_after_parse); my ($skip_netsanity); my $fn = "httpd.conf"; my $basedir = "/usr/local/packages/apache"; my @fns = ( ); # List of ->{ file, server } my @configs = (); my %logs = (); # We also collate bind, port and listen statements, as well as VirtualHost names. my @listens = (); # ( {addr(unclean), inet, port, file} ) my %listens = (); # maps cleaned-up addr:port -> [ @listen entry ] my @nvhosts = (); # ( unclean addr ) my %nvhosts = (); # ( clean addr:port -> @nvhosts entry ) my @vhosts = (); # vhost addr:port pairs and corresponding pointers my @vhostservers = (); # array of vhost hash refs. # They're "currentserver" things... the $vhosts[n]->{'server'} # points to a hash in here. my $mainserver = {}; # the main, non-vhost server sub ex($) { my $fn = shift; if ($fn =~ /^\"(.*)\"$/) { $fn = $1; } return $fn if ($fn =~ /^\//); return $basedir . "/" . $fn; } my $list_separator = "\n"; my $exitvalue = 0; while (@ARGV) { my $arg = shift @ARGV; if ($arg eq "-v") { $verbose = 1; print "Setting verbose flag\n"; } elsif ($arg eq "-w") { $wait = 1; print "Setting wait flag\n" if $verbose; } elsif ($arg eq "-W") { $wait = $diff = 1; print "Setting diff flag\n" if $verbose; } elsif ($arg eq "-b") { $basedir = shift @ARGV; print "New ServerRoot $basedir\n" if $verbose; } elsif ($arg eq "-0") { $list_separator = "\0"; $exit_after_parse = 1; print "Null list_separator\n" if $verbose; } elsif ($arg eq "-c") { $list_config = 1; print "Setting list_config\n" if $verbose; } elsif ($arg eq "-C") { $list_config = $exit_after_parse = 1; print "Setting list_config and exit_after_parse\n" if $verbose; } elsif ($arg eq "-l") { $list_log = 1; print "Setting list_log\n" if $verbose; } elsif ($arg eq "-L") { $list_log = $exit_after_parse = 1; print "Setting list_log and exit_after_parse\n" if $verbose; } elsif ($arg eq "-n") { $list_network = 1; print "Setting list_network\n" if $verbose; } elsif ($arg eq "-N") { $list_network = $exit_after_parse = 1; print "Setting list_network and exit_after_parse\n" if $verbose; } elsif ($arg eq "-T") { $skip_netsanity ++; print "Incrementing skip_netsanity\n" if $verbose; } elsif ($arg eq "-x") { $exit_after_parse = 1; print "Setting exit_after_parse\n" if $verbose; } elsif ($arg eq "-h") { print "Usage: $0 [-h | -v | -w | -b serverroot | -W | \n"; print "\t\t-c | -C | -l | -L | -0 | -x ] conffile1 ...\n"; print "\t-h\tShow usage\n"; print "\t-v\tBe verbose\n"; print "\t-w\tShow file sizes after a pause\n"; print "\t-b\tSet ServerRoot\n"; print "\t-W\tShow file differences after a pause (implies -w)\n"; print "\t-c\tShow all configuration files\n"; print "\t-C\tEquivalent to -c -x\n"; print "\t-l\tList all log files - incompatible with -c\n"; print "\t-L\tEquivalent to -l -x\n"; print "\t-0\tMake -c, -l compatible with xargs -0\n"; print "\t-n\tSummarise Listen and VirtualHost settings\n"; print "\t-N\tEquivalent to -n -x\n"; print "\t-T\tSkip active tests for network config sanity\n"; print "\t-T -T\tSkip all tests for network config sanity\n"; print "\t-x\tExit after parsing config files\n"; exit 0; } else { $arg = ex($arg); push @fns, { 'file' => $arg, 'server' => $mainserver}; print "Adding config file $arg\n" if $verbose; } } if ($list_config + $list_log + $list_network > 1) { print STDERR "Only one of -c, -l and -n admissible\n"; exit; } push @fns, { 'file' => ex("conf/httpd.conf"), 'server' => $mainserver } unless @fns; sub dirname($) { my $fn = shift; $fn =~ /^(.*)\/[^\/]*$/; return $1; } sub checkpath($) { my $fn = shift; print "Checking path $fn\n" if $verbose; unless (stat $fn) { if (stat dirname($fn)) { print " Reachable but doesn't exist: $fn\n" if $verbose; return 0; } return -1; } return -s _; } sub process_config($$) { my ($fn, $entryserver) = @_; my $currentserver = $entryserver; push @configs, $fn; my $line; unless ( -e $fn ) { print STDERR "****DOESN'T EXIST: $fn\n"; $exitvalue |= 2; return; } unless ( -f $fn ) { print STDERR "****NOT A FILE: $fn\n"; $exitvalue |= 2; return; } unless (open (IN, "<$fn")) { print STDERR "****CANNOT OPEN $fn\n"; $exitvalue |= 2; return; } while ($line = ) { $line =~ s/#.*//; $line =~ s/^\s*//; $line =~ s/\s*$//; next if $line eq ""; if ($line =~ /^ServerRoot\s+\"?(.*?)\"?$/i) { $basedir = $1; print " Changing ServerRoot to $basedir\n" if $verbose; next; } if ($line =~ /^Include\s+\"?(.*?)\"?$/i) { my $nfn = ex($1); print " Adding included file $nfn\n" if $verbose; push @fns, {'file' => $nfn, 'server' => $currentserver}; next; } my @words = split( /\s+/, $line ); my $lfn; if ($words[0] =~ /^CustomLog$/i) { $lfn = ex($words[1]); } elsif ($words[0] =~ /^TransferLog$/i) { $lfn = ex($words[1]); } elsif ($words[0] =~ /^ErrorLog$/i) { $lfn = ex($words[1]); } elsif ($words[0] =~ /^RewriteLog$/i) { $lfn = ex($words[1]); } elsif ($words[0] =~ /^SSLLog$/i) { $lfn = ex($words[1]); } elsif ($words[0] =~ /^LogFormat$/i) { next; } elsif ($words[0] =~ /^SSLPassPhraseDialog$/i) { next; } elsif ($words[0] =~ /^SSLLogLevel$/i) { next; } elsif ($words[0] =~ /^RewriteLogLevel$/i) { next; } elsif ($words[0] =~ /^LogLevel$/i) { next; } elsif (($words[0] =~ /Log/i) && ($words[0] =~ /Level/i)) { print " Discarding on a whim: $line\n" if $verbose; next; } elsif ($words[0] =~ /^ProxyRequests$/i && $words[1] !~ /^off$/i) { print STDERR "****SEE ME AFTER SCHOOL. ProxyRequests line in apache conf: $fn\n"; $exitvalue |= 4; next; } elsif ($words[0] =~ /^BindAddress$/i) { print STDERR "****BindAddress deprecated, use Listen: $fn\n"; next; } elsif ($words[0] =~ /^Port$/i) { print STDERR "****Port deprecated, use Listen: $fn\n"; next; } elsif ($words[0] =~ /^Listen$/i) { push @listens, {'addr' => $words[1], 'file' => $fn, 'line' => $.}; next; } elsif ($words[0] =~ /^NameVirtualHost$/i) { push @nvhosts, {'addr' => $words[1], 'file' => $fn, 'line' => $.}; next; } elsif ($words[0] =~ /^ if ($currentserver != $mainserver) { print STDERR "****SYNTAX ERROR: Nested in $fn\n"; $exitvalue |= 64; } print " A VirtualHost in $fn\n" if $verbose; $currentserver = {}; # A new hash push @vhostservers, $currentserver; shift @words; while (@words) { if (scalar @words == 1) { $words[0] =~ s/>?$//; } push @vhosts, {'addr' => shift @words, 'file' => $fn, 'server' => $currentserver, 'line' => $.}; } next; } elsif ($words[0] =~ /^<\/VirtualHost>$/i) { if ($currentserver == $mainserver) { print STDERR "****Closing without opening in $fn\n"; $exitvalue |= 64; } $currentserver = $mainserver; next; } elsif ($words[0] =~ /^ServerName$/i) { if (exists $currentserver->{'names'}) { print STDERR "**** possibly multiple ServerName in $fn\n"; } push @{$currentserver->{'names'}}, $words[1]; print " Adding ServerName $words[1]\n" if $verbose; next; } elsif ($words[0] =~ /^ServerAlias$/i) { push @{$currentserver->{'names'}}, $words[1]; print " Adding ServerAlias $words[1]\n" if $verbose; next; } else { next unless ($words[0] =~ /log/i); $lfn = ex($words[1]); print " Guessing $lfn from $line\n" if $verbose; } my $len = checkpath ($lfn); if ($len < 0) { print STDERR "****COULD NOT REACH $lfn ($fn)\n"; $exitvalue |= 1; next; } $logs{$lfn} = $len; } close(IN); if ($currentserver != $entryserver) { print STDERR "****Warning: closing missing before end of $fn\n"; $exitvalue |= 64; $currentserver = $mainserver; } } # Process files (and directories, and wildcards) while (@fns) { $fn = shift @fns; print "Expanding config file $fn->{'file'}\n" if $verbose; while(my $single = glob($fn->{'file'})) { print " Looking in config file $single\n" if $verbose; unless (-d $single) { process_config $single, $fn->{'server'}; next; } # We do a recursive descent of the directory... print " Adding directory contents of $single...\n" if $verbose; while (my $nx = glob("$single/*")) { print " ... $nx\n" if $verbose; push @fns, {'file' => $nx, 'server' => $fn->{'server'}}; } } } # Check the sanity of Listen directives. sub listen_parse($) { my $addr = shift; my @parts = split(/:/, $addr); if (scalar @parts == 1) { # Just a port if ($parts[0] =~ /^\d+$/) { unshift @parts, "0.0.0.0"; } } elsif (scalar @parts != 2) { print STDERR "****Listen address not in recognised format: $addr\n"; $exitvalue |= 8; @parts = ("0.0.0.0", 0); } my ($host, $port) = @parts; my $inet = inet_aton($host); if (not $inet) { print STDERR "****Listen address cannot evaluate host: $addr, $!\n"; $exitvalue |= 8; return "0.0.0.0", INADDR_ANY, 0; } return inet_ntoa($inet), $inet, $port; } sub nvh_parse($) { my $addr = shift; my @parts = split(/:/, $addr); if (scalar @parts == 1) { if ($parts[0] =~ /^\d+$/) { # Just a port? unshift @parts, "*"; } else { # A name only? push @parts, "*"; } } elsif (scalar @parts != 2) { print STDERR "****NameVirtualHost address not in recognised format: $addr\n"; $exitvalue |= 8; return '-', '-'; } my ($host, $port) = @parts; if ($host eq "*") { return "*", $port; } my $inet = inet_aton($host); if (not $inet) { print STDERR "****NameVirtualHost address cannot evaluate host: $addr, $!\n"; $exitvalue |= 8; return "-", '-'; # Won't match anything } return inet_ntoa($inet), $port; } sub vh_parse($) { my $addr = shift; my @parts = split(/:/, $addr); if (scalar @parts == 1) { if ($parts[0] =~ /^\d+$/) { # Just a port? unshift @parts, "*"; } else { # A name only? print STDERR "****Warning: VirtualHost should specify a port or a wildcard: $addr\n"; push @parts, "*"; } } elsif (scalar @parts != 2) { print STDERR "****VirtualHost address not in recognised format: $addr\n"; $exitvalue |= 8; return '-', '-'; } my ($host, $port) = @parts; if ($host eq "*" or $host eq "_default_") { return $host, $port; } my $inet = inet_aton($host); if (not $inet) { print STDERR "****VirtualHost address cannot evaluate host: $addr, $!\n"; $exitvalue |= 8; return "-", '-'; # Won't match anything } return inet_ntoa($inet), $port; } sub nvh_match($$$) { my ($addr, $port, $tomatch) = @_; my ($maddr, $mport) = split(/:/, $tomatch); return 0 if $addr eq '-'; return 0 if ($port ne "*" and $port ne $mport); return 1 if $addr eq '*'; return 1 if $addr eq $maddr; # We match also the "0.0.0.0" address IFF the $addr # actually exists on the computer, otherwise error. return 0 if $maddr ne '0.0.0.0'; my $inet = inet_aton($addr) or return 0; return 1 if $skip_netsanity; my $error = validate_inet_port($inet, 0); if ($error eq "address") { print STDERR "****Warning: NameVirtualHost $addr:$port doesn't appear to be on this machine\n"; return 0; } return 1; } sub vh_match($$$) { my ($addr, $port, $tomatch) = @_; my ($maddr, $mport) = split(/:/, $tomatch); print " vh_match: vhost is $addr:$port; listen is $maddr:$mport\n" if $verbose; return 0 if $addr eq '-'; return 0 if ($port ne "*" and $port ne $mport); return 1 if $addr eq '*' or $addr eq '_default_'; return 1 if $addr eq $maddr; # We match also the "0.0.0.0" address IFF the $addr # actually exists on the computer, otherwise error. return 0 if $maddr ne '0.0.0.0'; my $inet = inet_aton($addr) or return 0; return 1 if $skip_netsanity; my $error = validate_inet_port($inet, 0); if ($error eq "address") { print STDERR "****Warning: VirtualHost $addr:$port doesn't appear to be on this machine\n"; return -1; } return 1; } sub vh_nvh_match($$$$) { my ($vhip, $vhport, $nvhip, $nvhport) = @_; return 0 if $vhip eq '-' or $nvhip eq '-'; # _default_ only applies to IP-based VHs return 0 if $vhip eq '_default_'; return 0 if $nvhip eq '_default_'; if ($vhport ne '*' and $nvhport ne '*' and $vhport ne $nvhport) { return 0; } my $precise = 0; if ($vhport eq '*' and $nvhport ne '*') { $precise = 1; } if ($vhport ne '*' and $nvhport eq '*') { $precise = 1; } if ($vhip eq '*') { return 1 + $precise; } if ($nvhip eq '*') { return 1 + $precise; } if ($vhip eq $nvhip) { return 1 + $precise; } return 0; } sub validate_inet_port($$) { my ($inet, $port) = @_; my $error; my $proto = getprotobyname('tcp'); socket(Server, PF_INET, SOCK_STREAM, $proto) || return "socket"; setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || return "setsockopt"; bind(Server, sockaddr_in($port, $inet)) || { $error = 0 + $! }; close(Server); if ($error == EACCES) { return "permission"; } # probably ok if ($error == EADDRINUSE) { return "in use"; } # probably ok if ($error == EADDRNOTAVAIL) { return "address"; } # bad return $error; } sub bind_attempt($$$$$) { my ($type, $address, $addr, $inet, $port) = @_; # We don't attempt the bind if skip_netsanity has been specified at all return if $skip_netsanity; my $error = validate_inet_port($inet, $port); my $needcheck; if ($error eq "permission") { print "Warning: permission to bind to $address needed.\n" if $verbose; $needcheck = 1; } elsif ($error eq "in use") { # It's probably an OK IP address, keep trying print "Warning: $address in use. Probably ok, trying general port\n" if $verbose; $needcheck = 1; } elsif ($error eq "address") { # A problem. print STDERR "****Configuration error: $type $address, address unavailable\n"; $exitvalue != 32; } elsif ($error) { # Dunno, report print STDERR "****Unknown error: $error, from $type $address\n"; $needcheck = 1; } next unless ($needcheck); $error = validate_inet_port($inet, 0); # Try the address, any port if ($error eq "permission") { print STDERR "****Warning: permission to bind to $addr denied.\n"; } elsif ($error eq "in use") { # It's probably an OK IP address, keep trying print STDERR "****Warning: IP address $addr in use when rechecking $type $address\n"; } elsif ($error eq "address") { # A problem. print STDERR "****Configuration error: $type $address, IP $addr unavailable\n"; $exitvalue != 32; } elsif ($error) { # Dunno, report print STDERR "****Unknown error: $error, from $type $address any-port check\n"; } else { print " General port bind succeeds for $type $addr\n" if $verbose; } } # We don't attempt the lookups if skip_netsanity has been specified twice unless ($skip_netsanity > 1) { foreach $fn (@listens) { my ($addr, $inet, $port) = listen_parse($fn->{'addr'}); print "Listen parsing: addr = $addr, port = $port\n" if $verbose; $fn->{'inet'} = $inet; $fn->{'port'} = $port; push @{$listens{"$addr:$port"}}, $fn; } # We've got unique pairs of addr:port now. Run through each in turn. # Make sure they don't overlap: this becomes an error. foreach my $address (keys %listens) { if (scalar @{$listens{$address}} != 1) { print STDERR "****Duplicate binds to $address:\n"; foreach my $doh (@{$listens{$address}}) { print STDERR "**** $doh->{'addr'} in $doh->{'file'}:$doh->{'line'}\n"; } $exitvalue |= 16; } # Ok, we don't try to bind the port, because apache might # already have it. my $addr = ${$listens{$address}}[0]->{'addr'}; my $inet = ${$listens{$address}}[0]->{'inet'}; my $port = ${$listens{$address}}[0]->{'port'}; bind_attempt("Listen", $address, $addr, $inet, $port); } # To-do here: duplicate the httpd -S work: check that NameVirtualHost # lines correspond to Listens; check that VirtualHost addresses correspond # to Listen or NVH settings. # Expand NVH declarations. Check for orphans against Listen. foreach my $nvh (@nvhosts) { my ($addr, $port) = nvh_parse($nvh->{'addr'}); $nvh->{'ip'} = $addr; $nvh->{'port'} = $port; if (exists $nvhosts{"$addr:$port"}) { print STDERR "****Warning: duplicate NameVirtualHost $nvh->{'addr'} matches setting for $addr:$port\n"; print STDERR "**** This occurrance: $nvh->{'file'}:$nvh->{'line'}\n"; print STDERR "**** Previous occurrance: $nvhosts{\"$addr:$port\"}->{'file'}:$nvhosts{\"$addr:$port\"}->{'line'}\n"; } else { $nvhosts{"$addr:$port"} = $nvh; } # addr and port are either numeric format or "*" my $found; print " Processing nvh of $nvh->{'addr'} ($addr:$port)\n" if $verbose; foreach my $addrport (keys %listens) { if (nvh_match($addr, $port, $addrport)) { $found = 1; print " Matching Listen $addrport found\n" if $verbose; } } unless ($found) { print STDERR "****Warning: ORPHAN NameVirtualHost $nvh->{'addr'} in $nvh->{'file'}:$nvh->{'line'} - no Listen\n"; $nvh->{'error'} = 'orphan'; } } # Expand VHosts. foreach my $vh (@vhosts) { # Check VHosts for NameVirtualHost matches # Where matches exist, mark the corresponding NVH as used. my ($ip, $port) = vh_parse($vh->{'addr'}); $vh->{'ip'} = $ip; $vh->{'port'} = $port; print " Processing vh of $vh->{'addr'} ($ip:$port)\n" if $verbose; # ip is either numeric, _default_ or * # port is either numeric or * my $found; if ($ip ne '_default_') { # _default_ is only used for IP-based vhosts foreach my $nvh (@nvhosts) { # Does my ip:port match that of the nvh? if (vh_nvh_match($ip, $port, $nvh->{'ip'}, $nvh->{'port'})) { $found = 1; push @{$nvh->{'used'}}, $vh; print " VHost is name-based\n" if $verbose; } } } unless ($found) { # Otherwise, the VHost is IP-based: print " VHost is ip-based\n" if $verbose; my $inet; if ($ip eq '_default_' or $ip eq '*') { $inet = INADDR_ANY; # Fix the value, it's equivalent for testing } else { $inet = inet_aton($ip); } # check for matching Listen print " vh is IP-based: checking Listens\n" if $verbose; foreach my $addrport (keys %listens) { my $error = vh_match($ip, $port, $addrport); if ($error == 1) { $found = 1; print " Matching Listen $addrport found\n" if $verbose; } elsif ($error == -1) { last; } } unless ($found) { print STDERR "****Warning: ORPHAN VirtualHost $vh->{'addr'} in $vh->{'file'}:$vh->{'line'} - no Listen\n"; } # check the IP is actually on the machine next if $skip_netsanity; print " vh is IP-based: checking interfaces\n" if $verbose; bind_attempt("VirtualHost", $vh->{'addr'}, $ip, $inet, $port); } } my %name_based = (); # List of name-based vhosts foreach my $nvh (@nvhosts) { # Check for unused NVHs and warn about them. if (exists $nvh->{'used'}) { # Check for overlapping servernames on used NVHs. print " Examining ServerName for NVH $nvh->{'addr'}\n" if $verbose; my $used = $nvh->{'used'}; my %nameset = (); # { name -> first vh } foreach my $vh (@$used) { $name_based{$vh} = 1; print " vhost from $vh->{'file'}\n" if $verbose; my $names = $vh->{'server'}->{'names'}; foreach my $name (@$names) { if (exists $nameset{$name}) { for my $othervh (@{$nameset{$name}}) { if (vh_nvh_match($vh->{'ip'}, $vh->{'port'}, $othervh->{'ip'}, $othervh->{'port'}) == 1) { print STDERR "****Warning: overlapping NameVirtualHost names for $nvh->{'addr'}\n"; print STDERR "**** Name: $name\n"; print STDERR "**** Here: $vh->{'ip'}:$vh->{'port'} $vh->{'file'}:$vh->{'line'}\n"; print STDERR "**** Here: $othervh->{'ip'}:$othervh->{'port'} $othervh->{'file'}:$othervh->{'line'}\n"; } } } push @{$nameset{$name}}, $vh; print " name $name\n" if $verbose; } } } else { print STDERR "****Warning: NameVirtualHost $nvh->{'addr'} uncovered by VirtualHost in $nvh->{'file'}:$nvh->{'line'}\n"; } } # Finally, check for overlapping ip-based VHosts. my %ip_based = (); # { bind-string -> first vhost } foreach my $vh (@vhosts) { next if exists $name_based{$vh}; my ($ip, $port) = ($vh->{'ip'}, $vh->{'port'}); if (exists $ip_based{"$ip:$port"}) { print STDERR "****Warning: overlapping IP-based VHosts for $ip:$port\n"; print STDERR "**** Here: $vh->{'file'}:$vh->{'line'}\n"; print STDERR "**** Duplicate: $ip_based{\"$ip:$port\"}->{'file'}:$ip_based{\"$ip:$port\"}->{'line'}\n"; } else { $ip_based{"$ip:$port"} = $vh; } } } if ($list_config) { foreach $fn (@configs) { print $fn, $list_separator; } } if ($list_log) { foreach $fn (sort keys %logs) { print $fn, $list_separator; } } if ($list_network) { foreach $fn (@listens) { print "LISTEN ", $fn->{'addr'}, " ", $fn->{'file'}, $list_separator; } foreach $fn (@nvhosts) { print "NVH ", $fn->{'addr'}, " ", $fn->{'file'}, $list_separator; } foreach $fn (@vhosts) { print "VHOST ", $fn->{'addr'}, " ", $fn->{'file'}, $list_separator; } } exit $exitvalue if $exit_after_parse; print "\nPrior to waiting...\n" if $wait; print "Size\t\tLog file\n"; foreach $fn (sort keys %logs) { print "$logs{$fn}\t\t$fn\n"; } exit unless $wait; while (1) { print "Hit return to check file sizes again, type q or ^C to quit.\n"; exit if <> =~ /^q/; my %dump = (); print "After waiting...\n"; print "Size\t\tLog file\n"; foreach $fn (sort keys %logs) { my $len = -s $fn; if ($len > $logs{$fn}) { $dump{$fn} = 1; print "*"; } print "$len\t\t$fn\n"; } next unless $diff; foreach $fn (sort keys %dump) { print "\n---ADDITIONAL LINES IN $fn---\n"; next unless open(IN, "<$fn"); seek(IN, $logs{$fn}, 0); my $line; while ($line = ) { print $line; } $logs{$fn} = -s IN; close(IN); print "---END ADDITIONAL LINES IN $fn---\n"; } }