diff --git a/ssl b/ssl index 9721f48..70eb610 100644 --- a/ssl +++ b/ssl @@ -1,21 +1,17 @@ cat >/usr/local/bin/issue_cert.sh <<'EOF' -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +# POSIX sh script: compatible with Debian/Ubuntu/Alpine/busybox +set -eu -# ===== 可调参数 ===== -INSTALL_BASE="/data" # 证书安装目录根路径 -ACME="${HOME}/.acme.sh/acme.sh" # acme.sh 路径(默认安装位置) -CA_SERVER="letsencrypt" # 默认使用 Let's Encrypt +INSTALL_BASE="/data" KEY_NAME="key.pem" FULLCHAIN_NAME="fullchain.pem" +CA_SERVER="letsencrypt" +DEBUG_LEVEL="${DEBUG_LEVEL:-0}" # 0=off, 1=--debug, 2=--debug 2 +ACME_SH="${ACME_SH:-}" # allow override: ACME_SH=/path/to/acme.sh issue_cert.sh xxx -# ===== 工具函数 ===== -log() { printf "\n[%s] %s\n" "$(date '+%F %T')" "$*"; } -die() { printf "\n[ERROR] %s\n" "$*" >&2; exit 1; } - -need_cmd() { - command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1,请先安装。" -} +log() { printf '\n[%s] %s\n' "$(date '+%F %T')" "$*"; } +err() { printf '\n[ERROR] %s\n' "$*" >&2; } usage() { cat < [--email you@example.com] [--force] [--wildcard] 说明: - - 自动尝试多种签发机制(Cloudflare DNS API -> standalone 80 -> 手动DNS保底) - - 证书安装到: ${INSTALL_BASE}//${KEY_NAME} 与 ${FULLCHAIN_NAME} - - 自动续期: DNS API/standalone 都会被 acme.sh cron 管理(手动DNS无法自动续期) + 1) 优先 Cloudflare DNS API(检测到 CF_Token 时自动启用,支持自动续期) + 2) 再尝试 standalone 80(需要公网80可达,支持自动续期但续期仍需80) + 3) 最后保底手动 DNS(可签发但无法自动续期) -参数: - --email 指定邮箱(首次安装 acme.sh 时用过也可不填) - --force 强制重签 - --wildcard 同时签发通配符 *.domain(仅DNS方式支持;standalone不支持) +输出: + /data//key.pem + /data//fullchain.pem + +可选环境变量: + ACME_SH=/root/.acme.sh/acme.sh # 指定 acme.sh 路径 + DEBUG_LEVEL=1 或 2 # 打开调试输出(对应 acme.sh --debug 或 --debug 2) + +示例: + issue_cert.sh ui.shanghi.net + CF_Token=xxx issue_cert.sh ui.shanghi.net + DEBUG_LEVEL=2 issue_cert.sh ui.shanghi.net --force USAGE } -# ===== 解析参数 ===== -if [[ $# -lt 1 ]]; then usage; exit 1; fi +# ---- parse args ---- +[ $# -ge 1 ] || { usage; exit 1; } +DOMAIN="$1"; shift -DOMAIN="" EMAIL="" FORCE="0" WILDCARD="0" -DOMAIN="$1"; shift || true -while [[ $# -gt 0 ]]; do +while [ $# -gt 0 ]; do case "$1" in - --email) EMAIL="${2:-}"; shift 2;; + --email) EMAIL="${2:-}"; [ -n "$EMAIL" ] || { err "--email 缺少参数"; exit 1; }; shift 2;; --force) FORCE="1"; shift;; --wildcard) WILDCARD="1"; shift;; -h|--help) usage; exit 0;; - *) die "未知参数: $1";; + *) err "未知参数: $1"; usage; exit 1;; esac done -# ===== 前置检查 ===== -need_cmd curl -need_cmd mkdir -need_cmd awk -need_cmd sed +# ---- find acme.sh ---- +find_acme() { + if [ -n "${ACME_SH}" ] && [ -x "${ACME_SH}" ]; then + echo "${ACME_SH}"; return 0 + fi -if [[ ! -x "$ACME" ]]; then + # Common locations + if [ -x "$HOME/.acme.sh/acme.sh" ]; then echo "$HOME/.acme.sh/acme.sh"; return 0; fi + if [ -x "/root/.acme.sh/acme.sh" ]; then echo "/root/.acme.sh/acme.sh"; return 0; fi + + # Try to locate in PATH + if command -v acme.sh >/dev/null 2>&1; then + command -v acme.sh; return 0 + fi + + return 1 +} + +ACME="$(find_acme || true)" + +# ---- install acme.sh if missing ---- +if [ -z "$ACME" ]; then log "未发现 acme.sh,开始安装..." - if [[ -n "${EMAIL}" ]]; then - curl -fsSL https://get.acme.sh | sh -s email="${EMAIL}" + if ! command -v curl >/dev/null 2>&1; then + err "系统没有 curl,请先安装 curl。"; exit 1 + fi + + if [ -n "$EMAIL" ]; then + curl -fsSL https://get.acme.sh | sh -s email="$EMAIL" else curl -fsSL https://get.acme.sh | sh fi + + ACME="$(find_acme || true)" fi -# 兼容 alias 未生效情况 -if [[ ! -x "$ACME" ]]; then - die "acme.sh 安装后仍未找到: $ACME。请确认是否安装在 ${HOME}/.acme.sh/ 下。" -fi +[ -n "$ACME" ] || { err "仍未找到 acme.sh。请确认安装用户与执行用户一致,或用 ACME_SH 指定路径。"; exit 1; } -# 设定默认 CA -log "设置默认 CA 为: ${CA_SERVER}" -"$ACME" --set-default-ca --server "${CA_SERVER}" >/dev/null +log "使用 acme.sh: $ACME" +# ---- prepare debug flags ---- +DBG="" +if [ "$DEBUG_LEVEL" = "1" ]; then DBG="--debug"; fi +if [ "$DEBUG_LEVEL" = "2" ]; then DBG="--debug 2"; fi + +# ---- set CA ---- +log "设置默认 CA 为: $CA_SERVER" +# shellcheck disable=SC2086 +$ACME --set-default-ca --server "$CA_SERVER" $DBG >/dev/null 2>&1 || true + +# ---- prepare install dir ---- INSTALL_DIR="${INSTALL_BASE}/${DOMAIN}" -mkdir -p "${INSTALL_DIR}" +mkdir -p "$INSTALL_DIR" KEY_PATH="${INSTALL_DIR}/${KEY_NAME}" FULLCHAIN_PATH="${INSTALL_DIR}/${FULLCHAIN_NAME}" -ISSUE_ARGS=(--issue -d "${DOMAIN}") -if [[ "${WILDCARD}" == "1" ]]; then - # 通配符只适用于 DNS 验证 - ISSUE_ARGS=(--issue -d "${DOMAIN}" -d "*.${DOMAIN}") -fi -if [[ "${FORCE}" == "1" ]]; then - ISSUE_ARGS+=("--force") +# ---- build issue args ---- +ISSUE_DOMAINS="-d $DOMAIN" +if [ "$WILDCARD" = "1" ]; then + # 注意:通配符只适用于 DNS 验证。并且对 ui.shanghi.net 这样的子域, + # 通配符 *.ui.shanghi.net 通常没意义。一般通配符用于 shanghi.net -> *.shanghi.net + ISSUE_DOMAINS="-d $DOMAIN -d *.$DOMAIN" fi +FORCE_FLAG="" +[ "$FORCE" = "1" ] && FORCE_FLAG="--force" + install_cert() { - log "安装证书到: ${INSTALL_DIR}" - "$ACME" --install-cert -d "${DOMAIN}" \ - --key-file "${KEY_PATH}" \ - --fullchain-file "${FULLCHAIN_PATH}" \ - --reloadcmd "echo cert_installed_for_${DOMAIN}" >/dev/null + log "安装证书到: $INSTALL_DIR" + # shellcheck disable=SC2086 + $ACME --install-cert -d "$DOMAIN" \ + --key-file "$KEY_PATH" \ + --fullchain-file "$FULLCHAIN_PATH" \ + --reloadcmd "echo cert_installed_for_$DOMAIN" $DBG - log "完成。证书文件:" - ls -l "${KEY_PATH}" "${FULLCHAIN_PATH}" || true + log "证书文件已生成:" + ls -l "$KEY_PATH" "$FULLCHAIN_PATH" 2>/dev/null || true if command -v openssl >/dev/null 2>&1; then log "证书信息:" - openssl x509 -in "${FULLCHAIN_PATH}" -noout -subject -issuer -dates || true + openssl x509 -in "$FULLCHAIN_PATH" -noout -subject -issuer -dates 2>/dev/null || true fi } -try_cloudflare_dns() { - # 若 WILDCARD=1 必须 DNS,且 Cloudflare 是最稳之一 - if [[ -n "${CF_Token:-}" ]]; then - log "检测到 CF_Token,尝试 Cloudflare DNS API(dns_cf)签发..." - "$ACME" "${ISSUE_ARGS[@]}" --dns dns_cf && return 0 - log "Cloudflare DNS API 签发失败,将尝试其他方式。" - return 1 +try_cf_dns() { + if [ -n "${CF_Token:-}" ]; then + log "检测到 CF_Token,尝试 Cloudflare DNS API(dns_cf)..." + # shellcheck disable=SC2086 + $ACME --issue $ISSUE_DOMAINS --dns dns_cf $FORCE_FLAG $DBG && return 0 + log "Cloudflare DNS API 失败,继续尝试其他方式。" fi return 1 } -try_standalone_80() { - if [[ "${WILDCARD}" == "1" ]]; then - log "通配符证书不支持 standalone 80 验证,跳过。" +try_standalone() { + if [ "$WILDCARD" = "1" ]; then + log "通配符不支持 standalone 80,跳过。" return 1 fi - # 仅做基础检查:80 端口本地可监听(不等于公网可达,但能过滤一部分情况) - if command -v ss >/dev/null 2>&1; then - if ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE '(:80$)|(\[::\]:80$)'; then - log "检测到本机已有服务占用 80 端口,standalone 可能失败(仍会尝试)。" - fi - fi - - log "尝试 standalone 80(需要公网 80 可达)..." - "$ACME" "${ISSUE_ARGS[@]}" --standalone && return 0 - log "standalone 80 签发失败。" + log "尝试 standalone 80(需要公网80可达,且本机可绑定80)..." + # shellcheck disable=SC2086 + $ACME --issue $ISSUE_DOMAINS --standalone $FORCE_FLAG $DBG && return 0 + log "standalone 80 失败。" return 1 } try_manual_dns() { log "进入保底:手动 DNS TXT 验证(--dns)。" - log "注意:这种模式无法自动续期;续期时仍需要你手动添加 TXT。" - "$ACME" "${ISSUE_ARGS[@]}" --dns - log "上面输出了 TXT 记录,请去 DNS 面板添加后,等待生效。" - log "添加完成后,执行下面命令完成签发/续期:" - echo " ${ACME} --renew -d ${DOMAIN} --force" - echo - log "当你确认 TXT 已生效后,也可以直接回车继续(脚本会尝试 renew)。" - read -r -p "按回车继续(或 Ctrl+C 退出): " _ + log "提示:手动 DNS 无法自动续期(续期仍需手动加 TXT)。" + # shellcheck disable=SC2086 + $ACME --issue $ISSUE_DOMAINS --dns $FORCE_FLAG $DBG || return 1 - "$ACME" --renew -d "${DOMAIN}" --force + log "上面已输出 TXT 记录,请去 DNS 面板添加,等待生效后再执行:" + echo " $ACME --renew -d $DOMAIN --force $DBG" return 0 } -main() { - log "开始签发域名证书:${DOMAIN}" - log "安装目录:${INSTALL_DIR}" +log "开始签发:$DOMAIN" +log "输出目录:$INSTALL_DIR" - # 1) Cloudflare DNS API(最稳) - if try_cloudflare_dns; then - install_cert - log "提示:DNS API 模式支持自动续期(acme.sh cronjob 已自动管理)。" - exit 0 - fi +if try_cf_dns; then + install_cert + log "完成:DNS API 模式支持自动续期(acme.sh 自带 cronjob 管理)。" + exit 0 +fi - # 2) standalone 80 - if try_standalone_80; then - install_cert - log "提示:standalone 模式支持自动续期,但续期时仍需要 80 可用且公网可达。" - exit 0 - fi +if try_standalone; then + install_cert + log "完成:standalone 模式支持自动续期,但续期时仍需要 80 可用且公网可达。" + exit 0 +fi - # 3) 手动 DNS 保底 - if try_manual_dns; then - install_cert - log "提示:手动 DNS 模式不支持自动续期。建议尽快切换到 DNS API(如 Cloudflare Token)。" - exit 0 - fi +if try_manual_dns; then + log "已进入手动 DNS 模式:按提示添加 TXT 后续期/完成。" + exit 0 +fi - die "所有方式都失败了。建议使用:${ACME} --issue -d ${DOMAIN} --debug 2 查看详细日志。" -} - -main +err "全部方式失败。建议:DEBUG_LEVEL=2 issue_cert.sh $DOMAIN --force 观察详细日志。" +exit 1 EOF chmod +x /usr/local/bin/issue_cert.sh