#!/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" </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='' }