diff options
Diffstat (limited to 'debian/perl-framework/Apache-Test/lib/Apache/TestConfig.pm')
-rw-r--r-- | debian/perl-framework/Apache-Test/lib/Apache/TestConfig.pm | 2253 |
1 files changed, 2253 insertions, 0 deletions
diff --git a/debian/perl-framework/Apache-Test/lib/Apache/TestConfig.pm b/debian/perl-framework/Apache-Test/lib/Apache/TestConfig.pm new file mode 100644 index 0000000..ad0b02c --- /dev/null +++ b/debian/perl-framework/Apache-Test/lib/Apache/TestConfig.pm @@ -0,0 +1,2253 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +package Apache::TestConfig; + +use strict; +use warnings FATAL => 'all'; + +use constant WIN32 => $^O eq 'MSWin32'; +use constant OSX => $^O eq 'darwin'; +use constant CYGWIN => $^O eq 'cygwin'; +use constant NETWARE => $^O eq 'NetWare'; +use constant SOLARIS => $^O eq 'solaris'; +use constant AIX => $^O eq 'aix'; +use constant WINFU => WIN32 || NETWARE; +use constant COLOR => ($ENV{APACHE_TEST_COLOR} && -t STDOUT) ? 1 : 0; + +use constant DEFAULT_PORT => 8529; + +use constant IS_MOD_PERL_2 => + eval { require mod_perl2 } || 0; + +use constant IS_MOD_PERL_2_BUILD => IS_MOD_PERL_2 && + eval { require Apache2::Build && Apache2::Build::IS_MOD_PERL_BUILD() }; + +use constant IS_APACHE_TEST_BUILD => + grep { -e "$_/lib/Apache/TestConfig.pm" } + qw(Apache-Test . .. ../Apache-Test); + +use lib (); +use File::Copy (); +use File::Find qw(finddepth); +use File::Basename qw(dirname); +use File::Path (); +use File::Spec::Functions qw(catfile abs2rel splitdir canonpath + catdir file_name_is_absolute devnull); +use Cwd qw(fastcwd); +use Socket (); +use Symbol (); + +use Apache::TestConfigPerl (); +use Apache::TestConfigParse (); +use Apache::TestTrace; +use Apache::TestServer (); +use Apache::TestRun (); + +use vars qw(%Usage); + +%Usage = ( + top_dir => 'top-level directory (default is $PWD)', + t_dir => 'the t/ test directory (default is $top_dir/t)', + t_conf => 'the conf/ test directory (default is $t_dir/conf)', + t_logs => 'the logs/ test directory (default is $t_dir/logs)', + t_pid_file => 'location of the pid file (default is $t_logs/httpd.pid)', + t_conf_file => 'test httpd.conf file (default is $t_conf/httpd.conf)', + src_dir => 'source directory to look for mod_foos.so', + serverroot => 'ServerRoot (default is $t_dir)', + documentroot => 'DocumentRoot (default is $ServerRoot/htdocs', + port => 'Port [port_number|select] (default ' . DEFAULT_PORT . ')', + servername => 'ServerName (default is localhost)', + user => 'User to run test server as (default is $USER)', + group => 'Group to run test server as (default is $GROUP)', + bindir => 'Apache bin/ dir (default is apxs -q BINDIR)', + sbindir => 'Apache sbin/ dir (default is apxs -q SBINDIR)', + httpd => 'server to use for testing (default is $bindir/httpd)', + target => 'name of server binary (default is apxs -q TARGET)', + apxs => 'location of apxs (default is from Apache2::BuildConfig)', + startup_timeout => 'seconds to wait for the server to start (default is 60)', + httpd_conf => 'inherit config from this file (default is apxs derived)', + httpd_conf_extra=> 'inherit additional config from this file', + minclients => 'minimum number of concurrent clients (default is 1)', + maxclients => 'maximum number of concurrent clients (default is minclients+1)', + perlpod => 'location of perl pod documents (for testing downloads)', + proxyssl_url => 'url for testing ProxyPass / https (default is localhost)', + sslca => 'location of SSL CA (default is $t_conf/ssl/ca)', + sslcaorg => 'SSL CA organization to use for tests (default is asf)', + sslproto => 'SSL/TLS protocol version(s) to test', + libmodperl => 'path to mod_perl\'s .so (full or relative to LIBEXECDIR)', + defines => 'values to add as -D defines (for example, "VAR1 VAR2")', + (map { $_ . '_module_name', "$_ module name"} qw(cgi ssl thread access auth php)), +); + +my %filepath_conf_opts = map { $_ => 1 } + qw(top_dir t_dir t_conf t_logs t_pid_file t_conf_file src_dir serverroot + documentroot bindir sbindir httpd apxs httpd_conf httpd_conf_extra + perlpod sslca libmodperl); + +sub conf_opt_is_a_filepath { + my $opt = shift; + $opt && exists $filepath_conf_opts{$opt}; +} + +sub usage { + for my $hash (\%Usage) { + for (sort keys %$hash){ + printf " -%-18s %s\n", $_, $hash->{$_}; + } + } +} + +sub filter_args { + my($args, $wanted_args) = @_; + my(@pass, %keep); + + my @filter = @$args; + + if (ref($filter[0])) { + push @pass, shift @filter; + } + + while (@filter) { + my $key = shift @filter; + # optinal - or -- prefix + if (defined $key && $key =~ /^-?-?(.+)/ && exists $wanted_args->{$1}) { + if (@filter) { + $keep{$1} = shift @filter; + } + else { + die "key $1 requires a matching value"; + } + } + else { + push @pass, $key; + } + } + + return (\@pass, \%keep); +} + +my %passenv = map { $_,1 } qw{ + APACHE_TEST_APXS + APACHE_TEST_HTTPD + APACHE_TEST_GROUP + APACHE_TEST_USER + APACHE_TEST_PORT +}; + +sub passenv { + \%passenv; +} + +sub passenv_makestr { + my @vars; + + for (sort keys %passenv) { + push @vars, "$_=\$($_)"; + } + + "@vars"; +} + +sub server { shift->{server} } + +sub modperl_build_config { + + my $self = shift; + + my $server = ref $self ? $self->server : new_test_server(); + + # we can't do this if we're using httpd 1.3.X + # even if mod_perl2 is installed on the box + # similarly, we shouldn't be loading mp2 if we're not + # absolutely certain we're in a 2.X environment yet + # (such as mod_perl's own build or runtime environment) + if (($server->{rev} && $server->{rev} == 2) || + IS_MOD_PERL_2_BUILD || $ENV{MOD_PERL_API_VERSION}) { + eval { + require Apache2::Build; + } or return; + + return Apache2::Build->build_config; + } + + return; +} + +sub new_test_server { + my($self, $args) = @_; + Apache::TestServer->new($args || $self) +} + +# setup httpd-independent components +# for httpd-specific call $self->httpd_config() +sub new { + my $class = shift; + + my $args; + + $args = shift if $_[0] and ref $_[0]; + + $args = $args ? {%$args} : {@_}; #copy + + #see Apache::TestMM::{filter_args,generate_script} + #we do this so 'perl Makefile.PL' can be passed options such as apxs + #without forcing regeneration of configuration and recompilation of c-modules + #as 't/TEST apxs /path/to/apache/bin/apxs' would do + while (my($key, $val) = each %Apache::TestConfig::Argv) { + $args->{$key} = $val; + } + + my $top_dir = fastcwd; + $top_dir = pop_dir($top_dir, 't'); + # untaint as we are going to use it a lot later on in -T sensitive + # operations (.e.g @INC) + $top_dir = $1 if $top_dir =~ /(.*)/; + + # make sure that t/conf/apache_test_config.pm is found + # (unfortunately sometimes we get thrown into / by Apache so we + # can't just rely on $top_dir + lib->import($top_dir); + + my $thaw = {}; + #thaw current config + for (qw(conf t/conf)) { + last if eval { + require "$_/apache_test_config.pm"; + $thaw = 'apache_test_config'->new; + delete $thaw->{save}; + #incase class that generated the config was + #something else, which we can't be sure how to load + bless $thaw, 'Apache::TestConfig'; + }; + } + + if ($args->{thaw} and ref($thaw) ne 'HASH') { + #dont generate any new config + $thaw->{vars}->{$_} = $args->{$_} for keys %$args; + $thaw->{server} = $thaw->new_test_server; + $thaw->add_inc; + return $thaw; + } + + #regenerating config, so forget old + if ($args->{save}) { + for (qw(vhosts inherit_config modules inc cmodules)) { + delete $thaw->{$_} if exists $thaw->{$_}; + } + } + + my $self = bless { + clean => {}, + vhosts => {}, + inherit_config => {}, + modules => {}, + inc => [], + %$thaw, + mpm => "", + httpd_defines => {}, + vars => $args, + postamble => [], + preamble => [], + postamble_hooks => [], + preamble_hooks => [], + }, ref($class) || $class; + + my $vars = $self->{vars}; #things that can be overridden + + for (qw(save verbose)) { + next unless exists $args->{$_}; + $self->{$_} = delete $args->{$_}; + } + + $vars->{top_dir} ||= $top_dir; + + $self->add_inc; + + #help to find libmodperl.so + unless ($vars->{src_dir}) { + my $src_dir = catfile $vars->{top_dir}, qw(.. src modules perl); + + if (-d $src_dir) { + $vars->{src_dir} = $src_dir; + } else { + $src_dir = catfile $vars->{top_dir}, qw(src modules perl); + $vars->{src_dir} = $src_dir if -d $src_dir; + } + } + + $vars->{t_dir} ||= catfile $vars->{top_dir}, 't'; + $vars->{serverroot} ||= $vars->{t_dir}; + $vars->{documentroot} ||= catfile $vars->{serverroot}, 'htdocs'; + $vars->{perlpod} ||= $self->find_in_inc('pods') || + $self->find_in_inc('pod'); + $vars->{perl} ||= $^X; + $vars->{t_conf} ||= catfile $vars->{serverroot}, 'conf'; + $vars->{sslca} ||= catfile $vars->{t_conf}, 'ssl', 'ca'; + $vars->{sslcaorg} ||= 'asf'; + + if (!defined($vars->{sslproto})) { + require Apache::TestSSLCA; + + $vars->{sslproto} = Apache::TestSSLCA::sslproto(); + } + else { + $vars->{sslproto} ||= 'all'; + } + + $vars->{t_logs} ||= catfile $vars->{serverroot}, 'logs'; + $vars->{t_conf_file} ||= catfile $vars->{t_conf}, 'httpd.conf'; + $vars->{t_pid_file} ||= catfile $vars->{t_logs}, 'httpd.pid'; + + if (WINFU) { + for (keys %$vars) { + $vars->{$_} =~ s|\\|\/|g if defined $vars->{$_}; + } + } + + $vars->{scheme} ||= 'http'; + $vars->{servername} ||= $self->default_servername; + $vars->{port} = $self->select_first_port; + $vars->{remote_addr} ||= $self->our_remote_addr; + + $vars->{user} ||= $self->default_user; + $vars->{group} ||= $self->default_group; + $vars->{serveradmin} ||= $self->default_serveradmin; + + $vars->{minclients} ||= 1; + $vars->{maxclients_preset} = $vars->{maxclients} || 0; + # if maxclients wasn't explicitly passed try to + # prevent 'server reached MaxClients setting' errors + $vars->{maxclients} ||= $vars->{minclients} + 1; + + # if a preset maxclients valus is smaller than minclients, + # maxclients overrides minclients + if ($vars->{maxclients_preset} && + $vars->{maxclients_preset} < $vars->{minclients}) { + $vars->{minclients} = $vars->{maxclients_preset}; + } + + # for threaded mpms MaxClients must be a multiple of + # ThreadsPerChild (i.e. maxclients % minclients == 0) + # so unless -maxclients was explicitly specified use a double of + # minclients + $vars->{maxclientsthreadedmpm} = + $vars->{maxclients_preset} || $vars->{minclients} * 2; + + $vars->{proxy} ||= 'off'; + $vars->{proxyssl_url} ||= ''; + $vars->{defines} ||= ''; + + $self->{hostport} = $self->hostport; + $self->{server} = $self->new_test_server; + + return $self; + +} + +# figure out where httpd is and run extra config hooks which require +# knowledge of where httpd is +sub httpd_config { + my $self = shift; + + $self->configure_apxs; + $self->configure_httpd; + + my $vars = $self->{vars}; + unless ($vars->{httpd} or $vars->{apxs}) { + + # mod_perl 2.0 build (almost) always knows the right httpd + + # location (and optionally apxs). if we get here we can't + # continue because the interactive config can't work with + # mod_perl 2.0 build (by design) + if (IS_MOD_PERL_2_BUILD){ + my $mp2_build = $self->modperl_build_config(); + # if mod_perl 2 was built against the httpd source it + # doesn't know where to find apxs/httpd, so in this case + # fall back to interactive config + unless ($mp2_build->{MP_APXS}) { + die "mod_perl 2 was built against Apache sources, we " . + "don't know where httpd/apxs executables are, therefore " . + "skipping the test suite execution" + } + + # not sure what else could go wrong but we can't continue + die "something is wrong, mod_perl 2.0 build should have " . + "supplied all the needed information to run the tests. " . + "Please post lib/Apache2/BuildConfig.pm along with the " . + "bug report"; + } + + $self->clean(1); + + error "You must explicitly specify -httpd and/or -apxs options, " . + "or set \$ENV{APACHE_TEST_HTTPD} and \$ENV{APACHE_TEST_APXS}, " . + "or set your \$PATH to include the httpd and apxs binaries."; + Apache::TestRun::exit_perl(1); + + } + else { + debug "Using httpd: $vars->{httpd}"; + } + + $self->inherit_config; #see TestConfigParse.pm + $self->configure_httpd_eapi; #must come after inherit_config + + $self->default_module(cgi => [qw(mod_cgi mod_cgid)]); + $self->default_module(thread => [qw(worker threaded)]); + $self->default_module(ssl => [qw(mod_ssl)]); + $self->default_module(access => [qw(mod_access mod_authz_host)]); + $self->default_module(auth => [qw(mod_auth mod_auth_basic)]); + $self->default_module(php => [qw(sapi_apache2 mod_php4 mod_php5)]); + + $self->{server}->post_config; + + return $self; +} + +sub default_module { + my($self, $name, $choices) = @_; + + my $mname = $name . '_module_name'; + + unless ($self->{vars}->{$mname}) { + ($self->{vars}->{$mname}) = grep { + $self->{modules}->{"$_.c"}; + } @$choices; + + $self->{vars}->{$mname} ||= $choices->[0]; + } + + $self->{vars}->{$name . '_module'} = + $self->{vars}->{$mname} . '.c' +} + +sub configure_apxs { + my $self = shift; + + $self->{APXS} = $self->default_apxs; + + return unless $self->{APXS}; + + $self->{APXS} =~ s{/}{\\}g if WIN32; + + my $vars = $self->{vars}; + + $vars->{bindir} ||= $self->apxs('BINDIR', 1); + $vars->{sbindir} ||= $self->apxs('SBINDIR'); + $vars->{target} ||= $self->apxs('TARGET'); + $vars->{conf_dir} ||= $self->apxs('SYSCONFDIR'); + + if ($vars->{conf_dir}) { + $vars->{httpd_conf} ||= catfile $vars->{conf_dir}, 'httpd.conf'; + } +} + +sub configure_httpd { + my $self = shift; + my $vars = $self->{vars}; + + debug "configuring httpd"; + + $vars->{target} ||= (WIN32 ? 'Apache.EXE' : 'httpd'); + + unless ($vars->{httpd}) { + #sbindir should be bin/ with the default layout + #but its eaiser to workaround apxs than fix apxs + for my $dir (map { $vars->{$_} } qw(sbindir bindir)) { + next unless defined $dir; + my $httpd = catfile $dir, $vars->{target}; + next unless -x $httpd; + $vars->{httpd} = $httpd; + last; + } + + $vars->{httpd} ||= $self->default_httpd; + } + + if ($vars->{httpd}) { + my @chunks = splitdir $vars->{httpd}; + #handle both $prefix/bin/httpd and $prefix/Apache.exe + for (1,2) { + pop @chunks; + last unless @chunks; + $self->{httpd_basedir} = catfile @chunks; + last if -d "$self->{httpd_basedir}/bin"; + } + } + + #cleanup httpd droppings + my $sem = catfile $vars->{t_logs}, 'apache_runtime_status.sem'; + unless (-e $sem) { + $self->clean_add_file($sem); + } +} + +sub configure_httpd_eapi { + my $self = shift; + my $vars = $self->{vars}; + + #deal with EAPI_MM_CORE_PATH if defined. + if (defined($self->{httpd_defines}->{EAPI_MM_CORE_PATH})) { + my $path = $self->{httpd_defines}->{EAPI_MM_CORE_PATH}; + + #ensure the directory exists + my @chunks = splitdir $path; + pop @chunks; #the file component of the path + $path = catdir @chunks; + unless (file_name_is_absolute $path) { + $path = catdir $vars->{serverroot}, $path; + } + $self->gendir($path); + } +} + +sub configure_proxy { + my $self = shift; + my $vars = $self->{vars}; + + #if we proxy to ourselves, must bump the maxclients + if ($vars->{proxy} =~ /^on$/i) { + unless ($vars->{maxclients_preset}) { + $vars->{minclients}++; + $vars->{maxclients}++; + } + $vars->{proxy} = $self->{vhosts}->{'mod_proxy'}->{hostport}; + return $vars->{proxy}; + } + + return undef; +} + +# adds the config to the head of the group instead of the tail +# XXX: would be even better to add to a different sub-group +# (e.g. preamble_first) of only those that want to be first and then, +# make sure that they are dumped to the config file first in the same +# group (e.g. preamble) +sub add_config_first { + my $self = shift; + my $where = shift; + unshift @{ $self->{$where} }, $self->massage_config_args(@_); +} + +sub add_config_last { + my $self = shift; + my $where = shift; + push @{ $self->{$where} }, $self->massage_config_args(@_); +} + +sub massage_config_args { + my $self = shift; + my($directive, $arg, $data) = @_; + my $args = ""; + + if ($data) { + $args = "<$directive $arg>\n"; + if (ref($data) eq 'HASH') { + while (my($k,$v) = each %$data) { + $args .= " $k $v\n"; + } + } + elsif (ref($data) eq 'ARRAY') { + # balanced (key=>val) list + my $pairs = @$data / 2; + for my $i (0..($pairs-1)) { + $args .= sprintf " %s %s\n", $data->[$i*2], $data->[$i*2+1]; + } + } + else { + $data=~s/\n(?!\z)/\n /g; + $args .= " $data"; + } + $args .= "</$directive>\n"; + } + elsif (ref($directive) eq 'ARRAY') { + $args = join "\n", @$directive; + } + else { + $args = join " ", grep length($_), $directive, + (ref($arg) && (ref($arg) eq 'ARRAY') ? "@$arg" : $arg || ""); + } + + return $args; +} + +sub postamble_first { + shift->add_config_first(postamble => @_); +} + +sub postamble { + shift->add_config_last(postamble => @_); +} + +sub preamble_first { + shift->add_config_first(preamble => @_); +} + +sub preamble { + shift->add_config_last(preamble => @_); +} + +sub postamble_register { + push @{ shift->{postamble_hooks} }, @_; +} + +sub preamble_register { + push @{ shift->{preamble_hooks} }, @_; +} + +sub add_config_hooks_run { + my($self, $where, $out) = @_; + + for (@{ $self->{"${where}_hooks"} }) { + if ((ref($_) and ref($_) eq 'CODE') or $self->can($_)) { + $self->$_(); + } + else { + error "cannot run configure hook: `$_'"; + } + } + + for (@{ $self->{$where} }) { + $self->replace; + s/\n?$/\n/; + print $out "$_"; + } +} + +sub postamble_run { + shift->add_config_hooks_run(postamble => @_); +} + +sub preamble_run { + shift->add_config_hooks_run(preamble => @_); +} + +sub default_group { + return if WINFU; + + my $gid = $); + + #use only first value if $) contains more than one + $gid =~ s/^(\d+).*$/$1/; + + my $group = $ENV{APACHE_TEST_GROUP} || (getgrgid($gid) || "#$gid"); + + if ($group eq 'root') { + # similar to default_user, we want to avoid perms problems, + # when the server is started with group 'root'. When running + # under group root it may fail to create dirs and files, + # writable only by user + my $user = default_user(); + my $gid = $user ? (getpwnam($user))[3] : ''; + $group = (getgrgid($gid) || "#$gid") if $gid; + } + + $group; +} + +sub default_user { + return if WINFU; + + my $uid = $>; + + my $user = $ENV{APACHE_TEST_USER} || (getpwuid($uid) || "#$uid"); + + if ($user eq 'root') { + my $other = (getpwnam('nobody'))[0]; + if ($other) { + $user = $other; + } + else { + die "cannot run tests as User root"; + #XXX: prompt for another username + } + } + + return $user; +} + +sub default_serveradmin { + my $vars = shift->{vars}; + join '@', ($vars->{user} || 'unknown'), $vars->{servername}; +} + +sub default_apxs { + my $self = shift; + + return $self->{vars}->{apxs} if $self->{vars}->{apxs}; + + if (my $build_config = $self->modperl_build_config()) { + return $build_config->{MP_APXS}; + } + + if ($ENV{APACHE_TEST_APXS}) { + return $ENV{APACHE_TEST_APXS}; + } + + # look in PATH as a last resort + if (my $apxs = which('apxs')) { + return $apxs; + } elsif ($apxs = which('apxs2')) { + return $apxs; + } + + return; +} + +sub default_httpd { + my $self = shift; + + my $vars = $self->{vars}; + + if (my $build_config = $self->modperl_build_config()) { + if (my $p = $build_config->{MP_AP_PREFIX}) { + for my $bindir (qw(bin sbin)) { + my $httpd = catfile $p, $bindir, $vars->{target}; + return $httpd if -e $httpd; + # The executable on Win32 in Apache/2.2 is httpd.exe, + # so try that if Apache.exe doesn't exist + if (WIN32) { + $httpd = catfile $p, $bindir, 'httpd.EXE'; + if (-e $httpd) { + $vars->{target} = 'httpd.EXE'; + return $httpd; + } + } + } + } + } + + if ($ENV{APACHE_TEST_HTTPD}) { + return $ENV{APACHE_TEST_HTTPD}; + } + + # look in PATH as a last resort + if (my $httpd = which('httpd')) { + return $httpd; + } elsif ($httpd = which('httpd2')) { + return $httpd; + } elsif ($httpd = which('apache')) { + return $httpd; + } elsif ($httpd = which('apache2')) { + return $httpd; + } + + return; +} + +my $localhost; + +sub default_localhost { + my $localhost_addr = pack('C4', 127, 0, 0, 1); + gethostbyaddr($localhost_addr, Socket::AF_INET()) || 'localhost'; +} + +sub default_servername { + my $self = shift; + $localhost ||= $self->default_localhost; + die "Can't figure out the default localhost's server name" + unless $localhost; +} + +# memoize the selected value (so we make sure that the same port is used +# via select). The problem is that select_first_port() is called 3 times after +# -clean, and it's possible that a lower port will get released +# between calls, leading to various places in the test suite getting a +# different base port selection. +# +# XXX: There is still a problem if two t/TEST's configure at the same +# time, so they both see the same port free, but only the first one to +# bind() will actually get the port. So there is a need in another +# check and reconfiguration just before the server starts. +# +my $port_memoized; +sub select_first_port { + my $self = shift; + + my $port ||= $port_memoized || $ENV{APACHE_TEST_PORT} + || $self->{vars}{port} || DEFAULT_PORT; + + # memoize + $port_memoized = $port; + + return $port unless $port eq 'select'; + + # port select mode: try to find another available port, take into + # account that each instance of the test suite may use more than + # one port for virtual hosts, therefore try to check ports in big + # steps (20?). + my $step = 20; + my $tries = 20; + $port = DEFAULT_PORT; + until (Apache::TestServer->port_available($port)) { + unless (--$tries) { + error "no ports available"; + error "tried ports @{[DEFAULT_PORT]} - $port in $step increments"; + return 0; + } + $port += $step; + } + + info "the default base port is used, using base port $port instead" + unless $port == DEFAULT_PORT; + + # memoize + $port_memoized = $port; + + return $port; +} + +my $remote_addr; + +sub our_remote_addr { + my $self = shift; + my $name = $self->default_servername; + my $iaddr = (gethostbyname($name))[-1]; + unless (defined $iaddr) { + error "Can't resolve host: '$name' (check /etc/hosts)"; + exit 1; + } + $remote_addr ||= Socket::inet_ntoa($iaddr); +} + +sub default_loopback { + '127.0.0.1'; +} + +sub port { + my($self, $module) = @_; + + unless ($module) { + my $vars = $self->{vars}; + return $self->select_first_port() unless $vars->{scheme} eq 'https'; + $module = $vars->{ssl_module_name}; + } + return $self->{vhosts}->{$module}->{port}; +} + +sub hostport { + my $self = shift; + my $vars = shift || $self->{vars}; + my $module = shift || ''; + + my $name = $vars->{servername}; + + join ':', $name , $self->port($module || ''); +} + +#look for mod_foo.so +sub find_apache_module { + my($self, $module) = @_; + + die "find_apache_module: module name argument is required" + unless $module; + + my $vars = $self->{vars}; + my $sroot = $vars->{serverroot}; + + my @trys = grep { $_ } + ($vars->{src_dir}, + $self->apxs('LIBEXECDIR'), + catfile($sroot, 'modules'), + catfile($sroot, 'libexec')); + + for (@trys) { + my $file = catfile $_, $module; + if (-e $file) { + debug "found $module => $file"; + return $file; + } + } + + # if the module wasn't found try to lookup in the list of modules + # inherited from the system-wide httpd.conf + my $name = $module; + $name =~ s/\.s[ol]$/.c/; #mod_info.so => mod_info.c + $name =~ s/^lib/mod_/; #libphp4.so => mod_php4.c + return $self->{modules}->{$name} if $self->{modules}->{$name}; + +} + +#generate files and directories + +my %warn_style = ( + html => sub { "<!-- @_ -->" }, + c => sub { "/* @_ */" }, + php => sub { "<?php /* \n@_ \n*/ ?>" }, + default => sub { join '', grep {s/^/\# /gm} @_ }, +); + +my %file_ext = ( + map({$_ => 'html'} qw(htm html)), + map({$_ => 'c' } qw(c h)), + map({$_ => 'php' } qw(php)), +); + +# return the passed file's extension or '' if there is no one +# note: that '/foo/bar.conf.in' returns an extension: 'conf.in'; +# note: a hidden file .foo will be recognized as an extension 'foo' +sub filename_ext { + my ($self, $filename) = @_; + my $ext = (File::Basename::fileparse($filename, '\..*'))[2] || ''; + $ext =~ s/^\.(.*)/lc $1/e; + $ext; +} + +sub warn_style_sub_ref { + my ($self, $filename) = @_; + my $ext = $self->filename_ext($filename); + return $warn_style{ $file_ext{$ext} || 'default' }; +} + +sub genwarning { + my($self, $filename, $from_filename) = @_; + return unless $filename; + my $time = scalar localtime; + my $warning = "WARNING: this file is generated"; + $warning .= " (from $from_filename)" if defined $from_filename; + $warning .= ", do not edit\n"; + $warning .= "generated on $time\n"; + $warning .= calls_trace(); + return $self->warn_style_sub_ref($filename)->($warning); +} + +sub calls_trace { + my $frame = 1; + my $trace = ''; + + while (1) { + my($package, $filename, $line) = caller($frame); + last unless $filename; + $trace .= sprintf "%02d: %s:%d\n", $frame, $filename, $line; + $frame++; + } + + return $trace; +} + +sub clean_add_file { + my($self, $file) = @_; + + $self->{clean}->{files}->{ rel2abs($file) } = 1; +} + +sub clean_add_path { + my($self, $path) = @_; + + $path = rel2abs($path); + + # remember which dirs were created and should be cleaned up + while (1) { + $self->{clean}->{dirs}->{$path} = 1; + $path = dirname $path; + last if -e $path; + } +} + +sub genfile_trace { + my($self, $file, $from_file) = @_; + my $name = abs2rel $file, $self->{vars}->{t_dir}; + my $msg = "generating $name"; + $msg .= " from $from_file" if defined $from_file; + debug $msg; +} + +sub genfile_warning { + my($self, $file, $from_file, $fh) = @_; + + if (my $msg = $self->genwarning($file, $from_file)) { + print $fh $msg, "\n"; + } +} + +# $from_file == undef if there was no templates used +sub genfile { + my($self, $file, $from_file, $nowarning) = @_; + + # create the parent dir if it doesn't exist yet + my $dir = dirname $file; + $self->makepath($dir); + + $self->genfile_trace($file, $from_file); + + my $fh = Symbol::gensym(); + open $fh, ">$file" or die "open $file: $!"; + + $self->genfile_warning($file, $from_file, $fh) unless $nowarning; + + $self->clean_add_file($file); + + return $fh; +} + +# gen + write file +sub writefile { + my($self, $file, $content, $nowarning) = @_; + + my $fh = $self->genfile($file, undef, $nowarning); + + print $fh $content if $content; + + close $fh; +} + +sub perlscript_header { + + require FindBin; + + my @dirs = (); + + # mp2 needs its modper-2.0/lib before blib was created + if (IS_MOD_PERL_2_BUILD || $ENV{APACHE_TEST_LIVE_DEV}) { + # the live 'lib/' dir of the distro + # (e.g. modperl-2.0/ModPerl-Registry/lib) + my $dir = canonpath catdir $FindBin::Bin, "lib"; + push @dirs, $dir if -d $dir; + + # the live dir of the top dir if any (e.g. modperl-2.0/lib) + if (-e catfile($FindBin::Bin, "..", "Makefile.PL")) { + my $dir = canonpath catdir $FindBin::Bin, "..", "lib"; + push @dirs, $dir if -d $dir; + } + } + + for (qw(. ..)) { + my $dir = canonpath catdir $FindBin::Bin, $_ , "Apache-Test", "lib"; + if (-d $dir) { + push @dirs, $dir; + last; + } + } + + { + my $dir = canonpath catdir $FindBin::Bin, "t", "lib"; + push @dirs, $dir if -d $dir; + } + + push @dirs, canonpath $FindBin::Bin; + + my $dirs = join("\n ", '', @dirs) . "\n";; + + return <<"EOF"; + +use strict; +use warnings FATAL => 'all'; + +use lib qw($dirs); + +EOF +} + +# gen + write executable perl script file +sub write_perlscript { + my($self, $file, $content) = @_; + + my $fh = $self->genfile($file, undef, 1); + + my $shebang = make_shebang(); + print $fh $shebang; + + $self->genfile_warning($file, undef, $fh); + + print $fh $content if $content; + + close $fh; + chmod 0755, $file; +} + +sub make_shebang { + # if perlpath is longer than 62 chars, some shells on certain + # platforms won't be able to run the shebang line, so when seeing + # a long perlpath use the eval workaround. + # see: http://en.wikipedia.org/wiki/Shebang + # http://homepages.cwi.nl/~aeb/std/shebang/ + my $shebang = length $Config{perlpath} < 62 + ? "#!$Config{perlpath}\n" + : <<EOI; +$Config{'startperl'} + eval 'exec $Config{perlpath} -S \$0 \${1+"\$@"}' + if \$running_under_some_shell; +EOI + + return $shebang; +} + +sub cpfile { + my($self, $from, $to) = @_; + File::Copy::copy($from, $to); + $self->clean_add_file($to); +} + +sub symlink { + my($self, $from, $to) = @_; + CORE::symlink($from, $to); + $self->clean_add_file($to); +} + +sub gendir { + my($self, $dir) = @_; + $self->makepath($dir); +} + +# returns a list of dirs successfully created +sub makepath { + my($self, $path) = @_; + + return if !defined($path) || -e $path; + + $self->clean_add_path($path); + + return File::Path::mkpath($path, 0, 0755); +} + +sub open_cmd { + my($self, $cmd) = @_; + # untaint some %ENV fields + local @ENV{ qw(IFS CDPATH ENV BASH_ENV) }; + local $ENV{PATH} = untaint_path($ENV{PATH}); + + # launder for -T + $cmd = $1 if $cmd =~ /(.*)/; + + my $handle = Symbol::gensym(); + open $handle, "$cmd|" or die "$cmd failed: $!"; + + return $handle; +} + +sub clean { + my $self = shift; + $self->{clean_level} = shift || 2; #2 == really clean, 1 == reconfigure + + $self->new_test_server->clean; + $self->cmodules_clean; + $self->sslca_clean; + + for (sort keys %{ $self->{clean}->{files} }) { + if (-e $_) { + debug "unlink $_"; + unlink $_; + } + else { + debug "unlink $_: $!"; + } + } + + # if /foo comes before /foo/bar, /foo will never be removed + # hence ensure that sub-dirs are always treated before a parent dir + for (reverse sort keys %{ $self->{clean}->{dirs} }) { + if (-d $_) { + my $dh = Symbol::gensym(); + opendir($dh, $_); + my $notempty = grep { ! /^\.{1,2}$/ } readdir $dh; + closedir $dh; + next if $notempty; + debug "rmdir $_"; + rmdir $_; + } + } +} + +my %special_tokens = ( + nextavailableport => sub { shift->server->select_next_port } +); + +sub replace { + my $self = shift; + my $file = $Apache::TestConfig::File + ? "in file $Apache::TestConfig::File" : ''; + + s[@(\w+)@] + [ my $key = lc $1; + if (my $callback = $special_tokens{$key}) { + $self->$callback; + } + elsif (exists $self->{vars}->{$key}) { + $self->{vars}->{$key}; + } + else { + die "invalid token: \@$1\@ $file\n"; + } + ]ge; +} + +#need to configure the vhost port for redirects and $ENV{SERVER_PORT} +#to have the correct values +my %servername_config = ( + 0 => sub { + my($name, $port) = @_; + [ServerName => ''], [Port => 0]; + }, + 1 => sub { + my($name, $port) = @_; + [ServerName => $name], [Port => $port]; + }, + 2 => sub { + my($name, $port) = @_; + [ServerName => "$name:$port"]; + }, +); + +sub servername_config { + my $self = shift; + $self->server->version_of(\%servername_config)->(@_); +} + +sub parse_vhost { + my($self, $line) = @_; + + my($indent, $module, $namebased); + if ($line =~ /^(\s*)<VirtualHost\s+(?:_default_:|([^:]+):(?!:))?(.*?)\s*>\s*$/) { + $indent = $1 || ""; + $namebased = $2 || ""; + $module = $3; + } + else { + return undef; + } + + my $vars = $self->{vars}; + my $mods = $self->{modules}; + my $have_module = "$module.c"; + my $ssl_module = $vars->{ssl_module}; + + #if module ends with _ssl and it is not the module that implements ssl, + #then assume this module is a vhost with SSLEngine On (or similar) + #see mod_echo in extra.conf.in for example + if ($module =~ /^(mod_\w+)_ssl$/ and $have_module ne $ssl_module) { + $have_module = "$1.c"; #e.g. s/mod_echo_ssl.c/mod_echo.c/ + return undef unless $mods->{$ssl_module}; + } + + #don't allocate a port if this module is not configured + #assumes the configuration is inside an <IfModule $have_module> + if ($module =~ /^mod_/ and not $mods->{$have_module}) { + return undef; + } + + #allocate a port and configure this module into $self->{vhosts} + my $port = $self->new_vhost($module, $namebased); + + #extra config that should go *inside* the <VirtualHost ...> + my @in_config = $self->servername_config($namebased + ? $namebased + : $vars->{servername}, + $port); + + my @out_config = (); + if ($self->{vhosts}->{$module}->{namebased} < 2) { + #extra config that should go *outside* the <VirtualHost ...> + @out_config = ([Listen => '0.0.0.0:' . $port]); + + if ($self->{vhosts}->{$module}->{namebased}) { + push @out_config => ["<IfVersion < 2.3.11>\n". + "${indent}${indent}NameVirtualHost" + => "*:$port\n${indent}</IfVersion>"]; + } + } + + $self->{vars}->{$module . '_port'} = $port; + + #there are two ways of building a vhost + #first is when we parse test .pm and .c files + #second is when we scan *.conf.in + my $form_postamble = sub { + my $indent = shift; + for my $pair (@_) { + $self->postamble("$indent@$pair"); + } + }; + + my $form_string = sub { + my $indent = shift; + join "\n", map { "$indent@$_\n" } @_; + }; + + my $double_indent = $indent ? $indent x 2 : ' ' x 4; + return { + port => $port, + #used when parsing .pm and .c test modules + in_postamble => sub { $form_postamble->($double_indent, @in_config) }, + out_postamble => sub { $form_postamble->($indent, @out_config) }, + #used when parsing *.conf.in files + in_string => $form_string->($double_indent, @in_config), + out_string => $form_string->($indent, @out_config), + line => "$indent<VirtualHost " . ($namebased ? '*' : '_default_') . + ":$port>", + }; +} + +sub find_and_load_module { + my ($self, $name) = @_; + my $mod_path = $self->find_apache_module($name) or return; + my ($sym) = $name =~ m/mod_(\w+)\./; + + if ($mod_path && -e $mod_path) { + $self->preamble(IfModule => "!mod_$sym.c", + qq{LoadModule ${sym}_module "$mod_path"\n}); + } + return 1; +} + +sub replace_vhost_modules { + my $self = shift; + + if (my $cfg = $self->parse_vhost($_)) { + $_ = ''; + for my $key (qw(out_string line in_string)) { + next unless $cfg->{$key}; + $_ .= "$cfg->{$key}\n"; + } + } +} + +sub replace_vars { + my($self, $in, $out) = @_; + + local $_; + while (<$in>) { + $self->replace; + $self->replace_vhost_modules; + print $out $_; + } +} + +sub index_html_template { + my $self = shift; + return "welcome to $self->{server}->{name}\n"; +} + +sub generate_index_html { + my $self = shift; + my $dir = $self->{vars}->{documentroot}; + $self->gendir($dir); + my $file = catfile $dir, 'index.html'; + return if -e $file; + my $fh = $self->genfile($file); + print $fh $self->index_html_template; +} + +sub types_config_template { + return <<EOF; +text/html html htm +image/gif gif +image/jpeg jpeg jpg jpe +image/png png +text/plain asc txt +EOF +} + +sub generate_types_config { + my $self = shift; + + # handle the case when mod_mime is built as a shared object + # but wasn't included in the system-wide httpd.conf + $self->find_and_load_module('mod_mime.so'); + + unless ($self->{inherit_config}->{TypesConfig}) { + my $types = catfile $self->{vars}->{t_conf}, 'mime.types'; + unless (-e $types) { + my $fh = $self->genfile($types); + print $fh $self->types_config_template; + close $fh; + } + $self->postamble(<<EOI); +<IfModule mod_mime.c> + TypesConfig "$types" +</IfModule> +EOI + } +} + +# various dup bugs in older perl and perlio in perl < 5.8.4 need a +# workaround to explicitly rewind the dupped DATA fh before using it +my $DATA_pos = tell DATA; +sub httpd_conf_template { + my($self, $try) = @_; + + my $in = Symbol::gensym(); + if (open $in, $try) { + return $in; + } + else { + my $dup = Symbol::gensym(); + open $dup, "<&DATA" or die "Can't dup DATA: $!"; + seek $dup, $DATA_pos, 0; # rewind to the beginning + return $dup; # so we don't close DATA + } +} + +#certain variables may not be available until certain config files +#are generated. for example, we don't know the ssl port until ssl.conf.in +#is parsed. ssl port is needed for proxyssl testing + +sub check_vars { + my $self = shift; + my $vars = $self->{vars}; + + unless ($vars->{proxyssl_url}) { + my $ssl = $self->{vhosts}->{ $vars->{ssl_module_name} }; + if ($ssl) { + $vars->{proxyssl_url} ||= $ssl->{hostport}; + } + + if ($vars->{proxyssl_url}) { + unless ($vars->{maxclients_preset}) { + $vars->{minclients}++; + $vars->{maxclients}++; + } + } + } +} + +sub extra_conf_files_needing_update { + my $self = shift; + + my @need_update = (); + finddepth(sub { + return unless /\.in$/; + (my $generated = $File::Find::name) =~ s/\.in$//; + push @need_update, $generated + unless -e $generated && -M $generated < -M $File::Find::name; + }, $self->{vars}->{t_conf}); + + return @need_update; +} + +sub generate_extra_conf { + my $self = shift; + + my(@extra_conf, @conf_in, @conf_files); + + finddepth(sub { + return unless /\.in$/; + push @conf_in, catdir $File::Find::dir, $_; + }, $self->{vars}->{t_conf}); + + #make ssl port always be 8530 when available + for my $file (@conf_in) { + if (basename($file) =~ /^ssl/) { + unshift @conf_files, $file; + } + else { + push @conf_files, $file; + } + } + + for my $file (@conf_files) { + (my $generated = $file) =~ s/\.in$//; + debug "Will 'Include' $generated config file"; + push @extra_conf, $generated; + } + + # regenerate .conf files + for my $file (@conf_files) { + local $Apache::TestConfig::File = $file; + + my $in = Symbol::gensym(); + open($in, $file) or next; + + (my $generated = $file) =~ s/\.in$//; + my $out = $self->genfile($generated, $file); + $self->replace_vars($in, $out); + + close $in; + close $out; + + $self->check_vars; + } + + #we changed order to give ssl the first port after DEFAULT_PORT + #but we want extra.conf Included first so vhosts inherit base config + #such as LimitRequest* + return [ sort @extra_conf ]; +} + +sub sslca_can { + my($self, $check) = @_; + + my $vars = $self->{vars}; + return 0 unless $self->{modules}->{ $vars->{ssl_module} }; + return 0 unless -d "$vars->{t_conf}/ssl"; + + require Apache::TestSSLCA; + + if ($check) { + my $openssl = Apache::TestSSLCA::openssl(); + if (which($openssl)) { + return 1; + } + + error "cannot locate '$openssl' program required to generate SSL CA"; + exit(1); + } + + return 1; +} + +sub sslca_generate { + my $self = shift; + + my $ca = $self->{vars}->{sslca}; + return if $ca and -d $ca; #t/conf/ssl/ca + + return unless $self->sslca_can(1); + + Apache::TestSSLCA::generate($self); +} + +sub sslca_clean { + my $self = shift; + + # XXX: httpd config is required, for now just skip ssl clean if + # there is none. should probably add some flag which will tell us + # when httpd_config was already run + return unless $self->{vars}->{httpd} && $self->{vars}->{ssl_module}; + + return unless $self->sslca_can; + + Apache::TestSSLCA::clean($self); +} + +#XXX: just a quick hack to support t/TEST -ssl +#outside of httpd-test/perl-framework +sub generate_ssl_conf { + my $self = shift; + my $vars = $self->{vars}; + my $conf = "$vars->{t_conf}/ssl"; + my $httpd_test_ssl = "../httpd-test/perl-framework/t/conf/ssl"; + my $ssl_conf = "$vars->{top_dir}/$httpd_test_ssl"; + + if (-d $ssl_conf and not -d $conf) { + $self->gendir($conf); + for (qw(ssl.conf.in)) { + $self->cpfile("$ssl_conf/$_", "$conf/$_"); + } + for (qw(certs keys crl)) { + $self->symlink("$ssl_conf/$_", "$conf/$_"); + } + } +} + +sub find_in_inc { + my($self, $dir) = @_; + for my $path (@INC) { + my $location = "$path/$dir"; + return $location if -d $location; + } + return ""; +} + +sub prepare_t_conf { + my $self = shift; + $self->gendir($self->{vars}->{t_conf}); +} + +my %aliases = ( + "perl-pod" => "perlpod", + "binary-httpd" => "httpd", + "binary-perl" => "perl", +); +sub generate_httpd_conf { + my $self = shift; + my $vars = $self->{vars}; + + #generated httpd.conf depends on these things to exist + $self->generate_types_config; + $self->generate_index_html; + + $self->gendir($vars->{t_logs}); + $self->gendir($vars->{t_conf}); + + my @very_last_postamble = (); + if (my $extra_conf = $self->generate_extra_conf) { + for my $file (@$extra_conf) { + my $entry; + if ($file =~ /\.conf$/) { + next if $file =~ m|/httpd\.conf$|; + $entry = qq(Include "$file"); + } + elsif ($file =~ /\.pl$/) { + $entry = qq(<IfModule mod_perl.c>\n PerlRequire "$file"\n</IfModule>\n); + } + else { + next; + } + + # put the .last includes very last + if ($file =~ /\.last\.(conf|pl)$/) { + push @very_last_postamble, $entry; + } + else { + $self->postamble($entry); + } + + } + } + + $self->configure_proxy; + + my $conf_file = $vars->{t_conf_file}; + my $conf_file_in = join '.', $conf_file, 'in'; + + my $in = $self->httpd_conf_template($conf_file_in); + + my $out = $self->genfile($conf_file); + + $self->find_and_load_module('mod_alias.so'); + + $self->preamble_run($out); + + for my $name (qw(user group)) { #win32 + if ($vars->{$name}) { + print $out qq[\u$name "$vars->{$name}"\n]; + } + } + + #2.0: ServerName $ServerName:$Port + #1.3: ServerName $ServerName + # Port $Port + my @name_cfg = $self->servername_config($vars->{servername}, + $vars->{port}); + for my $pair (@name_cfg) { + print $out "@$pair\n"; + } + + $self->replace_vars($in, $out); + + # handle the case when mod_alias is built as a shared object + # but wasn't included in the system-wide httpd.conf + + print $out "<IfModule mod_alias.c>\n"; + for (sort keys %aliases) { + next unless $vars->{$aliases{$_}}; + print $out " Alias /getfiles-$_ $vars->{$aliases{$_}}\n"; + } + print $out "</IfModule>\n"; + + print $out "\n"; + + $self->postamble_run($out); + + print $out join "\n", @very_last_postamble; + + close $in; + close $out or die "close $conf_file: $!"; +} + +sub need_reconfiguration { + my($self, $conf_opts) = @_; + my @reasons = (); + my $vars = $self->{vars}; + + # if '-port select' we need to check from scratch which ports are + # available + if (my $port = $conf_opts->{port} || $Apache::TestConfig::Argv{port}) { + if ($port eq 'select') { + push @reasons, "'-port $port' requires reconfiguration"; + } + } + + my $exe = $vars->{apxs} || $vars->{httpd} || ''; + # if httpd.conf is older than executable + push @reasons, + "$exe is newer than $vars->{t_conf_file}" + if -e $exe && + -e $vars->{t_conf_file} && + -M $exe < -M $vars->{t_conf_file}; + + # any .in files are newer than their derived versions? + if (my @files = $self->extra_conf_files_needing_update) { + # invalidate the vhosts cache, since a different port could be + # assigned on reparse + $self->{vhosts} = {}; + for my $file (@files) { + push @reasons, "$file.in is newer than $file"; + } + } + + # if special env variables are used (since they can change any time) + # XXX: may be we could check whether they have changed since the + # last run and thus avoid the reconfiguration? + { + my $passenv = passenv(); + if (my @env_vars = sort grep { $ENV{$_} } keys %$passenv) { + push @reasons, "environment variables (@env_vars) are set"; + } + } + + # if the generated config was created with a version of Apache-Test + # less than the current version + { + my $current = Apache::Test->VERSION; + my $config = $self->{apache_test_version}; + + if (! $config || $config < $current) { + push @reasons, "configuration generated with old Apache-Test"; + } + } + + return @reasons; +} + +sub error_log { + my($self, $rel) = @_; + my $file = catfile $self->{vars}->{t_logs}, 'error_log'; + my $rfile = abs2rel $file, $self->{vars}->{top_dir}; + return wantarray ? ($file, $rfile) : + $rel ? $rfile : $file; +} + +#utils + +#For Win32 systems, stores the extensions used for executable files +#They may be . prefixed, so we will strip the leading periods. + +my @path_ext = (); + +if (WIN32) { + if ($ENV{PATHEXT}) { + push @path_ext, split ';', $ENV{PATHEXT}; + for my $ext (@path_ext) { + $ext =~ s/^\.*(.+)$/$1/; + } + } + else { + #Win9X: doesn't have PATHEXT + push @path_ext, qw(com exe bat); + } +} + +sub which { + my $program = shift; + + return undef unless $program; + + my @dirs = File::Spec->path(); + + require Config; + my $perl_bin = $Config::Config{bin} || ''; + push @dirs, $perl_bin if $perl_bin and -d $perl_bin; + + for my $base (map { catfile $_, $program } @dirs) { + if ($ENV{HOME} and not WIN32) { + # only works on Unix, but that's normal: + # on Win32 the shell doesn't have special treatment of '~' + $base =~ s/~/$ENV{HOME}/o; + } + + return $base if -x $base && -f _; + + if (WIN32) { + for my $ext (@path_ext) { + return "$base.$ext" if -x "$base.$ext" && -f _; + } + } + } +} + +sub apxs { + my($self, $q, $ok_fail) = @_; + return unless $self->{APXS}; + my $val; + unless (exists $self->{_apxs}{$q}) { + local @ENV{ qw(IFS CDPATH ENV BASH_ENV) }; + local $ENV{PATH} = untaint_path($ENV{PATH}); + my $devnull = devnull(); + my $apxs = shell_ready($self->{APXS}); + $val = qx($apxs -q $q 2>$devnull); + chomp $val if defined $val; # apxs post-2.0.40 adds a new line + if ($val) { + $self->{_apxs}{$q} = $val; + } + unless ($val) { + if ($ok_fail) { + return ""; + } + else { + warn "APXS ($self->{APXS}) query for $q failed\n"; + return $val; + } + } + } + $self->{_apxs}{$q}; +} + +# return an untainted PATH +sub untaint_path { + my $path = shift; + return '' unless defined $path; + ($path) = ( $path =~ /(.*)/ ); + # win32 uses ';' for a path separator, assume others use ':' + my $sep = WIN32 ? ';' : ':'; + # -T disallows relative and empty directories in the PATH + return join $sep, grep File::Spec->file_name_is_absolute($_), + grep length($_), split /$sep/, $path; +} + +sub pop_dir { + my $dir = shift; + + my @chunks = splitdir $dir; + while (my $remove = shift) { + pop @chunks if $chunks[-1] eq $remove; + } + + catfile @chunks; +} + +sub add_inc { + my $self = shift; + return if $ENV{MOD_PERL}; #already setup by mod_perl + require lib; + # make sure that Apache-Test/lib will be first in @INC, + # followed by modperl-2.0/lib (or some other project's lib/), + # followed by blib/ and finally system-wide libs. + my $top_dir = $self->{vars}->{top_dir}; + my @dirs = map { catdir $top_dir, "blib", $_ } qw(lib arch); + + my $apache_test_dir = catdir $top_dir, "Apache-Test"; + unshift @dirs, $apache_test_dir if -d $apache_test_dir; + + lib::->import(@dirs); + + if ($ENV{APACHE_TEST_LIVE_DEV}) { + # add lib/ in a separate call to ensure that it'll end up on + # top of @INC + my $lib_dir = catdir $top_dir, "lib"; + lib::->import($lib_dir) if -d $lib_dir; + } + + #print join "\n", "add_inc", @INC, ""; +} + +#freeze/thaw so other processes can access config + +sub thaw { + my $class = shift; + $class->new({thaw => 1, @_}); +} + +sub freeze { + require Data::Dumper; + local $Data::Dumper::Terse = 1; + my $data = Data::Dumper::Dumper(shift); + chomp $data; + $data; +} + +sub sync_vars { + my $self = shift; + + return if $self->{save}; #this is not a cached config + + my $changed = 0; + my $thaw = $self->thaw; + my $tvars = $thaw->{vars}; + my $svars = $self->{vars}; + + for my $key (@_) { + for my $v ($tvars, $svars) { + if (exists $v->{$key} and not defined $v->{$key}) { + $v->{$key} = ''; #rid undef + } + } + next if exists $tvars->{$key} and exists $svars->{$key} and + $tvars->{$key} eq $svars->{$key}; + $tvars->{$key} = $svars->{$key}; + $changed = 1; + } + + return unless $changed; + + $thaw->{save} = 1; + $thaw->save; +} + +sub save { + my($self) = @_; + + return unless $self->{save}; + + # add in the Apache-Test version for later comparisions + $self->{apache_test_version} = Apache::Test->VERSION; + + my $name = 'apache_test_config'; + my $file = catfile $self->{vars}->{t_conf}, "$name.pm"; + my $fh = $self->genfile($file); + + debug "saving config data to $name.pm"; + + (my $obj = $self->freeze) =~ s/^/ /; + + print $fh <<EOF; +package $name; + +sub new { +$obj; +} + +1; +EOF + + close $fh or die "failed to write $file: $!"; +} + +sub as_string { + my $cfg = ''; + my $command = ''; + + # httpd opts + my $test_config = Apache::TestConfig->new({thaw=>1}); + # XXX: need to run httpd config to get the value of httpd + if (my $httpd = $test_config->{vars}->{httpd}) { + $httpd = shell_ready($httpd); + $command = "$httpd -V"; + $cfg .= "\n*** $command\n"; + $cfg .= qx{$command}; + + $cfg .= ldd_as_string($httpd); + } + else { + $cfg .= "\n\n*** The httpd binary was not found\n"; + } + + # perl opts + my $perl = shell_ready($^X); + $command = "$perl -V"; + $cfg .= "\n\n*** $command\n"; + $cfg .= qx{$command}; + + return $cfg; +} + +sub ldd_as_string { + my $httpd = shift; + + my $command; + if (OSX) { + my $otool = which('otool'); + $command = "$otool -L $httpd" if $otool; + } + elsif (!WIN32) { + my $ldd = which('ldd'); + $command = "$ldd $httpd" if $ldd; + } + + my $cfg = ''; + if ($command) { + $cfg .= "\n*** $command\n"; + $cfg .= qx{$command}; + } + + return $cfg; +} + +# make a string suitable for feed to shell calls (wrap in quotes and +# escape quotes) +sub shell_ready { + my $arg = shift; + $arg =~ s!\\?"!\\"!g; + return qq["$arg"]; +} + + +1; + +=head1 NAME + +Apache::TestConfig -- Test Configuration setup module + +=head1 SYNOPSIS + + use Apache::TestConfig; + + my $cfg = Apache::TestConfig->new(%args) + my $fh = $cfg->genfile($file); + $cfg->writefile($file, $content); + $cfg->gendir($dir); + ... + +=head1 DESCRIPTION + +C<Apache::TestConfig> is used in creating the C<Apache::Test> +configuration files. + +=head1 FUNCTIONS + +=over + +=item genwarning() + + my $warn = $cfg->genwarning($filename) + +genwarning() returns a warning string as a comment, saying that the +file was autogenerated and that it's not a good idea to modify this +file. After the warning a perl trace of calls to this this function is +appended. This trace is useful for finding what code has created the +file. + + my $warn = $cfg->genwarning($filename, $from_filename) + +If C<$from_filename> is specified it'll be used in the warning to tell +which file it was generated from. + +genwarning() automatically recognizes the comment type based on the +file extension. If the extension is not recognized, the default C<#> +style is used. + +Currently it support C<E<lt>!-- --E<gt>>, C</* ... */> and C<#> +styles. + +=item genfile() + + my $fh = $cfg->genfile($file); + +genfile() creates a new file C<$file> for writing and returns a file +handle. + +If parent directories of C<$file> don't exist they will be +automagically created. + +The file C<$file> and any created parent directories (if found empty) +will be automatically removed on cleanup. + +A comment with a warning and calls trace is added to the top of this +file. See genwarning() for more info about this comment. + + my $fh = $cfg->genfile($file, $from_file); + +If C<$from_filename> is specified it'll be used in the warning to tell +which file it was generated from. + + my $fh = $cfg->genfile($file, $from_file, $nowarning); + +If C<$nowarning> is true, the warning won't be added. If using this +optional argument and there is no C<$from_file> you must pass undef as +in: + + my $fh = $cfg->genfile($file, undef, $nowarning); + + +=item writefile() + + $cfg->writefile($file, $content, [$nowarning]); + +writefile() creates a new file C<$file> with the content of +C<$content>. + +A comment with a warning and calls trace is added to the top of this +file unless C<$nowarnings> is passed and set to a true value. See +genwarning() for more info about this comment. + +If parent directories of C<$file> don't exist they will be +automagically created. + +The file C<$file> and any created parent directories (if found empty) +will be automatically removed on cleanup. + +=item write_perlscript() + + $cfg->write_perlscript($filename, @lines); + +Similar to writefile() but creates an executable Perl script with +correctly set shebang line. + +=item gendir() + + $cfg->gendir($dir); + +gendir() creates a new directory C<$dir>. + +If parent directories of C<$dir> don't exist they will be +automagically created. + +The directory C<$dir> and any created parent directories will be +automatically removed on cleanup if found empty. + +=back + +=head1 Environment Variables + +The following environment variables affect the configuration and the +run-time of the C<Apache::Test> framework: + +=head2 APACHE_TEST_COLOR + +To aid visual control over the configuration process and the run-time +phase, C<Apache::Test> uses coloured fonts when the environment +variable C<APACHE_TEST_COLOR> is set to a true value. + +=head2 APACHE_TEST_LIVE_DEV + +When using C<Apache::Test> during the project development phase, it's +often convenient to have the I<project/lib> (live) directory appearing +first in C<@INC> so any changes to the Perl modules, residing in it, +immediately affect the server, without a need to rerun C<make> to +update I<blib/lib>. When the environment variable +C<APACHE_TEST_LIVE_DEV> is set to a true value during the +configuration phase (C<t/TEST -config>, C<Apache::Test> will +automatically unshift the I<project/lib> directory into C<@INC>, via +the autogenerated I<t/conf/modperl_inc.pl> file. + + +=head1 Special Placeholders + +When generating configuration files from the I<*.in> templates, +special placeholder variables get substituted. To embed a placeholder +use the C<@foo@> syntax. For example in I<extra.conf.in> you can +write: + + Include @ServerRoot@/conf/myconfig.conf + +When I<extra.conf> is generated, C<@ServerRoot@> will get replaced +with the location of the server root. + +Placeholders are case-insensitive. + +Available placeholders: + +=head2 Configuration Options + +All configuration variables that can be passed to C<t/TEST>, such as +C<MaxClients>, C<DocumentRoot>, C<ServerRoot>, etc. To see the +complete list run: + + % t/TEST --help + +and you will find them in the C<configuration options> sections. + +=head2 NextAvailablePort + +Every time this placeholder is encountered it'll be replaced with the +next available port. This is very useful if you need to allocate a +special port, but not hardcode it. Later when running: + + % t/TEST -port=select + +it's possible to run several concurrent test suites on the same +machine, w/o having port collisions. + +=head1 AUTHOR + +=head1 SEE ALSO + +perl(1), Apache::Test(3) + +=cut + + +__DATA__ +Listen 0.0.0.0:@Port@ + +ServerRoot "@ServerRoot@" +DocumentRoot "@DocumentRoot@" + +PidFile @t_pid_file@ +ErrorLog @t_logs@/error_log +LogLevel debug + +<IfModule mod_version.c> +<IfVersion > 2.4.1> + DefaultRunTimeDir "@t_logs@" + LogLevel trace8 +</IfVersion> +</IfModule> + +<IfModule mod_log_config.c> + TransferLog @t_logs@/access_log +</IfModule> + +<IfModule mod_cgid.c> + ScriptSock @t_logs@/cgisock +</IfModule> + +ServerAdmin @ServerAdmin@ + +#needed for http/1.1 testing +KeepAlive On + +HostnameLookups Off + +<Directory /> + Options FollowSymLinks + AllowOverride None +</Directory> + +<IfModule @THREAD_MODULE@> +<IfModule mod_version.c> +<IfVersion < 2.3.4> + LockFile @t_logs@/accept.lock +</IfVersion> +</IfModule> + StartServers 1 + MinSpareThreads @MinClients@ + MaxSpareThreads @MinClients@ + ThreadsPerChild @MinClients@ + MaxClients @MaxClientsThreadedMPM@ + MaxRequestsPerChild 0 +</IfModule> + +<IfModule perchild.c> +<IfModule mod_version.c> +<IfVersion < 2.3.4> + LockFile @t_logs@/accept.lock +</IfVersion> +</IfModule> + NumServers 1 + StartThreads @MinClients@ + MinSpareThreads @MinClients@ + MaxSpareThreads @MinClients@ + MaxThreadsPerChild @MaxClients@ + MaxRequestsPerChild 0 +</IfModule> + +<IfModule prefork.c> +<IfModule mod_version.c> +<IfVersion < 2.3.4> + LockFile @t_logs@/accept.lock +</IfVersion> +</IfModule> + StartServers @MinClients@ + MinSpareServers @MinClients@ + MaxSpareServers @MinClients@ + MaxClients @MaxClients@ + MaxRequestsPerChild 0 +</IfModule> + +<IfDefine APACHE1> + LockFile @t_logs@/accept.lock + StartServers @MinClients@ + MinSpareServers @MinClients@ + MaxSpareServers @MinClients@ + MaxClients @MaxClients@ + MaxRequestsPerChild 0 +</IfDefine> + +<IfModule mpm_winnt.c> + ThreadsPerChild 50 + MaxRequestsPerChild 0 +</IfModule> + +<Location /server-info> + SetHandler server-info +</Location> + +<Location /server-status> + SetHandler server-status +</Location> + |