#!/bin/bash # # 脚本名称: acme_test.sh (Enhanced) # 功能: 全面诊断 ACME 挑战环境(权限、配置、网络) # 日期: 2026-03-03 # ================= 配置区域 ================= # 默认配置,可被交互式输入覆盖 DEFAULT_WEBROOT="/var/www/letsencrypt" NGINX_CONF_DIR="/etc/nginx/conf.d" # 彩色输出 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} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "\n${BLUE}[STEP]${NC} $1"; } log_substep() { echo -e " ${CYAN}➜${NC} $1"; } # 检查 root 权限 check_root() { if [[ $EUID -ne 0 ]]; then log_error "请使用 root 权限运行此脚本 (sudo)" exit 1 fi } # 获取域名和 Webroot get_input() { log_step "配置检查参数" # 域名输入 if [[ -z "$1" ]]; then read -p "请输入要测试的域名 (例如: example.com): " DOMAIN else DOMAIN="$1" fi if [[ -z "$DOMAIN" ]]; then log_error "域名不能为空" exit 1 fi log_info "目标域名: $DOMAIN" # Webroot 输入 if [[ -z "$2" ]]; then read -p "请输入 Webroot 路径 [默认: $DEFAULT_WEBROOT]: " INPUT_WEBROOT WEBROOT="${INPUT_WEBROOT:-$DEFAULT_WEBROOT}" else WEBROOT="$2" fi log_info "Webroot: $WEBROOT" } # 检查 DNS 解析 check_dns() { log_step "DNS 解析检查" # 获取本机公网 IP (尝试多个源) LOCAL_IP=$(curl -s4 ifconfig.me || curl -s4 icanhazip.com) log_substep "本机公网 IP: ${LOCAL_IP:-无法获取}" # 获取域名解析 IP # 优先使用 dig,如果没有则尝试 ping if command -v dig &> /dev/null; then DOMAIN_IP=$(dig +short "$DOMAIN" | head -n1) else DOMAIN_IP=$(ping -c 1 "$DOMAIN" 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1) fi log_substep "域名解析 IP: ${DOMAIN_IP:-无法获取}" if [[ -n "$LOCAL_IP" && -n "$DOMAIN_IP" ]]; then if [[ "$LOCAL_IP" == "$DOMAIN_IP" ]]; then log_info "✅ DNS 解析正确指向本机" else log_warn "⚠️ DNS 解析 IP ($DOMAIN_IP) 与本机 IP ($LOCAL_IP) 不一致" log_warn " (如果是 CDN/负载均衡环境,请忽略此警告)" fi elif [[ -z "$DOMAIN_IP" ]]; then log_warn "⚠️ 无法解析域名 $DOMAIN,请检查 DNS 设置" fi } # 检查 Webroot 权限与写入 check_webroot_access() { log_step "Webroot 写入测试" CHALLENGE_DIR="${WEBROOT}/.well-known/acme-challenge" # 检查 Webroot 是否存在 if [[ ! -d "$WEBROOT" ]]; then log_warn "Webroot 目录不存在: $WEBROOT" read -p "是否创建该目录? [y/N] " create_choice if [[ "$create_choice" =~ ^[Yy]$ ]]; then mkdir -p "$WEBROOT" log_info "✅ 已创建目录: $WEBROOT" else log_error "无法继续测试,目录不存在" exit 1 fi fi # 创建挑战目录 if [[ ! -d "$CHALLENGE_DIR" ]]; then mkdir -p "$CHALLENGE_DIR" log_substep "创建挑战目录: $CHALLENGE_DIR" fi # 权限检查 (简单版) TEST_FILE="${CHALLENGE_DIR}/test_token_$(date +%s)" echo "acme_test_content" > "$TEST_FILE" if [[ -f "$TEST_FILE" ]]; then log_info "✅ Root 用户写入成功: $TEST_FILE" else log_error "❌ Root 用户写入失败,请检查磁盘空间或文件系统只读状态" return 1 fi # 修正权限 (确保 Nginx 可读) chown -R www-data:www-data "$WEBROOT" chmod -R 755 "$WEBROOT" log_info "✅ 已修正 Webroot 权限 (owner: www-data, mod: 755)" return 0 } # Nginx 配置与端口检查 check_nginx() { log_step "Nginx 配置与服务检查" # 检查端口 if ss -tlnp | grep -q ':80\b'; then log_info "✅ 端口 80 正在监听" else log_error "❌ 端口 80 未被监听,Nginx 可能未启动" log_info "尝试查看 Nginx 状态:" systemctl status nginx --no-pager | head -n 5 fi # 检查配置中是否有 location 规则 CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.conf" # 如果找不到标准命名的 conf,尝试模糊搜索 if [[ ! -f "$CONF_FILE" ]]; then CONF_FILE=$(grep -l "server_name.*$DOMAIN" ${NGINX_CONF_DIR}/*.conf 2>/dev/null | head -n1) fi if [[ -f "$CONF_FILE" ]]; then log_substep "检查配置文件: $CONF_FILE" if grep -q "location.*\.well-known/acme-challenge" "$CONF_FILE"; then log_info "✅ 发现 ACME challenge location 规则" else log_warn "⚠️ 未在配置文件中发现显式的 .well-known/acme-challenge 规则" log_warn " (如果使用了 include 或通用配置,可能也是正常的)" fi else log_warn "⚠️ 未找到域名 $DOMAIN 的专属配置文件 (在 $NGINX_CONF_DIR)" fi } # 模拟 HTTP 请求 perform_http_test() { log_step "HTTP 访问测试 (模拟 ACME 验证)" TEST_FILENAME=$(basename "$TEST_FILE") # 构造 URL # 本地测试 URL (绕过 DNS) TEST_URL="http://127.0.0.1/.well-known/acme-challenge/$TEST_FILENAME" # 外部访问 URL DOMAIN_URL="http://${DOMAIN}/.well-known/acme-challenge/$TEST_FILENAME" log_substep "1. 本地回环测试 (127.0.0.1)" # 使用 Host 头强制指定域名 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: $DOMAIN" "$TEST_URL") if [[ "$HTTP_CODE" == "200" ]]; then log_info "✅ 本地访问成功 (HTTP 200)" CONTENT=$(curl -s -H "Host: $DOMAIN" "$TEST_URL") if [[ "$CONTENT" == "acme_test_content" ]]; then log_info "✅ 内容匹配成功" else log_error "❌ 内容不匹配: 获取到了 '$CONTENT'" fi else log_error "❌ 本地访问失败 (HTTP $HTTP_CODE)" log_warn " 可能原因: Nginx root/alias 配置错误,或 location 匹配错误" log_info " 尝试获取详细 Header:" curl -I -H "Host: $DOMAIN" "$TEST_URL" 2>&1 | head -n 10 fi log_substep "2. 域名解析访问测试 (公共网络模拟)" echo " 尝试直接访问: $DOMAIN_URL" EXTERNAL_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$DOMAIN_URL") if [[ "$EXTERNAL_CODE" == "200" ]]; then log_info "✅ 域名访问成功 (HTTP 200)" elif [[ "$EXTERNAL_CODE" == "301" ]] || [[ "$EXTERNAL_CODE" == "302" ]]; then log_warn "⚠️ 检测到重定向 (HTTP $EXTERNAL_CODE)" REDIRECT_URL=$(curl -s -o /dev/null -w "%{redirect_url}" "$DOMAIN_URL") log_info " 重定向目标: $REDIRECT_URL" log_info " ACME 协议通常支持重定向,但请确保最终目标可达 (HTTPS 配置是否正确?)" elif [[ "$EXTERNAL_CODE" == "000" ]]; then log_error "❌ 无法连接到服务器 (Timeout/Connection Refused)" log_warn " 请检查防火墙 (ufw/iptables) 和安全组设置 (端口 80)" else log_error "❌ 域名访问失败 (HTTP $EXTERNAL_CODE)" fi } # 清理 cleanup() { log_step "清理测试文件" if [[ -f "$TEST_FILE" ]]; then rm -f "$TEST_FILE" log_info "✅ 已删除测试文件: $TEST_FILE" else log_info "无文件需要清理" fi } # 主流程 main() { # 注册清理钩子,脚本退出时自动清理 trap cleanup EXIT echo -e "${BLUE}========================================${NC}" echo -e "${BLUE} 🔍 ACME 验证环境诊断工具 ${NC}" echo -e "${BLUE}========================================${NC}" check_root get_input "$1" "$2" check_dns check_webroot_access check_nginx perform_http_test echo -e "\n${GREEN}=== 诊断结束 ===${NC}" } main "$@"