#!/bin/bash
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2007-2022  IPFire Team  <info@ipfire.org>                     #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

eval $(/usr/local/bin/readhash /var/ipfire/ethernet/settings)
eval $(/usr/local/bin/readhash /var/ipfire/dns/settings)

ip2bin() {
	local address="${1}"

	local IFS='.'
	local octet

	local n=0

	for octet in ${address}; do
		# Shift n
		(( n <<= 8 ))

		# Apply the octet
		(( n |= octet ))
	done

	echo "${n}"
}

bin2ip() {
	local n="${1}"

	local IFS='.'
	local address=()

	for i in {3..0}; do
		address+=( $(( n >> (8 * i) & 0xff )) )
	done

	echo "${address[*]}"
}

network_get_intfs() {
	local zone="${1}"

	case "${zone^^}" in
		RED)
			# For PPPoE, the RED interface is called ppp0 (unless we use QMI)
			if [ "${RED_TYPE}" = "PPPOE" ] && [ "${RED_DRIVER}" != "qmi_wwan" ]; then
				echo "ppp0"
				return 0

			# Otherwise we return RED_DEV
			elif [ -n "${RED_DEV}" ]; then
				echo "${RED_DEV}"
				return 0
			fi
			;;

		GREEN)
			if [ -n "${GREEN_DEV}" ]; then
				echo "${GREEN_DEV}"
				return 0
			fi
			;;

		ORANGE)
			if [ -n "${ORANGE_DEV}" ]; then
				echo "${ORANGE_DEV}"
				return 0
			fi
			;;

		BLUE)
			if [ -n "${BLUE_DEV}" ]; then
				echo "${BLUE_DEV}"
				return 0
			fi
			;;

		IPSEC)
			local VARS=(
				id status x1 x2 type x3 x4 x5 x6 x7 x8 x9 x10
				x11 x12 x13 x14 x15 x16 x17 x18 x19 x20
				x21 x22 x23 x24 x25 x26 x27 x28 x29 x30
				x31 x32 x33 x34 interface_mode rest
			)

			while IFS="," read -r "${VARS[@]}"; do
				# Check if the connection is enabled
				[ "${status}" = "on" ] || continue

				# Check if this a net-to-net connection
				[ "${type}" = "net" ] || continue

				# Determine the interface name
				case "${interface_mode}" in
					gre|vti)
						echo "${interface_mode}${id}"
						;;
				esac
			done < /var/ipfire/vpn/config

			return 0
			;;

		WIREGUARD|WG)
			echo "wg+"
			return 0
			;;

		OPENVPN|OVPN)
			# OpenVPN is using all tun devices
			echo "tun+"
			return 0
			;;
	esac

	# Not found
	return 1
}

network_get_address() {
	local network="${1}"

	# Return everything before the slash
	echo "${network%%/*}"
}

network_get_prefix() {
	local network="${1}"

	# Consider everything after the / the prefix
	local prefix="${network##*/}"

	# If the prefix is valid, return it
	if network_prefix_is_valid "${prefix}"; then
		echo "${prefix}"

	# Otherwise it might be a subnet mask
	else
		network_netmask_to_prefix "${prefix}"
	fi
}

network_get_netmask() {
	local network="${1}"

	# Consider everything after the / the netmask
	local netmask="${network##*/}"

	# If we have a prefix, we need to convert
	if network_prefix_is_valid "${netmask}"; then
		network_prefix_to_netmask "${netmask}"

	# Otherwise return what we got
	else
		echo "${netmask}"
	fi
}

network_prefix_is_valid() {
	local prefix="${1}"

	# The prefix must be numbers only
	if ! [[ "${prefix}" =~ ^[0-9]+$ ]]; then
		return 1
	fi

	# Must be a number between 0 and 32 (inclusive)
	[ "${prefix}" -ge 0 -a "${prefix}" -le 32 ]
}

network_prefix_to_netmask() {
	local prefix="${1}"

	# Set n with all bits set
	local n=0xffffffff

	# Shift
	(( n <<= (32 - prefix) ))

	# Convert back
	bin2ip "${n}"
}

network_netmask_to_prefix() {
	local netmask="${1}"

	local prefix=0

	# Convert to binary
	local n="$(ip2bin "${netmask}")"

	while [ "${n}" -gt 0 ]; do
		# If the highest bit is not set, we are done
		[ "$(( n & (1 << 31) ))" -eq 0 ] && break

		# Increment prefix & shift n
		(( prefix++ ))
		(( n <<= 1 ))
	done

	echo "${prefix}"
}

network_address_in_network() {
	local address="${1}"
	local network="${2}"

	# Split the network into its address & mask
	local netaddr="$(network_get_address "${network}")"
	local netmask="$(network_get_netmask "${network}")"

	# Abort if we could not parse the network
	if [ -z "${netaddr}" -o -z "${netmask}" ]; then
		return 1
	fi

	# Convert everything to binary
	address="$(ip2bin "${address}")"
	netaddr="$(ip2bin "${netaddr}")"
	netmask="$(ip2bin "${netmask}")"

	# Ensure the network address is the first address
	(( netaddr &= netmask ))

	# Compute broadcast
	local broadcast=$(( netaddr | (~netmask & 0xffffffff) ))

	# Return true if address is in the network
	[ "${address}" -ge "${netaddr}" -a "${address}" -le "${broadcast}" ]
}

# Takes a network and list of IP addresses and will return the first IP address
# that is in the given network.
first_address_in_network() {
	local network="${1}"
	shift

	local addr
	for addr in $@; do
		if network_address_in_network "${addr}" "${network}"; then
			echo "${addr}"
			return 0
		fi
	done

	return 1
}

# Returns the first of IPFire's own IP addresses that is in any of the given networks
ipfire_address_in_networks() {
	local addresses=()

	local var
	for var in GREEN_ADDRESS BLUE_ADDRESS ORANGE_ADDRESS; do
		if [ -n "${!var}" ]; then
			addresses+=( "${!var}" )
		fi
	done

	local network
	for network in $@; do
		# Find and end after the first match
		if first_address_in_network "${network}" "${addresses[@]}"; then
			return 0
		fi
	done

	# Nothing found
	return 1
}

dhcpcd_get_pid() {
		# This function returns the pid of a dhcpcd by a given
		# network device, if a pidfile exists.

		local device="$1"
		local pidfile="/var/run/dhcpcd/${device}.pid"

		# Check if a pid file exists.
		if [ -f "${pidfile}" ] ; then

			# Get the pid from the file.
			local pid="$(<"${pidfile}")"

			echo "${pid}"
		fi
}

dhcpcd_is_running() {
	# This functions checks if a dhcpcd is running by a given pid.

	local pid="$1"

	# Check if a dhcpcd is running.
	if [ -n "${pid}" -a -d "/proc/${pid}" ]; then
		# Return "0" (True) if a dhcpcd is running.
		return 0
	fi

	# Return 1 (False) no dhcpcd is running.
	return 1
}

dhcpcd_start() {
	# This function will start a dhcpcd on a speciefied device.
	local device="$1"
	shift

	local dhcp_start=()

	boot_mesg -n "Starting dhcpcd on the ${device} interface..."

	# Check if a dhcpcd is already running.
	local pid="$(dhcpcd_get_pid "${device}")"

	if  dhcpcd_is_running "${pid}"; then
		boot_mesg "dhcpcd already running!" ${WARNING}
		echo_warning
		exit 2
	fi

	# Check if a DHCP hostname has been set.
	if [ -n "${RED_DHCP_HOSTNAME}" ]; then
		dhcp_start+=( "-h" "${RED_DHCP_HOSTNAME}" )
	fi

	# Tell dhcpcd to use the configured MTU
	if [ -n "${RED_DHCP_FORCE_MTU}" ]; then
		dhcp_start+=( "--static" "mtu=${RED_DHCP_FORCE_MTU}" )
	fi

	# Append any further command line options
	dhcp_start+=( $@ )

	# Start dhcpcd.
	/sbin/dhcpcd "${dhcp_start[@]}" ${device} >/dev/null 2>&1
	ret="$?"

	if [ "${ret}" -eq 0 ]; then
		. /var/ipfire/dhcpc/dhcpcd-"${device}".info

			if [ $ip_address ]; then
			echo ""
			echo_ok
			boot_mesg "           DHCP Assigned Settings for ${device}:"
			boot_mesg_flush
			boot_mesg "           IP Address:      $ip_address"
			boot_mesg_flush

			if [ -n "${RED_DHCP_HOSTNAME}" ]; then
				boot_mesg "           Hostname:        $RED_DHCP_HOSTNAME"
				boot_mesg_flush
			fi

			boot_mesg "           Subnet Mask:     $subnet_mask"
			boot_mesg_flush
			boot_mesg "           Default Gateway: $routers"
			boot_mesg_flush
			boot_mesg "           DNS Server:      $domain_name_servers"
			boot_mesg_flush
		else
			echo ""
			echo_ok
			boot_mesg "DHCP for ${device} still running..."
			boot_mesg_flush
		fi
	else
		echo ""
		$(exit "${ret}")
 		evaluate_retval
	fi
}

dhcpcd_stop() {
	# This function stops a previously started dhcpcd on a given device.

	local device="$1"
	local dhcp_stop="-k"
	local leaseinfo="/var/ipfire/dhcpc/dhcpcd-${device}.info"

	boot_mesg -n "Stopping dhcpcd on the ${device} interface..."

	# Check if a dhcpcd is running.
	local pid="$(dhcpcd_get_pid "${device}")"

	if ! dhcpcd_is_running "${pid}"; then
		boot_mesg "    Not running." ${WARNING}
		echo_warning
		exit 1
	fi

	# Stop dhcpcd.
	/sbin/dhcpcd ${dhcp_stop} ${device} &> /dev/null
	ret="$?"

	# Wait until dhcpd has stopped.
	while [ -d "/proc/${pid}" ]; do
		sleep 1
		# repeat stop if dhcp was still running
		/sbin/dhcpcd ${dhcp_stop} ${device} &> /dev/null
	done

	# Display console message, depended on the exit code
	# of the stopped dhcpcd.
	if [ "${ret}" -eq 0 ]; then
		boot_mesg
		echo_ok
	elif [ "${ret}" -eq 1 ]; then
		boot_mesg "failed to stop dhcpcd!" ${WARNING}
		echo_warning
	else
		boot_mesg
		echo_failure
	fi
}

# QMI stuff

qmi_find_device() {
	local intf="${1}"
	local _intf

	local path
	for path in /dev/cdc-*; do
		if [ -c "${path}" ]; then
			_intf="$(qmi_find_interface "${path}")"

			# Check if the interface matches
			if [ "${intf}" = "${_intf}" ]; then
				echo "${path}"
				return 0
			fi
		fi
	done

	# Nothing found
	return 1
}

qmi_find_interface() {
	local device="${1}"

	qmicli --device="${device}" --device-open-proxy --get-wwan-iface
}

qmi_enable_rawip_mode() {
	local intf="${1}"

	# Shut down the device first
	ip link set "${intf}" down &>/dev/null

	echo "Y" > "/sys/class/net/${intf}/qmi/raw_ip"
}

qmi_configure_apn() {
	local device="${1}"

	# APN settings
	local apn="${2}"
	local auth="${3}"
	local username="${4}"
	local password="${5}"

	local args=(
		# We only support IPv4 right now
		"ip-type=4"
	)

	# Set APN
	if [ -n "${apn}" ]; then
		args+=( "apn=${apn}" )
	fi

	# Set auth
	case "${auth}" in
		PAP|CHAP)
			args+=( "auth=${auth}" )
			;;
	esac

	# Set username
	if [ -n "${username}" ]; then
		args+=( "username=${username}" )
	fi

	# Set password
	if [ -n "${password}" ]; then
		args+=( "password=${password}" )
	fi

	local _args

	local arg
	for arg in ${args[@]}; do
		if [ -n "${_args}" ]; then
			_args="${_args},"
		fi
		_args="${_args}${arg}"
	done

	qmicli --device="${device}" --device-open-proxy \
		--wds-start-network="${_args}" \
		--client-no-release-cid &>/dev/null
}

qmi_reset() {
	local device="${1}"

	qmicli --device="${device}" --device-open-proxy \
		--wds-reset &>/dev/null
}

# Assigns a "static" MAC address
qmi_assign_address() {
	local intf="${1}"

	# Find the device
	local device="$(qmi_find_device "${intf}")"

	# Switch off the raw_ip mode to be able to proper
	# assign the generated MAC address.
	echo "N" > "/sys/class/net/${intf}/qmi/raw_ip"

	local address

	# Generate a "random" MAC address using the device number
	printf -v address "02:ff:ff:ff:ff:%02x" "${device:12}"

	# Change the MAC address
	ip link set "${intf}" address "${address}"
}
