#!/bin/sh

sp='[[:space:]]\+'
spb='\(^\|[[:space:]]\+\)'
spe='\([[:space:]]\+\|$\)'

servicedir=/etc/alterator/net-iptables

ifacedir="/etc/net/ifaces/"
chaindir="$ifacedir/default/fw/iptables/filter/"
inputchainfile="$chaindir/INPUT"
outputchainfile="$chaindir/OUTPUT"
forwardchainfile="$chaindir/FORWARD"
postroutingchainfile="$ifacedir/default/fw/iptables/nat/POSTROUTING"

alterator_api_version=1

. alterator-sh-functions
. alterator-net-functions
. shell-config
. shell-getopt

commit_stage=1

### low level: network

run_efw()
{
    /etc/net/scripts/contrib/efw --iptables "$@" 2>&1 >/dev/null |
	while read line; do
	    case "$line" in
		iptables:*)
		    msg="${line#iptables:}"
		    ;;
		ERROR:*)
		    msg="${line#ERROR:}:$msg"
		    write_error "$msg"
		    exit 1
		    ;;
	    esac
	done
}

### low level: rules for some chain
add_rule()
{
    local rulelist="$1";shift
    local iface="$1";shift
    local proto="$1";shift
    local port="$1";shift
    local policy="$1";shift

    if [ -n "$port" ];then
	printf -- '-i %s -p %s --dport %s -j %s\n' \
	    "$iface" \
	    "$proto" \
	    "$port" \
	    "$policy"
    else
	printf -- '-i %s -p %s -j %s\n' \
	    "$iface" \
	    "$proto" \
	    "$policy"
    fi >>"$rulelist"
}

remove_rule()
{
    local rulelist="$1";shift

    local iface="$1";shift
    local proto="$1";shift
    local port="$1";shift

    if [ -n "$port" ];then
	sed -i \
	    -n \
	    -e "/^#/ {p;b}" \
	    -e "/^$sp\$/ {p;b}" \
	    -e "/$spb-i$sp$iface$spe/! {p;b}" \
	    -e "/$spb-p$sp$proto$spe/! {p;b}" \
	    -e "/$spb--dport$sp$port$spe/! {p;b}" \
	    "$rulelist"
    else
	sed -i \
	    -n \
	    -e "/^#/ {p;b}" \
	    -e "/^$sp\$/ {p;b}" \
	    -e "/$spb-i$sp$iface$spe/! {p;b}" \
	    -e "/$spb-p$sp"$proto$spe"/! {p;b}" \
	    "$rulelist"
    fi
}

find_rule()
{
    local rulelist="$1";shift

    local iface="$1";shift
    local proto="$1";shift
    local port="$1";shift

    if [ -n "$port" ];then
	sed -n \
	    -e "/^#/ b" \
	    -e "/^$sp\$/ b" \
	    -e "/$spb-i$sp$iface$spe/! b" \
	    -e "/$spb-p$sp$proto$spe/! b" \
	    -e "/$spb--dport$sp$port$spe/! b" \
	    -e 'p' \
	    "$rulelist"
    else
	sed -n \
	    -e "/^#/ b" \
	    -e "/^$sp\$/ b" \
	    -e "/$spb-i$sp$iface$spe/! b" \
	    -e "/$spb-p$sp"$proto$spe"/! b" \
	    -e "p" \
	    "$rulelist"
    fi
}

test_rule()
{
    local policy="$1";shift
    find_rule "$@"|
	grep -qs "$spb-j$sp$policy$spe"
}

list_opened_ports()
{
    local rulelist="$1";shift

    local iface="$1";shift
    local policy="ACCEPT"
    local proto=
    for proto in "tcp" "udp"; do
      sed -n \
        -e "/^#/ b" \
        -e "/^$sp\$/ b" \
        -e "/$spb-i$sp$iface$spe/! b" \
        -e "/$spb-p$sp$proto$spe/! b" \
        -e "/$spb-j$sp$policy$spe/! b" \
        -e "s/^.*$spb--dport$sp\([0-9][0-9]*\)$spe.*\$/$proto:\2/" \
        -e 'p' \
        $rulelist
    done
}

### low level: service (set of rules)

list_desktop()
{
    alterator-dump-desktop \
	    -v lang="$in_language" \
	    -v out="Filename;X-Alterator-Port;Name" \
	    -v def="notfound;noport;" \
    $servicedir/*.desktop|
	while read filename port name; do
	    filename="${filename##*/}"
	    filename="${filename%.desktop}"
	    printf '%s\t%s\t%s\n' "$filename" "$port" "$name"
	done
}


# is all ports in portset are on?
test_portset()
{
    local iface="$1";shift
    local portlist="$1";shift

    local IFS=";"
    for i in $portlist; do
	local proto=${i%:*}
	local port=${i#*:}

	test_rule ACCEPT "$inputchainfile" "$iface" "$proto" "$port" || return 1
    done
    return 0
}

# is alterator port are on?
is_dangerous()
{
  [ -f "/etc/ahttpd/ahttpd.conf" ] || return 0
  port=$(shell_config_get "/etc/ahttpd/ahttpd.conf" "server-port" "[[:space:]]\+")
  local iface="$1";shift
  ! test_rule ACCEPT "$inputchainfile" "$iface" "tcp" "$port"
}


write_portset()
{
    local iface="$1";shift
    local portlist="$1";shift
    local policy="$1";shift

    local IFS=";"
    for i in $portlist; do
	local proto=${i%:*}
	local port=${i#*:}

	remove_rule "$inputchainfile" "$iface" "$proto" "$port"
	add_rule "$inputchainfile" "$iface" "$proto" "$port" "$policy"
    done
    return 0
}



### high level: backend : services

# Return list of enabled services separated by ";"
# Service is enabled if all its ports are opened!

read_active()
{
    local iface="$1"

    echo "$desktop_data" |
	while read filename port name; do
	    test_portset "$iface" "$port" && echo "$filename"
	done |
	tr '\n' ';'
}

# remove lst2 from lst1 only if lst1 contains lst2
remove_ports(){
  local lst1="$1"
  local lst2="$2"

  local p1
  local p2

  for p2 in $lst2; do
    local inc=
    for p1 in $lst1; do
      [ "$p1" != "$p2" ] || inc='1'
    done
    if [ -z "$inc" ]; then
      echo $lst1
      return
    fi
  done

  for p1 in $lst1; do
    local rem=""
    for p2 in $lst2; do
      [ "$p1" != "$p2" ] || rem=1
    done
    [ -n "$rem" ] || echo $p1
  done
}

# Return list of extra opened ports for a given iface
read_extra()
{
    local iface="$1"
    local opened_ports=$(list_opened_ports $inputchainfile $iface)

    echo "$desktop_data" |
      (
        while read filename port name; do
          port="$(echo $port | tr ';' '\n')"
          opened_ports="$(remove_ports "$opened_ports" "$port")"
        done
        echo "$opened_ports"
      )
}


# get port list for a specified protocol
filter_ports()
{
    local proto="$1"

    local port
    local p
    local s=
    read port
    port="$(echo $port | tr ',; ' '\n')"

    for p in $port; do
      if [ -z ${p##$proto:*} ]; then
        echo -n "$s${p#$proto:}"
        s=','
      fi
    done
}

# write settings for services: write_active eth0 ftp;ssh;telnet
# services must be separated by ";"
write_active()
{
    local iface="$1";shift
    local lst=";$1;";shift

    echo "$desktop_data" |
	while read filename port name; do
	    local policy="DROP"
	    [ -n "${lst##*;$filename;*}" ] || policy="ACCEPT"
	    write_portset "$iface" "$port" "$policy"
	done
}


# open extra ports: write_extra eth0 tcp 100,200,300
# ports can be separated by "," ";" "\n" or " "
write_extra()
{
    local iface="$1"
    local proto="$2"
    local lst="$(echo $3 | tr -s ",;\n "  " ")"

    for p in $lst; do
      write_portset "$iface" "$proto:$p" "ACCEPT"
    done
}

reset_basic()
{
    rm -f "$inputchainfile"
    printf -- '-P DROP\n' >>"$inputchainfile" #default input policy
    printf -- '-i lo -j ACCEPT\n' >>"$inputchainfile" #enable loopback
    printf -- '-f -j DROP\n' >>"$inputchainfile" #drop fragmented packets
    printf -- '-m state --state ESTABLISHED,RELATED -j ACCEPT\n' >> "$inputchainfile" #save established connections

    rm -f "$outputchainfile"
    printf -- '-P ACCEPT\n' >>"$outputchainfile" #default output policy
    printf -- '-f -j DROP\n' >>"$outputchainfile" #drop fragmented packets
    printf -- '-m state --state ESTABLISHED,RELATED -j ACCEPT\n' >>"$outputchainfile" #save established connections
}

reset_active()
{
    local iface="$1";shift

    echo "$desktop_data" |
	while read filename port name; do
	    write_portset "$iface" "$port" ACCEPT
	done
}

### high level: backend : forwarding

frdelim='[[:space:]]*=[[:space:]]*'
fwdelim=' = '

read_forwarding()
{
    shell_config_get /etc/net/sysctl.conf net.ipv4.ip_forward "$frdelim"
}

write_forwarding()
{
    shell_config_set /etc/net/sysctl.conf net.ipv4.ip_forward "$1" "$frdelim" "$fwdelim"
    echo "$1" >/proc/sys/net/ipv4/ip_forward
}

### high level: nat

read_nat()
{
    local GETOPT_ALLOW_UNKNOWN=1
    while read line; do
	case "$line" in
	     \#*|'') continue ;;
	esac

	local tmp=`getopt -o 's:,j:,o:' -- $line` || continue
	eval set -- $tmp

	local nat_from=
	local nat_jump=
	local nat_to=

	while [ $# -gt 0 ] ;do
	    case "$1" in
		-s) nat_from=$2; shift ;;
		-j) nat_jump=$2;shift ;;
		-o) nat_to=$2;shift ;;
	    esac
	    shift
	done
	if [ "$nat_jump" = "MASQUERADE" ]; then
	    write_bool_param nat_status "yes"
	    write_string_param nat_from "$nat_from"
	    write_string_param nat_to "$nat_to"
	    return
	fi
    done<"$postroutingchainfile"

    write_bool_param nat_status "no"
}

write_nat()
{
    sed -i "/$spb-j${sp}MASQUERADE$spe/ d" "$postroutingchainfile"
    if test_bool "$in_nat_status" &&
       [ -n "$in_nat_from" ] &&
       [ -n "$in_nat_to" ] ; then
	    printf -- '-s %s -o %s -j MASQUERADE\n' \
		"$in_nat_from" \
		"$in_nat_to" >>"$postroutingchainfile"
    fi
}

### high level: iptables reloading with rollback on probelems

tmpdir=
ipt_begin()
{
    write_debug "> begin\n"
    tmpdir=$(mktemp -td alterator-XXX)
    cp "$inputchainfile" "$tmpdir/INPUT"
    cp "$outputchainfile" "$tmpdir/OUTPUT"
    cp "$forwardchainfile" "$tmpdir/FORWARD"
}

ipt_commit()
{
    write_debug "> commit\n"
    if ! run_efw default restart; then
        cp -f "$tmpdir/INPUT" "$inputchainfile"
        cp -f "$tmpdir/OUTPUT" "$outputchainfile"
        cp -f "$tmpdir/FORWARD" "$forwardchainfile"
        run_efw default restart 3>/dev/null 2>/dev/null >/dev/null
    fi
    rm -rf "$tmpdir"
}

ipt_reset()
{
    [ -d "$tmpdir" ] || return
    write_debug "> reset\n"
    mv -f "$tmpdir/INPUT" "$inputchainfile"
    mv -f "$tmpdir/OUTPUT" "$outputchainfile"
    mv -f "$tmpdir/FORWARD" "$forwardchainfile"
    run_efw default restart
    rm -rf "$tmpdir"
}


#initial settings
shell_config_set "$ifacedir/default/fw/options" IPTABLES_HUMAN_SYNTAX no

old_language=
on_message()
{
	if [ "$old_language" != "$in_language" ]; then
	  desktop_data="$(list_desktop)"
	  old_language="$in_language"
	fi

	case "$in_action" in
		list)
		    case "$in__objects" in
			avail_network)
			    list_network|
				write_enum
			    ;;
			avail_iface)
			    list_iface|
				write_enum
			    ;;
			avail_service)
#			    local l1="`_ "TCP ports:"`"
#			    local l2="`_ "UDP ports:"`"
			    echo "$desktop_data" |
				while read filename port name; do
#				    local tp="$(echo $port | filter_ports tcp)"
#				    local up="$(echo $port | filter_ports udp)"
#				    tp="${tp:+, $l1 $tp}"
#				    up="${up:+, $l2 $up}"
#				    write_enum_item "$filename" "$name$tp$up"
				    write_enum_item "$filename" "$name"
				done
			    ;;
		    esac
		    ;;
		read)
		    read_nat

		    [ -n "$in_name" ] || in_name="$(list_iface|head -n1)"

		    write_bool_param 'status' "$(read_iface_option "$ifacedir/default" CONFIG_FW)"
		    write_bool_param 'forwarding' "$(read_forwarding)"

		    local active="$(read_active "$in_name")"
		    write_string_param service "${active%;}"

		    write_string_param tcp_ports "$(read_extra "$in_name" | filter_ports "tcp")"
		    write_string_param udp_ports "$(read_extra "$in_name" | filter_ports "udp")"

		    write_string_param name "$in_name"
		    write_string_param commit_stage "$commit_stage"
		    ;;
		write)
#		    if [ -n "$in_reset" ]; then
#			ipt_begin
#			    write_iface_option "$ifacedir/default" CONFIG_FW yes
#			    reset_basic
#			    list_iface|
#				while read iface ; do
#				    reset_active "$iface"
#				done
#			ipt_end
		    if [ "$in_commit_stage" != 1 ]; then
		      if [ -n "$in_yes" ]; then
		        ipt_commit
		      else
		        ipt_reset
		      fi
		      commit_stage=1
		    elif [ -n "$in_commit" ];then
			ipt_begin
			if test_bool "$in_status"; then
			    write_nat
			    reset_basic
			    write_iface_option "$ifacedir/default" CONFIG_FW yes
			    run_efw default start 3>/dev/null 2>/dev/null >/dev/null
			    if test_bool "$in_forwarding"; then
			        write_forwarding 1
			    else
			        write_forwarding 0
			    fi
			    if [ -n "$in_name" ]; then 
			      write_active "$in_name" "$in_service"
			      write_extra "$in_name" "tcp" "$in_tcp_ports"
			      write_extra "$in_name" "udp" "$in_udp_ports"
			    fi
			else
			    run_efw default stop 3>/dev/null 2>/dev/null >/dev/null
			    write_iface_option "$ifacedir/default" CONFIG_FW no
			fi

			if [ -n "$in_commit_stage" ] && is_dangerous "$in_name"; then
			  commit_stage=2
			else
			  ipt_commit
			fi

		    fi
		    ;;
	esac
}

message_loop
