Files
archy/scripts/lib/installer-owner.sh
T
Dorian f397661e3f Integrate accepted RC6 installer for release 2.1.23
Exact reviewed integration tree from 7bf8c8d8e9da45c533a73e59ade18df37eab9ed5.
Preserves installer branch fe383efe70 and its history.
Incorporates Gitea PR #168 with source UI approval and completed source regressions.
The canonical proposal uses one parent to avoid ngit's ten-commit submission guard; no source changes or history rewriting.
2026-11-14 06:58:59 -05:00

253 lines
11 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Sourced by the live ISO installer. Keep owner secrets in shell memory only.
installer_valid_user() {
[[ "$1" =~ ^[a-z][a-z0-9_-]{1,30}$ ]] &&
[[ "$1" != archipelago && "$1" != root && "$1" != nobody ]]
}
installer_valid_hostname() {
[[ ${#1} -le 63 && "$1" =~ ^[a-z][a-z0-9-]*[a-z0-9]$ ]] &&
[[ "$1" != localhost && "$1" != archipelago ]]
}
installer_owner_header() {
local rows top
clear
rows=$(tput lines 2>/dev/null || printf '24')
top=$(( (rows - 20) / 2 ))
(( top < 0 )) && top=0
(( top > 8 )) && top=8
printf '%*s' "$top" '' | tr ' ' '\n'
printf '%s' "${ORANGE:-}"
printf ' ▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█\n'
printf ' █▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █\n'
printf ' ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀\n'
printf '%s' "${NC:-}"
printf '\n'
printf ' SET UP YOUR ARCHIPELAGO\n'
printf ' Your own account, your own node\n'
printf '\n'
}
installer_wired_link() {
local path name
for path in /sys/class/net/*; do
name=${path##*/}
[[ "$name" == lo || -d "$path/wireless" ]] && continue
[[ -r "$path/carrier" && $(cat "$path/carrier" 2>/dev/null) == 1 ]] && return 0
done
return 1
}
installer_wifi_skip() {
printf ' Continuing without Wi-Fi. You can set it up later in the web UI: Server > WiFi.\n'
}
installer_wifi_safe_name() {
local LC_ALL=C.UTF-8
[[ -n "$1" && ! "$1" =~ [[:cntrl:]] ]] || return 1
# Direction overrides can make one network appear to be another in the
# installer. Ordinary Unicode names and spaces remain readable.
case "$1" in
*$'\u061c'*|*$'\u200e'*|*$'\u200f'*|*$'\u202a'*|*$'\u202b'*|\
*$'\u202c'*|*$'\u202d'*|*$'\u202e'*|*$'\u2066'*|*$'\u2067'*|\
*$'\u2068'*|*$'\u2069'*) return 1 ;;
esac
}
# BSSID-first records make a newline inside an SSID an invalid extra row. Check
# against a separate BSSID-only listing before offering any numbered choice;
# a malformed scan must never turn attacker-controlled SSID text into a menu.
installer_wifi_scan() {
local interface=$1 bssid_output record_output bssid record ssid index
local -a bssids records
local -A seen=()
networks=()
bssid_output=$(nmcli -t --escape no -f BSSID device wifi list ifname "$interface" --rescan no && printf '\036') || return 1
record_output=$(nmcli -t --escape no -f BSSID,SSID device wifi list ifname "$interface" --rescan no && printf '\036') || return 1
bssid_output=${bssid_output%$'\036'}
record_output=${record_output%$'\036'}
# nmcli terminates each row with one newline. Preserve any additional
# newline from an SSID so it becomes an invalid record below.
bssid_output=${bssid_output%$'\n'}
record_output=${record_output%$'\n'}
[[ -z "$bssid_output" && -z "$record_output" ]] && return 0
[[ -n "$bssid_output" && -n "$record_output" ]] || return 1
mapfile -t bssids <<< "$bssid_output"
mapfile -t records <<< "$record_output"
((${#bssids[@]} == ${#records[@]})) || return 1
for index in "${!records[@]}"; do
bssid=${bssids[index]}
record=${records[index]}
[[ "$bssid" =~ ^([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}$ &&
"${record:0:17}" == "$bssid" && "${record:17:1}" == : ]] || return 1
ssid=${record:18}
installer_wifi_safe_name "$ssid" || continue
[[ -n "${seen[$ssid]:-}" ]] && continue
seen[$ssid]=1
networks+=("$ssid")
done
}
installer_collect_wifi() {
INSTALLER_WIFI_UUID=''
INSTALLER_WIFI_PROFILE=''
local answer interface ssid profile choice index
local -a networks
if installer_wired_link; then
printf ' Wired link detected.\n'
read -r -p ' Set up Wi-Fi as well? [y/N] ' answer || return 1
[[ ! "$answer" =~ ^[Yy]$ ]] && return 0
else
printf ' No wired link detected. An offline install is still possible.\n'
read -r -p ' Set up Wi-Fi now? [Y/n] ' answer || return 1
[[ "$answer" =~ ^[Nn]$ ]] && return 0
fi
if ! command -v nmcli >/dev/null || ! systemctl start NetworkManager; then
printf ' Wi-Fi manager is unavailable.\n' >&2
installer_wifi_skip
return 0
fi
if ! nmcli radio wifi on; then
printf ' Wi-Fi could not be enabled.\n' >&2
installer_wifi_skip
return 0
fi
interface=$(nmcli -t -f DEVICE,TYPE device status | awk -F: '$2 == "wifi" { print $1; exit }') || interface=''
if [[ -z "$interface" ]]; then
printf ' No supported Wi-Fi adapter was found.\n' >&2
installer_wifi_skip
return 0
fi
while :; do
nmcli device wifi rescan ifname "$interface" >/dev/null 2>&1 || true
printf '\n Available Wi-Fi networks:\n'
if ! installer_wifi_scan "$interface"; then
networks=()
printf ' Wi-Fi scan could not be displayed safely. Rescan, enter a network name, or skip.\n' >&2
fi
for index in "${!networks[@]}"; do
printf ' %2d. %s\n' "$((index + 1))" "${networks[index]}"
done
printf ' Choose a number, M for a hidden network, R to rescan, or S to skip.\n'
read -r -p ' Wi-Fi choice: ' choice || return 1
case "$choice" in
[Ss]) installer_wifi_skip; return 0 ;;
[Rr]) continue ;;
[Mm])
read -r -p ' Wi-Fi network name (SSID): ' ssid || return 1
[[ -n "$ssid" && "$ssid" != *$'\n'* && "$ssid" != *$'\r'* ]] || {
printf ' Enter a network name.\n'; continue;
}
;;
*)
[[ "$choice" =~ ^[0-9]+$ ]] || { printf ' Choose a listed number.\n'; continue; }
index=$((10#$choice - 1))
(( index >= 0 && index < ${#networks[@]} )) || {
printf ' Choose a listed number.\n'; continue;
}
ssid=${networks[index]}
;;
esac
profile="archipelago-install-$$"
printf ' Enter the Wi-Fi password at the hidden prompt if asked.\n'
if ! nmcli --ask device wifi connect "$ssid" ifname "$interface" name "$profile"; then
nmcli connection delete "$profile" >/dev/null 2>&1 || true
printf ' Wi-Fi connection failed. Choose another network or skip.\n' >&2
continue
fi
INSTALLER_WIFI_UUID=$(nmcli -g connection.uuid connection show "$profile") || INSTALLER_WIFI_UUID=''
INSTALLER_WIFI_PROFILE=$(grep -l -F -x "uuid=$INSTALLER_WIFI_UUID" \
/etc/NetworkManager/system-connections/*.nmconnection 2>/dev/null | head -n 1)
if [[ ! "$INSTALLER_WIFI_UUID" =~ ^[0-9a-fA-F-]{36}$ ||
-z "$INSTALLER_WIFI_PROFILE" || ! -f "$INSTALLER_WIFI_PROFILE" ||
-L "$INSTALLER_WIFI_PROFILE" ]]; then
printf ' Wi-Fi connected, but its profile was not saved for reboot. Choose another network or skip.\n' >&2
INSTALLER_WIFI_UUID=''; INSTALLER_WIFI_PROFILE=''
continue
fi
printf ' Wi-Fi connected. This connection will be available after reboot.\n'
return 0
done
}
installer_collect_owner() {
installer_owner_header
printf ' Create the account used at the machine console.\n\n'
while :; do
read -r -p ' Username: ' INSTALLER_OWNER_USER || return 1
if installer_valid_user "$INSTALLER_OWNER_USER" &&
! getent passwd "$INSTALLER_OWNER_USER" >/dev/null; then break; fi
printf ' Use 2–31 lowercase letters, digits, _ or -; choose a personal name.\n'
done
local proposed pass_again
proposed="archipelago-$(od -An -N3 -tx1 /dev/urandom | tr -d ' \n')"
while :; do
read -r -p " Hostname [$proposed]: " INSTALLER_OWNER_HOSTNAME || return 1
INSTALLER_OWNER_HOSTNAME=${INSTALLER_OWNER_HOSTNAME:-$proposed}
installer_valid_hostname "$INSTALLER_OWNER_HOSTNAME" && break
printf ' Use a hostname of 2–63 lowercase letters, digits or hyphens.\n'
done
while :; do
read -r -s -p ' Password (12+ chars; upper, lower, number, symbol): ' INSTALLER_OWNER_PASSWORD || return 1; printf '\n'
read -r -s -p ' Confirm password: ' pass_again || return 1; printf '\n'
if [[ ${#INSTALLER_OWNER_PASSWORD} -ge 12 && "$INSTALLER_OWNER_PASSWORD" != archipelago &&
"$INSTALLER_OWNER_PASSWORD" =~ [A-Z] && "$INSTALLER_OWNER_PASSWORD" =~ [a-z] &&
"$INSTALLER_OWNER_PASSWORD" =~ [0-9] && "$INSTALLER_OWNER_PASSWORD" =~ [^[:alnum:]] &&
"$INSTALLER_OWNER_PASSWORD" != [[:space:]]* &&
"$INSTALLER_OWNER_PASSWORD" != *[[:space:]] &&
"$INSTALLER_OWNER_PASSWORD" == "$pass_again" ]]; then
break
fi
INSTALLER_OWNER_PASSWORD=''; pass_again=''
printf ' Passwords must match and meet all strength requirements.\n'
done
pass_again=''
printf '\n'
installer_collect_wifi || return 1
printf '\n Account: %s | Hostname: %s\n' "$INSTALLER_OWNER_USER" "$INSTALLER_OWNER_HOSTNAME"
read -r -p ' Continue to disk selection? [Y/n] ' answer || return 1
[[ ! "$answer" =~ ^[Nn]$ ]]
}
installer_install_owner() {
local target=${1:?target root required}
[[ -d "$target/etc" && -n "$INSTALLER_OWNER_USER" && -n "$INSTALLER_OWNER_HOSTNAME" &&
-n "$INSTALLER_OWNER_PASSWORD" ]] || return 1
chroot "$target" useradd -m -s /bin/bash -G sudo -- "$INSTALLER_OWNER_USER" || return 1
if ! printf '%s:%s\n' "$INSTALLER_OWNER_USER" "$INSTALLER_OWNER_PASSWORD" |
chroot "$target" chpasswd >/dev/null 2>&1; then
printf ' Could not set the owner password; installation is incomplete.\n' >&2
return 1
fi
printf '%s\n' "$INSTALLER_OWNER_HOSTNAME" > "$target/etc/hostname" || return 1
cat > "$target/etc/hosts" <<HOSTS || return 1
127.0.0.1 localhost
127.0.1.1 $INSTALLER_OWNER_HOSTNAME
::1 localhost ip6-localhost ip6-loopback
HOSTS
chmod 644 "$target/etc/hosts" || return 1
if [[ -n "${INSTALLER_WIFI_UUID:-}" ]]; then
[[ -n "$INSTALLER_WIFI_PROFILE" && -f "$INSTALLER_WIFI_PROFILE" &&
! -L "$INSTALLER_WIFI_PROFILE" ]] || return 1
install -d -m 700 "$target/etc/NetworkManager/system-connections" || return 1
install -m 600 "$INSTALLER_WIFI_PROFILE" "$target/etc/NetworkManager/system-connections/archipelago-wifi.nmconnection" || return 1
fi
# Stage only an Argon2 verifier. The dashboard account is claimed by the
# same password on the final login screen after seed onboarding.
if ! printf '%s\n' "$INSTALLER_OWNER_PASSWORD" |
chroot "$target" /usr/local/bin/archipelago stage-installer-password-v1 /var/lib/archipelago >/dev/null; then
printf ' Could not stage dashboard login; installation is incomplete.\n' >&2
return 1
fi
[[ -f "$target/var/lib/archipelago/installer-password.json" ]] || return 1
chown 1000:1000 "$target/var/lib/archipelago/installer-password.json" || return 1
install -d -m 755 "$target/etc/archipelago" || return 1
printf '%s\n' "$INSTALLER_OWNER_USER" > "$target/etc/archipelago/owner-account" || return 1
chmod 644 "$target/etc/archipelago/owner-account" || return 1
# The fixed archipelago UID owns services and data, never the human login.
# SSH remains key-only; this account uses its chosen password at the console.
INSTALLER_OWNER_PASSWORD=''
}