# --------------------------------------------------------------------------
###
#
# Copyright (C) Ensim Corporation 2000, 2001   All Rights Reserved.
#
# This software is furnished under a license and may be used and copied
# only  in  accordance  with  the  terms  of such  license and with the
# inclusion of the above copyright notice. This software or any other
# copies thereof may not be provided or otherwise made available to any
# other person. No title to and ownership of the software is hereby
# transferred.
#
# The information in this software is subject to change without notice
# and  should  not be  construed  as  a commitment by Ensim Corporation.
# Ensim assumes no responsibility for the use or  reliability  of its
# software on equipment which is not supplied by Ensim.
#
# $Id: DnsUpdate.pm,v 1.9 2003/02/11 04:10:06 naris Exp $
# $Name:  $
# --------------------------------------------------------------------------

package DnsUpdate;

use strict;
use IPC::Open3;
use IO::Handle;
use IO::Select;

use DnsUpdateResult;
use DnsCommon;
use Zone_TSIG;

use constant DEBUG => 0;

sub new () {
    my $class = shift;
    my $self  = {};

    bless $self, $class;
    $self->_init (@_);
    
    return $self;
}

sub pushPrereq {
    my $self = shift;
    my $op   = shift;
    my $val = shift;

    if ($op eq "nxdomain") {
        push @{$self->{prereq}}, "nxdomain $val";
    } elsif ($op eq "yxdomain") {
        push @{$self->{prereq}}, "yxdomain $val";
    } elsif ($op eq "nxrrset") {
        # we support only IN type
        push @{$self->{prereq}}, "nxrrset $val";
    } elsif ($op eq "yxrrset") {
        my $data = shift;
        $data or ($data = "");
        push @{$self->{prereq}}, "yxrrset $val $data";
    }
}

sub pushUpdate {
    my $self = shift;
    my $op   = shift;

    # class is assumed to be IN
    my $name;
    my $ttl;
    my $type;
    my $data;

    if ($op eq "delete") {
        $name = shift;
        ($type = shift) or ($type = "");
        ($data = shift) or ($data = "");
        
        push @{$self->{update}}, "delete $name $type $data";
    } elsif ($op eq "add") {
        $name = shift;
        $ttl  = shift;
        $type = shift;
        $data = shift;

        push @{$self->{update}}, "add $name $ttl $type $data";
    }
}
sub send {
    my $self = shift;
    
    my ($cin, $cout, $cerr) = (IO::Handle->new, IO::Handle->new, 
                             IO::Handle->new);
    my $pid;
    my $childerr=0;
    
    my $cmd = $self->_getCmd();
    my $cmdArgs = $self->_getNsupdateArgs();
    $pid = open3($cin, $cout, $cerr, "$cmd $cmdArgs");

    $SIG{CHLD} = sub {
	if (waitpid ($pid, 0) > 0) {
	    $childerr = ($? >> 8);
	}
    };

    # use local as server by default
    print $cin "server $self->{server}\n";
    if (DEBUG) {
        print STDERR ">server $self->{server}\n";
    }

    if ($self->{zone}) {
        print $cin "zone $self->{zone}\n";
        if (DEBUG) {
            print STDERR ">zone $self->{zone}\n";
        }
    }

    foreach my $prereq (@{$self->{prereq}}) {
        print $cin "prereq $prereq\n";
        if (DEBUG) {
            print STDERR ">prereq $prereq\n";
        }
    }
    
    foreach my $update (@{$self->{update}}) {
        print $cin "update $update\n";
        if (DEBUG) {
            print STDERR ">update $update\n";
        }
    }

    print $cin "\n";

    $cin->close();

    my $selector = IO::Select->new();
    $selector->add($cerr, $cout);
    
    my @ready;
    my $buf;
    my @message = ();
    while (@ready = $selector->can_read) {
        foreach my $fh (@ready) {
            if ($fh->fileno == $cerr->fileno) {
                $buf = <$fh>;
                if (DEBUG) {
                    print STDERR $buf;
                }

		if ($buf) {
		    chomp $buf;
		    push @message, $buf;
		}
            } elsif ($fh->fileno == $cout->fileno) {
                # discard stdout
                $buf = <$fh>;
            }

            $selector->remove($fh) if $fh->eof;
        }
    }

    if (DEBUG) {
	print "--END OF output--\n";
    }

    $cout->close;
    $cerr->close;
    
    # handle the result
    if ($childerr == 1) {
	# fatal error, last line == error
	$self->{errorstring} = pop @message;
	chomp $self->{errorstring};

    } else {
	###
	# we would like to look for:
	#  - "Reply from update query"
	#  - "Found zone name:..."
	#  - "The master is:..."
	#  - ";...."
	###

	my $zone;
	my $master;
	my @headerMsg = ();

	# precompile the patterns
	my $zonePattern   = qr/^Found zone name: (.*)$/;
	my $masterPattern = qr/^The master is: (.*)$/;
	my $replyPattern  = qr/^Reply from update query:\w*$/;
	my $errPattern    = qr/^; (.*)$/;
	    
	for (my $i=0; $i<scalar(@message); $i++) { 
	    if ($message[$i] =~ /$zonePattern/) {
		$zone = $1;
	    } elsif ($message[$i] =~ /$masterPattern/) {
		$master = $1;
	    } elsif ($message[$i] =~ /$replyPattern/) {
		push @headerMsg, $message[++$i];
		push @headerMsg, $message[++$i];
		
		last;
	    } elsif ($message[$i] =~ /$errPattern/) {
		# error case !!
		$self->{errorstring} = $1;
		chomp $self->{errorstring};

		return;
	    }
	}

	# if we have set zone in the transaction
	# there will be no master, and zone from stderr
	if ($self->{zone}) {
	    $zone = $self->{zone};
	    $master = "";
	}
	
	return new DnsUpdateResult $zone, $master, \@headerMsg;
    }

    return;
}

sub errorstring {
    my $self = shift;
    return $self->{errorstring};
}

# This method would fill out a DnsUpdate Object
# with the right information to perform SOA update
# 
# Returns:
#  - false if nothing has to be done
#  - true  if update has to be performed
#
# Exception:
#  - SOA lookup failure
sub updateSOA {
    my $class = shift;
    my $updater = shift;
    my $zone = shift;
    my $newValues = shift;
    my $rnameAutoUpdate = shift; # optional, default false

    # get the current SOA record
    my $res = DnsCommon::getResolver();
    my $query = $res->query($zone, "SOA");

    my $cursoa;
    if ($query) {
	$cursoa = ($query->answer)[0];
    } else {
	die $res->errorstring . "\n";
    }
    

    # convert the current RData into string and assigning
    # existing value
    my @curRData;
    my @newRData;
    
    # increment the serial number, unless the caller provided one
    if(!$newValues->{serial}) {
        $newValues->{serial} = DnsCommon::incSerial ($cursoa->{serial});
    }

    my $updateRequired = -1;

    # handle sync of rname to mname
    if ($rnameAutoUpdate &&  (!$newValues->{rname}) &&
	($newValues->{mname}) && $cursoa->rname) {

	# see if the mail domain is the same as mname
	my $adminMailDomain = $cursoa->rname;
	$adminMailDomain =~ s/^([^\.]*)\.?//;
	my $adminMailUser = "$1";

	if (lc($adminMailDomain) eq lc($cursoa->mname)) {
	    # we need to update the rname mail domain
	    $newValues->{rname} = sprintf ("%s.%s", $adminMailUser, 
					   $newValues->{mname});
	}
    }
    
    # need to add '.' to the end of email and master
    map {
	push @curRData, $cursoa->$_ . '.';
	push @newRData, ($newValues->{$_} || $cursoa->$_ . '.');
	
	if ($newValues->{$_} &&
	    ($newValues->{$_} ne ($cursoa->$_ . '.'))) {
	    $updateRequired++;
	}
    } ("mname", "rname");
    
    
    map {
	push @curRData, $cursoa->$_;
	push @newRData, ($newValues->{$_} || $cursoa->$_);
	
	if ($newValues->{$_} &&
	    ($newValues->{$_} ne $cursoa->$_)) {
	    $updateRequired++;
	}
    } ("serial", "refresh", "retry", "expire", "minimum");

    
    if (!$updateRequired) {
	return "";
    }


    $updater->pushPrereq ("yxrrset", join (' ', "$zone IN SOA", @curRData));
    $updater->pushUpdate ("add", "$zone",
			DnsCommon::getDefaultRRTTL(), "SOA",
			  join ' ', @newRData);
    
    return 1;
}

sub _init { 
    my $self = shift;
    my $zone = shift;

    my @pre = ();
    $self->{prereq} = \@pre;

    my @update = ();
    $self->{update} = \@update;

    if ($zone) {
        $self->{zone} = $zone;
    }

    $self->{server} = "127.0.0.1";
}

sub _getNsupdateArgs($) {
    my $self = shift;
    my $TSIG_keyFile = Zone_TSIG::getZoneTSIGPrivateKeyFile($self->{zone});

    return "-k $TSIG_keyFile -d";
}

sub _getCmd($) {
    my $self = shift;

    return "/usr/bin/nsupdate";
}

1;




