From bd4caa0f0993f912d00b7d9d4b1b752b08dd529a Mon Sep 17 00:00:00 2001 From: wdh-home <243823965@qq.com> Date: Mon, 18 May 2026 22:32:08 +0800 Subject: [PATCH] 1 --- .gitattributes | 5 + .gitignore | 33 +++ README.md | 187 +++++++++++++++++ conf/conf.d/ai.sggai.site.conf | 266 +++++++++++++------------ conf/conf.d/ai.sggai.site.conf.back | 149 ++++++++++++++ conf/conf.d/dms.sggai.site.conf | 64 ++---- conf/conf.d/lsbd2.loveteemo.com.conf | 93 +++------ conf/stream.d/dms.sggai.site.emqx.conf | 43 ++++ docker-compose.yml | 10 +- init-certs.sh | 16 ++ logs/.gitkeep | 1 + logs/error.log | 42 ---- scripts/ensure-domain-conf.sh | 67 +++++++ scripts/ensure-dummy-certs.sh | 78 ++++++++ scripts/init-certs-core.sh | 246 +++++++++++++++++++++++ scripts/install-renew-cron.sh | 36 ++++ scripts/lib-compose.sh | 16 ++ scripts/renew-certs.sh | 26 +++ scripts/request-certs.sh | 31 +++ scripts/uninstall-renew-cron.sh | 21 ++ uninstall.sh | 6 + www/index.html | 39 ++++ 22 files changed, 1178 insertions(+), 297 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 conf/conf.d/ai.sggai.site.conf.back create mode 100644 conf/stream.d/dms.sggai.site.emqx.conf create mode 100644 init-certs.sh create mode 100644 logs/.gitkeep delete mode 100644 logs/error.log create mode 100644 scripts/ensure-domain-conf.sh create mode 100644 scripts/ensure-dummy-certs.sh create mode 100644 scripts/init-certs-core.sh create mode 100644 scripts/install-renew-cron.sh create mode 100644 scripts/lib-compose.sh create mode 100644 scripts/renew-certs.sh create mode 100644 scripts/request-certs.sh create mode 100644 scripts/uninstall-renew-cron.sh create mode 100644 uninstall.sh create mode 100644 www/index.html diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..020af41 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.conf text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.yaml text eol=lf +*.yml text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92b4229 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Runtime output +logs/* +!logs/.gitkeep +*.log + +# Certbot and TLS material +certs/ +*.key +*.pem +*.crt +*.csr + +# Webroot runtime files +www/* +!www/index.html +!www/.well-known/ +www/.well-known/acme-challenge/* + +# Local configuration +.env +.env.* +!.env.example +!.env.*.example +docker-compose.override.yml + +# Editor and OS files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..be4743b --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# OpenResty Gateway + +这个目录用于通过 Docker Compose 启动 OpenResty 网关,并使用 Certbot 申请和续期 Let's Encrypt 证书。 + +## 前置条件 + +- Linux 服务器 +- Docker 和 Docker Compose +- `openssl` +- `crontab`,可选,用于安装自动续期任务 +- 域名 DNS 已解析到当前服务器 + +当前配置使用 `network_mode: host`,OpenResty 会直接监听宿主机端口。 + +## 脚本结构 + +- `init-certs.sh`:首次初始化入口,内部调用 `scripts/init-certs-core.sh`。 +- `uninstall.sh`:卸载入口,目前用于卸载证书自动续期 cron。 +- `scripts/`:放具体实现脚本,包括 nginx 域名模板生成、证书初始化核心逻辑、手动续期、安装和卸载续期 cron。 + +## 首次部署 + +进入目录: + +```bash +cd /path/to/openresty-gateway +``` + +如果需要直接执行脚本,可以先添加执行权限: + +```bash +chmod +x *.sh scripts/*.sh +``` + +首次申请证书并启动服务: + +先确认 `init-certs.sh` 里的 `DOMAINS` 和 `CERT_EMAIL` 已经按当前部署环境配置好。 + +```bash +sh init-certs.sh +``` + +默认情况下,脚本只校验 nginx 配置是否存在,不会自动创建新站点配置。缺配置时可以显式生成基础模板: + +```bash +sh init-certs.sh --create-missing-conf +``` + +这个脚本会: + +1. 校验每个域名都有 nginx 配置、配置里放行 `/.well-known/acme-challenge/`,且 nginx 引用的证书域名都已加入 `DOMAINS`。 +2. 为缺失的证书路径生成临时 dummy 证书。 +3. 启动 OpenResty,保证 `/.well-known/acme-challenge/` 可以访问。 +4. 删除本次创建的 dummy 证书文件。 +5. 运行 Certbot 分别为每个域名申请正式证书。 +6. 重启 OpenResty 让正式证书生效。 +7. 如果系统有 `crontab`,安装证书自动续期 cron 任务。 + +不想在初始化时安装自动续期任务: + +```bash +sh init-certs.sh --skip-renew-cron +``` + +重复执行 `init-certs.sh` 不会强制重签已有证书;Certbot 会保留还没到期的证书。 +重复执行时也不会重复添加 cron;安装脚本会先删除旧任务块,再写入新任务。 + +## 日常启动和停止 + +启动: + +```bash +docker compose up -d openresty +``` + +停止: + +```bash +docker compose down +``` + +重启: + +```bash +docker compose restart openresty +``` + +检查配置: + +```bash +docker compose config +docker compose exec openresty openresty -t +``` + +查看日志: + +```bash +docker compose logs -f openresty +``` + +## 证书续期 + +手动续期: + +```bash +sh scripts/renew-certs.sh +``` + +`scripts/renew-certs.sh` 使用 `certbot renew`,Certbot 会自己判断证书是否快过期;没到续期时间不会真正重签。 + +## 安装自动续期 + +安装每天 03:00 自动续期: + +```bash +sh scripts/install-renew-cron.sh +``` + +安装脚本可以重复执行。它会先删除旧的自动续期任务块,再写入新的任务,避免重复添加。 + +自定义执行时间,例如每天 04:30: + +```bash +SCHEDULE="30 4 * * *" sh scripts/install-renew-cron.sh +``` + +查看当前 crontab: + +```bash +crontab -l +``` + +卸载自动续期: + +```bash +sh uninstall.sh +``` + +自动续期日志: + +```bash +tail -f logs/cert-renew.log +``` + +## AI 网关 mock 规则 + +`conf/conf.d/000-ai.sggai.site-mock-small-chat.conf` 会优先于 `ai.sggai.site.conf` 加载。 + +它会拦截: + +- 路径:`/v1/chat/completions` +- 方法:`POST` +- JSON 中 `"stream": false` +- 请求体不超过 `1024` 字节 +- `messages` 文本内容总长度不超过 `20` 字节 + +命中后直接返回 mock 响应: + +```json +{ + "id": "chatcmpl-mock", + "object": "chat.completion", + "created": 1716030000, + "model": "xxx", + "choices": [ + { + "index": 0, + "message": { "role": "assistant", "content": "ok" }, + "finish_reason": "stop" + } + ], + "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2 } +} +``` + +不满足 mock 条件的请求会继续转发到: + +```text +http://10.1.0.1:3001 +``` + +## 注意事项 + +- `certs/`、`logs/`、运行时 webroot 文件默认不提交到 Git。 +- 如果证书文件不存在,OpenResty 的 HTTPS 配置会启动失败;首次部署请先运行 `init-certs.sh`。 +- `ai.sggai.site` 当前通过 `000-` 前缀配置文件覆盖原配置,`openresty -t` 可能出现同名 server 被忽略的 warning。 +- 如果需要透传带下划线的请求头,例如 `Session_id`,需要确认 Nginx 的 `underscores_in_headers` 策略是否符合预期。 diff --git a/conf/conf.d/ai.sggai.site.conf b/conf/conf.d/ai.sggai.site.conf index 8337fe4..454690f 100644 --- a/conf/conf.d/ai.sggai.site.conf +++ b/conf/conf.d/ai.sggai.site.conf @@ -1,149 +1,155 @@ server { + # 文件名前缀是 000-,会比 ai.sggai.site.conf 更早加载。 + # 这样可以在不修改原配置文件的情况下,让这里的 mock 逻辑优先生效。 listen 80; - # http2 on; + listen 443 ssl; server_name ai.sggai.site; + ssl_certificate /etc/letsencrypt/live/ai.sggai.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ai.sggai.site/privkey.pem; + location ^~ /.well-known/acme-challenge/ { root /var/www; default_type text/plain; try_files $uri =404; } - # 关键:允许 Session_id 这种带下划线的请求头 - # underscores_in_headers on; - # ignore_invalid_headers off; - - client_max_body_size 200m; - - gzip off; - gunzip off; - location / { - proxy_pass http://10.1.0.1:3001; - proxy_http_version 1.1; - - # 保持你原来“模拟 IP 直连”的行为 - proxy_set_header Host $host; - - # 基础请求头 - proxy_set_header Authorization $http_authorization; - proxy_set_header Content-Type $http_content_type; - proxy_set_header Accept $http_accept; - proxy_set_header User-Agent $http_user_agent; - - # 关键:Codex / 上游 prompt cache 相关头 - proxy_set_header Originator $http_originator; - proxy_set_header Session_id $http_session_id; - proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; - proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; - - # Claude CLI 相关头,保留无害 - proxy_set_header X-Stainless-Arch $http_x_stainless_arch; - proxy_set_header X-Stainless-Lang $http_x_stainless_lang; - proxy_set_header X-Stainless-Os $http_x_stainless_os; - proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; - proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; - proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; - proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; - proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; - proxy_set_header X-App $http_x_app; - proxy_set_header Anthropic-Beta $http_anthropic_beta; - proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; - proxy_set_header Anthropic-Version $http_anthropic_version; - - # 禁用压缩干扰 - proxy_set_header Accept-Encoding ""; - - # 继续模拟直连,不暴露外层代理链 - proxy_set_header X-Real-IP ""; - proxy_set_header X-Forwarded-For ""; - proxy_set_header X-Forwarded-Proto ""; - proxy_set_header X-Forwarded-Host ""; - proxy_set_header X-Forwarded-Port ""; - - proxy_set_header Connection ""; - - # SSE / 流式响应 - proxy_buffering off; - proxy_request_buffering off; - proxy_cache off; - proxy_cache_bypass 1; - - proxy_connect_timeout 600s; - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } -} -server { - listen 443 ssl; - # http2 on; - - server_name ai.sggai.site; - - ssl_certificate /etc/letsencrypt/live/ai.sggai.site/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/ai.sggai.site/privkey.pem; - - # 关键:允许 Session_id 这种带下划线的请求头 http2 on 和下面这2个加上就容易出 status_code=400, Invalid 'prompt_cache_key': string too long. Expected a string with maximum length 64, but got a string with length 74 instead. - # underscores_in_headers on; - # ignore_invalid_headers off; - client_max_body_size 200m; gzip off; gunzip off; + # 公共反代配置放在 server 级别,主后端 proxy_pass location 会继承这些配置。 + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_set_header Content-Type $http_content_type; + proxy_set_header Accept $http_accept; + proxy_set_header User-Agent $http_user_agent; + proxy_set_header Originator $http_originator; + proxy_set_header Session_id $http_session_id; + proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; + proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; + proxy_set_header X-Stainless-Arch $http_x_stainless_arch; + proxy_set_header X-Stainless-Lang $http_x_stainless_lang; + proxy_set_header X-Stainless-Os $http_x_stainless_os; + proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; + proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; + proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; + proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; + proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; + proxy_set_header X-App $http_x_app; + proxy_set_header Anthropic-Beta $http_anthropic_beta; + proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; + proxy_set_header Anthropic-Version $http_anthropic_version; + proxy_set_header Accept-Encoding ""; + proxy_set_header X-Real-IP ""; + proxy_set_header X-Forwarded-For ""; + proxy_set_header X-Forwarded-Proto ""; + proxy_set_header X-Forwarded-Host ""; + proxy_set_header X-Forwarded-Port ""; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_request_buffering off; + proxy_cache off; + proxy_cache_bypass 1; + proxy_connect_timeout 600s; + 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 + break + 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([[{ + "id": "chatcmpl-mock", + "object": "chat.completion", + "created": 1716030000, + "model": "xxx", + "choices": [ + { + "index": 0, + "message": { "role": "assistant", "content": "ok" }, + "finish_reason": "stop" + } + ], + "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2 } +}]]) + return ngx.exit(ngx.HTTP_OK) + } + } + + # 仅供上面的 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; - proxy_http_version 1.1; - - # 保持你原来“模拟 IP 直连”的行为 - proxy_set_header Host $host; - - # 基础请求头 - proxy_set_header Authorization $http_authorization; - proxy_set_header Content-Type $http_content_type; - proxy_set_header Accept $http_accept; - proxy_set_header User-Agent $http_user_agent; - - # 关键:Codex / 上游 prompt cache 相关头 - proxy_set_header Originator $http_originator; - proxy_set_header Session_id $http_session_id; - proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; - proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; - - # Claude CLI 相关头,保留无害 - proxy_set_header X-Stainless-Arch $http_x_stainless_arch; - proxy_set_header X-Stainless-Lang $http_x_stainless_lang; - proxy_set_header X-Stainless-Os $http_x_stainless_os; - proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; - proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; - proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; - proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; - proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; - proxy_set_header X-App $http_x_app; - proxy_set_header Anthropic-Beta $http_anthropic_beta; - proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; - proxy_set_header Anthropic-Version $http_anthropic_version; - - # 禁用压缩干扰 - proxy_set_header Accept-Encoding ""; - - # 继续模拟直连,不暴露外层代理链 - proxy_set_header X-Real-IP ""; - proxy_set_header X-Forwarded-For ""; - proxy_set_header X-Forwarded-Proto ""; - proxy_set_header X-Forwarded-Host ""; - proxy_set_header X-Forwarded-Port ""; - - proxy_set_header Connection ""; - - # SSE / 流式响应 - proxy_buffering off; - proxy_request_buffering off; - proxy_cache off; - proxy_cache_bypass 1; - - proxy_connect_timeout 600s; - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; } } diff --git a/conf/conf.d/ai.sggai.site.conf.back b/conf/conf.d/ai.sggai.site.conf.back new file mode 100644 index 0000000..8337fe4 --- /dev/null +++ b/conf/conf.d/ai.sggai.site.conf.back @@ -0,0 +1,149 @@ +server { + listen 80; + # http2 on; + server_name ai.sggai.site; + + location ^~ /.well-known/acme-challenge/ { + root /var/www; + default_type text/plain; + try_files $uri =404; + } + + # 关键:允许 Session_id 这种带下划线的请求头 + # underscores_in_headers on; + # ignore_invalid_headers off; + + client_max_body_size 200m; + + gzip off; + gunzip off; + location / { + proxy_pass http://10.1.0.1:3001; + proxy_http_version 1.1; + + # 保持你原来“模拟 IP 直连”的行为 + proxy_set_header Host $host; + + # 基础请求头 + proxy_set_header Authorization $http_authorization; + proxy_set_header Content-Type $http_content_type; + proxy_set_header Accept $http_accept; + proxy_set_header User-Agent $http_user_agent; + + # 关键:Codex / 上游 prompt cache 相关头 + proxy_set_header Originator $http_originator; + proxy_set_header Session_id $http_session_id; + proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; + proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; + + # Claude CLI 相关头,保留无害 + proxy_set_header X-Stainless-Arch $http_x_stainless_arch; + proxy_set_header X-Stainless-Lang $http_x_stainless_lang; + proxy_set_header X-Stainless-Os $http_x_stainless_os; + proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; + proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; + proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; + proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; + proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; + proxy_set_header X-App $http_x_app; + proxy_set_header Anthropic-Beta $http_anthropic_beta; + proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; + proxy_set_header Anthropic-Version $http_anthropic_version; + + # 禁用压缩干扰 + proxy_set_header Accept-Encoding ""; + + # 继续模拟直连,不暴露外层代理链 + proxy_set_header X-Real-IP ""; + proxy_set_header X-Forwarded-For ""; + proxy_set_header X-Forwarded-Proto ""; + proxy_set_header X-Forwarded-Host ""; + proxy_set_header X-Forwarded-Port ""; + + proxy_set_header Connection ""; + + # SSE / 流式响应 + proxy_buffering off; + proxy_request_buffering off; + proxy_cache off; + proxy_cache_bypass 1; + + proxy_connect_timeout 600s; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} +server { + listen 443 ssl; + # http2 on; + + server_name ai.sggai.site; + + ssl_certificate /etc/letsencrypt/live/ai.sggai.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ai.sggai.site/privkey.pem; + + # 关键:允许 Session_id 这种带下划线的请求头 http2 on 和下面这2个加上就容易出 status_code=400, Invalid 'prompt_cache_key': string too long. Expected a string with maximum length 64, but got a string with length 74 instead. + # underscores_in_headers on; + # ignore_invalid_headers off; + + client_max_body_size 200m; + + gzip off; + gunzip off; + + location / { + proxy_pass http://10.1.0.1:3001; + proxy_http_version 1.1; + + # 保持你原来“模拟 IP 直连”的行为 + proxy_set_header Host $host; + + # 基础请求头 + proxy_set_header Authorization $http_authorization; + proxy_set_header Content-Type $http_content_type; + proxy_set_header Accept $http_accept; + proxy_set_header User-Agent $http_user_agent; + + # 关键:Codex / 上游 prompt cache 相关头 + proxy_set_header Originator $http_originator; + proxy_set_header Session_id $http_session_id; + proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; + proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; + + # Claude CLI 相关头,保留无害 + proxy_set_header X-Stainless-Arch $http_x_stainless_arch; + proxy_set_header X-Stainless-Lang $http_x_stainless_lang; + proxy_set_header X-Stainless-Os $http_x_stainless_os; + proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; + proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; + proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; + proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; + proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; + proxy_set_header X-App $http_x_app; + proxy_set_header Anthropic-Beta $http_anthropic_beta; + proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; + proxy_set_header Anthropic-Version $http_anthropic_version; + + # 禁用压缩干扰 + proxy_set_header Accept-Encoding ""; + + # 继续模拟直连,不暴露外层代理链 + proxy_set_header X-Real-IP ""; + proxy_set_header X-Forwarded-For ""; + proxy_set_header X-Forwarded-Proto ""; + proxy_set_header X-Forwarded-Host ""; + proxy_set_header X-Forwarded-Port ""; + + proxy_set_header Connection ""; + + # SSE / 流式响应 + proxy_buffering off; + proxy_request_buffering off; + proxy_cache off; + proxy_cache_bypass 1; + + proxy_connect_timeout 600s; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} diff --git a/conf/conf.d/dms.sggai.site.conf b/conf/conf.d/dms.sggai.site.conf index 5eb3444..77b4834 100644 --- a/conf/conf.d/dms.sggai.site.conf +++ b/conf/conf.d/dms.sggai.site.conf @@ -1,78 +1,44 @@ server { listen 80; - server_name dms.sggai.site; - - location ^~ /.well-known/acme-challenge/ { - root /var/www; - default_type text/plain; - try_files $uri =404; - } - # 静态网站案例 - # location / { - # root /var/www/dms.sggai.site; - # index index.html; - # try_files $uri $uri/ /index.html; - # } - - # 反向代理案例 - # location / { - # proxy_pass http://10.1.0.1:3001; - # - # proxy_set_header Host $host; - # proxy_set_header X-Real-IP $remote_addr; - # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # - # proxy_read_timeout 3600s; - # proxy_send_timeout 3600s; - # } - - location / { - return 301 https://$host$request_uri; - } -} -server { listen 443 ssl; server_name dms.sggai.site; ssl_certificate /etc/letsencrypt/live/dms.sggai.site/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dms.sggai.site/privkey.pem; + root /var/www/dms.sggai.site; + index index.html; + + location ^~ /.well-known/acme-challenge/ { + root /var/www; + default_type text/plain; + try_files $uri =404; + } + location / { - root /var/www/dms.sggai.site; - index index.html; + if ($scheme = http) { + return 301 https://$host$request_uri; + } + try_files $uri $uri/ /index.html; } } -server { - # 对外暴露的 HTTPS 端口 - # 用户访问:https://dms.sggai.site:18083/ - listen 18083 ssl; - # 只匹配这个域名 +server { + listen 18083 ssl; server_name dms.sggai.site; ssl_certificate /etc/letsencrypt/live/dms.sggai.site/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dms.sggai.site/privkey.pem; location / { - # 转发到局域网后端设备 - # 这里是 http,表示 nginx 到 10.1.0.1 使用明文 HTTP proxy_pass http://10.1.0.1:18083; - # 传递原始访问域名 - # 如果后端需要带端口,建议用 $http_host proxy_set_header Host $http_host; - - # 传递客户端真实 IP proxy_set_header X-Real-IP $remote_addr; - - # 传递完整代理链 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # 告诉后端:用户外部访问协议是 HTTPS proxy_set_header X-Forwarded-Proto https; - # 长连接/慢请求超时时间 proxy_read_timeout 3600s; proxy_send_timeout 3600s; } diff --git a/conf/conf.d/lsbd2.loveteemo.com.conf b/conf/conf.d/lsbd2.loveteemo.com.conf index 398a372..7f385f5 100644 --- a/conf/conf.d/lsbd2.loveteemo.com.conf +++ b/conf/conf.d/lsbd2.loveteemo.com.conf @@ -1,81 +1,39 @@ +map $server_port $lsbd2_prod_api_backend { + default 10.1.0.64:8080; + 8000 10.1.0.100:8080; +} + server { listen 80; + listen 443 ssl; + listen 8000 ssl; server_name lsbd2.loveteemo.com; + ssl_certificate /etc/letsencrypt/live/lsbd2.loveteemo.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/lsbd2.loveteemo.com/privkey.pem; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + location ^~ /.well-known/acme-challenge/ { root /var/www; default_type text/plain; try_files $uri =404; } - # 静态网站案例 - # location / { - # root /usr/share/nginx/html/lsbd2.loveteemo.com; - # index index.html; - # try_files $uri $uri/ /index.html; - # } - location / { - return 301 https://$host$request_uri; - } - - -} -server { - listen 443 ssl; - server_name lsbd2.loveteemo.com; - - ssl_certificate /etc/letsencrypt/live/lsbd2.loveteemo.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/lsbd2.loveteemo.com/privkey.pem; - location / { proxy_pass http://10.1.0.64:80; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } - location /prod-api/ { - proxy_pass http://10.1.0.64:8080; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } -} -server { - listen 8000 ssl; - server_name lsbd2.loveteemo.com; - - ssl_certificate /etc/letsencrypt/live/lsbd2.loveteemo.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/lsbd2.loveteemo.com/privkey.pem; - location /{ - proxy_pass http://10.1.0.64:80; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; } location /prod-api/ { - proxy_pass http://10.1.0.100:8080/; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; + rewrite ^/prod-api/(.*)$ /$1 break; + proxy_pass http://$lsbd2_prod_api_backend; } } + server { listen 8001 ssl; server_name lsbd2.loveteemo.com; @@ -83,14 +41,13 @@ server { ssl_certificate /etc/letsencrypt/live/lsbd2.loveteemo.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/lsbd2.loveteemo.com/privkey.pem; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + location / { proxy_pass http://36.111.46.77:31777/prod-api/; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; } } diff --git a/conf/stream.d/dms.sggai.site.emqx.conf b/conf/stream.d/dms.sggai.site.emqx.conf new file mode 100644 index 0000000..908fbd7 --- /dev/null +++ b/conf/stream.d/dms.sggai.site.emqx.conf @@ -0,0 +1,43 @@ +upstream emqx_1883 { + server 10.1.0.1:1883; +} + +upstream emqx_8883 { + server 10.1.0.1:8883; +} + +upstream emqx_8083 { + server 10.1.0.1:8083; +} + +upstream emqx_8084 { + server 10.1.0.1:8084; +} + +server { + listen 1883; + proxy_pass emqx_1883; + proxy_connect_timeout 10s; + proxy_timeout 1h; +} + +server { + listen 8883; + proxy_pass emqx_8883; + proxy_connect_timeout 10s; + proxy_timeout 1h; +} + +server { + listen 8083; + proxy_pass emqx_8083; + proxy_connect_timeout 10s; + proxy_timeout 1h; +} + +server { + listen 8084; + proxy_pass emqx_8084; + proxy_connect_timeout 10s; + proxy_timeout 1h; +} diff --git a/docker-compose.yml b/docker-compose.yml index f319432..9757146 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,17 +18,11 @@ services: # 日志 - ./logs:/usr/local/openresty/nginx/logs + # Certbot 是给脚本或手动命令使用的工具容器,不是常驻服务。 + # ./www 用于 ACME HTTP-01 域名验证,./certs 用于持久化保存签发的证书。 certbot: image: certbot/certbot container_name: certbot volumes: - ./www:/var/www - ./certs:/etc/letsencrypt - entrypoint: "" - command: sh -c "certbot certonly \ - --webroot -w /var/www \ - -d ai.sggai.site \ - -d dms.sggai.site \ - -d lsbd2.loveteemo.com \ - --email 243823965@qq.com \ - --agree-tos --non-interactive --force-renewal" diff --git a/init-certs.sh b/init-certs.sh new file mode 100644 index 0000000..b853368 --- /dev/null +++ b/init-certs.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" + +# 需要签证的域名列表。 +# 注意:这里的域名必须覆盖 nginx 配置里所有 ssl_certificate 使用到的域名。 +# 如果以后新增 HTTPS 域名,也要同步加到这里,否则 OpenResty 可能因为缺证书启动失败。 +DOMAINS="ai.sggai.site dms.sggai.site lsbd2.loveteemo.com" + +# Let's Encrypt 注册邮箱。 +CERT_EMAIL="243823965@qq.com" + +export DOMAINS CERT_EMAIL + +sh "$SCRIPT_DIR/scripts/init-certs-core.sh" "$@" diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..c708524 --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ +# Keep this log directory in Git. diff --git a/logs/error.log b/logs/error.log deleted file mode 100644 index a7a2b30..0000000 --- a/logs/error.log +++ /dev/null @@ -1,42 +0,0 @@ -2026/05/18 05:56:42 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:43 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:43 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:44 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:45 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:50 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:56:56 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:57:09 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:57:35 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:01 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:02 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:02 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:03 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:04 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:06 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:09 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:16 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:29 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:58:54 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 05:59:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:00:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:01:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:02:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:38 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:39 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:39 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:40 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:41 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:42 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:03:52 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:04:05 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:04:31 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:22 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:45 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:47 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:48 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:49 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) -2026/05/18 06:05:53 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/ai.sggai.site/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/ai.sggai.site/fullchain.pem, r) error:10000080:BIO routines::no such file) diff --git a/scripts/ensure-domain-conf.sh b/scripts/ensure-domain-conf.sh new file mode 100644 index 0000000..9df4fa7 --- /dev/null +++ b/scripts/ensure-domain-conf.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env sh +set -eu + +# 为传入的域名生成基础 nginx 配置。 +# 如果 conf/conf.d/xxx.conf 已经存在,直接跳过,避免覆盖人工维护的配置。 + +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 + exit 1 +fi + +ensure_domain_conf() { + domain="$1" + conf_file="$CONF_DIR/$domain.conf" + + # 已存在的域名配置不重新生成,避免覆盖已有转发、限流、路径规则等自定义配置。 + if [ -f "$conf_file" ]; then + echo "跳过已存在的 nginx 配置:$conf_file" + return + fi + + # 这里只生成一个能响应 ACME webroot 校验的最小 HTTPS 站点模板。 + # 业务代理规则可以后续在生成的 conf 文件里按需补充。 + echo "创建 nginx 配置模板:$conf_file" + mkdir -p "$CONF_DIR" + cat > "$conf_file" <&2 + exit 1 +fi + +if ! command -v openssl >/dev/null 2>&1; then + echo "错误:缺少 openssl 命令。" >&2 + exit 1 +fi + +for domain in $DOMAINS; do + cert_dir="$CERT_ROOT/$domain" + cert_file="$cert_dir/fullchain.pem" + key_file="$cert_dir/privkey.pem" + marker_file="$cert_dir/.dummy-init-certs" + tmp_cert_file="$cert_file.tmp" + tmp_key_file="$key_file.tmp" + + mkdir -p "$cert_dir" + + if [ -f "$cert_file" ] && [ -f "$key_file" ]; then + if [ -f "$marker_file" ]; then + # 上一次脚本可能中途失败,留下了 dummy 证书。 + # 继续把它当作 dummy 处理,后面会删除并重新申请正式证书。 + echo "复用已存在的 dummy 证书:$domain" >&2 + echo "$domain" + continue + fi + + # 已经有正式证书时不覆盖,避免误删线上可用证书。 + echo "跳过已存在的正式证书:$domain" >&2 + continue + fi + + if [ -f "$cert_file" ] || [ -f "$key_file" ]; then + if [ -f "$marker_file" ]; then + # 上次生成 dummy 证书时可能中途退出,留下了不完整文件。 + # 这些文件由本脚本创建,可以安全清理后重建。 + echo "清理不完整的 dummy 证书:$domain" >&2 + rm -f "$cert_file" "$key_file" "$marker_file" "$tmp_cert_file" "$tmp_key_file" + else + # 只存在证书或只存在私钥,状态不完整。 + # 自动处理可能误删用户文件,所以直接停止,让用户手工确认。 + echo "错误:$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 + if ! openssl req -x509 -nodes -newkey rsa:2048 -days 1 \ + -keyout "$tmp_key_file" \ + -out "$tmp_cert_file" \ + -subj "/CN=$domain"; then + rm -f "$tmp_cert_file" "$tmp_key_file" "$marker_file" + exit 1 + fi + + mv -f "$tmp_key_file" "$key_file" + mv -f "$tmp_cert_file" "$cert_file" + echo "$domain" + fi +done diff --git a/scripts/init-certs-core.sh b/scripts/init-certs-core.sh new file mode 100644 index 0000000..6ea44b8 --- /dev/null +++ b/scripts/init-certs-core.sh @@ -0,0 +1,246 @@ +#!/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 任务,后续证书会定期检查续期。 + +# 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 证书,不动已经存在的正式证书。 +DUMMY_DOMAINS="" + +cd "$ROOT_DIR" + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +if [ -z "${DOMAINS:-}" ]; then + echo "错误:必须设置 DOMAINS。请先在 init-certs.sh 中配置,再调用 init-certs-core.sh。" >&2 + exit 1 +fi + +if [ -z "${CERT_EMAIL:-}" ]; then + echo "错误:必须设置 CERT_EMAIL。请先在 init-certs.sh 中配置,再调用 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 + if is_enabled "$CREATE_MISSING_CONF"; then + 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 + 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 + 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 + [ -f "$conf_file" ] || continue + sed -n 's#^[[:space:]]*ssl_certificate[[:space:]][[:space:]]*/etc/letsencrypt/live/\([^/][^/]*\)/fullchain\.pem;.*#\1#p' "$conf_file" + done | sort -u + )" +else + echo "错误:nginx 配置扫描目录不存在:$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 + conf_error=1 + fi +done + +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..." +compose up -d openresty + +if [ -n "$DUMMY_DOMAINS" ]; then + # 阶段 5:删除本次创建或复用的 dummy 证书。 + # 必须先删掉 dummy 文件,否则 certbot 可能认为 live/域名 目录已经被占用。 + # 已存在的正式证书不会进入 DUMMY_DOMAINS,所以不会被删除。 + echo "申请正式证书前删除 dummy 证书文件..." + 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 +fi + +# 阶段 6:申请正式 Let's Encrypt 证书。 +# certbot 会把 challenge 文件写到 ./www,然后由 OpenResty 对外提供访问。 +if ! sh "$ROOT_DIR/scripts/request-certs.sh"; then + echo "证书申请失败,恢复缺失域名的 dummy 证书,避免 OpenResty 后续因证书文件缺失而无法启动..." >&2 + if ! sh "$ROOT_DIR/scripts/ensure-dummy-certs.sh" >/dev/null; then + echo "警告:dummy 证书恢复失败,请手动检查 $CERT_ROOT。" >&2 + fi + exit 1 +fi + +# 阶段 7:重启 OpenResty。 +# OpenResty 启动时已经加载过 dummy 证书,正式证书签发后需要重启才能加载新文件。 +echo "重启 OpenResty 以加载正式证书..." +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 任务..." + sh "$ROOT_DIR/scripts/install-renew-cron.sh" + else + echo "警告:缺少 crontab 命令,已跳过证书自动续期任务安装。" >&2 + echo "提示:安装 crontab 后可手动执行 sh scripts/install-renew-cron.sh。" >&2 + fi +else + echo "跳过证书自动续期 cron 安装。" +fi + +echo "完成。" diff --git a/scripts/install-renew-cron.sh b/scripts/install-renew-cron.sh new file mode 100644 index 0000000..8de68dc --- /dev/null +++ b/scripts/install-renew-cron.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +set -eu + +# 安装证书自动续期定时任务。 +# 默认每天 03:00 执行 scripts/renew-certs.sh,并把日志写到 logs/cert-renew.log。 +# 可以重复执行:安装前会先删除旧的 openresty-gateway cert renew 任务块,再写入新的任务。 + +MARK_BEGIN="# BEGIN openresty-gateway cert renew" +MARK_END="# END openresty-gateway cert renew" +SCHEDULE="${SCHEDULE:-0 3 * * *}" +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +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 + exit 1 +fi + +mkdir -p "$LOG_DIR" + +tmp_file="$(mktemp)" +trap 'rm -f "$tmp_file"' EXIT + +echo "删除已存在的证书续期 cron 块(如果存在)..." +crontab -l 2>/dev/null | sed "/$MARK_BEGIN/,/$MARK_END/d" > "$tmp_file" +{ + echo "$MARK_BEGIN" + echo "$CRON_LINE" + echo "$MARK_END" +} >> "$tmp_file" + +crontab "$tmp_file" + +echo "已安装证书续期 cron:" +echo "$CRON_LINE" diff --git a/scripts/lib-compose.sh b/scripts/lib-compose.sh new file mode 100644 index 0000000..a204ec2 --- /dev/null +++ b/scripts/lib-compose.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# 定义 compose(),统一兼容新版 `docker compose` 和旧版 `docker-compose`。 + +if docker compose version >/dev/null 2>&1; then + compose() { + docker compose "$@" + } +elif command -v docker-compose >/dev/null 2>&1; then + compose() { + docker-compose "$@" + } +else + echo "错误:缺少 docker compose 或 docker-compose 命令。" >&2 + exit 1 +fi diff --git a/scripts/renew-certs.sh b/scripts/renew-certs.sh new file mode 100644 index 0000000..f682bfd --- /dev/null +++ b/scripts/renew-certs.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +set -eu + +# 日常续期用: +# certbot renew 会自己判断证书是否快过期;没到续期时间不会真正重签。 +# renew 成功或无须续期后,重载 OpenResty,让已经更新的证书生效。 + +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" + +cd "$ROOT_DIR" + +. "$ROOT_DIR/scripts/lib-compose.sh" + +echo "按需续期证书..." +compose run --rm --entrypoint certbot certbot \ + renew --webroot -w /var/www + +echo "重载 OpenResty 以使用续期后的证书..." +if compose exec -T openresty openresty -s reload; then + echo "OpenResty 已重载。" +else + echo "重载失败,改为重启 OpenResty..." + compose restart openresty +fi + +echo "完成。" diff --git a/scripts/request-certs.sh b/scripts/request-certs.sh new file mode 100644 index 0000000..dae8aa1 --- /dev/null +++ b/scripts/request-certs.sh @@ -0,0 +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. + +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" + +cd "$ROOT_DIR" + +if [ -z "${DOMAINS:-}" ]; then + echo "错误:必须设置 DOMAINS。" >&2 + exit 1 +fi + +if [ -z "${CERT_EMAIL:-}" ]; then + echo "错误:必须设置 CERT_EMAIL。" >&2 + exit 1 +fi + +. "$ROOT_DIR/scripts/lib-compose.sh" + +echo "使用 certbot 申请正式证书..." +for domain in $DOMAINS; do + echo "申请正式证书:$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 "证书申请步骤完成。" diff --git a/scripts/uninstall-renew-cron.sh b/scripts/uninstall-renew-cron.sh new file mode 100644 index 0000000..2692ef7 --- /dev/null +++ b/scripts/uninstall-renew-cron.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +set -eu + +# 卸载 scripts/install-renew-cron.sh 安装的证书自动续期定时任务。 +# 只删除固定标记之间的内容,不影响其它 crontab 任务。 + +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 + exit 1 +fi + +tmp_file="$(mktemp)" +trap 'rm -f "$tmp_file"' EXIT + +crontab -l 2>/dev/null | sed "/$MARK_BEGIN/,/$MARK_END/d" > "$tmp_file" +crontab "$tmp_file" + +echo "已卸载证书续期 cron。" diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..95aebcf --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" + +sh "$SCRIPT_DIR/scripts/uninstall-renew-cron.sh" "$@" diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..2e2a1f3 --- /dev/null +++ b/www/index.html @@ -0,0 +1,39 @@ + + + + + + OpenResty Gateway + + + +
+

OpenResty Gateway

+

Placeholder page

+
+ +