# Copyright Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.

# Copyright (C) Stichting NLnet, Netherlands, stichting@nlnet.nl.
# Copyright (C) John Eaglesham
#
# The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
# conceived and contributed by Rob Butler.
#
# SPDX-License-Identifier: ISC and MPL-2.0
#
# Permission to use, copy, modify, and distribute this software for any purpose
# with or without fee is hereby granted, provided that the above copyright
# notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND STICHTING NLNET DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL STICHTING NLNET BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
# OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package dlz_perl_example;

use warnings;
use strict;

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;

# Constructor. Everything after the class name can be folded into a hash of
# various options and settings. Right now only log_context and argv are
# available.
sub new {
    my ( $class, %config ) = @_;
    my $self = {};
    bless $self, $class;

    $self->{log} = sub {
        my ( $level, $msg ) = @_;
        DLZ_Perl::log( $config{log_context}, $level, $msg );
    };

    if ( $config{argv} ) { warn "Got argv: $config{argv}\n"; }

    $self->{zones} = {
        'example.com' => {
            '@' => [
                {
                    type => 'SOA',
                    ttl  => 86400,
                    data =>
                     'ns1.example.com. hostmaster.example.com. 12345 172800 900 1209600 3600',
                }
            ],
            perlrr => [
                {
                    type => 'A',
                    ttl  => 444,
                    data => '1.1.1.1',
                },
                {
                    type => 'A',
                    ttl  => 444,
                    data => '1.1.1.2',
                }
            ],
            perltime => [
                {
                    code => sub {
                        return ['TXT', '1', time()];
                    },
                },
            ],
            sourceip => [
                {
                    code => sub {
                        my ( $opaque ) = @_;
                        # Passing anything other than the proper opaque value,
                        # 0, or undef to this function will cause a crash (at
                        # best!).
                        my ( $addr, $port ) =
                         DLZ_Perl::clientinfo::sourceip( $opaque );
                        if ( !$addr ) { $addr = $port = 'unknown'; }
                        return ['TXT', '1', $addr], ['TXT', '1', $port];
                    },
                },
            ],
        },
    };

    $self->{log}->(
        DLZ_Perl::LOG_INFO(),
        'DLZ Perl Script: Called init. Loaded zone data: '
         . Dumper( $self->{zones} )
    );
    return $self;
}

# Do we have data for this zone? Expects a simple true or false return value.
sub findzone {
    my ( $self, $zone ) = @_;
    $self->{log}->(
        DLZ_Perl::LOG_INFO(),
        "DLZ Perl Script: Called findzone, looking for zone $zone"
    );

    return exists $self->{zones}->{$zone};
}

# Return the data for a given record in a given zone. The final parameter is
# an opaque value that can be passed to DLZ_Perl::clientinfo::sourceip to
# retrieve the client source IP and port. Expected return value is an array
# of array refs, with each array ref representing one record and containing
# the type, ttl, and data in that order. Data is as it appears in a zone file.
sub lookup {
    my ( $self, $name, $zone, $client_info ) = @_;
    $self->{log}->(
        DLZ_Perl::LOG_INFO(),
        "DLZ Perl Script: Called lookup, looking for record $name in zone $zone"
    );
    return unless $self->{zones}->{$zone}->{$name};

    my @results;
    foreach my $rr ( @{ $self->{zones}->{$zone}->{$name} } ) {
        if ( $rr->{'code'} ) {
            my @r = $rr->{'code'}->( $client_info );
            if ( @r ) {
                push @results, @r;
            }
        } else {
            push @results, [$rr->{'type'}, $rr->{'ttl'}, $rr->{'data'}];
        }
    }

    return @results;
}

# Will we allow zone transfer for this client? Expects a simple true or false
# return value.
sub allowzonexfr {
    my ( $self, $zone, $client ) = @_;
    $self->{log}->(
        DLZ_Perl::LOG_INFO(),
        "DLZ Perl Script: Called allowzonexfr, looking for zone $zone for " .
        "client $client"
    );
    if ( $client eq '127.0.0.1' ) { return 1; }
    return 0;
}

# Note the return AoA for this method differs from lookup in that it must
# return the name of the record as well as the other data.
sub allnodes {
    my ( $self, $zone ) = @_;
    my @results;
    $self->{log}->(
        DLZ_Perl::LOG_INFO(),
        "DLZ Perl Script: Called allnodes, looking for zone $zone"
    );

    foreach my $name ( keys %{ $self->{zones}->{$zone} } ) {
        foreach my $rr ( @{ $self->{zones}->{$zone}->{$name} } ) {
            if ( $rr->{'code'} ) {
                my @r = $rr->{'code'}->();
                # The code returns an array of array refs without the name.
                # This makes things easy for lookup but hard here. We must
                # iterate over each array ref and inject the name into it.
                foreach my $a ( @r ) {
                    unshift @{$a}, $name;
                }
                push @results, @r;
            } else {
                push @results,
                 [$name, $rr->{'type'}, $rr->{'ttl'}, $rr->{'data'}];
            }
        }
    }
    return @results;
}

1;
