#!/bin/bash
#########################################################################################
#
# Chknodes
# Fast parallel ping and http response checker
#
# © Copyright 2012 Arto Jääskeläinen <temp001(at)pp.inet.fi>
#   All rights reserved.
# The program is distributed under the terms of the GNU General Public License
#
#    Chknodes is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Chknodes is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#     <http://www.gnu.org/licenses/>.
#
#
# v2.0  2012-11-28 Any length netmask, get http response 
# v2.01 2012-11-29 wlan included
# v2.1  2014-01-01 support for multiple networks
# v2.11 2014-01-15 vpn "tun-" and virtual "vir-" detection
# v2.2  2017-04-26 systemd network device names included
#
#########################################################################################

function Dt { date "+%F %T %z"; echo "  "; }

logging()
{
sw=$1
if [ $(echo ${sw,,}) = "on" ]; then
pipe_to_log=" | sed 's/\x1b.\{2,5\}[mK]//g' | tee -a $logs_dir/$log_file"
else
if [ $(echo ${sw,,}) = "off" ]; then
pipe_to_log=""
fi
fi	
}
function Bk { echo -en "\e[1;30m"; }
function R { echo -en "\e[1;31m"; }
function G { echo -en "\e[1;32m"; }
function Y { echo -en "\e[1;33m"; } 
function B { echo -en "\e[1;34m"; }
function P { echo -en "\e[1;35m"; }
function C { echo -en "\e[1;36m"; }
function W { echo -en "\e[1;37m"; }
function Gy { echo -en "\e[0;37m"; }
function H { echo -en "\e[1m"; }
function D { echo -en "\e(B\e[m"; }

get_version_list()
{
version_list=""
[[ -f "$@" ]] || return 100	
version_list=($(cat "$1" | tr '\t' ' '| grep -E -o '^# +[Vv][0-9]{1,2}.[0-9]{1,2}' | sed 's/# *[Vv]//g'))
[[ -z "$version_list" ]] || return 101
}

compare()
{
local longer
local a=$1
local b=$2
if [ -z $a ] || [ -z $b ]; then return 1; fi
int_a=$(echo ${a%%.*})
fract_a=$(echo ${a##*.})
int_b=$(echo ${b%%.*})
fract_b=$(echo ${b##*.})
if [ $int_a -gt $int_b ]; then return 101; fi
if [ $int_a -lt $int_b ]; then return 102; fi
longer=${#fract_a}
if [ ${#fract_b} -gt ${#fract_a} ]; then longer=${#fract_b}; fi
while [ ${#fract_a} -lt $longer ]; do fract_a=$fract_a"0"; done
while [ ${#fract_b} -lt $longer ]; do fract_b=$fract_b"0"; done
if [ $fract_a -gt $fract_b ]; then return 101; fi
if [ $fract_a -lt $fract_b ]; then return 102; fi
return 100	
}

get_biggest()
{
local i
list=( "$@" )
i_biggest=""
n_list=${#list[@]}
if [ -z $list ] || [ $n_list -lt 2 ]; then biggest=""; return 1; fi
biggest=${list[0]}
i_biggest=0
for (( i=1; i<$n_list; i++ )); do
	compare $biggest ${list[i]}
	result="$?"
	case $result in
	102)	biggest=${list[i]}; i_biggest=$i ;;
	100)	i_biggest="" ;;		
	esac
done
}

get_smallest()
{
local i
list=( "$@" )
i_smallest=""
n_list=${#list[@]}
if [ -z $list ] || [ $n_list -lt 2 ]; then smallest=""; return 1; fi
smallest=${list[0]}
i_smallest=0
for (( i=1; i<$n_list; i++ )) do
compare $smallest ${list[i]}
result="$?"
case $result in
101)	smallest=${list[i]}; i_smallest=$i ;;
100)	i_smallest="" ;;		
esac
done
}

check_unique()
{
table=("$@")
n_cells=${#table[@]}
for ((i=0; i<$n_cells; i++ ))
do
	for ((j=$[i+1]; j<$n_cells; j++ ))
	do
	compare "${table[i]}" "${table[j]}"
	if [ $? = 100 ]; then 
	return 1 
	fi
	done
done
}

show_version()
{
pathfile="$1"	
file=$(echo ${pathfile##*/})	
if [ ! -f "$pathfile" ]; then 
	return 1
fi
get_version_list "$pathfile"
	if [ -z $version_list ]; then
	eval echo -e '$indent$(Dt)$2"$blank"Version $(R)not found$(D) at $run_pathfile.' $pipe_to_log
	return 1
	fi 
if [ ${#version_list[@]} -gt 1 ]; then
get_biggest ${version_list[@]}
	if [ -z $i_biggest ]; then
	eval echo -e '$indent$(Dt)$2$(R)Multiple latest version entries found.' $pipe_to_log; D
	fi
else
	biggest=${version_list[0]}
fi
eval echo -e '$indent$(Dt)$2$blank"v"$biggest' $pipe_to_log
}

check_version()
{
local pathfile="$@"	
get_version_list "$pathfile"
if [ ${#version_list[@]} -gt 1 ]; then
	check_unique "${version_list[@]}"
	if [ $? != 0 ]; then
		eval echo -e '"$indent$(Dt)"Warning: $(R)Duplicate$(D) version list entry found at $pathfile' $pipe_to_log
	fi
	get_biggest ${version_list[@]}
else
	biggest=${version_list[0]}
fi
version=$biggest
}

install_it()
{
echo "Installing $(B) $display_name $(D)"
sudo cp $0 $run_pathfile
if [ -f "$run_pathfile" ]; then
eval echo -e '$indent$(Dt)$"Installation to "$run_pathfile succesful.' $pipe_to_log
else
eval echo -e '$indent$(Dt)"Installation to "$run_pathfile $(R)FAILED$(D)' $pipe_to_log
fi
}

ask_anything()
{
local question="$1"
local default_answer="$2"
response="" 
while [[ x$response != x"y" && x$response != x"n" ]]; do 
echo -n "$question"
read -e -p " " -i "$default_answer" response 
done
}

install_or_not()
{
if [ ! -f "$run_pathfile" ]; then
ask_anything "Install $display_name as $run_name to $install_location $(G)y$(D)/$(R)n$(D) ? " "y"
else
	check_version "$run_pathfile"
	installed_version=$version
	if [ -z "$version" ]; then
	eval echo -e "'$indent$(Dt)''$(R)''Unknown ''$(D)''version of $run_name found at $run_pathfile.'" $pipe_to_log
	ask_anything "Overwrite unknown y/n ? " "n"
	else
	check_version "$0"
	my_version=$version
	compare "$installed_version" "$my_version"
	local result="$?"
	case $result in
	100)	return; eval echo -e '"$indent$(Dt)"Same version of $run_name already installed.' $pipe_to_log ;;
	101)	B
			eval echo -e '"$indent$(Dt)"This is OLDER than installed version of $run_name found at $run_pathfile' $pipe_to_log
			eval echo -e '"$indent$(Dt)"This is v$my_version, installed software is v$installed_version' $pipe_to_log
			D ;; 
	102)	eval echo -e '"$indent$(Dt)"This is $display_name v$my_version' $pipe_to_log
			eval echo -e '"$indent$(Dt)"Older $display_name $installed_version found at $run_pathfile.' $pipe_to_log
			ask_anything "Update it now y/n ? " "y";;
	esac
	fi
fi
[[ $response = "y" ]] && install_it
}

uninstall_it()
{
echo "Uninstalling $(B) $display_name $(D)"
if [ -f "$run_pathfile" ]; then
sudo rm $run_pathfile
	if [ -f "$run_pathfile" ]; then
	eval echo -e '$indent$(Dt)"Uninstalling $run_pathfile $(R)FAILED.$(D)"' $pipe_to_log
	else
	eval echo -e '$indent$(Dt)"Uninstalling "$run_pathfile successful.' $pipe_to_log
	fi
else
eval echo -e '$indent$(Dt)"Could $(R)not$(D) find $run_pathfile"' $pipe_to_log
fi
}

wait_for_done()
{
local proc_name="$@"	
while [ "$(pgrep "$proc_name")" != "" ]; do
echo -en "\r                       "
echo -en "\rActive: $(pgrep $proc_name | wc -l)"
sleep 0.2
done
echo -e "\r                         "
}

get_my_ip()
{
ip_path=""
if [ "$(which ip)" = "" ]; then
ip_path=$(find /bin /sbin -type f -name "ip"  -print -quit)
	if [ "$ip_path" = "" ]; then
	eval echo -e '$indent$(Dt)"Attempt to find required \"ip\" utility $(R)FAILED.$(D)"' $pipe_to_log
	exit 1
	fi
fi
my_ip=( $(ip -f inet addr | grep -e "en" -e "wl" -e "sl" -e "ww" -e "eth" -e "wlan" -e "tun" -e "vir" |grep "inet" \
| grep -o -E  '[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\/[[:digit:]]{1,2}') )
if [ "$my_ip" = "" ]; then
eval echo -e '$indent$(Dt)"Detecting own ip address $(R)FAILED.$(D)"' $pipe_to_log
else
eval echo -e '$indent$(Dt)"Detected own ip address(es) ${my_ip[*]}"' $pipe_to_log
fi
}

zero_pad()
{
local s="$1"
while [ "${#s}" -lt "$2" ]; do
s="0$s"
done
unset result
result="$s"
}

remove_leading_zeros()
{
unset result	
result=$(echo "$1" | sed  's/^0*/0/g; s/\(^0*\)\([1-9]\)/\2/')	
}	

space_pad()
# $1 string
# $2 length
{
local s="$1"
while [ "${#s}" -lt "$2" ]; do
s="$s "
done
unset result
result="$s"
}

conv()
{
Q="$1"
B="$2"
while [ $Q -gt 0 ]; do
R+=$[ $Q % $B ]
Q=$[ $Q / $B ]
done
unset result
result=$(echo $R | rev)
}

dec_to_bin()
{
local i=0
local bn=""
bin="too_big"
remove_leading_zeros "$1"
[[ ${#result} -gt 18 ]] && return 1
local Q=$result
until [ x$Q = x"0" ]; do
bn=$bn$((10#$Q%2))
Q=$((10#$Q/2))
(( i++ ))
done
unset result
result=$(echo $bn | rev)
}	

dec_to_hex()
{
local i=0
local bn=""
hex="too_big"
remove_leading_zeros "$1"
[[ ${#result} -gt 18 ]] && return 1
local Q="$1"
until [ x$Q = x"0" ]; do
rem=$((10#$Q%16))
case $rem in
10) dig="A" ;;
11) dig="B" ;;
12) dig="C" ;;
13) dig="D" ;;
14) dig="E" ;;
15) dig="F" ;;
*)  dig="$rem"
esac
bn=$bn$dig
Q=$((10#$Q/16))
(( i++ ))
done
unset result
result=$(echo $bn | rev)
}	

dec_to_ip()
{
local Q="$@"	
ip_3=$[Q / (256**3)]
R=$[Q % (256**3)]
Q=$R
ip_2=$[Q / (256**2)]
R=$[Q % (256**2)]
Q=$R
ip_1=$[Q / 256]
R=$[Q % 256]
ip_0=$R
unset result
result="$ip_3.$ip_2.$ip_1.$ip_0"	
}

separate()
{
unset result	
result=$(echo "$1" | sed "s/\(.\)/\1$2/g; s/$2$//g")	
}

mask()
{
local str="$1"	
local num=$2
local mask_char="$3"
local st
str_rev=$(echo "$str" | rev) 
separate "$str_rev" " "
st=($result)
for (( i=0; i<$num; i++ )); do
st[i]="$mask_char"
done
unset result
result=$(echo ${st[@]} | tr -d ' ' | rev)
}

rem_dupes()
{
local st=$(echo "$@" | tr -d '\r' | tr "\t" " ")	
local st_tbl=(${st[@]})
num=${#st_tbl[@]}
for (( i=0; i<$[$num-1]; i++ )); do
	for (( j=$[$i+1]; j<$num; j++ )); do
	if [ "x${st_tbl[i]}" = "x${st_tbl[j]}" ]; then
	st_tbl[j]=""
	fi
	done
done
unset result
result=${st_tbl[@]}
}

get_bin_ip()
{
local ip_st=${ip//./ }
local ip_tbl=(${ip_st[@]})
local ip_bin
for (( i=0; i<4; i++ )); do
dec_to_bin "${ip_tbl[i]}"
ip_bin[i]=$result
zero_pad "${ip_bin[i]}" "8"
ip_bin[i]=$result
done
unset result
result=(${ip_bin[@]})
}

valid_ip()
{
local  ip=$1
local  stat=1

if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
    OIFS=$IFS
    IFS='.'
    ip=($ip)
    IFS=$OIFS
    [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
        && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
    stat=$?
fi
return $stat
}

ping_now()
{
local ip="$@"
( ping -n -q -c1 -w5 -W5 "$ip" &>/dev/null && echo "$ip" >>  /tmp/found ) &
}

check_http()
# $1 string
# $2 requested response length
{
local ip="$@"	
http_info=$(wget -S -t1 -T1 -O- "$ip" 2>&1 | tr -d '\r' | grep  -ie title -e server -e phone\
| sed 's/Server://g; s/<[^>]*>//g' | xargs 2>/dev/null | head -c120)
rem_dupes "$http_info"
unset result
result=${http_info:0:$field_http}
}

check_host()
# $1 = ip address
# result = hostname
{
local resp=$(host -4 -W10 "$1")    
local a=${resp##* }
unset result
result=${a%.*}  # Drop trailing dot    
}


close_all()
{
# Print out field lengths
field_ip="16"
field_host="45"
field_http="70"  # Will scroll on terminal if too big
local header_text_host""
local header_text_http=""
	
# Wait for all parallel pings to complete
wait_for_done "ping"
# and handle the results
if [ -f /tmp/found ]; then
	space_pad "IP Address" "$field_ip"
    header_text_ip=$result
    space_pad "Host" "$field_host"
    header_text_host=$result
    header_text_http="Http response"
    [[ $rdns = "off" ]] && header_text_host=""
    [[ $http = "off" ]] && header_text_http=""
    # Show some additional header text only if test was requested
	if [ "$http" = "on" ] || [ "$rdns" = "on" ]; then  
	echo "$(H)$header_text_ip$header_text_host$header_text_http$(D)"
    fi       
	cat /tmp/found | sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n >/tmp/sorted
	while read line 
		do 
        if [ "$rdns" = "on" ]; then  # hostname requested
            check_host "$line"
            if [ -z "$result" ]; then host_response="-"; else host_response="$result"; fi
            space_pad "$host_response" "$field_host"
            host_spaced=$result
        else
            host_spaced=""
		fi
        # let's use all space if host name not requested
        [[ "$rdns" = "off" ]] && field_http="115"
		if [ "$http" = "on" ]; then  # http test requested
            check_http "$line" "$field_http"
            if [ -z "$result" ]; then http_response="-"; else http_response="$result"; fi
		fi
        # constant field length for ip...
		space_pad "$line" "$field_ip"
		ip_spaced="$result"
        
		eval echo -e '"$ip_spaced"$(C)"$host_spaced""$http_response"$(D)' $pipe_to_log
		done <"/tmp/sorted"
else
	if [ "$done" = "y" ]; then
	echo "Nothing found."
	fi
fi
chmod 777 /tmp/found /tmp/sorted &>/dev/null
exit	
}

init()
{
par_0="$0"
my_name=$(echo ${par_0##*/})
if [ x"$install_location" = "x" ]; then install_location="/usr/local/bin"; fi
if [ x"$display_name" = "x" ]; then display_name="Version control and installer module"; fi
if [ x"$run_name" = "x" ]; then run_name="$my_name"; fi
if [ x"$logs_dir" = "x" ]; then logs_dir="."; fi
log_file="$run_name.log"
if [ "$log" = "yes" ] || [ "$log" = "y" ] || [ "$log" = "on" ]; then
logging on
else
logging off
fi
install_or_not
}

help()
{
help_screen=\
"
chknodes ---- fast parallel ping

$(H)$run_name [-l|--log] [-h|--help] [-H|--http] [-u|--uninstall] [-v|--version] [-V|--verbose] $(D)

  -l | --log        Log to file
  -h | --help       Show this help screen
  -H | --http       Check also http response
  -n | --names      Get also host names
  -u | --uninstall  Remove installation
  -v | --version    Show version
  -V | --verbose    Show each ip address pinged
"
echo "$help_screen"
}

# Main #################################################################

install_location="/usr/local/bin"
run_name=chknodes
run_pathfile="$install_location/$run_name"
log="off"; http="off"; verbose="off"; rdns="off"
display_name="chknodes --- fast parallel ping"
indent=''; blank=' '
done="n"
trap close_all HUP INT USR1 USR2 TERM
rm /tmp/found /tmp/sorted &>/dev/null
cmd_line="$@"
par_num="${#@}"
while [ "$par_num" -gt 0 ]; do
	case "$1" in
	-l | --log)         log="on";;
	-h | --help)		help; exit;;
	-H | --http)		http="on";;
    -n | --names)       rdns="on";;
	-u | --uninstall)   uninstall_it; exit;;
	-v | --version)     show_version "$0" "$display_name"; exit ;;
	-V | --verbose)     verbose="on";;
	*)                  echo "$(R)Invalid option: $1 $(D)"; help; exit;; 
	esac
	(( par_num-- ))
	shift
done
init
get_my_ip  
# Build network choices menu
echo "Select network to check:"
for i in ${!my_ip[*]}  #selaa indeksinumeroilla kun huutomerkki
do
    # Let's replace 4th field of ip by "0" to indicate network instead of single address
    network=$(sed 's/\([[:digit:]]\{1,3\}.[[:digit:]]\{1,3\}.[[:digit:]]\{1,3\}.\)\([[:digit:]]\{1,3\}\)\(\/.*\)/\10\3/' <<< "${my_ip[i]}")
    echo "  $i = network $network"
done
echo "  s = Specify other network"
read -p "  Select: " choice 
# Require to select 0..9 from menu or "s"
case $choice in
    [0-9])  if [ $choice -le ${#my_ip[*]} ]; then network=${my_ip[$choice]}; else echo "Invalid choice";exit 1; fi ;;
    s)      read -p "Give network: " choice; network=$choice  ;;
    *)      echo "Invalid choice"; exit 1  ;;      
esac
# We should have now network in "$network"
# Check that network was given as CIDR aa.bb.cc.dd/xx
# and some other things
if ! $(echo $network | grep "/" &>/dev/null); then
    echo "CIDR: /xx missing"; exit 1 
fi

ip_only=${network%%\/*}   # /24 cut off...
bits_only=${network##*/}

if ! valid_ip "$ip_only"; then
    echo "Invalid ip address"; exit 1 
fi
if [ -z "$bits_only" ] || [ "$bits_only" -gt 31 ]; then
    echo "CIDR: Invalid /xx"; exit 1 
fi

net_bits=$(echo ${network##*/}) 
echo "Netbits= $net_bits"
if [ "$net_bits" -lt "9" ]; then
eval echo -e '$indent$(Dt)"Too many nodes to ping, net $net_bits"' $pipe_to_log
exit 1   
fi
# Checks passed, let's go ahead
node_bits=$[32-$net_bits]
echo "Nodebits=$node_bits"
ip=${network%%/*}     
get_bin_ip "$ip"  
ip_bin_tbl=${result[@]} 
ip_bin_st=${ip_bin_tbl// /} 
mask "$ip_bin_st" "$node_bits" "0" 
ip_bin_net="$result"
ip_dec_net=$[2#$ip_bin_net]
i_max=$[ 2 ** $node_bits -1 ]
echo "i_max=$i_max"
for ((i=1; i<i_max; i++)); do 
	ip_dec=$[$ip_dec_net + $i]
	dec_to_ip $ip_dec
	ip=$result
	[[ "$verbose" = "on" ]] && echo $ip
	ping_now "$ip"
	done
done="y"
close_all
