#!/bin/ash

# shellcheck disable=SC2034 # variable is used in libalpine.sh
PROGRAM=setup-interfaces
PREFIX=/usr
: "${LIBDIR=$PREFIX/lib}"
. "$LIBDIR/libalpine.sh"

PKGS=

unconfigured_add() {
	touch "$1.noconf"
}

unconfigured_detect() {
	local i
	for i in ${INTERFACES:-$(available_ifaces)}; do
		case "$i" in
			lo|docker0) continue;;
		esac
		unconfigured_add "$i"
	done
}

unconfigured_get_first() {
	find -- *.noconf 2>/dev/null | head -n 1 | sed 's/.noconf//'
}

unconfigured_del() {
	rm -f "$1.noconf"
}

unconfigured_all_done() {
	local i
	for i in *.noconf; do
		[ -f "$i" ] && return 1
	done
	return 0
}

unconfigured_list() {
	local list i
	for i in *.noconf; do
		[ -f "$i" ] || continue
		list="${list}${list:+ }${i%.noconf}"
	done
	echo "$list"
}

unconfigured_isin() {
	[ -f "$1.noconf" ]
}

iface_exists() {
	test -e "$ROOT/sys/class/net/$1"
}

get_default_addr() {
	local iface="$1"
	local family="$2"
	# check if dhcpcd is running
	if pidof dhcpcd > /dev/null && [ -f "$ROOT/var/lib/dhcpc/dhcpcd-$iface.info" ]; then
		echo dhcp
	elif iface_exists "$iface"; then
		local family_type="inet"
		if [ "$family" = "IPv6" ]; then
			family_type="inet6"
		fi
		$MOCK ip addr show "$iface" | awk "/$family_type / {print \$2}" | head -n 1
	fi
}

get_default_gateway() {
	local iface="$1"
	local family="$2"
	if iface_exists "$iface"; then
		local flags=""
		if [ "$family" = "IPv6" ]; then
			flags="-6"
		fi
		$MOCK ip $flags route show dev "$iface" | awk '/^default/ {print $3}'
	fi
}

ipaddr_help() {
	cat <<-EOF

		Select the IP address for this interface.

		dhcp					Dynamic/automatic IP via DHCP
		none					Do not add any address
		n.n.n.n		(ex: 192.168.0.1)	Static IPv4
		n.n.n.n/m	(ex: 192.168.0.1/24)	Static IPv4 with mask
		<IPv6>		(ex: fec0:0:0:1::2)	Static IPv6
		<IPv6>/m	(ex: fec0:0:0:1::2/64)	Static IPv6 with mask
		br[0-9]+	(ex: br0)		Add this interface to a bridge
		bridge[0-9]	(ex: bridge0)		Add this interface to a bridge

		You will be prompted for netmask if not specified with the address.

	EOF
}

bridge_add_port() {
	local bridge="$1" iface
	shift
	for iface; do
		echo "$iface" >> "$bridge.bridge_ports"
		unconfigured_add "$bridge"
		unconfigured_del "$iface"
	done
}

bridge_list_ports() {
	if [ -r "$1.bridge_ports" ]; then
		cat_collapsing_whitespace "$1.bridge_ports"
	fi
}

is_bridge() {
	test -r "$1.bridge_ports"
}

is_wifi() {
	test -d "$ROOT/sys/class/net/$1/phy80211"
}

find_essids() {
	local iface="$1"
	export essids_list=wlans
	# Supports only open or PSK
	$MOCK ip link set dev "$iface" up
	(iw dev "$iface" scan; echo BSS) | awk -F": " '
		/^BSS/ { if (ssid) { print ssid "/" auth }; ssid=""; auth="" }
		$1 ~ /^[[:blank:]]*SSID$/ { ssid=$2 }
		$1 ~ /Authentication suites/ { auth=$2 }' \
		| grep -E -v '(802.1x|\\x00)' | sort -u >"$essids_list"
	if [ -s "$essids_list" ]; then
		# we use / as separator since it is an illegal char in ssids
		awk -F/ '{print NR ") " $1}' "$essids_list"
	else
		return 1
	fi
}

config_wpa_supp() {
	local iface="$1" essid="$2" auth_type="$3" psk="$4"
	local conffile="$ROOT/etc/wpa_supplicant/wpa_supplicant.conf"
	mkdir -p "${conffile%/*}"
	if [ "$auth_type" = "WPA-PSK" ]; then
		(umask 0077 && wpa_passphrase "$essid" "$psk" | sed -e '/^\t#psk=.*/d' >> "$conffile")
	else
		cat >> "$conffile" <<-EOF
		network={
			ssid="$essid"
			key_mgmt=$auth_type
		}
		EOF
	fi
	mkdir -p "$ROOT/etc/conf.d"
	if grep -q ^wpa_supplicant_args= "$ROOT/etc/conf.d/wpa_supplicant" 2>/dev/null; then
		sed -i -e "s/^wpa_supplicant_args=.*/wpa_supplicant_args=\"-i $iface\"/" /etc/conf.d/wpa_supplicant
	else
		printf 'wpa_supplicant_args="-i%s"\n' "$iface" >> "$ROOT/etc/conf.d/wpa_supplicant"
	fi
	rc-update --quiet add wpa_supplicant boot
	rc-service wpa_supplicant start
}

essid_is_valid()  {
	[ -n "$1" ] && cut -d/ -f1 "$essids_list" | grep -q -w -F "$1"
}

is_number() {
	echo "$1" | grep -q -E  '^[0-9]+$'
}

essid_by_index() {
	if is_number "$1"; then
		cut -d/ -f1 "$essids_list" | sed "$1!d"
	fi
}

wlan_is_psk() {
	local essid="$1"
	awk -F/ -v essid="$essid" '$1==essid {print $2}' "$essids_list" | grep -q -F -w 'PSK'
}

is_ipv6_enabled() {
	test -f /proc/net/if_inet6
}

contains() {
  case "$1" in
    (*"$2"*) true;;
    (*) false;;
  esac
}

cat_collapsing_whitespace() {
	local path="$1"
	# shellcheck disable=SC2005 disable=SC2046
	echo $(cat "$path")
}

config_iface() {
	local iface="$1"
	local prefix="$2"
	local default_address="$3"
	local conf="$prefix$iface.conf"
	local bridge

	if [ -n "$ask_bridge" ] && ! is_bridge "$iface" \
			&& ask_yesno "Do you want to bridge the interface $iface? (y/n)" y; then

		bridge="br${iface//[^0-9]/}"
		ask "Name of the bridge you want add $iface to:" "$bridge"
		bridge_add_port "$resp" "$iface"
		return
	fi

	if is_wifi "$iface"; then
		apk add --quiet --no-progress iw wpa_supplicant || return
		local wifi_configured=false
		while ! $wifi_configured; do
			echo "Available wireless networks (scanning):"
			if ! find_essids "$iface"; then
				printf "\nNo available wireless networks\n"
				return
			fi

			local essid auth_type="NONE"
			ask "Type the wireless network name to connect to:"
			if ! essid_is_valid "$resp"; then
				case "$resp" in
					""|done|abort) echo "Aborting $iface setup"; return;;
				esac
				local by_index
				by_index="$(essid_by_index "$resp")"
				if essid_is_valid "$by_index"; then
					resp="$by_index"
				else
					echo "Invalid SSID: $resp"
					continue
				fi
			fi
			essid="$resp"

			if wlan_is_psk "$essid"; then
				auth_type="WPA-PSK"
				askpass "Type the \"$essid\" network Pre-Shared Key (will not echo):"
				psk="$resp"
			fi
			config_wpa_supp "$iface" "$essid" "$auth_type" "$psk" && wifi_configured=true
		done
	fi

	config_iface_family "$iface" "$conf" "$default_address" "IPv4" 32 24 "dhcp"
	if is_ipv6_enabled; then
		config_iface_family "$iface" "$conf" "$default_address" "IPv6" 128 64 "auto" "auto"
	fi
}

config_iface_family() {
	local iface="$1"
	local conf="$2"
	local default_address="$3"
	local family="$4"
	local max_mask_size="$5"
	local default_mask_size="$6"
	local default_method="$7"
	local extra_methods="$8"
	local address gateway bridge_ports

	if [ -r "$iface.bridge_ports" ]; then
		bridge_ports="$(cat_collapsing_whitespace "$iface.bridge_ports")"
		echo "bridge_ports_$family=\"$bridge_ports\"" >> "$conf"
	fi
	if [ -r "$iface.bond_slaves" ]; then
		bond_slaves="$(cat_collapsing_whitespace "$iface.bond_slaves")"
		echo "bond_slaves_$family=\"$bond_slaves\"" >> "$conf"
	fi
	if [ -r "$iface.raw_device" ]; then
		raw_device="$(cat_collapsing_whitespace "$iface.raw_device")"
		echo "raw_device_$family=\"$raw_device\"" >> "$conf"
	fi

	while ! ( ([ "$family" = "IPv4" ] && ipcalc -s -m "$address/0" >/dev/null 2>&1) || ([ "$family" = "IPv6" ] && contains "$address" ":") ); do
		address=${default_address:-$(get_default_addr "$iface" "$family")}
		[ -z "$address" ] && address="$default_method"
		local method extra_options=""
		for method in $extra_methods; do
			extra_options="$extra_options'$method', "
		done
		ask "$family address for $iface? (or $extra_options'dhcp', 'none', '?')" "$address"
		address=$resp
		case "$resp" in
		'?')	ipaddr_help;;
		"abort") return;;
		auto|dhcp)
			echo "type_$family=$resp" >> "$conf"
			unconfigured_del "$iface"
			return ;;
		"none")
			echo "type_$family=manual" >> "$conf"
			unconfigured_del "$iface"
			return;;
		br[0-9]*|bridge[0-9]*)
			case "$iface" in
				# we dont allow bridge bridges
				br[0-9]*|bridge[0-9]*) continue;;
			esac
			bridge_add_port "$resp" "$iface"
			return ;;
		esac
	done

	# extract netmask if entered together with address
	local address_without_netmask="${address%%/*}"
	local netmask=""
	if contains "$address" "/"; then
		netmask="${address##*/}"
	fi

	while [ -z "$netmask" ] || [ "$netmask" -lt 0 ] || [ "$netmask" -gt "$max_mask_size" ]; do
		netmask=$default_mask_size
		ask "Netmask?" "$netmask"
		netmask=$resp
		[ "$netmask" = "abort" ] && return
	done

	while ! ( ([ "$family" = "IPv4" ] && ipcalc -s -m "$gateway/0" >/dev/null 2>&1) || ([ "$family" = "IPv6" ] && contains "$gateway" ":") ); do
		gateway=$(get_default_gateway "$iface" "$family")
		[ -z "$gateway" ] && gateway=none
		ask "Gateway? (or 'none')" "$gateway"
		gateway=$resp
		[ "$gateway" = "abort" ] && return
		[ "$gateway" = "none" ] && gateway=""
		[ -z "$gateway" ] && break
	done

	echo "$family configuration for $iface:"
	{
		echo "type_$family=static"
		echo "address_$family=$address_without_netmask/$netmask"
		echo "gateway_$family=$gateway"
	} | tee -a "$conf" | sed 's/^/  /' | sed "s/_$family//"

	unconfigured_del "$iface"
}

is_bridge() {
	[ -e "$ROOT/sys/class/net/$1/bridge" ] || [ -f "$1.bridge_ports" ]
}

is_bond_master() {
	[ -f "$1.bond_slaves" ]
}

unconfigured_available() {
	local i iflist
	for i in $(unconfigured_list); do
		if ! is_bridge "$i" && ! is_bond_master "$i"; then
			iflist="${iflist}${iflist:+ }$i"
		fi
	done
	echo "$iflist"
}

unconfigured_all_are() {
	local i=
	for i; do
		unconfigured_isin "$i" || return 1
	done
	return 0
}

config_bridge() {
	local bridge="$1" iflist i ports
	while ! unconfigured_all_done; do
		# shellcheck disable=SC2046
		set -- $(unconfigured_available)
		[ $# -eq 0 ] && return 0;
		ports=$(bridge_list_ports "$bridge")
		if [ -n "$ports" ]; then
			echo "Bridge ports in $bridge are: $ports"
		fi
		echo "Available bridge ports are: $*"
		ask "Which port(s) do you want add to bridge $bridge? (or 'done')" "$1"
		case $resp in
			'abort') return 1;;
			'done') return 0;;
		esac
		for i in $resp; do
			if unconfigured_isin "$i"; then
				bridge_add_port "$bridge" "$i"
			else
				echo "$i is not valid"
			fi
		done
	done
}

bond_add_slave() {
	local master="$1" slave
	shift
	for slave; do
		echo "$slave" >> "$master.bond_slaves"
		unconfigured_add "$master"
		unconfigured_del "$slave"
	done
}

bond_list_slaves() {
	if [ -r "$1.bond_slaves" ]; then
		cat_collapsing_whitespace "$1.bond_slaves"
	fi
}

config_bond() {
	local master="$1" slaves=
	while ! unconfigured_all_done; do
		# shellcheck disable=SC2046
		set -- $(unconfigured_available)
		[ $# -eq 0 ] && return 0;
		slaves=$(bond_list_slaves "$master")
		if [ -n "$slaves" ]; then
			echo "Bond slaves in $master are: $slaves"
		fi
		echo "Available bond slaves are: $*"
		ask "Which slave(s) do you want add to $master? (or 'done')" "$1"
		case $resp in
			'abort') return 1;;
			'done') return 0;;
		esac
		for i in $resp; do
			if unconfigured_isin "$i"; then
				bond_add_slave "$master" "$i"
			else
				echo "$i is not valid"
			fi
		done
	done
}

config_vlan() {
	local iface="$1" raw_device
	case $iface in
	*.*)
		raw_device=${iface%.*}
		;;
	vlan*)
		ask_which "raw device" "do you want use for $iface" "$(unconfigured_list)"
		echo "$resp" > "$iface.raw_device"
		return 0
		;;
	esac
	if unconfigured_isin "$raw_device" || is_bond_master "$raw_device"; then
		return 0
	fi
	echo "$raw_device is not a valid raw device for $iface"
	return 1
}

usage() {
	cat <<-EOF
		usage: setup-interfaces [-abhir] [-p ROOT] [-w FILE]

		Setup network interfaces

		options:
		 -a  Automatic interface setup using DHCP
		 -b  Ask for bridging of interfaces
		 -h  Show this help
		 -i  Read new contents of ${ROOT}/etc/network/interfaces from stdin
		 -p  Set the system root to operate in
		 -r  Restart the networking service after the setup
		 -w  Import FILE as ${ROOT}etc/wpa_supplicant/wpa_supplicant.conf
	EOF
	exit "$1"
}

iface_help() {
	cat <<-EOF

		Select the interface you wish to configure.

		For advanced configurations, you can also enter:
		br[0-9]+	(ex: br0)	bridge interface
		bridge[0-9]+	(ex: bridge0)	bridge interface
		bond[0-9]+	(ex: bond32)	bonded interface
		vlan[0-9]+	(ex: vlan371)	vlan interface
		eth?.[0-9]+	(ex: eth0.371)	vlan interface
		bond?.[0.9]+	(ex: bond0.371)	vlan interface

		You will be asked which physical interface(s) to
		use for advanced configurations.

		Select 'none' to leave configuration unmodified.

	EOF
}
prompt_for_interfaces() {
	init_tmpdir TMP

	cd_assert "$TMP"
	unconfigured_detect

	index=1
	while ! unconfigured_all_done; do
		echo "Available interfaces are: $(unconfigured_list)."
		echo "Enter '?' for help on bridges, bonding and vlans."
		ask "Which one do you want to initialize? (or '?' or 'done')" "$(unconfigured_get_first)"
		iface="$resp"

		case "$iface" in
			"none") exit;;
			"done") break;;
			'?') iface_help; continue;;
			br[0-9]*|bridge[0-9]*|virbr[0-9]*)
				config_bridge "$iface" || continue;;
			bond[0-9]*.[0-9]*)
				config_bond "${iface%.*}" || continue
				config_iface "${iface%.*}" "$(printf "%.3d~" $index)" none
				index=$(( index + 1 ))
				config_vlan "$iface" || continue
				;;
			bond[0-9]*)
				config_bond "$iface" || continue;;
			*.[0-9]*|vlan[0-9]*)
				config_vlan "$iface" || continue;;
			*) unconfigured_isin "$iface" || continue;;
		esac
		config_iface "$iface" "$(printf "%.3d~" $index)"
		index=$(( index + 1 ))
	done

	if [ "$(openrc --sys)" != "LXC" ] || ! ip addr show lo | grep -q 'inet.*127\.0'; then
		echo "type_IPv4=loopback" > 000~lo.conf
		if is_ipv6_enabled; then
			echo "type_IPv6=loopback" >> 000~lo.conf
		fi
	fi

	for path in *.conf ; do
		iface=$(basename "$path" .conf)
		iface=${iface#[0-9]*~}
		type_IPv4=
		address_IPv4=
		gateway_IPv4=
		bridge_ports_IPv4=
		bond_slaves_IPv4=
		raw_device_IPv4=
		type_IPv6=
		address_IPv6=
		gateway_IPv6=
		bridge_ports_IPv6=
		bond_slaves_IPv6=
		raw_device_IPv6=
		# shellcheck disable=SC1090
		. "./$path"
		echo "auto $iface" >> interfaces
		render_iface_family inet "$type_IPv4" "$address_IPv4" "$gateway_IPv4" "$bridge_ports_IPv4" "$bond_slaves_IPv4" "$raw_device_IPv4"
		render_iface_family inet6 "$type_IPv6" "$address_IPv6" "$gateway_IPv6" "$bridge_ports_IPv6" "$bond_slaves_IPv6" "$raw_device_IPv6"
		case "$iface" in
			*.[0-9]*|vlan[0-9]*)
				if ! [ -f "$ROOT/usr/libexec/ifupdown-ng/link" ]; then
					PKGS="$PKGS vlan"
				fi
				;;
		esac
		echo "" >> interfaces
	done

	if ask_yesno "Do you want to do any manual network configuration? (y/n)" n; then
		case "$EDITOR" in
			nano)	apk add nano;;
			vim)	apk add vim;;
		esac
		${EDITOR:-vi} interfaces
	fi

	if [ -n "$PKGS" ]; then
		# shellcheck disable=SC2086
		apk add --quiet $PKGS
	fi

	mkdir -p "$ROOT/etc/network"
	cp interfaces "$ROOT/etc/network/"
}

render_iface_family() {
	local family_type="$1"
	local type="$2"
	local address="$3"
	local gateway="$4"
	local bridge_ports="$5"
	local bond_slaves="$6"
	local raw_device="$7"
	if [ -z "$type" ]; then
		return
	fi
	echo "iface $iface $family_type $type" >> interfaces
	if [ -n "$bridge_ports" ]; then
		PKGS="$PKGS bridge"
		printf "\tbridge-ports %s\n" "$bridge_ports" >> interfaces
	fi
	if [ -n "$bond_slaves" ]; then
		PKGS="$PKGS bonding"
		printf "\tbond-slaves %s\n" "$bond_slaves" >> interfaces
	fi
	if [ -n "$raw_device" ]; then
		printf "\tvlan-raw-device %s\n" "$raw_device" >> interfaces
	fi
	case $type in
	manual)
		printf "\tup ip link set \$IFACE up\n" >> interfaces
		printf "\tdown ip link set \$IFACE down\n" >> interfaces
		;;
	static)
		printf "\taddress %s\n" "$address" >> interfaces
		[ "$gateway" ] \
			&& printf "\tgateway %s\n" "$gateway" >> interfaces
		;;
	esac
}

find_first_iface_up() {
	local n=0
	[ $# -eq 0 ] && return
	while [ $n -le "${SETUP_INTERFACES_LINK_WAIT_MAX:-11}" ]; do
		for i in "$@"; do
			if [ "$(cat "$ROOT/sys/class/net/$i/operstate" 2>/dev/null)" = "up" ]; then
				echo "$i"
				return
			fi
		done
		sleep 0.1
		n=$((n+1))
	done
}

auto_setup() {
	local iface
	# shellcheck disable=SC2046
	set -- $(available_ifaces)
	if [ $# -eq 0 ]; then
		return
	fi
	for iface in "$@"; do
		$MOCK ip link set dev "$iface" up
	done
	iface="$(find_first_iface_up "$@")"
	if [ -z "$iface" ]; then
		iface="$1"
	fi

	# we will likely use the found interface later so lets keep it up
	for i in "$@"; do
		if [ "$i" != "$iface" ]; then
			$MOCK ip link set dev "$i" down
		fi
	done

	if is_ipv6_enabled; then
		cat > "$ROOT/etc/network/interfaces" <<-EOF
		auto lo
		iface lo inet loopback
		iface lo inet6 loopback

		auto $iface
		iface $iface inet dhcp
		iface $iface inet6 auto
		EOF
	else
		cat > "$ROOT/etc/network/interfaces" <<-EOF
		auto lo
		iface lo inet loopback

		auto $iface
		iface $iface inet dhcp
		EOF
	fi

}

import_wifi_config() {
	local wifiopts="$1"
	local conffile="$ROOT/etc/wpa_supplicant/wpa_supplicant.conf"

	[ -n "$wifiopts" ] || return 0
	[ -f "$wifiopts" ] || die "$wifiopts: wpa_supplicant config not found"

	apk add --quiet --no-progress wpa_supplicant
	rc-update --quiet add wpa_supplicant boot
	mkdir -p "${conffile%/*}"
	cp -f "$wifiopts" "$conffile"
}

ask_bridge=
is_xen_dom0 && ask_bridge=1

while getopts "abhip:rw:" opt; do
	case $opt in
		a) auto=1;;
		b) ask_bridge=1;;
		h) usage 0;;
		i) STDINPUT=1;;
		p) ROOT=$OPTARG;;
		r) restart=1;;
		w) WIFIOPTS=$OPTARG;;
		'?') usage "1" >&2;;
	esac
done

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

mkdir -p "$ROOT/etc/network"
if [ "$STDINPUT" = "1" ]; then
	cat > "$ROOT/etc/network/interfaces"
elif [ -n "$auto" ]; then
	auto_setup
else
	prompt_for_interfaces
fi

import_wifi_config "$WIFIOPTS"

if [ -n "$restart" ]; then
	rc-service networking --quiet restart >/dev/null
fi
