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

package Amavis::OS_Fingerprint;
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 Errno qw(EINTR EAGAIN);
use Socket;
use IO::Socket::UNIX;
#use IO::Socket::INET;
use Time::HiRes ();

use Amavis::Conf qw(:platform);
use Amavis::Util qw(ll do_log idn_to_ascii);

sub new {
  my($class, $service_method,$timeout,
     $src_ip,$src_port, $dst_ip,$dst_port, $nonce) = @_;
  local($1,$2,$3); my($service_host, $service_port, $service_path);
  if ($service_method =~
      m{^p0f: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) }six) {
    ($service_host, $service_port) = ($1.$2, $3);
  } elsif ($service_method =~
      m{^p0f: ( / [^ ]+ ) \z}six) {  # looks like a unix socket
    $service_path = $1;
  } else {
    die "Bad p0f method syntax: $service_method";
  }
  $dst_ip = '0.0.0.0'  if !defined $dst_ip;         # our MTA's IP address
  $dst_port = defined $dst_port ? 0+$dst_port : 0;  # our MTA port, usually 25
  $src_port = defined $src_port ? 0+$src_port : 0;  # remote client's port no.
  do_log(4,"Fingerprint query: [%s]:%s %s %s",
           $src_ip, $src_port, $nonce, $service_method);
  my $sock; my $query; my $query_sent = 0;
  # send a UDP query to p0f-analyzer
  $query = '['.$src_ip.']' . ($src_port==0 ? '' : ':'.$src_port);
  if (defined $service_path) {
    $sock = IO::Socket::UNIX->new(Type => SOCK_DGRAM, Peer => $service_path);
    $sock or do_log(0,"Can't connect to a Unix socket %s: %s",
                       $service_path, $!);
  } else {  # assume an INET or INET6 protocol family
    $service_host = idn_to_ascii($service_host);
    $sock = $io_socket_module_name->new(
              Type => SOCK_DGRAM, Proto => 'udp',
              PeerAddr => $service_host, PeerPort => $service_port);
    $sock or do_log(0,"Can't create a socket [%s]:%s: %s",
                       $service_host, $service_port, $!);
  }
  if ($sock) {
    defined $sock->syswrite("$query $nonce")
      or do_log(0, "Fingerprint - error sending a query: %s", $!);
    $query_sent = 1;
  }
  return  if !$query_sent;
  bless { sock => $sock, wait_until => (Time::HiRes::time + $timeout),
          query => $query, nonce => $nonce }, $class;
}

sub collect_response {
  my $self = $_[0];
  my $timeout = $self->{wait_until} - Time::HiRes::time;
  if ($timeout < 0) { $timeout = 0 };
  my $sock = $self->{sock};
  my($resp,$nfound,$inbuf);
  my($rin,$rout); $rin = ''; vec($rin,fileno($sock),1) = 1;
  for (;;) {
    $nfound = select($rout=$rin, undef, undef, $timeout);
    last  if !$nfound || $nfound < 0;
    my $rv = $sock->sysread($inbuf,1024);
    if (!defined $rv) {
      if ($! == EAGAIN || $! == EINTR) {
        Time::HiRes::sleep(0.1);  # slow down, just in case
      } else {
        do_log(2, "Fingerprint - error reading from socket: %s", $!);
      }
    } elsif (!$rv) {  # sysread returns 0 at eof
      last;
    } else {
      local($1,$2,$3);
      if ($inbuf =~ /^([^ ]*) ([^ ]*) (.*)\015\012\z/) {
        my($r_query,$r_nonce,$r_resp) = ($1,$2,$3);
        if ($r_query eq $self->{query} && $r_nonce eq $self->{nonce})
          { $resp = $r_resp };
      }
      do_log(4,"Fingerprint collect: max_wait=%.3f, %.35s... => %s",
               $timeout,$inbuf,$resp);
      $timeout = 0;
    }
  }
  defined $nfound && $nfound >= 0
    or die "Fingerprint - select on socket failed: $!";
  $sock->close  or die "Error closing socket: $!";
  $resp;
}

1;
