#!/bin/sh
#***************************************************************************
#
# $Source: /sc/CvsTree/sc/gen/src/prg/scripts/firewall-lib.sh,v $
#
# Copyright (c) 2015
# BRUKER BIOSPIN GMBH
# D-76287 Rheinstetten, Germany
#
# All Rights Reserved
#
# $Id: firewall-lib.sh,v 1.7 2017/10/25 08:04:37 jgo Exp $
#
#***************************************************************************
#			Description:
#
# Library to configure Linux firewall.
#***************************************************************************
# POSIX compliant (test with POSIXLY_CORRECT=1 or --posix or set -o posix)
#***************************************************************************
# Useful DBus commands:
#
# firewall-cmd --get-zone-of-interface=eth1
#	format: [RED]no zone[NORM]
# firewall-cmd --zone=trusted --query-interface=eth1
#	Query whether interface interface is bound to zone zone.
# firewall-cmd --change-interface=eth1 --zone=trusted
#	Change zone the interface interface is bound to to zone zone.
#	New interfaces are added automatically.
# firewall-cmd --get-default-zone
#	Print default zone for connections and interfaces.
# firewall-cmd --get-active-zones
#	Print currently active zones with interfaces and sources.
#	format: public
#		  interfaces: ens160 virbr0
# firewall-cmd --remove-interface=eth1
#	Remove binding of interface interface from zone zone.
# firewall-cmd [--zone=zone] --list-ports
#	5900-5953/tcp 5901/tcp 5904/tcp 5905/tcp 5902/tcp 5903/tcp
# firewall-cmd [--zone=zone] --query-port 8080/tcp
#	format: yes [exit 0] | no [exit 1]
# firewall-cmd [--zone=zone] --add-port 8080/tcp
# firewall-cmd [--zone=zone] --remove-port 8080/tcp
#***************************************************************************


. $(dirname $0)/service-lib.sh


MainUsage ()
{
    SetSysconfig
    FwUsageGetAll
    ProgUsage
}


FwUsageGetAll ()
{
    FwUsageGetSynopsis
    FwUsageGetDescription
    FwUsageGetOptions
    FwUsageGetActions
}


FwUsageGetSynopsis ()
{
    echo "
SYNOPSIS
    $0 [OPTIONS] [ACTIONS]"
}


FwUsageGetDescription ()
{
    echo "
DESCRIPTION
    Configure firewall rules for Spectrometer Operating System.
    (Set the ethernet interface used for the spectrometer to 'trusted'.)"
}


FwUsageGetOptions ()
{
    echo "
OPTIONS
  -b  Suppress creation of backup files.
        (Backup files get the extension '-<date>T<time>.backup'
         where <date> and <time> are in the format YYYYMMDD and HHMMSS.)
  -f  Force configuration even if network interface does not exist.
  -m METHOD (one of dbus, ipt)
        dbus: use dbus via firewall-cmd
        ipt: use iptables command
	Default: use dbus (firewall-cmd) if it ex.; otherwise use iptables
  -t  Run as unit test only.
  -T TITLE
        Program title (used for print messages).
  -U UNINSTALL_DIR
        Program uninstall dir (used to save/find restoration scripts).
  -h  Print this help message."
}


FwUsageGetActions ()
{
    echo "
ACTIONS
    install  Install firewall rules for Spectrometer Operating System (default).
    remove   Remove firewall rules for Spectrometer Operating System.
    show     Show commands to configure firewall but do not change anything."
}


ProgUsage ()
{
    :
}


FwInit ()
{
    SvcLibSetDefaults
    ScanArguments "$@"
}


SetDefaults ()
{
    set -eu
    OptBackups=1
    Method=
    Indent=""
    IptablesReloadNeeded=0
    ProgTitle=$0
}


ScanArguments ()
{
    OPTIND=0
    while getopts "bfm:tT:U:h" szOption
    do
	case "$szOption" in
	    b) OptBackups=0;;
	    f) OptForce=1;;
	    m) Method=$OPTARG;;
	    t) OptTest=1;;
	    T) ProgTitle=$OPTARG;;
	    U) SzUnInstDir=$OPTARG;;
	    h) Usage;;
	    *) Fatal "Invalid option ($szOption)"
	esac
    done
    shift $(expr $OPTIND - 1)

    if test $OptTest = 1
    then
	export PATH=$OLDPATH
    fi

    AssertIdIsRoot
    SetSysconfig
    FwCheckMethod
    FwCheckOptions "$@"
    Output "$0: $(GetActionMsg) firewall rules for $ProgTitle$(GetDryRunMsg)"
    FwPrintState "Previous firewall state: "
}


FwCheckMethod ()
{
    case $Method in
	"")
	    if FwHasFirewallDbusCmd
	    then
		Method=dbus
	    else
		Method=ipt
	    fi
	    ;;
	dbus)
	    FwHasFirewallDbusCmd || Fatal "command firewall-cmd not found"
	    ;;
	ipt)
	    :
	    ;;
	FindNetIf)
	    # Used for unit test.
	    FwApplyOptIfc_FindNetIf () { :; }
	    FwPrintState_FindNetIf () { :; }
	    ;;
	*)
	    Fatal "Invalid value for option -m"
    esac
    MethodSave=$Method
}


FwCheckOptions ()
{
    local action
    for action in "$@"
    do
	case "$action" in
	    REMOVE|Remove|remove)
		OptRemove=1
		;;
	    SHOW|Show|show)
		OptDryRun=1
		;;
	    INSTALL|Install|install)
		:
		;;
	    *)
		Fatal "Invalid action ($action)"
	esac
    done

    if test $OptDryRun = 1
    then
	RunCmd=:
	OptBackups=0
    else
	RunCmd=
    fi
}


RunActions_FindNetIf ()
{
    ExitHandler 0
}


FwTerminate ()
{
    FwPrintState "New firewall state: "
    ExitHandler $NExitValue
}


#---------------------------------------------------------------------------


FwApplyOptIfc ()
{
    local ifcName=$1 && shift
    FwApplyOptIfc_$Method $ifcName
    if test $MethodSave != $Method; then FwApplyOptIfc_$Method $ifcName; fi
}


FwApplyOptDstPort ()
{
    local dstPortProtZone=$1 && shift
    FwApplyOptDstPort_$Method $dstPortProtZone
    if test $MethodSave != $Method; then FwApplyOptDstPort_$Method $dstPortProtZone; fi
}


#---------------------------------------------------------------------------


FwApplyOptIfc_dbus ()
{
    local ifcName=$1 && shift

    local queryCmd="--zone=trusted --query-interface=$ifcName"
    local applyCmd="$(FwDbusSetZoneOfIfc $ifcName trusted)"
    local restoreCmd=
    if test $OptRemove = 0
    then
	restoreCmd=$(FwDbusFindZoneOfIfc $ifcName)
    fi

    FwApplyCmd_$Method "$applyCmd" "$restoreCmd" "$queryCmd"
}


FwApplyOptDstPort_dbus ()
{
    local dstPortProtZone=$1 && shift

    local port=$(FwGetPortFromDstPortProt $dstPortProtZone)
    local prot=$(FwGetProtFromDstPortProt $dstPortProtZone)
    local zone=$(FwGetZoneFromDstPortProt $dstPortProtZone)

    local queryCmd="--query-port=$port/$prot --zone=$zone"
    local applyCmd="--add-port=$port/$prot --zone=$zone"
    local restoreCmd="--remove-port=$port/$prot --zone=$zone"

    FwApplyCmd_$Method "$applyCmd" "$restoreCmd" "$queryCmd"
}


FwApplyCmd_dbus ()
{
    local applyCmd=$1 && shift
    local restoreCmd=$1 && shift
    local queryCmd=$1 && shift
    local backupFile=restore-dbus.sh

    if ! RuleIsUptodate $queryCmd
    then
	CreateIptablesBackup

	if test $OptRemove = 0
	then
	    FwBackupFileWrite_dbus $backupFile "$restoreCmd"
	    FwDbusExec $applyCmd
	else
	    FwBackupFileExecute $backupFile
	fi
    fi
}


FwBackupFileWrite_dbus ()
{
    local backupFile=$1 && shift
    local backupText=$1 && shift
    FwBackupFileWrite $backupFile "firewall-cmd --permanent $backupText"
    FwBackupFileWrite $backupFile "firewall-cmd             $backupText"
}


FwDbusExec ()
{
    Verbose firewall-cmd             "$@"
    Verbose firewall-cmd --permanent "$@"
}


FwDbusFindZoneOfIfc ()
{
    local ifcName=$1 && shift
    local curZone
    local restoreCmd=

    if ! curZone=$(firewall-cmd --get-zone-of-interface=$ifcName)
    then
	curZone="unknown zone"
    fi

    if test "$curZone" = "unknown zone" -o "$curZone" = "no zone"
    then
	if ip link show "$ifcName" 1>/dev/null
	then
	    Warning "$ifcName exists but is not in any zone"
	else
	    local winFunc=Warning
	    if test $OptForce = 0
	    then
		winFunc=Fatal
	    fi
	    $winFunc "$ifcName does not exist and is not in any zone"
	fi
	restoreCmd=--remove-interface=$ifcName
    else
	restoreCmd=$(FwDbusSetZoneOfIfc $ifcName "$curZone")
    fi

    echo "$restoreCmd"
}


FwDbusSetZoneOfIfc ()
{
    local ifcName=$1 && shift
    local curZone=$1 && shift
    echo --change-interface=$ifcName --zone="$curZone"
}


#---------------------------------------------------------------------------


FwApplyOptIfc_ipt ()
{
    local ifcName=$1 && shift
    FwApplyCmd_ipt "$(FwGetConfigRuleIfc $ifcName)" -i $ifcName
}


FwApplyOptDstPort_ipt ()
{
    local dstPortProtZone=$1 && shift
    FwApplyCmd_ipt "$(FwGetConfigRuleDstPort $dstPortProtZone)" -p $dstPortProtZone
}


FwApplyCmd_ipt ()
{
    local configRuleToApply=$1 && shift
    local iptRuleSpec=$1 && shift
    local iptRuleArg=$1 && shift

    FwHandleConfigFile $Sysconfig/system-config-securitylevel "$configRuleToApply" # CentOS 5
    FwHandleConfigFile $Sysconfig/system-config-firewall "$configRuleToApply" # CentOS 6
    FwHandleIptablesFile iptables $iptRuleSpec "$iptRuleArg"
    FwHandleIptablesFile ip6tables $iptRuleSpec "$iptRuleArg"
}


FwHandleConfigFile ()
{
    local configFile=$1 && shift
    local ruleToApply=$1 && shift
    local rulePredecessor="--enabled"

    if FwInitConfigFile $configFile "$ruleToApply"
    then
	FwModifyRuleInConfigFile $configFile "$ruleToApply" "$rulePredecessor"
	TermRuleFile
    fi
}


FwModifyRuleInConfigFile ()
{
    local configFile=$1 && shift
    local ruleToApply=$1 && shift
    local rulePredecessor=${1:-}
    local out

    if test $OptRemove = 0
    then
	Output "change: add line to $configFile: $ruleToApply"
	out=$(FwInsertRuleInFileText "$configFile" "$ruleToApply" "$rulePredecessor")
    else
	Output "change: delete line from $configFile: $ruleToApply"
	out=$(FwRemoveRuleInFileText "$configFile" "$ruleToApply")
    fi

    if test $OptDryRun = 0
    then
	CreateFileBackup $configFile
	WriteFile "$out" $configFile
    fi
}


FwInsertRuleInFileText ()
{
    local file=$1 && shift
    local ruleToApply=$1 && shift
    local rulePredecessor=$1 && shift

    local out0
    local out1
    out0=$(cat $file)

    if test true
    then
	out1=$(awk '{
		if ($0=="'"$rulePredecessor"'")
		{
			print $0;
			print "'"$ruleToApply"'";
		}
		else print;
	}' \
	    $file)
    fi

    if test "$out0" = "$out1"
    then
	out1=$(awk '{
		if (!done && !index($0,"#") && !index($0,"*") && !index($0,":"))
		{
			done=1;
			print "'"$ruleToApply"'"; 
		};
		print;
	}' \
	    $file)
    fi

    if test "$out0" = "$out1"
    then
	out1=$(awk '{if (FNR==1) print "'"$ruleToApply"'"; print; }' $file)
    fi

    echo "$out1"
}


FwRemoveRuleInFileText ()
{
    local file=$1 && shift
    local ruleToApply=$1 && shift

    grep -v -e "$ruleToApply" $file
}


FwHandleIptablesFile ()
{
    local iptCmd=$1 && shift
    local iptRuleSpec=$1 && shift
    local iptRuleArg=$1 && shift

    IptablesLoad $iptCmd # Assert all saved rules are loaded
    case $iptRuleSpec in
	-i) IptablesApplyRuleIfc $iptCmd $iptRuleArg;;
	-p) IptablesApplyRuleDstPort $iptCmd $iptRuleArg;;
    esac
    IptablesSave $iptCmd # Save modified kernel rules
    TermRuleFile
}


IptablesApplyRuleIfc ()
{
    local iptCmd=$1 && shift
    local ifcName=$1 && shift

    local chain
    for chain in INPUT FORWARD
    do
	local ruleToApply="-A $chain -i $ifcName -j ACCEPT"
	IptablesApplyRule $iptCmd "$ruleToApply" "-A $chain" "-i $ifcName" "-j ACCEPT"
    done
}


IptablesApplyRuleDstPort ()
{
    local iptCmd=$1 && shift
    local dstPortProtZone=$1 && shift

    local port=$(FwGetPortFromDstPortProt $dstPortProtZone)
    local prot=$(FwGetProtFromDstPortProt $dstPortProtZone)
    local ruleToApply="-I INPUT -m $prot -p $prot --dport $port -j ACCEPT"
    # iptables-save says "-A INPUT" even for command "-I INPUT"
    IptablesApplyRule $iptCmd "$ruleToApply" "INPUT" "-m $prot" "-p $prot" "--dport $port" "-j ACCEPT"
}


GetServiceName ()
{
    local portProt=$1 && shift
    awk '$2 == "$portProt" {print $1}' /etc/services
}


IptablesApplyRule ()
{
    local iptCmd=$1 && shift
    local ruleToApply=$1 && shift
    local iptContents=$($iptCmd-save)

    if ! RuleIsUptodate "$iptContents" "$@" && CheckRuleFile $Sysconfig/$iptCmd $iptCmd-restore
    then
	IptablesReloadNeeded=1
	CreateIptableBackup $iptCmd
	if test $OptRemove = 1
	then
	    # iptables rejects the -D command if no exact match (incl --state NEW on CentOS6)
	    ruleToApply=$(MultipleGrep "$iptContents" "$@" | sed -e 's|^-[AI]|-D|')
	fi
	Verbose $iptCmd $ruleToApply
    fi
}


#---------------------------------------------------------------------------


FwInitConfigFile ()
{
    local configFile=$1 && shift
    local ruleToApply=$1 && shift
    local ruleCmds="system-config-securitylevel system-config-securitylevel-tui system-config-firewall system-config-firewall-tui lokkit"

    if test ! -e $configFile || \
	RuleIsUptodate "$(cat $configFile)" "$ruleToApply" || \
	! CheckRuleFile $configFile "$ruleCmds"
    then
	return 1
    fi

    CreateFileBackup $configFile
    CreateIptablesBackup
}


FwGetConfigRuleIfc ()
{
    local ifcName=$1 && shift
    echo "--trust=$ifcName"
}


FwGetConfigRuleDstPort ()
{
    local dstPortProtZone=$1 && shift
    local port=$(FwGetPortFromDstPortProt $dstPortProtZone)
    local prot=$(FwGetProtFromDstPortProt $dstPortProtZone)
    echo "--port=$port:$prot"
}


CheckRuleFile ()
{
    local ruleFile=$1 && shift
    local ruleCmds=$1 && shift

    if test ! -e $ruleFile
    then
	false
	return
    fi

    local cmd lsRuleCmdEx=
    for cmd in $ruleCmds
    do
	if which $cmd>/dev/null 2>/dev/null
	then
	    lsRuleCmdEx="$lsRuleCmdEx $cmd"
	fi
    done

    if ! HasFileVar $ruleFile print
    then
	Output "Note: Update rules in $ruleFile"
	Output "Note: affected by the command$(Plural $lsRuleCmdEx) $lsRuleCmdEx"
	SetFileVar $ruleFile print
    fi
}


TermRuleFile ()
{
    Indent=""
}


RuleIsUptodate ()
{
    if test $OptDryRun = 0
    then
	if CheckRuleIsThere_$Method "$@"
	then
	    if test $OptRemove = 0
	    then
		RuleIsUptodate_Output_$Method "nothing to add" "$@"
		return
	    fi
	else
	    if test $OptRemove = 1
	    then
		RuleIsUptodate_Output_$Method "nothing to remove" "$@"
		return
	    fi
	fi
    fi

    return 1
}


RuleIsUptodate_Output_dbus ()
{
    local prefix=$1 && shift
    Output "$prefix ($@)"
}


RuleIsUptodate_Output_ipt ()
{
    local prefix=$1 && shift
    local out=$1 && shift
    Output "$prefix ($@)"
}


CheckRuleIsThere_dbus ()
{
    firewall-cmd "$@" > /dev/null
}


CheckRuleIsThere_ipt ()
{
    test "$(MultipleGrep "$@")"
}


MultipleGrep ()
{
    local out="$1" && shift
    local dd_list="$@"

    if test $OptDryRun = 1
    then
	out="-A $@"
    else
	while test $# -gt 0
	do
	    local pattern=$1 && shift
	    out=$(echo "$out" | grep -e "$pattern")
	done
    fi

    echo "$out"
}


CreateIptablesBackup ()
{
    CreateIptableBackup iptables
    CreateIptableBackup ip6tables
}


CreateIptableBackup ()
{
    ipt=$1 && shift
    CreateFileBackup $Sysconfig/$ipt
}


FwPrintState ()
{
    return
    $ECHO -n "$@"
    FwPrintState_$Method
}


FwPrintState_dbus ()
{
    Verbose firewall-cmd --list-all
}


FwPrintState_ipt ()
{
    Verbose iptables-save
    Verbose ip6tables-save
}


#---------------------------------------------------------------------------


CreateFileBackup ()
{
    local file=$1 && shift

    if test $OptBackups = 1 -a -e $file && ! HasBackupDone $file
    then
	Verbose cp -af $file $file-$(date +%Y-%m-%dT%H.%M.%S).backup
	SetBackupDone $file
    fi
}


HasBackupDone ()
{
    HasFileVar "$@" backup
}


SetBackupDone ()
{
    SetFileVar "$@" backup
}


HasFileVar ()
{
    local varName varVal
    varName=$(GetFileVarName "$@")
    shellOpts=$-
    set +u
    varVal=$(eval echo \$$varName)
    set $shellOpts
    test "$varVal" = 1
}


SetFileVar ()
{
    local varName
    varName=$(GetFileVarName "$@")
    eval $varName=1
}


GetFileVarName ()
{
    local varName
    varName=$(echo "$@"|tr ' /.-' 'ZZZZ')
    echo "$varName"
}


WriteFile ()
{
    local out=$1 && shift
    local file=$1 && shift

    echo "$out" > $file || ErrorCmd "echo ... > $file"
}


AppendFile ()
{
    local out=$1 && shift
    local file=$1 && shift

    echo "$out" >> $file || ErrorCmd "echo ... > $file"
}


IptablesLoad ()
{
    local iptCmd=$1 && shift
    IptablesCommand $iptCmd load
}


IptablesSave ()
{
    local iptCmd=$1 && shift
    if test $IptablesReloadNeeded = 0
    then
	Output "saving of $iptCmd not necessary"
	return
    fi

    IptablesCommand $iptCmd save
    IptablesCommand $iptCmd condrestart
}


# @note:	'service $iptCmd reload' launched by installer GUI alerts with:
#	SELinux is preventing the ip6tables-resto from using potentially mislabeled files
#	Source Context    user_u:system_r:iptables_t
#	Target Context    user_u:object_r:tmp_t
#
# @note:	'service $iptCmd in /etc/init.d has no option for simple restore(load)

IptablesCommand ()
{
    local iptCmd=$1 && shift
    case $1 in
	load)
	    Verbose -i $Sysconfig/$iptCmd $iptCmd-restore
	    ;;
	save)
	    Verbose -o $Sysconfig/$iptCmd $iptCmd-save
	    ;;
	status)
	    IptablesService $iptCmd "$@"
	    ;;
	condrestart)
	    IptablesService $iptCmd "$@"
    esac
}


IptablesService ()
{
    local iptCmd=$1 && shift
    if type service >/dev/null 2>/dev/null
    then
	Verbose -e service $iptCmd "$@"
    else
	Verbose -e sh $EtcInitD/$iptCmd "$@"
    fi
}


FwHasFirewallDbusCmd ()
{
    if which firewall-cmd >/dev/null 2>/dev/null
    then
	echo "Note: Using firewall DBus commands"
    else
	return 1
    fi
}


SetSysconfig ()
{
    if test $OptTest = 1
    then
	Sysconfig=$UT_DIR/sysconfig
	SysFsDir=$UT_DIR/sys
    else
	Sysconfig=/etc/sysconfig
	SysFsDir=/sys
    fi
}


FwBackupFileWrite ()
{
    local backupFile=$(FwGetBackupFilePath $1) && shift
    local backupText=$1 && shift
    local backupDir=$(FwGetBackupDir)

    test -d $backupDir || Verbose mkdir -p $backupDir
    Verbose AppendFile "$backupText" "$backupFile"
}


FwBackupFileExecute ()
{
    local backupFile=$(FwGetBackupFilePath $1) && shift

    if test -e $backupFile
    then
	RunCmd sh $backupFile
	RunCmd rm -f $backupFile
    else
	echo "nothing to restore: no backup file $backupFile"
    fi
}


FwGetBackupFilePath ()
{
    local backupFile=$1 && shift
    echo $(FwGetBackupDir)/$backupFile
}


FwGetBackupDir ()
{
    if test $OptTest = 1
    then
	echo $UT_DIR/uninstall
    else
	echo $SzUnInstDir
    fi
}


FwGetPortFromDstPortProt ()
{
    local dstPortProtZone=$1 && shift
    echo $dstPortProtZone | awk -F / '{print $1}'
}


FwGetProtFromDstPortProt ()
{
    local dstPortProtZone=$1 && shift
    echo $dstPortProtZone | awk -F / '{print $2}'
}


FwGetZoneFromDstPortProt ()
{
    local dstPortProtZone=$1 && shift
    echo $dstPortProtZone | awk -F / '{print $3}'
}


Output ()
{
    echo "${Indent}$@"
}


Warning ()
{
    echo >&2 "Warning: $@"
}


ErrorCmd ()
{
    echo >&2 "Command failed: $@"
    NExitValue=1
}


Fatal ()
{
    echo >&2 "Fatal error: $@"
    ExitHandler 3
}
