#!/bin/bash # ============================================================ # TPanel v1.3.30 一键安装脚本 # 官网: https://tpanel.cn # 作者: Zhang Pu # -------------------------------------------------------- # 完整修复历史(2026-06-04 ~ 2026-06-08): # v1.3.4 - zip 魔数校验 + 解压补根文件 + nginx 目录权限 # v1.3.5 - systemd ExecStart 传端口 + 前端 JS 语法错误 2 处 + MariaDB 装 + SQL 注入修 # v1.3.6 - PHP-FPM 改 TCP 127.0.0.1:9000(修 unix socket 在 systemd 环境失效) # v1.3.8 - 修复创建 /etc/nginx/tpanel 目录权限 + 多处隐藏 bug # v1.3.10 - phpMyAdmin 装完 on_complete 钩子自动配 Nginx 8443 # v1.3.11 - sudoers 完整授权(useradd/userdel/chown/chmod/nginx 全套) # v1.3.12 - Nginx /api/tasks//stream 拉专用 location(1800s + buffering off) # v1.3.13 - sudoers 动态路径探测 + !requiretty + visudo 验证 # v1.3.14 - static-check.py 静态分析工具(10 项检查)+ ssl_manager.py 裸调修 # v1.3.22 - 移动端侧边栏 ☰ 按钮(修复 v1.3.21 CSS display:none bug) # v1.3.23 - phpMyAdmin URL 修复(不再用 hostname 查到的 127.0.0.1) # v1.3.24 - SSL 申请 .well-known 路径修(查 sqlite 拿真 site_path) # v1.3.25 - SSL 申请进度框(走任务流 + SSE) # v1.3.26 - 创建站点支持"纯静态"类型 # v1.3.27 - SSE 401 修(require_auth 支持 query string)+ dpkg configure -a 兜底 # v1.3.28 - 添加 Sury PHP 第三方源(让 Debian 12 也能装 PHP 5.6~8.4) # v1.3.29 - 多 PHP 版本 FPM 端口隔离(8.2→9000, 7.4→9074, 8.3→9083...) # v1.3.30 - 后台默认白主题 + 🌙 切换按钮 + static-check v2.0(14 项检查) # ============================================================ # 📌 安装前重要提示: # 请确保用的是最新 install.sh(从 tpanel.cn 官网拉): # wget -O install.sh https://tpanel.cn/install.sh && bash install.sh # 镜像会优先从 https://tpanel.cn 下载源码包,国内速度最佳 # fallback 到 GitHub Release(github.com/zhang-pu/tpanel/releases) # ============================================================ set -e # v1.3.27+: 全局强制非交互(避免 phpMyAdmin / mariadb 装时跳 dialog 阻塞) export DEBIAN_FRONTEND=noninteractive PANEL_VERSION="1.3.30" PANEL_PORT=8888 GITHUB_REPO="zhang-pu/tpanel" INSTALL_DIR="/opt/tpanel" LOG_FILE="/tmp/tpanel_install.log" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' log() { echo -e "${GREEN}[$(date +%H:%M:%S)]${NC} $1" | tee -a "$LOG_FILE"; } warn() { echo -e "${YELLOW}[!]${NC} $1" | tee -a "$LOG_FILE"; } err() { echo -e "${RED}[✗]${NC} $1" | tee -a "$LOG_FILE"; } ok() { echo -e "${GREEN}[✓]${NC} $1" | tee -a "$LOG_FILE"; } if [ "$EUID" -ne 0 ]; then err "请用 root 用户运行: sudo bash install.sh"; exit 1 fi # 探测包管理器 if command -v apt-get &>/dev/null; then PKG="apt-get"; PKG_UPDATE="apt-get update"; PKG_INSTALL="apt-get install -y" elif command -v yum &>/dev/null; then PKG="yum"; PKG_UPDATE="yum makecache"; PKG_INSTALL="yum install -y" elif command -v dnf &>/dev/null; then PKG="dnf"; PKG_UPDATE="dnf makecache"; PKG_INSTALL="dnf install -y" else err "不支持的发行版(需要 apt / yum / dnf)"; exit 1 fi log "检测到包管理器: $PKG" read -p "请输入 TPanel 访问域名(留空用 IP): " PANEL_DOMAIN PANEL_DOMAIN=$(echo "$PANEL_DOMAIN" | xargs) # 1/8 依赖 log "[1/8] 安装系统依赖..." # v1.3.27 修复:之前中断过 apt 会锁 dpkg,报"dpkg was interrupted" # 装任何东西都先清一下(幂等:本来健康就几行输出) if [ "$PKG" = "apt-get" ]; then dpkg --configure -a >>"$LOG_FILE" 2>&1 || true fi $PKG_UPDATE >>"$LOG_FILE" 2>&1 || warn "更新源失败,继续..." DEB_PKGS="nginx python3 python3-pip python3-venv sqlite3 certbot python3-certbot-nginx curl wget unzip php8.2-fpm php8.2-cli php8.2-mysql php8.2-curl php8.2-mbstring php8.2-xml php8.2-zip php8.2-gd mariadb-server mariadb-client sudo" RPM_PKGS="nginx python3 python3-pip sqlite certbot python3-certbot-nginx curl wget unzip php-fpm php-mysqlnd php-curl php-mbstring php-xml php-zip php-gd mariadb-server mariadb sudo" if [ "$PKG" = "apt-get" ]; then $PKG_INSTALL $DEB_PKGS >>"$LOG_FILE" 2>&1 || { err "依赖安装失败,查看 $LOG_FILE"; exit 1; } else $PKG_INSTALL $RPM_PKGS >>"$LOG_FILE" 2>&1 || { err "依赖安装失败,查看 $LOG_FILE"; exit 1; } fi # 启动 MariaDB(v1.3.8+) if [ "$PKG" = "apt-get" ]; then systemctl enable mariadb >>"$LOG_FILE" 2>&1 || warn "启用 mariadb 失败,继续..." systemctl start mariadb >>"$LOG_FILE" 2>&1 || warn "启动 mariadb 失败,继续..." else systemctl enable mariadb >>"$LOG_FILE" 2>&1 || warn "启用 mariadb 失败,继续..." systemctl start mariadb >>"$LOG_FILE" 2>&1 || warn "启动 mariadb 失败,继续..." fi # 允许 tpanel 用户免密 sudo 跑 mysql(v1.3.8+) cat > /etc/sudoers.d/tpanel-mysql << 'EOF' tpanel ALL=(ALL) NOPASSWD: /usr/bin/mysql EOF chmod 440 /etc/sudoers.d/tpanel-mysql # 允许 tpanel 用户免密 sudo 管理站点用户 + Nginx(v1.3.11+,v1.3.13 增强) # 关键修复 v1.3.13+:动态探测命令路径(不同发行版路径不同)、加 !requiretty、 # visudo -c 验证、测 NOPASSWD 生效 USERADD_BIN=$(command -v useradd 2>/dev/null || echo /usr/sbin/useradd) USERDEL_BIN=$(command -v userdel 2>/dev/null || echo /usr/sbin/userdel) USERMOD_BIN=$(command -v usermod 2>/dev/null || echo /usr/sbin/usermod) CHOWN_BIN=$(command -v chown 2>/dev/null || echo /usr/bin/chown) CHMOD_BIN=$(command -v chmod 2>/dev/null || echo /usr/bin/chmod) NGINX_BIN=$(command -v nginx 2>/dev/null || echo /usr/sbin/nginx) SYSTEMCTL_BIN=$(command -v systemctl 2>/dev/null || echo /bin/systemctl) log "sudoers 探到路径: useradd=$USERADD_BIN nginx=$NGINX_BIN systemctl=$SYSTEMCTL_BIN" cat > /etc/sudoers.d/tpanel-admin << SUDOEOF # TPanel sudoers(v1.3.13+ 自动生成,v1.3.15+ 补 mv/rm/ln/tee) # 关键:!requiretty 避免后台 systemd 服务调用时 要 TTY Defaults:tpanel !requiretty tpanel ALL=(ALL) NOPASSWD: ${USERADD_BIN} tpanel ALL=(ALL) NOPASSWD: ${USERDEL_BIN} tpanel ALL=(ALL) NOPASSWD: ${USERMOD_BIN} tpanel ALL=(ALL) NOPASSWD: ${CHOWN_BIN} tpanel ALL=(ALL) NOPASSWD: ${CHMOD_BIN} tpanel ALL=(ALL) NOPASSWD: ${NGINX_BIN} tpanel ALL=(ALL) NOPASSWD: ${NGINX_BIN} -t tpanel ALL=(ALL) NOPASSWD: ${NGINX_BIN} -s reload tpanel ALL=(ALL) NOPASSWD: ${NGINX_BIN} -s stop tpanel ALL=(ALL) NOPASSWD: ${SYSTEMCTL_BIN} # v1.3.15+:write_nginx_config 需要写 /etc/nginx/sites-available、 # remove_nginx_config 需要删、ln 需要建软链、tee 是可选的安全写文件方式 tpanel ALL=(ALL) NOPASSWD: /usr/bin/mv tpanel ALL=(ALL) NOPASSWD: /usr/bin/rm tpanel ALL=(ALL) NOPASSWD: /usr/bin/ln tpanel ALL=(ALL) NOPASSWD: /usr/bin/tee tpanel ALL=(ALL) NOPASSWD: /usr/bin/cp tpanel ALL=(ALL) NOPASSWD: /usr/bin/mkdir tpanel ALL=(ALL) NOPASSWD: /usr/bin/mysqldump tpanel ALL=(ALL) NOPASSWD: /usr/bin/mysql tpanel ALL=(ALL) NOPASSWD: /usr/bin/apt-get tpanel ALL=(ALL) NOPASSWD: /usr/bin/apt SUDOEOF chmod 440 /etc/sudoers.d/tpanel-admin chown root:root /etc/sudoers.d/tpanel-admin # 验证 sudoers 语法合法(v1.3.13+) if ! visudo -c -f /etc/sudoers.d/tpanel-admin >>"$LOG_FILE" 2>&1; then warn "/etc/sudoers.d/tpanel-admin 语法错误!tpanel 后续操作可能需要密码" visudo -c -f /etc/sudoers.d/tpanel-admin || true fi # 验证 NOPASSWD 真的生效(v1.3.13+) if id tpanel &>/dev/null; then if sudo -u tpanel sudo -n "$USERADD_BIN" -m -s /usr/sbin/nologin -d /home/_tpanel_diag _tpanel_diag 2>/dev/null; then sudo -u tpanel sudo -n "$USERDEL_BIN" -r _tpanel_diag 2>/dev/null ok "sudo NOPASSWD 验证通过(useradd 测试成功)" else warn "sudo NOPASSWD 验证失败!tpanel 后续调用 sudo 会要密码(参考 tpanel-fix-sudo.sh)" fi fi # 启动 PHP-FPM(Debian 8.2 / CentOS php-fpm) if [ "$PKG" = "apt-get" ]; then systemctl enable php8.2-fpm >>"$LOG_FILE" 2>&1 || warn "启用 php8.2-fpm 失败,继续..." # 改用 TCP 127.0.0.1:9000(避免 unix socket 在某些 systemd 环境失效) sed -i 's|^listen = /run/php/php8.2-fpm.sock|listen = 127.0.0.1:9000|' /etc/php/8.2/fpm/pool.d/www.conf 2>/dev/null || warn "修改 PHP-FPM listen 失败" systemctl start php8.2-fpm >>"$LOG_FILE" 2>&1 || warn "启动 php8.2-fpm 失败,继续..." else systemctl enable php-fpm >>"$LOG_FILE" 2>&1 || warn "启用 php-fpm 失败,继续..." # CentOS/RHEL PHP-FPM 默认就是 TCP 9000,跳过 systemctl start php-fpm >>"$LOG_FILE" 2>&1 || warn "启动 php-fpm 失败,继续..." fi ok "依赖安装完成" # 2/8 用户和目录 log "[2/8] 创建 TPanel 用户和目录..." id tpanel &>/dev/null || useradd -r -s /bin/bash -m tpanel mkdir -p "$INSTALL_DIR"/{backend,frontend,logs,backups,sites,ssl} chown -R tpanel:tpanel "$INSTALL_DIR" # 创建 nginx 配置目录并授权 tpanel 用户(config.py 需要写这里) mkdir -p /etc/nginx/tpanel chown tpanel:tpanel /etc/nginx/tpanel chmod 755 /etc/nginx/tpanel ok "目录就绪: $INSTALL_DIR" # 3/8 下载源码 log "[3/8] 下载 TPanel v${PANEL_VERSION} 源码..." TMP_ZIP="/tmp/tpanel-v${PANEL_VERSION}.zip" # 校验文件是否为合法 zip(看前 4 字节魔数 50 4b 03 04) is_valid_zip() { local file="$1" [ -f "$file" ] || return 1 local magic magic=$(od -An -tx1 -N4 "$file" 2>/dev/null | tr -d ' \n') [ "$magic" = "504b0304" ] } download_from_github() { local url="$1" log " 尝试下载: $url" rm -f "$TMP_ZIP" if curl -fsSL --connect-timeout 15 --max-time 300 -o "$TMP_ZIP" "$url" 2>>"$LOG_FILE"; then if is_valid_zip "$TMP_ZIP"; then return 0 else warn " 下载到的东西不是 zip(可能 404 HTML)" return 1 fi else warn " 下载失败 (HTTP 错误或网络不通)" return 1 fi } # ✅ 修正后的下载源(按优先级,v1.3.30 调整:tpanel.cn 优先,国内用户友好) URLS=( # 1. 官网镜像(优先,最快,国内友好) "https://tpanel.cn/tpanel-v${PANEL_VERSION}-source.zip" # 2. 官网最新主包(v1.3.30 新增,兼容老 install.sh 拉新版) "https://tpanel.cn/tpanel-latest-source.zip" # 3. GitHub release 当前版本 "https://github.com/${GITHUB_REPO}/releases/download/v${PANEL_VERSION}/tpanel-v${PANEL_VERSION}-source.zip" # 4. 源码 tag zip "https://github.com/${GITHUB_REPO}/archive/refs/tags/v${PANEL_VERSION}.zip" # 5. main 分支源码 zip "https://github.com/${GITHUB_REPO}/archive/refs/heads/main.zip" ) DOWNLOADED=0 for url in "${URLS[@]}"; do if download_from_github "$url"; then ok " 源码下载成功 ($(ls -lh $TMP_ZIP | awk '{print $5}'))" DOWNLOADED=1 break fi done # 本地兜底 if [ $DOWNLOADED -eq 0 ]; then for local_zip in /tmp/tpanel.zip /tmp/tpanel_v1.3.zip /tmp/tpanel-v1.3.1.zip /tmp/tpanel-v1.3.1-source.zip; do if [ -f "$local_zip" ] && is_valid_zip "$local_zip"; then warn " GitHub 下载失败,使用本地 $local_zip" cp "$local_zip" "$TMP_ZIP" DOWNLOADED=1 ok " 使用本地源码" break fi done fi if [ $DOWNLOADED -eq 0 ]; then err "所有下载源都失败!" echo echo " 手动下载后放到 /tmp/tpanel.zip 再重跑:" echo " wget -O /tmp/tpanel.zip https://tpanel.cn/tpanel-v${PANEL_VERSION}-source.zip" echo " # 或拉最新:wget -O /tmp/tpanel.zip https://tpanel.cn/tpanel-latest-source.zip" echo " bash install.sh" exit 1 fi # 解压(自适应目录) log " 解压源码..." rm -rf "$INSTALL_DIR"/{backend,frontend} unzip -q -o "$TMP_ZIP" -d /tmp/tpanel_extracted if [ -d /tmp/tpanel_extracted/tpanel-main ]; then EXTRACT_DIR=/tmp/tpanel_extracted/tpanel-main elif [ -d /tmp/tpanel_extracted/tpanel-${PANEL_VERSION} ]; then EXTRACT_DIR=/tmp/tpanel_extracted/tpanel-${PANEL_VERSION} elif [ -d /tmp/tpanel_extracted/backend ]; then EXTRACT_DIR=/tmp/tpanel_extracted else err " 解压目录结构不认识: /tmp/tpanel_extracted/" ls -la /tmp/tpanel_extracted/ exit 1 fi cp -r "$EXTRACT_DIR"/backend "$INSTALL_DIR/" cp -r "$EXTRACT_DIR"/frontend "$INSTALL_DIR/" # 复制根目录的配置文件(如果有) for f in requirements.txt .gitignore README.md SPEC.md; do if [ -f "$EXTRACT_DIR/$f" ]; then cp "$EXTRACT_DIR/$f" "$INSTALL_DIR/" fi done chown -R tpanel:tpanel "$INSTALL_DIR" ok "源码部署完成" # 4/8 venv log "[4/8] 配置 Python 虚拟环境..." sudo -u tpanel python3 -m venv "$INSTALL_DIR/venv" sudo -u tpanel "$INSTALL_DIR/venv/bin/pip" install --upgrade pip >>"$LOG_FILE" 2>&1 sudo -u tpanel "$INSTALL_DIR/venv/bin/pip" install -r "$INSTALL_DIR/requirements.txt" >>"$LOG_FILE" 2>&1 || { err "Python 依赖安装失败,查看 $LOG_FILE"; exit 1; } ok "Python 环境就绪" # 5/8 Nginx log "[5/8] 配置 Nginx..." cat > /etc/nginx/sites-available/tpanel <>"$LOG_FILE" 2>&1 && systemctl reload nginx ok "Nginx 配置完成" # 6/8 systemd log "[6/8] 注册 systemd 服务..." cat > /etc/systemd/system/tpanel.service <>"$LOG_FILE" 2>&1 systemctl restart tpanel sleep 2 if curl -fs http://127.0.0.1:${PANEL_PORT}/api/health >/dev/null 2>&1; then ok "TPanel 启动成功" else warn "健康检查失败,查看: journalctl -u tpanel -n 50" fi # 7/8 防火墙 log "[7/8] 配置防火墙..." if command -v ufw &>/dev/null; then ufw allow 80/tcp >/dev/null 2>&1 || true ufw allow 443/tcp >/dev/null 2>&1 || true ok "ufw 规则已添加" elif command -v firewall-cmd &>/dev/null; then firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 || true ok "firewalld 规则已添加" else warn "未检测到防火墙,跳过" fi # 8/8 SSL if [ -n "$PANEL_DOMAIN" ]; then log "[8/8] 申请 SSL 证书(${PANEL_DOMAIN})..." if certbot --nginx -d "$PANEL_DOMAIN" --non-interactive --agree-tos -m "admin@${PANEL_DOMAIN}" >>"$LOG_FILE" 2>&1; then ok "SSL 证书签发成功" else warn "SSL 签发失败,稍后手动: certbot --nginx -d ${PANEL_DOMAIN}" fi else log "[8/8] 跳过 SSL(无域名)" fi ACCESS_URL="http://${PANEL_DOMAIN:-$(curl -s ifconfig.me 2>/dev/null || echo 'YOUR_SERVER_IP')}" echo echo "============================================================" echo -e "${GREEN} TPanel v${PANEL_VERSION} 安装完成!${NC}" echo "============================================================" echo echo " 访问地址: ${ACCESS_URL}" echo " 默认账号: admin" echo " 默认密码: tpanel.cn" echo echo -e "${YELLOW} ⚠️ 请登录后立即修改密码!${NC}" echo echo -e "${YELLOW} 💡 首次访问请用 Ctrl+Shift+R 硬刷新(跳过浏览器缓存)${NC}" echo "============================================================"