#!/bin/sh
# Imprion PoS - first-boot configuration (Doc 1 sect. 7 step 1, D27, D33).
#
# Runs once, from imprion-firstboot.service, before sshd and gdm and before
# NetworkManager brings up DHCP (so the DHCP client announces the final
# hostname). Never runs in the live session (ConditionKernelCommandLine).
#
#   (a) generate ssh host keys if none are present (they are never baked
#       into the image - hooks/live/0400 removes them at build time)
#   (b) set the hostname to pos-<suffix>, derived locally from the DMI
#       product serial, falling back to the first physical NIC's MAC and
#       finally to the machine-id
#   (c) drop the completion marker
#
# Idempotent: safe to re-run after deleting /var/lib/imprion/firstboot-done.
# All output goes to the journal (stdout/stderr of a systemd oneshot).
#
# NOTE: log() writes to STDERR, not stdout, and must stay that way.
# derive_hostname() logs while it works and is called as `$(derive_hostname)`,
# so anything it wrote to stdout would be captured into the hostname itself.

set -eu

MARKER_DIR=/var/lib/imprion
MARKER="${MARKER_DIR}/firstboot-done"
MAX_HOSTNAME_LEN=63          # RFC 1035 label limit, and the kernel's limit

log() { echo "imprion-firstboot: $*" >&2; }

# --------------------------------------------------------------------------
# (a) ssh host keys
# --------------------------------------------------------------------------
ensure_ssh_host_keys() {
	# `set -- /etc/ssh/ssh_host_*_key` leaves the literal pattern in $1 when
	# nothing matches, so test for an actual file rather than for $#.
	for _k in /etc/ssh/ssh_host_*_key; do
		if [ -f "${_k}" ]; then
			log "ssh host keys already present, leaving them alone"
			return 0
		fi
	done

	if ! command -v ssh-keygen >/dev/null 2>&1; then
		log "WARNING: ssh-keygen not found, cannot generate host keys"
		return 0
	fi

	log "generating ssh host keys"
	ssh-keygen -A
	# ssh-keygen -A already creates them 0600/0644 root:root; on Debian the
	# private keys additionally get group root, which sshd is happy with.
}

# --------------------------------------------------------------------------
# (b) hostname
# --------------------------------------------------------------------------

# Lowercase, then keep only [a-z0-9]. Anything else (spaces, dots, slashes,
# the "O.E.M." dots) is dropped so the result is always a legal DNS label.
normalise() {
	printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9'
}

# Vendors ship a depressing variety of non-serials in the DMI table.
is_placeholder_serial() {
	case "$1" in
		'' | \
		none | null | na | nil | empty | invalid | unknown | oem | \
		default | defaultstring | systemserialnumber | \
		chassisserialnumber | baseboardserialnumber | \
		tobefilledbyoem | tobefilledbyoem1 | tobefilledbyoem2 | \
		notspecified | notapplicable | notavailable | \
		serialnumber | serial | 0123456789 | 123456789 | 1234567890)
			return 0
			;;
	esac
	# Too short to be a real serial.
	if [ "${#1}" -lt 4 ]; then
		return 0
	fi
	# All the same character: 0000000, xxxxxxxx, ffffffff, ...
	_first=$(printf '%s' "$1" | cut -c1)
	if [ -z "$(printf '%s' "$1" | tr -d "${_first}")" ]; then
		return 0
	fi
	return 1
}

dmi_serial_suffix() {
	_f=/sys/class/dmi/id/product_serial
	[ -r "${_f}" ] || return 1
	_s=$(normalise "$(cat "${_f}" 2>/dev/null || true)")
	is_placeholder_serial "${_s}" && return 1
	printf '%s' "${_s}"
}

# First non-loopback, non-virtual interface, in stable (sorted) name order.
# A NIC backed by real hardware has a /sys/class/net/<if>/device symlink;
# bridges, bonds, veth, docker0, tun/tap and wireguard do not.
mac_suffix() {
	for _d in $(ls -1 /sys/class/net 2>/dev/null | sort); do
		[ "${_d}" = "lo" ] && continue
		[ -e "/sys/class/net/${_d}/device" ] || continue
		[ -r "/sys/class/net/${_d}/address" ] || continue
		_m=$(cat "/sys/class/net/${_d}/address" 2>/dev/null || true)
		case "${_m}" in
			''|00:00:00:00:00:00) continue ;;
		esac
		_m=$(normalise "${_m}")
		[ -n "${_m}" ] || continue
		printf '%s' "${_m}"
		return 0
	done
	return 1
}

# Last resort. /etc/machine-id is cleared at build time (live-build's stock
# 8060 hook) and regenerated once, persistently, on this boot.
machine_id_suffix() {
	[ -s /etc/machine-id ] || return 1
	_m=$(normalise "$(cut -c1-12 /etc/machine-id)")
	[ -n "${_m}" ] || return 1
	printf '%s' "${_m}"
}

derive_hostname() {
	if _sfx=$(dmi_serial_suffix); then
		log "hostname suffix from DMI product serial"
	elif _sfx=$(mac_suffix); then
		log "hostname suffix from first physical NIC MAC (DMI serial unusable)"
	elif _sfx=$(machine_id_suffix); then
		log "hostname suffix from machine-id (no usable DMI serial or MAC)"
	else
		log "WARNING: no usable hostname source, using 'unset'"
		_sfx=unset
	fi

	# Cap so that "pos-" + suffix stays within the label limit. Keep the
	# TAIL of the serial: the distinguishing digits are at the end far more
	# often than at the front.
	_max=$((MAX_HOSTNAME_LEN - 4))
	if [ "${#_sfx}" -gt "${_max}" ]; then
		_sfx=$(printf '%s' "${_sfx}" | tail -c "${_max}")
	fi

	printf 'pos-%s' "${_sfx}"
}

apply_hostname() {
	_new="$1"
	_old=$(cat /etc/hostname 2>/dev/null || echo "")

	if [ "${_old}" = "${_new}" ]; then
		log "hostname already ${_new}"
	else
		log "setting hostname: '${_old}' -> '${_new}'"
		printf '%s\n' "${_new}" > /etc/hostname
		chmod 0644 /etc/hostname
	fi

	# /etc/hosts: keep exactly one 127.0.1.1 line, pointing at the new name.
	# Debian convention; NetworkManager and sudo both rely on it resolving.
	if [ -f /etc/hosts ]; then
		_tmp=$(mktemp /etc/hosts.imprion.XXXXXX)
		grep -v '^[[:space:]]*127\.0\.1\.1[[:space:]]' /etc/hosts > "${_tmp}" || true
		printf '127.0.1.1\t%s\n' "${_new}" >> "${_tmp}"
		chmod 0644 "${_tmp}"
		mv "${_tmp}" /etc/hosts
	else
		{
			printf '127.0.0.1\tlocalhost\n'
			printf '127.0.1.1\t%s\n' "${_new}"
			printf '::1\t\tlocalhost ip6-localhost ip6-loopback\n'
			printf 'ff02::1\t\tip6-allnodes\n'
			printf 'ff02::2\t\tip6-allrouters\n'
		} > /etc/hosts
		chmod 0644 /etc/hosts
	fi

	# Apply to the running kernel. `hostname` (from the essential hostname
	# package) is used rather than hostnamectl on purpose: this unit runs
	# before basic.target's consumers and there is no guarantee that
	# systemd-hostnamed / D-Bus are usable yet.
	hostname "${_new}" || log "WARNING: hostname(1) failed, name applies at next boot"
}

# --------------------------------------------------------------------------
main() {
	log "starting"
	ensure_ssh_host_keys
	apply_hostname "$(derive_hostname)"

	mkdir -p "${MARKER_DIR}"
	chmod 0755 "${MARKER_DIR}"
	date -u '+%Y-%m-%dT%H:%M:%SZ' > "${MARKER}"
	chmod 0644 "${MARKER}"
	log "done"
}

main "$@"
