722 lines
25 KiB
Bash
722 lines
25 KiB
Bash
#!/bin/bash
|
||
#
|
||
# 脚本名称: update_acme_cert.sh
|
||
# 功能: 自动检测并更新 ACME SSL 证书(增强版)
|
||
# 作者: Assistant
|
||
# 日期: 2026-03-23
|
||
# 用法:
|
||
# sudo ./update_acme_cert.sh
|
||
#
|
||
# 交互式输入:
|
||
# - 域名 (例如: example.com)
|
||
#
|
||
# 新增增强功能:
|
||
# 1. 自动安装/升级 acme.sh(如未安装)
|
||
# 2. 自动备份旧证书配置
|
||
# 3. nginx 配置冲突智能清理
|
||
# 4. 域名可达性自动检测
|
||
# 5. 证书更新失败自动重试(最多3次,指数退避)
|
||
# 6. 多维度最终验证(HTTPS访问、HTTP跳转、SSL证书)
|
||
# 7. 详细的诊断建议和排查步骤
|
||
#
|
||
# 前提条件:
|
||
# - 需要 root 权限运行
|
||
# - 需要安装 nginx
|
||
# - 需要开放 80 和 443 端口
|
||
# - 域名需正确解析到本服务器
|
||
#
|
||
# sudo chmod +x update_acme_cert.sh
|
||
|
||
# ================= 配置区域 =================
|
||
ACME_SH="/root/.acme.sh/acme.sh"
|
||
NGINX_CONF_DIR="/etc/nginx/conf.d"
|
||
WEBROOT="/var/www/letsencrypt"
|
||
TLS_BASE_DIR="/etc/nginx/tls"
|
||
LOG_FILE="/var/log/acme_update.log"
|
||
EMAIL="admin@$(hostname -f)"
|
||
# ===========================================
|
||
|
||
# 彩色输出(方便阅读)
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# 日志函数 - 中文输出
|
||
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_warn() { echo -e "${YELLOW}[警告]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_error() { echo -e "${RED}[错误]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_step() { echo -e "${BLUE}[步骤]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
log_success() { echo -e "${CYAN}[完成]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
|
||
|
||
# 检查是否 root 权限
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_error "请使用 root 权限运行此脚本"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 检查并安装 acme.sh
|
||
install_acme_sh() {
|
||
log_step "检查 acme.sh 安装状态..."
|
||
|
||
if [[ -f "$ACME_SH" ]]; then
|
||
log_success "acme.sh 已安装: $ACME_SH"
|
||
|
||
local version=$("$ACME_SH" --version 2>/dev/null | head -n1)
|
||
log_info "当前版本: $version"
|
||
|
||
echo ""
|
||
read -p "$(echo -e "${YELLOW}[确认]${NC} 是否升级 acme.sh 到最新版本? [y/N]: ")" upgrade
|
||
if [[ "$upgrade" =~ ^[Yy]$ ]]; then
|
||
log_info "正在升级 acme.sh..."
|
||
if "$ACME_SH" --upgrade --auto-upgrade; then
|
||
log_success "acme.sh 升级成功"
|
||
else
|
||
log_warn "升级失败,继续使用当前版本"
|
||
fi
|
||
fi
|
||
else
|
||
log_warn "未检测到 acme.sh,开始自动安装..."
|
||
|
||
log_info "执行安装命令: curl https://get.acme.sh | sh -s email=$EMAIL"
|
||
if curl https://get.acme.sh | sh -s email=$EMAIL; then
|
||
log_success "acme.sh 安装成功"
|
||
|
||
if [[ -f "$ACME_SH" ]]; then
|
||
log_info "验证安装: $ACME_SH"
|
||
else
|
||
log_error "acme.sh 安装验证失败"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_error "acme.sh 安装失败,请检查网络连接"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
log_info "设置默认 CA 为 Let's Encrypt..."
|
||
"$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1 || true
|
||
}
|
||
|
||
# 备份旧配置(幂等性支持)
|
||
backup_old_configs() {
|
||
log_step "检查并备份旧配置..."
|
||
|
||
local backup_count=0
|
||
local timestamp=$(date +%s)
|
||
|
||
# 备份证书目录
|
||
if [[ -d "$CERT_DIR" ]] && [[ -n "$(ls -A $CERT_DIR 2>/dev/null)" ]]; then
|
||
local backup_dir="${CERT_DIR}.bak-${timestamp}"
|
||
cp -r "$CERT_DIR" "$backup_dir"
|
||
log_info "已备份证书目录: $backup_dir"
|
||
backup_count=$((backup_count + 1))
|
||
fi
|
||
|
||
if [[ $backup_count -eq 0 ]]; then
|
||
log_info "未发现旧配置,无需备份"
|
||
else
|
||
log_success "共备份 $backup_count 个旧配置"
|
||
fi
|
||
}
|
||
|
||
# 输入域名并验证
|
||
input_domain() {
|
||
echo ""
|
||
read -p "$(echo -e "${BLUE}[输入]${NC} 请输入要更新的域名(例如:example.com): ")" DOMAIN
|
||
if [[ -z "$DOMAIN" ]]; then
|
||
log_error "域名不能为空"
|
||
exit 1
|
||
fi
|
||
log_info "目标域名: $DOMAIN"
|
||
}
|
||
|
||
# 检查 nginx 配置文件是否存在
|
||
check_nginx_conf() {
|
||
CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.conf"
|
||
if [[ ! -f "$CONF_FILE" ]]; then
|
||
log_warn "未找到配置文件: $CONF_FILE"
|
||
# 尝试模糊匹配
|
||
CONF_FILE=$(grep -l "server_name.*$DOMAIN" ${NGINX_CONF_DIR}/*.conf 2>/dev/null | head -n1)
|
||
if [[ -z "$CONF_FILE" ]]; then
|
||
log_error "在 $NGINX_CONF_DIR 中未找到包含域名 $DOMAIN 的 nginx 配置"
|
||
exit 1
|
||
fi
|
||
log_info "通过模糊匹配找到配置文件: $CONF_FILE"
|
||
else
|
||
log_info "找到配置文件: $CONF_FILE"
|
||
fi
|
||
}
|
||
|
||
# 从 nginx 配置中提取证书路径
|
||
extract_cert_path() {
|
||
log_step "解析配置文件,提取证书路径..."
|
||
|
||
# 提取 ssl_certificate 路径
|
||
CERT_PATH=$(grep -E "^\s*ssl_certificate\s+" "$CONF_FILE" | head -n1 | awk '{print $2}' | tr -d ';')
|
||
KEY_PATH=$(grep -E "^\s*ssl_certificate_key\s+" "$CONF_FILE" | head -n1 | awk '{print $2}' | tr -d ';')
|
||
|
||
if [[ -z "$CERT_PATH" || -z "$KEY_PATH" ]]; then
|
||
log_warn "未在配置中找到 ssl_certificate 或 ssl_certificate_key 指令"
|
||
# 使用默认路径
|
||
CERT_DIR="${TLS_BASE_DIR}/${DOMAIN}"
|
||
CERT_FILE="${CERT_DIR}/cert.pem"
|
||
KEY_FILE="${CERT_DIR}/key.pem"
|
||
log_info "使用默认证书目录: $CERT_DIR"
|
||
else
|
||
CERT_DIR=$(dirname "$CERT_PATH")
|
||
CERT_FILE="$CERT_PATH"
|
||
KEY_FILE="$KEY_PATH"
|
||
log_info "证书文件: $CERT_FILE"
|
||
log_info "密钥文件: $KEY_FILE"
|
||
fi
|
||
|
||
# 确保目录存在
|
||
mkdir -p "$CERT_DIR"
|
||
}
|
||
|
||
# 清理 nginx 配置冲突
|
||
cleanup_nginx_conflicts() {
|
||
log_step "清理 nginx 配置冲突..."
|
||
|
||
local cleaned=0
|
||
|
||
# 1. 清理所有包含此域名的配置文件
|
||
log_info "搜索包含 ${DOMAIN} 的配置文件..."
|
||
local conflict_files=$(grep -rl "server_name.*${DOMAIN}" /etc/nginx/ 2>/dev/null | grep -v ".bak-" | grep -v ".bak/")
|
||
|
||
if [[ -n "$conflict_files" ]]; then
|
||
echo "$conflict_files" | while read file; do
|
||
log_warn "发现冲突配置: $file"
|
||
|
||
local count=$(grep -c "server_name" "$file" 2>/dev/null || echo 0)
|
||
|
||
if [[ $count -gt 1 ]]; then
|
||
log_warn "文件包含多个域名,仅移除此域名的配置"
|
||
log_error "需要手动处理: $file"
|
||
elif [[ $count -eq 1 ]]; then
|
||
local backup="${file}.bak-conflict-$(date +%s)"
|
||
mv "$file" "$backup"
|
||
log_info "已备份并移除冲突配置: $backup"
|
||
cleaned=$((cleaned + 1))
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# 2. 清理 sites-enabled 中的符号链接
|
||
if [[ -L "/etc/nginx/sites-enabled/${DOMAIN}.conf" ]]; then
|
||
rm -f "/etc/nginx/sites-enabled/${DOMAIN}.conf"
|
||
log_info "已删除符号链接: /etc/nginx/sites-enabled/${DOMAIN}.conf"
|
||
cleaned=$((cleaned + 1))
|
||
fi
|
||
|
||
# 3. 清理 sites-available 中的配置
|
||
if [[ -f "/etc/nginx/sites-available/${DOMAIN}.conf" ]]; then
|
||
rm -f "/etc/nginx/sites-available/${DOMAIN}.conf"
|
||
log_info "已删除配置: /etc/nginx/sites-available/${DOMAIN}.conf"
|
||
cleaned=$((cleaned + 1))
|
||
fi
|
||
|
||
# 4. 清理 conf.d 中所有相关配置
|
||
local confd_files=$(find /etc/nginx/conf.d/ -name "*${DOMAIN}*.conf" -type f 2>/dev/null)
|
||
echo "$confd_files" | while read file; do
|
||
if [[ "$file" != "$CONF_FILE" ]]; then
|
||
local backup="${file}.bak-old-$(date +%s)"
|
||
mv "$file" "$backup"
|
||
log_info "已备份旧配置: $backup"
|
||
cleaned=$((cleaned + 1))
|
||
fi
|
||
done
|
||
|
||
if [[ $cleaned -eq 0 ]]; then
|
||
log_info "未发现配置冲突"
|
||
else
|
||
log_success "已清理 $cleaned 个冲突配置"
|
||
log_warn "建议运行: systemctl restart nginx"
|
||
fi
|
||
}
|
||
|
||
# 检测域名 80 端口可达性
|
||
check_domain_reachability() {
|
||
log_step "检测域名 80 端口可达性..."
|
||
|
||
local test_url="http://${DOMAIN}/.well-known/acme-challenge/test"
|
||
log_info "测试 URL: $test_url"
|
||
|
||
local response
|
||
response=$(curl -I -s --connect-timeout 10 "$test_url" 2>&1)
|
||
local http_code=$(echo "$response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
local location=$(echo "$response" | grep -i "^Location:" | head -n1 | awk '{print $2}' | tr -d '\r')
|
||
|
||
log_info "HTTP 响应码: ${http_code:-无响应}"
|
||
|
||
case "$http_code" in
|
||
200)
|
||
log_success "✅ 域名 80 端口可达性检测通过 (HTTP 200)"
|
||
log_info "ACME 挑战路径正常响应"
|
||
return 0
|
||
;;
|
||
404)
|
||
log_success "✅ 域名 80 端口可达性检测通过 (HTTP 404)"
|
||
log_warn "ACME 挑战路径返回 404,但 nginx 已响应"
|
||
log_info "nginx 会自动创建挑战文件,检测通过"
|
||
return 0
|
||
;;
|
||
301|302)
|
||
log_warn "检测到 HTTP 跳转 (HTTP $http_code)"
|
||
if [[ -n "$location" ]]; then
|
||
log_info "跳转目标: $location"
|
||
|
||
if [[ "$location" =~ ^https:// ]]; then
|
||
log_warn "⚠️ HTTPS 跳转已配置生效!"
|
||
log_info "这说明之前已经成功配置过 HTTPS"
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}检测到域名 $DOMAIN 已配置 HTTPS 跳转${NC}"
|
||
echo "这可能是之前运行的配置残留"
|
||
echo ""
|
||
|
||
if [[ "$location" == "https://" ]] || [[ "$location" == "https://$DOMAIN" ]] || [[ "$location" =~ ^https://.*$ ]]; then
|
||
log_info "HTTPS 跳转配置正常,域名可达"
|
||
log_info "可以继续证书更新流程"
|
||
return 0
|
||
else
|
||
log_error "跳转目标异常: $location"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warn "跳转到非 HTTPS 地址: $location"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warn "收到 HTTP $http_code 但无 Location 头"
|
||
return 1
|
||
fi
|
||
;;
|
||
000)
|
||
log_error "❌ 连接失败 (HTTP $http_code)"
|
||
log_error "域名可能未解析或防火墙阻止"
|
||
return 1
|
||
;;
|
||
*)
|
||
log_error "❌ 意外的 HTTP 响应码: $http_code"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# 检查证书是否过期(返回 0=已过期,1=未过期)
|
||
check_cert_expired() {
|
||
local cert_file="$1"
|
||
if [[ ! -f "$cert_file" ]]; then
|
||
log_warn "证书文件不存在: $cert_file,视为需要更新"
|
||
return 0
|
||
fi
|
||
|
||
# 获取过期时间
|
||
EXPIRE_DATE=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
|
||
if [[ -z "$EXPIRE_DATE" ]]; then
|
||
log_warn "无法读取证书过期时间,视为需要更新"
|
||
return 0
|
||
fi
|
||
|
||
EXPIRE_TIMESTAMP=$(date -d "$EXPIRE_DATE" +%s 2>/dev/null)
|
||
NOW_TIMESTAMP=$(date +%s)
|
||
DAYS_LEFT=$(( (EXPIRE_TIMESTAMP - NOW_TIMESTAMP) / 86400 ))
|
||
|
||
log_info "证书过期时间: $EXPIRE_DATE"
|
||
log_info "距离过期还有: ${DAYS_LEFT} 天"
|
||
|
||
# 提前 30 天预警更新
|
||
if [[ $DAYS_LEFT -lt 30 ]]; then
|
||
log_warn "证书即将过期(<30天),需要更新"
|
||
return 0
|
||
else
|
||
log_info "证书仍在有效期内,无需更新"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 使用 acme.sh 颁发/更新证书
|
||
renew_certificate() {
|
||
log_step "开始更新证书: $DOMAIN"
|
||
|
||
# 方式1: 设置默认 CA(可选)
|
||
log_info "【方式1】设置默认 CA 为 Let's Encrypt..."
|
||
if ! "$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1; then
|
||
log_warn "设置 CA 失败,继续尝试颁发证书..."
|
||
fi
|
||
|
||
# 方式2: 颁发证书(webroot 模式)
|
||
log_info "【方式2】使用 webroot 模式颁发证书..."
|
||
log_info "Webroot 路径: $WEBROOT"
|
||
|
||
local cmd="$ACME_SH --issue -d $DOMAIN --webroot $WEBROOT"
|
||
if [[ "$FORCE_RENEW" == "true" ]]; then
|
||
cmd="$cmd --force"
|
||
log_info "⚠️ 已启用强制更新模式"
|
||
fi
|
||
|
||
# 捕获输出用于分析,同时也显示在终端
|
||
local tmp_out=$(mktemp)
|
||
$cmd 2>&1 | tee "$tmp_out"
|
||
local ret=${PIPESTATUS[0]}
|
||
|
||
# 分析结果
|
||
if [[ $ret -eq 0 ]]; then
|
||
log_info "✅ 证书颁发/更新成功!"
|
||
rm -f "$tmp_out"
|
||
return 0
|
||
else
|
||
# 检查是否是因为已经更新而跳过
|
||
if grep -qE "Domains not changed|Skipping|already issued" "$tmp_out"; then
|
||
log_warn "⚠️ ACME 提示证书无需更新或已存在,视为成功"
|
||
rm -f "$tmp_out"
|
||
return 0
|
||
else
|
||
log_error "❌ 证书颁发失败,请检查域名解析和 webroot 权限"
|
||
rm -f "$tmp_out"
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 带重试的证书更新函数
|
||
renew_certificate_with_retry() {
|
||
local max_retries=3
|
||
local retry_count=0
|
||
local wait_seconds=5
|
||
|
||
while [[ $retry_count -lt $max_retries ]]; do
|
||
retry_count=$((retry_count + 1))
|
||
|
||
echo ""
|
||
log_step "尝试更新证书 (第 $retry_count/$max_retries 次)..."
|
||
|
||
# 调用基本的证书更新函数
|
||
if renew_certificate; then
|
||
log_success "✅ 证书更新成功!"
|
||
return 0
|
||
else
|
||
# 更新失败,分析原因
|
||
log_warn "⚠️ 第 $retry_count 次更新失败"
|
||
|
||
# 检查是否是配置问题导致的失败
|
||
if grep -q "Invalid response.*404" /var/log/acme_update.log 2>/dev/null || grep -q "conflicting server name" /var/log/nginx/error.log 2>/dev/null; then
|
||
log_warn "检测到配置冲突问题,尝试清理并重试..."
|
||
|
||
# 再次清理配置
|
||
cleanup_nginx_conflicts
|
||
|
||
# 完全重启 nginx
|
||
log_info "重启 nginx 服务..."
|
||
if systemctl restart nginx >/dev/null 2>&1 || systemctl reload nginx >/dev/null 2>&1 || service nginx restart >/dev/null 2>&1; then
|
||
log_success "nginx 重启成功"
|
||
else
|
||
log_error "nginx 重启失败,请手动检查"
|
||
fi
|
||
|
||
# 等待一下让 nginx 完全启动
|
||
sleep $wait_seconds
|
||
fi
|
||
|
||
# 如果不是最后一次尝试,等待后重试
|
||
if [[ $retry_count -lt $max_retries ]]; then
|
||
echo ""
|
||
log_info "等待 ${wait_seconds} 秒后重试..."
|
||
sleep $wait_seconds
|
||
|
||
# 增加等待时间(指数退避)
|
||
wait_seconds=$((wait_seconds * 2))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# 所有重试都失败
|
||
log_error "❌ 经过 $max_retries 次尝试,证书更新仍然失败"
|
||
echo ""
|
||
log_error "可能的原因:"
|
||
echo " 1. 域名未正确解析到本服务器"
|
||
echo " 2. 防火墙阻止了 80 端口"
|
||
echo " 3. nginx 配置存在冲突"
|
||
echo " 4. webroot 目录权限不足"
|
||
echo ""
|
||
echo "建议的排查步骤:"
|
||
echo " # 1. 检查域名解析"
|
||
echo " dig ${DOMAIN}"
|
||
echo ""
|
||
echo " # 2. 测试 80 端口"
|
||
echo " curl -I http://${DOMAIN}/.well-known/acme-challenge/test"
|
||
echo ""
|
||
echo " # 3. 检查 nginx 配置"
|
||
echo " grep -r 'server_name.*${DOMAIN}' /etc/nginx/"
|
||
echo ""
|
||
echo " # 4. 检查 webroot 权限"
|
||
echo " ls -la ${WEBROOT}"
|
||
echo " sudo chown -R www-data:www-data ${WEBROOT}"
|
||
echo ""
|
||
|
||
return 1
|
||
}
|
||
|
||
# 验证证书内容(匹配用户提供的特征)
|
||
verify_cert_success() {
|
||
local cert_file="$1"
|
||
if [[ ! -f "$cert_file" ]]; then
|
||
log_warn "验证失败: 证书文件不存在 -> $cert_file"
|
||
return 1
|
||
fi
|
||
|
||
# 检查是否包含证书结束标记
|
||
if grep -q "END CERTIFICATE" "$cert_file"; then
|
||
log_info "✅ 证书内容验证通过"
|
||
return 0
|
||
fi
|
||
|
||
log_warn "证书内容验证未通过 - 未找到结束标记"
|
||
return 1
|
||
}
|
||
|
||
# 安装证书到指定位置
|
||
install_certificate() {
|
||
log_step "安装证书到生产环境..."
|
||
|
||
INSTALL_CMD="$ACME_SH --install-cert -d $DOMAIN \
|
||
--key-file ${KEY_FILE} \
|
||
--fullchain-file ${CERT_FILE} \
|
||
--reloadcmd \"service nginx force-reload\""
|
||
|
||
log_info "执行安装命令: $INSTALL_CMD"
|
||
|
||
if eval "$INSTALL_CMD"; then
|
||
log_info "✅ 证书安装成功"
|
||
return 0
|
||
else
|
||
log_error "❌ 证书安装失败"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 设置证书文件权限
|
||
set_cert_permissions() {
|
||
log_step "设置证书文件权限..."
|
||
|
||
# 确保使用实际目录
|
||
CERT_DIR=$(dirname "$CERT_FILE")
|
||
|
||
if sudo chown www-data:www-data "${CERT_DIR}"/*.pem 2>/dev/null; then
|
||
log_info "✅ 文件所有者已设置为 www-data:www-data"
|
||
else
|
||
log_warn "⚠️ chown 执行失败,请手动检查权限"
|
||
fi
|
||
|
||
if sudo chmod 600 "${CERT_DIR}"/*.pem 2>/dev/null; then
|
||
log_info "✅ 文件权限已设置为 600(仅所有者可读写)"
|
||
else
|
||
log_warn "⚠️ chmod 执行失败,请手动检查权限"
|
||
fi
|
||
|
||
# 确保 nginx 可读(www-data 是 nginx 运行用户)
|
||
log_info "✅ 权限设置完成,nginx 可正常读取证书"
|
||
}
|
||
|
||
# 重载 nginx 使配置生效
|
||
reload_nginx() {
|
||
log_step "重载 nginx 配置..."
|
||
if systemctl restart nginx >/dev/null 2>&1; then
|
||
log_success "nginx 重启成功"
|
||
elif systemctl reload nginx >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功"
|
||
elif service nginx restart >/dev/null 2>&1; then
|
||
log_success "nginx 重启成功 (service 命令)"
|
||
elif service nginx reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (service 命令)"
|
||
elif service nginx force-reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (force-reload)"
|
||
elif nginx -s reload >/dev/null 2>&1; then
|
||
log_success "nginx 重载成功 (nginx -s)"
|
||
else
|
||
log_error "nginx 重启/重载失败,请手动执行: systemctl restart nginx"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 增强的最终验证
|
||
enhanced_final_verification() {
|
||
log_step "开始最终验证流程..."
|
||
|
||
# 测试 HTTPS 访问
|
||
echo ""
|
||
echo -e "${BLUE}测试 1: HTTPS 访问${NC}"
|
||
local https_response=$(curl -I -s --connect-timeout 10 "https://${DOMAIN}" 2>&1)
|
||
local https_code=$(echo "$https_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
|
||
if [[ "$https_code" == "200" ]] || [[ "$https_code" == "301" ]] || [[ "$https_code" == "302" ]]; then
|
||
log_success "✅ HTTPS 访问成功 (HTTP $https_code)"
|
||
else
|
||
log_warn "⚠️ HTTPS 响应异常: ${https_code:-无响应}"
|
||
fi
|
||
|
||
# 测试 HTTP 跳转
|
||
echo ""
|
||
echo -e "${BLUE}测试 2: HTTP 跳转测试${NC}"
|
||
local http_response=$(curl -I -s --connect-timeout 10 "http://${DOMAIN}" 2>&1)
|
||
local http_code=$(echo "$http_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}')
|
||
local location=$(echo "$http_response" | grep -i "^Location:" | head -n1 | sed 's/Location: *//i' | tr -d '\r')
|
||
|
||
if [[ "$http_code" == "301" ]] || [[ "$http_code" == "302" ]]; then
|
||
log_success "✅ HTTP 跳转正常 (HTTP $http_code)"
|
||
if [[ -n "$location" ]]; then
|
||
log_info "跳转目标: $location"
|
||
|
||
# 验证跳转目标是否正确
|
||
if [[ "$location" =~ ^https://${DOMAIN} ]] || [[ "$location" =~ ^https://${DOMAIN}/ ]]; then
|
||
log_success "✅ 跳转目标正确: $location"
|
||
else
|
||
log_warn "⚠️ 跳转目标可能不正确: $location"
|
||
log_info "期望格式: https://${DOMAIN}..."
|
||
fi
|
||
else
|
||
log_warn "⚠️ 收到 HTTP $http_code 但没有 Location 头"
|
||
log_error "这可能导致浏览器无法自动跳转"
|
||
fi
|
||
else
|
||
log_warn "⚠️ HTTP 跳转异常: ${http_code:-无响应}"
|
||
echo ""
|
||
log_error "完整响应头:"
|
||
echo "$http_response" | head -n 10
|
||
echo ""
|
||
log_error "如果显示 nginx 默认页面,可能原因:"
|
||
echo " 1. nginx 使用了旧的配置文件"
|
||
echo " 2. 80 端口有多个 server 块冲突"
|
||
echo " 3. 需要完全重启 nginx 而不是重载"
|
||
fi
|
||
|
||
# 验证 SSL 证书
|
||
echo ""
|
||
echo -e "${BLUE}测试 3: SSL 证书验证${NC}"
|
||
local ssl_verify=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null)
|
||
if [[ -n "$ssl_verify" ]]; then
|
||
log_success "✅ SSL 证书有效"
|
||
echo "$ssl_verify" | while read line; do
|
||
log_info " $line"
|
||
done
|
||
else
|
||
log_warn "⚠️ SSL 证书验证失败"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}========================================${NC}"
|
||
echo -e "${GREEN} ✅ SSL 证书更新成功! ${NC}"
|
||
echo -e "${GREEN}========================================${NC}"
|
||
echo ""
|
||
echo -e "${BLUE}📋 配置信息:${NC}"
|
||
echo " 域名: $DOMAIN"
|
||
echo " 证书文件: $CERT_FILE"
|
||
echo " 密钥文件: $KEY_FILE"
|
||
echo " Nginx 配置: $CONF_FILE"
|
||
echo " 日志文件: $LOG_FILE"
|
||
echo ""
|
||
echo -e "${BLUE}🧪 验证命令:${NC}"
|
||
echo " 1. 测试 HTTPS 访问:"
|
||
echo -e " ${GREEN}curl -I https://${DOMAIN}${NC}"
|
||
echo ""
|
||
echo " 2. 查看证书详情:"
|
||
echo -e " ${GREEN}openssl x509 -in $CERT_FILE -noout -text | head -20${NC}"
|
||
echo ""
|
||
echo " 3. 在线验证 SSL 配置:"
|
||
echo -e " ${GREEN}https://www.ssllabs.com/ssltest/analyze.html?d=${DOMAIN}${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}⚠️ 提示:${NC}"
|
||
echo " - HTTP 请求会自动 301 跳转到 HTTPS"
|
||
echo " - 证书将在 60 天后自动续期"
|
||
echo " - 如需手动续期: $ACME_SH --renew -d $DOMAIN"
|
||
echo ""
|
||
}
|
||
|
||
# 主流程
|
||
main() {
|
||
echo -e "${BLUE}========================================${NC}"
|
||
echo -e "${BLUE} 🔄 ACME SSL 证书自动更新脚本 (Robust) ${NC}"
|
||
echo -e "${BLUE} 日志文件: $LOG_FILE ${NC}"
|
||
echo -e "${BLUE}========================================${NC}"
|
||
|
||
check_root
|
||
install_acme_sh
|
||
input_domain
|
||
check_nginx_conf
|
||
extract_cert_path
|
||
|
||
echo ""
|
||
log_info "开始执行证书更新流程..."
|
||
echo ""
|
||
|
||
backup_old_configs
|
||
|
||
# 清理可能存在的 nginx 配置冲突
|
||
cleanup_nginx_conflicts
|
||
|
||
local need_renew=false
|
||
local do_install=false
|
||
|
||
# 检查证书是否需要更新
|
||
if check_cert_expired "$CERT_FILE"; then
|
||
need_renew=true
|
||
else
|
||
echo ""
|
||
echo -e "${YELLOW}当前证书仍在有效期内,请选择操作:${NC}"
|
||
echo "1) 强制更新证书 (Force Renew)"
|
||
echo "2) 跳过更新,仅执行安装和重载 (Install Only)"
|
||
echo "3) 退出脚本 (Exit)"
|
||
read -p "请输入选项 [1-3]: " choice
|
||
case "$choice" in
|
||
1) need_renew=true; FORCE_RENEW=true ;;
|
||
2) need_renew=false; do_install=true ;;
|
||
*) log_info "用户选择退出"; exit 0 ;;
|
||
esac
|
||
fi
|
||
|
||
# 只有在需要更新证书时才执行验证和更新流程
|
||
if [[ "$need_renew" == "true" ]]; then
|
||
if check_domain_reachability; then
|
||
if renew_certificate_with_retry; then
|
||
# 更新成功(或跳过)后,必须执行安装
|
||
do_install=true
|
||
else
|
||
log_error "❌ 证书更新失败,流程终止"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_error "域名可达性检测失败,无法继续"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [[ "$do_install" == "true" ]]; then
|
||
if install_certificate; then
|
||
set_cert_permissions
|
||
reload_nginx
|
||
|
||
# 使用增强的最终验证
|
||
enhanced_final_verification
|
||
|
||
log_success "🎉 域名 [$DOMAIN] 证书更新/安装全流程完成!"
|
||
else
|
||
log_error "❌ 证书安装失败"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_info "✨ 域名 [$DOMAIN] 证书无需更新,任务结束"
|
||
fi
|
||
|
||
echo ""
|
||
log_success "🎉 域名 [$DOMAIN] SSL 证书更新完成!"
|
||
echo ""
|
||
}
|
||
|
||
# 执行主函数
|
||
main "$@" |