#!/bin/sh
#
# wrtbwmon: traffic logging tool for routers
#
# Peter Bailey (peter.eldridge.bailey+wrtbwmon AT gmail.com)
#
# Based on work by:
# Emmanuel Brucy (e.brucy AT qut.edu.au)
# Fredrik Erlandsson (erlis AT linux.nu)
# twist - http://wiki.openwrt.org/RrdTrafficWatch

trap "rm -f /tmp/*_$$.tmp; kill $$" INT
binDir=/usr/sbin
dataDir=/usr/share/wrtbwmon
lockDir=/tmp/wrtbwmon.lock
pidFile=$lockDir/pid
networkFuncs=/lib/functions/network.sh
uci=`which uci 2>/dev/null`
nslookup=`which nslookup 2>/dev/null`
nvram=`which nvram 2>/dev/null`

chains='INPUT OUTPUT FORWARD'
DEBUG=
interfaces='eth0 tun0' # in addition to detected WAN
DB=$2
mode=

# DNS server for reverse lookups provided in "DNS".
# don't perform reverse DNS lookups by default
DO_RDNS=${DNS-}

header="#mac,ip,iface,in,out,total,first_date,last_date"

createDbIfMissing()
{
    [ ! -f "$DB" ] && echo $header > "$DB"
}

checkDbArg()
{
    [ -z "$DB" ] && echo "ERROR: Missing argument 2 (database file)" && exit 1
}

checkDB()
{
    [ ! -f "$DB" ] && echo "ERROR: $DB does not exist" && exit 1
    [ ! -w "$DB" ] && echo "ERROR: $DB is not writable" && exit 1   
}

checkWAN()
{
    [ -z "$wan" ] && echo "Warning: failed to detect WAN interface."
}

lookup()
{
    MAC=$1
    IP=$2
    userDB=$3
    for USERSFILE in $userDB /tmp/dhcp.leases /tmp/dnsmasq.conf /etc/dnsmasq.conf /etc/hosts; do
	[ -e "$USERSFILE" ] || continue
	case $USERSFILE in
	    /tmp/dhcp.leases )
		USER=$(grep -i "$MAC" $USERSFILE | cut -f4 -s -d' ')
		;;
	    /etc/hosts )
		USER=$(grep "^$IP " $USERSFILE | cut -f2 -s -d' ')
		;;
	    * )
		USER=$(grep -i "$MAC" "$USERSFILE" | cut -f2 -s -d,)
		;;
	esac
	[ "$USER" = "*" ] && USER=
	[ -n "$USER" ] && break
    done
    if [ -n "$DO_RDNS" -a -z "$USER" -a "$IP" != "NA" -a -n "$nslookup" ]; then
	USER=`$nslookup $IP $DNS | awk '!/server can/{if($4){print $4; exit}}' | sed -re 's/[.]$//'`
    fi
    [ -z "$USER" ] && USER=${MAC}
    echo $USER
}

detectIF()
{
    if [ -f "$networkFuncs" ]; then
	IF=`. $networkFuncs; network_get_device netdev $1; echo $netdev`
	[ -n "$IF" ] && echo $IF && return
    fi

    if [ -n "$uci" -a -x "$uci" ]; then
	IF=`$uci get network.${1}.device 2>/dev/null`
	[ $? -eq 0 -a -n "$IF" ] && echo $IF && return
    fi

    if [ -n "$nvram" -a -x "$nvram" ]; then
	IF=`$nvram get ${1}_ifname 2>/dev/null`
	[ $? -eq 0 -a -n "$IF" ] && echo $IF && return
    fi
}

detectLAN()
{
    [ -e /sys/class/net/br-lan ] && echo br-lan && return
    lan=$(detectIF lan)
    [ -n "$lan" ] && echo $lan && return
}

detectWAN()
{
    [ -n "$WAN_IF" ] && echo $WAN_IF && return
    wan=$(detectIF wan)
    [ -n "$wan" ] && echo $wan && return
    wan=$(ip route show 2>/dev/null | grep default | sed -re '/^default/ s/default.*dev +([^ ]+).*/\1/')
    [ -n "$wan" ] && echo $wan && return
    [ -f "$networkFuncs" ] && wan=$(. $networkFuncs; network_find_wan wan; echo $wan)
    [ -n "$wan" ] && echo $wan && return
}

lock()
{
    attempts=0
    while [ $attempts -lt 10 ]; do
	mkdir $lockDir 2>/dev/null && break
	attempts=$((attempts+1))
	pid=`cat $pidFile 2>/dev/null`
	if [ -n "$pid" ]; then
	    if [ -d "/proc/$pid" ]; then
		[ -n "$DEBUG" ] && echo "WARNING: Lockfile detected but process $(cat $pidFile) does not exist !"
		rm -rf $lockDir
	    else
		sleep 1
	    fi
	fi
    done
    mkdir $lockDir 2>/dev/null
    echo $$ > $pidFile
    [ -n "$DEBUG" ] && echo $$ "got lock after $attempts attempts"
    trap '' INT
}

unlock()
{
    rm -rf $lockDir
    [ -n "$DEBUG" ] && echo $$ "released lock"
    trap "rm -f /tmp/*_$$.tmp; kill $$" INT
}

# chain
newChain()
{
    chain=$1
    # Create the RRDIPT_$chain chain (it doesn't matter if it already exists).
    iptables -t mangle -N RRDIPT_$chain 2> /dev/null
    
    # Add the RRDIPT_$chain CHAIN to the $chain chain if not present
    iptables -t mangle -C $chain -j RRDIPT_$chain 2>/dev/null
    if [ $? -ne 0 ]; then
	[ -n "$DEBUG" ] && echo "DEBUG: iptables chain misplaced, recreating it..."
	iptables -t mangle -I $chain -j RRDIPT_$chain
    fi
}

# chain tun
newRuleIF()
{
    chain=$1
    IF=$2
    
    #!@todo test
    if [ "$chain" = "OUTPUT" ]; then
	cmd="iptables -t mangle -o $IF -j RETURN"
	eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain"
    elif [ "$chain" = "INPUT" ]; then
	cmd="iptables -t mangle -i $IF -j RETURN"
	eval $cmd " -C RRDIPT_$chain 2>/dev/null" || eval $cmd " -A RRDIPT_$chain"
    fi
}

update()
{
    #!@todo could let readDB.awk handle this; that would place header
    #!info in fewer places
    createDbIfMissing
    
    checkDB
    checkWAN

    > /tmp/iptables_$$.tmp
    lock
    # only zero our own chains
    for chain in $chains; do
	iptables -nvxL RRDIPT_$chain -t mangle -Z >> /tmp/iptables_$$.tmp
    done
    # the iptables and readDB commands have to be separate. Otherwise,
    # they will fight over iptables locks
    awk -v mode="$mode" -v interfaces=\""$interfaces"\" -f $binDir/readDB.awk \
	$DB \
	/proc/net/arp \
	/tmp/iptables_$$.tmp
    unlock
}

############################################################

case $1 in
    "dump" )
	checkDbArg
	lock
	tr ',' '\t' < "$DB"
	unlock
    ;;

    "update" )
	checkDbArg
	wan=$(detectWAN)
	interfaces="$interfaces $wan"
	update
	rm -f /tmp/*_$$.tmp
	exit
	;;

    "publish" )
	checkDbArg
	[ -z "$3" ] && echo "ERROR: Missing argument 3 (output html file)" && exit 1
	
	# sort DB
	lock

	# busybox sort truncates numbers to 32 bits
	grep -v '^#' $DB | awk -F, '{OFS=","; a=sprintf("%f",$4/1e6); $4=""; print a,$0}' | tr -s ',' | sort -rn | awk -F, '{OFS=",";$1=sprintf("%f",$1*1e6);print}' > /tmp/sorted_$$.tmp

        # create HTML page
	rm -f $3.tmp
	cp $dataDir/usage.htm1 $3.tmp

	#!@todo fix publishing
	while IFS=, read PEAKUSAGE_IN MAC IP IFACE PEAKUSAGE_OUT TOTAL FIRSTSEEN LASTSEEN
	do
	    echo "
new Array(\"$(lookup $MAC $IP $4)\",\"$MAC\",\"$IP\",
$PEAKUSAGE_IN,$PEAKUSAGE_OUT,$TOTAL,\"$FIRSTSEEN\",\"$LASTSEEN\")," >> $3.tmp
	done < /tmp/sorted_$$.tmp
	echo "0);" >> $3.tmp
	
	sed "s/(date)/`date`/" < $dataDir/usage.htm2 >> $3.tmp
	mv $3.tmp $3

	unlock
	
	#Free some memory
	rm -f /tmp/*_$$.tmp
	;;
    
    "setup" )
	checkDbArg
	[ -w "$DB" ] && echo "Warning: using existing $DB"
	createDbIfMissing
	
	for chain in $chains; do
	    newChain $chain
	done

	#lan=$(detectLAN)
	wan=$(detectWAN)
	checkWAN
	interfaces="$interfaces $wan"

	# track local data
	for chain in INPUT OUTPUT; do
	    for interface in $interfaces; do
		[ -n "$interface" ] && [ -e "/sys/class/net/$interface" ] && newRuleIF $chain $interface
	    done
	done

	# this will add rules for hosts in arp table
	update

	rm -f /tmp/*_$$.tmp
	;;

    "remove" )
	iptables-save | grep -v RRDIPT | iptables-restore
	rm -rf "$lockDir"
	;;

    *)
	echo \
"Usage: $0 {setup|update|publish|remove} [options...]
Options:
   $0 setup database_file
   $0 update database_file
   $0 publish database_file path_of_html_report [user_file]
Examples:
   $0 setup /tmp/usage.db
   $0 update /tmp/usage.db
   $0 publish /tmp/usage.db /www/user/usage.htm /jffs/users.txt
   $0 remove
Note: [user_file] is an optional file to match users with MAC addresses.
       Its format is \"00:MA:CA:DD:RE:SS,username\", with one entry per line."
	;;
esac
