#!/bin/sh /etc/rc.common

START=80
####### Error code table
#101: ERROR: target time profile($item) doesn't exist, abort profile enable
#102: ERROR: target ip profile($item) doesn't exist, abort profile enable
#103: ERROR: target user profile($item) doesn't exist, abort profile enable
#104: ERROR: target user group($item) doesn't exist, abort profile enable
#105: ERROR: some internal errors results in failing to add profile($1) rule, please disable/enable the profile again

UCI_CONFIG=connlimit
IPTABLES=iptables
FORWARD_CONNLIMIT="FORWARD_CONNLIMIT"
#SCRIPT_LOCK="/tmp/web_apply_lock/connlimit"
UCI_TMP_PATH="/tmp/.uci/$UCI_CONFIG"
HASHTABLE=connlimit
BOOT_FILE="/tmp/profile_backup/connlimit/boot_file"
DEBUG_LOG="/tmp/profile_backup/connlimit/debug_log"
LOG="logger"
CGI_ERROR_MSG="/tmp/cgi_error_msg"
#ECHO_DEBUG="echo"
pre_rule=1

source /etc/func_htable.sh

setup_parameters()
{
	is_legal_rule="yes"
	if [ "$2" = "boot" ] ;then
		config_get status $1 status
		config_get time_objs $1 time_objs
		config_get time_grps $1 time_grps
		config_get ip_obj $1 ip_obj
		config_get ip_grp $1 ip_grp
		config_get usr_obj $1 usr_obj
		config_get usr_grp $1 usr_grp
		config_get session $1 session
		#expired options:
		config_get time_obj $1 time_obj
		config_get srcip $1 srcip
	elif [ "$2" = "apply" ] ;then
		status=$(uci get $UCI_CONFIG.$1.status)
		time_objs=$(uci get $UCI_CONFIG.$1.time_objs)
		time_grps=$(uci get $UCI_CONFIG.$1.time_grps)
		ip_obj=$(uci get $UCI_CONFIG.$1.ip_obj)
		ip_grp=$(uci get $UCI_CONFIG.$1.ip_grp)
		usr_obj=$(uci get $UCI_CONFIG.$1.usr_obj)
		usr_grp=$(uci get $UCI_CONFIG.$1.usr_grp)
		session=$(uci get $UCI_CONFIG.$1.session)
	fi
	
	time_set=""
	ip_set=""
	usr_set=""
	usrgrp_set=""
	
	#upgrade old revison's uci option 
	[ -n "$time_obj" ] && handle_old_time_prof "$time_obj" "$1"
	[ -n "$srcip" ] && handle_old_srcip "$srcip" "$1"
	
	#### maximum number of multiple selection
	#time_objs=8
	#time_grps=8
	#ip_objs=8
	#ip_grps=8
	#usr_obj=8
	#usr_grp=8
	
	#time_objs and time_grps
	if [ -n "$time_objs" ] || [ -n "$time_grps" ] ;then
		time_set=""
		time_profile="$time_objs $time_grps"
		for item in $time_profile; do
			ipset -L tmo_$item >/dev/null 2>/dev/null
			if [ "$?" = 0 ] ;then
				time_set=$time_set" --set2 tmo_$item src"
			else
				is_legal_rule=101
				echo -n "ERROR: target time profile($item) does not exist" >$CGI_ERROR_MSG
			fi
		done
	fi
	
	#ip_objs and ip_grps
	if [ -n "$ip_obj" ] || [ -n "$ip_grp" ] ;then
		ip_set=""
		ip_profile="$ip_obj $ip_grp"
		for item in $ip_profile ; do
			ipset -L ipo_$item >/dev/null 2>/dev/null
			if [ "$?" = 0 ] ;then
				ip_set=$ip_set" --set2 ipo_$item src"
			else
				is_legal_rule=102
				echo -n "ERROR: target IP object/group($item) does not exist" >$CGI_ERROR_MSG
			fi
		done
	fi
	
	#user profile
	if [ -n "$usr_obj" ] ;then
		usr_set=""
		for item in $usr_obj ; do
			ipset -L usr_$item >/dev/null 2>/dev/null
			if [ "$?" = 0 ] ;then
				usr_set=$usr_set" --set2 usr_$item src"
			else
				is_legal_rule=103
				echo -n "ERROR: target user profile($item) does not exist" >$CGI_ERROR_MSG
			fi
		done
	fi
	
	#user group
	if [ -n "$usr_grp" ] ;then
		usrgrp_set=""
		for item in $usr_grp ; do
			ipset -L usr_grp_$item >/dev/null 2>/dev/null
			if [ "$?" = 0 ] ;then
				usrgrp_set=$usrgrp_set" --set2 usr_grp_$item src"
			else
				is_legal_rule=104
				echo -n "ERROR: target user group($item) does not exist" >$CGI_ERROR_MSG
			fi
		done
	fi
	
	srcip_act=""
	[ -n "$time_set" ] && {
		srcip_act="$srcip_act -m mset2 $time_set"
	}
	[ -n "$ip_set" ] && {
		srcip_act="$srcip_act -m mset2 $ip_set"
	}
	[ -n "$usr_set" ] && {
		srcip_act="$srcip_act -m mset2 $usr_set"
	}
	[ -n "$usrgrp_set" ] && {
		srcip_act="$srcip_act -m mset2 $usrgrp_set"
	}
	
	flag=""
	[ "$status" != "enable" ] && flag="-u"
	
	[ "$is_legal_rule" = "no" ] && uci set $UCI_CONFIG.$1.status=disable
}

#In old rev, time obj/group are both saved in one uci option:time_obj , also it's only single-selected
handle_old_time_prof()
{
	#time profile==$1
	#profile name==$2
	ver_upgrade=1
	
	local time_obj_list=
	local time_grp_list=
	local section_type=
	local tmo_set=
	local tmg_set=
	
	ipset -L tmo_$1 >/dev/null 2>/dev/null
	[ "$?" = 0 ] && {
		section_type=`uci get time_object.$1`
		if [ "$section_type" = "time_object" ] ;then
			time_obj_list="$1"
			tmo_set=" --set2 tmo_$1 src"
		elif [ "$section_type" = "time_object_group" ] ;then
			time_grp_list="$1"
			tmg_set=" --set2 tmo_$1 src"
		fi
	}
	#remove old version uci option
	uci delete $UCI_CONFIG.$2.time_obj
	#dispatch to new options
	for i in $time_obj_list ;do
		uci add_list $UCI_CONFIG.$2.time_objs="$i"
	done
	for i in $time_grp_list ;do
		uci add_list $UCI_CONFIG.$2.time_grps="$i"
	done
	
	#setup profile parameter
	if [ -n "$tmo_set" ] || [ -n "$tmg_set" ] ;then
		time_set="$tmo_set $tmg_set"
	fi
}

#In old rev, ip obj/group are both saved in one uci option:srcip , also it's only single-selected
handle_old_srcip()
{
	#ip profile==$1
	#profile name==$2
	ver_upgrade=1
	
	local ipo_list=
	local ipg_list=
	local section_type=
	
	ipset -L ipo_$1 >/dev/null 2>/dev/null
	[ "$?" = 0 ] && {
		section_type=`uci get ip_object.$1`
		if [ "$section_type" = "ip_object" ] ;then
			ipo_list="$1"
			ipo_set=" --set2 ipo_$1 src"
		elif [ "$section_type" = "ip_object_group" ] ;then
			ipg_list="$1"
			ipg_set=" --set2 ipo_$1 src"
		fi
	}
	#remove old version uci option
	uci delete $UCI_CONFIG.$2.srcip
	#dispatch to new options
	for i in $ipo_list ;do
		uci add_list $UCI_CONFIG.$2.ip_obj="$i"
	done
	for i in $ipg_list ;do
		uci add_list $UCI_CONFIG.$2.ip_grp="$i"
	done
	
	#setup profile parameter
	if [ -n "$ipo_set" ] || [ -n "$ipg_set" ] ;then
		ip_set="$ipo_set $ipg_set"
	fi
}

connlimit_boot_add()
{
	setup_parameters $1 boot
	rule_record=""
	if [ "$is_legal_rule" = "yes" ] ;then
		if [ -f "$BOOT_FILE" -a "$fastboot" = 1 ] ;then
			echo "-A $FORWARD_CONNLIMIT $flag $srcip_act -m connlimit --connlimit-above $session -j DROP" >> $BOOT_FILE
		else
			$IPTABLES -A $FORWARD_CONNLIMIT $flag $srcip_act -m connlimit --connlimit-above $session -j DROP
		fi
		if [ "$?" = 0 ] ;then
			rule_record=$boot_rule_index
		else
			echo "connlimit_boot_add($1): fails to add profile($1) iptables rule at boot time" >>$DEBUG_LOG
			logger "(Session limit boot), $1 ERROR: fails to add profile($1) iptables rule at boot time"
			uci set $UCI_CONFIG.$1.status=disable
			#use a dummy rule instead of failed case, otherwise sequence will be broken
			$IPTABLES -A $FORWARD_CONNLIMIT -u -j RETURN
			[ "$?" = 0 ] && rule_record=$boot_rule_index
		fi
	else
		#use a dummy rule to keep rule's sequence order, this rule MUST be marked by "-u"
		if [ -f "$BOOT_FILE" -a "$fastboot" = 1 ] ;then
			echo "-A $FORWARD_CONNLIMIT -u -j RETURN" >> $BOOT_FILE
		else
			$IPTABLES -A $FORWARD_CONNLIMIT -u -j RETURN
		fi
		if [ "$?" = 0 ] ;then
			rule_record=$boot_rule_index
		else
			echo "connlimit_boot_add($1): fails to add profile($1) dummy iptables rule at boot time" >>$DEBUG_LOG
			logger "(Session limit boot), $1 ERROR: fails to add profile($1) dummy iptables rule at boot time"
		fi
	fi
	
	#prevent files from duplicated add by fastboot procedure if fastboot fails => clear the profile record first then add
	ht_rm_file $HASHTABLE $1 2>/dev/null
	#record iptables rule's index
	if [ -n "$rule_record" ] ;then
		ht_put $HASHTABLE $1 IDX $boot_rule_index
		boot_rule_index=$(($boot_rule_index+1))
	fi
}

add_rule_apply() 
{
	$ECHO_DEBUG "do add_rule_apply: $1" >/dev/console 2>/dev/null
	setup_parameters $1 apply
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid $flag $srcip_act -m connlimit --connlimit-above $session -j DROP
		if [ "$?" = 0 ] ;then
			ht_put $HASHTABLE $1 IDX $uciid
		else
			echo "add_rule_apply($1): fails to add profile($1) iptables rule" >>$DEBUG_LOG
			echo -n "ERROR: some internal errors results in failing to add profile($1) rule, please reset the profile again" >$CGI_ERROR_MSG
			is_legal_rule=105
			uci set $UCI_CONFIG.$1.status=disable
			#use a dummy rule instead of failed case, otherwise sequence will be broken
			$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
			if [ "$?" = 0 ] ;then 
				ht_put $HASHTABLE $1 IDX $uciid
			else
				#This should not happen. If even a dummy rule can not be inserted, it's fatal
				echo "add_rule_apply($1): fails to add profile($1) dummy iptables rule" >>$DEBUG_LOG
				logger "(WebUI session limit), $1 ERROR: fails to add profile($1) dummy iptables rule into index $uciid!!!"
				#What we can do is to rebuild all again
				iptables -F $FORWARD_CONNLIMIT
				iptables -A $FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN
				config_load $UCI_CONFIG
				fastboot=0
				boot_rule_index=$(($pre_rule+1))
				ver_upgrade=0
				config_foreach connlimit_boot_add profile
			fi
		fi
	else
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
		if [ "$?" = 0 ] ;then
			ht_put $HASHTABLE $1 IDX $uciid
		else
			#This should not happen. If even a dummy rule can not be inserted, it's fatal
			echo "add_rule_apply($1): fails to add profile($1) dummy iptables rule" >>$DEBUG_LOG
			logger "(WebUI session limit), $1 ERROR: fails to add profile($1) dummy iptables rule into index $uciid!!!"
			#What we can do is to rebuild all again
			iptables -F $FORWARD_CONNLIMIT
			iptables -A $FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN
			config_load $UCI_CONFIG
			fastboot=0
			boot_rule_index=$(($pre_rule+1))
			ver_upgrade=0
			config_foreach connlimit_boot_add profile
		fi
	fi
}

rename_rule_apply()
{
	$ECHO_DEBUG "rename_rule: $1 to $2" >/dev/console 2>/dev/null
	ht_rename_file $HASHTABLE $1 $2
}

swap_rule_apply()
{
	$ECHO_DEBUG "swap_rule: $1" >/dev/console 2>/dev/null
	setup_parameters $1 apply
	#kill my original position, then insert to the new position according to the current uciid
	orig_pos=`ht_get $HASHTABLE $1 IDX`
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -D $FORWARD_CONNLIMIT $orig_pos
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid $flag $srcip_act -m connlimit --connlimit-above $session -j DROP
		if [ "$?" != 0 ] ;then
			echo "swap_rule_apply($1): fails to swap profile($1), orig position($orig_pos)" >>$DEBUG_LOG
			logger "(WebUI session limit), $1 ERROR: fails to swap profile($1), orig position($orig_pos)"
			uci set $UCI_CONFIG.$1.status=disable
			#use a dummy rule instead of failed case, otherwise sequence will be broken
			$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
		fi
	else
		$IPTABLES -D $FORWARD_CONNLIMIT $orig_pos
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
		if [ "$?" != 0 ] ;then
			echo "swap_rule_apply($1): fails to swap profile($1), orig position($orig_pos)" >>$DEBUG_LOG
			logger "(WebUI session limit), $1 ERROR: fails to swap profile($1), orig position($orig_pos)"
		fi
	fi
	
	#find which profile's index equals to my new position, do IDX record exchange
	target_profile=`ht_find_word_in_profiles $HASHTABLE IDX $uciid`
	if [ -n "$target_profile" ] ;then
		#In this case, $target_profile should contains only one
		test=`echo "$target_profile" |awk -F" " '{print $2}'`
		if [ -z "$test" ] ;then
			if [ "$target_profile" != "$1" ] ;then
				#swap profile_backup
				ht_replace_entry $HASHTABLE $target_profile IDX $orig_pos
				ht_replace_entry $HASHTABLE $1 IDX $uciid
			fi
		fi
	fi
}

replace_rule_apply()
{
	$ECHO_DEBUG "replace_rule attributes: $1" >/dev/console 2>/dev/null
	setup_parameters $1 apply
	if [ "$is_legal_rule" = "yes" ] ;then
		$IPTABLES -D $FORWARD_CONNLIMIT $uciid
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid $flag $srcip_act -m connlimit --connlimit-above $session -j DROP
		if [ "$?" != 0 ] ;then
			echo "replace_rule_apply($1): fails to replace profile($1) iptables rule" >>$DEBUG_LOG
			echo -n "ERROR: some internal errors results in failing to add profile($1) rule, please reset the profile again" >$CGI_ERROR_MSG
			is_legal_rule=105
			uci set $UCI_CONFIG.$1.status=disable
			#use a dummy rule instead of failed case, otherwise sequence will be broken
			$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
			if [ "$?" = 0 ] ;then 
				ht_put $HASHTABLE $1 IDX $uciid
			else
				#This should not happen. If even a dummy rule can not be inserted, it's fatal
				echo "add_rule_apply($1): fails to add profile($1) dummy iptables rule" >>$DEBUG_LOG
				logger "(WebUI session limit), $1 ERROR: fails to add profile($1) dummy iptables rule into index $uciid!!!"
				#What we can do is to rebuild all again
				iptables -F $FORWARD_CONNLIMIT
				iptables -A $FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN
				config_load $UCI_CONFIG
				fastboot=0
				boot_rule_index=$(($pre_rule+1))
				ver_upgrade=0
				config_foreach connlimit_boot_add profile
			fi
		fi
	else
		$IPTABLES -D $FORWARD_CONNLIMIT $uciid
		$IPTABLES -I $FORWARD_CONNLIMIT $uciid -u -j RETURN
		if [ "$?" != 0 ] ;then
			#This should not happen. If even a dummy rule can not be added, it's fatal.
			echo "replace_rule_apply($1): fails to replace profile($1) dummy iptables rule" >>$DEBUG_LOG
			logger "(WebUI session limit), $1 ERROR: fails to replace profile($1) dummy iptables rule"
			#What we can do is to try to rebuild all rules again
			iptables -F $FORWARD_CONNLIMIT
			iptables -A $FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN
			config_load $UCI_CONFIG
			fastboot=0
			boot_rule_index=$(($pre_rule+1))
			ver_upgrade=0
			config_foreach connlimit_boot_add profile
		fi
	fi
}

delete_rule_apply()
{
	$ECHO_DEBUG "delete_rule: $1" >/dev/console 2>/dev/null
	pos=`ht_get $HASHTABLE $1 IDX`
	ht_rm_file $HASHTABLE $1
	
	#echo "pos=$pos" >/dev/console
	while true ;do
		target_profile=`ht_find_word_in_profiles $HASHTABLE IDX $(($pos+1))`
		if [ -n "$target_profile" ] ;then
			#echo "position ($pos+1) found=> replace $target_profile IDX" >/dev/console
			ht_replace_entry $HASHTABLE $target_profile IDX $pos
			pos=$(($pos+1))
		else
			#echo "position ($pos+1) not found=> break" >/dev/console
			break
		fi
	done
	$IPTABLES -D $FORWARD_CONNLIMIT $uciid 2>/dev/dull
}

apply_rule() {
	config_get uciaction $1 uciaction
	config_get uciid $1 uciid
	#increase rule index offset due to the 1st rule in start(): -I FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN
	uciid=$(($uciid+$pre_rule))
	
	case $uciaction in
		add)
			$ECHO_DEBUG "action: add" >/dev/console 2>/dev/null
			add_rule_apply $1
			;;
		modify)
			#inspect what kind of modification is: position swapping, profile renaming or attribute modifying
			action_type=`cat $UCI_TMP_PATH`
			$ECHO_DEBUG "action: modify, profile:$1, action_type=$action_type" >/dev/console 2>/dev/null
			if [ $(echo $action_type |grep '@') ] ;then
				#a profile renaming action
				old=`echo $action_type|sed 's/.*\.//'|awk '{FS="="; print $1}'`
				rename_rule_apply $old $1
				$ECHO_DEBUG "Rule $old rename to $1" >/dev/console 2>/dev/null
			elif [ $(echo $action_type |grep '*') ] ;then
				#a position swapping action
				$ECHO_DEBUG "Iptables Rule $1 SWAP to position $uciid" >/dev/console 2>/dev/null
				swap_rule_apply $1
			else
				#a attribute modifying action
				$ECHO_DEBUG "Rule $1 modify attributes" >/dev/console 2>/dev/null
				replace_rule_apply $1
			fi
			;;
		delete)
			$ECHO_DEBUG "action: delete" >/dev/console 2>/dev/null
			delete_rule_apply $1
			;;
	esac
	#clear related conntrack: improve here if any arguments can be used to filter
	conntrack -F >/dev/null 2 >/dev/null
}

boot()
{
	start
}

stop()
{
	#restart() reviewed ok (rev.1709)
	$IPTABLES -F $FORWARD_CONNLIMIT
	echo "Execute connlimt restart" >/dev/console
}

start()
{
	#Upgrade default message to new one(rev:1233), the message is moved to another config: connlimit_msg
	ret=$(uci get $UCI_CONFIG.msg_connlimit)
	[ -n "$ret" ] && {
		uci delete $UCI_CONFIG.msg_connlimit
		uci commit $UCI_CONFIG
	}
	
	#Start to boot process
	ht_init $HASHTABLE
	if [ -f "$BOOT_FILE" ] ;then
		rm -f $BOOT_FILE
		touch $BOOT_FILE
	else
		touch $BOOT_FILE
	fi
	echo "*filter" >> $BOOT_FILE
	#$IPTABLES -A FORWARD_CONNLIMIT -p udp --dport 53 -j RETURN 	#ignore DNS query: has been moved to ip_set_webcatagory.c (rev.1172)
	echo "-A $FORWARD_CONNLIMIT -p tcp --dport 80 --tcp-flags SYN,FIN,ACK SYN -j RETURN" >> $BOOT_FILE
	fastboot=1
	boot_rule_index=$(($pre_rule+1))
	ver_upgrade=0
	config_load $UCI_CONFIG
	config_foreach connlimit_boot_add profile
	echo "COMMIT" >> $BOOT_FILE
	iptables-restore -n < $BOOT_FILE
	#If iptables-restore fails, go back to regular boot
	[ "$?" != 0 ] && {
		echo "connlimit: fast boot failed, use regular boot" >>$DEBUG_LOG
		logger "connlimit: fast boot failed, use regular boot"
		fastboot=0
		boot_rule_index=$(($pre_rule+1))
		ver_upgrade=0
		config_foreach connlimit_boot_add profile
	}
	[ "$ver_upgrade" = 1 ] && uci commit $UCI_CONFIG

	/etc/init.d/connlimit_msg default_connlimit
	conn_ena=$(cat /etc/config/connlimit | grep enable -c)
	# 1:enable session limit  2:debug log 3:clean old node 4:flush all node
	if [ "$conn_ena" != "0" ]; then
		echo 1 > /proc/sys/net/netfilter/nf_conntrack_session_limit
	fi
}

apply() {
	tmpcfg=${UCI_CONFIG}_`cat /proc/uptime | cut -d. -f 1`
	uci fchanges export $UCI_CONFIG > /etc/config/$tmpcfg
	config_load $tmpcfg
	rm -f /etc/config/$tmpcfg
	is_change=`uci fchanges all $UCI_CONFIG`
	[ -n "$is_change" ] && {
		config_foreach apply_rule
		uci commit $UCI_CONFIG
		pid=$(ps | awk '/\/sbin\/dataflow/ {print $1}')
		kill -47 $pid 2>/dev/null >/dev/null
		
		conn_ena=$(cat /etc/config/connlimit | grep enable -c)
		da_ena=$(uci -q get dataflow.profile.enable)
		def_connlimit=$(uci get $UCI_CONFIG.msg_connlimit.use_default_session_limit)
		# 1:enable session limit  2:debug log 3:clean old node 4:flush all node
		if [ "$conn_ena" = "0" -a "$da_ena" = "disable" -a "$def_connlimit" = "disable" ]; then
			echo 4 > /proc/sys/net/netfilter/nf_conntrack_session_limit
		else
			echo 1 > /proc/sys/net/netfilter/nf_conntrack_session_limit
		fi
		#Handle return error code
		if [ "$is_legal_rule" != "yes" -a "$is_legal_rule" != "no" ] ;then
			exit $is_legal_rule
		fi
		
		/etc/init.d/connlimit_msg default_connlimit
	}
}
