#!/usr/bin/perl

use strict;
use warnings;

use Config::Tiny;
use Crypt::Eksblowfish::Bcrypt qw/bcrypt_hash en_base64/;
use Data::Entropy::Algorithms qw/rand_bits/;
use DBI;
use English;
use Getopt::Long qw(GetOptions);
use Readonly;
use threads;
use threads::shared;
use Data::Dumper;

Readonly my $DB_CFG => "/etc/default/ngcp-db";
Readonly my $DEFAULT_DBHOST => "127.0.0.1";
Readonly my $DEFAULT_DBPORT => "3306";

STDOUT->autoflush(1);

my ($verbose, $man, $help);

sub Usage {
    print <<USAGE;
==
    Creates encrypted passwords from subscribers' webpasswords using bcypt
==
$PROGRAM_NAME [options]
Options:
    --help|-h|-?      -- this help
    --verbose|-v      -- verbose mode
USAGE
    exit 0;
}

GetOptions("h|?|help"          => \&Usage,
           "v|verbose"         => \$verbose);

print "We are going to encrypt all subscribers webpasswords using bcrypt. Are you sure you want to continue? (yes/no): ";
my $answer = <STDIN>;
chomp $answer;
exit 0 unless ( $answer eq 'yes' );

my $dbh;
my $done_cnt :shared = 0;
my $sub_id :shared = 0;

eval {
    chomp(my $cpu_count = `grep -c -P '^processor\\s+:' /proc/cpuinfo`);
    print("CPU number: $cpu_count\n");
    my ($dbhost, $dbport);
    if (my $db_cfg = Config::Tiny->read($DB_CFG)) {
        ($dbhost, $dbport) = @{$db_cfg->{_}}{qw(PAIR_DBHOST PAIR_DBPORT)};
    } else {
        debug(sprintf "Cannot open %s: %s, using host=%s port=%s",
            $DB_CFG, $ERRNO, $DEFAULT_DBHOST, $DEFAULT_DBPORT);
        ($dbhost, $dbport) = ($DEFAULT_DBHOST, $DEFAULT_DBPORT);
    }

    my $dsn = "DBI:mysql:database=mysql;host=${dbhost};port=${dbport}";
    my $dbuser = '';
    my $dbpass = '';

    print "-> connecting to database on host=${dbhost}, port=${dbport}\n";
    my $dbh = DBI->connect($dsn, $dbuser, $dbpass, { PrintError => 1 })
        or die "Can't connect to MySQL database 'mysql': ". $DBI::errstr;

    my $started = time;

    my ($sub_cnt) = int $dbh->selectrow_array(<<SQL)
SELECT COUNT(id) as count
FROM provisioning.voip_subscribers
WHERE webpassword IS NOT NULL
SQL
    or die "Cannot select sub count: ".$DBI::errstr;

    print "-> total subscribers with webpasswords: $sub_cnt\n";

    my %processors = ();
    for (1..$cpu_count) {
        debug('starting generator thread ' . $_ . ' of ' . $cpu_count);
        my $processor = threads->create(\&update_subscribers, $dbhost, $dbport, $started, $sub_cnt);
        if (!defined $processor) {
            debug('thread ' . $_ . ' of ' . $cpu_count . ' is NOT started');
        }
        $processors{$processor->tid()} = $processor;
    }
    while ((scalar keys %processors) > 0) {
        foreach my $processor (values %processors) {
            if (defined $processor and $processor->is_joinable()) {
                $processor->join();
                delete $processors{$processor->tid()};
                debug('generator thread tid ' . $processor->tid() . ' joined');
            }
        }
        sleep(0.1);
    }
};
if ($@) {
    die "Error: $@";
}

$dbh->disconnect if $dbh;

print "Done\n";

sub debug { print $_[0]."\n" if $_[0] && $verbose; }
sub progress { printf "==> %d/%d (ETA: %d sec)\r", $_[0], $_[1], ((time-$_[2])/$_[0])*($_[1]-$_[0]) if $_[0] && $_[1] && !$verbose; }

sub encrypt_password {
    my ($webpassword) = @_;

    my $salt = rand_bits(128);
    my $b64salt = en_base64($salt);
    #encrypting with cost 5 to make it faster and get rid of plain text webpasswords
    #when subscriber logs in, it will be encrypted with cost 13 to be more secure
    my $cost = 5;

    my $b64hash = en_base64(bcrypt_hash({
        key_nul => 1,
        cost => $cost,
        salt => $salt,
    }, $webpassword));
    return $cost . '$' . $b64salt . '$' . $b64hash;
}

sub update_subscribers {
    my ($dbhost, $dbport, $started, $sub_cnt) = @_;

    return if $sub_id < 0;

    my $dsn = "DBI:mysql:database=mysql;host=${dbhost};port=${dbport}";
    my $dbuser = '';
    my $dbpass = '';

    print "-> connecting to database on host=${dbhost}, port=${dbport}\n";
    my $dbh = DBI->connect($dsn, $dbuser, $dbpass, { PrintError => 1 })
        or die "Can't connect to MySQL database 'mysql': ". $DBI::errstr;

    while ($sub_id >= 0) {

        my $data;
        {
            lock $sub_id;

            $data = $dbh->selectall_arrayref(<<SQL, undef, $sub_id)
SELECT id, webpassword, webusername
FROM provisioning.voip_subscribers
WHERE id > ? and webpassword IS NOT NULL
LIMIT 10000
SQL
            or die "Could not select webpasswords: ".$DBI::errstr;

            unless (@{$data}) {
                $sub_id = -1;
                return;
            }
            $sub_id = $data->[-1][0];
        }

        return unless @{$data};

        print "encrypting passwords for " . ($#$data+1) . " fetched records\n";

        my $vals;

        foreach my $ref (@{$data}) {
            my ($id, $pass, $user) = @{$ref};
            progress(++$done_cnt, $sub_cnt, $started);
            if (length $pass > 40 && split (/\$/, $pass) == 3) {
                debug("--> password is already encrypted.");
                next;
            }
            my $e_pass = encrypt_password($pass);
            $vals and $vals .= ",";
            $vals .= "(".$id.",'".$e_pass."')";
        }

        if ($vals) {
            $dbh->do(<<SQL)
INSERT INTO provisioning.voip_subscribers
(id,webpassword)
VALUES
$vals
ON DUPLICATE KEY UPDATE webpassword = VALUES(webpassword)
SQL
            or die "Could not update webpasswords: ".$DBI::errstr;
        }
   }
}

exit 0;
