#!/usr/bin/perl
# http://userside.ua/wiki/index.php/usm_ping - Module Information
$moduleRevision = "71";
$moduleVersion  = "usm_ping v.3.19." . $moduleRevision; #Version

#========================================================================
# Custom configuration
#========================================================================

$isDebugMode    = 0; #Debug Mode [0/1]
$isSaveAllLogs  = 0; #Flag - save all logs from module to timestamp-name files

#========================================================================

use Time::Local;
use threads;
use Thread::Queue;
use Net::SSL;
use Net::Ping;
use Socket; 
use FindBin;
use URI;
use URI::Escape;
use LWP::UserAgent;
use JSON;
use Data::Dumper;

my $master = $$;
my $verbose = $ARGV[0] eq q(-v);
my $RequestQ = Thread::Queue->new;
my $ResultQ = Thread::Queue->new;
my @kids;

$modulePath = $FindBin::RealBin . "/";

#Загрузка параметров из файла конфигурации
open ($file, $modulePath . "usm_ping.conf") || die "ERROR: Open Config File " . $modulePath . "usm_ping.conf\n";
close $file;
require($modulePath . "usm_ping.conf");

#Присвоение пустым параметрам значений по-умолчанию
$logsFullPath = $logsPath . '/usm_ping_';
if ($arpPath eq '') { 
    $arpPath = '/bin/';
}
if ($pingType eq '') {
    $pingType = 'icmp';
}
if ($isOnlyDevice ne 1) {
    $isOnlyDevice = 0;
}
if ($isChangeMac ne 1) {
    $isChangeMac = 0
};
if ($threadCount < 1) {
    $threadCount = 75
};
if ($isSaveAllLogs eq 1) {
    $logAdditionalDate = getFormatDateTime();
} else {
    $logAdditionalDate = '';
}
if ($allowNonRef ne 1) {
    $allowNonRef = 0;
}

#Стартовые действия
initializeModule();

#Загружаем параметры из системы
my $data = {
  'key'             => $usApiKey,
  'cat'             => 'usm_ping',
  'action'          => 'module_start',
  'module_version'  => $moduleVersion,
  'module_path'     => uri_escape($modulePath),
  'logs_path'       => uri_escape($logsPath)
};

$response = apiRequest('post', $data, 'module_start');

$isModuleEnable = $response->{'data'}->{'is_module_enable'};
$moduleMinAllowVersion = $response->{'data'}->{'module_min_allow_version'};

#Проверяем версию и включен ли модуль
verifyModulePermit();

#Фиксируем в логе параметры этого модуля
writeToLogModule("threadCount           = " . $threadCount);
writeToLogModule("isOnlyDevice          = " . $isOnlyDevice);
writeToLogModule("arpPath               = " . $arpPath);
writeToLogModule("isChangeMac           = " . $isChangeMac);
writeToLogModule("isSaveAllLogs         = " . $isSaveAllLogs);
writeToLogModule("ipRangeStart          = " . $ipRangeStart);
writeToLogModule("ipRangeFinish         = " . $ipRangeFinish);
writeToLogModule("pingType              = " . $pingType);
writeToLogModule("isAlternativePing     = " . $isAlternativePing);
writeToLogModule("alternativePingPath   = " . $alternativePingPath);
writeToLogModule("citiesList            = " . $citiesList);
writeToLogModule("allowNonRef           = " . $allowNonRef);

if ($ipRangeStart ne '') {
    $ipRangeStart = ip2long($ipRangeStart);
}
if ($ipRangeFinish ne '') {
    $ipRangeFinish = ip2long($ipRangeFinish);
}

#Начинаем работу
$ipCount = 0;
%hashIpAddr = ();
@arrayIpPolling = ();

if ($isOnlyDevice ne 1) {
    #Работаем с ARP-таблицей (сначала получаем информацию по адресам, что уже есть)
    writeToLogModule("ARP1. Start");
    $logArpPath = $logAdditionalDate . "arp_first.log";
    open ($file, ">" . $logsFullPath . $logArpPath) || die "ERROR: Open file " . $logArpPath . ". Verify path and rights\n";
        print $file `/$arpPath/arp -n -a` . "\n";
    close $file;
    
    #Считываем маки и ставим активность
    @arrayArpIp = ();
    @arrayArpMac = ();
    open ($file, $logsFullPath . $logArpPath) || die "ERROR: Open file " . $logArpPath . ". Verify path and rights\n";
        while ($string = <$file>) {
            $i = index($string, ":");
            if ($i > 0 && index($string, "permanent") < 2 && index($string, "incomplete") < 2) {
                $ip = trim(substr($string, 3, 20));
                $ip = trim(substr($ip, -length($ip), index($ip,")")));
                $mac = trim(substr($string, -length($string) + $i - 2, 17));
                
                if ($isDebugMode eq 1) {
                    writeToLogModule("ARP: " . $ip . " " . $mac);
                }
                
                $ipCount++;
                $ipLong = ip2long($ip);
                %hashIpAddr = (%hashIpAddr, $ipLong, $ipCount);
                
                push (@arrayArpIp, $ip);
                push (@arrayArpMac, $mac);
            }
        }
    close $file;
    writeToLogModule("Received rows: " . $#arrayArpIp);
    
    if ($#arrayArpIp > 0) {
        #Передаём список на API
        $list = '';
        for ($i = 0; $i<$#arrayArpIp; $i++) {
            if ($list ne '') {
                $list .= ',';
            }
            $list .= @arrayArpIp[$i] . "|" . @arrayArpMac[$i];
        }
        my $data = {
          'key'             => $usApiKey,
          'cat'             => 'usm_ping',
          'action'          => 'arp_processing',
          'is_change_mac'   => $isChangeMac,
          'data'            => $list
        };
        $response = apiRequest('post', $data, 'arp_processing');
        writeToLogModule("Report - ObjectUpdateActivity: " . $response->{'report'}->{'object_update_activity'});
        writeToLogModule("Report - UnknownMac: " . $response->{'report'}->{'unknown_mac'});
        writeToLogModule("Report - UpdateMac: " . $response->{'report'}->{'update_mac'});
    }
    
    writeToLogModule("ARP1. Finish");
}

#Пингуем только адреса, что не найдены в АРП
writeToLogModule("Ping IP. Start");

#Получаем список IP-адресов для пинга
my $data = {
  'key'             => $usApiKey,
  'cat'             => 'usm_ping',
  'action'          => 'get_ip_to_ping',
  'is_only_device'  => $isOnlyDevice,
  'cities_list'     => $citiesList,
  'ip_range_start'  => $ipRangeStart,
  'ip_range_finish' => $ipRangeFinish
};

$response = apiRequest('get', $data, 'get_ip_to_ping');

#print "get_ip_to_ping\n";
#print  Dumper($response);
#print  Dumper($ipList);
#exit;

$ipList = $response->{'data'};
@arrayIp = split(/,/, $ipList);

#Пингуем только адреса, что не найдены в АРП
$ipToPing = 0;
open ($file, ">" . $logsFullPath . $logAdditionalDate . "ip_list.log") || die "ERROR: Open file " .  $logAdditionalDate . "ip_list.log. Verify path and rights\n";
foreach my $ip (@arrayIp) {
    #Проверяем - есть ли такая запись уже
    if ($hashIpAddr{$ip} < 1) {
        $ipToPing++;
        $ipString = long2ip($ip);
        print $file $ipString . "\n";        
        $RequestQ->enqueue($ipString);
    }
}
close $file;

writeToLogModule("IpToPing: " . $ipToPing);

my @targets;

for (0..($threadCount - 1)) {
    $RequestQ->enqueue(undef);
    if ($isAlternativePing ne 1) {
        push @kids, threads->new(\&pingIt, $_);
    } else {
        push @kids, threads->new(\&pingItAlternative, $_);
    }
}

foreach (threads->list){
   $_->join;
}
$ResultQ->enqueue(undef);

@arrayPingSuccessIp = ();
open ($file, ">" . $logsFullPath . $logAdditionalDate . "success.log") || die "ERROR: Open file " .  $logAdditionalDate . "success.log. Verify path and rights\n";
open ($file2, ">" . $logsFullPath . $logAdditionalDate . "result.log") || die "ERROR: Open file " .  $logAdditionalDate . "result.log. Verify path and rights\n";
while ($target = $ResultQ->dequeue) {
    print $file2 $target . "\n";
    my ($dateTime, $ip, $result) = split(/\|/, $target);
    if ($result eq 1) {
        $ipLong = ip2long($ip);
        if ($hashIpAddr{$ipLong} < 1) {
            $ipCount++;
            %hashIpAddr = (%hashIpAddr, $ipLong, $ipCount);
        }
        push (@arrayPingSuccessIp, $ip);
        print $file $dateTime . "|" . $ip . "\n";
    }
}
close $file;
close $file2;

writeToLogModule("Ping success: " . ($#arrayPingSuccessIp + 1));

if ($#arrayPingSuccessIp > -1) {
    #Передаём список на API
    $list = '';
    $c = $#arrayPingSuccessIp + 1;
    for ($i = 0; $i<$c; $i++) {
        if ($list ne '') {
            $list .= ',';
        }
        $list .= @arrayPingSuccessIp[$i];
    }
    my $data = {
      'key'             => $usApiKey,
      'cat'             => 'usm_ping',
      'action'          => 'ping_processing',
      'data'            => $list
    };
    $response = apiRequest('post', $data, 'ping_processing');
    writeToLogModule("Report - ObjectUpdateActivity: " . $response->{'report'}->{'object_update_activity'});
}
writeToLogModule("Ping IP. Finish");


if ($isOnlyDevice ne 1) {
    #Работаем с ARP-таблицей (второй проход - ловим тех, кто не ответил на Ping)
    writeToLogModule("ARP2. Start");
    $logArpPath = $logAdditionalDate . "arp_second.log";
    open ($file, ">" . $logsFullPath . $logArpPath) || die "ERROR: Open file " . $logArpPath . ". Verify path and rights\n";
        print $file `/$arpPath/arp -n -a` . "\n";
    close $file;
    
    #Считываем маки и ставим активность
    @arrayArpIp = ();
    @arrayArpMac = ();
    open ($file, $logsFullPath . $logArpPath) || die "ERROR: Open file " . $logArpPath . ". Verify path and rights\n";
        while ($string = <$file>) {
            $i = index($string, ":");
            if ($i > 0 && index($string, "permanent") < 2 && index($string, "incomplete") < 2) {
                $ip = trim(substr($string, 3, 20));
                $ip = trim(substr($ip, -length($ip), index($ip,")")));
                $mac = trim(substr($string, -length($string) + $i - 2, 17));

                #Проверяем - есть ли такая запись уже
                $ipLong = ip2long($ip);
                if ($hashIpAddr{$ipLong} < 1) {
                    $ipCount++;
                    %hashIpAddr = (%hashIpAddr, $ipLong, $ipCount);
                    push (@arrayArpIp, $ip);
                    push (@arrayArpMac, $mac);
                    if ($isDebugMode eq 1) {
                        writeToLogModule("ARP2: " . $ip . " " . $mac);
                    }
                }
            }
        }
    close $file;
    writeToLogModule("Received rows: " . $#arrayArpIp);
    
    if ($#arrayArpIp > 0) {
        #Передаём список на API
        $list = '';
        for ($i = 0; $i<$#arrayArpIp; $i++) {
            if ($list ne '') {
                $list .= ',';
            }
            $list .= @arrayArpIp[$i] . "|" . @arrayArpMac[$i];
        }
        my $data = {
          'key'             => $usApiKey,
          'cat'             => 'usm_ping',
          'action'          => 'arp_processing',
          'is_change_mac'   => $isChangeMac,
          'data'            => $list
        };
        $response = apiRequest('post', $data, 'arp_processing');
        writeToLogModule("Report - ObjectUpdateActivity: " . $response->{'report'}->{'object_update_activity'});
        writeToLogModule("Report - UnknownMac: " . $response->{'report'}->{'unknown_mac'});
        writeToLogModule("Report - UpdateMac: " . $response->{'report'}->{'update_mac'});
    }
    
    writeToLogModule("ARP2. Finish");
}

#Отмечаем, что мы закончили работу
my $data = {
  'key'             => $usApiKey,
  'cat'             => 'usm_ping',
  'action'          => 'module_finish',
  'module_version'  => $moduleVersion,
  'module_path'     => uri_escape($modulePath)
};
$response = apiRequest('post', $data, 'module_finish');

moduleFinish();

unlink $pidFile;
exit(0);

#================================================================================================================
#Custom Module Function
#================================================================================================================

sub pingIt
{
    my ($work_count, $miss_count);
    my $t=shift;
    my $pinger=Net::Ping->new($pingType);
    while (my $target=$RequestQ->dequeue){
        $work_count++;
        my $retval=0;
        $verbose and print "Child $target started\n";
        $pr1 = $pinger->ping($target, 3);#timeout 3 sec
        if ($pr1) {
            $verbose and print "Found $target!\n";
            $retval = 1
        } else {
            $verbose and print "No response from $target\n";
            $retval = 0;
            $miss_count++;
        }
        $ResultQ->enqueue(getFormatDateTime() . '|' . $target . "|" . $retval);
        threads->yield();
        $verbose and  print "Child $target exiting\n";
    }
    $pinger->close();
    $ResultQ->enqueue("Thread $t processed $work_count, missed $miss_count");
}

sub pingItAlternative 
{
    my ($work_count, $miss_count);
    my $t = shift;
    while (my $target = $RequestQ->dequeue) {
        $work_count ++;
        my $retval = 0;
        $verbose and print "Child $target started\n";
        my $ping = `ping $alternativePingPath -c4 $target`;
        my $substring = "64 bytes from $target";
        if($ping =~ /$substring/) {
            $verbose and print "ALT. Found $target!\n";
            $retval = 1;
        } else {
            $verbose and print "ALT. No response from $target\n";
            $retval = 0;
            $miss_count++;
        }
        $ResultQ->enqueue(getFormatDateTime() . '|' . $target . "|" . $retval);
        threads->yield();
        $verbose and  print "Child $target exiting\n";
    }
    $ResultQ->enqueue("ALT. Thread $t processed $work_count, missed $miss_count");
}


### ================================================================================================================
#Standart Module Light Function v.2.8 - 29.01.2016
#================================================================================================================

sub moduleFinish
{
    my($additionalText) = @_;    
    open ($file, ">>" . $logLaunches) || die "ERROR: Open file " . $logLaunches . ". Verify path and rights\n";
        if ($additionalText ne '') {
            print $file $moduleVersion . " " . $additionalText . "\n";
            if ($isSilence ne 1) {
                print $additionalText . "\n";
            }
            #writeToLogModule($additionalText);
        }
        if ($additionalText eq '') {
            writeToLogModule($moduleVersion . " - finish module");
            print $fileLogModule "===============================================================================\n";
            close $fileLogModule;
            print $file $moduleVersion . " Module FINISH at " . getFormatDateTime() . "\n";
            print $file "==========================================================\n";
        }
    close $file;
    if ($isSilence ne 1 && $additionalText eq '') {
        print 'Finish module at ' . getFormatDateTime() . "\n";
        print "====================================\n";
    }
}

sub verifyModulePermit 
{
    if ($isModuleEnable ne 1) {
        moduleFinish("Module disabled in system - exit");
        exit(1);
    }
    if ($moduleRevision < $moduleMinAllowVersion || $moduleMinAllowVersion < 1) {
        moduleFinish('Version failure. Need version: ' . $moduleMinAllowVersion . " - exit");
        exit(1);
    }
}

sub initializeModule {
    my $isAllowPid     = $_[0];
    $logLaunches = $logsFullPath . 'launches.log';
    $logModule = $logsFullPath . 'module.log';

    if ($ARGV[0] eq 'debug' || $ARGV[1] eq 'debug' || $ARGV[2] eq 'debug') {
        $isDebugMode = 1;
    }

    #Удаляем громоздкий файл
    $size  = -s $logLaunches;
    if ($size > 100000) {
        unlink($logLaunches);
    }
    
    open ($file, ">>" . $logLaunches) || die "ERROR: Open file " . $logLaunches . ". Verify path and rights\n";
        print $file $moduleVersion . ' Module START  at ' . getFormatDateTime() . "\n";
    close $file;

    if ($isAllowPid ne 1) {
        $pidFile = $logsFullPath . 'pid.pid';
        if (-e $pidFile) {
            unless (open(PIDFILE, $pidFile)) {
                moduleFinish("Find previous PID - exit");
                close PIDFILE;
                exit(1);
            }
            $oldpid = <PIDFILE>;
            close PIDFILE;
            if ($oldpid > 1 && kill(0, $oldpid)) {
                moduleFinish("Find previous PID " . $oldpid . " - exit");
                exit(1);
            }
        }
        open(PID, ">" . $pidFile) || die "ERROR: Open file pid.pid. Verify path and rights\n";
            print PID $$;
        close(PID);
    }

    #Выведем заголовок модуля
    if ($isSilence ne 1) {
        print $moduleVersion . "\n";
        print "====================================\n";
        print ' Start module at ' . getFormatDateTime() . "\n";
    }
    
    open ($fileLogModule, ">" . $logModule) || die "ERROR: Open file " . $logModule . ". Verify path and rights\n";
    print $fileLogModule "===============================================================================\n";
    
    #Фиксируем версию модуля и путь к модулю
    writeToLogModule($moduleVersion . " - start module");
    writeToLogModule($modulePath);
    
    #Фиксируем основные параметры
    writeToLogModule("isSilence = " . $isSilence);
}

sub writeToLogModule
{
    my($string) = @_;
    print $fileLogModule getFormatDateTime() . " - " . $string . "\n";
}

sub trim 
{
    my($string) = @_;
    for ($string) {
        s/^\s+//;
        s/\s+$//;
    }
    return $string;
}

sub ip2long
{
    return unpack("N", inet_aton(shift));
}

sub long2ip 
{
    return inet_ntoa(pack("N",shift));
}

sub apiRequest
{
    my $requestType     = $_[0];
    my $data     = $_[1];
    my $comment     = $_[2];
    writeToLogModule("API Request: " . $comment);
    if ($requestType eq 'get') {
        $response = curlGet($data);
    }
    if ($requestType eq 'post') {
        $response = curlPost($data);
    }
    $result = $response->{'result'};
    if ($result eq '') {
        $result = $response->{'Result'};
    }
    writeToLogModule("Result: " . $result);
    if ($result eq 'ERROR') {
        $errorText = $response->{'error_text'};
        if ($errorText eq '') {
            $errorText = $response->{'ErrorText'};
        }
        writeToLogModule("Error: " . $errorText);
        moduleFinish("API Error - " . $errorText . " - exit");
        exit(1);
    }
    return $response;
}

sub curlPost
{
    my $data     = $_[0];
    my $ua       = LWP::UserAgent->new(ssl_opts=>{verify_hostname => 0});
    my $response = $ua->post($usUrl . "/api.php", $data);
    if ($isDebugMode eq 1) {
        print  Dumper($response);
    }
    $content = trim($response->decoded_content);
    if ($isDebugMode eq 1) {
        writeToLogModule("Content: " . $content);
    }
    if (substr(trim($content), 0, 1) ne '{') {
        writeToLogModule("Content: " . $content);
        $content = '{}';
    }
    if ($allowNonRef eq 1) {
    	my $json = JSON->new->allow_nonref;
    	return $json->decode($content);
    } else {
    	return decode_json($response->decoded_content);
    }
}

sub curlGet
{
    my $data     = $_[0];
    my $ua       = LWP::UserAgent->new(ssl_opts=>{verify_hostname => 0});
    my $url = URI->new($usUrl . "/api.php");
    $url->query_form($data);
    my $req = HTTP::Request->new(GET => $url);
    $req->header('content-type' => 'application/json', 'accept' => 'application/json');
    my $response = $ua->request($req);
    if ($isDebugMode eq 1) {
        print  Dumper($response);
    }
    if ($allowNonRef eq 1) {
    	my $json = JSON->new->allow_nonref;
    	return $json->decode($response->decoded_content);
    } else {
    	return decode_json($response->decoded_content);
    }
}

sub getFormatDateTime 
{
    my $format = shift @_ || "YYYY-MM-DD hh:mm:ss";
    my $datetime = shift @_ || time;
    my %strings = (
        '500'=>"AM", '501'=>"PM",
        '400'=>"January", '401'=>"February", '402'=>"March", '403'=>"April", '404'=>"May", '405'=>"June", '406'=>"July", '407'=>"August", '408'=>"September", '409'=>"October", '410'=>"November", '411'=>"December",
        '300'=>"Jan", '301'=>"Feb", '302'=>"Mar", '303'=>"Apr", '304'=>"May", '305'=>"Jun", '306'=>"Jul", '307'=>"Aug", '308'=>"Sep", '309'=>"Oct", '310'=>"Nov", '311'=>"Dec",
        '200'=>"Sunday", '201'=>"Monday", '202'=>"Tuesday", '203'=>"Wednesday", '204'=>"Thursday", '205'=>"Friday", '206'=>"Saturday",
        '100'=>"Sun", '101'=>"Mon", '102'=>"Tue", '103'=>"Wed", '104'=>"Thu", '105'=>"Fri", '106'=>"Sat"
    );
    ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($datetime);
    $year += 1900;
    if ($format=~m/AM/) {
        my $ampmhour=($hour % 12) || 12;
        $format=~s/hh/sprintf("%02u",$ampmhour)/eg;
        $format=~s/h/$ampmhour/eg;
        $format=~s/AM/($hour>11?"^501":"^500")/eg;
    } else {
        $format=~s/hh/sprintf("%02u",$hour)/eg;
        $format=~s/h/$hour/eg;
    };
    $format=~s/mm/sprintf("%02u",$min)/eg;
    $format=~s/m/$min/eg;
    $format=~s/ss/sprintf("%02u",$sec)/eg;
    $format=~s/s/$sec/eg;

    $format=~s/YYYY/$year/eg;
    $format=~s/YY/sprintf("%02u",$year % 100)/eg;
    $format=~s/MMMM/sprintf("^4%02u",$mon)/eg;
    $format=~s/MMM/sprintf("^3%02u",$mon)/eg;
    $format=~s/MM/sprintf("%02u",$mon+1)/eg;
    $format=~s/M/($mon+1)/eg;
    $format=~s/DDDD/sprintf("^2%02u",$wday)/eg;
    $format=~s/DDD/sprintf("^1%02u",$wday)/eg;
    $format=~s/DD/sprintf("%02u",$mday)/eg;
    $format=~s/D/$mday/eg;
    $format=~s/\^(\d\d\d)/$strings{$1}/g;
    return $format;
}
