Files
sub2api-add/scripts/install-local.sh
T
Qiufeng d3ff9be315
Business Plugins CI / check (plugin-admin) (push) Successful in 1m37s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m31s
release: harden plugin deployment and recovery
2026-08-30 13:14:22 +08:00

232 lines
7.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH=; cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
PREFIX=${PLUGIN_INSTALL_PREFIX:-/opt/sub2api-add}
ETC_DIR=${PLUGIN_ETC_DIR:-/etc/sub2api-add}
VAR_DIR=${PLUGIN_VAR_DIR:-/var/lib/sub2api-add}
RUN_USER=${PLUGIN_SYSTEM_USER:-sub2api-plugin}
SELECTION=all
usage() {
cat <<'EOF'
用法:scripts/install-local.sh [--plugin all|plugin-admin|subscription-admin]
环境变量:
PLUGIN_INSTALL_PREFIX 二进制目录,默认 /opt/sub2api-add
PLUGIN_ETC_DIR 环境文件目录,默认 /etc/sub2api-add
PLUGIN_VAR_DIR 插件数据目录,默认 /var/lib/sub2api-add
PLUGIN_SYSTEM_USER systemd 用户,默认 sub2api-plugin
PLUGIN_MARKETPLACE_INDEX
plugin-admin 市场索引(默认数据目录下的 marketplace/index.json)
PLUGIN_MARKETPLACE_ALLOWED_HOSTS
远程市场索引/插件包允许的精确主机列表
EOF
}
die() { printf '错误:%s\n' "$*" >&2; exit 1; }
info() { printf '[sub2api-add] %s\n' "$*"; }
while (($#)); do
case "$1" in
--plugin)
(($# >= 2)) || die "--plugin 需要参数"
SELECTION=$2
shift 2
;;
-h|--help)
usage
exit 0
;;
*) die "未知参数:$1" ;;
esac
done
[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行"
command -v go >/dev/null 2>&1 || die "缺少 Go;请安装 Go 1.23 或更高版本"
command -v systemctl >/dev/null 2>&1 || die "缺少 systemd/systemctl"
command -v install >/dev/null 2>&1 || die "缺少 install 命令"
GO_MAJOR=$(go version | sed -nE 's/.* go([0-9]+)\.([0-9]+).*/\1/p')
GO_MINOR=$(go version | sed -nE 's/.* go([0-9]+)\.([0-9]+).*/\2/p')
if [[ -z "$GO_MAJOR" || -z "$GO_MINOR" ]] || (( GO_MAJOR < 1 || (GO_MAJOR == 1 && GO_MINOR < 23) )); then
die "需要 Go 1.23 或更高版本"
fi
case "$SELECTION" in
all) PLUGINS=(plugin-admin subscription-admin) ;;
plugin-admin|subscription-admin) PLUGINS=("$SELECTION") ;;
*) die "插件必须是 all、plugin-admin 或 subscription-admin" ;;
esac
if ! id -u "$RUN_USER" >/dev/null 2>&1; then
useradd --system --user-group --home-dir "$VAR_DIR" --create-home --shell /usr/sbin/nologin "$RUN_USER"
fi
ensure_env_value() {
local file=$1 key=$2 value=$3
if grep -q "^${key}=" "$file"; then
if command sed --version >/dev/null 2>&1; then
sed -i "s#^${key}=.*#${key}=${value}#" "$file"
else
sed -i '' "s#^${key}=.*#${key}=${value}#" "$file"
fi
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
random_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
od -An -N32 -tx1 /dev/urandom | tr -d ' \n'
fi
}
validate_install_root() {
local value=$1 label=$2 component current=""
[[ -n "$value" && "$value" = /* && "$value" != "/" ]] || die "$label 必须是非根绝对路径"
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$label 包含非法换行"
local parts=()
IFS='/' read -r -a parts <<< "${value#/}"
for component in "${parts[@]}"; do
[[ -z "$component" ]] && continue
[[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件"
current="$current/$component"
[[ ! -L "$current" ]] || die "$label 的路径组件不能是符号链接:$current"
[[ ! -e "$current" || -d "$current" ]] || die "$label 的路径组件不是目录:$current"
done
if [[ "$value" != */sub2api-add && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then
die "$label 必须位于受管的 sub2api-add 目录;如确需自定义路径请显式设置 PLUGIN_ALLOW_CUSTOM_PATHS=true"
fi
}
ensure_real_dir_tree() {
local value=$1 label=$2 component current=""
IFS='/' read -r -a parts <<< "${value#/}"
for component in "${parts[@]}"; do
[[ -z "$component" ]] && continue
current="$current/$component"
if [[ -L "$current" ]]; then
die "$label 的路径组件不能是符号链接:$current"
elif [[ -e "$current" ]]; then
[[ -d "$current" ]] || die "$label 的路径组件不是目录:$current"
else
mkdir -- "$current"
[[ -d "$current" && ! -L "$current" ]] || die "$label 创建了不安全的路径组件:$current"
fi
done
}
ensure_real_parent() {
local path=$1 label=$2 parent
parent=$(dirname -- "$path")
[[ -d "$parent" && ! -L "$parent" ]] || die "$label 的父目录必须是已存在的真实目录:$parent"
}
ensure_regular_target() {
local path=$1 label=$2
[[ ! -L "$path" ]] || die "$label 不能是符号链接:$path"
}
validate_install_root "$PREFIX" PLUGIN_INSTALL_PREFIX
validate_install_root "$ETC_DIR" PLUGIN_ETC_DIR
validate_install_root "$VAR_DIR" PLUGIN_VAR_DIR
ensure_real_dir_tree "$PREFIX" PLUGIN_INSTALL_PREFIX
ensure_real_dir_tree "$ETC_DIR" PLUGIN_ETC_DIR
ensure_real_dir_tree "$VAR_DIR" PLUGIN_VAR_DIR
ensure_real_dir_tree /etc/systemd/system SYSTEMD_UNIT_DIR
install_one() {
local name=$1 source="$ROOT/plugins/$1" env_file="$ETC_DIR/$1.env"
local binary_dir="$PREFIX/$1/bin" data_dir="$VAR_DIR/$1"
[[ -d "$source" ]] || die "插件目录不存在:$source"
ensure_real_dir_tree "$binary_dir" PLUGIN_INSTALL_PREFIX
ensure_real_dir_tree "$data_dir" PLUGIN_VAR_DIR
ensure_real_parent "$env_file" PLUGIN_ETC_DIR
ensure_regular_target "$env_file" PLUGIN_ETC_DIR
chmod 0755 "$binary_dir" "$data_dir"
if [[ ! -f "$env_file" ]]; then
install -m 0600 "$source/.env.example" "$env_file"
fi
if [[ "$name" == plugin-admin ]]; then
ensure_env_value "$env_file" PLUGIN_REGISTRY_DIR "$data_dir"
ensure_real_dir_tree "$data_dir/marketplace" PLUGIN_VAR_DIR
chmod 0700 "$data_dir/marketplace"
ensure_regular_target "$data_dir/marketplace/index.json" PLUGIN_MARKETPLACE_INDEX
if [[ ! -f "$data_dir/marketplace/index.json" && -f "$source/marketplace/index.example.json" ]]; then
install -m 0600 "$source/marketplace/index.example.json" "$data_dir/marketplace/index.json"
fi
if grep -q '^PLUGIN_CONFIG_KEY=generate-and-replace-with-a-random-32-byte-secret$' "$env_file" ||
! grep -q '^PLUGIN_CONFIG_KEY=.' "$env_file"; then
ensure_env_value "$env_file" PLUGIN_CONFIG_KEY "$(random_secret)"
fi
fi
local tmp="$binary_dir/.${name}.tmp"
ensure_real_parent "$tmp" PLUGIN_INSTALL_PREFIX
ensure_regular_target "$tmp" PLUGIN_INSTALL_PREFIX
info "构建 $name"
(cd "$source" && CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o "$tmp" .)
chmod 0755 "$tmp"
ensure_regular_target "$binary_dir/$name" PLUGIN_INSTALL_PREFIX
mv -f "$tmp" "$binary_dir/$name"
chown -R "$RUN_USER:$RUN_USER" "$data_dir"
chown "$RUN_USER:$RUN_USER" "$binary_dir/$name"
chmod 0600 "$env_file"
ensure_real_parent "/etc/systemd/system/sub2api-$name.service" SYSTEMD_UNIT_DIR
ensure_regular_target "/etc/systemd/system/sub2api-$name.service" SYSTEMD_UNIT_DIR
cat > "/etc/systemd/system/sub2api-$name.service" <<EOF
[Unit]
Description=Sub2API ${name} business plugin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${RUN_USER}
Group=${RUN_USER}
WorkingDirectory=${data_dir}
EnvironmentFile=${env_file}
ExecStart=${binary_dir}/${name}
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictSUIDSGID=true
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
TasksMax=128
MemoryMax=512M
CPUQuota=200%
ReadWritePaths=${data_dir}
[Install]
WantedBy=multi-user.target
EOF
}
for plugin in "${PLUGINS[@]}"; do
install_one "$plugin"
done
systemctl daemon-reload
for plugin in "${PLUGINS[@]}"; do
systemctl enable --now "sub2api-$plugin.service"
info "$plugin 已启动:$(systemctl is-active "sub2api-$plugin.service")"
done
info "安装完成。配置文件位于 $ETC_DIR;数据位于 $VAR_DIR。"
info "查看日志:journalctl -u sub2api-plugin-admin -u sub2api-subscription-admin -f"