#!/usr/local/bin/perl
### BEGIN INIT INFO
# Provides:          vhostcnames
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Register Apache virtual host names.
# Description:       Register Apache virtual host names.
### END INIT INFO
# Copyright (C) 2014-2016 Sergey Poznyakoff <gray@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use Getopt::Long qw(:config gnu_getopt no_ignore_case);
use Pod::Usage;
use Pod::Man;
use Sys::Hostname;
use Cwd qw(getcwd realpath);
use Net::DNS;
use Data::Dumper;

my $progname;        # This script name;
my $progdescr = "update DNS from Apache virtual host configuration";
my $config_file = "/etc/vhostcname.conf";
my %config = (
    core => {
	'cache' => "/var/run/vhostcname.cache",
# Default TTL.
	'ttl' => 3600,
# A globbing pattern for Apache configuration files.
	'apache-config-pattern' => "*",
    }
);

use constant EX_OK => 0;
use constant EX_NOTUPDATED => 1;
use constant EX_USAGE => 64;
use constant EX_NOINPUT => 66;
use constant EX_CANTCREAT => 73;
use constant EX_CONFIG => 78;

my $host;          # This host name.
my $nameserver;    # Nameserver to use for updates.
my $dry_run;       # Dry-run mode.
my $debug;         # Debug level.

my $status = EX_OK;# Default exit status.

sub err {
    print STDERR "$progname: ";
    print STDERR $_ for (@_);
    print STDERR "\n";
}		

sub abend {
    my $code = shift;
    &err;
    exit($code);
}

sub parse_section {
    my ($conf, $input) = @_;
    my $ref = $conf;
    my $quote;
    my $rootname;
    while ($input ne '') {
	my $name;
	if (!defined($quote)) {
	    if ($input =~ /^"(.*)/) {
		$quote = '';
		$input = $1;
	    } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) {
		$name = $1;
		$input = $2;
	    } else {
		$name = $input;
		$input = '';
	    }
	} else {
	    if ($input =~ /^([^\\"]*)\\(.)(.*)/) {
		$quote .= $1 . $2;
		$input = $3;
	    } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) {
		$name = $quote . $1;
		$input = $2;
		$quote = undef;
	    } else {
		abend(EX_CONFIG, "unparsable input $input");
	    }
	}

	if (defined($name)) {
	    $rootname = $name unless defined $rootname;
	    $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH';
	    $ref = $ref->{$name};
	    $name = undef;
	}
    }
    return ($ref, $rootname);
}

sub check_mandatory {
    my ($section, $kw, $loc, $s) = @_;
    my $err = 0;
    while (my ($k, $d) = each %{$kw}) {
	if (ref($d) eq 'HASH'
	    and $d->{mandatory}
	    and !exists($section->{$k})) {
	    if (exists($d->{section})) {
		if ($s) {
		    err("$loc: mandatory section [$k] not present");
		    ++$err;
		}
	    } else {
		err("$loc: mandatory variable \"$k\" not set");
		++$err;
	    }
	}
    }
    return $err;
}

sub readconfig {
    my $file = shift;
    my $conf = shift;
    my %param = @_;

#    debug(2, "reading $file");
    open(my $fd, "<", $file)
	or do {
	    err("can't open configuration file $file: $!");
	    return 1 if $param{include};
	    exit(EX_NOINPUT);
    };
    
    my $line;
    my $err;
    my $section = $conf;
    my $kw = $param{kw};
    my $include = 0;
    my $rootname;
    
    while (<$fd>) {
	++$line;
	chomp;
	if (/\\$/) {
	    chop;
	    $_ .= <$fd>;
	    redo;
	}
	
	s/^\s+//;
	s/\s+$//;
	s/#.*//;
	next if ($_ eq "");

	if (/^\[(.+?)\]$/) {
	    $include = 0;
	    my $arg = $1;
	    $arg =~ s/^\s+//;
	    $arg =~ s/\s+$//;
	    if ($arg eq 'include') {
		$include = 1;
	    } else {
		($section, $rootname) = parse_section($conf, $1);
		if (ref($param{kw}) eq 'HASH') {
		    if (defined($rootname)) {
			if (ref($param{kw}{$rootname}) eq 'HASH'
			    and exists($param{kw}{$rootname}{section})) {
			    $kw = $param{kw}{$rootname}{section};
			} else {
			    err("$file:$line: unknown section");
			    $kw = undef;
			}
		    } else {
			$kw = $param{kw};
		    }
		}
	    }
	} elsif (/([\w_-]+)\s*=\s*(.*)/) {
	    my ($k, $v) = ($1, $2);
	    $k = lc($k) if $param{ci};

	    if ($include) {
		if ($k eq 'path') {
		    $err += readconfig($v, $conf, include => 1, @_);
		} elsif ($k eq 'pathopt') {
		    $err += readconfig($v, $conf, include => 1, @_)
			if -f $v;
		} elsif ($k eq 'glob') {
		    foreach my $file (bsd_glob($v, 0)) {
			$err += readconfig($file, $conf, include => 1, @_);
		    }
		} else {
		    err("$file:$line: unknown keyword");
		    ++$err;
		}
		next;
	    }

	    if (defined($kw)) {
		my $x = $kw->{$k};
		if (!defined($x)) {
		    err("$file:$line: unknown keyword $k");
		    ++$err;
		    next;
		} elsif (ref($x) eq 'HASH') {
		    if (exists($x->{re})) {
			if ($v !~ /$x->{re}/) {
			    err("$file:$line: invalid value for $k");
			    ++$err;
			    next;
			}
			if (exists($x->{check})
			    and !&{$x->{check}}($k, $v, "$file:$line")) {
			    ++$err;
			    next;
			}
		    } elsif (exists($x->{check})) {
			if (!&{$x->{check}}($k, $v, "$file:$line")) {
			    ++$err;
			    next;
			}
		    } elsif (!exists($x->{var}) and
			     !exists($x->{parser}) and
			     !exists($x->{mandatory})) {
			err("$file:$line: unknown keyword $k");
			++$err;
			next;
		    }
		    if (exists($x->{parser})
			and !&{$x->{parser}}($k, \$v, "$file:$line")) {
			++$err;
			next;
		    }
		}
	    }

            $section->{$k} = $v;
        } else {
    	    err("$file:$line: malformed line");
	    ++$err;
	    next;
	}
    }
    close $fd;
    exit(EX_CONFIG) if $err;
}

# Domain names may be formed from the set of alphanumeric ASCII characters 
# (a-z, A-Z, 0-9). In addition the hyphen is permitted if it is surrounded 
# by characters, digits or hyphens, although it is not to start or end a 
# label.
sub valid_domain_name {
    my $name = shift;
    $name =~ s/^\*\.// if ($config{core}{'allow-wildcards'});
    foreach my $label (split(/\./, $name)) {
        $label =~ s/-+/-/g;
        $label =~ s/[a-zA-Z0-9]-[a-zA-Z0-9]//g;
        return 0 if $label =~ /^-/ or $label =~ /-$/;
        return 0 if $label =~ /[^a-zA-Z0-9]/;
    }
    return 1;
}

sub get_cnames($) {
    my $dir = shift;
    my %ret;

    foreach my $file (glob "$dir/$config{core}{'apache-config-pattern'}") {
	next unless (-f $file);
	print STDERR "$progname: reading cnames from $file\n" if ($debug > 2);
	
	open(my $fd, "<", $file) or do {
	    err("can't open file $file: $!");
	    next;
	};
	my $line = 0;
	while (<$fd>) {
	    s/#.*//;
	    s/^\s+//;
	    s/\s+$//;
	    next if (/^$/);
	    if (/^Server(Name|Alias)\s+(.*)/) {
		foreach my $name (split /\s+/, $2) {
		    unless (valid_domain_name($name)) {
			err("$file:$line: $name: invalid domain name");
			next;
		    }
		    foreach my $z (keys %{$config{zone}}) {
			if ($name =~ /.*\.$z$/) {
			    if ($name =~ /^\*\.(.+)/ and $1 eq $z) {
				err("$file:$line: $name: first-level wildcard");
				next;
			    }
			    $ret{$name} = $z;
			    last;
			}
		    }
		}
	    }
	}
	close($fd)
    }

    while (my ($z,$conf) = each %{$config{zone}}) {
	if (exists($conf->{hostnames})) {
	    foreach my $name (@{$conf->{hostnames}}) {
		$name .= '.' . $z unless $name =~ /\.$/;
		$ret{$name} = $z;
	    }
	}
    }
    
    return %ret;
}

sub read_cname_list($) {
    my $file = shift;
    my %ret;

    if (-f $file) {
	open(my $fd, "<", $file) or abend(EX_NOINPUT, "cannot open $file: $!");
	while (<$fd>) {
	    chomp;
	    s/^\s+//;
	    s/\s+$//;
	    s/#.*//;
	    next if ($_ eq "");
	    my @a = split / /;
	    $ret{$a[0]} = $a[1];
	}
	close($fd);
    }
    return %ret;
}

sub write_cname_list {
    my ($file, %hash) = @_;

    return if ($dry_run);

    open(my $fd, ">", $file) or
	abend(EX_CANTCREAT, "cannot open $file for writing: $!");
    foreach my $h (sort keys %hash) {
	print $fd "$h $hash{$h}\n";
    }
    close($fd);
}

my %nsupdate_diag = (
    NXRRSET => sub {
	my $rr = shift;
	return $rr->type ne 'ANY';
    },
    NXDOMAIN => sub {
	my $rr = shift;
	return $rr->type eq 'ANY';
    }
);    

sub nsupdate_strerror {
    my ($reply, $hash) = @_;
    my $s;
    if (exists($hash->{prereq})
	and exists($nsupdate_diag{$reply->header->rcode})) {
	my $prereq;
	if (ref($hash->{prereq}) eq 'ARRAY') {
	    $prereq = $hash->{prereq};
	} else {
	    $prereq = [$hash->{prereq}];
	}
	my $diag = $nsupdate_diag{$reply->header->rcode};
	foreach my $rr (@{$prereq}) {
	    if (&{$diag}($rr)) {
		$s = "prerequisite \"".$rr->plain ."\" not met";
		last;
	    }
	}
    }

    $s = $reply->header->rcode unless defined $s;

    return $s;
}

sub ns_update {
    my $name = shift;
    my $domain = shift;
    my %hash = @_;
    my %ignorerr;

    my $resolver = get_zone_resolver($domain);

    print STDERR "$progname: updating $name in $domain: ".
	join(',', map { "$_ => $hash{$_}" } keys %hash) .
	"\n" if ($debug > 1);
    return 1 if ($dry_run);
    
    my $update = new Net::DNS::Update($domain);

    while (my ($k, $v) = each %hash) {
	if ($k eq 'ignore') {
	    $ignorerr{$v} = 1;
	} elsif (ref($v) eq 'ARRAY') {
	    foreach my $r (@{$v}) {
		$update->push($k => $r);
	    }
        } else {
	    $update->push($k => $v);
        }
    }
    zone_sign_tsig($update, $domain);
    my $reply = $resolver->send($update);
    if ($reply) {
	if ($reply->header->rcode eq 'NOERROR') {
	    print STDERR "$progname: update successful\n" if ($debug>3);
        } elsif ($ignorerr{$reply->header->rcode}) {
            print STDERR "$progname: ignoring: " .
		nsupdate_strerror($reply, \%hash) ."\n"	if $debug > 2;
	} else {
	    err("updating $name failed: " .
		nsupdate_strerror($reply, \%hash));
	    $status = EX_NOTUPDATED;
	    return 0;
	}
    } else {
	err("updating $name failed: " . $resolver->errorstring);
	$status = EX_NOTUPDATED;
	return 0;
    }
    return 1;
}

sub get_zone_resolver {
    my $zone = shift;
    unless (defined($config{zone}{$zone}{resolver})) {
	my $resolver = new Net::DNS::Resolver;
	my @servers;
	
	if (defined $config{zone}{$zone}{server}) {
	    push @servers, $config{zone}{$zone}{server};
	} elsif (defined $config{core}{server}) {
	    push @servers, $config{core}{server};
	} else {
	    my $query = $resolver->query($zone, "SOA");
	    if ($query) {
		my @rr = $query->answer;
		push @servers, $rr[0]->mname;
		print STDERR "$progname: using ".$rr[0]->mname." as name server for $zone\n"
		    if $debug;
	    } else {
		err("can't get soa for $zone: " . $resolver->errorstring);
	    }
	}
	$resolver->nameservers(@servers);
	$config{zone}{$zone}{resolver} = $resolver;
    }
    return $config{zone}{$zone}{resolver};
}

sub zone_sign_tsig {
    my ($update, $zone) = @_;
    my @tsig_args;

    my $zcfg = $config{zone}{$zone};
    if (exists($zcfg->{'ns-key-file'})) {
	push @tsig_args, split(/\s+/, $zcfg->{'ns-key-file'});
    } elsif (exists($zcfg->{'ns-key'})) {
	push @tsig_args, @{$zcfg->{'ns-key'}};
    }
    if ($#tsig_args == -1) {
	if (exists($config{core}{'ns-key-file'})) {
	    push @tsig_args, split(/\s+/, $config{core}{'ns-key-file'});
	} elsif (exists($config{core}{'ns-key'})) {
	    push @tsig_args, @{$config{core}{'ns-key'}};
	}
    }
    $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0);
}

sub update_cnames_from_hash {
    my %hash = @_;
    
    print STDERR "$progname: " . keys(%hash) . " names to update\n"
	if ($debug > 2);
    my %oldhash = read_cname_list($config{core}{cache});
    my @namelist = sort(keys(%hash));
    if (join(",", @namelist) eq join(",", sort(keys(%oldhash)))) {
	print STDERR "$progname: nothing to update\n" if ($debug);
	return;
    }
    
    my $name;
    foreach $name (@namelist) {
	if ($oldhash{$name}) {
	    delete $oldhash{$name};
	} elsif (ns_update($name, $hash{$name},
		      prereq => [yxdomain($name),
				 yxrrset("$name CNAME $config{core}{hostname}")],
		      update => rr_del("$name CNAME"),
			   ignore => 'NXDOMAIN')) {
	    print STDERR "$progname: $name $config{core}{ttl} CNAME $config{core}{hostname}\n" if ($debug);
	    unless (ns_update($name, $hash{$name},
			      update => rr_add("$name $config{core}{ttl} CNAME $config{core}{hostname}"))) {
		delete $hash{$name};
	    }
	} else {
	    delete $hash{$name};
	}
    }

    foreach $name (keys %oldhash) {
	ns_update($name, $oldhash{$name},
		  prereq => [
		      yxdomain($name),
		      yxrrset("$name CNAME $config{core}{hostname}.")
		  ],
		  update => rr_del("$name CNAME"),
                  ignore => 'NXDOMAIN');
    }

    write_cname_list($config{core}{cache}, %hash);
}

sub update_cnames_from_dir($) {
    update_cnames_from_hash(get_cnames(shift));
}
		      
sub nscleanup {
    print STDERR "$progname: Removing DNS CNAME records\n" if ($debug);

    my %hash = read_cname_list($config{core}{cache});
    foreach my $name (keys %hash) {
	print STDERR "$progname: removing $name from $hash{$name}\n"
            if ($debug);
	delete $hash{$name}
	    if ns_update($name, $hash{$name},
			 prereq => [
			     yxdomain($name),
			     yxrrset("$name CNAME $config{core}{hostname}.")
			 ],
			 update => rr_del("$name CNAME"),
                         ignore => 'NXDOMAIN');
    }

    write_cname_list($config{core}{cache}, %hash);
}

###
sub com_start {
    abend(EX_USAGE, "too many arguments") unless $#_ == 0;
    nscleanup();
    com_reload(@_);
}

sub com_reload {
    abend(EX_USAGE, "too many arguments") unless $#_ == 0;
    my $confdir = -d "$config{core}{'apache-config-directory'}/sites-enabled"
	 ? "$config{core}{'apache-config-directory'}/sites-enabled"
	 : $config{core}{'apache-config-directory'};
    my %cnames = get_cnames($confdir);
    update_cnames_from_hash(%cnames);
    err("no cnames defined") unless (keys(%cnames) > 0);  
}

sub com_stop {
    abend(EX_USAGE, "too many arguments") unless $#_ == 0;
    nscleanup;
}

sub com_status {
    err("status command ignored");
    my %stat;
    
    my %hash = read_cname_list($config{core}{cache});
    while (my ($name, $zone) = each %hash) {
#	$name =~ s/.$zone$//;
	push @{${stat}{$zone}}, $name;
    }

    foreach my $zone (sort(keys %stat)) {
	print "Names in zone $zone:\n";
	foreach my $name (sort(@{$stat{$zone}})) {
	    print " $name\n";
	}
    }
}

###
($progname = $0) =~ s/.*\///;

my %comtab = (
    start    => \&com_start,
    'force-restart' => 'start',
    reload   => \&com_reload,
    restart  => 'reload',
    stop     => \&com_stop,
    status   => \&com_status
);

sub getcom {
    my $com = shift;

    while (exists($comtab{$com}) and ref($comtab{$com}) ne 'CODE') {
	die "internal error: unresolved command alias"
	    unless exists $comtab{$com};
	$com = $comtab{$com};
    }
    return $comtab{$com} if exists $comtab{$com};

    my @v = map { /^$com/ ? $_ : () } sort keys %comtab;
    if ($#v == -1) {
	abend(EX_USAGE, "unrecognized command");
    } elsif ($#v > 0) {
	abend(EX_USAGE, "ambiguous command: ".join(', ', @v));
    }
    return getcom($v[0]);
}


## Read configuration
sub parse_ns_key {
    my ($var, $ref, $loc) = @_;
    my @result;
    if ($$ref =~ /(.+?)=(.+)/) {
	push @result, $1, $2;
	$$ref = \@result;
    } else {
	err("$loc: $var argument must be must be NAME=KEY");
	return 0;
    }
    return 1;
}

sub parse_hostnames {
    my ($var, $ref, $loc) = @_;
    my @result = split /\s+/, $$ref;
    $$ref = \@result;
    return 1;
}

sub parse_boolean {
    my ($var, $ref, $loc) = @_;
    my %bool = ( yes => 1,
		 no => 0,
		 true => 1,
		 false => 0,
		 t => 1,
		 nil => 0,
		 f => 0,
		 on => 1,
		 off => 0,
		 1 => 1,
		 0 => 0);

    my $s = $$ref;
    $s =~ tr/A-Z/a-z/;
    if (exists($bool{$s})) {
	$$ref = $bool{$s};
    } else {
	err("$loc: argument must be boolean");
	return 0;
    }
    return 1;
}

my %kw = (
    core => {
	section => {
	    'apache-config-directory' => 1,
	    'apache-config-pattern' => 1,
	    'cache' => 1,
	    'server' => 1,
	    'ttl' => 1,
	    'ns-key' => { parser => \&parse_ns_key },
	    'ns-key-file' => 1,
	    'hostname' => 1,
	    'allow-wildcards' => { parser => \&parse_boolean }
	},
    },
    zone => {
	section => {
	    'server' => 1,
	    'ttl' => 1,
	    'ns-key' => { parser => \&parse_ns_key },
	    'ns-key-file' => 1,
	    'hostnames' => { parser => \&parse_hostnames }
	},
    }
);

GetOptions("help" => sub {
              pod2usage(-exitstatus => EX_OK, -verbose => 2);
	   },
           "h" => sub {
	      pod2usage(-message => "$progname: $progdescr",
			-exitstatus => EX_OK);
	   },
	   "usage" => sub {
	      pod2usage(-exitstatus => EX_OK, -verbose => 0);
	   },
	   
           "debug|d+" => \$debug,
	   "dry-run|n" => \$dry_run,
	   "config|c=s" => \$config_file,
	   ) or exit(EX_USAGE);

readconfig($config_file, \%config, kw => \%kw);

unless (exists($config{core}{'apache-config-directory'})) {
    foreach my $dir ("/etc/apache2", "/etc/httpd") {
	if (-e "$dir/sites-enabled" and -e "$dir/sites-available") {
	    $config{core}{'apache-config-directory'} = $dir;
	    last;
	}
	if (-e "$dir/vhosts.d") {
	    $config{core}{'apache-config-directory'} = "$dir/vhosts.d";
	    last;
	}
    }
    abend(EX_CONFIG,
	  "don't know where virtual host configurations are located; define apache-config-directory")
	unless exists($config{core}{'apache-config-directory'});
}

$config{core}{hostname} = hostname() unless defined($config{core}{hostname});
$config{zone}{$host} = {} unless exists $config{zone};

$debug++ if ($dry_run);

if ($#ARGV == -1) {
    abend(EX_USAGE, "command not given") unless ($ENV{'DIREVENT_FILE'});
    print STDERR "$progname: started as direvent handler for " .
	         "$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n"
		 if ($debug);
    my $cwd = getcwd;
    my $update_dir;
    my $confdir = $config{core}{'apache-config-directory'};
    if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") {
	if ($cwd eq "$confdir/sites-available") {
	    foreach my $file (glob "$confdir/sites-enabled/$config{core}{'apache-config-pattern'}") {
		next unless (-l $file);
		if (realpath(readlink($file)) eq 
		    "$confdir/sites-available/$ENV{'DIREVENT_FILE'}") {
		    $update_dir = "$confdir/sites-enabled";
		    last;
		}
	    }
	} elsif ($cwd eq "$confdir/sites-enabled") {
	    $update_dir = $cwd;
	}
    } else {
	$update_dir = $cwd;
    }

    update_cnames_from_dir($update_dir) if defined($update_dir);
} else {
    &{getcom($ARGV[0])}(@ARGV);
}

exit($status);

__END__
=head1 NAME

vhostcname - synchronize DNS with Apache virtual host configuration

=head1 SYNOPSIS

B<vhostcname> [OPTIONS] B<COMMAND>

=head1 DESCRIPTION

The program takes a list of DNS zones and scans Apache virtual host
configuration files.  For each hostname found in B<ServerName> and
B<ServerAlias> statements, it checks whether this name ends in a
zone from the list, and if so, attempts to register this hostname
using the DNS dynamic updates mechanism (B<RFC 2136>).  For each
such name a CNAME record is created, pointing to the name of the
system's host.  The program will refuse to update the hostname if
a CNAME record already exists and points to another host.

A reverse operation is also supported: deregister all host names
registered during the previous run.

The mode of operation is requested by the B<COMMAND> argument.
The available B<COMMAND>s have been chosen so as to allow
B<vhostcname> to be run as one of the machine's startup
scripts.  The exact ways to register it to be run on server startup
and shutdown depend on the operating system and distribution in use.
For example, on Debian-based GNU/Linux:

    cd /etc/init.d
    ln -sf /usr/bin/vhostcname /etc/init.d
    update-rc.d vhostcname defaults

The program can also be used as a B<direvent>(8) handler.  This use
allows for immediate updates of the DNS records upon any modifications
to the Apache configuration files.  The following example shows the
corresponding B<direvent.conf>(5) entry:    

    watcher {
        path /etc/apache2/sites-available;
        path /etc/apache2/sites-enabled;
        event (create,delete,write);
        timeout 10;
        option (stderr,stdout);
        command /usr/bin/vhostcname;
    }
    
=head1 COMMANDS

Unless the program is started as a B<direvent>(8) handler, exactly one
command must be given in the command line.  A command may be supplied
in full or abbreviated form.  Any unambiguous abbreviation is allowed.

Available commands are:    
    
=over 4

=item B<start>

Scan the apache configuration files and register all server names that
match the configured zones.
    
=item B<stop>

Deregister all hostnames registered previously.

=item B<restart>, B<reload>

Builds a list of names from the apache configuration (I<apache-list>) and
compares them with the names registered at the previous run (I<cache>).  If
the two lists differ, the names present in I<apache-list>, but absent in
I<cache> are registered.  The names present in I<cache>, but lacking in
I<apache-list> are deleted from the DNS.

=item B<force-restart>

Deregister all hostnames registered previously, rescan Apache files, and
register all names that match the configured zones.
    
=item B<status>

Displays registered host names.
    
=back    
    
=head1 OPTIONS

=over 4    

=item B<-c>, B<--config=>I<FILE> 

Read configuration from I<FILE> instead of the default location
(F</etc/vhostcname.conf>).
    
=item B<-d>, B<--debug>

Increases the debug level.  Multiple B<-d> options are allowed.
    
=item B<-n>, B<--dry-run>,

Enables I<dry-run> mode: print what would have been done without actually
doing it.    

=item B<--help>

Displays B<vhostcname> man page.
    
=item B<-h>

Displays a short help summary and exits.

=item B<--usage>

Displays a short command line syntax reminder.    
    
=back

=head1 CONFIGURATION FILE

Configuration is read from F</etc/vhostcname.conf> or a file specified
by the B<--config> (B<-c>) command line option.  The file consists of
a number of variable assignments (I<variable> B<=> I<value>), grouped into
sections.  Whitespace is ignored, except that it serves to separate input
tokens.  However, I<value> is read verbatim, including eventual whitespace
characters that can appear within it.

A section begins with the line containing its name within square brackets
(e.g. B<[core]>).  The name can be followed by one or more arguments, if
the section semantics requires so (e.g. B<[zone example.com]>).

The following sections are recognized:

=over 4

=item B<[core]>

=over 8
    
=item B<apache-config-directory => I<DIR>

Sets the Apache configuration directory.  I<DIR> should be either a directory
where virtual configuration file are located or a directory which hosts the
B<sites-available> and B<sites-enabled> directories.  In the latter case,
B<vhostcname> will look for files matching B<apache-config-pattern> in
I<DIR>B</sites-enabled>.

If this option is not given, B<vhostcname> will try to deduce where the
configuration files are located.  It will issue a warning message and
terminate if unable to do that.
    
=item B<apache-config-pattern => I<PATTERN>

Shell globbing pattern for virtual host configuration files.  By default,
B<*> is used, meaning that B<vhostcname> will scan all files in the
configuration directory (note: that includes backup copies too!).
    
=item B<cache => I<FILE>

Name of the cache file where B<vhostcname> keeps successfully registered
host names.  Default is B</var/run/vhostcname.cache>.    

=item B<hostname => I<HOSTNAME>

Sets the target hostname.  This name will be used at the right side of
CNAME records created.  Defaults to the system's hostname.
    
=item B<allow-wildcards => I<BOOL>

Allow the use of wildcard (B<*>) in host names.  When this option is in
effect, a wildcard will be allowed if it is the very first label in a domain
name and it is separated from the base zone (see the B<zone> section) by one
more labels.

I<BOOL> is one of B<yes>, B<true>, B<t>, B<on>, or B<1> to allow wildcards,
or one of B<no>, B<false>, B<f>, B<nil>, B<off>, B<0> to disallow them (the
default).    

=back

The following variables provide defaults for zones that lack the
corresponding settings:    
    
=over 8    
    
=item B<server => I<HOST>

Name of the DNS server to use.  Normally B<vhostcname> determines what server
to use based on the B<SOA> record of the zone to be updated, so this option
is rarely needed.
    
=item B<ttl => I<SECONDS>

TTL value for new DNS records.  Default is 3600.
    
=item B<ns-key => I<NAME>=I<HASH>

Defines the TSIG key.

=item B<ns-key-file => I<FILE>

Name of the key file.  The argument should be the name of a file
generated by the B<dnssec-keygen> utility.  Either B<.key> or B<.private>
file can be used.

If both B<ns-key> and B<ns-key-file> are used, the latter is given preference.
    
=back

=item B<[zone I<NAME>]>    

The B<zone> section informs B<vhostcname> that it should handle names
in zone I<NAME>.  Any number of B<[zone]> sections can be defined.  If
none is defined, B<vhostcname> will use the name of the host it runs on
as the name of the zone to update.    

The variables in a B<zone> section define parameters to be used for that
particular zone.  As such, none of them is mandatory.  If the zone I<NAME>
uses default settings (or settings, defined in the B<[core]> section),
the section can be empty.    
    
=over 8
    
=item B<server => I<HOST>

Name of the DNS server to use when updating this zone.
    
=item B<ttl => I<SECONDS>

TTL for records in this zone.
    
=item B<ns-key => I<NAME>=I<HASH>

TSIG key.
    
=item B<ns-key-file => I<FILE>

Name of the key file.  The argument should be the name of a file
generated by the B<dnssec-keygen> utility.  Either B<.key> or B<.private>
file can be used.

If both B<ns-key> and B<ns-key-file> are used, the latter is given preference.

=item B<hostnames => I<NAME> [I<NAME>...]

Define immutable hostnames.  Each I<NAME> will be registered unconditionally.
    
=back
    
=back

=head1 EXIT CODE

=over 4

=item 0

Success

=item 1

Some of the host names could not be updated.    
    
=item 64

Command line usage error.

=item 66

Required input file cannot be opened.

=item 73

Required output file cannot be created or written.

=item 78

Configuration error.    

=back
    
=head1 SEE ALSO

B<direvent>(8).    
    
=head1 AUTHOR

Sergey Poznyakoff <gray@gnu.org>

=cut
    

