package Tie::DNS;
{
  $Tie::DNS::VERSION = '1.132260';
}
use Carp;
use strict;
use warnings;
use Net::DNS;

my %config_rec_defaults = (
    'AAAA'   => 'address',
    'AFSDB'  => 'subtype',
    'A'      => 'address',
    'CNAME'  => 'cname',
    'EID'    => 'rdlength',
    'HINFO'  => 'cpu',
    'ISDN'   => 'address',
    'LOC'    => 'version',
    'MB'     => 'madname',
    'MG'     => 'mgmname',
    'MINFO'  => 'rmailbx',
    'MR'     => 'newname',
    'MX'     => 'exchange',
    'NAPTR'  => 'order',
    'NIMLOC' => 'rdlength',
    'NSAP'   => 'idp',
    'NS'     => 'nsdname',
    'NULL'   => 'rdlength',
    'PTR'    => 'ptrdname',
    'PX'     => 'preference',
    'RP'     => 'mbox',
    'RT'     => 'intermediate',
    'SOA'    => 'mname',
    'SRV'    => 'target',
    'TXT'    => 'txtdata'
);

my %config_type = (
    'AAAA'  => [ 'address',  'ttl' ],
    'AFSDB' => [ 'subtype',  'ttl' ],
    'A'     => [ 'address',  'ttl' ],
    'CNAME' => [ 'cname',    'ttl' ],
    'EID'   => [ 'rdlength', 'rdata', 'ttl' ],
    'HINFO' => [ 'cpu',      'os', 'ttl' ],
    'ISDN'  => [ 'address',  'subaddress', 'ttl' ],
    'LOC' => [
        'version',  'size',      'horiz_pre', 'vert_pre',
        'latitude', 'longitude', 'latlon',    'altitude',
        'ttl'
    ],
    'MB'    => [ 'madname',  'ttl' ],
    'MG'    => [ 'mgmname',  'ttl' ],
    'MINFO' => [ 'rmailbx',  'emailbx', 'ttl' ],
    'MR'    => [ 'newname',  'ttl' ],
    'MX'    => [ 'exchange', 'preference' ],
    'NAPTR' => [
        'order',  'preference',  'flags', 'service',
        'regexp', 'replacement', 'ttl'
    ],
    'NIMLOC' => [ 'rdlength', 'rdata', 'ttl' ],
    'NSAP'   => [
        'idp',  'dsp', 'afi',  'idi', 'dfi', 'aa',
        'rsvd', 'rd',  'area', 'id',  'sel', 'ttl'
    ],
    'NS'   => [ 'nsdname',    'ttl' ],
    'NULL' => [ 'rdlength',   'rdata', 'ttl' ],
    'PTR'  => [ 'ptrdname',   'ttl' ],
    'PX'   => [ 'preference', 'map822', 'mapx400', 'ttl' ],
    'RP'  => [ 'mbox',         'txtdname',   'ttl' ],
    'RT'  => [ 'intermediate', 'preference', 'ttl' ],
    'SOA' => [
        'mname', 'rname',  'serial',  'refresh',
        'retry', 'expire', 'minimum', 'ttl'
    ],
    'SRV' => [ 'target',  'port', 'weight', 'priority', 'ttl' ],
    'TXT' => [ 'txtdata', 'ttl' ]
);

sub TIEHASH {
    my $class = shift;
    my $args  = shift;

    if ( defined($args) ) {
        die "Bad argument format" unless ( ref($args) eq 'HASH' );
    }
    else {
        $args = {};
    }

    my $self = {};
    bless $self, $class;
    
    $self->{'dns'} = new Net::DNS::Resolver(%{ ($args->{resolver_args} || {}) });

    $self->args($args);

    return ($self);
}

sub STORE {    #Dynamic update.  Oh my.  :-)
    my $self  = shift;
    my $key   = shift;
    my $value = shift;

    my $root_server = $self->get_root_server
      || die("Dynamic update attempted but no (or bad) domain specified.");

    my $update        = new Net::DNS::Update( $self->_get_arg('domain') );
    my $update_string = sprintf( '%s. %s %s %s',
        $key, $self->{'ttl'}, $self->{'lookup_type'}, $value );
    $update->push( 'update', rr_add($update_string) );

    my $res = new Net::DNS::Resolver(%{ ($self->args->{resolver_args} || {}) });
    $res->nameservers($root_server);
    my $reply = $res->send($update);
    if ( defined($reply) ) {
        if ( $reply->header->rcode eq 'NOERROR' ) {
            return ($value);
        }
        else {
            $self->{'errstring'} = $self->{'dns'}->errorstring;
            return (undef);
        }
    }
    else {
        $self->{'errstring'} = $self->{'dns'}->errorstring;
        return (undef);
    }
}

sub args {
    my $self = shift;
    my $args = shift;
    $self->{'args'} = $args;
    $self->_process_args;
}

sub FETCH {
    my $self   = shift;
    my $lookup = shift;

    if ( $lookup =~ /^\d+\.\d+\.\d+\.\d+$/ ) {
        return ( $self->do_reverse_lookup($lookup) );
    }
    else {
        return ( $self->do_forward_lookup($lookup) );
    }
}

sub FIRSTKEY {
    my $self      = shift;
    my @full_zone = $self->{'dns'}->axfr( $self->{'root_name_server'} );
    if ( scalar(@full_zone) == 0 ) {
        $self->{'errstring'} = $self->{'dns'}->errorstring;
        return (undef);
    }

    my @zone;
    foreach my $rr (@full_zone) {
        push( @zone, $rr ) if ( $rr->type eq 'A' );
    }
    my $rr = shift(@zone);
    $self->{'zone'} = \@zone;
    return ( $rr->name );
}

sub NEXTKEY {
    my $self = shift;
    my @zone = @{ $self->{'zone'} };
    if ( scalar(@zone) == 0 ) {
        return (undef);
    }
    my $rr = shift(@zone);
    $self->{'zone'} = \@zone;
    return ( $rr->name );
}

sub CLEAR {
    my $self = shift;

    #	die ("dynamic DNS updates are not yet available.");
}

sub DELETE {
    my $self = shift;
    die('Tie::DNS: DELETE function not implemented');
}

sub DESTROY {
    my $self = shift;

    #There isn't any real Net::DNS requirement to call anything when
    #we go bye-bye, so we'll just go bye-bye quietly.
}

sub _process_args {
    my $self = shift;

    if ( defined( $self->_get_arg('domain') ) ) {    #find the root name
                                                     #server for this domain
        $self->{'root_name_server'} = $self->get_root_server;
        $self->{'dns'}->nameservers( $self->{'root_name_server'} );
    }

    if ( defined( $self->_get_arg('multiple') ) ) {    #multiple return
            #objects
            #I don't think there's any setup required for this.
    }

    if ( defined( $self->_get_arg('all_fields') ) ) {    #all fields
            #I don't think there's any setup for this one either.
    }

    if ( defined( $self->_get_arg('type') ) ) {
        if ( !defined( $config_type{ $self->_get_arg('type') } ) ) {
            die( 'Bad record type: ' . $self->_get_arg('type') );
        }
        $self->{'lookup_type'} = $self->_get_arg('type');
    }
    else {
        $self->{'lookup_type'} = 'A';
    }

    if ( defined( $self->_get_arg('ttl') ) ) {
        $self->{'ttl'} = $self->_get_arg('ttl');
    }
    else {
        $self->{'ttl'} = 86400;
    }

    if ( my $cache_param = $self->_get_arg('cache') ) {
        eval { require Tie::Cache; };
        unless ($@) {
            tie my %cache, 'Tie::Cache', $cache_param;
            $self->{cache} = \%cache;
        }
    }
    else {
        delete $self->{'cache'};
    }
}

sub get_root_server {
    my $self = shift;
    my $query = $self->{'dns'}->query( $self->_get_arg('domain'), 'SOA' );
    if ($query) {
        foreach my $rr ( $query->answer ) {
            print "Root: $rr->mname\n";
            return ( $rr->mname );
        }
    }
    else {
        die 'Domain specified, but unable to get SOA record: '
          . $self->{'dns'}->errorstring;
    }
}

sub _get_arg {
    my $self     = shift;
    my $arg_name = shift;
    return (undef) unless ( defined( $self->{'args'} ) );

    return $self->{'args'}{$arg_name};
}

sub do_reverse_lookup {
    my $self   = shift;
    my $lookup = shift;

    my $query = $self->{'dns'}->search($lookup);
    my @retvals;
    if ($query) {
        foreach my $rr ( $query->answer ) {
            next unless $rr->type eq 'PTR';
            push( @retvals, $rr->ptrdname );
        }
    }
    else {
        $self->{'errstring'} = $self->{'dns'}->errorstring;
        return (undef);
    }
    if ( defined( $self->_get_arg('multiple') ) ) {
        return ( \@retvals );
    }
    else {
        return ( shift(@retvals) );
    }
}

sub do_forward_lookup {
    my $self   = shift;
    my $lookup = shift;
    my @things = $self->_lookup_to_thing($lookup);
    if ( defined( $self->_get_arg('multiple') ) ) {
        return ( \@things );
    }
    else {
        return ( shift(@things) );
    }
}

sub _lookup_to_thing {
    my $self   = shift;
    my $lookup = shift;

    my $ttl   = 0;
    my $now   = time();
    my $cache = $self->{cache};

    if ( $cache and my $old = $cache->{$lookup} ) {
        my ( $expire, $ret ) = @$old;
        if ( $now > $expire ) {
            delete $cache->{$lookup};
        }
        else {
            return @$ret;
        }
    }

    my $query = $self->{'dns'}->search( $lookup, $self->{'lookup_type'} );

    my @retvals;
    if ($query) {
        foreach my $rr ( $query->answer ) {
            $ttl ||= $rr->{ttl};
            next unless ( $rr->type eq $self->{'lookup_type'} );
            if ( defined( $self->_get_arg('all_fields') ) ) {
                my %fields;
                foreach
                  my $field ( @{ $config_type{ $self->{'lookup_type'} } } )
                {
                    $fields{$field} = $rr->{$field};
                }
                push( @retvals, \%fields );
            }
            else {
                push(
                    @retvals,
                    $rr->{
                        $config_rec_defaults{ $self->{'lookup_type'} }

                          #					$config_type{$self->{'lookup_type'}}[0]
                      }
                );
            }
        }
    }
    else {
        $self->{'errstring'} = $self->{'dns'}->errorstring;
    }

    if ($cache) {
        $cache->{$lookup} = [ $now + $ttl, \@retvals ];
    }
    @retvals;
}

sub error {
    my $self = shift;
    return ( $self->{'errstring'} );
}

1;
__END__

=head1 NAME

Tie::DNS - Tie interface to Net::DNS

=head1 SYNOPSIS

  use Tie::DNS;

  tie(my %dns, 'Tie::DNS');

  print "$dns{'foo.bar.com'}\n";

  print "$dns{'208.180.41.1'}\n";

=head1 DESCRIPTION 

Net::DNS is a very complete, extensive and well-written module.  
It's completeness, however, makes many comman cases uses a bit
wordy, code-wise.  Tie::DNS is meant to make common DNS operations
trivial, and more complex DNS operations easier.

=head1 EXAMPLES

=head2 Forward lookup

See Above.

=head2 Zone transfer

Get all of the A records from 'foo.com'.  (Sorry foo.com if
everyone hits your name server testing this module.  :-)

  tie (%dns, 'Tie::DNS', {'Domain' => 'foo.com'});

  while (my ($name, $ip) = each %dns) {
        print "$name = $ip\n";
  }

This obviously requires that your host has zone transfer
privileges with a name server hosting that zone.  The
zone transfer is initiated with the first each, keys or
values operation.  The tie operation does a SOA query
to find the name server for the cited zone.

=head2 Fetching multiple records

Pass the configuration parameter of 'multiple' to any Perl true 
value, and all FETCH values from Tie::DNS will be an array
reference of records.

  tie (my %dns, 'Tie::DNS', {'multiple' => 'true'});

  my $ip_ref = $dns{'cnn.com'};
  foreach (@{$ip_ref}) {
	print "Address: $_\n";
  }


=head2 Fetching records of type besides 'A'

Pass the configuration parameter of 'type' to one of the
Net::DNS supported record types causes all FETCHes to
get records of that type.

  tie (%dns, 'Tie::DNS', {	'multiple' => 'true',
				'type' => 'SOA'});

  my $ip_ref = $dns{'cnn.com'};
  foreach (@{$ip_ref}) {
	print "primary nameserver: $_\n";
  }

Here are the most popular types supported:

  CNAME - Returns the records canonical name.
  A - Returns the records address field.
  TXT - Returns the descriptive text.
  MX - Returns name of this mail exchange.
  NS - Returns the domain name of the nameserver.
  PTR - Returns the domain name associated with this record.
  SOA - Returns the domain name of the original or
      nameserver for this zone.

  (The descriptions are right out of the Net::DNS POD.)

See Net::DNS documentation for further information about these
types and a comprehensive list of all available types.

=head2 Fetching all of the fields associated with a given record type.

  tie (%dns, 'Tie::DNS', {'type' => 'SOA'});

  my $dns_ref = $dns{'cnn.com'};
  foreach my $field (keys %{$dns_ref}) {
	print "$field = " . ${$dns_ref}{$field} . "\n";
  }

This code fragment will print all of the SOA fields associated
with cnn.com.

=head2 Caching

The argument 'cache' will cause the DNS results to be cached.  The default
is no caching.  The 'cache' argument is passed through to L<Tie::Cache>.
If L<Tie::Cache> cannot be loaded, caching will be disabled.  Entries
whose DNS TTL has expired will be re-queried automatically.

  tie (%dns, 'Tie::DNS', { cache => 100 });
  print "$dns{'cnn.com'}\n";
  print "$dns{'cnn.com'}\n";  ## cached!

=head2 Getting all/different fields associated with a record

  tie (%dns, 'Tie::DNS', {'all_fields' => 'true'});
  my $dns_ref = $dns{'cnn.com'};
  print $dns_ref->{'ttl'}, "\n";

=head2 Passing arguments to Net::DNS::Resolver->new()

  tie my %from_localhost, "Tie::DNS", { resolver_args => { nameservers => ['127.0.0.1'] } };
  print "$from_localhost{'test.local'}\n";

You can pass arbitrary arguments to the Net::DNS::Resolver constructor by 
setting the C<resolver_args> argument. In the example above, an alternative
nameserver is used instead of the default one.

=head2 Changing various arguments to the tie on the fly

  tie (%dns, 'Tie::DNS', {'type' => 'SOA'});
  print $dns{'cnn.com'} . "\n";

  tied(%dns)->args({'type' => 'A'});
  print $dns{'cnn.com'} . "\n";

This code fragment first does an SOA query for cnn.com, and then
changes the default mode to A queries, and displays that.

=head2 Simple Dynamic Updates

Assign into the hash, key DNS name, value IP address, to add a record
to the zone in the domain argument.  For instance:

  tie (%dns, 'Tie::DNS', {
		'domain' => 'realms.lan',
		'multiple' => 'true'});

  $dns{'food.realms.lan.'} = '131.22.40.1';

  foreach (@{$dns{'food'}}) {
        print " $_\n";
  }

=head2 Methods

=head3 error

Returns the last error, either from Tie::DNS or Net::DNS

=head3 get_root_server

Returns the root name server.

=head3 do_forward_lookup

Returns the results of a forward lookup.

=head3 do_reverse_lookup

Returns the results of a reverse lookup.

=head3 args

Change various arguments to the tie on the fly.

=head1 TODO

This 0.62 release supports the basic functionality of 
Net::DNS.  The 1.0 release will support the following:

Different access methods for forward and reverse lookups.

The 2.0 release will strive to support DNS security options.

=head1 AUTHOR

Dana M. Diederich <dana@realms.org>

=head1 ACKNOWLEDGMENTS

kevin Brintnall <kbrint@rufus.net> for Caching patch
Alvar Freude <alvar@a-blast.org> for arguments to resolver patch

=head1 BUGS

in-addr.arpa zone transfers aren't yet supported.

Patches, flames, opinions, enhancement ideas are all welcome.

=head1 COPYRIGHT 
Copyright (c) 2009,2013 Dana M. Diederich. All Rights Reserved.
This module is free software. It may be used, redistributed
and/or modified under the terms of the Perl Artistic License
  (see http://www.perl.com/perl/misc/Artistic.html)

=cut
