| 83 | rodolico | 1 | #! /usr/bin/env perl
 | 
        
           |  |  | 2 |   | 
        
           |  |  | 3 | use warnings;
 | 
        
           |  |  | 4 | use strict;
 | 
        
           |  |  | 5 |   | 
        
           |  |  | 6 | use Sys::Syslog;
 | 
        
           |  |  | 7 | use version
 | 
        
           |  |  | 8 | our $VERSION = version->declare( '2.0.0' );
 | 
        
           |  |  | 9 |   | 
        
           |  |  | 10 | #  Copyright (c) 2021, R. W. Rodolico
 | 
        
           |  |  | 11 | #  
 | 
        
           |  |  | 12 | #  Redistribution and use in source and binary forms, with or without 
 | 
        
           |  |  | 13 | #  modification, are permitted provided that the following conditions
 | 
        
           |  |  | 14 | #  are met:
 | 
        
           |  |  | 15 | #
 | 
        
           |  |  | 16 | #      Redistributions of source code must retain the above copyright
 | 
        
           |  |  | 17 | #      notice, this list of conditions and the following disclaimer.
 | 
        
           |  |  | 18 | #      
 | 
        
           |  |  | 19 | #      Redistributions in binary form must reproduce the above 
 | 
        
           |  |  | 20 | #      copyright notice, this list of conditions and the following 
 | 
        
           |  |  | 21 | #      disclaimer in the documentation and/or other materials provided
 | 
        
           |  |  | 22 | #      with the distribution.
 | 
        
           |  |  | 23 | #
 | 
        
           |  |  | 24 | #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 | 
        
           |  |  | 25 | #  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 | 
        
           |  |  | 26 | #  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 | 
        
           |  |  | 27 | #  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 | 
        
           |  |  | 28 | #  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 | 
        
           |  |  | 29 | #  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 | 
        
           |  |  | 30 | #  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 | 
        
           |  |  | 31 | #  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 | 
        
           |  |  | 32 | #  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 | 
        
           |  |  | 33 | #  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 | 
        
           |  |  | 34 | #  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 | 
        
           |  |  | 35 | #  POSSIBILITY OF SUCH DAMAGE.[9] 
 | 
        
           |  |  | 36 |   | 
        
           |  |  | 37 | # called as an ssh command
 | 
        
           |  |  | 38 | # ssh my_server hostname [ip]
 | 
        
           |  |  | 39 | # where hostname is the host name to be processed
 | 
        
           |  |  | 40 | # if IP is not given, will take the source IP from the ssh connection
 | 
        
           |  |  | 41 | # Can also be called from command line as
 | 
        
           |  |  | 42 | # ./updatedns hostname ip
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 44 | # version 2.0.0
 | 
        
           |  |  | 45 | # 20210417 RWR
 | 
        
           |  |  | 46 | # major modification to use one script for local and remote
 | 
        
           |  |  | 47 | # also, added ability to set up a configuration file
 | 
        
           |  |  | 48 |   | 
        
           |  |  | 49 |   | 
        
           |  |  | 50 | # we'll store the configuration here, so we can load it easily from
 | 
        
           |  |  | 51 | # a config file, if it exists
 | 
        
           |  |  | 52 | my %configuration = (
 | 
        
           |  |  | 53 |    'local'  => 1,
 | 
        
           |  |  | 54 |    'domain' => '',
 | 
        
           |  |  | 55 |    'server' => '',
 | 
        
           |  |  | 56 |    'keyfile' => '',
 | 
        
           |  |  | 57 |    'ttl' => 3600
 | 
        
           |  |  | 58 |    );
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |   | 
        
           |  |  | 61 | ######################################################
 | 
        
           |  |  | 62 | # you should not have to change anything below here. #
 | 
        
           |  |  | 63 | ######################################################
 | 
        
           |  |  | 64 |   | 
        
           |  |  | 65 | # get script path so we can find the auxiliary files
 | 
        
           |  |  | 66 | use Cwd qw(abs_path);
 | 
        
           |  |  | 67 | use File::Basename;
 | 
        
           |  |  | 68 | my $appDir = dirname(abs_path(__FILE__)) . '/';
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 | my $statusFile = $appDir . 'updatedns.status'; # stores current status of known entries
 | 
        
           |  |  | 71 | my $template = $appDir . 'updatedns.template'; # a template used for commands to nsupdate
 | 
        
           |  |  | 72 | # If local set, use -l parameter, otherwise, use the key file
 | 
        
           | 84 | rodolico | 73 | my $nsupdate = `which nsupdate`; chomp $nsupdate;
 | 
        
           | 83 | rodolico | 74 | my $configFile = $appDir . 'updatedns.conf';
 | 
        
           |  |  | 75 |   | 
        
           |  |  | 76 | my $hostname;
 | 
        
           |  |  | 77 | my $realIP;
 | 
        
           |  |  | 78 | my %entries;
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 | # this reads an INI type file in the form 
 | 
        
           |  |  | 81 | # key:value
 | 
        
           |  |  | 82 | # where : is any delimiter
 | 
        
           |  |  | 83 | # it then merges it into %$hash, overwriting anything with the same
 | 
        
           |  |  | 84 | # key with a new value
 | 
        
           |  |  | 85 | sub readFile {
 | 
        
           |  |  | 86 |    my %temp;
 | 
        
           |  |  | 87 |    my ($filename,$hash,$delimiter ) = @_;
 | 
        
           |  |  | 88 |    $delimiter = ':' unless $delimiter;
 | 
        
           |  |  | 89 |    return unless -f $filename;
 | 
        
           |  |  | 90 |    open CONF,"<$filename" or die "Could not read existing configuration file $filename: $!\n";
 | 
        
           |  |  | 91 |    # turn the delimited file into a hash
 | 
        
           |  |  | 92 |    chomp( %temp = map { split $delimiter } grep{ !/^#/ } my (@a) = <CONF> );
 | 
        
           |  |  | 93 |    close CONF;
 | 
        
           |  |  | 94 |    # merge into existing hash, leaving items not common alone
 | 
        
           |  |  | 95 |    @$hash{keys %temp} = values %temp;
 | 
        
           |  |  | 96 | }
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 | # merge the template file with our values and save it to a file
 | 
        
           |  |  | 99 | # in /tmp/hostname.nsupdate (overwriting if necessary)
 | 
        
           |  |  | 100 | # Call nsupdate with the filename as a parameter.
 | 
        
           |  |  | 101 | # leaves the file in /tmp for debugging
 | 
        
           |  |  | 102 | sub doUpdateNS {
 | 
        
           |  |  | 103 |    my ($hostname, $ip) = @_;
 | 
        
           |  |  | 104 |    my $nsupdateFileName = "/tmp/$hostname.nsupdate";
 | 
        
           |  |  | 105 |    # grab the template
 | 
        
           |  |  | 106 |    open TEMPLATE, "<$template" or die "could not open template $template for read: $!\n";
 | 
        
           |  |  | 107 |    my $template = join( '', <TEMPLATE> );
 | 
        
           |  |  | 108 |    close TEMPLATE;
 | 
        
           |  |  | 109 |    # now, replace our keys with our current values
 | 
        
           |  |  | 110 |    $template =~ s/\{hostname\}/$hostname/gi;
 | 
        
           |  |  | 111 |    $template =~ s/\{server\}/$configuration{'server'}/gi;
 | 
        
           |  |  | 112 |    $template =~ s/\{zone\}/$configuration{'domain'}/gi;
 | 
        
           |  |  | 113 |    $template =~ s/\{ttl\}/$configuration{'ttl'}/gi;
 | 
        
           |  |  | 114 |    $template =~ s/\{ip\}/$ip/gi;
 | 
        
           |  |  | 115 |    # save the file
 | 
        
           |  |  | 116 |    open OUTPUT, ">$nsupdateFileName" or die "Could not create $nsupdateFileName: $!\n";
 | 
        
           |  |  | 117 |    print OUTPUT $template;
 | 
        
           |  |  | 118 |    close OUTPUT;
 | 
        
           |  |  | 119 |    # execute nsupdate, and return the results to the caller
 | 
        
           |  |  | 120 |    return `$nsupdate $nsupdateFileName`;
 | 
        
           |  |  | 121 | }
 | 
        
           |  |  | 122 |   | 
        
           |  |  | 123 | &readFile( $configFile, \%configuration, '=' );
 | 
        
           |  |  | 124 |   | 
        
           | 84 | rodolico | 125 | # prepend 'server ' to the server command if it exists and we are not local
 | 
        
           |  |  | 126 | $configuration{'server'} = "server $configuration{server}" if $configuration{'server'} and not $configuration{'local'};
 | 
        
           |  |  | 127 | # and set nsupdate for local or remote
 | 
        
           |  |  | 128 | $nsupdate .= $configuration{'local'} ? ' -l ' : " -k $configuration{keyfile} ";
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |   | 
        
           | 83 | rodolico | 131 | #foreach my $key ( sort keys %configuration ) {
 | 
        
           |  |  | 132 | #   print "$key\t$configuration{$key}\n";
 | 
        
           |  |  | 133 | #}
 | 
        
           | 84 | rodolico | 134 | # die;
 | 
        
           | 83 | rodolico | 135 |   | 
        
           |  |  | 136 | # user should send hostname as a parameter on the command
 | 
        
           |  |  | 137 | # user may also send the IP
 | 
        
           |  |  | 138 |   | 
        
           |  |  | 139 | if ( defined $ENV{'SSH_ORIGINAL_COMMAND'} ) {
 | 
        
           |  |  | 140 |    ($hostname,$realIP) = split( ' ', $ENV{'SSH_ORIGINAL_COMMAND'});
 | 
        
           |  |  | 141 |    $realIP = $ENV{'SSH_CLIENT'} unless $realIP;
 | 
        
           |  |  | 142 | } else {
 | 
        
           |  |  | 143 |    $hostname = shift;
 | 
        
           |  |  | 144 |    $realIP = shift;
 | 
        
           |  |  | 145 | }
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 | die "Invalid call\n" unless $hostname and $realIP;
 | 
        
           |  |  | 148 | # validate IP is valid IPv4
 | 
        
           |  |  | 149 | # NOT a very good regex for that, but ... 
 | 
        
           |  |  | 150 | $realIP =~ m/^([\d.]+)/;
 | 
        
           |  |  | 151 | $realIP = $1;
 | 
        
           |  |  | 152 |   | 
        
           |  |  | 153 | #die "$hostname\t$realIP\n";
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 | # Start logging to syslog
 | 
        
           |  |  | 156 | openlog('updatedns', 'cons,pid', 'user');
 | 
        
           |  |  | 157 |   | 
        
           |  |  | 158 | unless ( $hostname ) {
 | 
        
           |  |  | 159 |    syslog( 'FATAL','%s',"no hostname passed in from IP $realIP" );
 | 
        
           |  |  | 160 |    die "Invalid Invocation: hostname\n" ;
 | 
        
           |  |  | 161 | }
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 | # validate the hostname is part of the $domain
 | 
        
           |  |  | 164 | if ( $hostname !~ m/[a-z0-9-]\.$configuration{'domain'}/ ) {
 | 
        
           |  |  | 165 |    syslog( 'warning', '%s', "Attempt to set incoming server name to invalid host [$hostname]");
 | 
        
           |  |  | 166 |    exit;
 | 
        
           |  |  | 167 | }
 | 
        
           |  |  | 168 | # slup in the status file
 | 
        
           |  |  | 169 |   | 
        
           |  |  | 170 | &readFile( $statusFile,\%entries, "\t" );
 | 
        
           |  |  | 171 |   | 
        
           |  |  | 172 | # is the entry for $hostname alread set? Do nothing except log it
 | 
        
           |  |  | 173 | if ( ( exists $entries{$hostname} ) && ( $entries{$hostname} eq $realIP ) ) {
 | 
        
           |  |  | 174 |    print "$configuration{'domain'}: Already set to the correct IP: [$realIP]\n";
 | 
        
           |  |  | 175 |    syslog('info', '%s', "Already set to the correct IP, [$hostname] = [$realIP]");
 | 
        
           |  |  | 176 | } else { # we have a new IP, or possibly a new hostname, so set it
 | 
        
           |  |  | 177 |    $entries{$hostname} = $realIP;
 | 
        
           |  |  | 178 |    open STATUS, ">$statusFile" or die "could not open $statusFile: $!\n";
 | 
        
           |  |  | 179 |    foreach my $key ( keys %entries ) {
 | 
        
           |  |  | 180 |       print STATUS "$key\t$entries{$key}\n";
 | 
        
           |  |  | 181 |    } # foreach
 | 
        
           |  |  | 182 |    my $output = &doUpdateNS( $hostname, $realIP );
 | 
        
           |  |  | 183 |    print "$hostname updated to $realIP, results are\n$output\n";
 | 
        
           |  |  | 184 |    syslog('info', '%s', "$hostname updated to $realIP" );
 | 
        
           |  |  | 185 | }
 | 
        
           |  |  | 186 | closelog();
 | 
        
           |  |  | 187 |   | 
        
           |  |  | 188 | 1;
 |