#!/bin/bash
#
# SSL 证书过期检测脚本
# 功能: 批量检测 SSL 证书有效期，支持多域名检测和过期告警
# 兼容: RockyLinux, Ubuntu, Debian, CentOS 等主流 Linux 发行版
#

set -o pipefail

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

# 默认配置
TIMEOUT=10
WARN_DAYS=30
CRIT_DAYS=7
DOMAINS_FILE=""
PORT=443
TOTAL_COUNT=0
VALID_COUNT=0
WARN_COUNT=0
CRIT_COUNT=0
ERROR_COUNT=0
RESULTS=()

# 使用说明
usage() {
    cat << EOF
用法: $0 [选项] [域名...]

SSL 证书过期检测脚本 - 批量检测 SSL 证书有效期

选项:
  -f <file>       从文件读取域名列表
  -p <port>       指定端口 (默认: 443)
  -w <days>       警告阈值天数 (默认: 30)
  -c <days>       严重阈值天数 (默认: 7)
  -t <seconds>    连接超时时间 (默认: 10)
  -h              显示此帮助信息

示例:
  $0 example.com
  $0 example.com google.com github.com
  $0 -p 8443 example.com
  $0 -w 60 -c 14 example.com
  $0 -f domains.txt

配置文件格式 (每行一条记录):
  example.com
  example.com:8443
  # 这是注释
  subdomain.example.com

退出码:
  0 - 所有证书有效且未达到警告阈值
  1 - 存在证书已过期或获取失败
  2 - 存在证书即将过期(严重)
  3 - 存在证书即将过期(警告)
EOF
    exit 0
}

# 错误退出
die() {
    echo -e "${RED}错误: $1${NC}" >&2
    exit 1
}

# 检查依赖
check_dependencies() {
    if ! command -v openssl &> /dev/null; then
        die "未找到 openssl 命令，请先安装 openssl"
    fi
}

# 分隔线
print_line() {
    echo "================================================================================"
}

print_dash_line() {
    echo "--------------------------------------------------------------------------------"
}

# 头部信息
print_header() {
    print_line
    echo -e "                        ${CYAN}SSL 证书过期检测${NC}"
    echo "                    $(date '+%Y-%m-%d %H:%M:%S')"
    print_line
    echo ""
}

# 获取证书信息
# 返回: 颁发者|主题|生效日期|过期日期|剩余天数|状态
get_cert_info() {
    local domain=$1
    local port=$2
    local cert_info expire_date expire_ts now_ts days_left
    local issuer subject not_before not_after status

    # 获取证书信息
    cert_info=$(echo | timeout "$TIMEOUT" openssl s_client \
        -servername "$domain" \
        -connect "${domain}:${port}" \
        2>/dev/null)

    if [ -z "$cert_info" ] || ! echo "$cert_info" | grep -q "BEGIN CERTIFICATE"; then
        echo "||||||ERROR"
        return 1
    fi

    # 解析证书详情
    local cert_text
    cert_text=$(echo "$cert_info" | openssl x509 -noout -dates -issuer -subject 2>/dev/null)

    if [ -z "$cert_text" ]; then
        echo "||||||ERROR"
        return 1
    fi

    # 提取各字段
    issuer=$(echo "$cert_text" | grep "^issuer=" | sed 's/^issuer=//' | sed 's/.*CN *= *//' | cut -d',' -f1 | cut -d'/' -f1)
    subject=$(echo "$cert_text" | grep "^subject=" | sed 's/^subject=//' | sed 's/.*CN *= *//' | cut -d',' -f1 | cut -d'/' -f1)
    not_before=$(echo "$cert_text" | grep "^notBefore=" | sed 's/^notBefore=//')
    not_after=$(echo "$cert_text" | grep "^notAfter=" | sed 's/^notAfter=//')

    # 计算剩余天数
    # 使用兼容多系统的日期解析方式
    expire_ts=$(date -d "$not_after" +%s 2>/dev/null)
    if [ -z "$expire_ts" ]; then
        # macOS 兼容
        expire_ts=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$not_after" +%s 2>/dev/null)
    fi

    if [ -z "$expire_ts" ]; then
        echo "||||||PARSE_ERROR"
        return 1
    fi

    now_ts=$(date +%s)
    days_left=$(( (expire_ts - now_ts) / 86400 ))

    # 判断状态
    if [ "$days_left" -lt 0 ]; then
        status="EXPIRED"
    elif [ "$days_left" -le "$CRIT_DAYS" ]; then
        status="CRITICAL"
    elif [ "$days_left" -le "$WARN_DAYS" ]; then
        status="WARNING"
    else
        status="OK"
    fi

    echo "${issuer}|${subject}|${not_before}|${not_after}|${days_left}|${status}"
}

# 格式化日期显示
format_date() {
    local date_str=$1
    # 尝试转换为更易读的格式
    local formatted
    formatted=$(date -d "$date_str" '+%Y-%m-%d %H:%M' 2>/dev/null)
    if [ -z "$formatted" ]; then
        # macOS 兼容
        formatted=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$date_str" '+%Y-%m-%d %H:%M' 2>/dev/null)
    fi
    if [ -z "$formatted" ]; then
        echo "$date_str"
    else
        echo "$formatted"
    fi
}

# 检测单个域名
check_domain() {
    local input=$1
    local domain port result
    local issuer subject not_before not_after days_left status

    # 解析域名和端口
    if [[ "$input" == *":"* ]]; then
        domain=$(echo "$input" | cut -d':' -f1)
        port=$(echo "$input" | cut -d':' -f2)
    else
        domain="$input"
        port="$PORT"
    fi

    ((TOTAL_COUNT++))

    # 获取证书信息
    result=$(get_cert_info "$domain" "$port")

    # 解析结果
    IFS='|' read -r issuer subject not_before not_after days_left status <<< "$result"

    # 输出结果
    case "$status" in
        OK)
            ((VALID_COUNT++))
            printf " %-35s %6d 天   ${GREEN}✓ 正常${NC}\n" "${domain}:${port}" "$days_left"
            ;;
        WARNING)
            ((WARN_COUNT++))
            printf " %-35s %6d 天   ${YELLOW}⚠ 警告${NC}\n" "${domain}:${port}" "$days_left"
            ;;
        CRITICAL)
            ((CRIT_COUNT++))
            printf " %-35s %6d 天   ${RED}⚠ 严重${NC}\n" "${domain}:${port}" "$days_left"
            ;;
        EXPIRED)
            ((ERROR_COUNT++))
            printf " %-35s %6d 天   ${RED}✗ 已过期${NC}\n" "${domain}:${port}" "$days_left"
            ;;
        ERROR|PARSE_ERROR|*)
            ((ERROR_COUNT++))
            printf " %-35s %6s     ${RED}✗ 获取失败${NC}\n" "${domain}:${port}" "-"
            ;;
    esac

    # 保存详细结果
    RESULTS+=("${domain}|${port}|${issuer}|${subject}|${not_before}|${not_after}|${days_left}|${status}")
}

# 从文件读取并检测
check_from_file() {
    local file=$1

    if [ ! -f "$file" ]; then
        die "文件 '$file' 不存在"
    fi

    if [ ! -r "$file" ]; then
        die "无法读取文件 '$file'"
    fi

    local domains=()
    while IFS= read -r line || [ -n "$line" ]; do
        # 去除注释和空白
        line=$(echo "$line" | sed 's/#.*//' | xargs)
        [ -z "$line" ] && continue
        domains+=("$line")
    done < "$file"

    if [ ${#domains[@]} -eq 0 ]; then
        die "配置文件中没有有效的域名"
    fi

    echo "检测目标: ${#domains[@]} 个域名"
    echo "告警阈值: 警告 ${WARN_DAYS} 天, 严重 ${CRIT_DAYS} 天"
    echo "超时时间: ${TIMEOUT} 秒"
    echo ""
    print_dash_line
    printf " %-35s %10s   %s\n" "域名" "剩余有效期" "状态"
    print_dash_line

    for domain in "${domains[@]}"; do
        check_domain "$domain"
    done

    print_dash_line
}

# 打印详细信息
print_details() {
    if [ ${#RESULTS[@]} -eq 0 ]; then
        return
    fi

    echo ""
    echo -e "${BLUE}【详细信息】${NC}"
    echo ""

    for result in "${RESULTS[@]}"; do
        IFS='|' read -r domain port issuer subject not_before not_after days_left status <<< "$result"

        if [ "$status" = "ERROR" ] || [ "$status" = "PARSE_ERROR" ]; then
            echo -e "  ${CYAN}${domain}:${port}${NC}"
            echo -e "    状态: ${RED}获取证书失败${NC}"
            echo ""
            continue
        fi

        local status_text status_color
        case "$status" in
            OK)       status_text="正常"; status_color="$GREEN" ;;
            WARNING)  status_text="即将过期(警告)"; status_color="$YELLOW" ;;
            CRITICAL) status_text="即将过期(严重)"; status_color="$RED" ;;
            EXPIRED)  status_text="已过期"; status_color="$RED" ;;
        esac

        echo -e "  ${CYAN}${domain}:${port}${NC}"
        echo "    颁发者: ${issuer:-未知}"
        echo "    证书主题: ${subject:-未知}"
        echo "    生效时间: $(format_date "$not_before")"
        echo "    过期时间: $(format_date "$not_after")"
        echo -e "    剩余天数: ${days_left} 天"
        echo -e "    状态: ${status_color}${status_text}${NC}"
        echo ""
    done
}

# 打印结果汇总
print_summary() {
    echo ""
    print_line
    echo -e "                           ${CYAN}检测结果汇总${NC}"
    print_line
    echo " 检测总数: $TOTAL_COUNT"
    echo -e " 正常: ${GREEN}$VALID_COUNT${NC}"
    echo -e " 警告 (≤${WARN_DAYS}天): ${YELLOW}$WARN_COUNT${NC}"
    echo -e " 严重 (≤${CRIT_DAYS}天): ${RED}$CRIT_COUNT${NC}"
    echo -e " 失败/过期: ${RED}$ERROR_COUNT${NC}"
    print_line
}

# 获取退出码
get_exit_code() {
    if [ "$ERROR_COUNT" -gt 0 ]; then
        echo 1
    elif [ "$CRIT_COUNT" -gt 0 ]; then
        echo 2
    elif [ "$WARN_COUNT" -gt 0 ]; then
        echo 3
    else
        echo 0
    fi
}

# 解析参数
while getopts "f:p:w:c:t:h" opt; do
    case $opt in
        f)
            DOMAINS_FILE="$OPTARG"
            ;;
        p)
            PORT="$OPTARG"
            if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
                die "无效的端口号: $PORT"
            fi
            ;;
        w)
            WARN_DAYS="$OPTARG"
            if ! [[ "$WARN_DAYS" =~ ^[0-9]+$ ]]; then
                die "警告阈值必须是正整数: $WARN_DAYS"
            fi
            ;;
        c)
            CRIT_DAYS="$OPTARG"
            if ! [[ "$CRIT_DAYS" =~ ^[0-9]+$ ]]; then
                die "严重阈值必须是正整数: $CRIT_DAYS"
            fi
            ;;
        t)
            TIMEOUT="$OPTARG"
            if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
                die "超时时间必须是正整数: $TIMEOUT"
            fi
            ;;
        h)
            usage
            ;;
        *)
            usage
            ;;
    esac
done

shift $((OPTIND - 1))

# 检查依赖
check_dependencies

# 验证阈值
if [ "$CRIT_DAYS" -gt "$WARN_DAYS" ]; then
    die "严重阈值 ($CRIT_DAYS) 不能大于警告阈值 ($WARN_DAYS)"
fi

# 主逻辑
print_header

if [ -n "$DOMAINS_FILE" ]; then
    check_from_file "$DOMAINS_FILE"
elif [ $# -ge 1 ]; then
    echo "检测目标: $# 个域名"
    echo "告警阈值: 警告 ${WARN_DAYS} 天, 严重 ${CRIT_DAYS} 天"
    echo "超时时间: ${TIMEOUT} 秒"
    echo ""
    print_dash_line
    printf " %-35s %10s   %s\n" "域名" "剩余有效期" "状态"
    print_dash_line

    for domain in "$@"; do
        check_domain "$domain"
    done

    print_dash_line
else
    echo -e "${YELLOW}提示: 使用 -h 参数查看帮助信息${NC}"
    echo ""
    usage
fi

print_details
print_summary

exit "$(get_exit_code)"
