#!/usr/bin/env bash
#version 0.95-4-N/HS-I ConsolePi Rev 1.5

#You may share this script on the condition a reference to RaspberryConnect.com
#must be included in copies or derivatives of this script.

#Network Wifi & Hotspot with Internet
#A script to switch between a wifi network and an Internet routed Hotspot
#A Raspberry Pi with a network port required for Internet in hotspot mode.
#Works at startup or with a seperate timer or manually without a reboot
#Other setup required find out more at
#http://www.raspberryconnect.com

# -- Modified for ConsolePi --
# -- variablelize hotspot IP referenced in createAdHocNetwork
# -- import ConsolePi configuration (ConsolePi.yaml)
# -- INFO/DEBUG Logging sent to consolepi.log
# -- Add chkWiredState function
#      chkWiredState modifies the dnsmasq DHCP server configuration
#      So the hotspot will provide an IP with no default gateway
#      if the wired interface is down.  It also cheks the domain
#      provided to the wired interface via DHCP and provides that
#      same domain to hotspot clients.

# -- The default-gateway bit is done so you can connect
# -- in a dual nic setup and not corrupt your routing table
# -- with an invalid route

# -- Passing on the domain the the hostpot uses is primarily done
# -- for auto-openvpn which will not initiate a connection if the
# -- ConsolePi is connected to your home network.  So it's primarily
# -- for me during dev to prevent openvpn from starting while I'm testing
# -- stuff i.e. ConsolePi connected to ConsolePi

# Supported override variables (define the variable in ConsolePi.yaml OVERRIDES section)
# -- None @ this time

# -- // GLOBALS \\ --
hostapd_conf="/etc/hostapd/hostapd.conf"
common_funcs="/etc/ConsolePi/installer/common.sh"
cfg_file_yaml="/etc/ConsolePi/ConsolePi.yaml"
cfg_file_conf="/etc/ConsolePi/ConsolePi.conf"  # legacy config support should not be used
DEBUG=false
ahs_dhcp_config="/etc/ConsolePi/dnsmasq.d/autohotspot/autohotspot"
# -- allows for support of old autohotspot config using dnsmasq.conf and new separate instance --
if [ -f $ahs_dhcp_config ] && [ -f /etc/dnsmasq.d/01-consolepi ] &&
   ( [ -L /etc/systemd/system/consolepi-autohotspot-dhcp.service ] || [ -f /etc/systemd/system/consolepi-autohotspot-dhcp.service ] ); then
    dhcp_config=$ahs_dhcp_config
else
    dhcp_config='/etc/dnsmasq.conf'
fi
log_file="/var/log/ConsolePi/consolepi.log"
process="AutoHotSpot"
# -- Terminal coloring --
_norm='\e[0m'
_bold='\033[1;32m'
_blink='\e[5m'
_red='\e[31m'
_blue='\e[34m'
_lred='\e[91m'
_yellow='\e[33;1m'
_green='\e[32m'

logit() {
    # Logging Function: logit <message|string> [<status|string>]
    # usage:
    #   process="AutoHotSpot"  # Define prior to calling or defaults to UNDEFINED
    #   logit "building package" <"WARNING">
    # NOTE: Sending a status of "ERROR" results in the script exiting
    #       default status is INFO if none provided.
    [ -z "$process" ] && process="UNDEFINED"
    message=$1                                      # 1st arg = the log message
    [ -z "${2}" ] && status="INFO" && c_status="INFO" || status=${2^^} # to upper
    fatal=false                                     # fatal is determined by status. default to false.  true if status = ERROR
    if [[ "${status}" == "ERROR" ]]; then
        fatal=true
        c_status="${_red}${status}${_norm}"
    elif [[ ! "${status}" == "INFO" ]]; then
        c_status="${_yellow}${status}${_norm}"
        # [[ "${status}" == "WARNING" ]]
    fi

    # -- Strip any ascii coloring out for sending to log
    no_color_msg=${message/\\${_green}/} ; no_color_msg=${no_color_msg/\\${_red}/} ; no_color_msg=${no_color_msg/\\${_norm}/}

    # -- Log to log-file --
    echo -e "$(date +"%b %d %T") [$$][${status}][${process}] ${no_color_msg}" >> $log_file

    # -- Any warning/errors log to syslog/stdout as well --
    if [[ ! "${status}" == "INFO" ]] && [[ ! "${status}" == "DEBUG" ]]; then
        if [ -t 1 ]; then
            echo -e "[${c_status}] ${message}"  # log any errors to syslog as well
        else
            echo -e "[${status}] ${no_color_msg}"  # log any errors to syslog as well
        fi

        if $fatal ; then
            # if status was ERROR which means FATAL then log and exit script
            echo -e "[${status}] Last Error is fatal, script exiting Please review log ${log_file}" && exit 1
        fi
    else
        # -- Always Log to stdout if this is a tty --
        [ -t 1 ] && echo -e "[${c_status}][${process}] ${message}"
    fi
    return 0
}

# -- update path so script can find executables when ran as cron job --
[[ ! "$PATH" =~ 'sbin' ]]  && PATH="$PATH:/sbin:/usr/sbin"

# -- // extract wlan_ip and hotspot variables from config \\ --
if [[ -f $cfg_file_yaml ]] ; then
    wlan_ip=$(grep wlan_ip: $cfg_file_yaml | cut -d : -f 2 | cut -d' ' -f 2)
    wlan_ip="${wlan_ip//\"/}"
    hotspot=$(grep "hotspot:" $cfg_file_yaml | cut -d : -f 2 | cut -d' ' -f 2)
    hotspot="${hotspot//\"/}"
elif [[ -f $cfg_file_conf ]] ; then
    . $cfg_file_conf
fi
if [[ -z $wlan_ip ]] ; then
    logit "Unable to collect HotSpot IP from ConsolePi.yaml" "WARNING"
    if [[ -f "$dhcp_config" ]] ; then
        wlan_pfx=$(grep -q wlan0 $dhcp_config && grep dhcp-range= $dhcp_config | cut -d = -f 2 | cut -d . -f 1-3)
        [[ ! -z $wlan_pfx ]] && wlan_ip="${wlan_pfx}.1" && logit "Guessed HotSpot IP based on $dhcp_config ($wlan_ip)" ||
            ( logit "Unable to Guess HotSpot IP based on $dhcp_config Giving Up" "ERROR" && exit 1 )
    fi
fi

[[ -z $hotspot ]] && hotspot=true && logit "HotSpot variable not found in ConsolePi Configuration falling back to true (Enabled)" "WARNING"
! $hotspot && logit "ConsolePi Auto HotSpot is disabled in the config but this script was triggered, Disable the systemd file" "WARNING" &&
    logit "ConsolePi Auto HotSpot exiting" && exit 0

ifaces=$(ip -br link show | grep -v lo | awk '{printf $1 " "}')
# -- get wlan interface from hostapd.conf if found/defined --
[[ -f "$hostapd_conf" ]] && . "$hostapd_conf"
[ ! -z "$interface" ] && wifidev=$interface || wifidev="wlan0"
ethdev="eth0" #Ethernet port to use with IP tables
# TODO test for adverse .. change iptables rules to -s $wifinet -d ! $wifinet
# wifinet= printf "%s.0/24" $(echo $wlan_ip | cut -d. -f1-3)  # Not Used for now

IFSdef=$IFS
cnt=0
#These four lines capture the wifi networks the RPi is setup to use
wpassid=$(awk '/ssid="/{ print $0 }' /etc/wpa_supplicant/wpa_supplicant.conf | awk -F'ssid=' '{ print $2 }' ORS=',' | sed 's/\"/''/g' | sed 's/,$//')
IFS=","
ssids=($wpassid)
IFS=$IFSdef #reset back to defaults

# Number of seconds the script will pause after wifi is enabled before checking to see if it successfully connected.
[ -z $wlan_wait_time ] && wlan_wait_time=20


# TODO add this to overrides
#Enter the Routers Mac Addresses for hidden SSIDs, seperated by spaces ie
#( '11:22:33:44:55:66' 'aa:bb:cc:dd:ee:ff' )
mac=()

ssidsmac=("${ssids[@]}" "${mac[@]}") #combines ssid and MAC for checking

ip_forward_enabled() {
    local sysctl_files=($(ls -1 /etc/sysctl.d/*.conf))
    local sysctl_files+=(/etc/sysctl.conf)
    local is_set=false
    for f in ${sysctl_files[@]}; do
        if grep -q "^net.ipv4.ip_forward\s*=\s*1" "$f"; then
            local is_set=true
            break
        fi
    done
    $is_set && return 0 || return 1
}

get_ssid_details() {
    wlan_ip_addr=$(wpa_cli -i $wifidev status | grep ip_address | cut -d'=' -f2)
    if which iwgetid >/dev/null ; then
        local _log="$1 connected to ${_green}$(iwgetid $wifidev -r)${_norm} with IP ${wlan_ip_addr} AP $(iwgetid $wifidev -a | awk '{print $4}') using $(iwgetid $wifidev -f|awk '{print $2}')"
        [ ! -z "$2" ] && logit "${_log} Time to Connect ${_cyan}$2 sec${_norm}" || logit "${_log}"
    else
        logit "$HOSTNAME is connected to a valid SSID"
    fi
}

start_stop_dnsmasq() {
    # if all files are in place to use new separate instance for hotspot dhcp do so otherwise assume using dnsmasq default instance
    if [ -f $ahs_dhcp_config ] && [ -f /etc/dnsmasq.d/01-consolepi ] &&
       ( [ -L /etc/systemd/system/consolepi-autohotspot-dhcp.service ] || [ -f /etc/systemd/system/consolepi-autohotspot-dhcp.service ] ); then
        [[ $1 == "start" ]] && logit "Using ConsolePi Specific dnsmasq instance"
        systemctl $1 consolepi-autohotspot-dhcp.service || logit "Failed to $1 AutoHotSpot DHCP"
    else
        [[ $1 == "start" ]] && logit "ConsolePi Specific dnsmasq instance *not* Configured, using default dnsmasq instance"
        systemctl $1 dnsmasq.service || logit "Failed to $1 AutoHotSpot DHCP (dnsmasq)"
    fi
}

createAdHocNetwork() {
    logit "Creating Hotspot"
    ip link set dev "$wifidev" down
    ip a add ${wlan_ip}/24 brd + dev "$wifidev"
    ip link set dev "$wifidev" up
    debug_ip=`ip addr show dev wlan0 | grep 'inet '| cut -d: -f2 |cut -d/ -f1| awk '{ print $2}'`
	logit "${wifidev} is up with ip: ${debug_ip}"
    dhcpcd -k "$wifidev" >/dev/null 2>&1
    iptables -t nat -A POSTROUTING -o "$ethdev" -j MASQUERADE
    iptables -A FORWARD -i "$ethdev" -o "$wifidev" -m state --state RELATED,ESTABLISHED -j ACCEPT
    iptables -A FORWARD -i "$wifidev" -o "$ethdev" -j ACCEPT
    ChkWiredState
    # systemctl start dnsmasq
    start_stop_dnsmasq start
    systemctl start hostapd
    echo 1 > /proc/sys/net/ipv4/ip_forward
}

KillHotspot() {
    logit "Shutting Down Hotspot"
    ip link set dev "$wifidev" down
    systemctl stop hostapd
    # systemctl stop dnsmasq
    start_stop_dnsmasq stop
    iptables -D FORWARD -i "$ethdev" -o "$wifidev" -m state --state RELATED,ESTABLISHED -j ACCEPT
    iptables -D FORWARD -i "$wifidev" -o "$ethdev" -j ACCEPT
    ! ip_forward_enabled && echo 0 > /proc/sys/net/ipv4/ip_forward
    ip addr flush dev "$wifidev"
    ip link set dev "$wifidev" up
    dhcpcd  -n "$wifidev" >/dev/null 2>&1
}

ChkWifiUp() {
    logit "Checking that WiFi connection is OK"
    # logit "pausing $wlan_wait_time seconds to allow wpa_supplicant time to connect"
    # sleep $wlan_wait_time #give time for connection to be completed to router

    # Wait for connection and ip on interface
    start_time=$(date +"%s")
    _bumped=false
    while ! wpa_cli -i "$wifidev" status | grep -q 'ip_address'; do
        sleep 3
        # if wlan is associated give it a little more time to complete AuthN
        ! $_bumped && wpa_cli -i "$wifidev" status | grep -q 'ASSOCIAT' && ((wlan_wait_time+=10)) && _bumped=true
        [[ $(date +"%s") > $((start_time + wlan_wait_time)) ]] && break
    done
    connect_time=$(($(date +"%s") - start_time))


    if ! wpa_cli -i "$wifidev" status | grep 'ip_address' >/dev/null 2>&1; then
        logit 'Wifi failed to connect, falling back to Hotspot.' "WARNING"
        wpa_cli terminate "$wifidev" >/dev/null 2>&1
        createAdHocNetwork
    else
        get_ssid_details Now $connect_time
    fi
}


ChkWiredState() {
    if [[ "$ifaces" =~ "$ethdev" ]]; then
        # eth0_ip=`ip addr show dev $ethdev | grep 'inet '| cut -d: -f2 |cut -d/ -f1| awk '{ print $2}'`
        eth0_ip=$(ip -br a | grep "^${ethdev} " |awk '{ print $3}' | cut -d'/' -f1)
        eth0_state=`ip addr show dev $ethdev | head -1 | sed -n -e 's/^.*state //p' | cut -d ' ' -f1 |awk '{ print $1 }'`
        if [ ${eth0_state} == "UP" ] && [ ${#eth0_ip} -gt 6 ]; then
            eth0_up=true
        else
            eth0_up=false
        fi
    else
        eth0_up=false
    fi

    # Find out if lease file exists for wired interface
    [ -f /var/lib/dhcpcd*/*${ethdev}*.lease ] && wired_lease=true || wired_lease=false
    # If ethdev is up and has a lease, pass the same domain to WLAN clients on the hotspot
    if $eth0_up && $wired_lease; then
        [ -f /tmp/$ethdev ] && eth0_dom=$(grep 'domain=' /tmp/$ethdev) || logit "[ConsolePi-AutoHotSpot] $ethdev tmp file not found" "WARNING"
        [ ${#eth0_dom} -gt 9 ] && valid_dom=true || valid_dom=false
        if $valid_dom; then
            if ! $(grep -q 'domain=' $dhcp_config); then
                # sed -i -e :a -e  '/^\\n*$/{$d;N;};/\\n$/ba' $dhcp_config # make sure EoF has \n
                sed -i -e '$a\' $dhcp_config # make sure EoF has \n
                echo "${eth0_dom}" >> $dhcp_config &&
                use_eth_dom=true || use_eth_dom=false
            else
                sed -i "/domain=/s/.*/${eth0_dom}/" $dhcp_config &&
                use_eth_dom=true || use_eth_dom=false
            fi
            $use_eth_dom && logit "Active Lease Found on $ethdev with $eth0_dom using same for HotSpot clients" ||
                logit "[ChkWiredState] Failed to Configure Domain $eth0_dom for HostSpot clients" "WARNING"
        else
            logit "A lease was found for $ethdev but no Domain was provided Removing Domain for HotSpot clients"
            sed -i '/^domain=.*/s/^/#/g' $dhcp_config
        fi
    fi

    if ! $eth0_up || ! $wired_lease; then
        sed -i '/^domain=.*/s/^/#/g' $dhcp_config &&
        logit "Removing Domain for HotSpot clients as there is no lease on $ethdev or $ethdev is down" ||
        logit "[ChkWiredState] Failed to Remove Domain for HotSpot clients" "WARNING"
    fi

    if $eth0_up; then
        sed -i '/^dhcp-option=wlan0,3/s/^/#/g' $dhcp_config  # comment out option 3 - result is default behavior assign i/f address as def-gw
        logit "Bringing up hotspot with gateway as eth0 is up with IP $eth0_ip"
    else
        sed -i '/^#dhcp-option=wlan0,3/s/^#//g' $dhcp_config  # uncomment line defines option 3 with no value, over-riding default behavior no gw assigned
        logit "Bringing up hotspot with no gateway due to no eth0 connection"
    fi
}

FindSSID() {
    #Check to see what SSID's and MAC addresses are in range
    ssidChk=('NoSSid')
    # used to determine if we just booted, if so double loop counter
    uptime=$(sudo cat /proc/uptime | awk '{print $1}') && uptime=$(echo ${uptime//.*/})
    # uptime on initial hit on Pi4 was consistently 14 seconds
    i=0; j=1
    until [ $i -eq 1 ] ; do # wait for wifi if busy, usb wifi is slower.
        ssidreply=$((iw dev "$wifidev" scan ap-force | grep -v HESSID | egrep "^BSS|SSID:") 2>&1) >/dev/null 2>&1
        # FIXME handle SSID with space i.e. 'Sun Room.k,'
        # _IFS=$IFS
        # IFS=$'\n'
        # x=($(echo -e ${ssidreply//BSS/\\nBSS} | tail -n +2))
        # # close but bash: warning: command substitution: ignored null byte in input
        # x=($(for line in ${ssidlines[@]}; do ssid=$(echo $line | rev | cut -d: -f1 | rev); echo ${ssid//' '/};done))
        # IFS=$_IFS

        if ! $DEBUG ; then
            x=($(echo "$ssidreply" |  grep -v '\\x00\\x00' | grep "SSID:" | cut -d' ' -f 2))
            # x=($(echo "$ssidreply" |  grep -v '\\x00\\x00' | grep "SSID:" | cut -d' ' -f 2-))
            for ssid in "${x[@]}" ; do if [[ ! "$ssids_pretty" =~ "$ssid" ]] ; then ssids_pretty="$ssids_pretty, $ssid"; fi; done
            logit "SSID's in range: ${ssids_pretty/, /}"
        else
            logit "SSID's in range: $ssidreply" "DEBUG"
        fi

        (($j > 1)) # && logit "Device Available Check try $j"
        if (($j >= 10)) && (($uptime > 60)); then #if busy 10 times goto hotspot unless we just booted up then give it a min
            logit "Device busy or unavailable $j times, enabling Hotspot"
            ssidreply=""
            i=1
        elif echo "$ssidreply" | grep -q "No such device (-19)"; then  # >/dev/null 2>&1; then
            logit "No Device Reported, try $j"
            NoDevice
        elif echo "$ssidreply" | grep -q  "Network is down (-100)"; then  # >/dev/null 2>&1 ; then
            logit "Network Not available, trying again $j"
            j=$((j + 1))
            sleep 2
        elif echo "$ssidreply" | grep -q "Read-only file system (-30)"; then  # >/dev/null 2>&1 ; then
            logit "Temporary Read only file system, trying again"
            j=$((j + 1))
            sleep 2
        elif echo "$ssidreply" | grep -q "Invalid exchange (-52)"; then  # >/dev/null 2>&1 ; then
            logit "Invalid Exchange ~ Transient Error, trying again"
            j=$((j + 1))
            sleep 2
        elif echo "$ssidreply" | grep -q "esource temporarily unavailable "; then  # >/dev/null 2>&1 ; then
            logit "Resource temporarily unavailable"
            j=$((j + 1))
            sleep 2
        elif ! echo "$ssidreply" | grep -q "esource busy (-16)" ; then  # >/dev/null 2>&1 ; then
            logit "Device Available, checking SSid Results"
            i=1
        else #see if device not busy in 2 seconds
            logit "Device unavailable checking again, try $j"
            j=$((j + 1))
            sleep 2
        fi
    done

    for ssid in "${ssidsmac[@]}" ; do
        if (echo "$ssidreply" | grep "$ssid") >/dev/null 2>&1 ; then
            #Valid SSid found, passing to script
            logit "Valid SSID $ssid Detected, assesing Wifi status"
            ssidChk=$ssid
            return 0
        else
            #No Network found, NoSSid issued"
            logit "Configured SSID $ssid not found..."
            ssidChk='NoSSid'
        fi
    done
}

NoDevice() {
	# if no wifi device,ie usb wifi removed, activate wifi so when it is
	# reconnected wifi to a router will be available
	logit "No wifi device connected"
	wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1
	exit 1
}

# -- // main \\ --
FindSSID

#Create Hotspot or connect to valid wifi networks
if [ "$ssidChk" != "NoSSid" ] ; then
    ! ip_forward_enabled && echo 0 > /proc/sys/net/ipv4/ip_forward #deactivate ip forwarding
    if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 ; then #hotspot running and ssid in range
        KillHotspot
        logit "Hotspot Deactivated, Bringing Wifi Up"
        wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1
        ChkWifiUp
    elif { wpa_cli -i "$wifidev" status | grep 'ip_address'; } >/dev/null 2>&1 ; then #Already connected
        # echo "Wifi already connected to a network"
        get_ssid_details Already
        exit 0
    else #ssid exists and no hotspot running connect to wifi network
        logit "Connecting to the WiFi Network"
        wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1
        ChkWifiUp
    fi
else #ssid or MAC address not in range
    if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 ; then
        logit "Hostspot already active"
    elif { wpa_cli status | grep "$wifidev"; } >/dev/null 2>&1 ; then
        logit "Cleaning wifi files and Activating Hotspot"
        wpa_cli terminate >/dev/null 2>&1
        ip addr flush "$wifidev"
        ip link set dev "$wifidev" down
        _ctrl_if=$(grep "^ctrl_interface" /etc/wpa_supplicant/wpa_supplicant.conf)
        _ctrl_if=$(echo ${x// GROUP*/} | rev | cut -d= -f 1 | rev)
        rm -r "$_ctrl_if" >/dev/null 2>&1
        createAdHocNetwork
        exit 0
    else #"No SSID, activating Hotspot"
        createAdHocNetwork
        exit 0
    fi
fi
