Files
Aether/apps/aether-tunnel/install.sh
T

661 lines
24 KiB
Bash
Raw Normal View History

2026-05-14 19:34:33 +08:00
#!/bin/sh
set -eu
umask 077
2026-05-14 19:34:33 +08:00
REPO="${AETHER_TUNNEL_RELEASE_REPO:-fawney19/Aether}"
TAG="${AETHER_TUNNEL_RELEASE_TAG:-}"
INSTALL_DIR="${AETHER_TUNNEL_INSTALL_DIR:-}"
CONFIG_PATH="${AETHER_TUNNEL_CONFIG:-}"
2026-05-14 19:34:33 +08:00
TMP_DIR=""
CONFIG_TMP_PATH=""
2026-05-14 19:34:33 +08:00
say() { printf '%s\n' "[Aether Tunnel] $1"; }
fail() { printf '%s\n' "[Aether Tunnel] $1" >&2; exit 1; }
2026-05-14 19:34:33 +08:00
cleanup() {
if [ -n "$CONFIG_TMP_PATH" ] && [ -f "$CONFIG_TMP_PATH" ] && [ ! -L "$CONFIG_TMP_PATH" ]; then
rm -f "$CONFIG_TMP_PATH"
fi
2026-05-14 19:34:33 +08:00
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT INT TERM
need_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "缺少命令:$1"
}
validate_https_download_url() {
url="$1"
case "$url" in
https://*) ;;
*) fail "远程下载必须使用绝对 HTTPS URL" ;;
esac
case "$url" in
*'#'*) fail "远程下载 URL 不得包含 fragment" ;;
esac
authority=${url#https://}
authority=${authority%%[/?]*}
[ -n "$authority" ] || fail "远程下载 URL 的 host 不能为空"
case "$authority" in
*'@'*) fail "远程下载 URL 不得包含凭据" ;;
esac
}
validate_trusted_github_download_url() {
url="$1"
validate_https_download_url "$url"
authority=${url#https://}
authority=${authority%%[/?]*}
case "$authority" in
*:*) fail "GitHub 下载 URL 不得使用非标准端口:$authority" ;;
esac
host=$(printf '%s' "$authority" | tr 'A-Z' 'a-z')
case "$host" in
api.github.com|github.com|objects.githubusercontent.com|*.objects.githubusercontent.com|release-assets.githubusercontent.com|*.release-assets.githubusercontent.com) ;;
*) fail "GitHub 下载重定向到了不受信任的主机:$host" ;;
esac
}
2026-05-14 19:34:33 +08:00
download() {
url="$1"
out="$2"
validate_trusted_github_download_url "$url"
command -v curl >/dev/null 2>&1 || fail "需要 curl 安全下载 GitHub release 制品"
download_dir=$(dirname "$out")
[ -d "$download_dir" ] && [ ! -L "$download_dir" ] \
|| fail "下载目标目录不是安全目录:$download_dir"
current_url="$url"
redirect_count=0
while :; do
response_tmp=$(mktemp "$download_dir/.aether-download.body.XXXXXXXX") \
|| fail "无法创建安全下载临时文件"
headers_tmp=$(mktemp "$download_dir/.aether-download.headers.XXXXXXXX") || {
rm -f "$response_tmp"
fail "无法创建安全响应头临时文件"
}
if ! status=$(curl -sS --retry 3 --connect-timeout 10 \
--proto '=https' --max-redirs 0 --dump-header "$headers_tmp" \
--output "$response_tmp" --write-out '%{http_code}' "$current_url"); then
rm -f "$response_tmp" "$headers_tmp"
fail "GitHub 下载失败:$current_url"
fi
case "$status" in
2??)
rm -f "$headers_tmp"
[ -f "$response_tmp" ] && [ ! -L "$response_tmp" ] \
|| fail "下载结果不是普通文件"
mv -f "$response_tmp" "$out" || {
rm -f "$response_tmp"
fail "无法原子保存下载结果:$out"
}
return
;;
301|302|303|307|308)
location=$(awk '
tolower(substr($0, 1, 9)) == "location:" {
value = substr($0, 10)
sub(/^[[:space:]]*/, "", value)
sub(/\r$/, "", value)
}
END { print value }
' "$headers_tmp")
rm -f "$response_tmp" "$headers_tmp"
[ -n "$location" ] || fail "GitHub 重定向缺少 Location 响应头"
validate_trusted_github_download_url "$location"
redirect_count=$((redirect_count + 1))
[ "$redirect_count" -le 10 ] || fail "GitHub 下载重定向次数过多"
current_url="$location"
;;
*)
rm -f "$response_tmp" "$headers_tmp"
fail "GitHub 下载返回 HTTP $status:$current_url"
;;
esac
done
}
validate_release_repo() {
value="$1"
[ -n "$value" ] && [ "${#value}" -le 200 ] \
|| fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符"
case "$value" in
*/*) ;;
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
esac
owner=${value%%/*}
name=${value#*/}
case "$name" in
*/*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
esac
case "$owner" in
[A-Za-z0-9]*) ;;
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
esac
case "$name" in
[A-Za-z0-9]*) ;;
*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
esac
case "$owner$name" in
*[!A-Za-z0-9._-]*) fail "release 仓库必须是安全的 GitHub OWNER/REPO 标识符" ;;
esac
}
validate_tunnel_release_tag() {
value="$1"
[ -n "$value" ] && [ "${#value}" -le 128 ] \
|| fail "release tag 包含不安全的 URL 或路径字符"
case "$value" in
*[!A-Za-z0-9._+-]*) fail "release tag 包含不安全的 URL 或路径字符" ;;
esac
version=${value#tunnel-v}
[ "$version" != "$value" ] || fail "release tag 必须使用 tunnel-v<version> 格式"
semver_identifier='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'
if ! printf '%s' "$version" | LC_ALL=C grep -Eq "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-(${semver_identifier})(\\.${semver_identifier})*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"; then
fail "release tag 必须包含有效的 SemVer 版本"
fi
}
validate_release_asset_name() {
value="$1"
[ -n "$value" ] && [ "${#value}" -le 200 ] \
|| fail "release 制品名称无效"
case "$value" in
aether-tunnel-*.tar.gz) ;;
*) fail "release 制品名称无效" ;;
esac
case "$value" in
*[!A-Za-z0-9._-]*) fail "release 制品名称包含不安全的路径字符" ;;
esac
}
validate_node_name() {
value="$1"
[ -n "$value" ] && [ "${#value}" -le 255 ] \
|| fail "node name 必须是 1 到 255 个字符"
newline='
'
carriage_return=$(printf '\r')
case "$value" in
*"$newline"*|*"$carriage_return"*) fail "node name 不得包含控制字符" ;;
esac
case "$value" in
[[:space:]]*|*[[:space:]]) fail "node name 不得包含首尾空白" ;;
esac
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
fail "node name 不得包含控制字符"
2026-05-14 19:34:33 +08:00
fi
}
prompt_if_empty() {
name="$1"
value="$2"
prompt="$3"
if [ -n "$value" ]; then
printf '%s' "$value"
return
fi
printf '%s' "$prompt" >&2
if [ -r /dev/tty ]; then
IFS= read -r value < /dev/tty
else
fail "$name 未通过环境变量提供,且当前环境无法交互输入"
fi
[ -n "$value" ] || fail "$name 不能为空"
printf '%s' "$value"
}
toml_quote() {
value="$1"
if command -v python3 >/dev/null 2>&1; then
quoted=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$value" 2>/dev/null || true)
if [ -n "$quoted" ]; then
printf '%s\n' "$quoted"
return
fi
2026-05-14 19:34:33 +08:00
fi
printf '%s' "$value" | awk '
BEGIN { printf "\"" }
{
if (NR > 1) printf "\\n"
gsub(/\\/, "\\\\")
gsub(/\"/, "\\\"")
gsub(/\t/, "\\t")
gsub(/\r/, "\\r")
printf "%s", $0
}
END { print "\"" }
'
2026-05-14 19:34:33 +08:00
}
resolve_latest_tunnel_tag() {
validate_release_repo "$REPO"
if [ -n "$TAG" ]; then
validate_tunnel_release_tag "$TAG"
printf '%s\n' "$TAG"
return
fi
2026-05-14 19:34:33 +08:00
api_url="https://api.github.com/repos/${REPO}/releases?per_page=100"
releases="$TMP_DIR/releases.json"
download "$api_url" "$releases" >/dev/null 2>&1 || fail "无法读取 GitHub Releases:$api_url"
if command -v python3 >/dev/null 2>&1; then
python3 - "$releases" <<'PY'
import json, re, sys
2026-05-14 19:34:33 +08:00
releases = json.load(open(sys.argv[1], encoding='utf-8'))
tag_pattern = re.compile(r'^tunnel-v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$')
tunnel = [r for r in releases if not r.get('draft') and not r.get('prerelease') and tag_pattern.fullmatch(str(r.get('tag_name', '')))]
tunnel.sort(key=lambda r: r.get('published_at') or r.get('created_at') or '', reverse=True)
if tunnel:
print(tunnel[0]['tag_name'])
2026-05-14 19:34:33 +08:00
PY
else
# Without a JSON parser, only accept an exact stable SemVer tag. This
# keeps the fallback from accidentally selecting a prerelease entry.
grep -Eo '"tag_name"[[:space:]]*:[[:space:]]*"tunnel-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"' "$releases" |
head -n 1 |
sed 's/.*"\(tunnel-v[^"]*\)".*/\1/'
2026-05-14 19:34:33 +08:00
fi
}
detect_asset() {
os=$(uname -s 2>/dev/null || printf unknown)
arch=$(uname -m 2>/dev/null || printf unknown)
case "$os" in
Linux) platform=linux ;;
Darwin) platform=macos ;;
MINGW*|MSYS*|CYGWIN*) fail "检测到 Windows shell,请使用 PowerShell:irm <install.ps1-url> | iex" ;;
*) fail "不支持的系统:$os" ;;
esac
case "$arch" in
x86_64|amd64) cpu=amd64 ;;
aarch64|arm64) cpu=arm64 ;;
*) fail "不支持的 CPU 架构:$arch" ;;
esac
if [ "$platform" = "linux" ] && command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
printf 'aether-tunnel-linux-musl-%s.tar.gz\n' "$cpu"
2026-05-14 19:34:33 +08:00
else
printf 'aether-tunnel-%s-%s.tar.gz\n' "$platform" "$cpu"
2026-05-14 19:34:33 +08:00
fi
}
choose_paths() {
if [ -z "$INSTALL_DIR" ]; then
if [ "$(id -u 2>/dev/null || printf 1)" = "0" ]; then
INSTALL_DIR="/usr/local/bin"
else
INSTALL_DIR="$HOME/.local/bin"
fi
fi
if [ -z "$CONFIG_PATH" ]; then
if [ "$(id -u 2>/dev/null || printf 1)" = "0" ]; then
CONFIG_PATH="/etc/aether-tunnel/aether-tunnel.toml"
2026-05-14 19:34:33 +08:00
else
CONFIG_PATH="$HOME/.aether-tunnel/aether-tunnel.toml"
2026-05-14 19:34:33 +08:00
fi
fi
}
stat_owner_id() {
stat -c '%u' "$1" 2>/dev/null || stat -f '%u' "$1" 2>/dev/null
}
stat_mode() {
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1" 2>/dev/null
}
stat_link_count() {
stat -c '%h' "$1" 2>/dev/null || stat -f '%l' "$1" 2>/dev/null
}
require_single_link_regular_file() {
path="$1"
description="$2"
[ -f "$path" ] && [ ! -L "$path" ] || fail "$description 不是普通文件:$path"
link_count=$(stat_link_count "$path") || fail "无法读取 $description 的硬链接计数:$path"
[ "$link_count" = "1" ] || fail "$description 不得是多重硬链接文件:$path"
}
validate_trusted_directory_ancestors() {
path="$1"
description="$2"
current="$path"
while :; do
[ ! -L "$current" ] || fail "$description 的祖先目录不得是符号链接:$current"
if [ -e "$current" ]; then
[ -d "$current" ] || fail "$description 的祖先路径不是目录:$current"
ancestor_owner=$(stat_owner_id "$current") \
|| fail "无法读取 $description 的祖先目录所有者:$current"
current_uid=$(id -u)
[ "$ancestor_owner" = "0" ] || [ "$ancestor_owner" = "$current_uid" ] \
|| fail "$description 的祖先目录必须属于 root 或当前用户:$current"
ancestor_mode=$(stat_mode "$current") \
|| fail "无法读取 $description 的祖先目录权限:$current"
case "$ancestor_mode" in
*[!0-7]*) fail "$description 的祖先目录权限格式无效:$ancestor_mode" ;;
esac
ancestor_permissions=$((0$ancestor_mode))
[ $((ancestor_permissions & 0022)) -eq 0 ] \
|| fail "$description 的祖先目录不得允许组或其他用户写入:$current"
fi
parent=$(dirname "$current")
[ "$parent" != "$current" ] || break
current="$parent"
done
}
prepare_secure_config_path() {
config_dir=$(dirname "$CONFIG_PATH")
config_name=$(basename "$CONFIG_PATH")
validate_trusted_directory_ancestors "$config_dir" "配置目录"
[ ! -L "$config_dir" ] || fail "配置目录不得是符号链接:$config_dir"
if [ -e "$config_dir" ] && [ ! -d "$config_dir" ]; then
fail "配置目录路径不是目录:$config_dir"
fi
if [ ! -d "$config_dir" ]; then
mkdir -p "$config_dir"
chmod 700 "$config_dir"
fi
[ ! -L "$config_dir" ] || fail "配置目录不得是符号链接:$config_dir"
config_dir=$(cd "$config_dir" && pwd -P) || fail "无法解析配置目录:$config_dir"
CONFIG_PATH="$config_dir/$config_name"
config_dir_owner=$(stat_owner_id "$config_dir") || fail "无法读取配置目录所有者:$config_dir"
[ "$config_dir_owner" = "$(id -u)" ] || fail "配置目录必须属于当前用户:$config_dir"
config_dir_mode=$(stat_mode "$config_dir") || fail "无法读取配置目录权限:$config_dir"
case "$config_dir_mode" in
*[!0-7]*) fail "配置目录权限格式无效:$config_dir_mode" ;;
esac
config_dir_permissions=$((0$config_dir_mode))
[ $((config_dir_permissions & 0022)) -eq 0 ] \
|| fail "配置目录不得允许组或其他用户写入:$config_dir"
[ ! -L "$CONFIG_PATH" ] || fail "配置文件不得是符号链接:$CONFIG_PATH"
if [ -e "$CONFIG_PATH" ]; then
require_single_link_regular_file "$CONFIG_PATH" "配置文件"
config_owner=$(stat_owner_id "$CONFIG_PATH") || fail "无法读取配置文件所有者:$CONFIG_PATH"
[ "$config_owner" = "$(id -u)" ] || fail "配置文件必须属于当前用户:$CONFIG_PATH"
chmod 600 "$CONFIG_PATH" || fail "无法保护配置文件权限:$CONFIG_PATH"
fi
}
2026-05-14 19:34:33 +08:00
verify_checksum() {
archive="$1"
sums="$2"
asset="$3"
require_single_link_regular_file "$archive" "release 制品"
require_single_link_regular_file "$sums" "SHA256 校验文件"
matches=$(awk -v asset="$asset" '
{
targets_asset = 0
for (field = 2; field <= NF; field += 1) {
if ($field == asset || $field == "*" asset) targets_asset = 1
}
if (targets_asset) {
if (NF == 2 && ($2 == asset || $2 == "*" asset)) print $1
else print "INVALID"
}
}
' "$sums")
match_count=$(printf '%s\n' "$matches" | awk 'NF { count += 1 } END { print count + 0 }')
[ "$match_count" -eq 1 ] \
|| fail "SHA256SUMS.txt 必须且只能包含一个制品条目:$asset"
expected=$(printf '%s\n' "$matches" | awk 'NF { print; exit }')
[ "${#expected}" -eq 64 ] || fail "SHA256SUMS.txt 中的哈希格式无效:$asset"
case "$expected" in
*[!0-9A-Fa-f]*)
fail "SHA256SUMS.txt 中的哈希格式无效:$asset"
;;
esac
2026-05-14 19:34:33 +08:00
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$archive" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
else
fail "缺少 sha256sum 或 shasum,无法验证 release 制品"
2026-05-14 19:34:33 +08:00
fi
actual=$(printf '%s' "$actual" | tr 'A-F' 'a-f')
expected=$(printf '%s' "$expected" | tr 'A-F' 'a-f')
2026-05-14 19:34:33 +08:00
[ "$actual" = "$expected" ] || fail "SHA256 校验失败:$asset"
}
extract_tunnel_binary() {
archive="$1"
destination="$2"
require_single_link_regular_file "$archive" "release 制品"
members=$(tar -tzf "$archive" 2>/dev/null) || fail "无法读取 release 制品"
member_count=$(printf '%s\n' "$members" | awk 'NF { count += 1 } END { print count + 0 }')
[ "$member_count" -eq 1 ] || fail "制品必须且只能包含一个 aether-tunnel 文件"
[ "$members" = "aether-tunnel" ] || fail "制品必须在根目录包含 aether-tunnel"
listing=$(tar -tvzf "$archive" 2>/dev/null) || fail "无法读取 release 制品元数据"
member_type=$(printf '%s\n' "$listing" | awk 'NF { print substr($1, 1, 1); exit }')
[ "$member_type" = "-" ] || fail "制品中的 aether-tunnel 不是普通文件"
destination_dir=$(dirname "$destination")
extract_tmp=$(mktemp "$destination_dir/.aether-tunnel.extract.XXXXXXXX") \
|| fail "无法创建安全解压临时文件"
if ! tar -xOzf "$archive" aether-tunnel > "$extract_tmp"; then
rm -f "$extract_tmp"
fail "无法安全提取 aether-tunnel"
fi
[ -s "$extract_tmp" ] || {
rm -f "$extract_tmp"
fail "制品中的 aether-tunnel 为空"
}
mv -f "$extract_tmp" "$destination" || {
rm -f "$extract_tmp"
fail "无法原子保存解压后的 aether-tunnel"
}
require_single_link_regular_file "$destination" "解压后的 aether-tunnel"
}
prepare_secure_install_dir() {
validate_trusted_directory_ancestors "$INSTALL_DIR" "安装目录"
[ ! -L "$INSTALL_DIR" ] || fail "安装目录不得是符号链接:$INSTALL_DIR"
if [ -e "$INSTALL_DIR" ] && [ ! -d "$INSTALL_DIR" ]; then
fail "安装目录路径不是目录:$INSTALL_DIR"
fi
if [ ! -d "$INSTALL_DIR" ]; then
mkdir -p "$INSTALL_DIR"
fi
[ ! -L "$INSTALL_DIR" ] || fail "安装目录不得是符号链接:$INSTALL_DIR"
INSTALL_DIR=$(cd "$INSTALL_DIR" && pwd -P) || fail "无法解析安装目录:$INSTALL_DIR"
install_dir_owner=$(stat_owner_id "$INSTALL_DIR") || fail "无法读取安装目录所有者:$INSTALL_DIR"
[ "$install_dir_owner" = "$(id -u)" ] || fail "安装目录必须属于当前用户:$INSTALL_DIR"
install_dir_mode=$(stat_mode "$INSTALL_DIR") || fail "无法读取安装目录权限:$INSTALL_DIR"
case "$install_dir_mode" in
*[!0-7]*) fail "安装目录权限格式无效:$install_dir_mode" ;;
esac
install_dir_permissions=$((0$install_dir_mode))
[ $((install_dir_permissions & 0022)) -eq 0 ] \
|| fail "安装目录不得允许组或其他用户写入:$INSTALL_DIR"
}
install_tunnel_binary_file() {
source_binary="$1"
require_single_link_regular_file "$source_binary" "待安装二进制"
prepare_secure_install_dir
target_binary="$INSTALL_DIR/aether-tunnel"
[ ! -L "$target_binary" ] || fail "安装目标不得是符号链接:$target_binary"
if [ -e "$target_binary" ]; then
require_single_link_regular_file "$target_binary" "安装目标"
target_owner=$(stat_owner_id "$target_binary") || fail "无法读取安装目标所有者:$target_binary"
[ "$target_owner" = "$(id -u)" ] || fail "安装目标必须属于当前用户:$target_binary"
fi
install_tmp=$(mktemp "$INSTALL_DIR/.aether-tunnel.tmp.XXXXXXXX") \
|| fail "无法在安装目录中创建安全临时文件"
if ! cat "$source_binary" > "$install_tmp"; then
rm -f "$install_tmp"
fail "无法写入临时安装文件"
fi
chmod 0755 "$install_tmp" || {
rm -f "$install_tmp"
fail "无法设置临时安装文件权限"
}
[ ! -L "$target_binary" ] || {
rm -f "$install_tmp"
fail "安装目标在写入期间变成了符号链接:$target_binary"
}
if [ -e "$target_binary" ]; then
if ! require_single_link_regular_file "$target_binary" "安装目标"; then
rm -f "$install_tmp"
fail "安装目标在写入期间变得不安全:$target_binary"
fi
fi
mv -f "$install_tmp" "$target_binary" || {
rm -f "$install_tmp"
fail "无法原子替换安装目标:$target_binary"
}
}
2026-05-14 19:34:33 +08:00
install_binary() {
tag="$1"
asset="$2"
validate_release_repo "$REPO"
validate_tunnel_release_tag "$tag"
validate_release_asset_name "$asset"
2026-05-14 19:34:33 +08:00
base="https://github.com/${REPO}/releases/download/${tag}"
archive="$TMP_DIR/$asset"
say "下载 $tag / $asset"
download "$base/$asset" "$archive"
download "$base/SHA256SUMS.txt" "$TMP_DIR/SHA256SUMS.txt" >/dev/null 2>&1 || fail "无法下载 SHA256SUMS.txt"
2026-05-14 19:34:33 +08:00
verify_checksum "$archive" "$TMP_DIR/SHA256SUMS.txt" "$asset"
extract_tunnel_binary "$archive" "$TMP_DIR/aether-tunnel"
install_tunnel_binary_file "$TMP_DIR/aether-tunnel"
say "已安装二进制:$INSTALL_DIR/aether-tunnel"
2026-05-14 19:34:33 +08:00
}
has_legacy_single_server_keys() {
[ -f "$CONFIG_PATH" ] || return 1
awk '
/^[[:space:]]*\[/ { exit }
/^[[:space:]]*(aether_url|management_token)[[:space:]]*=/ { found=1; exit }
END { exit found ? 0 : 1 }
' "$CONFIG_PATH"
}
server_exists() {
[ -f "$CONFIG_PATH" ] || return 1
quoted_url="$1"
quoted_name="$2"
awk -v url="aether_url = $quoted_url" -v name="node_name = $quoted_name" '
BEGIN { found_url=0; found_name=0 }
/^\[\[servers\]\]/ {
if (found_url && found_name) { found=1 }
found_url=0; found_name=0
}
$0 == url { found_url=1 }
$0 == name { found_name=1 }
END { if (found_url && found_name) { found=1 }; exit found ? 0 : 1 }
' "$CONFIG_PATH"
}
append_server_config() {
aether_url="$1"
management_token="$2"
node_name="$3"
2026-05-21 16:08:14 +08:00
tunnel_security="$4"
tunnel_encryption_key="$5"
2026-05-14 19:34:33 +08:00
validate_node_name "$node_name"
prepare_secure_config_path
2026-05-14 19:34:33 +08:00
quoted_url=$(toml_quote "$aether_url")
quoted_token=$(toml_quote "$management_token")
quoted_name=$(toml_quote "$node_name")
2026-05-21 16:08:14 +08:00
quoted_encryption_key=$(toml_quote "$tunnel_encryption_key")
2026-05-22 15:20:36 +08:00
if [ -n "$tunnel_security" ]; then
quoted_security=$(toml_quote "$tunnel_security")
fi
2026-05-14 19:34:33 +08:00
if has_legacy_single_server_keys; then
fail "现有配置仍使用旧的顶层 aether_url/management_token,请先运行 aether-tunnel setup 迁移为 [[servers]] 后重试:$CONFIG_PATH"
2026-05-14 19:34:33 +08:00
fi
if server_exists "$quoted_url" "$quoted_name"; then
say "配置中已存在相同 aether_url + node_name,跳过追加:$CONFIG_PATH"
return
fi
config_existed=false
2026-05-14 19:34:33 +08:00
if [ -f "$CONFIG_PATH" ]; then
config_existed=true
backup_path=$(mktemp "$CONFIG_PATH.bak.$(date +%Y%m%d%H%M%S).XXXXXXXX") \
|| fail "无法创建安全配置备份"
chmod 600 "$backup_path" || fail "无法保护配置备份权限:$backup_path"
cat "$CONFIG_PATH" > "$backup_path" || fail "无法备份配置文件:$CONFIG_PATH"
2026-05-14 19:34:33 +08:00
fi
CONFIG_TMP_PATH=$(mktemp "$CONFIG_PATH.tmp.XXXXXXXX") || fail "无法创建安全配置临时文件"
chmod 600 "$CONFIG_TMP_PATH" || fail "无法保护配置临时文件权限"
if [ "$config_existed" = true ]; then
cat "$CONFIG_PATH" > "$CONFIG_TMP_PATH" || fail "无法读取现有配置:$CONFIG_PATH"
fi
2026-05-14 19:34:33 +08:00
{
if [ -s "$CONFIG_TMP_PATH" ]; then
2026-05-14 19:34:33 +08:00
printf '\n'
fi
printf '# Added by Aether Tunnel one-click installer. Existing config is preserved.\n'
2026-05-14 19:34:33 +08:00
printf '[[servers]]\n'
printf 'aether_url = %s\n' "$quoted_url"
printf 'management_token = %s\n' "$quoted_token"
printf 'node_name = %s\n' "$quoted_name"
2026-05-22 15:20:36 +08:00
if [ -n "$tunnel_security" ]; then
printf 'tunnel_security = %s\n' "$quoted_security"
fi
2026-05-21 16:08:14 +08:00
if [ -n "$tunnel_encryption_key" ]; then
printf 'tunnel_encryption_key = %s\n' "$quoted_encryption_key"
fi
} >> "$CONFIG_TMP_PATH"
chmod 600 "$CONFIG_TMP_PATH" || fail "无法保护配置临时文件权限"
[ ! -L "$CONFIG_PATH" ] || fail "配置文件在写入期间变成了符号链接:$CONFIG_PATH"
if [ -e "$CONFIG_PATH" ]; then
require_single_link_regular_file "$CONFIG_PATH" "配置文件"
fi
mv -f "$CONFIG_TMP_PATH" "$CONFIG_PATH" || fail "无法原子替换配置文件:$CONFIG_PATH"
CONFIG_TMP_PATH=""
chmod 600 "$CONFIG_PATH" || fail "无法保护配置文件权限:$CONFIG_PATH"
2026-05-14 19:34:33 +08:00
say "已追加 [[servers]] 到:$CONFIG_PATH"
}
main() {
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t aether-tunnel)
2026-05-14 19:34:33 +08:00
need_cmd tar
validate_release_repo "$REPO"
2026-05-14 19:34:33 +08:00
choose_paths
aether_url=$(prompt_if_empty AETHER_TUNNEL_AETHER_URL "${AETHER_TUNNEL_AETHER_URL:-}" "Aether URL: ")
management_token=$(prompt_if_empty AETHER_TUNNEL_MANAGEMENT_TOKEN "${AETHER_TUNNEL_MANAGEMENT_TOKEN:-}" "Management token (ae_xxx): ")
node_name=$(prompt_if_empty AETHER_TUNNEL_NODE_NAME "${AETHER_TUNNEL_NODE_NAME:-}" "Node name: ")
validate_node_name "$node_name"
2026-05-22 15:20:36 +08:00
tunnel_security="${AETHER_TUNNEL_SECURITY:-}"
2026-05-21 16:08:14 +08:00
tunnel_encryption_key="${AETHER_TUNNEL_ENCRYPTION_KEY:-}"
case "$tunnel_security" in
2026-05-22 15:20:36 +08:00
""|off|non_tls_required) ;;
2026-05-21 16:08:14 +08:00
*) fail "AETHER_TUNNEL_SECURITY 必须是 off 或 non_tls_required" ;;
esac
if [ "$tunnel_security" = "non_tls_required" ] && [ -z "$tunnel_encryption_key" ]; then
fail "AETHER_TUNNEL_SECURITY=non_tls_required 时必须设置 AETHER_TUNNEL_ENCRYPTION_KEY"
fi
2026-05-14 19:34:33 +08:00
tag=$(resolve_latest_tunnel_tag)
[ -n "$tag" ] || fail "没有找到可用的 tunnel-v* release"
validate_tunnel_release_tag "$tag"
2026-05-14 19:34:33 +08:00
asset=$(detect_asset)
install_binary "$tag" "$asset"
2026-05-21 16:08:14 +08:00
append_server_config "$aether_url" "$management_token" "$node_name" "$tunnel_security" "$tunnel_encryption_key"
2026-05-14 19:34:33 +08:00
say "完成。运行以下命令启动/配置服务:"
say " $INSTALL_DIR/aether-tunnel setup $CONFIG_PATH"
2026-05-14 19:34:33 +08:00
}
main "$@"