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

# cpanel - bin/build_mydns_conf                    Copyright 2022 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

BEGIN {
    # not used but auto loaded
    $INC{'IO/Socket/UNIX.pm'} = '__FAKEOUT__';
}

use Cpanel::Config::LoadCpConf           ();
use Cpanel::MysqlUtils::Connect          ();
use Cpanel::MysqlUtils::Command          ();
use Cpanel::MysqlUtils::Quote            ();
use Cpanel::MysqlUtils::MyCnf::Basic     ();
use Cpanel::NameServer::Conf::Mydns      ();
use Cpanel::PasswdStrength::Generate     ();
use Cpanel::Sys::Hostname                ();
use Cpanel::NameServer::Utils::MyDNS::DB ();

use Try::Tiny;

if ( $> != 0 ) {
    die "Insufficient permissions to install MyDNS\n";
}

my $cpconf_ref = Cpanel::Config::LoadCpConf::loadcpconf();
if ( $cpconf_ref->{'local_nameserver_type'} ne 'mydns' ) {
    die "MyDNS is not current name server\n";
}

my $hostname = Cpanel::Sys::Hostname::gethostname();

my $dbh;
my $dbserver = Cpanel::MysqlUtils::MyCnf::Basic::getmydbhost('root') || 'localhost';
my $dbport   = Cpanel::MysqlUtils::MyCnf::Basic::getmydbport('root') || 3306;
try {
    $dbh = Cpanel::MysqlUtils::Connect::get_dbi_handle();
}
catch {
    die "Error while connecting to MySQL database: $_";
};

check_mysql();

my $namedconf_obj = Cpanel::NameServer::Conf::Mydns->new;
$namedconf_obj->initialize();

unless ( -x $namedconf_obj->{'dnsbin'} ) {
    die "No mydns binary is found\n";
}

if ( defined $ARGV[0] && $ARGV[0] eq '--createdb' ) {
    if ( create_db(1) ) {
        create_soa_table();
        create_rr_table();
        run_migrations( $dbh, $namedconf_obj->{'dns_settings'} );
    }
    exit;
}

my $fresh_install     = ( !-e '/etc/mydns.conf' || $namedconf_obj->{'dns_settings'}->{'db-user'} eq 'username' ) ? 1 : 0;
my $mydns_is_sanitize = Cpanel::NameServer::Utils::MyDNS::DB::sanity_check();

# create db and tables if fresh install or something is missing
if ( $fresh_install || !$mydns_is_sanitize ) {
    ( $namedconf_obj->{'dns_settings'}->{'db-user'}, $namedconf_obj->{'dns_settings'}->{'db-password'} ) = create_dbuser();
    $namedconf_obj->{'dns_settings'}->{'db-host'}   = $dbserver . ':' . $dbport;
    $namedconf_obj->{'dns_settings'}->{'database'}  = create_db();
    $namedconf_obj->{'dns_settings'}->{'soa-table'} = create_soa_table();
    $namedconf_obj->{'dns_settings'}->{'rr-table'}  = create_rr_table();
    $namedconf_obj->{'dns_settings'}->{'allow-tcp'} = 'yes';
}

if ( $fresh_install || !$mydns_is_sanitize || $namedconf_obj->{'dns_settings'}->{'user'} eq 'nobody' || $namedconf_obj->{'dns_settings'}->{'group'} eq 'nobody' ) {
    $namedconf_obj->{'dns_settings'}->{'user'}  = 'named';
    $namedconf_obj->{'dns_settings'}->{'group'} = 'named';
}

$namedconf_obj->flushsettings();

run_migrations( $dbh, $namedconf_obj->{'dns_settings'} );

exit;

sub run_migrations {
    my ( $dbh, $settings ) = @_;

    return unless $dbh && $dbh->ping && $settings->{'database'};

    # use the current database
    $dbh->do( 'use ' . $settings->{'database'} );

    my $table = 'migrations';
    my $query = qq{CREATE TABLE IF NOT EXISTS `$table` ( `version` varchar(255) NOT NULL,
    UNIQUE KEY `unique_schema_migrations` (`version`))};
    $dbh->do($query);

    my $rr_table = $settings->{'rr-table'};

    # we should start with the default definition coming from the rpm
    #   then apply a patch for any changes we made to the schema
    my $migrations = {
        '2013052801' => sub {

            # Case 52969: Make MyDNS handle TXT records properly
            $dbh->do("ALTER TABLE `$rr_table` MODIFY `data` VARBINARY(512) NOT NULL");
        },
        '2013052802' => sub {

            # Case 69185: MyDNS silently truncates DNS names to 64 characters
            $dbh->do("ALTER TABLE `$rr_table` MODIFY `name` CHAR(255) NOT NULL");
        },
    };

    my $check  = $dbh->prepare("select 1 from $table where version = ? limit 1");
    my $update = $dbh->prepare("insert into $table ( version ) values ( ? )");

    foreach my $id ( sort keys %$migrations ) {
        $check->execute($id);
        my $run = ( $check->fetchrow_array() )[0];
        next if $run;
        my $migrate = $migrations->{$id};
        print "Running migration: $id\n";
        eval { $migrate->() };
        next if $@;
        $update->execute($id);
    }
}

sub check_mysql {
    print "Checking database connection...\n";
    my $sth = $dbh->prepare('show engines;');
    unless ($sth) {
        die "Database error, unable to show engines in MySQL: " . $dbh->errstr;
    }
    unless ( $sth->execute() ) {
        die "Database error, unable to show engines in MySQL: " . $sth->errstr;
    }

    $sth->finish;

    return;
}

sub create_dbuser {
    my $sth          = undef;
    my $settings_ref = $namedconf_obj->{'dns_settings'};
    my $db_user      = ( $settings_ref->{'db-user'}     && $settings_ref->{'db-user'} ne 'username' )     ? $settings_ref->{'db-user'}     : 'cpmydns';
    my $db_password  = ( $settings_ref->{'db-password'} && $settings_ref->{'db-password'} ne 'password' ) ? $settings_ref->{'db-password'} : Cpanel::PasswdStrength::Generate::generate_password(10);

    unless ( $sth = $dbh->prepare("select count(*) from mysql.user U where U.user = ?") ) {
        die "Database error, unable to check database user for MyDNS: " . $dbh->errstr;
    }

    my $user_exists = 1;
    my $suffix      = 1;
    my $base        = $db_user;

    # base should not contain a number at the end
    $base =~ s/\d+$//;
    while ($user_exists) {
        unless ( $sth->execute($db_user) ) {
            die "Database error, unable to check database user for MyDNS: " . $sth->errstr;
        }
        $user_exists = $sth->fetchrow_array;
        if ($user_exists) {
            $db_user = $base . "$suffix";
            $suffix++;
        }
    }
    $sth->finish;

    if ($user_exists) {
        print "Database user ($db_user) already exists in MySQL.\n";
    }
    else {
        print "Creating database user ($db_user) for MyDNS...\n";

        my $query = "create user '" . $db_user . "'" . '@' . "'localhost' identified by ?;";
        unless ( $sth = $dbh->prepare($query) ) {
            die "Database error, unable to create database user for MyDNS: " . $dbh->errstr;
        }
        unless ( $sth->execute($db_password) ) {
            die "Database error, unable to create database user for MyDNS: " . $sth->errstr;
        }
        $sth->finish;
        $query = "create user '" . $db_user . "'" . '@' . "'$hostname' identified by ?;";
        unless ( $sth = $dbh->prepare($query) ) {
            die "Database error, unable to create database user for MyDNS: " . $dbh->errstr;
        }
        unless ( $sth->execute($db_password) ) {
            die "Database error, unable to create database user for MyDNS: " . $sth->errstr;
        }
        $sth->finish;
        $user_exists = 1;
    }

    return ( $db_user, $db_password );
}

sub create_db {
    my $dont_continue_if_exists = shift;

    my $sth          = undef;
    my $settings_ref = $namedconf_obj->{'dns_settings'};

    my @names = split( /\./, $hostname );
    $names[0] =~ s/-/_/g;
    @names = split( /[^a-zA-Z0-9_]+/, $names[0] );

    my $dns_db = $fresh_install ? 'mydns' . '_' . $names[0] : $settings_ref->{'database'};

    unless ( $sth = $dbh->prepare("show databases;") ) {
        die "Database error, unable to check database for MyDNS: " . $dbh->errstr;
    }
    unless ( $sth->execute ) {
        die "Database error, unable to check database for MyDNS: " . $sth->errstr;
    }

    my (%databases) = ();
    while ( my $data = $sth->fetchrow_array() ) {
        $databases{$data} = 1;
    }
    $sth->finish;
    my ($db_exists) = exists $databases{$dns_db} ? 1 : 0;
    return if $db_exists && $dont_continue_if_exists;
    my $suffix = 1;
    my $base   = $dns_db;

    # base should not contain a number at the end
    $base =~ s/\d+$//;
    while ($db_exists) {
        $dns_db = $base . "$suffix";
        $suffix++;
        $db_exists = exists $databases{$dns_db} ? 1 : 0;
    }

    my $query;
    if ($db_exists) {
        print "Database ($dns_db) already exists in MySQL\n";
    }
    else {
        print "Creating database ($dns_db) for MyDNS...\n";
        $query = "create database $dns_db;";
        unless ( $dbh->do($query) ) {
            die "Database error, unable to create database for MyDNS: " . $dbh->errstr;
        }
    }

    $query = "grant select on $dns_db" . '.*' . " to '$settings_ref->{'db-user'}'" . '@' . "'localhost'";
    unless ( $dbh->do($query) ) {
        die "Database error, unable to update rights for database user: " . $dbh->errstr;
    }

    $query = "grant select on $dns_db" . '.*' . " to '$settings_ref->{'db-user'}'" . '@' . "'$hostname'";
    unless ( $dbh->do($query) ) {
        die "Database error, unable to update rights for database user: " . $dbh->errstr;
    }

    my $clientip = Cpanel::MysqlUtils::Quote::safesqlstring( Cpanel::MysqlUtils::Command::sqlcmd('SELECT SUBSTRING_INDEX(`host`,":",1) FROM `information_schema`.`processlist` WHERE ID = CONNECTION_ID();') );
    $query = "grant select on $dns_db" . '.*' . " to '$settings_ref->{'db-user'}'" . '@' . "'$clientip'";
    unless ( $dbh->do($query) ) {
        die "Database error, unable to update rights for database user: " . $dbh->errstr;
    }

    return $dns_db;
}

sub create_soa_table {
    my $sth          = undef;
    my $settings_ref = $namedconf_obj->{'dns_settings'};
    my $soa_table    = $settings_ref->{'soa-table'} ? $settings_ref->{'soa-table'} : 'soa';

    my $query = "show table status in $settings_ref->{'database'};";
    unless ( $sth = $dbh->prepare($query) ) {
        die "Database error, unable to check soa table for MyDNS: " . $dbh->errstr;
    }
    unless ( $sth->execute() ) {
        die "Database error, unable to check soa table for MyDNS: " . $sth->errstr;
    }

    my $table_exists = 0;
    while ( my $data = $sth->fetchrow_hashref ) {
        if ( $data->{'Name'} eq $soa_table ) {
            $table_exists = 1;
            last;
        }
    }
    $sth->finish;

    if ($table_exists) {
        print "SOA table ($soa_table) already exists in MySQL\n";
    }
    else {
        print "Creating SOA table ($soa_table) for MyDNS...\n";
        $dbh->do( 'use ' . $settings_ref->{'database'} );
        $query = <<EOD_QUERY;
    create table $soa_table (
        id         INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
        origin     CHAR(255) NOT NULL,
        ns         CHAR(255) NOT NULL,
        mbox       CHAR(255) NOT NULL,
        serial     INT UNSIGNED NOT NULL default '1',
        refresh    INT UNSIGNED NOT NULL default '28800',
        retry      INT UNSIGNED NOT NULL default '7200',
        expire     INT UNSIGNED NOT NULL default '604800',
        minimum    INT UNSIGNED NOT NULL default '86400',
        ttl        INT UNSIGNED NOT NULL default '86400',
        active     ENUM('Y','N') NOT NULL DEFAULT 'Y',
        UNIQUE KEY (origin)
        ) ;
EOD_QUERY

        unless ( $dbh->do($query) ) {
            die "Database error, unable to create SOA table: " . $dbh->errstr;
        }
    }

    return $soa_table;
}

sub create_rr_table {
    my $sth          = undef;
    my $settings_ref = $namedconf_obj->{'dns_settings'};
    my $rr_table     = $settings_ref->{'rr-table'} ? $settings_ref->{'rr-table'} : 'rr';

    my $query = "show table status in $settings_ref->{'database'};";
    unless ( $sth = $dbh->prepare($query) ) {
        die "Database error, unable to check rr table for MyDNS: " . $dbh->errstr;
    }
    unless ( $sth->execute() ) {
        die "Database error, unable to check rr table for MyDNS: " . $sth->errstr;
    }

    my $table_exists = 0;
    while ( my $data = $sth->fetchrow_hashref ) {
        if ( $data->{'Name'} eq $rr_table ) {
            $table_exists = 1;
            last;
        }
    }
    $sth->finish;

    if ($table_exists) {
        print "RR table ($rr_table) already exists in MySQL\n";
    }
    else {
        print "Creating RR table ($rr_table) for MyDNS...\n";
        $dbh->do( 'use ' . $settings_ref->{'database'} );
        $query = <<EOD_QUERY;

    create table $rr_table (
        id         INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
        zone       INT UNSIGNED NOT NULL,
        name       CHAR(255) NOT NULL,
        data       VARBINARY(512) NOT NULL,
        aux        INT UNSIGNED NOT NULL,
        ttl        INT UNSIGNED NOT NULL default '86400',
        type       ENUM('A','AAAA','CNAME','HINFO','MX','NAPTR','NS','PTR','RP','SRV','TXT'),
        active     ENUM('Y','N','D') NOT NULL DEFAULT 'Y',
        serial     INT UNSIGNED DEFAULT NULL,
        UNIQUE KEY rr (zone,name,type,data)
        ) ;
EOD_QUERY

        unless ( $dbh->do($query) ) {
            die "Database error, unable to create rr table: " . $dbh->errstr;
        }
    }

    return $rr_table;
}
