# SPDX-License-Identifier: GPL-2.0-or-later

package Amavis::DB::SNMP;
use strict;
use re 'taint';
use warnings;
use warnings FATAL => qw(utf8 void);
no warnings 'uninitialized';
# use warnings 'extra'; no warnings 'experimental::re_strict'; use re 'strict';

BEGIN {
  require Exporter;
  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
  $VERSION = '2.412';
  @ISA = qw(Exporter);
}

use BerkeleyDB;
use MIME::Base64;
use Time::HiRes ();

use Amavis::Conf qw(:platform $myversion $nanny_details_level);
use Amavis::Util qw(ll do_log do_log_safe
                    snmp_initial_oids snmp_counters_get
                    add_entropy fetch_entropy_bytes);

# open existing databases (called by each child process)
#
sub new {
  my($class,$db_env) = @_; $! = 0; my $env = $db_env->get_db_env;
  defined $env or die "BDB get_db_env (dbS/dbN): $BerkeleyDB::Error, $!.";
  $! = 0; my $dbs = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
  defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
  $! = 0; my $dbn = BerkeleyDB::Hash->new(-Filename=>'nanny.db',-Env=>$env);
  defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
  bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
}

sub DESTROY {
  my $self = $_[0];
  local($@,$!,$_); my $myactualpid = $$;
  if (defined($my_pid) && $myactualpid != $my_pid) {
    do_log_safe(5,"Amavis::DB::SNMP DESTROY skip, clone [%s] (born as [%s])",
                  $myactualpid, $my_pid);
  } else {
    do_log_safe(5,"Amavis::DB::SNMP DESTROY called");
    for my $db_name ('db_snmp', 'db_nanny') {
      my $db = $self->{$db_name};
      if (defined $db) {
        eval {
          $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!.";  1;
        } or do { $@ = "errno=$!"  if $@ eq '' };
        if ($@ ne '' && $@ !~ /\bDatabase is already closed\b/)
          { warn "[$myactualpid] BDB S+N DESTROY INFO ($db_name): $@" }
        undef $db;
      }
    }
  }
}

#sub lock_stat($) {
# my $label = $_[0];
# my $s = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\'';
# do_log(0, "lock_stat %s: %s", $label,$s);
#}

# insert startup time SNMP entry, called from the master process at startup
# (a classical subroutine, not a method)
#
sub put_initial_snmp_data($) {
  my $db = $_[0];
  my($eval_stat,$interrupt); $interrupt = '';
  { my $cursor;
    my $h1 = sub { $interrupt = $_[0] };
    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
    eval {  # ensure cursor will be unlocked even in case of errors or signals
      $cursor = $db->db_cursor(DB_WRITECURSOR);  # obtain write lock
      defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
      my $list_ref = snmp_initial_oids();
      for my $obj (@$list_ref) {
        my($key,$type,$val) = @$obj;
        $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
          or die "BDB S c_put: $BerkeleyDB::Error, $!.";
      };
      $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
      undef $cursor;  1;
    } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
    $cursor->c_close  if defined $cursor;  # unlock, ignoring status
    undef $cursor;
  };  # restore signal handlers
  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal, ignoring status
  elsif (defined $eval_stat) {
    chomp $eval_stat;
    die "put_initial_snmp_data: BDB S $eval_stat\n";
  }
}

sub update_snmp_variables {
  my $self = $_[0];
  do_log(5,"updating snmp variables in BDB");
  my $snmp_var_names_ref = snmp_counters_get();
  my($eval_stat,$interrupt); $interrupt = '';
  if (defined $snmp_var_names_ref && @$snmp_var_names_ref) {
    my $db = $self->{'db_snmp'}; my $cursor;
    my $h1 = sub { $interrupt = $_[0] };
    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
    eval {  # ensure cursor will be unlocked even in case of errors or signals
      $cursor = $db->db_cursor(DB_WRITECURSOR);  # obtain write lock
      defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
      for my $key (@$snmp_var_names_ref) {
        my($snmp_var_name,$arg,$type) = ref $key ? @$key : ($key);
        $type = 'C32'  if !defined($type) || $type eq '';
        if ($type eq 'C32' || $type eq 'C64') {  # a counter
          if (!defined($arg)) { $arg = 1 } # by default counter increments by 1
          elsif ($arg < 0)    { $arg = 0 } # counter is supposed to be unsigned
        } elsif ($type eq 'TIM') {  # TimeTicks
          if    ($arg < 0)    { $arg = 0 } # non-decrementing
        }
        my($val,$flags); local($1);
        my $stat = $cursor->c_get($snmp_var_name,$val,DB_SET);
        if ($stat==0) {  # exists, update it (or replace it)
          if    ($type eq 'C32' && $val=~/^C32 (\d+)\z/) { $val = $1+$arg }
          elsif ($type eq 'C64' && $val=~/^C64 (\d+)\z/) { $val = $1+$arg }
          elsif ($type eq 'TIM' && $val=~/^TIM (\d+)\z/) { $val = $1+$arg }
          elsif ($type eq 'INT' && $val=~/^INT ([+-]?\d+)\z/) { $val = $arg }
          elsif ($type=~/^(STR|OID)\z/ && $val=~/^\Q$type\E (.*)\z/) {
            if ($snmp_var_name ne 'entropy') { $val = $arg }
            else {  # blend-in entropy
              $val = $1; add_entropy($val, Time::HiRes::gettimeofday);
              $val = fetch_entropy_bytes(18);  # 18 bytes
              $val = encode_base64($val,'');   # 18*8/6 = 24 chars
              $val =~ tr{+/}{-_};  # base64 -> RFC 4648 base64url [A-Za-z0-9-_]
            }
          }
          else {
            do_log(-2,"WARN: variable syntax? %s: %s, clearing",
                      $snmp_var_name,$val);
            $val = 0;
          }
          $flags = DB_CURRENT;
        } else {  # create new entry
          $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error, $!.";
          $flags = DB_KEYLAST; $val = $arg;
        }
        my $fmt = $type eq 'C32' ? "%010d" : $type eq 'C64' ? "%020.0f"
                : $type eq 'INT' ? "%010d" : undef;
        # format for INT should really be %011d, but keep compatibility for now
        my $str = defined($fmt) ? sprintf($fmt,$val) : $val;
        $cursor->c_put($snmp_var_name, $type.' '.$str, $flags) == 0
          or die "c_put: $BerkeleyDB::Error, $!.";
      }
      $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
      undef $cursor;  1;
    } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
    if (defined $db) {
      $cursor->c_close  if defined $cursor;  # unlock, ignoring status
      undef $cursor;
#     if (!defined($eval_stat)) {
#       my $stat; $db->db_sync();  # not really needed
#       $stat==0 or warn "BDB S db_sync,status $stat: $BerkeleyDB::Error, $!.";
#     }
    }
  };  # restore signal handlers
  delete $self->{'cnt'};
  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal, ignoring status
  elsif (defined $eval_stat) {
    chomp $eval_stat;
    die $eval_stat  if $eval_stat =~ /^timed out\b/;  # resignal timeout
    die "update_snmp_variables: BDB S $eval_stat\n";
  }
}

sub read_snmp_variables {
  my($self,@snmp_var_names) = @_;
  my($eval_stat,$interrupt); $interrupt = '';
  my $db = $self->{'db_snmp'}; my $cursor; my(@values);
  { my $h1 = sub { $interrupt = $_[0] };
    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
    eval {  # ensure cursor will be unlocked even in case of errors or signals
      $cursor = $db->db_cursor;  # obtain read lock
      defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
      for my $cname (@snmp_var_names) {
        my $val; my $stat = $cursor->c_get($cname,$val,DB_SET);
        push(@values, $stat==0 ? $val : undef);
        $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
      }
      $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
      undef $cursor;  1;
    } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
    if (defined $db) {
      $cursor->c_close  if defined $cursor;  # unlock, ignoring status
      undef $cursor;
    }
  };  # restore signal handlers
  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal, ignoring status
  elsif (defined $eval_stat) {
    chomp $eval_stat;
    die $eval_stat  if $eval_stat =~ /^timed out\b/;  # resignal timeout
    die "read_snmp_variables: BDB S $eval_stat\n";
  }
  for my $val (@values) {
    if (!defined($val)) {}  # keep undefined
    elsif ($val =~ /^(?:C32|C64) (\d+)\z/)  { $val = 0+$1 }
    elsif ($val =~ /^(?:INT) ([+-]?\d+)\z/) { $val = 0+$1 }
    elsif ($val =~ /^(?:STR|OID) (.*)\z/)   { $val = $1 }
    else { do_log(-2,"WARN: counter syntax? %s", $val); undef $val }
  }
  \@values;
}

sub register_proc {
  my($self, $details_level, $reset_timestamp, $state, $task_id) = @_;
  my $eval_stat; my $interrupt = '';
  if (!defined($state) || $details_level <= $nanny_details_level) {
    $task_id = ''  if !defined $task_id;
    my $db = $self->{'db_nanny'}; my $key = sprintf("%05d",$$);
    my $cursor; my $val;
    my $h1 = sub { $interrupt = $_[0] };
    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
    eval {  # ensure cursor will be unlocked even in case of errors or signals
      $cursor = $db->db_cursor(DB_WRITECURSOR);  # obtain write lock
      defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
      my $stat = $cursor->c_get($key,$val,DB_SET);
      $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
      if ($stat==0 && !defined $state) {  # remove existing entry
        $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
      } elsif (defined $state) {  # add new, or update existing entry
        my $timestamp; local($1);
        # keep its timestamp when updating existing record
        $timestamp = $1  if $stat==0 && $val=~/^(\d+(?:\.\d*)?) /s;
        $timestamp = sprintf("%014.3f", Time::HiRes::time)
                       if !defined($timestamp) || $reset_timestamp;
        my $new_val = sprintf("%s %-14s", $timestamp, $state.$task_id);
        $cursor->c_put($key, $new_val,
                       $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
          or die "c_put: $BerkeleyDB::Error, $!.";
      }
      $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
      undef $cursor;  1;
    } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" };
    if (defined $db) {
      $cursor->c_close  if defined $cursor;  # unlock, ignoring status
      undef $cursor;
#     if (!defined($eval_stat)) {
#       my $stat = $db->db_sync();  # not really needed
#       $stat==0 or warn "BDB N db_sync,status $stat: $BerkeleyDB::Error, $!.";
#     }
    }
  };  # restore signal handlers
  if ($interrupt ne '') {
    kill($interrupt,$$);  # resignal, ignoring status
  } elsif (defined $eval_stat) {
    chomp $eval_stat;
    do_log_safe(5, "register_proc: BDB N %s", $eval_stat);
    die $eval_stat  if $eval_stat =~ /^timed out\b/;  # resignal timeout
    die "register_proc: BDB N $eval_stat\n";
  }
}

1;
