cat >/usr/local/bin/issue_cert.sh <<'EOF' #!/bin/sh # POSIX sh script: compatible with Debian/Ubuntu/Alpine/busybox set -eu 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')" "$*"; } err() { printf '\n[ERROR] %s\n' "$*" >&2; } usage() { cat < [--email you@example.com] [--force] [--wildcard] 说明: 1) 优先 Cloudflare DNS API(检测到 CF_Token 时自动启用,支持自动续期) 2) 再尝试 standalone 80(需要公网80可达,支持自动续期但续期仍需80) 3) 最后保底手动 DNS(可签发但无法自动续期) 输出: /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 } # ---- parse args ---- [ $# -ge 1 ] || { usage; exit 1; } DOMAIN="$1"; shift EMAIL="" FORCE="0" WILDCARD="0" while [ $# -gt 0 ]; do case "$1" in --email) EMAIL="${2:-}"; [ -n "$EMAIL" ] || { err "--email 缺少参数"; exit 1; }; shift 2;; --force) FORCE="1"; shift;; --wildcard) WILDCARD="1"; shift;; -h|--help) usage; exit 0;; *) err "未知参数: $1"; usage; exit 1;; esac done # ---- find acme.sh ---- find_acme() { if [ -n "${ACME_SH}" ] && [ -x "${ACME_SH}" ]; then echo "${ACME_SH}"; return 0 fi # 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 ! 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 [ -n "$ACME" ] || { err "仍未找到 acme.sh。请确认安装用户与执行用户一致,或用 ACME_SH 指定路径。"; exit 1; } 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" KEY_PATH="${INSTALL_DIR}/${KEY_NAME}" FULLCHAIN_PATH="${INSTALL_DIR}/${FULLCHAIN_NAME}" # ---- 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" # 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" 2>/dev/null || true if command -v openssl >/dev/null 2>&1; then log "证书信息:" openssl x509 -in "$FULLCHAIN_PATH" -noout -subject -issuer -dates 2>/dev/null || true fi } 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() { if [ "$WILDCARD" = "1" ]; then log "通配符不支持 standalone 80,跳过。" return 1 fi 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 "提示:手动 DNS 无法自动续期(续期仍需手动加 TXT)。" # shellcheck disable=SC2086 $ACME --issue $ISSUE_DOMAINS --dns $FORCE_FLAG $DBG || return 1 log "上面已输出 TXT 记录,请去 DNS 面板添加,等待生效后再执行:" echo " $ACME --renew -d $DOMAIN --force $DBG" return 0 } log "开始签发:$DOMAIN" log "输出目录:$INSTALL_DIR" if try_cf_dns; then install_cert log "完成:DNS API 模式支持自动续期(acme.sh 自带 cronjob 管理)。" exit 0 fi if try_standalone; then install_cert log "完成:standalone 模式支持自动续期,但续期时仍需要 80 可用且公网可达。" exit 0 fi if try_manual_dns; then log "已进入手动 DNS 模式:按提示添加 TXT 后续期/完成。" exit 0 fi err "全部方式失败。建议:DEBUG_LEVEL=2 issue_cert.sh $DOMAIN --force 观察详细日志。" exit 1 EOF chmod +x /usr/local/bin/issue_cert.sh