install.sh 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #!/usr/bin/env bash
  2. #
  3. # vocat install / update script for binary + systemd deployments.
  4. #
  5. # Usage:
  6. # sudo bash install.sh [version] # install a specific version
  7. # sudo bash install.sh # install latest release
  8. # sudo bash install.sh --force # reinstall even at the same version
  9. # curl -fsSL <raw url> | sudo bash # one-liner (latest)
  10. #
  11. # Behavior:
  12. # - Prompts for script language (中文 / English) as soon as it runs.
  13. # - If the installed version equals the target version, does nothing (unless --force).
  14. # - On first install, generates a random 32-char admin password, writes it to
  15. # /etc/vocat/env (0600, loaded by the systemd unit), and prints it ONCE.
  16. # - On update, preserves the existing env file and credentials.
  17. # - (Re)writes the systemd unit and restarts the service.
  18. #
  19. # Published script: must contain no secrets, IPs, or passwords.
  20. set -euo pipefail
  21. # --- Publisher configuration -------------------------------------------------
  22. # Default GitHub repository in owner/name form. Publishers: set this to your
  23. # own repo, or override per-run with VOCAT_REPO.
  24. REPO="${VOCAT_REPO:-MengMengCode/VoCat}"
  25. INSTALL_DIR="/opt/vocat/bin"
  26. BINARY_PATH="${INSTALL_DIR}/vocat"
  27. LINK_PATH="/usr/local/bin/vocat"
  28. ENV_DIR="/etc/vocat"
  29. ENV_FILE="${ENV_DIR}/env"
  30. UNIT_PATH="/etc/systemd/system/vocat.service"
  31. # --- Language ----------------------------------------------------------------
  32. LANG_CHOICE=""
  33. msg() {
  34. # $1 = zh text, $2 = en text
  35. if [ "$LANG_CHOICE" = "en" ]; then
  36. printf '%s\n' "$2"
  37. else
  38. printf '%s\n' "$1"
  39. fi
  40. }
  41. prompt_language() {
  42. if ! ( : </dev/tty ) 2>/dev/null; then
  43. case "${VOCAT_LANG:-en}" in
  44. zh|zh-CN|cn) LANG_CHOICE="zh" ;;
  45. *) LANG_CHOICE="en" ;;
  46. esac
  47. return
  48. fi
  49. while true; do
  50. echo "选择语言 / Select language: 1) 中文 2) English" >/dev/tty
  51. printf '> ' >/dev/tty
  52. if ! read -r choice </dev/tty; then
  53. LANG_CHOICE="en"
  54. return
  55. fi
  56. case "$choice" in
  57. 1|"") LANG_CHOICE="zh"; return ;;
  58. 2) LANG_CHOICE="en"; return ;;
  59. esac
  60. done
  61. }
  62. die() {
  63. msg "$1" "$2" >&2
  64. exit 1
  65. }
  66. # --- Root --------------------------------------------------------------------
  67. [ "$(id -u)" -eq 0 ] || die "请以 root 身份运行此脚本。" "Run this script as root."
  68. prompt_language
  69. # --- Parse args --------------------------------------------------------------
  70. FORCE=0
  71. TARGET_VERSION=""
  72. for arg in "$@"; do
  73. case "$arg" in
  74. --force) FORCE=1 ;;
  75. -h|--help)
  76. msg "用法: sudo bash install.sh [--force] [版本]" "Usage: sudo bash install.sh [--force] [version]"
  77. exit 0
  78. ;;
  79. *) TARGET_VERSION="${arg#v}" ;;
  80. esac
  81. done
  82. # --- Resolve target version --------------------------------------------------
  83. resolve_target_version() {
  84. if [ -n "$TARGET_VERSION" ]; then
  85. TARGET_VERSION="${TARGET_VERSION#v}"
  86. return
  87. fi
  88. local api_url="https://api.github.com/repos/${REPO}/releases/latest"
  89. local auth_hdr=()
  90. if [ -n "${GITHUB_TOKEN:-}" ]; then
  91. auth_hdr=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
  92. fi
  93. local resp
  94. resp=$(curl -fsSL "${auth_hdr[@]}" "$api_url") || die "无法获取最新版本信息。检查网络或 REPO 设置。" "Failed to fetch latest release. Check network or REPO."
  95. # Parse "tag_name": "vX.Y.Z" without jq.
  96. local tag
  97. tag=$(printf '%s\n' "$resp" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
  98. [ -n "$tag" ] || die "无法解析最新版本的 tag_name。" "Could not parse tag_name from the release response."
  99. TARGET_VERSION="${tag#v}"
  100. }
  101. # --- Skip if already installed at the same version ---------------------------
  102. skip_if_equal() {
  103. [ -x "$BINARY_PATH" ] || return 0
  104. [ "$FORCE" -eq 1 ] && return 0
  105. local installed
  106. installed=$("$BINARY_PATH" version 2>/dev/null | awk '{print $2}' | sed -E 's/[[:space:]]*\(.*$//') || return 0
  107. [ -z "$installed" ] && return 0
  108. if [ "$installed" = "$TARGET_VERSION" ]; then
  109. install -d -m 0755 "$(dirname "$LINK_PATH")"
  110. ln -sfn "$BINARY_PATH" "$LINK_PATH"
  111. msg "已安装版本 $installed,与目标版本相同,跳过更新。" "Installed version $installed equals target; skipping."
  112. exit 0
  113. fi
  114. msg "当前 $installed -> $TARGET_VERSION,开始更新。" "Updating $installed -> $TARGET_VERSION."
  115. }
  116. # --- Detect architecture -----------------------------------------------------
  117. detect_arch() {
  118. case "$(uname -m)" in
  119. x86_64) ARCH="amd64" ;;
  120. i386|i486|i586|i686) ARCH="386" ;;
  121. aarch64|arm64) ARCH="arm64" ;;
  122. armv7l|armv7*) ARCH="armv7" ;;
  123. *) die "不支持的架构: $(uname -m)" "Unsupported architecture: $(uname -m)" ;;
  124. esac
  125. }
  126. # --- Download + verify -------------------------------------------------------
  127. VOCAT_TMP=""
  128. download_and_verify() {
  129. VOCAT_TMP=$(mktemp -d)
  130. trap 'rm -rf "$VOCAT_TMP"' EXIT
  131. local base="https://github.com/${REPO}/releases/download/v${TARGET_VERSION}"
  132. local asset="vocat-linux-${ARCH}"
  133. msg "下载 $asset ..." "Downloading $asset ..."
  134. curl -fsSL -o "${VOCAT_TMP}/vocat" "${base}/${asset}" || die "下载二进制失败。" "Failed to download the binary."
  135. curl -fsSL -o "${VOCAT_TMP}/SHA256SUMS" "${base}/SHA256SUMS" || die "下载 SHA256SUMS 失败。" "Failed to download SHA256SUMS."
  136. local expected actual
  137. # Match a line whose filename field equals the asset (with optional binary-mode * prefix).
  138. expected=$(awk -v a="$asset" '$2 == a || $2 == ("*" a) {print $1; exit}' "${VOCAT_TMP}/SHA256SUMS")
  139. [ -n "$expected" ] || die "SHA256SUMS 中找不到 $asset 的校验行。" "$asset not found in SHA256SUMS."
  140. actual=$(sha256sum "${VOCAT_TMP}/vocat" | awk '{print $1}')
  141. [ "$actual" = "$expected" ] || die "SHA-256 校验失败。" "SHA-256 verification failed."
  142. }
  143. # --- Install binary ----------------------------------------------------------
  144. install_binary() {
  145. install -d -m 0755 "$INSTALL_DIR"
  146. install -m 0755 "${VOCAT_TMP}/vocat" "${BINARY_PATH}.new"
  147. if [ -e "$BINARY_PATH" ]; then
  148. cp -a "$BINARY_PATH" "${BINARY_PATH}.bak"
  149. fi
  150. mv -f "${BINARY_PATH}.new" "$BINARY_PATH"
  151. install -d -m 0755 "$(dirname "$LINK_PATH")"
  152. ln -sfn "$BINARY_PATH" "$LINK_PATH"
  153. }
  154. # --- Data directory ----------------------------------------------------------
  155. ensure_data_dir() {
  156. install -d -m 0755 /opt/vocat/data
  157. chown -R root:root /opt/vocat
  158. }
  159. # --- Env file (first install only) -------------------------------------------
  160. # Generates a random 32-char secret, stores it in the 0600 env file, and flags
  161. # FIRST_INSTALL so we can print the secret once at the end.
  162. FIRST_INSTALL=0
  163. setup_env() {
  164. if [ -f "$ENV_FILE" ]; then
  165. return
  166. fi
  167. install -d -m 0755 "$ENV_DIR"
  168. local secret
  169. secret=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
  170. [ -n "$secret" ] || die "生成随机密钥失败。" "Failed to generate a random secret."
  171. printf 'VOCAT_ADMIN_PASSWORD=%s\n' "$secret" > "$ENV_FILE"
  172. chmod 0600 "$ENV_FILE"
  173. FIRST_INSTALL=1
  174. }
  175. # --- systemd unit ------------------------------------------------------------
  176. write_unit() {
  177. cat > "$UNIT_PATH" <<EOF
  178. [Unit]
  179. Description=vocat cellular and VoWiFi control service
  180. After=network-online.target
  181. Wants=network-online.target
  182. [Service]
  183. Type=simple
  184. User=root
  185. Group=root
  186. WorkingDirectory=/opt/vocat
  187. EnvironmentFile=${ENV_FILE}
  188. Environment=VOCAT_ADDR=0.0.0.0:7575
  189. Environment=VOCAT_DATABASE_PATH=/opt/vocat/data/vocat.db
  190. ExecStart=${BINARY_PATH}
  191. Restart=on-failure
  192. RestartSec=3s
  193. TimeoutStartSec=30s
  194. TimeoutStopSec=20s
  195. AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
  196. CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
  197. NoNewPrivileges=true
  198. PrivateTmp=true
  199. PrivateDevices=false
  200. ProtectSystem=strict
  201. ProtectHome=true
  202. ProtectKernelLogs=true
  203. ProtectKernelModules=true
  204. ProtectKernelTunables=true
  205. ProtectControlGroups=true
  206. ReadWritePaths=/opt/vocat/data
  207. RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
  208. RestrictRealtime=true
  209. LockPersonality=true
  210. MemoryDenyWriteExecute=true
  211. UMask=0077
  212. LimitNOFILE=65536
  213. [Install]
  214. WantedBy=multi-user.target
  215. EOF
  216. chmod 0644 "$UNIT_PATH"
  217. }
  218. enable_and_start() {
  219. systemctl daemon-reload
  220. systemctl enable vocat
  221. if systemctl restart vocat; then
  222. return
  223. fi
  224. if [ -e "${BINARY_PATH}.bak" ]; then
  225. msg "新版本启动失败,正在恢复旧二进制。" "The new version failed to start; restoring the previous binary."
  226. cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
  227. systemctl restart vocat || true
  228. fi
  229. die "vocat 服务启动失败。" "The vocat service failed to start."
  230. }
  231. # --- Main --------------------------------------------------------------------
  232. resolve_target_version
  233. detect_arch
  234. skip_if_equal
  235. download_and_verify
  236. install_binary
  237. ensure_data_dir
  238. setup_env
  239. write_unit
  240. enable_and_start
  241. if [ "$FIRST_INSTALL" -eq 1 ]; then
  242. secret=$(grep -E '^VOCAT_ADMIN_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)
  243. echo
  244. msg "================ 安装完成 ================" "================ Install complete ================"
  245. msg "首次安装已生成管理员初始密码 (仅显示一次):" "First-install admin password (shown once):"
  246. echo
  247. echo " $secret"
  248. echo
  249. msg "用户名为 admin。请立即记录此密码。" "Username is admin. Record this password now."
  250. msg "登录后或运行以下命令修改密码:" "Change it via the web UI or run:"
  251. echo " sudo vocat menu"
  252. msg "==========================================" "=============================================="
  253. else
  254. echo
  255. msg "================ 更新完成 ================" "================ Update complete ================"
  256. msg "已更新到 $TARGET_VERSION,服务已重启。" "Updated to $TARGET_VERSION; service restarted."
  257. msg "管理员密码保持不变。" "Admin password unchanged."
  258. msg "==========================================" "=============================================="
  259. fi