#!/bin/sh

# SPDX-FileCopyrightText: Copyright 2022-2026, macmpi
# SPDX-License-Identifier: MIT

ALHB_VERSION="2.0"

_preserve() {
# Create a back-up of element (file, folder, symlink).
	[ -z "${1}" ] && return 1
	[ -e "${1}" ] && cp -a "${1}" "${1}".orig
}

_restore() {
# Remove element (file, folder, symlink) and replace by
# previous back-up if available.
# shellcheck disable=SC2317  # known special case
	[ -z "${1}" ] && return 1
# shellcheck disable=SC2317  # known special case
	rm -rf "${1}"
# shellcheck disable=SC2317  # known special case
	[ -e "${1}".orig ] && mv -f "${1}".orig "${1}"
}

# shellcheck disable=SC2142  # known special case
alias _logger='logger -st "${0##*/}"'

_setup_sshd() {
## Setup temporary SSH server (root login, no password):
# we use some bundled (or optionaly provided) keys to avoid generation at startup and save time.
apk add openssh-server --virtual alhb_ssh --force-non-repository
# Preserve sshd-session & al binaries before uninstall
[ -d /usr/lib/ssh ] && cp -a /usr/lib/ssh /tmp/.ALHB/.

_preserve "/etc/conf.d/sshd"

cat <<-EOF >/etc/ssh/sshd_config.d/alhb.conf
	PermitRootLogin yes
	Banner /tmp/.ALHB/banner
	EOF

# Client authorized_keys or no authentication.
if [ -f /root/.ssh/authorized_keys ] || install -m600 "${ovlpath}"/authorized_keys /tmp/.ALHB/authorized_keys >/dev/null 2>&1; then
	_logger "Enabling public key SSH authentication..."
	cat <<-EOF >>/etc/ssh/sshd_config.d/alhb.conf
			AuthenticationMethods publickey
			EOF
	[ -f  /root/.ssh/authorized_keys ] || cat <<-EOF >>/etc/ssh/sshd_config.d/alhb.conf
		AuthorizedKeysFile /tmp/.ALHB/authorized_keys
		# relax strict mode as authorized_keys are inside /tmp
		StrictModes no
		EOF
else
	_logger "No SSH authentication."
	cat <<-EOF >>/etc/ssh/sshd_config.d/alhb.conf
		AuthenticationMethods none
		PermitEmptyPasswords yes
		EOF
fi

# Define sshd-session & al files new location into sshd_config
for f in /tmp/.ALHB/ssh/sshd-*; do
	[ -e "$f" ] || continue # protect failing glob
	name=$(basename "$f" | cut -c6-)
	# shellcheck disable=SC2018,SC2019
	initial=$(echo "$name" | cut -c1 | tr 'a-z' 'A-Z')
	final=$(echo "$name" | cut -c2-)
	echo "Sshd${initial}${final}Path $f" >>/etc/ssh/sshd_config.d/alhb.conf
done

# Server keys: inject optional custom keys, or generate new (might be stored),
# or use bundeled ones (not stored)
local keygen_stance="sshd_disable_keygen=yes"
if install -m600 "${ovlpath}"/ssh_host_*_key* /etc/ssh/ >/dev/null 2>&1; then
	# Check for empty key within injected ones: if found, generate new keys.
	if find /etc/ssh/ -maxdepth 1 -type f -name 'ssh_host_*_key*' -empty | grep -q .; then
		rm -f /etc/ssh/ssh_host_*_key*
		keygen_stance=""
		_logger "Will generate new SSH keys..."
	else
		chmod 644 /etc/ssh/ssh_host_*_key.pub
		_logger "Using injected SSH keys..."
	fi
else
	_logger "Using bundled ssh keys from RAM..."
	cat <<-EOF >>/etc/ssh/sshd_config.d/alhb.conf
		HostKey /tmp/.ALHB/ssh_host_ed25519_key
		HostKey /tmp/.ALHB/ssh_host_rsa_key
		EOF
fi

echo "$keygen_stance" >>/etc/conf.d/sshd

rc-service sshd restart
}

_updt_apkovl() {
## Update apkovl overlay file & eventually reboot
# URL redirects to apkovl file on github master: is.gd shortener provides basic analytics.
# Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_master
# Privacy policy: https://is.gd/privacy.php
local updt_status="failed, keeping original version"
local sha_url="https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz.sha512"
local file_url="https://is.gd/apkovl_master"

# Ensure system date is correct to allow SSL transactions
ntpd -N -p pool.ntp.org -n -q

_is_ro && mount -o remount,rw "${ovlpath}"
if wget -q -O "${ovl}_new" -T 10 "$file_url" >/dev/null 2>&1 && \
	wget -q -O /tmp/sha -T 10 "$sha_url" >/dev/null 2>&1 && \
	[ "$( sha512sum "${ovl}_new" | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ]; then
		mv -f "${ovl}_new" "${ovl}"
		updt_status="successful"
fi
rm -f "${ovl}_new" /tmp/sha
_is_ro && mount -o remount,ro "${ovlpath}"
_logger "Update $updt_status"

if [ "$updt_status" = "successful" ]; then
	printf '%s\n\n' "Updated (Read release notes!)" >>/tmp/.ALHB/banner
else
	printf '\n' >>/tmp/.ALHB/banner
	return 1
fi
# Reboot if specified in auto-updt file (and no ssh session ongoing nor unattended.sh script available).
! pgrep -a -P "$( cat /run/sshd.pid 2>/dev/null )" 2>/dev/null | grep -q "sshd: root@pts" && \
	! [ -f /tmp/headless_unattended ] && \
		grep -q "^reboot$" "${ovlpath}"/auto-updt && \
			_logger "Will reboot in 3sec..." && sleep 3 && reboot
exit 0
}

_tst_version() {
## Compare current version with latest online, notify & eventally calls for update
# URL redirects to github project page: is.gd shortener provides basic analytics.
# Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_run
# Privacy policy: https://is.gd/privacy.php
local vers=""
local ref="/macmpi/alpine-linux-headless-bootstrap/releases/tag/v"
local url="https://is.gd/apkovl_run"

if wget -q -O /tmp/homepg -T 10 --no-check-certificate "$url" >/dev/null 2>&1; then
	_logger "Internet access: success"
	vers="$( grep -o "$ref.*\"" /tmp/homepg | grep -Eo '[0-9]+[\.[0-9]+]*' )"
	rm -f /tmp/homepg
	if [ -n "$vers" ] && ! [ "$vers" = "$ALHB_VERSION" ]; then
		vers="!! Version $vers is available on Github project page !!"
		_logger "$vers"
		printf '%s\n' "$vers" >>/tmp/.ALHB/banner
		# Optionally update apkovl if key-file allows it.
		if [ -f "${ovlpath}"/auto-updt ]; then
			_logger "Updating overlay file..."
			_updt_apkovl &
		else
			_logger "(check doc to enable auto-update)"
			printf '%s\n\n' "(check doc to enable auto-update)" >>/tmp/.ALHB/banner
		fi
	fi
else
	_logger "Internet access: failed"
fi
}

_setup_networking() {
## Setup network interfaces.
# Determine if wlan is to be used (list interfaces, and check wpa_supplicant.conf)
wlan_lst="$( find /sys/class/net/*/phy80211 -maxdepth 0 -exec \
	sh -c 'printf %s\| "$( basename "$( dirname "$0" )" )"' {} \; 2>/dev/null )"
wlan_lst="${wlan_lst%\|}"
[ -n "$wlan_lst" ] && [ -f "${ovlpath}"/wpa_supplicant.conf ]
has_wifi=$?
_has_wifi() { return "$has_wifi"; }

_preserve "/etc/network/interfaces"
if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces >/dev/null 2>&1; then
	_logger "No interfaces file supplied, building defaults..."
	cat <<-EOF >/etc/network/interfaces
		auto lo
		iface lo inet loopback

		EOF
	for dev in /sys/class/net/*; do
		[ -e "$dev" ] || continue # protect failing glob
		# shellcheck disable=SC2034  # Unused IFINDEX while still sourced from uevent.
		local DEVTYPE INTERFACE IFINDEX
		DEVTYPE=""
		# shellcheck source=/dev/null
		. "$dev"/uevent
		case ${INTERFACE%%[0-9]*} in
			lo)
				;;
			eth)
				cat <<-EOF >>/etc/network/interfaces
					auto $INTERFACE
					iface $INTERFACE inet dhcp

					EOF
				;;
			*)
				# According to below we could rely on DEVTYPE for wlan devices
				# https://lists.freedesktop.org/archives/systemd-devel/2014-January/015999.html
				# but...some wlan might still be ill-behaved: use wlan_lst
				# shellcheck disable=SC3060  # ash does support string replacement.
				_has_wifi && ! [ "${wlan_lst/$INTERFACE/}" = "$wlan_lst" ] && \
					cat <<-EOF >>/etc/network/interfaces
						auto $INTERFACE
						iface $INTERFACE inet dhcp

						EOF
				# Ensure considered gadget interface is actually the connected one (may have several).
				[ "$DEVTYPE" = "gadget" ] && \
					grep -vwq "0" /sys/class/net/"$INTERFACE"/carrier_up_count && \
							cat <<-EOF1 >>/etc/network/interfaces && cat <<-EOF2 >/etc/resolv.conf
								auto $INTERFACE
								iface $INTERFACE inet static
								    address 10.42.0.2/24
								    gateway 10.42.0.1

								EOF1
								nameserver 1.1.1.1
								nameserver 1.0.0.1

								EOF2
				;;
		esac
	done
fi

echo "###################################"
echo "Using following network interfaces:"
cat /etc/network/interfaces
echo "###################################"

if _has_wifi && grep -qE "$wlan_lst" /etc/network/interfaces; then
	_logger "Configuring wifi..."
	apk add wpa_supplicant --virtual alhb_wpa --force-non-repository
	_preserve "/etc/wpa_supplicant/wpa_supplicant.conf"
	install -m600 "${ovlpath}"/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf
	rc-service wpa_supplicant restart
else
	_logger "No wifi interface or SSID/pass file supplied"
fi

_preserve "/etc/hostname"
echo "alpine-headless" >/etc/hostname
hostname -F /etc/hostname

rc-service networking restart
}


#############################################################################
## Main

# Redirect stdout and errors to logfile as service won't show messages
exec 1>>/tmp/alhb.log 2>&1
_logger "Alpine Linux headless bootstrap v$ALHB_VERSION by macmpi"

# Temporarily put default runlevel away so services do not conflict (renaming won't work)
mv /etc/runlevels/default /tmp/.
rm -f /tmp/default/firstboot # initially set by initramfs, will be started after we start networking

# Determine ovl file location.
# Grab used ovl filename from dmesg.
ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )"
if [ -f "${ovl}" ]; then
	ovlpath="$( dirname "$ovl" )"
else
	# Search path again as mountpoint have been changed later in the boot process...
	ovl="$( basename "${ovl}" )"
	ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 )
	ovl="${ovlpath}/${ovl}"
fi

# Detect apkovl volume ro/rw state
grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$?
_is_ro() { return "$is_ro"; }

# Create banner file.
warn=""
_is_ro && warn="(remount partition rw!)"
cat <<-EOF >/tmp/.ALHB/banner

	Alpine Linux headless bootstrap v$ALHB_VERSION by macmpi

	You may want to delete/rename .apkovl file before reboot ${warn}:
	${ovl}
	(can be done automatically with unattended script - see sample snippet)


	EOF

## Setup USB gadget ports if any connected
my_vol="$( df | grep "$ovlpath" | awk '{print $1}' )"
/tmp/.ALHB/xg_multi -V "$my_vol" >/dev/null 2>&1

# Set gadget serial port when available
if [ -c /dev/ttyGS0 ]; then
	# Enabling terminal login
	# no choice than making permanent change to /etc/securetty (Alpine 3.19 already has ttyGS0).
	grep -q "ttyGS0" /etc/securetty || echo "ttyGS0" >>/etc/securetty
	/sbin/getty -L -w -f /tmp/.ALHB/banner 115200 /dev/ttyGS0 vt100 &
fi

if install -m755 "${ovlpath}"/unattended.sh /tmp/headless_unattended >/dev/null 2>&1; then
	cat <<-EOF >/etc/init.d/headless_unattended
		#!/sbin/openrc-run

		# SPDX-FileCopyrightText: Copyright 2022-2026, macmpi
		# SPDX-License-Identifier: MIT

		description="Headless unattended setup script (optional)"
		name="Headless unattended"

		command="/tmp/headless_unattended"
		command_background=true
		pidfile="/run/headless_unattended.pid"
		EOF
	chmod +x /etc/init.d/headless_unattended
	rc-update --update
fi

# Help randomness for wpa_supplicant and sshd (urandom until 3.16).
rc-service seedrng restart || rc-service urandom restart

_setup_networking

service firstboot start
# let firstboot settle if needed (WARNING may block if wget does not timeout in getting keys!)
while ! service firstboot status | grep -q started; do sleep 0.1; done

# Test latest available version online.
# Can be skipped by creating a 'opt-out'-named dummy file aside apkovl file.
[ -f "${ovlpath}"/opt-out ] || _tst_version &

# Setup sshd unless unattended.sh script prevents it.
grep -q "^#NO_SSH$" /tmp/headless_unattended >/dev/null 2>&1 \
	|| _setup_sshd

_logger "Initial setup done, cleaning-up now..."
# let sshd settle if needed
if command -v sshd >/dev/null 2>&1; then
	while ! service sshd status | grep -q started; do sleep 0.1; done
	/tmp/.ALHB/tiny-mdns-ann --ssh &
else
	/tmp/.ALHB/tiny-mdns-ann &
fi
rm -f /etc/ssh/sshd_config.d/alhb.conf
_restore "/etc/conf.d/sshd"
apk del alhb_ssh 2>/dev/null

# let wpa_supplicant settle if needed
if command -v wpa_supplicant >/dev/null 2>&1; then
	while ! service wpa_supplicant status | grep -q started; do sleep 0.1; done
fi
_restore "/etc/wpa_supplicant/wpa_supplicant.conf"
apk del alhb_wpa 2>/dev/null

_restore "/etc/network/interfaces"
_restore "/etc/hostname"

# remove brcmfmac options file
rm -f /etc/modprobe.d/alhb-brcmfmac.conf

# bring back default runlevel
mv /tmp/default /etc/runlevels/.

# Remove from runlevels to avoid spurious openrc recalls from unattended script.
rm -f /etc/runlevels/*/headless_bootstrap

# Run unattended script if available.
if [ -f /tmp/headless_unattended ]; then
	_logger "Starting headless_unattended service"
	rc-service headless_unattended start
fi

# clear all headless services
rm -f /etc/init.d/headless_*

_logger "Clean-up done, enjoy !"

exit 0

