203 lines
5.6 KiB
Plaintext
203 lines
5.6 KiB
Plaintext
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 <<USAGE
|
||
用法:
|
||
issue_cert.sh <domain> [--email you@example.com] [--force] [--wildcard]
|
||
|
||
说明:
|
||
1) 优先 Cloudflare DNS API(检测到 CF_Token 时自动启用,支持自动续期)
|
||
2) 再尝试 standalone 80(需要公网80可达,支持自动续期但续期仍需80)
|
||
3) 最后保底手动 DNS(可签发但无法自动续期)
|
||
|
||
输出:
|
||
/data/<domain>/key.pem
|
||
/data/<domain>/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
|