[ SEA-GHOST MINI SHELL]

Path : /scripts/
FILE UPLOADER :
Current File : //scripts/migrate-pdns-conf

#!/usr/local/cpanel/3rdparty/bin/perl

#                                      Copyright 2024 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package scripts::migrate_pdns_conf;

use strict;
use warnings;

use Try::Tiny;

use Pod::Usage               ();
use Getopt::Long             ();
use Cpanel::LoadFile         ();
use Cpanel::Exception        ();
use Cpanel::FileUtils::Write ();
use Cpanel::Rand::Get        ();
use Cpanel::PwCache          ();

my $CONF_FILE = '/etc/pdns/pdns.conf';

exit( __PACKAGE__->script( \@ARGV ) ) unless caller;

sub script {
    my ( $class, $argv ) = @_;

    die Cpanel::Exception::create('RootRequired')->to_string_no_id() unless ( $> == 0 && $< == 0 );
    my $self = bless { 'notify' => 1 }, $class;

    my $help;
    Getopt::Long::GetOptionsFromArray(
        $argv,
        'notify!'    => \$self->{'notify'},
        'dry-run'    => \$self->{'dry-run'},
        'man|help|h' => \$help,
    ) or return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -output => \*STDERR, -verbose => 99, -sections => [qw(NAME DESCRIPTION SYNOPSIS)] );

    # -1 to get the right exit code
    return Pod::Usage::pod2usage( -exitval => 'NOEXIT', -output => \*STDOUT, -verbose => 99, -sections => [qw(NAME DESCRIPTION SYNOPSIS)] ) - 1 if $help;

    return _nofile() unless -e $CONF_FILE;

    my $current_conf = Cpanel::LoadFile::loadfileasarrayref($CONF_FILE);
    my $changes      = $self->migrate_conf($current_conf);
    $changes->{enabled} = $self->enable_settings($current_conf);

    return _nochanges() if !_has_changes($changes);

    _print_changes($changes);
    return 0 if $self->{'dry-run'};

    if ( _overwrite_and_fix_ownership( $CONF_FILE, join( '', @{$current_conf} ) ) ) {
        print "[+] Updated $CONF_FILE successfully\n";
        $self->send_notification($changes)
          if $self->{'notify'};
    }
    else {
        print "[!] Failed to update $CONF_FILE: $!\n";
        return 1;
    }

    return 0;
}

sub migrate_conf {
    my ( $self, $current_conf ) = @_;

    my $changes = {
        'removed' => [],
        'renamed' => [],
        'manual'  => [],
    };

    my @to_remove = qw/
      pipebackend-abi-version
      strict-rfc-axfrs
      send-root-referral
      experimental-lua-policy-script
      allow-recursion
      recursive-cache-ttl
      recursor
      api-readonly
      experimental-api-readonly
      api-logfile
      default-soa-mail
      default-soa-name
      soa-expire-default
      soa-minimum-ttl
      soa-refresh-default
      soa-retry-default
      /;
    my %remove_if_value_starts_with = (
        'local-ipv6' => '::',    #No longer force pdns to bind to ::
    );

    my %renamed = (
        'allow-dns-update-from'       => 'allow-dnsupdate-from',
        'allow-unsigned-supermaster'  => 'allow-unsigned-autoprimary',
        'experimental-json-interface' => 'api',
        'forward-dnsupdates'          => 'forward-dnsupdate',
        'local-ipv6-nonexist-fail'    => 'local-address-nonexist-fail',
        'master'                      => 'primary',
        'slave'                       => 'secondary',
        'slave-cycle-interval'        => 'xfr-cycle-interval',
        'slave-renotify'              => 'secondary-do-renotify',
        'superslave'                  => 'autosecondary',
        ( map { $_ => substr( $_, 0, -1 ) } qw{default-ksk-algorithms default-zsk-algorithms} ),
        ( map { $_ => substr( $_, 13 ) } qw{experimental-api-key experimental-dnsupdate experimental-dname-processing} ),
    );

    foreach my $line ( @{$current_conf} ) {
        next if $line =~ m/^\s*(#|$)/;

        # Parse it first to make life easier and run less regexes overall
        my ( $key, $value ) = $line =~ /^\s*([^=]+?)\s*=\s*(.*?)\s*$/;

        if ( my @remove = grep { $_ eq $key } @to_remove ) {
            push @{ $changes->{'removed'} }, @remove;
            $line = '#' . $line;
        }
        elsif ( exists( $remove_if_value_starts_with{$key} ) && index( $value, $remove_if_value_starts_with{$key} ) == 0 ) {
            push @{ $changes->{'removed'} }, "$key=$remove_if_value_starts_with{$key}";
            $line = '#' . $line;
        }
        elsif ( exists( $renamed{$key} ) ) {

            # Possibly could have made this conditional on array of things?
            # Anyways force list context as otherwise perl whines about it.
            if ( ( grep { $_ eq $key } qw{default-ksk-algorithms default-zsk-algorithms} ) && split( /,/, $value ) > 1 ) {
                $line = '#' . $line;
                push @{ $changes->{'manual'} }, $key;
                next;
            }
            push @{ $changes->{'renamed'} }, { $key => $renamed{$key} };
            $line = "$renamed{$key}=$value\n";
        }
    }

    return $changes;
}

sub enable_settings {
    my ( $self, $current_conf ) = @_;

    my @changes;

    my %to_enable = (
        'webserver'                  => 'yes',
        'api'                        => 'yes',
        'webserver-address'          => '127.0.0.1',
        'webserver-allow-from'       => '127.0.0.1,::1',
        'webserver-port'             => '953',
        'api-key'                    => undef,
        'webserver-password'         => undef,
        'bind-ignore-broken-records' => 'yes',
        'upgrade-unknown-types'      => '1',
    );

    my %seen_settings;

    foreach my $line ( @{$current_conf} ) {
        next if $line =~ m/^\s*(#|$)/;

        if ( $line =~ m{^\s*bind-dnssec-db\s*=\s*/etc/pdns/dnssec\.db\s*} ) {
            $line = "bind-dnssec-db=/var/cpanel/pdns/dnssec.db\n";
            push( @changes, { 'bind-dnssec-db' => '/var/cpanel/pdns/dnssec.db' } );
            next;
        }

        my ( $key, $value ) = ( $line =~ /^\s*([^\s=]+)\s*=\s*(\S+)?\s*$/ );
        next unless defined $key;
        next unless exists $to_enable{$key};

        # Remove duplicate, empty and templated values
        if ( $seen_settings{$key} || !defined($value) || ( $value eq '@@REPLACE@@' || $value eq '@@REPLACE_PASS@@' ) ) {
            $line = '';
            next;
        }

        $seen_settings{$key} = 1;

        # leave generated credentials as-is
        next if ( !defined( $to_enable{$key} ) );

        # leave correct settings alone
        next if ( $value eq $to_enable{$key} );

        # fix setting
        $line = "$key=$to_enable{$key}\n";
        push( @changes, { $key => $to_enable{$key} } );
    }

    # add missing settings
    foreach my $key ( keys %to_enable ) {
        next if $seen_settings{$key};
        push( @changes, { $key => ( $to_enable{$key} // '***HIDDEN***' ) } );
        $to_enable{$key} //= Cpanel::Rand::Get::getranddata(16);
        unshift( @{$current_conf}, "$key=$to_enable{$key}\n" );
    }

    return \@changes;
}

sub _has_changes {
    my $changes = shift;

    foreach my $type (qw/removed renamed manual enabled/) {
        return 1 if scalar @{ $changes->{$type} };
    }
    return 0;
}

sub _print_changes {
    my $changes = shift;

    foreach my $removed ( @{ $changes->{'removed'} } ) {
        print "[*] Deprecated directive: '$removed' will be disabled.\n";
    }
    foreach my $renamed ( @{ $changes->{'renamed'} } ) {
        print "[*] Renamed directive: '$_' will be updated to '$renamed->{$_}'.\n" foreach keys %{$renamed};
    }
    foreach my $manual ( @{ $changes->{'manual'} } ) {
        print "[*] Deprecated configuration: '$manual' will be disabled as this requires admin intervention.\n";
    }
    foreach my $enabled ( @{ $changes->{'enabled'} } ) {
        print "[*] New enabled settings: The '$_' directive was added and set to '$enabled->{$_}'.\n" foreach keys %{$enabled};
    }

    return 0;
}

sub _nochanges {
    print "[+] $CONF_FILE does not contain any directives that need to be updated.\n";
    return 0;
}

sub _nofile {
    print "[*] $CONF_FILE is not present on the system. Nothing to do.\n";
    return 0;
}

sub send_notification {
    my ( $self, $changes ) = @_;

    return if $ENV{'CPANEL_BASE_INSTALL'};    # Nothing is setup. it's meaningless to notify here.
    my $old = $self->_locale()->set_context_plain();

    require Cpanel::Notify;
    my $ic_obj = Cpanel::Notify::notification_class(
        'class'            => 'Check::PdnsConf',
        'application'      => 'Check::PdnsConf',
        'status'           => 1,
        'constructor_args' => [
            'origin'    => 'migrate-pdns-conf',
            'skip_send' => 1,
            %{$changes},
        ]
    );
    $ic_obj->send();

    $self->_locale()->set_context($old);
    return 1;
}

sub _locale {
    my ($self) = @_;
    require Cpanel::Locale;
    return ( $self->{'_locale'} ||= Cpanel::Locale->get_handle() );
}

sub _overwrite_and_fix_ownership {
    my ( $conf, $content ) = @_;

    my ( $named_uid, $named_gid ) = ( Cpanel::PwCache::getpwnam_noshadow('named') )[ 2, 3 ];
    my $overwrite_callback = sub {
        my $fh = shift;
        chmod( 0600, $fh );
        chown( $named_uid, $named_gid, $fh );
    };

    return Cpanel::FileUtils::Write::overwrite( $conf, $content, { before_installation => $overwrite_callback } );
}

1;

__END__

=pod

=encoding utf8

=head1 NAME

migrate-pdns-conf

=head1 DESCRIPTION

Utility to update PowerDNS configuration from v3.x to v4.1:

    * Deprecated options will be removed.
    * Renamed configuration directives will be updated to the new names.
    * New settings will be enabled.

=head1 SYNOPSIS

    migrate-pdns-conf [OPTIONS]

    OPTIONS:
    --notify    Send notification about changes made to System Administrator.
                Default: on
                To disable notifications, use --no-notify

    --dry-run   Do a dry-run without altering the file, or sending the notification.
                Prints the changes that would be made to screen.

    --help      This documentation.

=cut

SEA-GHOST - SHELL CODING BY SEA-GHOST