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.
253 lines
11 KiB
Bash
253 lines
11 KiB
Bash
#!/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=''
|
||
}
|