aboutsummaryrefslogtreecommitdiff
path: root/vhostcname/vhostcname
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2014-04-07 15:45:39 +0300
committerSergey Poznyakoff <gray@gnu.org.ua>2014-04-07 15:45:39 +0300
commitb12e3a907ec855affb3b91ae924e54887b90f3a8 (patch)
tree76f5dccf212b879fe86f5b08f046d3a0e6009808 /vhostcname/vhostcname
parentf65436abbfe64f380ee7af600bf569333fccb3bb (diff)
downloaddnstools-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-xvhostcname/vhostcname517
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
+
+

Return to:

Send suggestions and report system problems to the System administrator.