#!/usr/bin/perl
# 
# autodns-dhcp_ddns
# The procedure here is simple.   
#  If a record has been added since the last change, delte any previous
#  mentions and add a new record. This can lead to expired records but this
#  should not be a problem.
#
# Copyright (C) 1999 Stephen Carville
#
# 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 2 of the License, 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# use inverse time functions
use Time::Local;
use strict;
use Socket;

my $DEBUG = 0;
my $version="0.7";

# these are the only valid character for a machine name -- used in checkname()
my $NAMECHARS="[a-zA-Z0-9\\-]";

# define the extra filters to apply for machine names -- in checkname()
# these filters allow for extra name filtering if needed. For example, this
# filter only allows "legitimate" names into my employers's DNS 
#my $CYPFILTER="cy(pci|pca|hpr|sun)t\\d{2}|\\d{3}";
# these are the active filters -- set this to "" if you don't need any extra
# filters.
my(@filters)=("");

# filter for a bogus ethernet address (look for seven octets :-)
#my $ETHER="\\w\\w:\\w\\w:\\w\\w:\\w\\w:\\w\\w:\\w\\w:\\w\\w";
# lets try filtering on the first two octets of the bogus address this may 
# cause a legitimate address to choke but all the info I can find indicates
# the 52:41 sequence has never been issued to any manufacturer
my $ETHER="\^52:41:";

# nsupdate command strings
my $IFEXIST="prereq yxdomain";
my $IFNOTEXIST="prereq nxdomain";
my $ADD="update add";
my $DELETE="update delete";

# use a default time to live of one hour
my $TTL="3600";

my $LEASE_TEMP="dhcpd.leases.last";
# my $UPDATE="nsupdate.data";

my (%newip,%newmac,%newname,$hostname,$hostip,$hard);
my ($nowtime,$lastime,$ip,$linea);
my ($home,$dhcpdleases,$update,$dhcpup,$dhcpdleases_temp,$domain,$dhcpdconf);
my (@ddnscommand,@OUTPUT,@SALIDA);
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,
    $blksize,$blocks);

# get the program environment variables
$home = $ENV{DDNSHOME};
$dhcpdleases = $ENV{DHCPDLEASES};
$update = $ENV{UPDATE};
$dhcpdleases_temp = $ENV{DHCPDLEASES_TEMP};
$dhcpdconf = $ENV{DHCPDCONF};
$domain = $ENV{DNS_DOMAIN};
$dhcpup = $ENV{DHCPUP};

# get the current GMT
$nowtime=time();
# get last update time;
if (-e "$home/$LEASE_TEMP") {
    ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,
     $blksize,$blocks)=stat("$home/$LEASE_TEMP");
    $lastime=$mtime;
}
else {
    $lastime=0;
}

# now copy the dhcpd.leases file over to DDNSHOME

system "/bin/cp", $dhcpdleases, $dhcpdleases_temp;

# get the new data
# I have changed this to two global hashes:
# newip is indexed by IP and contains the last MAC asociated with that IP
# newmac is indexed by MAC and contains the last IP and the name for that MAC
load_new_data($nowtime,$lastime);

unless (open OUTPUT, ">$update") {
    print STDERR "Unable to open $update\n";
    die;
}
unless (open SALIDA, ">$dhcpup") {
    print STDERR "Unable to open $dhcpup\n";
    die;
}
unless (open ENTRADA, "<$dhcpdconf") {
    print STDERR "Unable to open $dhcpdconf\n";
    die;
}

if ($DEBUG == 1) {
    *OUTPUT=*STDERR;
}

# We parse the dhcpd.conf file getting the already fixed parameters to make
# sure a MAC doesn't get two IPs and fixed IPs aren't asigned again.
# I know this can only happen in a few cases, but this has gotta be strong.
while (<ENTRADA>) {
  if (/host.+hardware ethernet (.+); fixed-address (.+);.+/) {
     delete $newmac{$1};
     delete $newip{$2};
  }
}
seek(ENTRADA,0,0);

# now do the adds

#For each of the found ips (sorted numerically)
foreach $ip (sort ipcomp keys (%newip)) {
  #Get the mac for the ip we'll work with
  $hard=$newip{$ip};
  #For that mac, get it's IP and the host name
  ($hostip,$hostname) = split(/ /,$newmac{$hard});
  #Do not add the host if the IP doesn't match the one we were asking for
  unless ($hostip eq $ip) { next; }
  #Do not add the host if the name is not valid acording to our criteria
  unless (checkname($hostname)) { next; }
  #Do not add the host if the name already exists on dns
  if (gethostbyname("$hostname.$domain.")) { next; }
  #Check if we had added the same name on this round (only add the first one)
  if ( $newname{$hostname} ) { next; }
  #Mark the name so that we don't add the name more than once
  $newname{$hostname} = 1;

  @ddnscommand=add_record($hostname,$ip);

  while (($linea=<ENTRADA>) ne "range $ip;\n") {
    print SALIDA $linea
  }
  print SALIDA "host $hostname {hardware ethernet $hard; fixed-address $ip;}\n";
  print OUTPUT @ddnscommand;
}

while ($linea=<ENTRADA>) { print SALIDA $linea }

close SALIDA;
close ENTRADA;

#
# check an ethernet id 
# Windows will try to grab multiple addresses.  I can't stop DHCP from 
# granting them but I can make sure only addresses granted to valid 
# ethernet addresses get into the DNS
#
sub checkethid {
  my ($ether)=@_;

  if ($ether =~m/$ETHER/) {
    return 1;
  }
return 0;
}

#
# check a name to see if it is a valid name
# first check the name is FQDN valid then does it fit our
# internal naming policy.  I may add an exception list here later.
# 
sub checkname {
  my ($name)=@_;
  my (@array,$letter,$filter);

  @array=split(//,$name);

  foreach $letter(@array) {
    unless ($letter=~m/$NAMECHARS/) {
      return 0;
    }
  }
  foreach $filter (@filters) {
    unless ($name=~m/$filter/) {
      return 0;
    }
  }
  return 1;
}

#
# add a record
#
sub add_record {
    my ($machine,$address) = @_;
    my ($xaddress,@c,$arpa);

# get reverse lookup for the new address
    $arpa=get_arpa($address);
# add the new forward record
    push @c, "$ADD $machine.$domain $TTL IN A $address\n\n";
# add the new reverse record
    push @c, "$ADD $arpa $TTL IN PTR $machine.$domain\n\n";

    return @c;
}

#
# get the reverse lookup value for an address
#
sub get_arpa {
    my ($address) = @_;
    my ($arpa,$a,$b,$c,$d);

   ($a,$b,$c,$d) = split /\./, $address;
    $arpa = "$d.$c.$b.$a.in-addr.arpa";
    return $arpa;
}

#
# load the current dhcpd.leases file
# here we look at the hostname, the lease start time and the lease end time.
# if a hostname does not exist, there will be no entry made in DNS.
sub load_new_data{
    my ($nowtime,$lastime)=@_;
    my(@DATA,$hn,$ip,$startime,$endtime,@date,@time,@a,$hard,$bogus);

    unless (open DATA,"$dhcpdleases_temp") {
	print STDERR "autodns-dhcp_ddns $version: can't open $dhcpdleases_temp\n";
	die;
    }

# set the bogus ethernet id flag to 0
    $bogus=0;

# parse each line in the file
    while (<DATA>) {

# get rid of semicolons and quote marks
	$_=~s/\"//g;
	$_=~s/\;//g;

	@a=split(" ",$_);
	
# get IP address
	if ($a[0] eq "lease") {
# start of a new entry so reset bogus
	  $bogus=0;
	  $ip= $a[1];
	}

# get the starting GMT for the lease
	if ($a[0] eq "starts") {
	  @date=split("/",$a[2]);
	  @time=split(":",$a[3]);
	  $startime=timegm($time[2],$time[1],$time[0],$date[2],$date[1]-1,$date[0]);
	}

# get the ending GMT for the lease
	if ($a[0] eq "ends") {
	    @date=split("/",$a[2]);	
	    @time=split(":",$a[3]);
	    $endtime=timegm($time[2],$time[1],$time[0],$date[2],$date[1]-1,$date[0]);
	}

# check if the Ethernet address is legit.  Some Win boxen will request
# multiple addresses using bogus ethernet id's (yuck!)

	if ($a[0] eq "hardware") {
# if it is a bogus address go to the next entry 
	  if (checkethid($hard=$a[2])) {
	    $bogus=$a[2];
	    next;
	  }
	}

# since dhcpd adds new leases to the end of the file, it is assumed that 
# a later entry is the most likely to be correct.  Therefore just overwrite 
# any duplicate hostnames found

	if ($a[0] eq "client-hostname") {
	    $hn=lc($a[1]);
# is this lease new and valid? (is it worthwhile here to check for bogosity?)
	    if ($startime > $lastime && $endtime > $nowtime) {
	      unless ($bogus) {
		$newip{$ip} = $hard;
		$newmac{$hard} = "$ip $hn";
	      }
	    }
# if the lease is not valid and not bogus we remove the posible valid leases
# that had been assigned before.
	    else {
	      unless ($bogus) {
		delete $newip{$ip};
		delete $newmac{$hard};
	      }
	    }
	}
    }
    close DATA;
}

sub ipcomp {
	my @ip1 = split(/\./,$a);
	my @ip2 = split(/\./,$b);
	my $ip1n = (((($ip1[0]*256)+$ip1[1])*256)+$ip1[2])*256+$ip1[3];
	my $ip2n = (((($ip2[0]*256)+$ip2[1])*256)+$ip2[2])*256+$ip2[3];
	return ($ip1n <=> $ip2n);
}
