#!/bin/sh # Install Kokoro Hub on a clean Debian/Ubuntu host. set -eu REPO="Vincentkeio/kokoro" RELEASE_TAG="v0.1.1" INSTALL_DIR="/opt/kokoro" DATA_DIR="/var/lib/kokoro" SERVICE_FILE="/etc/systemd/system/kokoro-hub.service" CADDY_FILE="/etc/caddy/Caddyfile" DOMAIN="" ADMIN_USER="admin" HAS_TTY=0 TMP_DIR="" GENERATED_PASSWORD=0 NGINX_MODE=0 NGINX_BIN="" NGINX_VHOST_DIR="" NGINX_CONFIG_FILE="" INSTALL_SUCCEEDED=0 CADDY_STORAGE="/var/lib/caddy/kokoro-caddy" MENU_PATH="/usr/local/bin/kokoro" MENU_PREVIOUS_TARGET="" MENU_LINK_CHANGED=0 fail() { printf '错误:%s\n' "$*" >&2; exit 1; } info() { printf '==> %s\n' "$*"; } usage() { cat <<'EOF' Kokoro Hub 从零安装器(Debian / Ubuntu) 用法:install-hub.sh [--domain panel.example.com] [--admin-user 用户名] 未指定 --domain 时,安装器会交互询问面板域名。 要求:全新主机、systemd、域名 DNS 已指向本机,入站 TCP 80/443 可达。 安装器配置 Kokoro 和 systemd 服务;无 Nginx 时由 Caddy 提供 HTTPS,有 Nginx 时保留 Nginx 作为公网入口。 安装器不会改动系统防火墙。 EOF } while [ "$#" -gt 0 ]; do case "$1" in --domain) [ "$#" -ge 2 ] || fail "--domain 需要参数" DOMAIN=$2; shift 2 ;; --admin-user) [ "$#" -ge 2 ] || fail "--admin-user 需要参数" ADMIN_USER=$2; shift 2 ;; -h|--help) usage; exit 0 ;; *) fail "未知选项:$1(使用 --help 查看用法)" ;; esac done if ( : 2>/dev/null /dev/tty ); then HAS_TTY=1 fi [ "$(id -u)" -eq 0 ] || fail "请以 root 身份运行,例如:curl ... | sudo sh" if [ -z "$DOMAIN" ]; then if [ "$HAS_TTY" -eq 1 ]; then printf '请输入面板域名(例如 panel.example.com):' >/dev/tty IFS= read -r DOMAIN 63 || $i !~ /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/) exit 1 }' \ || fail "域名格式无效:$DOMAIN" case "$DOMAIN" in *[A-Za-z]*) ;; *) fail "请提供可用于 HTTPS 证书的 DNS 域名,不要填 IP 地址" ;; esac case "$ADMIN_USER" in ''|*[!A-Za-z0-9_.@-]*) fail "管理员用户名仅可包含字母、数字、点、下划线、@ 和连字符" ;; esac [ "${#ADMIN_USER}" -le 64 ] || fail "管理员用户名不能超过 64 个字符" [ -r /etc/os-release ] || fail "无法识别系统发行版" . /etc/os-release case "${ID:-}" in debian|ubuntu) ;; *) fail "仅支持 Debian 和 Ubuntu,当前系统:${PRETTY_NAME:-未知}" ;; esac command -v systemctl >/dev/null 2>&1 || fail "需要 systemd" command -v apt-get >/dev/null 2>&1 || fail "需要 apt-get" if [ -e "$INSTALL_DIR/kokoro" ] || [ -e "$SERVICE_FILE" ]; then fail "检测到已有 Kokoro Hub 安装;本脚本仅用于从零安装,不会覆盖或升级现有数据" fi if [ -e "$INSTALL_DIR/data" ] || [ -L "$INSTALL_DIR/data" ]; then fail "检测到已有 Hub 数据路径 $INSTALL_DIR/data;为避免覆盖残留数据,安装已停止" fi if [ -e "$CADDY_FILE" ] || [ -L "$CADDY_FILE" ]; then DEFAULT_CADDY_CONFIG=0 if [ ! -L "$CADDY_FILE" ] \ && grep -Fq 'The Caddyfile is an easy way to configure your Caddy web server.' "$CADDY_FILE" \ && awk ' /^[[:space:]]*#/ || /^[[:space:]]*$/ { next } /^[[:space:]]*:80[[:space:]]*\{/ { blocks++; next } /^[[:space:]]*\}/ { next } { line=$0 sub(/^[[:space:]]+/, "", line) sub(/[[:space:]]+$/, "", line) if (line != "root * /usr/share/caddy" && line != "file_server") invalid=1 } END { exit !(blocks == 1 && !invalid) } ' "$CADDY_FILE"; then DEFAULT_CADDY_CONFIG=1 fi if [ "$DEFAULT_CADDY_CONFIG" -ne 1 ]; then fail "检测到已有 Caddy 配置:$CADDY_FILE。为避免影响其他站点,请先为 Kokoro 准备独立主机,或手动合并反代配置" fi fi if [ -e "$MENU_PATH" ] || [ -L "$MENU_PATH" ]; then [ -L "$MENU_PATH" ] || fail "$MENU_PATH 已存在且不是软链接;为避免覆盖未知程序,安装已停止" MENU_PREVIOUS_TARGET=$(readlink "$MENU_PATH") case "$MENU_PREVIOUS_TARGET" in /usr/local/bin/kokoro-agent|/opt/kokoro/kokoro|/www/wwwroot/kokoro/kokoro) ;; *) fail "$MENU_PATH 指向非 Kokoro 程序($MENU_PREVIOUS_TARGET);为避免覆盖,安装已停止" ;; esac fi if command -v ss >/dev/null 2>&1; then PORT_OWNERS=$(ss -H -ltnp 2>/dev/null | awk ' $4 ~ /(^|:)(80|443)$/ { if ($0 ~ /"nginx"/) nginx=1 else if ($0 !~ /"caddy"/) other=1 } END { printf "%d %d", nginx, other } ') set -- $PORT_OWNERS if [ "${2:-0}" -eq 1 ]; then fail "TCP 80 或 443 被非 Nginx 服务占用。请先确认端口用途,避免影响现有网站" fi if [ "${1:-0}" -eq 1 ]; then NGINX_MODE=1 if command -v nginx >/dev/null 2>&1; then NGINX_BIN=$(command -v nginx) elif [ -x /www/server/nginx/sbin/nginx ]; then NGINX_BIN=/www/server/nginx/sbin/nginx else fail "检测到 Nginx 正监听 80/443,但找不到 nginx 管理程序" fi NGINX_CONFIG_FILES=$("$NGINX_BIN" -T 2>&1 | awk '/^# configuration file / { sub(/^# configuration file /, ""); sub(/:$/, ""); print }') if printf '%s\n' "$NGINX_CONFIG_FILES" | grep -Fq '/www/server/panel/vhost/nginx/'; then NGINX_VHOST_DIR=/www/server/panel/vhost/nginx elif printf '%s\n' "$NGINX_CONFIG_FILES" | grep -Fq '/etc/nginx/sites-enabled/'; then NGINX_VHOST_DIR=/etc/nginx/sites-enabled elif printf '%s\n' "$NGINX_CONFIG_FILES" | grep -Fq '/etc/nginx/conf.d/'; then NGINX_VHOST_DIR=/etc/nginx/conf.d else fail "检测到 Nginx,但无法确认安全的虚拟主机配置目录;没有修改现有配置" fi [ -d "$NGINX_VHOST_DIR" ] || fail "Nginx 虚拟主机目录不存在:$NGINX_VHOST_DIR" printf '%s\n' "$NGINX_CONFIG_FILES" | while IFS= read -r config_path; do [ -n "$config_path" ] || continue if awk -v domain="$DOMAIN" ' /^[[:space:]]*server_name[[:space:]]/ { for (i=2; i<=NF; i++) { gsub(/[;,]/, "", $i); if ($i == domain) found=1 } } END { exit !found } ' "$config_path" 2>/dev/null; then printf '发现已有 Nginx 域名配置:%s\n' "$config_path" >&2 exit 1 fi done || fail "该域名已在 Nginx 配置中使用;为避免覆盖现有网站,安装已停止" NGINX_CONFIG_FILE="$NGINX_VHOST_DIR/kokoro-hub-$DOMAIN.conf" [ ! -e "$NGINX_CONFIG_FILE" ] || fail "Nginx 配置文件已存在:$NGINX_CONFIG_FILE" info "检测到 Nginx:保留 Nginx 作为公网入口,Caddy 将使用本地备用端口管理证书" fi fi TMP_DIR=$(mktemp -d) cleanup() { if [ "$HAS_TTY" -eq 1 ] && command -v stty >/dev/null 2>&1; then stty echo /dev/null || true; fi if [ "$INSTALL_SUCCEEDED" -ne 1 ] && [ -n "$NGINX_CONFIG_FILE" ] && [ -e "$NGINX_CONFIG_FILE" ]; then rm -f "$NGINX_CONFIG_FILE" [ -z "$NGINX_BIN" ] || "$NGINX_BIN" -s reload >/dev/null 2>&1 || true fi if [ "$INSTALL_SUCCEEDED" -ne 1 ] && [ "$NGINX_MODE" -eq 1 ]; then systemctl disable --now kokoro-nginx-reload.path >/dev/null 2>&1 || true rm -f /etc/systemd/system/kokoro-nginx-reload.service /etc/systemd/system/kokoro-nginx-reload.path systemctl daemon-reload >/dev/null 2>&1 || true fi if [ "$INSTALL_SUCCEEDED" -ne 1 ] && [ "$MENU_LINK_CHANGED" -eq 1 ]; then if [ -n "$MENU_PREVIOUS_TARGET" ]; then ln -sfn "$MENU_PREVIOUS_TARGET" "$MENU_PATH" else rm -f "$MENU_PATH" fi fi [ -z "$TMP_DIR" ] || rm -rf "$TMP_DIR" } trap cleanup EXIT trap 'exit 1' HUP INT TERM ARCH=$(uname -m) case "$ARCH" in x86_64|amd64) HUB_ARCH=amd64 ;; aarch64|arm64) HUB_ARCH=arm64 ;; *) fail "暂不支持 CPU 架构:$ARCH(支持 amd64、arm64)" ;; esac ASSET="kokoro-linux-$HUB_ARCH" info "安装系统依赖和官方 Caddy 软件源" export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y ca-certificates curl gnupg debian-keyring debian-archive-keyring apt-transport-https openssl download_verified() { asset=$1 base="https://github.com/$REPO/releases/download/$RELEASE_TAG" curl -fL --retry 3 --proto '=https' --tlsv1.2 "$base/$asset" -o "$TMP_DIR/$asset" \ || fail "下载 $asset 失败(Release $RELEASE_TAG)" curl -fL --retry 3 --proto '=https' --tlsv1.2 "$base/$asset.sha256" -o "$TMP_DIR/$asset.sha256" \ || fail "Release 缺少 $asset.sha256 校验文件" expected=$(awk 'NR == 1 {print $1}' "$TMP_DIR/$asset.sha256") case "$expected" in *[!0-9a-fA-F]*|'') fail "$asset SHA-256 格式错误" ;; esac [ "${#expected}" -eq 64 ] || fail "$asset SHA-256 长度错误" printf '%s %s\n' "$expected" "$TMP_DIR/$asset" | sha256sum -c - >/dev/null \ || fail "$asset SHA-256 校验失败" } info "下载并校验 Kokoro Hub、Agent 和安装资源(Release $RELEASE_TAG)" download_verified "$ASSET" for TARGET_ARCH in amd64 arm64; do download_verified "kokoro-agent-linux-$TARGET_ARCH" done download_verified install.sh download_verified kokoro-bench.sh info "安装 Caddy" install -d -m 0755 /usr/share/keyrings curl -fsSL --proto '=https' --tlsv1.2 https://dl.cloudsmith.io/public/caddy/stable/gpg.key \ | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg chmod 0644 /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -fsSL --proto '=https' --tlsv1.2 https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \ -o /etc/apt/sources.list.d/caddy-stable.list apt-get update apt-get install -y caddy install -d -o caddy -g caddy -m 0750 "$CADDY_STORAGE" if [ "$HAS_TTY" -eq 1 ]; then printf '管理员用户名 [%s]:' "$ADMIN_USER" >/dev/tty IFS= read -r INPUT_USER /dev/tty stty -echo /dev/tty stty -echo /dev/tty [ "$ADMIN_PASSWORD" = "${ADMIN_PASSWORD_CONFIRM:-}" ] || fail "两次输入的密码不一致" [ "${#ADMIN_PASSWORD}" -ge 8 ] || fail "管理员密码至少 8 位" else ADMIN_PASSWORD=$(openssl rand -hex 24) GENERATED_PASSWORD=1 info "未连接交互终端,已生成随机管理员凭据;安装结束时只显示一次" fi info "安装 Kokoro Hub 和 systemd 服务" install -d -m 0755 "$INSTALL_DIR" "$DATA_DIR/dist" ln -s "$DATA_DIR" "$INSTALL_DIR/data" if ! id kokoro >/dev/null 2>&1; then useradd --system --home-dir /var/lib/kokoro --no-create-home --shell /usr/sbin/nologin kokoro fi install -o root -g root -m 0755 "$TMP_DIR/$ASSET" "$INSTALL_DIR/kokoro.new" mv -f "$INSTALL_DIR/kokoro.new" "$INSTALL_DIR/kokoro" install -o root -g root -m 0644 "$TMP_DIR/install.sh" "$DATA_DIR/dist/install.sh" install -o root -g root -m 0644 "$TMP_DIR/install.sh.sha256" "$DATA_DIR/dist/install.sh.sha256" install -o root -g root -m 0644 "$TMP_DIR/kokoro-bench.sh" "$DATA_DIR/dist/kokoro-bench.sh" for TARGET_ARCH in amd64 arm64; do AGENT_ASSET="kokoro-agent-linux-$TARGET_ARCH" install -o root -g root -m 0644 "$TMP_DIR/$AGENT_ASSET" "$DATA_DIR/dist/$AGENT_ASSET" install -o root -g root -m 0644 "$TMP_DIR/$AGENT_ASSET.sha256" "$DATA_DIR/dist/$AGENT_ASSET.sha256" done set_data_ownership() { chown kokoro:kokoro "$DATA_DIR" find "$DATA_DIR" -mindepth 1 -maxdepth 1 ! -name swapfile ! -name swapfile.new ! -name swapfile.old \ -exec chown -hR kokoro:kokoro {} + for LEGACY_SWAP in "$DATA_DIR/swapfile" "$DATA_DIR/swapfile.new" "$DATA_DIR/swapfile.old"; do if [ -f "$LEGACY_SWAP" ] && [ ! -L "$LEGACY_SWAP" ]; then chown root:root "$LEGACY_SWAP" chmod 0600 "$LEGACY_SWAP" fi done } set_data_ownership touch "$INSTALL_DIR/.managed-by-kokoro-installer" cat >"$SERVICE_FILE" <"$CADDY_FILE" <"$NGINX_CONFIG_FILE" <"$CADDY_FILE" </dev/null 2>&1; do curl -ksf --resolve "$DOMAIN:9443:127.0.0.1" "https://$DOMAIN:9443/" >/dev/null 2>&1 || true attempt=$((attempt + 1)) [ "$attempt" -lt 180 ] || fail "证书申请超时;查看 journalctl -u caddy,当前 Nginx 的 80/443 服务保持运行" sleep 1 done cat >"$NGINX_CONFIG_FILE" </etc/systemd/system/kokoro-nginx-reload.service </etc/systemd/system/kokoro-nginx-reload.path </dev/null; do attempt=$((attempt + 1)) [ "$attempt" -lt 20 ] || fail "Hub 启动失败;查看日志:journalctl -u kokoro-hub -n 100 --no-pager" sleep 1 done info "安装 Kokoro 快捷管理命令" install -d -m 0755 /usr/local/bin /usr/local/lib/kokoro ln -sfn "$INSTALL_DIR/kokoro" "$MENU_PATH" MENU_LINK_CHANGED=1 "$MENU_PATH" version >/dev/null 2>&1 || fail "kokoro 快捷命令检查失败" cat >/usr/local/lib/kokoro/uninstall-menu.sh <<'MENU_EOF' #!/bin/sh set -eu [ "$(id -u)" = "0" ] || { echo "请以 root 身份运行此脚本。" >&2; exit 1; } printf '确认删除快捷命令和 Kokoro 管理脚本?服务与程序不会卸载。(输入 yes 确认):' IFS= read -r answer || answer="" [ "$answer" = yes ] || { echo "已取消。"; exit 0; } menu=/usr/local/bin/kokoro if [ -L "$menu" ]; then rm -f "$menu"; elif [ -e "$menu" ]; then echo "$menu 不是软链接,为避免误删已保留。" >&2; exit 1; fi rm -f /usr/local/lib/kokoro/install.sh /usr/local/lib/kokoro/uninstall-menu.sh rmdir /usr/local/lib/kokoro 2>/dev/null || true echo "已移除 Kokoro 快捷命令和管理脚本;服务与程序仍保留。" MENU_EOF chmod 0755 /usr/local/lib/kokoro/uninstall-menu.sh cat </dev/null || true if [ "$NGINX_MODE" -eq 1 ]; then printf 'Nginx 保持现有网站服务并代理 Kokoro;Caddy 在本地备用端口管理证书,续期后会自动重载 Nginx。\n' else printf '请确认 DNS 已指向本机,并允许 TCP 80/443 入站;Caddy 将自动申请 HTTPS 证书并长期提供面板服务。\n' fi