#!/bin/bash -e
# Note: must be bash; uses bash-specific tricks
#
# ******************************************************************************************************************
# Copyright By Tunnelblick. Redistributed with Bitmask under the GPL.
# This Tunnelblick script does everything! It handles TUN and TAP interfaces, 
# pushed configurations and DHCP leases. :)
# 
# This is the "Down" version of the script, executed after the connection is 
# closed.
#
# Created by: Nick Williams (using original code and parts of old Tblk scripts)
# 
# ******************************************************************************************************************

# @param String message - The message to log
logMessage()
{
	echo "${@}"
}

# @param String message - The message to log
logDebugMessage()
{
	echo "${@}" > /dev/null
}

trim()
{
echo ${@}
}

# @param String list - list of network service names, output from disable_ipv6()
restore_ipv6() {

    # Undoes the actions performed by the disable_ipv6() routine in client.up.tunnelblick.sh by restoring the IPv6
    # 'automatic' setting for each network service for which that routine disabled IPv6.
    #
    # $1 must contain the output from disable_ipv6() -- the list of network services.
    #
    # This routine outputs log messages describing its activities.

    if [ "$1" = "" ] ; then
        exit
    fi

    printf %s "$1
" | \
    while IFS= read -r ripv6_service ; do
        networksetup -setv6automatic "$ripv6_service"
        logMessage "Re-enabled IPv6 (automatic) for '$ripv6_service'"
    done
}

##########################################################################################
flushDNSCache()
{
    if ${ARG_FLUSH_DNS_CACHE} ; then
        set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
        readonly OSVER="$(sw_vers | grep 'ProductVersion:' | grep -o '10\.[0-9]*')"
        set -e # We instruct bash that it CAN again fail on errors
	    if [ "${OSVER}" = "10.4" ] ; then

			if [ -f /usr/sbin/lookupd ] ; then
				set +e # we will catch errors from lookupd
				/usr/sbin/lookupd -flushcache
				if [ $? != 0 ] ; then
					logMessage "WARNING: Unable to flush the DNS cache via lookupd"
				else
					logMessage "Flushed the DNS cache via lookupd"
				fi
				set -e # bash should again fail on errors
			else
				logMessage "WARNING: /usr/sbin/lookupd not present. Not flushing the DNS cache"
			fi

		else

			if [ -f /usr/bin/dscacheutil ] ; then
				set +e # we will catch errors from dscacheutil
				/usr/bin/dscacheutil -flushcache
				if [ $? != 0 ] ; then
					logMessage "WARNING: Unable to flush the DNS cache via dscacheutil"
				else
					logMessage "Flushed the DNS cache via dscacheutil"
				fi
				set -e # bash should again fail on errors
			else
				logMessage "WARNING: /usr/bin/dscacheutil not present. Not flushing the DNS cache via dscacheutil"
			fi
		
			if [ -f /usr/sbin/discoveryutil ] ; then
				set +e # we will catch errors from discoveryutil
				/usr/sbin/discoveryutil udnsflushcaches
				if [ $? != 0 ] ; then
					logMessage "WARNING: Unable to flush the DNS cache via discoveryutil udnsflushcaches"
				else
					logMessage "Flushed the DNS cache via discoveryutil udnsflushcaches"
				fi
				/usr/sbin/discoveryutil mdnsflushcache
				if [ $? != 0 ] ; then
					logMessage "WARNING: Unable to flush the DNS cache via discoveryutil mdnsflushcache"
				else
					logMessage "Flushed the DNS cache via discoveryutil mdnsflushcache"
				fi
				set -e # bash should again fail on errors
			else
				logMessage "/usr/sbin/discoveryutil not present. Not flushing the DNS cache via discoveryutil"
			fi
			
			set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
			hands_off_ps="$( ps -ax | grep HandsOffDaemon | grep -v grep.HandsOffDaemon )"
			set -e # We instruct bash that it CAN again fail on errors
			if [ "${hands_off_ps}" = "" ] ; then
				if [ -f /usr/bin/killall ] ; then
					set +e # ignore errors if mDNSResponder isn't currently running
					/usr/bin/killall -HUP mDNSResponder
					if [ $? != 0 ] ; then
						logMessage "mDNSResponder not running. Not notifying it that the DNS cache was flushed"
					else
						logMessage "Notified mDNSResponder that the DNS cache was flushed"
					fi
					set -e # bash should again fail on errors
				else
					logMessage "WARNING: /usr/bin/killall not present. Not notifying mDNSResponder that the DNS cache was flushed"
				fi
			else
				logMessage "WARNING: Hands Off is running.  Not notifying mDNSResponder that the DNS cache was flushed"
			fi
		
		fi
    fi
}

##########################################################################################
resetPrimaryInterface()
{
	set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
	WIFI_INTERFACE="$(networksetup -listallhardwareports | awk '$3=="Wi-Fi" {getline; print $2}')"
	if [ "${WIFI_INTERFACE}" == "" ] ; then
		WIFI_INTERFACE="$(networksetup -listallhardwareports | awk '$3=="AirPort" {getline; print $2}')"
    fi
	PINTERFACE="$( scutil <<-EOF |
        open
        show State:/Network/Global/IPv4
        quit
EOF
    grep PrimaryInterface | sed -e 's/.*PrimaryInterface : //'
    )"
    set -e # resume abort on error

    if [ "${PINTERFACE}" != "" ] ; then
	    if [ "${PINTERFACE}" == "${WIFI_INTERFACE}" -a "${OSVER}" != "10.4" -a -f /usr/sbin/networksetup ] ; then
		    if [ "${OSVER}" == "10.5" ] ; then
			    logMessage "Resetting primary interface '${PINTERFACE}' via networksetup -setairportpower off/on..."
				/usr/sbin/networksetup -setairportpower off
				sleep 2
				/usr/sbin/networksetup -setairportpower on
			else
				logMessage "Resetting primary interface '${PINTERFACE}' via networksetup -setairportpower ${PINTERFACE} off/on..."
				/usr/sbin/networksetup -setairportpower "${PINTERFACE}" off
				sleep 2
				/usr/sbin/networksetup -setairportpower "${PINTERFACE}" on
			fi
		else
		    if [ -f /sbin/ifconfig ] ; then
			    logMessage "Resetting primary interface '${PINTERFACE}' via ifconfig ${PINTERFACE} down/up..."
                /sbin/ifconfig "${PINTERFACE}" down
                sleep 2
			    /sbin/ifconfig "${PINTERFACE}" up
			else
				logMessage "WARNING: Not resetting primary interface because /sbin/ifconfig does not exist."
			fi
		fi
    else
        logMessage "WARNING: Not resetting primary interface because it cannot be found."
    fi
}

##########################################################################################
trap "" TSTP
trap "" HUP
trap "" INT
export PATH="/bin:/sbin:/usr/sbin:/usr/bin"

readonly OUR_NAME=$(basename "${0}")

logMessage "**********************************************"
logMessage "Start of output from ${OUR_NAME}"

# Remove the flag file that indicates we need to run the down script

if [ -e   "/tmp/bitmask-downscript-needs-to-be-run.txt" ] ; then
    rm -f "/tmp/bitmask-downscript-needs-to-be-run.txt"
fi

# Test for the "-r" Bitmask option (Reset primary interface after disconnecting) because we _always_ need its value.
# Usually we get the value for that option (and the other options) from State:/Network/OpenVPN,
# but that key may not exist (because, for example, there were no DNS changes).
# So we get the value from the Bitmask options passed to this script by OpenVPN.
#
# We do the same thing for the -f Bitmask option (Flush DNS cache after connecting or disconnecting)
ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="false"
ARG_FLUSH_DNS_CACHE="false"
while [ {$#} ] ; do
    if [ "${1:0:1}" != "-" ] ; then				# Bitmask arguments start with "-" and come first
        break                                   # so if this one doesn't start with "-" we are done processing Bitmask arguments
    fi
    if [ "$1" = "-r" ] ; then
        ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="true"
    else
	    if [ "$1" = "-f" ] ; then
            ARG_FLUSH_DNS_CACHE="true"
        fi
    fi
    shift                                       # Shift arguments to examine the next option (if there is one)
done

# Quick check - is the configuration there?
if ! scutil -w State:/Network/OpenVPN &>/dev/null -t 1 ; then
	# Configuration isn't there
    logMessage "WARNING: Not restoring DNS settings because no saved Bitmask DNS information was found."
	
	flushDNSCache
    
	if ${ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT} ; then
        resetPrimaryInterface
    fi
    logMessage "End of output from ${OUR_NAME}"
    logMessage "**********************************************"
	exit 0
fi

# Get info saved by the up script
TUNNELBLICK_CONFIG="$( scutil <<-EOF
	open
	show State:/Network/OpenVPN
	quit
EOF
)"

ARG_MONITOR_NETWORK_CONFIGURATION="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*MonitorNetwork :' | sed -e 's/^.*: //g')"
LEASEWATCHER_PLIST_PATH="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*LeaseWatcherPlistPath :' | sed -e 's/^.*: //g')"
REMOVE_LEASEWATCHER_PLIST="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RemoveLeaseWatcherPlist :' | sed -e 's/^.*: //g')"
PSID="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*Service :' | sed -e 's/^.*: //g')"
# Don't need: SCRIPT_LOG_FILE="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*ScriptLogFile :' | sed -e 's/^.*: //g')"
# Don't need: ARG_RESTORE_ON_DNS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnDNSReset :' | sed -e 's/^.*: //g')"
# Don't need: ARG_RESTORE_ON_WINS_RESET="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreOnWINSReset :' | sed -e 's/^.*: //g')"
# Don't need: PROCESS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*PID :' | sed -e 's/^.*: //g')"
# Don't need: ARG_IGNORE_OPTION_FLAGS="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IgnoreOptionFlags :' | sed -e 's/^.*: //g')"
ARG_TAP="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*IsTapInterface :' | sed -e 's/^.*: //g')"
ARG_FLUSH_DNS_CACHE="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*FlushDNSCache :' | sed -e 's/^.*: //g')"
ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*ResetPrimaryInterface :' | sed -e 's/^.*: //g')"
bRouteGatewayIsDhcp="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RouteGatewayIsDhcp :' | sed -e 's/^.*: //g')"
bTapDeviceHasBeenSetNone="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*TapDeviceHasBeenSetNone :' | sed -e 's/^.*: //g')"
bAlsoUsingSetupKeys="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*bAlsoUsingSetupKeys :' | sed -e 's/^.*: //g')"
sTunnelDevice="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*TunnelDevice :' | sed -e 's/^.*: //g')"

# Note: '\n' was translated into '\t', so we translate it back (it was done because grep and sed only work with single lines)
sRestoreIpv6Services="$(echo "${TUNNELBLICK_CONFIG}" | grep -i '^[[:space:]]*RestoreIpv6Services :' | sed -e 's/^.*: //g' | tr '\t' '\n')"

# Remove leasewatcher
if ${ARG_MONITOR_NETWORK_CONFIGURATION} ; then
	launchctl unload "${LEASEWATCHER_PLIST_PATH}"
    if ${REMOVE_LEASEWATCHER_PLIST} ; then
        rm -f "${LEASEWATCHER_PLIST_PATH}"
    fi
	logMessage "Cancelled monitoring of system configuration changes"
fi

if ${ARG_TAP} ; then
	if [ "$bRouteGatewayIsDhcp" == "true" ]; then
        if [ "$bTapDeviceHasBeenSetNone" == "false" ]; then
            if [ -z "$dev" ]; then
                # If $dev is not defined, then use TunnelDevice, which was set from $dev by client.up.tunnelblick.sh
                # ($def is not defined when this script is called from MenuController to clean up when exiting Bitmask)
                if [ -n "${sTunnelDevice}" ]; then
                    logMessage "WARNING: \$dev not defined; using TunnelDevice: ${sTunnelDevice}"
                    set +e
                    ipconfig set "${sTunnelDevice}" NONE 2>/dev/null
                    set -e
                    logMessage "Released the DHCP lease via ipconfig set ${sTunnelDevice} NONE."
                else
                    logMessage "WARNING: Cannot configure TAP interface to NONE without \$dev or State:/Network/OpenVPN/TunnelDevice being defined. Device may not have disconnected properly."
                fi
            else
                set +e
                ipconfig set "$dev" NONE 2>/dev/null
                set -e
                logMessage "Released the DHCP lease via ipconfig set $dev NONE."
            fi
        fi
    fi
fi

# Issue warning if the primary service ID has changed
set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
PSID_CURRENT="$( scutil <<-EOF |
	open
	show State:/Network/OpenVPN
	quit
EOF
grep 'Service : ' | sed -e 's/.*Service : //'
)"
set -e # resume abort on error
if [ "${PSID}" != "${PSID_CURRENT}" ] ; then
	logMessage "Ignoring change of Network Primary Service from ${PSID} to ${PSID_CURRENT}"
fi

# Restore configurations
DNS_OLD="$( scutil <<-EOF
	open
	show State:/Network/OpenVPN/OldDNS
	quit
EOF
)"
SMB_OLD="$( scutil <<-EOF
	open
	show State:/Network/OpenVPN/OldSMB
	quit
EOF
)"
DNS_OLD_SETUP="$( scutil <<-EOF
	open
	show State:/Network/OpenVPN/OldDNSSetup
	quit
EOF
)"
TB_NO_SUCH_KEY="<dictionary> {
  BitmaskNoSuchKey : true
}"

if [ "${DNS_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
	scutil <<-EOF
		open
		remove State:/Network/Service/${PSID}/DNS
		quit
EOF
else
	scutil <<-EOF
		open
		get State:/Network/OpenVPN/OldDNS
		set State:/Network/Service/${PSID}/DNS
		quit
EOF
fi

if [ "${DNS_OLD_SETUP}" = "${TB_NO_SUCH_KEY}" ] ; then
	if ${bAlsoUsingSetupKeys} ; then
		logDebugMessage "DEBUG: Removing 'Setup:' DNS key"
		scutil <<-EOF
			open
			remove Setup:/Network/Service/${PSID}/DNS
			quit
EOF
	else
		logDebugMessage "DEBUG: Not removing 'Setup:' DNS key"
	fi
else
	if ${bAlsoUsingSetupKeys} ; then
		logDebugMessage "DEBUG: Restoring 'Setup:' DNS key"
		scutil <<-EOF
			open
			get State:/Network/OpenVPN/OldDNSSetup
			set Setup:/Network/Service/${PSID}/DNS
			quit
EOF
	else
		logDebugMessage "DEBUG: Not restoring 'Setup:' DNS key"
	fi
fi

if [ "${SMB_OLD}" = "${TB_NO_SUCH_KEY}" ] ; then
	scutil > /dev/null <<-EOF
		open
		remove State:/Network/Service/${PSID}/SMB
		quit
EOF
else
	scutil > /dev/null <<-EOF
		open
		get State:/Network/OpenVPN/OldSMB
		set State:/Network/Service/${PSID}/SMB
		quit
EOF
fi

logMessage "Restored the DNS and SMB configurations"

set +e # "grep" will return error status (1) if no matches are found, so don't fail if not found
new_resolver_contents="$( grep -v '#' < /etc/resolv.conf )"
set -e # resume abort on error
logDebugMessage "DEBUG:"
logDebugMessage "DEBUG: /etc/resolve = ${new_resolver_contents}"

set +e # scutil --dns will return error status in case dns is already down, so don't fail if no dns found
scutil_dns="$( scutil --dns)"
set -e # resume abort on error
logDebugMessage "DEBUG:"
logDebugMessage "DEBUG: scutil --dns = ${scutil_dns}"
logDebugMessage "DEBUG:"

restore_ipv6 "$sRestoreIpv6Services"

flushDNSCache

# Remove our system configuration data
scutil <<-EOF
	open
	remove State:/Network/OpenVPN/OldDNS
	remove State:/Network/OpenVPN/OldSMB
	remove State:/Network/OpenVPN/OldDNSSetup
	remove State:/Network/OpenVPN/DNS
	remove State:/Network/OpenVPN/SMB
	remove State:/Network/OpenVPN
	quit
EOF

if ${ARG_RESET_PRIMARY_INTERFACE_ON_DISCONNECT} ; then
    resetPrimaryInterface
fi

logMessage "End of output from ${OUR_NAME}"
logMessage "**********************************************"

exit 0
