#!/usr/bin/env bash # W-UI update script. # # Replaces the panel binary and the management script with a release and # restarts the service. It asks nothing: the port, the URL path, the # certificate, the database and the administrator are whatever this install # already has. Only when a panel has no certificate at all is one offered, # because a panel on plain HTTP is a panel handing its password to the wire. # # Usage: # bash <(curl -fsSL https://raw.githubusercontent.com/AbolfazlTafakori/w-ui/main/update.sh) # bash <(curl -fsSL .../update.sh) v1.2.0 a particular version # WUI_UPDATE_TAG=dev-latest bash <(curl -fsSL .../update.sh) # the rolling build of the latest commit # # The library of functions is the installer's own, so a download is verified # the same way, a certificate is issued the same way, and the closing box is # the same box. set -eu REPO="${WUI_REPO:-AbolfazlTafakori/w-ui}" REF="${WUI_REF:-main}" TAG="${1:-${WUI_UPDATE_TAG:-}}" [[ -z "$TAG" || "$TAG" == v* || "$TAG" == dev-latest ]] || TAG="v$TAG" [[ $EUID -eq 0 ]] || { printf '\n\033[0;31merror:\033[0m run as root (sudo bash %s)\n\n' "$0" >&2; exit 1; } command -v curl >/dev/null 2>&1 || { printf '\n\033[0;31merror:\033[0m curl is required\n\n' >&2; exit 1; } # Everything the installer knows, without the installer running: the same # release resolution, checksum check, certificate issue and unit writer. lib="$(mktemp)" trap 'rm -f "$lib"' EXIT curl -fsSL "https://raw.githubusercontent.com/${REPO}/${REF}/install.sh" -o "$lib" \ || { printf '\n\033[0;31merror:\033[0m could not fetch the installer library from GitHub\n\n' >&2; exit 1; } # shellcheck disable=SC1090 WUI_LIB_ONLY=1 source "$lib" detect_os printf '\n %s%sW-UI%s %s·%s WireGuard · OpenVPN · OpenConnect · IKEv2 · L2TP · PPTP %supdate%s\n' "${P:-}" "$B" "$N" "$D" "$N" "$D" "$N" printf ' %s%s · kernel %s · %s%s\n' "$D" "$OS_NAME" "$KERNEL" "$ARCH" "$N" [[ -f "$UNIT" || -x "$BIN_PATH" ]] || die "W-UI is not installed on this machine; run install.sh instead" # ── which build ────────────────────────────────────────────────────────────── step "Finding the release" resolve_release() { local api json urls if [[ -n "$TAG" ]]; then api="https://api.github.com/repos/${REPO}/releases/tags/${TAG}" else api="https://api.github.com/repos/${REPO}/releases/latest" fi json="$(curl -fsSL --max-time 20 "$api" 2>/dev/null)" || return 1 urls="$(printf '%s' "$json" | grep -o '"browser_download_url": *"[^"]*"' | sed 's/.*"browser_download_url": *"//; s/"$//')" RELEASE_TAG="$(printf '%s' "$json" | grep -o '"tag_name": *"[^"]*"' | head -1 | sed 's/.*"tag_name": *"//; s/"$//')" ASSET_URL="$(grep -E "linux[-_]${ARCH}$" <<<"$urls" | head -1)" SIG_URL="$(grep -E "linux[-_]${ARCH}\.sig$" <<<"$urls" | head -1)" # shellcheck disable=SC2034 # read by verify_download, from install.sh SUMS_URL="$(grep -E '/SHA256SUMS$' <<<"$urls" | head -1)" MENU_URL="$(grep -E '/w-ui\.sh$' <<<"$urls" | head -1)" [[ -n "$ASSET_URL" ]] } resolve_release || die "no release ${TAG:-(latest)} with a build for ${ARCH} was found" current="$("$BIN_PATH" version 2>/dev/null || echo unknown)" info "installed: $current" info "available: $RELEASE_TAG" # ── download and check ─────────────────────────────────────────────────────── step "Downloading W-UI $RELEASE_TAG" tmp="$(mktemp -d)" trap 'rm -rf "$tmp" "$lib"' EXIT curl -fsSL --retry 4 --retry-all-errors --retry-delay 3 "$ASSET_URL" -o "$tmp/wui" || die "download failed" verify_download "$tmp/wui" "${ASSET_URL##*/}" # The signature, checked with the key baked into the panel already installed. # A build that does not verify is not installed, whatever the checksum said. if [[ -n "$SIG_URL" ]] && curl -fsSL --retry 4 --retry-all-errors --retry-delay 3 "$SIG_URL" -o "$tmp/wui.sig" 2>/dev/null; then # 0: signed by this project's key. 2: it is not, and it is not installed. # Anything else: the installed panel cannot check (no key baked in, or a # panel from before this command existed) -- said, and carried on with # the checksum alone. set +e out="$("$BIN_PATH" verify "$tmp/wui" "$tmp/wui.sig" 2>&1)" rc=$? set -e # A panel from before this command existed does not know "verify" and # tries to start instead; only an answer that begins "verify:" is one. if [[ $rc -eq 0 && "$out" == *"signature ok"* ]]; then ok "signature verified" elif [[ $rc -eq 2 && "$out" == *"verify:"* ]]; then die "signature check failed: the download is not a build this project signed" else warn "the installed panel cannot check signatures; the checksum is what was verified" fi else warn "the release carries no signature for this build" fi # ── install ────────────────────────────────────────────────────────────────── step "Installing" # A release from before 3.0 has no L2TP agent. A tunnel this one carries is # taken down by it first, while it is still here to do it; the panel is # stopped for that, or it would describe the tunnel again, and the restart # below brings it back. if release_before_l2tp "$RELEASE_TAG" && [[ -f /etc/systemd/system/wui-l2tp.service || -f "$DATA_DIR/l2tp/state.json" ]]; then if have_systemd; then systemctl stop wui.service || true; fi remove_l2tp_agent fi # Nor a firewall helper: its unit runs `wui firewall-sync`, and an older # binary given a subcommand it does not know starts the whole panel instead # -- a second one, as root, at the next boot. if release_before_l2tp "$RELEASE_TAG"; then remove_firewall_helper fi install -m 0755 "$tmp/wui" "$BIN_PATH.new" && mv -f "$BIN_PATH.new" "$BIN_PATH" ok "$BIN_PATH" if [[ -n "${MENU_URL:-}" ]] && curl -fsSL --retry 4 --retry-all-errors --retry-delay 3 "$MENU_URL" -o "$tmp/w-ui.sh"; then sed 's/\r$//' "$tmp/w-ui.sh" > "$tmp/w-ui.clean" && install -m 0755 "$tmp/w-ui.clean" "$MENU_PATH" ok "$MENU_PATH" else warn "could not refresh the w-ui command; the one installed stays" fi # The helper that lets the panel install its own updates from here on. A # server installed before it existed gets it now. write_update_helper # L2TP/IPsec, installed and left off, and the agent that runs it once a # tunnel is made -- for a release that has one. if ! release_before_l2tp "$RELEASE_TAG"; then install_l2tp install_proxy_cores write_l2tp_agent fi if have_systemd; then systemctl daemon-reload systemctl restart wui.service for _ in $(seq 1 20); do systemctl is-active --quiet wui.service && break; sleep 0.5; done systemctl is-active --quiet wui.service || die "the panel did not come back — see: journalctl -u wui -n 50 --no-pager" ok "wui.service restarted" fi # ── after the update ───────────────────────────────────────────────────────── # What the panel answers on, read back; a missing URL path or certificate is # put right, since neither is something a panel should be without. step "Settings" read_existing # The panel's own environment -- the unit's lines, then db.env and wui.env -- # so what is read back is what the service runs with, not the defaults. env_of() { ( set -a # shellcheck disable=SC2034 # exported by set -a, for the panel binary WUI_DATA_DIR="$DATA_DIR" # shellcheck disable=SC2163 # each word is NAME=value, exported as such for kv in $(grep -oE 'WUI_[A-Z_]+=[^ ]+' "$UNIT" 2>/dev/null); do export "$kv"; done [[ -r "$CONF_DIR/db.env" ]] && . "$CONF_DIR/db.env" [[ -r "$CONF_DIR/wui.env" ]] && . "$CONF_DIR/wui.env" set +a "$BIN_PATH" "$@" ) } env_of setting show 2>/dev/null | sed 's/^/ /' || true # A certificate set from the menu lives in the database, not the unit; what # the panel actually serves with is what setting show reports. if [[ -z "$TLS_CERT" || -z "$TLS_KEY" ]]; then c="$(env_of setting show 2>/dev/null | sed -n 's/^cert: //p')" k="$(env_of setting show 2>/dev/null | sed -n 's/^key: //p')" if [[ -s "$c" && -s "$k" ]]; then TLS_CERT="$c"; TLS_KEY="$k"; TLS_MODE=files; fi fi base="$(env_of setting show 2>/dev/null | sed -n 's/^basePath: //p' | tr -d '/')" [[ -n "$base" ]] || base="$BASE_PATH" if (( ${#base} < 4 )); then BASE_PATH="$(gen_string 18)" warn "the URL path is missing or too short; a new one is set: /$BASE_PATH/" env_of setting set --base-path "/$BASE_PATH/" >/dev/null 2>&1 || true have_systemd && systemctl restart wui.service else BASE_PATH="$base" fi if [[ -z "$TLS_CERT" || -z "$TLS_KEY" ]]; then printf '\n %s═══════════════════════════════════════════%s\n' "$R" "$N" printf ' %s ⚠ NO SSL CERTIFICATE DETECTED ⚠ %s\n' "$R" "$N" printf ' %s═══════════════════════════════════════════%s\n' "$R" "$N" printf ' %sFor security, a certificate is strongly recommended for every panel.%s\n' "$Y" "$N" printf " %sLet's Encrypt supports both domains and IP addresses.%s\n\n" "$Y" "$N" open_tty if [[ "$INTERACTIVE" == 1 ]]; then ask_tls setup_tls if [[ -n "$TLS_CERT" && -n "$TLS_KEY" ]]; then write_unit have_systemd && systemctl restart wui.service # The subscription service on its own port gets the same certificate. env_of setting set --sub-cert "$TLS_CERT" --sub-key "$TLS_KEY" >/dev/null 2>&1 || true fi else warn "no terminal to ask on; get one later with: w-ui → 20" fi else ok "certificate: $TLS_CERT" fi ip="$(public_ip 2>/dev/null || true)" host="${ip:-}" case "$TLS_MODE" in acme) [[ -n "${ACME_DOMAIN:-}" ]] && host="$ACME_DOMAIN" ;; files) [[ -n "${TLS_DOMAIN:-}" ]] && host="$TLS_DOMAIN" ;; esac if [[ -n "$TLS_CERT" ]]; then # The folder a certificate lives in is named for what it was issued to. n="$(basename "$(dirname "$TLS_CERT")")" [[ "$n" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ || "$n" =~ ^[0-9.]+$ ]] && host="$n" scheme=https else scheme=http fi printf '\n %s═══════════════════════════════════════════%s\n' "$G" "$N" printf ' %s Panel Access Information %s\n' "$G" "$N" printf ' %s═══════════════════════════════════════════%s\n' "$G" "$N" printf ' %sAccess URL: %s://%s:%s/%s/%s\n' "$G" "$scheme" "$host" "$PANEL_PORT" "$BASE_PATH" "$N" printf ' %s═══════════════════════════════════════════%s\n' "$G" "$N" setup_fail2ban printf '\n' usage_box updating