sebys-hosting-scripts/install-wings.sh
Sebastian Cabrera 12c20e12e8
Strip all whitespace from join-data instead of just the first token
extract_join_data() previously used `awk '{print $1}'` as its final
step, which only keeps the first whitespace-delimited chunk. Since the
join-data token is base64 and never legitimately contains whitespace,
any paste that gets hard-wrapped across multiple lines (long tokens
wrapped by a terminal or the panel UI, or CRLF line endings) was
silently truncated at the first space/newline, even when the raw
input already contained the rest of the token. This produced a valid
but incomplete base64 string that decoded into a config.yml missing
trailing fields such as `remote`, causing wings to fail at startup
with "invalid remote configuration, cannot connect to panel".

Now strips every whitespace character from the token instead of only
keeping the first token, so wrapped/multi-line pastes are captured in
full regardless of how prompt() received them.

Also bumps the installer banner to v1.3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-09 21:19:14 -04:00

320 lines
8.5 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
########################################
# Seby's Calagopus Wings Installer
# curl | bash safe
########################################
########################################
# Styling
########################################
TTY="/dev/tty"
BOLD="\033[1m"
GREEN="\033[32m"
YELLOW="\033[33m"
RED="\033[31m"
BLUE="\033[34m"
RESET="\033[0m"
section() { printf "\n${BLUE}${BOLD}==> %s${RESET}\n" "$*"; }
say() { printf "${GREEN}✔ %s${RESET}\n" "$*"; }
warn() { printf "${YELLOW}⚠ %s${RESET}\n" "$*"; }
err() { printf "${RED}✖ %s${RESET}\n" "$*"; }
have() { command -v "$1" >/dev/null 2>&1; }
need_tty() {
if [[ ! -r "$TTY" || ! -w "$TTY" ]]; then
err "No interactive TTY available."
exit 1
fi
}
prompt() {
local msg="$1"
local out line
printf "${BOLD}%s${RESET}" "$msg" >"$TTY"
IFS= read -r out <"$TTY"
# A pasted value (e.g. join-data copied from the panel UI) can arrive with an
# embedded newline. `read` stops at the first one, which would otherwise
# silently truncate the rest onto a "phantom" line consumed by a later
# prompt. Drain any additional lines that are already buffered so nothing
# gets lost; this never blocks waiting on new keystrokes.
while IFS= read -r -t 0.1 line <"$TTY" 2>/dev/null; do
out+="$line"
done
printf "%s" "$out"
}
confirm_default_no() {
local msg="$1"
local ans
ans="$(prompt "$msg [y/N]: ")"
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]]
}
require_root_or_sudo() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
if have sudo; then
SUDO="sudo"
else
err "Root or sudo required."
exit 1
fi
else
SUDO=""
fi
}
apt_install() {
$SUDO apt-get update -y >/dev/null 2>&1
$SUDO apt-get install -y "$@" >/dev/null 2>&1
}
########################################
# Spinner (writes to /dev/tty so curl|bash stays safe)
########################################
run_with_spinner() {
local label="$1"; shift
# Run the provided command in the background
("$@") &
local pid=$!
local spin='|/-\'
local i=0
# Hide cursor
printf "\033[?25l" >"$TTY" || true
# Spinner loop
while kill -0 "$pid" 2>/dev/null; do
i=$(( (i + 1) % 4 ))
printf "\r${BLUE}${BOLD}..${RESET} %s %s" "${spin:$i:1}" "$label" >"$TTY"
sleep 0.12
done
# Wait for completion + capture exit status
wait "$pid"
local rc=$?
# Clear spinner line + show cursor
printf "\r\033[K" >"$TTY"
printf "\033[?25h" >"$TTY" || true
return "$rc"
}
########################################
# SSL config updater
########################################
update_ssl_config() {
local domain="$1"
local cfg="/etc/calagopus-wings/config.yml"
local cert="/etc/letsencrypt/live/${domain}/fullchain.pem"
local key="/etc/letsencrypt/live/${domain}/privkey.pem"
if [[ ! -f "$cfg" ]]; then
warn "Config not found — skipping SSL update."
return
fi
if [[ ! -f "$cert" || ! -f "$key" ]]; then
warn "Cert files not found — skipping SSL update."
return
fi
section "Updating SSL configuration"
$SUDO cp -a "$cfg" "${cfg}.bak"
$SUDO awk -v cert="$cert" -v key="$key" '
BEGIN { inssl=0 }
{
if ($0 ~ /^ ssl:[[:space:]]*$/) { inssl=1; print; next }
if (inssl==1) {
if ($0 ~ /^ [^[:space:]]/ && $0 !~ /^ ssl:/) { inssl=0 }
else if ($0 ~ /^ enabled:/) { print " enabled: true"; next }
else if ($0 ~ /^ cert:/) { print " cert: " cert; next }
else if ($0 ~ /^ key:/) { print " key: " key; next }
}
print
}
' "$cfg" | $SUDO tee "$cfg" >/dev/null
say "SSL block updated (backup saved)"
}
########################################
# Header
########################################
clear
cat << "EOF"
____ _
/ ___|__ _| | __ _ __ _ ___ _ __ _ _ ___
| | / _` | |/ _` |/ _` |/ _ \| '_ \| | | / __|
| |__| (_| | | (_| | (_| | (_) | |_) | |_| \__ \
\____\__,_|_|\__,_|\__, |\___/| .__/ \__,_|___/
|___/ |_|
Calagopus Wings Installer | v1.3
EOF
need_tty
require_root_or_sudo
if ! have apt-get; then
err "Only Debian/Ubuntu supported."
exit 1
fi
########################################
# Docker
########################################
section "Docker Check"
if have docker; then
say "Docker detected: $(docker --version || true)"
else
warn "Docker not installed."
if confirm_default_no "Install Docker?"; then
section "Installing Docker (quiet mode)"
apt_install ca-certificates curl
# Quiet install + spinner so users know it's working
if run_with_spinner "Installing Docker..." bash -c "curl -fsSL https://get.docker.com/ | CHANNEL=stable ${SUDO:-} bash >/dev/null 2>&1"; then
say "Docker installed successfully"
say "$(docker --version)"
else
err "Docker installation failed"
exit 1
fi
else
err "Docker required. Exiting."
exit 1
fi
fi
########################################
# Download Wings
########################################
section "Downloading Wings"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) WINGS_ARCH="x86_64" ;;
aarch64|arm64) WINGS_ARCH="aarch64" ;;
*)
err "Unsupported architecture: $ARCH"
exit 1
;;
esac
WINGS_URL="https://github.com/calagopus/wings/releases/latest/download/wings-rs-${WINGS_ARCH}-linux"
WINGS_BIN="/usr/local/bin/wings"
$SUDO curl -fL "$WINGS_URL" -o "$WINGS_BIN" >/dev/null 2>&1
$SUDO chmod +x "$WINGS_BIN"
say "Wings installed"
"$WINGS_BIN" version || true
########################################
# Configure
########################################
CONFIG_CREATED=false
section "Configuration"
# Pulls just the base64 join-data token out of whatever the user pastes.
# Accepts:
# - the raw token by itself
# - the full "calagopus-wings configure --join-data <token>" command as
# generated by the panel (or the older "wings configure --join-data ..." form)
# - either wrapped in single/double quotes, with extra surrounding whitespace
extract_join_data() {
local raw="$1"
local out="$raw"
# If a --join-data flag is present, keep only what comes after it.
if [[ "$out" == *"--join-data"* ]]; then
out="${out#*--join-data}"
fi
# Trim leading/trailing whitespace.
out="$(printf '%s' "$out" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
# Strip a single layer of surrounding single or double quotes, if present.
out="$(printf '%s' "$out" | sed -E "s/^['\"]//; s/['\"]\$//")"
# The join-data token is base64 and never legitimately contains whitespace.
# Strip EVERY whitespace character, not just the first token: a paste can
# get hard-wrapped across multiple lines (long tokens wrapped by a terminal
# or the panel's UI, or CRLF line endings), which previously left the token
# silently truncated at the first space/newline (via `awk '{print $1}'`)
# even when the raw input still contained the rest of it.
out="$(printf '%s' "$out" | tr -d '[:space:]')"
printf '%s' "$out"
}
if confirm_default_no "Run wings configure --join-data now?"; then
JOIN_DATA_RAW="$(prompt "Paste join-data (raw token or the full 'calagopus-wings configure --join-data ...' command): ")"
JOIN_DATA="$(extract_join_data "$JOIN_DATA_RAW")"
if [[ -n "$JOIN_DATA" ]]; then
$SUDO mkdir -p /etc/calagopus-wings
# --override skips wings' interactive "overwrite existing config?" prompt.
# Without it, a re-run (or any pre-existing config.yml) makes wings try to
# read a confirm from stdin, which fails with "not a terminal" under curl|bash.
$SUDO "$WINGS_BIN" configure --join-data "$JOIN_DATA" --override
CONFIG_CREATED=true
say "Configuration complete"
else
warn "No join-data provided"
fi
fi
########################################
# SSL
########################################
if $CONFIG_CREATED && confirm_default_no "Set up SSL with certbot?"; then
section "SSL Setup"
DOMAIN="$(prompt "Enter domain: ")"
EMAIL="$(prompt "Enter email: ")"
if [[ -n "$DOMAIN" && -n "$EMAIL" ]]; then
apt_install certbot
$SUDO certbot certonly \
--standalone \
-d "$DOMAIN" \
--non-interactive \
--agree-tos \
--email "$EMAIL" \
--no-eff-email
update_ssl_config "$DOMAIN"
else
warn "Domain/email missing — skipping SSL"
fi
fi
########################################
# Service
########################################
section "Service Installation"
if confirm_default_no "Install as systemd service?"; then
$SUDO "$WINGS_BIN" service-install
say "Service installed"
$SUDO systemctl --no-pager status wings || true
fi
section "Done"
say "Calagopus Wings installation complete."