This commit is contained in:
2026-05-19 10:10:03 +08:00
parent 3df2d52002
commit ed72a62687
4 changed files with 307 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env sh
set -eu
# Add certificates for new domains on an already running OpenResty gateway.
# This flow is separate from init-certs-core.sh and never restarts containers.
CERT_ROOT="./certs/live"
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
CONF_DIR="${CONF_DIR:-./conf/conf.d}"
SKIP_PUBLIC_HTTP_CHECK="${SKIP_PUBLIC_HTTP_CHECK:-0}"
DUMMY_DOMAINS=""
HTTP_PROBE_DIR="./www/.well-known/acme-challenge"
HTTP_PROBE_TOKEN="add-domain-certs-probe-$$"
HTTP_PROBE_PATH=".well-known/acme-challenge/$HTTP_PROBE_TOKEN"
HTTP_PROBE_VALUE="openresty-gateway-add-domain-probe-$$"
cd "$ROOT_DIR"
usage() {
cat <<EOF
Usage: sh add-domain-certs.sh [options]
Options:
--skip-public-http-check Skip public domain HTTP challenge probe
-h, --help Show this help
Environment:
CONF_DIR=./conf/conf.d Directory for per-domain nginx configs
SKIP_PUBLIC_HTTP_CHECK=1 Same as --skip-public-http-check
EOF
}
is_enabled() {
case "${1:-}" in
1|true|TRUE|yes|YES|on|ON)
return 0
;;
*)
return 1
;;
esac
}
domain_conf_matches() {
domain="$1"
conf_file="$2"
awk -v domain="$domain" '
/^[[:space:]]*server_name[[:space:]]/ {
for (i = 2; i <= NF; i++) {
name = $i
sub(/;$/, "", name)
if (name == domain) {
found = 1
}
}
}
END {
exit found ? 0 : 1
}
' "$conf_file"
}
find_domain_conf() {
domain="$1"
exact_conf_file="$CONF_DIR/$domain.conf"
if [ -f "$exact_conf_file" ]; then
echo "$exact_conf_file"
return 0
fi
for conf_file in "$CONF_DIR"/*.conf; do
[ -f "$conf_file" ] || continue
if domain_conf_matches "$domain" "$conf_file"; then
echo "$conf_file"
return 0
fi
done
return 1
}
cleanup_http_probe() {
rm -f "$HTTP_PROBE_DIR/$HTTP_PROBE_TOKEN"
}
check_http_challenge() {
if ! command -v curl >/dev/null 2>&1; then
echo "Warning: curl is missing; skipping HTTP-01 reachability probe." >&2
return 0
fi
mkdir -p "$HTTP_PROBE_DIR"
printf '%s' "$HTTP_PROBE_VALUE" > "$HTTP_PROBE_DIR/$HTTP_PROBE_TOKEN"
trap cleanup_http_probe EXIT HUP INT TERM
for domain in $DOMAINS; do
url="http://$domain/$HTTP_PROBE_PATH"
echo "Checking local HTTP-01 route for $domain..."
if ! body="$(curl -fsS --max-time 5 --resolve "$domain:80:127.0.0.1" "$url" 2>&1)"; then
echo "Error: OpenResty is not serving the challenge path for $domain on local port 80." >&2
echo "Tried: $url via 127.0.0.1" >&2
echo "$body" >&2
return 1
fi
if [ "$body" != "$HTTP_PROBE_VALUE" ]; then
echo "Error: local challenge response mismatch for $domain." >&2
echo "Expected: $HTTP_PROBE_VALUE" >&2
echo "Got: $body" >&2
return 1
fi
if is_enabled "$SKIP_PUBLIC_HTTP_CHECK"; then
echo "Skipping public HTTP-01 route check for $domain."
continue
fi
echo "Checking public HTTP-01 route for $domain..."
if ! body="$(curl -fsS --max-time 10 "$url" 2>&1)"; then
echo "Error: $domain is not reachable on public HTTP port 80." >&2
echo "Tried: $url" >&2
echo "$body" >&2
echo "If this host cannot hairpin to its public IP but external clients can, rerun with SKIP_PUBLIC_HTTP_CHECK=1." >&2
return 1
fi
if [ "$body" != "$HTTP_PROBE_VALUE" ]; then
echo "Error: public challenge response mismatch for $domain." >&2
echo "Expected: $HTTP_PROBE_VALUE" >&2
echo "Got: $body" >&2
return 1
fi
done
}
reload_openresty() {
echo "Validating OpenResty configuration..."
compose exec -T openresty openresty -t
echo "Reloading OpenResty without restart..."
compose exec -T openresty openresty -s reload
}
remove_dummy_certs() {
for domain in $DUMMY_DOMAINS; do
cert_dir="$CERT_ROOT/$domain"
rm -f "$cert_dir/fullchain.pem" "$cert_dir/privkey.pem" "$cert_dir/.dummy-init-certs"
rmdir "$cert_dir" 2>/dev/null || true
done
}
while [ "$#" -gt 0 ]; do
case "$1" in
--skip-public-http-check)
SKIP_PUBLIC_HTTP_CHECK=1
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
domain_count=0
for domain in $DOMAINS; do
domain_count=$((domain_count + 1))
done
if [ "$domain_count" -eq 0 ]; then
echo "Error: DOMAINS is required. Set it in add-domain-certs.sh." >&2
exit 1
fi
if [ -z "${CERT_EMAIL:-}" ]; then
echo "Error: CERT_EMAIL is required. Set it in add-domain-certs.sh." >&2
exit 1
fi
. "$ROOT_DIR/scripts/lib-compose.sh"
echo "Validating Docker Compose configuration..."
compose config >/dev/null
echo "Checking that OpenResty is already running..."
compose exec -T openresty openresty -v >/dev/null
conf_error=0
for domain in $DOMAINS; do
if ! conf_file="$(find_domain_conf "$domain")"; then
CONF_DIR="$CONF_DIR" sh "$ROOT_DIR/scripts/ensure-domain-conf.sh" "$domain"
conf_file="$CONF_DIR/$domain.conf"
fi
if ! grep -Fq "/.well-known/acme-challenge/" "$conf_file"; then
echo "Error: nginx config lacks ACME challenge location: $conf_file" >&2
conf_error=1
fi
done
if [ "$conf_error" -ne 0 ]; then
exit 1
fi
export DOMAINS CERT_ROOT
DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")"
reload_openresty
check_http_challenge
if [ -n "$DUMMY_DOMAINS" ]; then
echo "Removing dummy certificate files before requesting real certificates..."
remove_dummy_certs
fi
if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then
echo "Certificate request failed; restoring dummy certificates." >&2
sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null || true
reload_openresty >/dev/null 2>&1 || true
exit 1
fi
reload_openresty
echo "Done."