#!/bin/bash
# $Id: scan,v 1.15 2010/04/19 04:14:46 jlarsen Exp $
#
# Description: bash script that scans a user selected range of IP addresses.
# It tries pinging each address and reports back which are active.
#
# 19-Apr-10: Fix made by Jim Guion <jim_guion@hotmail.com> at line 486. Jim
#            figured out that strings needed to be converted to numbers for
#            proper comparison.
#

#-------------------------------------------------------------------------------
# get_version.  Function that parses script version and date info from the RCS
# Id line.
function get_version () {
local func_name=get_version
if [ ! $SCRIPT_VER ]; then
cat << EOF > /tmp/$$.version
\$Id: scan,v 1.15 2010/04/19 04:14:46 jlarsen Exp $
EOF
SCRIPT_VER=`awk '{print $3}' < /tmp/$$.version`
SCRIPT_VER="v$SCRIPT_VER"
SCRIPT_DATE=`awk '{print $4}' < /tmp/$$.version`
rm -f /tmp/$$.version
fi
} # get_version


#------------------------------------------------------------------------------
# display_debug_help:  Function to display debug help.
function display_debug_help () {
local func_name=display_debug_help
if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside display_debug_help";fi
cat << EOF | more
----- Debug Help ($SCRIPT_VER  -  $SCRIPT_DATE) ---------------------------------------
Debug is enabled in one of three ways in decending order of precedence: 

   "-d 0xNNNN" on command line
   PT_DEBUG_FLAGS environment variable
	"debug_flags" file in same directory as pt

Debug output can be changed "on the fly" by changing the contents of "debug_flags",
which is read each time pt goes through its while loop.

Each debug line is qualified with a construct like the following:

if [ \$((\$D & 0x1)) -ne 0 ]; then echo "[\$LINENO]\$func_name:\$\$> Debug message or action";fi

The expression on the left of -ne is a bash construct.  The $((expr)) gets
evaluated and returns a value.  The expression I'm using is "$D & 0xNN".  The
& performs a bitwise AND and returns the result.  If it is zero (no matching
bits) then the debug line is skipped.  If it is non zero then the debug line
is performed.  Each debug line can have multiple bits that turn it on or just
a single bit.  The pattern "0xNN" is the collection of bits that turn the 
debug on.  The action of each bit is defined below.

00000000 - No debug

00000001 - Enable all the "Inside function name" debug lines
00000002 - Enable process_command_line outputs
00000004 - Enable ping_ip debug output

80000000 - Don't output the "Debug flags changed" messages in "set_debug_level"
EOF
} # display_debug_help


#------------------------------------------------------------------------------
# set_debug_level:  Function that sets the $D variable based on a file, an
#environment variable, or a command line variable. See display_debug_help.
function set_debug_level () {
local func_name=set_debug_level

# Note: Command line -d overrides environment variable SCAN_DEBUG_FLAGS if it exists, which overrides debug_flags file if it exists
# Check one time if SCAN_DEBUG_FLAGS env variable exists and use it if it does
if [ "${ENV_VAR_TESTED:=FALSE}" = "FALSE" ]; then
	ENV_VAR_TESTED=TRUE
	D=${SCAN_DEBUG_FLAGS:-0}
	if [ "$D" != "0" ] && [ $(($D & 0x80000000)) -eq 0 ]; then echo "[$LINENO]$$> ${func_name:-startup}: set_debug_level: Debug flags changed to: $D"; fi
fi

# Check the existence of a file named "debug_flags" and use it if conditions are right
if [ -e debug_flags ]; then
	if [ "${DEBUG_FLAGS_FILE:=FIRST_READ}" = "FIRST_READ" ]; then
		# Getting here means the debug_flags file has never been read.
		DEBUG_FLAGS_FILE=`cat -s debug_flags`
		if [ "$D" = "0" ]; then
			# Getting here means $D was zero and the debug_flags contents should be used instead since env variable doesn't exist or has value of 0
			D=$DEBUG_FLAGS_FILE
			if [ "$D" != "0" ] && [ $(($D & 0x80000000)) -eq 0 ]; then echo "[$LINENO]$$> ${func_name:-startup}: set_debug_level: Debug flags changed to: $D"; fi
		fi
	else
		# Read debug_flags file and update $D if the file has changed
		NEW_DEBUG_FLAGS_FILE=`cat -s debug_flags`
		if [ $NEW_DEBUG_FLAGS_FILE != $DEBUG_FLAGS_FILE ]; then
			DEBUG_FLAGS_FILE=$NEW_DEBUG_FLAGS_FILE
			D=$DEBUG_FLAGS_FILE
			if [ $(($D & 0x80000000)) -eq 0 ]; then
				echo "[$LINENO]$$> ${func_name:-startup}: set_debug_level: Debug flags changed to: $D"
			fi
		fi
	fi
fi
} # set_debug_level


#-------------------------------------------------------------------------------
# setup_env.  Function that determines what OS the script is running on and
# setups environment variables accordingly.
function setup_env () {
local func_name=setup_env
if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside setup_env";fi
# Figure out which OS you're running on and setup program paths accordingly
CHECK_OS=`uname | sed -n -e 's/SunOS.*/SunOS/p' `
if [ "$CHECK_OS" = "SunOS" ]; then
#if [ $CHECK_OS = SunOS ]; then
   # Solaris paths
	HOST=`/usr/ucb/hostname`
	LS=/bin/ls
	MAIL=/usr/ucb/mail
	PING=/usr/sbin/ping
	PS=/bin/ps
	TRACE_ROUTE=/usr/sbin/traceroute
	ZIP_PGM=/usr/bin/zip
else
   # Mandrake Linux paths
	HOST=`/bin/hostname`
	LS=/bin/ls
	MAIL=/bin/mail
	PING=/bin/ping
	PS=/bin/ps
	TRACE_ROUTE=/usr/sbin/traceroute
	ZIP_PGM=/usr/bin/zip
fi

# Set the default values of all environment variables here
FILE_NAME=/dev/null
START_IP=not_entered
END_IP=not_entered
LIVE_IPS=0
DEAD_IPS=0
IP_A=0
IP_B=0
IP_C=0
IP_D=0
YES=1
NO=0
TRUE=1
FALSE=0
ACTIVE=0
DEAD=1
TIMED_OUT=255
TIME_OUT_CNT=2
GOOD_IP=0
BAD_IP=1
} # setup_env


#------------------------------------------------------------------------------
# Function to display help
function display_help () {
local func_name=display_help
if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside display_help";fi
cat << EOF
----- scan ($SCRIPT_VER  -  $SCRIPT_DATE) --------------------------------------------
usage:
scan start_IP end_IP [ options ]

Each IP address is tested between start_IP and end_IP.  Live IP addresses are reported.

Options:
-f name  Filename for output (Default is standard out)
-h       This help screen
-hd      Debug help
-to n    Ping timeout count in seconds (Default 2)

EOF
} # display_help


#------------------------------------------------------------------------------
# clock - Function that prints a changing pattern to a spot on the screen
# $1 - "first" means the first time function is called
function clock () {
clock[0]="/"
clock[1]="-"
clock[2]="\\"
clock[3]="|"
if [ ${1:-not_first} = "first" ]; then
	clock_cnt=0
	printf "|"
fi
if [ $clock_cnt -eq 4 ]; then
	clock_cnt=0	
fi
printf "\b%s" ${clock[$clock_cnt]}
let clock_cnt=clock_cnt+1
} # clock


#------------------------------------------------------------------------------
# process_command_line
function process_command_line () {
local func_name=process_command_line
if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside process_command_line";fi
while [ $# -ne 0 ]
	do
	case $1 in
		-d) #debug mode
		   # Command line -d overrides environment variable SCAN_DEBUG_FLAGS if it exists, which overrides debug_flags file if it exists
			D=$2
			if [ "$D" != "0" ] && [ $(($D & 0x80000000)) -eq 0 ]; then echo "[$LINENO]$func_name:$$> Debug flags changed to: $D"; fi
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -d"; fi 
			shift 
			;;

		-f)
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -f"; fi
			FILE_NAME=$2
			shift
			;;

		-h) #show help
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -h"; fi
			display_help
			exit
			;;

		-hd) #show debug help
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -hd"; fi
			display_debug_help
			exit
			;;

		-to)
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -to"; fi
			TIME_OUT_CNT=$2
			shift
			;;

		*.*.*.*)
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case *.*.*.*"; fi
			if [ $START_IP = "not_entered" ]; then
				START_IP=$1
			else
				END_IP=$1
			fi
			;;

		*.*.*)
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case *.*.*"; fi
			display_help
			echo "ERROR:  Incomplete IP address:  $1"
			exit
			;;

		*.*)
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case *.*"; fi
			display_help
			echo "ERROR:  Incomplete IP address:  $1"
			exit
			;;

		*) #IP address
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case *"; fi
			display_help
			echo "ERROR:  Incomplete IP address:  $1"
			exit
			;;

		-*) # Unknown command line argument
			if [ $(($D & 0x2)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> In case -*"; fi
			display_help
			echo "ERROR:  Unrecognized command line argument:  $1"
			exit
			;;

	esac

   # Don't shift command line if processing at last argument
	if [ $# -ne 0 ]; then
		shift
	fi
done
} # process_command_line


#------------------------------------------------------------------
# ping_ip:  Function that pings an IP 
# $1: IP to ping 
# $2: Timeout value (default: 2 seconds)
# $RC_PING_IP has the value $GOOD_IP or $BAD_IP
function ping_ip () {
	local func_name=ping_ip
	if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside ping_ip (IP: $1  Timeout: ${2:-2})";fi
   # This local function is what gets timed
	function _ping_ip() {
		if [ "$CHECK_OS" = "SunOS" ]; then
			# Solaris line
			PING_RESULT=`$PING -s -n $1 56 1`
		else
			# Linux line
			PING_RESULT=`$PING -q -c 1 $1`
		fi
      # Check for success.  Don't check for failure because it fails in different 
      # ways hiding a dropped connection.
		PING_STATUS=`echo $PING_RESULT | sed -n -e 's/.*, 0% packet loss.*/ping_good/p' `
		if [ $(($D & 0x100)) -ne 0 ]; then 
			print "\n"
			echo "[$LINENO]$func_name:$$> PING_RESULT: $PING_RESULT"
			echo "[$LINENO]$func_name:$$> PING_STATUS: $PING_STATUS"
			print "\n"
		fi

      # Process the results of the ping
		if [ "$PING_STATUS" = "ping_good" ]; then
			# Increment the good ping counters
			echo "$GOOD_IP" >| scan.ret_val
		else
			echo "$BAD_IP" >| scan.ret_val
		fi
	} # _ping_ip

	# Initialize the file that gets tested to detect timeout condition
   # The timed_function overwrites this with numerical return value when it succeeds
	echo "TIMED_OUT" >| scan.ret_val

   # Start the timed_function as a subshell in the background
	(_ping_ip $1  ) &

   # Allow up to $2 seconds for timed_function to complete but default to 2 seconds
	local count=0
	local max_count=${2:-2}
	while [ $count -lt $max_count ]; do
		ret_val=`cat scan.ret_val`
		if [ "$ret_val" != "TIMED_OUT" ]; then
			rm -f scan.ret_val
			RC_PING_IP=$ret_val
			return $ret_val
		fi
		let count=count+1
		if [ $(($D & 0x100)) -ne 0 ]; then printf "."; fi
		sleep 1
		clock
	done
   # Getting here means timed_function timed out. Kill the subshell $!
	kill -n 9 $! 
	if [ $(($D & 0x100)) -ne 0 ]; then 
		printf "\n"
		echo "[$LINENO]$func_name:$$> ping_ip -I $1 timed out"
	fi
	rm -f scan.ret_val
	RC_PING_IP=$BAD_IP
	return $BAD_IP
} # ping_ip


#------------------------------------------------------------------
# scan_ips:  This function does the IP scanning
function scan_ips () {
	local func_name=scan_ips
	if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside scan_ips";fi

   # Load the loop counters based on the starting IP address
	PARSE_IP=`echo $START_IP | sed -n -e 's/\./ /gp'`
	IP_A=`echo $PARSE_IP | awk  '{print $1}'`
	IP_B=`echo $PARSE_IP | awk  '{print $2}'`
	IP_C=`echo $PARSE_IP | awk  '{print $3}'`
	IP_D=`echo $PARSE_IP | awk  '{print $4}'`
	TEST_IP="$IP_A.$IP_B.$IP_C.$IP_D"

	start_time=`date +%Y\-%b\-%d\ %H\:%M\:%S\ %a`
	echo "----- scan ($SCRIPT_VER  -  $SCRIPT_DATE) ---------------------------------------" | tee $FILE_NAME
	echo "Test range: $START_IP  to  $END_IP" | tee -a $FILE_NAME
	echo "Ping timeout value: $TIME_OUT_CNT seconds" | tee -a $FILE_NAME
	echo "The following IP addresses respond to a ping:" | tee -a $FILE_NAME

	printf "%15s  " $TEST_IP
	clock first

	local col_cnt=0
	while [ $TEST_IP != $END_IP ]; do
		ping_ip $TEST_IP $TIME_OUT_CNT 2>/dev/null
		if [ $RC_PING_IP -eq $GOOD_IP ]; then
			let LIVE_IPS=LIVE_IPS+1
			if [ $col_cnt -eq 3 ]; then
            # Cleanup the screen by backing up and outputing a space
				printf "\b \n"
				# Output the IP to the file since it is alive
				printf "%15s\n" $TEST_IP >> $FILE_NAME
				col_cnt=0
			else
            # Cleanup the screen by backing up and outputing a space
				printf "\b  "
				# Output the IP to the file since it is alive
				printf "%15s  " $TEST_IP >> $FILE_NAME
				let col_cnt=col_cnt+1
			fi
		else
			let DEAD_IPS=DEAD_IPS+1
         # Since the IP is dead backup on the screen so it can be overwritten
			printf "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
		fi
		let IP_D=IP_D+1
		if [ $IP_D -eq 255 ]; then
			IP_D=1
			let IP_C=IP_C+1
			if [ $IP_C -eq 256 ]; then
				IP_C=0
				let IP_B=IP_B+1
				if [ $IP_B -eq 256 ]; then
					IP_B=0
					let IP_A=IP_A+1
					if [ $IP_A -eq 256 ]; then
						echo "ERROR: Illegal IP address reached!"
						exit
					fi
				fi
			fi
		fi
		TEST_IP="$IP_A.$IP_B.$IP_C.$IP_D"
      # Output only to STDOUT the IP under test
		printf "%15s  " $TEST_IP
		clock first
	done

   # Process the last IP address on its own
	ping_ip $TEST_IP $TIME_OUT_CNT 2>/dev/null
	if [ $RC_PING_IP -eq $GOOD_IP ]; then
		let LIVE_IPS=LIVE_IPS+1
		if [ $col_cnt -eq 4 ]; then
			printf "\b \n"
			printf "%15s\n" $TEST_IP >> $FILE_NAME
			col_cnt=0
		else
			printf "\b  "
			printf "%15s\n" $TEST_IP >> $FILE_NAME
			let col_cnt=col_cnt+1
		fi
	else
		let DEAD_IPS=DEAD_IPS+1
		printf "\n" >> $FILE_NAME
		printf "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                  "
	fi

	printf "\n"
	# Output to the file
	echo "Scan started: $start_time" | tee -a $FILE_NAME
	echo "Scan ended:   `date +%Y\-%b\-%d\ %H\:%M\:%S\ %a`" | tee -a $FILE_NAME
	echo "Live IPs: $LIVE_IPS   Dead IPs: $DEAD_IPS   Total IPs scanned: `expr $LIVE_IPS + $DEAD_IPS`" | tee -a $FILE_NAME
	echo "--------------------------------------------------------------------------------" | tee -a $FILE_NAME

} # scan_ips


#------------------------------------------------------------------------------
# main
function main () {
local func_name=main
if [ $(($D & 0x1)) -ne 0 ]; then echo "[$LINENO]$func_name:$$> Inside main";fi

# If no arguments are passed then output the help screen and exit
if [ $# -eq 0 ]; then
	display_help
	exit
fi

setup_env
process_command_line $*

# Check for START_IP on command line
if [ $START_IP = not_entered ]; then
	display_help
	echo "ERROR: You must provide a starting IP address"
	exit
fi

# Check for END_IP on command line
if [ $END_IP = not_entered ]; then
	display_help
	echo "ERROR: You must provide an ending IP address"
	exit
fi

# Make sure ending IP is greater than starting IP
# Can't compare the strings, it won't work, convert to numbers

# Convert starting address string to a number
PARSE_IP=`echo $START_IP | sed -n -e 's/\./ /gp'`
let START_NUMBER=`echo $PARSE_IP | awk  '{print $1}'`*1000000000
let START_NUMBER+=`echo $PARSE_IP | awk  '{print $2}'`*1000000
let START_NUMBER+=`echo $PARSE_IP | awk  '{print $3}'`*1000
let START_NUMBER+=`echo $PARSE_IP | awk  '{print $4}'`*1
# Convert ending address string to a number
PARSE_IP=`echo $END_IP | sed -n -e 's/\./ /gp'`
let END_NUMBER=`echo $PARSE_IP | awk  '{print $1}'`*1000000000
let END_NUMBER+=`echo $PARSE_IP | awk  '{print $2}'`*1000000
let END_NUMBER+=`echo $PARSE_IP | awk  '{print $3}'`*1000
let END_NUMBER+=`echo $PARSE_IP | awk  '{print $4}'`*1
if [[ "$END_NUMBER" < "$START_NUMBER" ]]; then
	echo "WARNING: Starting IP $START_IP greater than ending IP $END_IP.  Reversing IPs"
	temp_ip=$START_IP
	START_IP=$END_IP
	END_IP=$temp_ip
fi

# Perform the IP scan
scan_ips

# Remove the scan.ret_val file
rm -f scan.ret_val

} # main

set_debug_level
get_version
main $* 

