2
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
server {
|
||||
# 文件名前缀是 000-,会比 ai.sggai.site.conf 更早加载。
|
||||
# 这样可以在不修改原配置文件的情况下,让这里的 mock 逻辑优先生效。
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name ai.sggai.site;
|
||||
@@ -19,7 +17,6 @@ server {
|
||||
gzip off;
|
||||
gunzip off;
|
||||
|
||||
# 公共反代配置放在 server 级别,主后端 proxy_pass location 会继承这些配置。
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
@@ -57,59 +54,45 @@ server {
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
|
||||
# 只拦截很小的非流式 Chat Completions 请求。
|
||||
# 不满足条件的请求会内部跳转到正常后端,不直接返回 mock。
|
||||
location = /v1/chat/completions {
|
||||
client_body_buffer_size 16k;
|
||||
|
||||
content_by_lua_block {
|
||||
-- 不满足 mock 条件时,统一内部跳转到这个 named location。
|
||||
local backend = "@ai_sggai_site_backend"
|
||||
|
||||
-- 请求体总大小上限:只处理非常小的探测类请求。
|
||||
local max_body_bytes = 1024
|
||||
|
||||
-- 只处理 POST;其它方法按普通请求转发到真实后端。
|
||||
if ngx.req.get_method() ~= "POST" then
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- 先看 Content-Length,超过阈值就不读取 body,直接放行。
|
||||
local content_length = tonumber(ngx.var.http_content_length)
|
||||
if not content_length then
|
||||
-- 没有 Content-Length 的请求可能是 chunked,不为了判断 mock 去读取未知大小的 body。
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- 请求体太大,说明不是目标小请求,直接走真实后端。
|
||||
if content_length > max_body_bytes then
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- 到这里说明 body 足够小,可以安全读取并解析 JSON。
|
||||
ngx.req.read_body()
|
||||
|
||||
local body = ngx.req.get_body_data()
|
||||
if not body or #body > max_body_bytes then
|
||||
-- body 不在内存里或实际大小仍然超限时,不 mock,转发到真实后端。
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- JSON 解析失败,或 stream 不是 false,都不 mock。
|
||||
local cjson = require "cjson.safe"
|
||||
local payload = cjson.decode(body)
|
||||
if type(payload) ~= "table" or payload.stream ~= false then
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- 必须有 messages 数组;没有就不 mock。
|
||||
if type(payload.messages) ~= "table" then
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
local has_hi_content = false
|
||||
|
||||
-- 遍历所有 message,只匹配精确的 "content": "hi"。
|
||||
for _, message in ipairs(payload.messages) do
|
||||
if type(message) == "table" and message.content == "hi" then
|
||||
has_hi_content = true
|
||||
@@ -117,12 +100,10 @@ server {
|
||||
end
|
||||
end
|
||||
|
||||
-- 没有精确命中 "content": "hi",交给真实后端处理。
|
||||
if not has_hi_content then
|
||||
return ngx.exec(backend)
|
||||
end
|
||||
|
||||
-- 命中小请求 + stream=false + "content": "hi" 条件,直接返回 mock 响应。
|
||||
ngx.status = ngx.HTTP_OK
|
||||
ngx.header["Content-Type"] = "application/json; charset=utf-8"
|
||||
ngx.say([[{
|
||||
@@ -143,12 +124,10 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# 仅供上面的 Lua 逻辑通过 ngx.exec 内部跳转使用,外部 URL 不能直接访问这个 location。
|
||||
location @ai_sggai_site_backend {
|
||||
proxy_pass http://10.1.0.1:3001;
|
||||
}
|
||||
|
||||
# 普通流量保持原来的上游转发行为。
|
||||
location / {
|
||||
proxy_pass http://10.1.0.1:3001;
|
||||
}
|
||||
|
||||
@@ -5,21 +5,20 @@ services:
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# 主配置
|
||||
# Main config
|
||||
- ./conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
|
||||
# HTTP 配置
|
||||
# HTTP virtual hosts
|
||||
- ./conf/conf.d:/usr/local/openresty/nginx/conf/conf.d:ro
|
||||
# Stream 配置
|
||||
# TCP/stream virtual hosts
|
||||
- ./conf/stream.d:/usr/local/openresty/nginx/conf/stream.d:ro
|
||||
# 静态网站 + acme challenge
|
||||
# Static sites and ACME HTTP-01 challenges
|
||||
- ./www:/var/www
|
||||
# SSL 证书
|
||||
# Let's Encrypt certificates
|
||||
- ./certs:/etc/letsencrypt
|
||||
# 日志
|
||||
# OpenResty logs
|
||||
- ./logs:/usr/local/openresty/nginx/logs
|
||||
|
||||
# Certbot 是给脚本或手动命令使用的工具容器,不是常驻服务。
|
||||
# ./www 用于 ACME HTTP-01 域名验证,./certs 用于持久化保存签发的证书。
|
||||
# Tool container used by scripts and manual certbot commands.
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
container_name: certbot
|
||||
|
||||
@@ -3,12 +3,11 @@ set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
|
||||
# 需要签证的域名列表。
|
||||
# 注意:这里的域名必须覆盖 nginx 配置里所有 ssl_certificate 使用到的域名。
|
||||
# 如果以后新增 HTTPS 域名,也要同步加到这里,否则 OpenResty 可能因为缺证书启动失败。
|
||||
# Domains that need certificates. This list must cover every
|
||||
# /etc/letsencrypt/live/<domain>/ certificate referenced by nginx configs.
|
||||
DOMAINS="ai.sggai.site dms.sggai.site lsbd2.loveteemo.com"
|
||||
|
||||
# Let's Encrypt 注册邮箱。
|
||||
# Let's Encrypt registration email.
|
||||
CERT_EMAIL="243823965@qq.com"
|
||||
|
||||
export DOMAINS CERT_EMAIL
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 为传入的域名生成基础 nginx 配置。
|
||||
# 如果 conf/conf.d/xxx.conf 已经存在,直接跳过,避免覆盖人工维护的配置。
|
||||
# Generate a minimal nginx virtual host for each missing domain config.
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
|
||||
# 默认写入 openresty-gateway/conf/conf.d。
|
||||
# 如需生成到其他目录,可以在执行前设置 CONF_DIR,例如:
|
||||
# CONF_DIR="./conf/test.d" sh scripts/ensure-domain-conf.sh example.com
|
||||
CONF_DIR="${CONF_DIR:-./conf/conf.d}"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# 必须显式传入域名,避免无参数执行时静默成功但什么都不做。
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "用法:sh scripts/ensure-domain-conf.sh <域名> [域名...]" >&2
|
||||
echo "Usage: sh scripts/ensure-domain-conf.sh <domain> [domain...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -23,15 +17,12 @@ ensure_domain_conf() {
|
||||
domain="$1"
|
||||
conf_file="$CONF_DIR/$domain.conf"
|
||||
|
||||
# 已存在的域名配置不重新生成,避免覆盖已有转发、限流、路径规则等自定义配置。
|
||||
if [ -f "$conf_file" ]; then
|
||||
echo "跳过已存在的 nginx 配置:$conf_file"
|
||||
echo "Skipping existing nginx config: $conf_file"
|
||||
return
|
||||
fi
|
||||
|
||||
# 这里只生成一个能响应 ACME webroot 校验的最小 HTTPS 站点模板。
|
||||
# 业务代理规则可以后续在生成的 conf 文件里按需补充。
|
||||
echo "创建 nginx 配置模板:$conf_file"
|
||||
echo "Creating nginx config template: $conf_file"
|
||||
mkdir -p "$CONF_DIR"
|
||||
cat > "$conf_file" <<EOF
|
||||
server {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 为缺失证书的域名生成临时 dummy 证书。
|
||||
# 已有完整正式证书时直接跳过,不覆盖线上可用证书。
|
||||
# Create temporary self-signed certificates for domains whose certificate files
|
||||
# are missing. Existing real certificates are never overwritten.
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
CERT_ROOT="${CERT_ROOT:-./certs/live}"
|
||||
@@ -10,12 +10,12 @@ CERT_ROOT="${CERT_ROOT:-./certs/live}"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ -z "${DOMAINS:-}" ]; then
|
||||
echo "错误:必须设置 DOMAINS。" >&2
|
||||
echo "Error: DOMAINS is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "错误:缺少 openssl 命令。" >&2
|
||||
echo "Error: openssl is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -31,38 +31,30 @@ for domain in $DOMAINS; do
|
||||
|
||||
if [ -f "$cert_file" ] && [ -f "$key_file" ]; then
|
||||
if [ -f "$marker_file" ]; then
|
||||
# 上一次脚本可能中途失败,留下了 dummy 证书。
|
||||
# 继续把它当作 dummy 处理,后面会删除并重新申请正式证书。
|
||||
echo "复用已存在的 dummy 证书:$domain" >&2
|
||||
echo "Reusing existing dummy certificate: $domain" >&2
|
||||
echo "$domain"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 已经有正式证书时不覆盖,避免误删线上可用证书。
|
||||
echo "跳过已存在的正式证书:$domain" >&2
|
||||
echo "Skipping existing real certificate: $domain" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -f "$cert_file" ] || [ -f "$key_file" ]; then
|
||||
if [ -f "$marker_file" ]; then
|
||||
# 上次生成 dummy 证书时可能中途退出,留下了不完整文件。
|
||||
# 这些文件由本脚本创建,可以安全清理后重建。
|
||||
echo "清理不完整的 dummy 证书:$domain" >&2
|
||||
echo "Cleaning incomplete dummy certificate: $domain" >&2
|
||||
rm -f "$cert_file" "$key_file" "$marker_file" "$tmp_cert_file" "$tmp_key_file"
|
||||
else
|
||||
# 只存在证书或只存在私钥,状态不完整。
|
||||
# 自动处理可能误删用户文件,所以直接停止,让用户手工确认。
|
||||
echo "错误:$domain 存在不完整的证书文件,请手动检查目录:$cert_dir" >&2
|
||||
echo "Error: incomplete certificate files exist for $domain: $cert_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$cert_file" ] && [ ! -f "$key_file" ]; then
|
||||
# marker 先创建,避免 openssl 成功后脚本中断时留下无 marker 的 dummy 文件。
|
||||
rm -f "$tmp_cert_file" "$tmp_key_file"
|
||||
: > "$marker_file"
|
||||
|
||||
echo "创建 dummy 证书:$domain" >&2
|
||||
echo "Creating dummy certificate: $domain" >&2
|
||||
if ! openssl req -x509 -nodes -newkey rsa:2048 -days 1 \
|
||||
-keyout "$tmp_key_file" \
|
||||
-out "$tmp_cert_file" \
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 首次部署用:
|
||||
# 1. 校验每个域名都有 nginx 配置,且配置里放行 ACME HTTP-01 校验路径。
|
||||
# 2. 为缺失的证书路径生成 1 天有效期的自签 dummy 证书,避免 OpenResty 因证书文件不存在而启动失败。
|
||||
# 3. 启动 OpenResty,让 certbot 的 webroot 校验可以访问 /.well-known/acme-challenge/。
|
||||
# 4. 删除本次创建的 dummy 证书文件,避免挡住 certbot 创建正式证书目录。
|
||||
# 5. 运行 Certbot,为每个域名单独签发正式证书。
|
||||
# 6. 重启 OpenResty,让正式证书生效。
|
||||
# 7. 按需安装证书自动续期 cron 任务,后续证书会定期检查续期。
|
||||
# First deployment flow:
|
||||
# 1. Validate nginx configs and ACME HTTP-01 challenge locations.
|
||||
# 2. Create temporary dummy certificates for missing certificate files.
|
||||
# 3. Start OpenResty so HTTP-01 challenge files can be served.
|
||||
# 4. Verify that challenge files are reachable locally and via domain HTTP.
|
||||
# 5. Remove only the dummy certificates created or reused by this run.
|
||||
# 6. Request real Let's Encrypt certificates.
|
||||
# 7. Restart OpenResty so it loads the real certificates.
|
||||
# 8. Optionally install the renewal cron job.
|
||||
|
||||
# nginx 容器里挂载到 /etc/letsencrypt,宿主机这里对应 ./certs。
|
||||
# nginx 配置引用的是 /etc/letsencrypt/live/域名/fullchain.pem。
|
||||
CERT_ROOT="./certs/live"
|
||||
|
||||
# 无论用户从哪个目录执行脚本,都先切到项目根目录,保证相对路径稳定。
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
CONF_ROOT="${CONF_ROOT:-./conf}"
|
||||
CONF_DIR="${CONF_DIR:-./conf/conf.d}"
|
||||
CREATE_MISSING_CONF="${CREATE_MISSING_CONF:-0}"
|
||||
INSTALL_RENEW_CRON="${INSTALL_RENEW_CRON:-1}"
|
||||
|
||||
# 记录本次脚本创建或复用的 dummy 证书域名。
|
||||
# 后面只删除这些 dummy 证书,不动已经存在的正式证书。
|
||||
SKIP_PUBLIC_HTTP_CHECK="${SKIP_PUBLIC_HTTP_CHECK:-0}"
|
||||
DUMMY_DOMAINS=""
|
||||
HTTP_PROBE_DIR="./www/.well-known/acme-challenge"
|
||||
HTTP_PROBE_TOKEN="init-certs-probe-$$"
|
||||
HTTP_PROBE_PATH=".well-known/acme-challenge/$HTTP_PROBE_TOKEN"
|
||||
HTTP_PROBE_VALUE="openresty-gateway-probe-$$"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
用法:sh init-certs.sh [选项]
|
||||
Usage: sh init-certs.sh [options]
|
||||
|
||||
选项:
|
||||
--create-missing-conf 为缺少 nginx 配置的域名生成基础模板
|
||||
--skip-renew-cron 跳过证书自动续期 cron 安装
|
||||
-h, --help 显示帮助
|
||||
Options:
|
||||
--create-missing-conf Generate a minimal nginx config for missing domains
|
||||
--skip-renew-cron Do not install the certificate renewal cron job
|
||||
-h, --help Show this help
|
||||
|
||||
环境变量:
|
||||
CREATE_MISSING_CONF=1 等同于 --create-missing-conf
|
||||
INSTALL_RENEW_CRON=0 等同于 --skip-renew-cron
|
||||
CONF_ROOT=./conf 指定 nginx 配置扫描根目录
|
||||
CONF_DIR=./conf/conf.d 指定 nginx 域名配置目录
|
||||
Environment:
|
||||
CREATE_MISSING_CONF=1 Same as --create-missing-conf
|
||||
INSTALL_RENEW_CRON=0 Same as --skip-renew-cron
|
||||
CONF_ROOT=./conf Directory tree to scan for nginx configs
|
||||
CONF_DIR=./conf/conf.d Directory for per-domain nginx configs
|
||||
SKIP_PUBLIC_HTTP_CHECK=1 Skip public domain HTTP challenge probe
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -107,6 +107,62 @@ find_domain_conf() {
|
||||
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 "Check DNS, cloud security group, host firewall, and whether OpenResty is listening on 0.0.0.0:80." >&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
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--create-missing-conf)
|
||||
@@ -120,7 +176,7 @@ while [ "$#" -gt 0 ]; do
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "错误:未知参数:$1" >&2
|
||||
echo "Error: unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
@@ -129,16 +185,15 @@ while [ "$#" -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [ -z "${DOMAINS:-}" ]; then
|
||||
echo "错误:必须设置 DOMAINS。请先在 init-certs.sh 中配置,再调用 init-certs-core.sh。" >&2
|
||||
echo "Error: DOMAINS is required. Set it in init-certs.sh before calling init-certs-core.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CERT_EMAIL:-}" ]; then
|
||||
echo "错误:必须设置 CERT_EMAIL。请先在 init-certs.sh 中配置,再调用 init-certs-core.sh。" >&2
|
||||
echo "Error: CERT_EMAIL is required. Set it in init-certs.sh before calling init-certs-core.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 阶段 1:校验每个域名都有 nginx 配置和 ACME 校验路径。
|
||||
conf_error=0
|
||||
for domain in $DOMAINS; do
|
||||
if ! conf_file="$(find_domain_conf "$domain")"; then
|
||||
@@ -146,22 +201,20 @@ for domain in $DOMAINS; do
|
||||
CONF_DIR="$CONF_DIR" sh "$ROOT_DIR/scripts/ensure-domain-conf.sh" "$domain"
|
||||
conf_file="$CONF_DIR/$domain.conf"
|
||||
else
|
||||
echo "错误:缺少 nginx 配置:$CONF_DIR/$domain.conf" >&2
|
||||
echo "提示:确认配置文件存在,或执行 sh init-certs.sh --create-missing-conf 自动生成基础模板。" >&2
|
||||
echo "Error: missing nginx config: $CONF_DIR/$domain.conf" >&2
|
||||
echo "Hint: create the config or run sh init-certs.sh --create-missing-conf." >&2
|
||||
conf_error=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -Fq "/.well-known/acme-challenge/" "$conf_file"; then
|
||||
echo "错误:nginx 配置缺少 ACME 校验路径:$conf_file" >&2
|
||||
echo "提示:需要添加 location ^~ /.well-known/acme-challenge/ 并把 root 指向 /var/www。" >&2
|
||||
echo "Error: nginx config lacks ACME challenge location: $conf_file" >&2
|
||||
echo "Hint: add location ^~ /.well-known/acme-challenge/ with root /var/www." >&2
|
||||
conf_error=1
|
||||
fi
|
||||
done
|
||||
|
||||
# 反向检查 nginx 配置里引用的证书域名是否都被 DOMAINS 覆盖。
|
||||
# 否则 OpenResty 可能因为某个额外 HTTPS 站点缺证书而启动失败。
|
||||
if [ -d "$CONF_ROOT" ]; then
|
||||
ssl_domains="$(
|
||||
find "$CONF_ROOT" -type f -name '*.conf' | while IFS= read -r conf_file; do
|
||||
@@ -170,15 +223,15 @@ if [ -d "$CONF_ROOT" ]; then
|
||||
done | sort -u
|
||||
)"
|
||||
else
|
||||
echo "错误:nginx 配置扫描目录不存在:$CONF_ROOT" >&2
|
||||
echo "Error: nginx config scan directory does not exist: $CONF_ROOT" >&2
|
||||
ssl_domains=""
|
||||
conf_error=1
|
||||
fi
|
||||
|
||||
for domain in $ssl_domains; do
|
||||
if ! domain_in_list "$domain"; then
|
||||
echo "错误:nginx 配置引用了未加入 DOMAINS 的证书域名:$domain" >&2
|
||||
echo "提示:请把 $domain 加到 init-certs.sh 的 DOMAINS,或移除对应 ssl_certificate 配置。" >&2
|
||||
echo "Error: nginx config references a certificate domain not listed in DOMAINS: $domain" >&2
|
||||
echo "Hint: add $domain to init-certs.sh DOMAINS or remove the ssl_certificate config." >&2
|
||||
conf_error=1
|
||||
fi
|
||||
done
|
||||
@@ -187,25 +240,21 @@ if [ "$conf_error" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 阶段 2:选择可用的 Docker Compose 命令。
|
||||
. "$ROOT_DIR/scripts/lib-compose.sh"
|
||||
|
||||
# 阶段 3:为缺失证书的域名生成 dummy 证书。
|
||||
# 目的不是让浏览器信任,而是让 OpenResty 启动时能找到证书文件。
|
||||
export DOMAINS CERT_ROOT
|
||||
DUMMY_DOMAINS="$(sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh")"
|
||||
|
||||
# 阶段 4:启动 OpenResty。
|
||||
# 这里能启动,是因为上面已经确保每个 HTTPS 域名都有证书文件。
|
||||
# 启动后,Let's Encrypt 才能通过 HTTP 访问 /.well-known/acme-challenge/ 校验文件。
|
||||
echo "使用 dummy 证书启动 OpenResty..."
|
||||
echo "Starting OpenResty with available certificates..."
|
||||
compose up -d openresty
|
||||
|
||||
echo "Validating OpenResty configuration inside the container..."
|
||||
compose exec -T openresty openresty -t
|
||||
|
||||
check_http_challenge
|
||||
|
||||
if [ -n "$DUMMY_DOMAINS" ]; then
|
||||
# 阶段 5:删除本次创建或复用的 dummy 证书。
|
||||
# 必须先删掉 dummy 文件,否则 certbot 可能认为 live/域名 目录已经被占用。
|
||||
# 已存在的正式证书不会进入 DUMMY_DOMAINS,所以不会被删除。
|
||||
echo "申请正式证书前删除 dummy 证书文件..."
|
||||
echo "Removing dummy certificate files before requesting real certificates..."
|
||||
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"
|
||||
@@ -213,34 +262,27 @@ if [ -n "$DUMMY_DOMAINS" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# 阶段 6:申请正式 Let's Encrypt 证书。
|
||||
# certbot 会把 challenge 文件写到 ./www,然后由 OpenResty 对外提供访问。
|
||||
if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then
|
||||
echo "证书申请失败,恢复缺失域名的 dummy 证书,避免 OpenResty 后续因证书文件缺失而无法启动..." >&2
|
||||
echo "Certificate request failed; restoring dummy certificates for missing domains." >&2
|
||||
if ! sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null; then
|
||||
echo "警告:dummy 证书恢复失败,请手动检查 $CERT_ROOT。" >&2
|
||||
echo "Warning: failed to restore dummy certificates. Check $CERT_ROOT manually." >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 阶段 7:重启 OpenResty。
|
||||
# OpenResty 启动时已经加载过 dummy 证书,正式证书签发后需要重启才能加载新文件。
|
||||
echo "重启 OpenResty 以加载正式证书..."
|
||||
echo "Restarting OpenResty to load real certificates..."
|
||||
compose restart openresty
|
||||
|
||||
# 阶段 8:安装证书自动续期任务。
|
||||
# scripts/install-renew-cron.sh 是幂等的:重复执行会先删除旧任务块,再写入新任务。
|
||||
# 这样首次部署只需要执行 init-certs.sh,就能同时完成启动和自动续期安装。
|
||||
if is_enabled "$INSTALL_RENEW_CRON"; then
|
||||
if command -v crontab >/dev/null 2>&1; then
|
||||
echo "安装证书自动续期 cron 任务..."
|
||||
echo "Installing certificate renewal cron..."
|
||||
sh "$ROOT_DIR/scripts/install-renew-cron.sh"
|
||||
else
|
||||
echo "警告:缺少 crontab 命令,已跳过证书自动续期任务安装。" >&2
|
||||
echo "提示:安装 crontab 后可手动执行 sh scripts/install-renew-cron.sh。" >&2
|
||||
echo "Warning: crontab is missing; skipped renewal cron installation." >&2
|
||||
echo "Install crontab and run sh scripts/install-renew-cron.sh later if needed." >&2
|
||||
fi
|
||||
else
|
||||
echo "跳过证书自动续期 cron 安装。"
|
||||
echo "Skipped certificate renewal cron installation."
|
||||
fi
|
||||
|
||||
echo "完成。"
|
||||
echo "Done."
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 安装证书自动续期定时任务。
|
||||
# 默认每天 03:00 执行 scripts/renew-certs.sh,并把日志写到 logs/cert-renew.log。
|
||||
# 可以重复执行:安装前会先删除旧的 openresty-gateway cert renew 任务块,再写入新的任务。
|
||||
# Install an idempotent daily certificate renewal cron block.
|
||||
|
||||
MARK_BEGIN="# BEGIN openresty-gateway cert renew"
|
||||
MARK_END="# END openresty-gateway cert renew"
|
||||
@@ -13,7 +11,7 @@ LOG_DIR="$ROOT_DIR/logs"
|
||||
CRON_LINE="$SCHEDULE cd '$ROOT_DIR' && sh scripts/renew-certs.sh >> logs/cert-renew.log 2>&1"
|
||||
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
echo "错误:缺少 crontab 命令。" >&2
|
||||
echo "Error: crontab is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -22,7 +20,7 @@ mkdir -p "$LOG_DIR"
|
||||
tmp_file="$(mktemp)"
|
||||
trap 'rm -f "$tmp_file"' EXIT
|
||||
|
||||
echo "删除已存在的证书续期 cron 块(如果存在)..."
|
||||
echo "Replacing existing certificate renewal cron block if present..."
|
||||
crontab -l 2>/dev/null | sed "/$MARK_BEGIN/,/$MARK_END/d" > "$tmp_file"
|
||||
{
|
||||
echo "$MARK_BEGIN"
|
||||
@@ -32,5 +30,5 @@ crontab -l 2>/dev/null | sed "/$MARK_BEGIN/,/$MARK_END/d" > "$tmp_file"
|
||||
|
||||
crontab "$tmp_file"
|
||||
|
||||
echo "已安装证书续期 cron:"
|
||||
echo "Installed certificate renewal cron:"
|
||||
echo "$CRON_LINE"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# 定义 compose(),统一兼容新版 `docker compose` 和旧版 `docker-compose`。
|
||||
|
||||
# Define compose() for both Docker Compose v2 and legacy docker-compose.
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
compose() {
|
||||
docker compose "$@"
|
||||
@@ -11,6 +10,6 @@ elif command -v docker-compose >/dev/null 2>&1; then
|
||||
docker-compose "$@"
|
||||
}
|
||||
else
|
||||
echo "错误:缺少 docker compose 或 docker-compose 命令。" >&2
|
||||
echo "Error: docker compose or docker-compose is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 日常续期用:
|
||||
# certbot renew 会自己判断证书是否快过期;没到续期时间不会真正重签。
|
||||
# renew 成功或无须续期后,重载 OpenResty,让已经更新的证书生效。
|
||||
# Renew certificates when needed, then reload OpenResty.
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
|
||||
@@ -11,16 +9,16 @@ cd "$ROOT_DIR"
|
||||
|
||||
. "$ROOT_DIR/scripts/lib-compose.sh"
|
||||
|
||||
echo "按需续期证书..."
|
||||
echo "Renewing certificates if needed..."
|
||||
compose run --rm --entrypoint certbot certbot \
|
||||
renew --webroot -w /var/www
|
||||
|
||||
echo "重载 OpenResty 以使用续期后的证书..."
|
||||
echo "Reloading OpenResty..."
|
||||
if compose exec -T openresty openresty -s reload; then
|
||||
echo "OpenResty 已重载。"
|
||||
echo "OpenResty reloaded."
|
||||
else
|
||||
echo "重载失败,改为重启 OpenResty..."
|
||||
echo "Reload failed; restarting OpenResty..."
|
||||
compose restart openresty
|
||||
fi
|
||||
|
||||
echo "完成。"
|
||||
echo "Done."
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Request Let's Encrypt certificates for DOMAINS.
|
||||
# Existing valid certificates are kept by certbot because of --keep-until-expiring.
|
||||
# Request Let's Encrypt certificates for DOMAINS. Existing valid certificates
|
||||
# are kept by certbot because of --keep-until-expiring.
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ -z "${DOMAINS:-}" ]; then
|
||||
echo "错误:必须设置 DOMAINS。" >&2
|
||||
echo "Error: DOMAINS is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CERT_EMAIL:-}" ]; then
|
||||
echo "错误:必须设置 CERT_EMAIL。" >&2
|
||||
echo "Error: CERT_EMAIL is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "$ROOT_DIR/scripts/lib-compose.sh"
|
||||
|
||||
echo "使用 certbot 申请正式证书..."
|
||||
echo "Requesting Let's Encrypt certificates..."
|
||||
for domain in $DOMAINS; do
|
||||
echo "申请正式证书:$domain"
|
||||
echo "Requesting certificate: $domain"
|
||||
compose run --rm --entrypoint certbot certbot \
|
||||
certonly --webroot -w /var/www -d "$domain" \
|
||||
--email "$CERT_EMAIL" --agree-tos --non-interactive --keep-until-expiring
|
||||
done
|
||||
|
||||
echo "证书申请步骤完成。"
|
||||
echo "Certificate request step completed."
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# 卸载 scripts/install-renew-cron.sh 安装的证书自动续期定时任务。
|
||||
# 只删除固定标记之间的内容,不影响其它 crontab 任务。
|
||||
# Remove the cron block installed by scripts/install-renew-cron.sh.
|
||||
|
||||
MARK_BEGIN="# BEGIN openresty-gateway cert renew"
|
||||
MARK_END="# END openresty-gateway cert renew"
|
||||
|
||||
if ! command -v crontab >/dev/null 2>&1; then
|
||||
echo "错误:缺少 crontab 命令。" >&2
|
||||
echo "Error: crontab is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -18,4 +17,4 @@ trap 'rm -f "$tmp_file"' EXIT
|
||||
crontab -l 2>/dev/null | sed "/$MARK_BEGIN/,/$MARK_END/d" > "$tmp_file"
|
||||
crontab "$tmp_file"
|
||||
|
||||
echo "已卸载证书续期 cron。"
|
||||
echo "Uninstalled certificate renewal cron."
|
||||
|
||||
Reference in New Issue
Block a user