251 lines
8.0 KiB
Bash
251 lines
8.0 KiB
Bash
#!/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 "$@"
|