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>
320 lines
8.5 KiB
Bash
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."
|