232 lines
7.9 KiB
Bash
Executable File
232 lines
7.9 KiB
Bash
Executable File
#!/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"
|