Merge pull request #2865 from webmin/dev/fix-apache-test-dns-lookup

Fix Apache config test stalling on hostname DNS lookups
This commit is contained in:
Jamie Cameron
2026-10-02 16:35:38 -07:00
committed by GitHub
2 changed files with 305 additions and 22 deletions

View File

@@ -1991,29 +1991,56 @@ return @rv;
# or undef.
sub test_config
{
if ($httpd_modules{'core'} >= 1.301) {
# Test the configuration with the available command
local $cmd;
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl
$cmd = "\"$config{'apachectl_path'}\" configtest";
}
else {
# Test with httpd
local $httpd = &find_httpd();
$cmd = "\"$httpd\" -d \"$config{'httpd_dir'}\" -t";
if ($config{'httpd_conf'}) {
$cmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach $d (&get_httpd_defines()) {
$cmd .= " -D$d";
}
}
local $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ /(syntax|Checking).*\s+ok/i) {
return $out;
return undef if ($httpd_modules{'core'} < 1.301);
my $okre = qr/(syntax|Checking).*\s+ok/i;
# Build the httpd command from the configured paths
my $httpd = &find_httpd();
my $httpdcmd = "\"$httpd\" -d \"$config{'httpd_dir'}\"";
if ($config{'httpd_conf'}) {
$httpdcmd .= " -f \"$config{'httpd_conf'}\"";
}
foreach my $d (&get_httpd_defines()) {
$httpdcmd .= " -D$d";
}
# Pick the standard test command
my ($cmd, $apachectl);
if ($config{'test_apachectl'} &&
-x &translate_filename($config{'apachectl_path'})) {
# Test with apachectl, which may set up environment the config needs
$apachectl = "\"$config{'apachectl_path'}\"";
$cmd = "$apachectl configtest";
}
else {
# Test with httpd
$cmd = "$httpdcmd -t";
}
# Without a global ServerName, Apache looks up the system hostname in DNS
# while loading the config, which stalls the test when DNS is slow. So first
# try the test with a placeholder ServerName. Apache applies -C directives
# before reading the config files, so a ServerName that this check misses,
# such as one inside an IfModule section, still takes effect.
my $conf = &get_config();
if ($conf && !&find_directive("ServerName", $conf)) {
my $noname = "-C \"ServerName localhost\"";
my $out = &backquote_command(
($apachectl ? "$apachectl $noname -t" : "$httpdcmd $noname -t").
" 2>&1");
if ($apachectl && $httpd && $out =~ /no\s+longer\s+supported/i) {
# Red Hat's apachectl refuses extra arguments, and its
# configtest just runs httpd -t, so test with httpd instead
$out = &backquote_command("$httpdcmd $noname -t 2>&1");
}
return undef if ($out =~ $okre);
}
# Run the standard test. This also runs when the quick test failed, in case
# the extra arguments, rather than the config, caused the failure.
my $out = &backquote_command("$cmd 2>&1");
if ($out && $out !~ $okre) {
return $out;
}
return undef;
}

256
apache/t/test-config.t Normal file
View File

@@ -0,0 +1,256 @@
#!/usr/bin/perl
# Tests for test_config, which must check the Apache config without waiting
# on a DNS lookup of the system hostname.
use strict;
use warnings;
use Test::More;
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use Cwd qw(abs_path);
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..', '..'));
my $tmp = abs_path(tempdir(CLEANUP => 1));
my $webmin_config = File::Spec->catdir($tmp, 'webmin-config');
my $webmin_var = File::Spec->catdir($tmp, 'webmin-var');
my $bin = File::Spec->catdir($tmp, 'bin');
my $log = File::Spec->catfile($tmp, 'commands.log');
# Paths with spaces check that the configured paths are used and quoted
my $apache_root = File::Spec->catdir($tmp, 'apache root');
my $apache_conf = File::Spec->catfile($apache_root, 'main config.conf');
make_path($webmin_config, $webmin_var, "$webmin_config/apache",
"$webmin_var/apache", $apache_root, $bin);
# write_text(file, text, [mode])
# Writes a file, optionally setting its permissions
sub write_text
{
my ($file, $text, $mode) = @_;
open(my $fh, '>', $file) || die "Failed to write $file: $!";
print $fh $text;
close($fh) || die "Failed to close $file: $!";
chmod($mode, $file) || die "Failed to chmod $file: $!" if (defined($mode));
}
# Shell snippet that logs the program name and its arguments, one per field
my $log_args = <<'EOF';
{ printf '%s' "$FAKE_NAME"; for a in "$@"; do printf '|%s' "$a"; done;
printf '\n'; } >> "$FAKE_APACHE_LOG"
EOF
# Fake httpd, which passes or fails the test based on the environment
my $httpd = File::Spec->catfile($bin, 'httpd');
write_text($httpd, "#!/bin/sh\nFAKE_NAME=httpd\n".$log_args.<<'EOF', 0755);
if [ "$FAKE_APACHE_RESULT" = "error" ]; then
echo "AH00526: Syntax error on line 2 of test.conf:"
echo "Invalid command 'Bogus', perhaps misspelled"
exit 1
fi
echo "Syntax OK"
EOF
# Debian-style apache2ctl, which passes extra arguments to httpd
my $debian_ctl = File::Spec->catfile($bin, 'debian-ctl');
write_text($debian_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
case "\$*" in
configtest) exec "$httpd" -t ;;
*) exec "$httpd" "\$@" ;;
esac
EOF
# Red Hat-style apachectl, which refuses extra arguments
my $redhat_ctl = File::Spec->catfile($bin, 'redhat-ctl');
write_text($redhat_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
if [ "x\$2" != "x" ]; then
echo "Passing arguments to httpd using apachectl is no longer supported."
exit 1
fi
case "\$1" in
configtest|-t) exec "$httpd" -t ;;
esac
exit 2
EOF
# Wrapper that only understands configtest
my $strict_ctl = File::Spec->catfile($bin, 'strict-ctl');
write_text($strict_ctl,
"#!/bin/sh\nFAKE_NAME=apachectl\n".$log_args.<<"EOF", 0755);
case "\$1" in
configtest) exec "$httpd" -t ;;
esac
echo "usage: apachectl configtest"
exit 2
EOF
write_text(File::Spec->catfile($webmin_config, 'config'),
"os_type=debian-linux\n".
"os_version=12\n".
"real_os_type=Debian Linux\n".
"real_os_version=12\n");
write_text(File::Spec->catfile($webmin_config, 'miniserv.conf'),
"root=$root\n");
write_text(File::Spec->catfile($webmin_config, 'apache', 'config'),
"httpd_dir=$apache_root\n".
"httpd_path=$httpd\n".
"httpd_conf=$apache_conf\n".
"apachectl_path=$debian_ctl\n".
"httpd_version=2.4.57\n".
"test_apachectl=0\n".
"test_config=1\n");
$ENV{'WEBMIN_CONFIG'} = $webmin_config;
$ENV{'WEBMIN_VAR'} = $webmin_var;
$ENV{'FOREIGN_MODULE_NAME'} = 'apache';
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $root;
$ENV{'REMOTE_USER'} = 'root';
$ENV{'FAKE_APACHE_LOG'} = $log;
$ENV{'FAKE_APACHE_RESULT'} = 'ok';
unshift(@INC, $root);
require File::Spec->catfile($root, 'apache', 'apache-lib.pl');
{
no warnings 'once';
$main::httpd_modules{'core'} = 2.4;
}
# set_apache_conf(text)
# Writes the main Apache config file and clears the cached copy
sub set_apache_conf
{
my ($text) = @_;
write_text($apache_conf, $text);
main::flush_config_cache();
}
# A ServerName inside a virtual host is not global, so Apache still looks up
# the system hostname
my $no_global_name = "Listen 80\n".
"<VirtualHost *:80>\n".
" ServerName vhost.example\n".
"</VirtualHost>\n";
set_apache_conf($no_global_name);
# Expected log lines for the fake commands
my $noname = 'ServerName localhost';
my $direct_fast = join('|', 'httpd', '-d', $apache_root, '-f', $apache_conf,
'-C', $noname, '-t');
my $direct_std = join('|', 'httpd', '-d', $apache_root, '-f', $apache_conf,
'-t');
my $ctl_fast = join('|', 'apachectl', '-C', $noname, '-t');
my $ctl_std = 'apachectl|configtest';
my $httpd_fast = join('|', 'httpd', '-C', $noname, '-t');
my $httpd_std = 'httpd|-t';
# run_test(test-apachectl, [apachectl-path], result)
# Runs test_config with the given settings, and returns its result and the
# commands it ran
sub run_test
{
my ($use_ctl, $ctl, $result) = @_;
no warnings 'once';
$main::config{'test_apachectl'} = $use_ctl;
$main::config{'apachectl_path'} = $ctl if ($ctl);
$ENV{'FAKE_APACHE_RESULT'} = $result;
unlink($log);
my $err = main::test_config();
my @cmds;
if (open(my $fh, '<', $log)) {
chomp(@cmds = <$fh>);
close($fh);
}
return ($err, \@cmds);
}
subtest 'httpd test skips the hostname lookup' => sub {
my ($err, $cmds) = run_test(0, undef, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $direct_fast ],
'httpd runs once with configured paths and ServerName');
};
subtest 'httpd test reports config errors' => sub {
my ($err, $cmds) = run_test(0, undef, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $direct_fast, $direct_std ],
'failure is confirmed with the standard test');
};
subtest 'Debian apachectl passes ServerName to httpd' => sub {
my ($err, $cmds) = run_test(1, $debian_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $httpd_fast ],
'apachectl runs httpd with the placeholder ServerName');
};
subtest 'Debian apachectl reports config errors' => sub {
my ($err, $cmds) = run_test(1, $debian_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $ctl_fast, $httpd_fast, $ctl_std, $httpd_std ],
'failure is confirmed with apachectl configtest');
};
subtest 'Red Hat apachectl falls back to httpd' => sub {
my ($err, $cmds) = run_test(1, $redhat_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $direct_fast ],
'httpd is run directly when apachectl refuses arguments');
};
subtest 'Red Hat apachectl reports config errors' => sub {
my ($err, $cmds) = run_test(1, $redhat_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
unlike($err, qr/no longer supported/,
'apachectl refusal is not reported as a config error');
is_deeply($cmds, [ $ctl_fast, $direct_fast, $ctl_std, $httpd_std ],
'failure is confirmed with apachectl configtest');
};
subtest 'unknown apachectl falls back to configtest' => sub {
my ($err, $cmds) = run_test(1, $strict_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_fast, $ctl_std, $httpd_std ],
'standard test runs when apachectl rejects the arguments');
};
subtest 'global ServerName uses only the standard test' => sub {
set_apache_conf("ServerName www.example.com\n".$no_global_name);
my ($err, $cmds) = run_test(0, undef, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $direct_std ], 'httpd runs without a placeholder');
($err, $cmds) = run_test(1, $redhat_ctl, 'error');
like($err, qr/Syntax error on line 2/, 'error output is returned');
is_deeply($cmds, [ $ctl_std, $httpd_std ],
'apachectl configtest runs as before');
set_apache_conf($no_global_name);
};
subtest 'global ServerName in an included file is found' => sub {
# Webmin expands includes with glob(), which splits paths on spaces
my $inc = File::Spec->catfile($tmp, 'servername.conf');
write_text($inc, "ServerName www.example.com\n");
set_apache_conf("Include $inc\n".$no_global_name);
my ($err, $cmds) = run_test(1, $debian_ctl, 'ok');
is($err, undef, 'valid config passes');
is_deeply($cmds, [ $ctl_std, $httpd_std ],
'apachectl configtest runs as before');
unlink($inc);
set_apache_conf($no_global_name);
};
subtest 'old Apache versions are not tested' => sub {
no warnings 'once';
local $main::httpd_modules{'core'} = 1.3;
my ($err, $cmds) = run_test(0, undef, 'error');
is($err, undef, 'no error is reported');
is_deeply($cmds, [ ], 'no test command runs');
};
done_testing();