Manually generating Virtualmin configuration for Apache
A client came to me with what was initially a Virtualmin backup problem:
Copying Apache aliases ..
.. failed to find target virtual website!
The virtual host existed within /etc/httpd/conf/httpd.conf
but there were errors:
# apachectl configtest
httpd: Syntax error on line 394 of /etc/httpd/conf/httpd.conf: Expected </Directory> but saw </VirtualHost>
I fixed a couple of these issues but it quickly became clear that the whole file (around 4000 lines) was corrupt. There were sections of anything from 1-20 lines missing, and the lines seemed to reappear at random later in the file.
The backups had been failing and had been rotated away so restoring wasn't an option, and Apache was still running so disabling/re-enabling the sites was risky as it could easily break Apache and, as mentioned, there were no backups.
I decided to write something to try to parse the Virtualmin configuration, which has each domain as a series of key=value
parameters in numbered files under /etc/webmin/virtual-server/domains/
- some of these were just aliases, some had different versions of PHP, only some had SSL enabled.
I wrote a Perl script to generate the config so that the backups could be taken, then reloaded Apache with the new configuration. It worked for me, but I present the Perl script below as a proof of concept only. It makes a lot of assumptions, it might not work in every environment, and so I present it as broken.
This code will only generate the virtual host snippets and not the config file before that point. I copied the existing httpd.conf
to httpd.conf.top
, deleted everything after the first <VirtualHost>
, then: perl httpd.pl > http.conf.bottom && cat httpd.conf.top httpd.conf.bottom > /etc/httpd/conf/httpd.conf
.
#!/usr/bin/perl
# Copyright James Lawrie 2021
# james@silvermouse.net
#
use warnings;
use strict;
use Data::Dumper;
my @domains = glob('/etc/webmin/virtual-server/domains/*');
my @sites;
my %aliases;
foreach my $domain (@domains) {
my %temp_config;
open my $file, "<", $domain;
foreach (<$file>) {
if (/^([^=]+)=([^=]+)$/) {
chomp ($temp_config{$1} = $2);
}
}
close $file;
if ($temp_config{"alias_mode"} eq "1") {
if (!$aliases{$temp_config{"backup_alias_dom"}}) {
$aliases{$temp_config{"backup_alias_dom"}} = [];
}
push ($aliases{$temp_config{"backup_alias_dom"}}, $temp_config{'dom'});
}
else {
push (@sites, \%temp_config);
}
}
foreach my $site (@sites) {
my %config = %{$site};
print generate_config($config{'dom'}, $config{'ssl'}, %config);
if ($config{'logrotate'}) {
generate_logrotate($config{'dom'});
}
}
sub generate_config {
my $site = shift;
my $ssl = shift;
my %config = @_;
my $port = $ssl ? $config{'web_sslport'} : $config{'web_port'};
my $ssl_config;
my $config = "<VirtualHost $config{'dns_ip'}:${port}";
$config .= $config{ip6} ? " [$config{'ip6'}]:${port}>\n" : ">\n";
if ($ssl) {
$config .= <<"EOF";
SSLEngine on
SSLCertificateFile $config{'ssl_cert'}
SSLCertificateKeyFile $config{'ssl_key'}
EOF
$config .= " SSLCertificateChainFile $config{'ssl_chain'}\n" if $config{'ssl_chain'};
}
$config .= <<"EOF";
SuexecUserGroup "#$config{'uid'}" "#$config{'gid'}"
ServerName $site
ServerAlias www.${site}
ServerAlias autoconfig.${site}
ServerAlias autodiscover.${site}
EOF
my $aliases = $aliases{$config{'dom'}};
foreach my $alias (@$aliases) {
$config .= " ServerAlias ${alias}\n ServerAlias www.${alias}\n" if $alias;
}
$config .= <<"EOF";
DocumentRoot $config{'public_html_path'}
ErrorLog /var/log/virtualmin/$config{'dom'}_error_log
CustomLog /var/log/virtualmin/$config{'dom'}_access_log combined
RemoveHandler .php
RemoveHandler .php5
RemoveHandler .php5.6
RemoveHandler .php7.0
RemoveHandler .php7.2
RemoveHandler .php7.3
RemoveHandler .php7.4
php_admin_value engine Off
DirectoryIndex index.html index.htm index.php index.php4 index.php5
<Directory $config{'public_html_path'}>
Options -Indexes +IncludesNOEXEC +SymLinksIfOwnerMatch +ExecCGI
AllowOverride All Options=ExecCGI,Includes,IncludesNOEXEC,Indexes,MultiViews,SymLinksIfOwnerMatch
Require all granted
AddType application/x-httpd-php .php
EOF
my @fcgis = glob("$config{'home'}/fcgi-bin/*");
my $default_php = 0;
foreach (@fcgis) {
if (/.*(php(.*))\.fcgi/) {
$config .= " FCGIWrapper $_ .${1}\n";
$config .= " AddHandler fcgid-script ${1}\n";
$default_php = $2 > $default_php ? $2 : $default_php;
}
}
if ($config{'last_php_version'}) { $default_php = $config{'last_php_version'}; }
$config .= <<"EOF";
FCGIWrapper $config{'home'}/fcgi-bin/php${default_php}.fcgi .php
AddHandler fcgid-script .php
Require all granted
</Directory>
<Directory $config{'cgi_bin_path'}>
allow from all
AllowOverride All Options=ExecCGI,Includes,IncludesNOEXEC,Indexes,MultiViews,SymLinksIfOwnerMatch
Require all granted
</Directory>
FcgidMaxRequestLen 1073741824
Redirect /mail/config-v1.1.xml /cgi-bin/autoconfig.cgi
IPCCommTimeout 61
FcgidMaxRequestLen 1073741824
php_value memory_limit 32M
php_value suhosin.session.encrypt Off
<Files awstats.pl>
AuthName "$config{'dom'} statistics"
AuthType Basic
AuthUserFile $config{'home'}/.awstats-htpasswd
require valid-user
</Files>
</VirtualHost>
EOF
if ($ssl) {
$config .= generate_config($config{'dom'}, 0, %config);
}
else {
return $config;
}
}
sub generate_logrotate {
my $domain = shift;
my $logrot = "/etc/logrotate.d/${domain}.conf";
my $output = <<"EOF";
/var/log/virtualmin/${domain}_access_log /var/log/virtualmin/${domain}_error_log {
rotate 5
weekly
compress
postrotate
systemctl reload httpd.service ; sleep 5
endscript
sharedscripts
}
EOF
if (!-e $logrot) {
open my $fh, ">", $logrot;
print $fh $output;
}
}