diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2014-04-07 15:45:39 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2014-04-07 15:45:39 +0300 |
commit | b12e3a907ec855affb3b91ae924e54887b90f3a8 (patch) | |
tree | 76f5dccf212b879fe86f5b08f046d3a0e6009808 /vhostcname/vhostcname | |
parent | f65436abbfe64f380ee7af600bf569333fccb3bb (diff) | |
download | dnstools-b12e3a907ec855affb3b91ae924e54887b90f3a8.tar.gz dnstools-b12e3a907ec855affb3b91ae924e54887b90f3a8.tar.bz2 |
New program: vhostcname
* vhostcname/GNUmakefile: New file.
* vhostcname/MANIFEST: New file.
* vhostcname/Makefile.PL: New file.
* vhostcname/rc.vhostnames: New file.
* vhostcname/vhostcname: New file.
* axfr2acl/MANIFEST: New file.
* .gitignore: Add backup files.
Diffstat (limited to 'vhostcname/vhostcname')
-rwxr-xr-x | vhostcname/vhostcname | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/vhostcname/vhostcname b/vhostcname/vhostcname new file mode 100755 index 0000000..1d2562d --- /dev/null +++ b/vhostcname/vhostcname @@ -0,0 +1,517 @@ +#! /usr/bin/perl +# Copyright (C) 2014 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; + +my $script; # This script name; + +my $config_file = "/etc/vhostcname.conf"; + +my $cnamelist = "/var/run/vhostcname.cache"; +my $host; # This host name +my @zone; +my $nameserver; +my $nskey; +my $ttl = 3600; # Default TTL +my $confdir; # Apache configuration directory +my $confpat = "*"; # A globbing pattern for Apache configuration files +my $dry_run; # Dry-run mode +my $debug; + +my $help; # Display help summary +my $man; # Ditto in manpage format + +sub err { + print STDERR "$script: "; + print STDERR $_ for (@_); + print STDERR "\n"; +} + +sub abend { + my $code = shift; + &err; + exit($code); +} + +sub read_config_file($) { + my $file = shift; + unless (-f $file) { + print STDERR "$script: configuration file $file does not exist\n" + if ($debug); + return; + } + print STDERR "$script: reading $file\n" if ($debug); + open(my $fd, "<", $file) or abend(1, "cannot open $file: $!"); + while (<$fd>) { + chomp; + s/^\s+//; + s/\s+$//; + s/\s+=\s+/=/; + s/#.*//; + next if ($_ eq ""); + unshift(@ARGV, "--$_"); + } + close($fd); + } + +sub get_cnames($) { + my $dir = shift; + my %ret; + + foreach my $file (glob "$dir/$confpat") { + next unless (-f $file); + print STDERR "$script: reading cnames from $file\n" if ($debug > 3); + + open(my $fd, "<", $file) or do { + err("can't open file $file: $!"); + next; + }; + while (<$fd>) { + s/#.*//; + s/^\s+//; + s/\s+$//; + next if (/^$/); + if (/^Server(Name|Alias)\s+(.*)/) { + foreach my $name (split /\s+/, $2) { + foreach my $z (@zone) { + $ret{$name} = $z if ($name =~ /.*\.$z/); + } + } + } + } + close($fd) + } + + return %ret; +} + +sub read_cname_list($) { + my $file = shift; + my %ret; + + if (-f $file) { + open(my $fd, "<", $file) or abend(1, "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(1, "cannot open $file for writing: $!"); + foreach my $h (sort keys %hash) { + print $fd "$h $hash{$h}\n"; + } + close($fd); +} + +sub ns_update { + my $resolver = shift; + my $name = shift; + my $domain = shift; + my %hash = @_; + + print STDERR "$script: 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) { + $update->push($k => $v); + } + $update->sign_tsig($nskey) if defined($nskey); + my $reply = $resolver->send($update); + if ($reply) { + if ($reply->header->rcode eq 'NOERROR') { + print STDERR "$script: update successful\n" if ($debug>3); + } else { + err("updating $name failed: ", + join(',', map { "$_ => $hash{$_}" } keys %hash), + ': ', + $reply->header->rcode); + return 0; + } + } else { + err("updating $name failed: ", + join(',', map { "$_ => $hash{$_}" } keys %hash), + ': ', + $resolver->errorstring); + return 0; + } + return 1; +} + +sub update_cnames_from_hash { + my %hash = @_; + + print STDERR "$script: " . keys(%hash) . " names to update\n" + if ($debug > 1); + my %oldhash = read_cname_list($cnamelist); + my @namelist = sort(keys(%hash)); + if (join(",", @namelist) eq join(".", sort(keys(%oldhash)))) { + print STDERR "$script: nothing to update\n" if ($debug); + return; + } + + my $resolver = new Net::DNS::Resolver; + $resolver->nameservers($nameserver) if defined($nameserver); + + my $name; + foreach $name (@namelist) { + if ($oldhash{$name}) { + delete $oldhash{$name}; + } else { + ns_update($resolver, $name, $hash{$name}, + prereq => yxdomain($name), + update => rr_del($name)); + print "$name $ttl CNAME $host\n"; + delete $hash{$name} + unless ns_update($resolver, $name, $hash{$name}, + update => rr_add("$name $ttl CNAME $host")); + } + } + + foreach $name (keys %oldhash) { + ns_update($resolver, $name, $oldhash{$name}, + prereq => yxrrset("$name CNAME"), + update => rr_del("$name CNAME")); + } + + write_cname_list($cnamelist, %hash); +} + +sub update_cnames_from_dir($) { + update_cnames_from_hash(get_cnames(shift)); +} + +sub nssetup { + if (-f $cnamelist) { + unlink($cnamelist) or abend("can't unlink $cnamelist: $!"); + } + &update_cnames_from_hash; +} + +sub nscleanup { + print STDERR "$script: Removing DNS CNAME records\n" if ($debug); + + my $resolver = new Net::DNS::Resolver; + $resolver->nameservers($nameserver) if defined($nameserver); + + my %hash = read_cname_list($cnamelist); + foreach my $name (keys %hash) { + delete $hash{$name} + if ns_update($resolver, $name, $hash{$name}, + prereq => yxrrset("$name CNAME"), + update => rr_del("$name CNAME")); + } + + write_cname_list($cnamelist, %hash); +} + + +### +($script = $0) =~ s/.*\///; + +## Read configuration +read_config_file($ENV{'VHOSTCNAME_CONF'} ? + $ENV{'VHOSTCNAME_CONF'} : $config_file); + +GetOptions("help" => \$man, + "h" => \$help, + "debug|d+" => \$debug, + "dry-run|n" => \$dry_run, + "hostname|H=s" => \$host, + "apache-config-pattern=s" => \$confpat, + "apache-config-directory=s" => \$confdir, + "ns-key=s" => \$nskey, + "cname-file=s" => \$cnamelist, + "zone|z=s@" => \@zone, + "ttl=i" => \$ttl, + "server=s" => \$nameserver, + ) or exit(1); + +pod2usage(-message => "$script: update DNS from Apache virtual host configuration", + -exitstatus => 0) if $help; +pod2usage(-exitstatus => 0, -verbose => 2) if $man; + +unless (defined($confdir)) { + foreach my $dir ("/etc/apache2", "/etc/httpd") { + if (-e "$dir/sites-enabled" and -e "$dir/sites-available") { + $confdir = $dir; + last; + } + if (-e "$dir/vhosts.d") { + $confdir = "$dir/vhosts.d"; + last; + } + } + abend(1, "don't know where virtual host configurations are located; use --apache-config-directory option") + unless defined($confdir); +} + +$host = hostname() unless defined($host); +push(@zone, $host) if ($#zone == -1); +$debug++ if ($dry_run); + +if ($#ARGV == -1) { + abend(1, "command not given") unless ($ENV{'DIRCOND_FILE'}); + print STDERR "$script: started as dircond handler for " . + "$ENV{'DIRCOND_GENEV_NAME'} on $ENV{'DIRCOND_FILE'}\n" + if ($debug); + my $cwd = getcwd; + my $update_dir; + if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") { + if ($cwd eq "$confdir/sites-available") { + foreach my $file (glob "$cwd/*") { + next unless (-l $file); + if (realpath(readlink($file)) eq + "$confdir/sites-enabled/$ENV{'DIRCOND_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); +} elsif ($#ARGV != 0) { + abend(1, "too many arguments"); +} elsif ($ARGV[0] =~ /^start|restart|force-restart|reload$/) { + nscleanup unless ($ARGV[0] eq "start"); + my %cnames = get_cnames(-d "$confdir/sites-enabled" ? + "$confdir/sites-enabled" : $confdir); + if (keys(%cnames) > 0) { + nssetup(%cnames); + } elsif ($debug) { + print STDERR "$script: no cnames defined\n"; + } +} elsif ($ARGV[0] eq "stop") { + nscleanup; +} elsif ($ARGV[0] eq "status") { + err("status command ignored"); +} else { + abend(1, "invalid command, try $script --help for more info"); +} + +__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>). + +A reverse operation is also supported: deregister all host name +registered on 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 ised as a B<dircond>(1) 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<dircond.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 + +=over 4 + +=item B<start> + +Scan the apache configuration files and register all server names matching +the supplied zones. + +=item B<stop> + +Deregister all hostnames registered previously. + +=item B<restart>, B<force-restart>, B<reload> + +Same as running B<vhostcname stop; vhostcname start>. + +=item B<status> + +Ignored + +=back + +=head1 OPTIONS + +=over 4 + +=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<GLOB> + +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. + +=item B<--cname-file=>I<NAME> + +Name of the file where B<vhostcname> will keep successfully registered +host names. Default is B</var/run/vhostcname.cache>. + +=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<-H>, B<--hostname>=I<NAME> + +Sets the hostname. Use this if B<vhostcname> is unable to correctly +determine it. + +=item B<--ns-key=>I<KEYFILE> + +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. + +=item B<--server=>I<NAME> + +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<TIME> + +TTL value for new DNS records. Default is 3600. + +=item B<--zone=>I<NAME> + +Name of the zone which B<vhostcname> can update. Multiple B<--zone> options +can be given. + +If no B<--zone> option is given, B<vhostcname> will take hostname as the +name of the zone. + +=back + +=head1 CONFIGURATION FILE + +If the file B<etc/vhostcname.conf> exists, the program will read its +configuration from it. A familiar UNIX configuration format is used. +Empty lines and UNIX comments are ignored. Each non-empty line is either an +option name, or option assignment, i.e. B<opt>=B<val>, with any amount of +optional whitespace around the equals sign. Valid option names are +the same as the long command line options, but without the leading B<-->. +For example: + + zone=vhost.example.com + ns-key=/etc/bind/Kvhost+157+43558.key + ttl=3600 + +=head1 ENVIRONMENT + +=over 4 + +=item VHOSTCNAME_CONF + +The name of the configuration file to use instead of the default +F</etc/vhostcname.conf>. + +=back + +=head1 BUGS + +Only one key file can be given. This means that if you use multiple +B<--zone> options, all zones must be configured to accept the same +DNSSEC key. Ditto for the B<--server> option. + +=head1 SEE ALSO + +B<dircond>(1). + +=head1 AUTHOR + +Sergey Poznyakoff <gray@gnu.org> + +=cut + + |