# -*- coding: utf-8 -*- r'''按用户要求重排 nginx: <服务器IP> (80) -> 没东西 <服务器IP>:9100 (HTTP)-> 官网 yk.xxmxxs.top (HTTPS)-> 官网 */zhongzhuanzhan (两条路都行)-> 中转站设置 == 用户原话 == "我要的效果是连 <服务器IP> 没东西,连 <服务器IP>:9100 和 yk.xxmxxs.top 是官网,连 <服务器IP>:9100/zhongzhuanzhan 和 yk.xxmxxs.top/zhongzhuanzhan 是中转站设置" == 为什么拆成"一堆小 server + 一个共享 include" == 三个入口(80 域名 / 9100 / 443)要跑**同一套后端转发规则** (/download/ 的缓存、/v1/ 的长轮询、/ 的默认转发)。 如果每个 server 块各抄一遍,以后改后端端口或者改超时 就要改三处 —— 漏一处就是"某个入口偶尔 502", 而且很难查到。所以抽成 snippets/ykxmz-proxy.conf, 三个 server 块各 include 一次。 == 几个容易踩的点 == 1. **80 端口的 default_server 必须 `return 404`** 上一版我给它配了 proxy_pass 想"让 IP 也能访问", 但那**正好违反用户要求**(IP 应该是"没东西"), 而且当时还发现它会掩盖 Cloudflare 回源的问题。 2. **certbot 管的行不能动** 443 那个 server 块的 `listen ... ssl` 和 ssl_certificate 是 certbot 写的,续期时会检查。重排时**原样保留**。 ACME 挑战走 80 端口的 `server_name yk.xxmxxs.top`, 所以那个块也要留着(certbot 自己会往里塞 location)。 3. **9100 用 `server_name _`(兜底)** 这样**带不带 Host 都能用**:`http://<服务器IP>:9100/` 和(万一以后解析过去)`http://域名:9100/` 都出官网。 4. **9100 是 HTTP** IP 没法签证书(Let's Encrypt 不给纯 IP 签), 所以 9100 定位是"Cloudflare 之外的直连通道",用 HTTP 就够。 ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "_build")) from lssh import run, upload # noqa: E402 # ---------------------------------------------------------------- 共享转发规则 SNIPPET = """# ====================================================================== # ykxmz 后端转发规则(80 / 9100 / 443 三个入口共用) # ====================================================================== # # 抽出来是为了**只维护一份**。以前这段在三个 server 块里各写一遍, # 改个后端端口要改三处,漏一处就是"某个入口偶尔 502"。 # ---- 下载:让 Cloudflare 放心缓存 ---- # # 文件名带版本号(ykxmz-setup-2.1.8.exe),同一个名字内容永不改变, # 所以可以长缓存。 location /download/ { proxy_pass http://127.0.0.1:13245; 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_set_header CF-Connecting-IP $http_cf_connecting_ip; proxy_buffering on; # 这里**故意不设** Cache-Control —— relay 自己已经发了正确的 # (public, max-age=604800, immutable)。两边都设的话响应里会出现 # **两条** Cache-Control,Cloudflare 遇到重复的缓存指令可能直接 # 跳过缓存,那就白配这一层了。 proxy_cache_valid 200 7d; } # ---- 长轮询 ---- # # 中转的 /v1/agent/poll 要挂 60 秒。nginx 默认 60 秒就断, # 必须放宽,否则手表那边会一直掉线重连。 location /v1/ { proxy_pass http://127.0.0.1:13245; 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_set_header CF-Connecting-IP $http_cf_connecting_ip; proxy_read_timeout 180s; proxy_send_timeout 180s; proxy_connect_timeout 30s; # 长轮询不能缓冲,否则要等整个响应完才吐给客户端 —— 那就不是长轮询了 proxy_buffering off; proxy_cache off; } # ---- 其余(官网 / 中转站设置 / 抢救报告 / 截图)---- location / { proxy_pass http://127.0.0.1:13245; 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_set_header CF-Connecting-IP $http_cf_connecting_ip; proxy_read_timeout 180s; proxy_connect_timeout 30s; } """ # ---------------------------------------------------------------- 主配置 MAIN_CONF = """# ykxmz 中转 —— nginx 前置 # # ====================================================================== # 入口一览(用户明确要求的效果) # ====================================================================== # # <服务器IP> (80) -> 没东西(404) # <服务器IP>:9100 (HTTP)-> 官网 # yk.xxmxxs.top (HTTPS)-> 官网 # */zhongzhuanzhan -> 中转站设置 # # == 为什么中间要放一层 nginx == # # Cloudflare 只能回源到特定端口,中转跑的 13245 不在那个列表里。 # 而且下载 15MB 的安装包要靠 Cloudflare 缓存 —— # 第二次下载就不回源了,速度取决于离用户最近的 CF 边缘节点。 # # ====================================================================== # ---------------------------------------------------------------------- # 1. 80 端口默认站点:**没东西** # ---------------------------------------------------------------------- # # 用户要求:"连 <服务器IP> 没东西"。 # # 所以这里是**显式 404**,不转发给 relay。 # # ⚠ 上一版我在这里配了 proxy_pass(想让 IP 也能访问), # 那违反用户要求。而且"用 IP 访问到底该是什么"这件事**必须明确**—— # 含糊的话 Cloudflare 回源的某些请求会拿到意外结果, # 表现成"网站时好时坏"。 server { listen 80 default_server; listen [::]:80 default_server; server_name _; access_log /var/log/nginx/ykxmz-default-access.log; error_log /var/log/nginx/ykxmz-default-error.log; server_tokens off; return 404; } # ---------------------------------------------------------------------- # 2. 80 端口 + 域名:给 Let's Encrypt 续期用,并跳到 https # ---------------------------------------------------------------------- # # ⚠ 这个块**不能删** —— certbot 的 HTTP-01 挑战走这里。 # 删了的话证书到期续不上,90 天后网站会整个挂掉。 server { listen 80; listen [::]:80; server_name yk.xxmxxs.top; access_log /var/log/nginx/ykxmz-access.log; error_log /var/log/nginx/ykxmz-error.log; server_tokens off; client_max_body_size 200m; # 其余全部跳到 https(certbot --redirect 的效果) location / { return 301 https://$host$request_uri; } # certbot 会自己往这里塞 ACME 挑战的 location,别动它加的东西 } # ---------------------------------------------------------------------- # 3. 9100:官网 + 中转站设置(直连通道,不经过 Cloudflare) # ---------------------------------------------------------------------- # # 用户要求:"连 <服务器IP>:9100 和 yk.xxmxxs.top 是官网, # 连 <服务器IP>:9100/zhongzhuanzhan 是中转站设置"。 # # server_name 用 `_`(兜底):带不带 Host 都能用 —— # 用户可能直接敲 IP,也可能以后把某个域名解析过来。 # # ⚠ 这是 HTTP(不是 HTTPS):**IP 没法签证书**(Let's Encrypt 不给纯 IP 签)。 # 定位是"Cloudflare 出问题时的备用通道",HTTP 就够。 # # ⚠ 还需要 **AWS Lightsail 控制台开放 9100** —— 光配 nginx 没用。 server { listen 9100; listen [::]:9100; server_name _; access_log /var/log/nginx/ykxmz-9100-access.log; error_log /var/log/nginx/ykxmz-9100-error.log; server_tokens off; client_max_body_size 200m; include snippets/ykxmz-proxy.conf; } # ---------------------------------------------------------------------- # 4. 443:官网 + 中转站设置(正常入口,走 Cloudflare) # ---------------------------------------------------------------------- # # ⚠ `listen ... ssl` 和下面的 ssl_certificate 那几行是 **certbot 写的**, # 续期时它会检查。重排配置时**原样保留**,别手改。 server { server_name yk.xxmxxs.top; access_log /var/log/nginx/ykxmz-access.log; error_log /var/log/nginx/ykxmz-error.log; server_tokens off; client_max_body_size 200m; include snippets/ykxmz-proxy.conf; listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/yk.xxmxxs.top/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } """ def main() -> int: print("=" * 62) print(" 重排 nginx:IP 没东西 / 9100 官网 / 域名官网") print("=" * 62) print() # 先把现在的 443 块里 certbot 加的行抠出来(保留原样) # # ⚠ 只取**真正的指令**(listen / ssl_* / include options-ssl)。 # 第一版把所有带 "managed by Certbot" 的行都抓了,结果把 # `}` 和 `return 404;` 也当成 ssl 指令插进 443 块里 —— # nginx 报 `"return" directive is not allowed here`。 # 那些是**结构行**,属于别的 server 块。 print("── 1. 读现在的配置,抠出 certbot 的行 ──") r = run("sudo cat /etc/nginx/sites-available/ykxmz", timeout=90, quiet=True) old = r[1] if isinstance(r, tuple) else str(r) cert_lines = [] for ln in old.splitlines(): s = ln.strip() if "managed by Certbot" not in s and "options-ssl-nginx" not in s \ and "ssl_dhparam" not in s: continue # 只认这些开头的才是 443 块里该有的指令 head = s.split()[0] if s.split() else "" if head in ("listen",) or head.startswith("ssl_") \ or head == "include": cert_lines.append(s) else: print(" 跳过结构行: %s" % s[:60]) print(" 找到 %d 行可用的 certbot 配置" % len(cert_lines)) for ln in cert_lines: print(" " + ln) conf = MAIN_CONF if not cert_lines: print(" ⚠ 没找到 certbot 的行,用默认路径(可能不对,先跑 nginx -t 验)") else: # 用抓到的真实行替换模板里的那几行 conf = conf.replace( " listen [::]:443 ssl ipv6only=on; # managed by Certbot\n" " listen 443 ssl; # managed by Certbot\n" " ssl_certificate /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem; # managed by Certbot\n" " ssl_certificate_key /etc/letsencrypt/live/yk.xxmxxs.top/privkey.pem; # managed by Certbot\n" " include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n" " ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot", "\n".join(" " + ln.strip() for ln in cert_lines), 1) # ---- 写 snippet ---- print() print("── 2. 写共享转发规则 ──") tmp = ROOT / "_build" / "_snip.conf" tmp.write_text(SNIPPET, encoding="utf-8", newline="\n") upload(str(tmp), "/tmp/ykxmz-proxy.conf") tmp.unlink(missing_ok=True) tmp2 = ROOT / "_build" / "_main.conf" tmp2.write_text(conf, encoding="utf-8", newline="\n") upload(str(tmp2), "/tmp/ykxmz-main.conf") tmp2.unlink(missing_ok=True) r = run(r''' sudo mkdir -p /etc/nginx/snippets sudo cp /etc/nginx/sites-available/ykxmz /etc/nginx/sites-available/ykxmz.bak.$(date +%s) sudo mv /tmp/ykxmz-proxy.conf /etc/nginx/snippets/ykxmz-proxy.conf sudo mv /tmp/ykxmz-main.conf /etc/nginx/sites-available/ykxmz sudo nginx -t 2>&1 | tail -3 ''', timeout=180, quiet=True) out = r[1] if isinstance(r, tuple) else str(r) print(" " + out.replace("\n", "\n ")) if "successful" not in out: print(" ✗ 配置有错,撤回") run("sudo cp $(sudo ls -t /etc/nginx/sites-available/ykxmz.bak.* | head -1) " "/etc/nginx/sites-available/ykxmz && sudo nginx -t", timeout=120, quiet=True) return 1 print(" ✓ 写入成功") print() print("── 3. 重载 ──") r = run("sudo systemctl reload nginx && sleep 1 && systemctl is-active nginx", timeout=120, quiet=True) print(" nginx: " + (r[1] if isinstance(r, tuple) else str(r)).strip()) print() print("── 4. 复核(按用户要求逐条验)──") r = run(r''' echo " 要求:<服务器IP>(80)-> 没东西" printf " %-46s " "http://127.0.0.1/" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 10 http://127.0.0.1/ echo " 要求:<服务器IP>:9100 -> 官网" printf " %-46s " "http://127.0.0.1:9100/" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 10 http://127.0.0.1:9100/ echo " 要求:yk.xxmxxs.top -> 官网" printf " %-46s " "https://yk.xxmxxs.top/" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 15 https://yk.xxmxxs.top/ echo " 要求::9100/zhongzhuanzhan -> 中转站设置" printf " %-46s " "http://127.0.0.1:9100/zhongzhuanzhan" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 10 http://127.0.0.1:9100/zhongzhuanzhan echo " 要求:yk.xxmxxs.top/zhongzhuanzhan -> 中转站设置" printf " %-46s " "https://yk.xxmxxs.top/zhongzhuanzhan" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 15 https://yk.xxmxxs.top/zhongzhuanzhan echo echo " 另外验证中转接口没被弄坏:" printf " %-46s " "https://yk.xxmxxs.top/v1/health" curl -s -o /dev/null -w "%{http_code}\n" --max-time 15 https://yk.xxmxxs.top/v1/health printf " %-46s " "https://yk.xxmxxs.top/download/ykxmz-setup-2.1.8.exe" curl -s -o /dev/null -w "%{http_code} %{size_download} 字节\n" --max-time 30 -r 0-1000 https://yk.xxmxxs.top/download/ykxmz-setup-2.1.8.exe echo echo " 监听端口:" sudo ss -tlnp 2>/dev/null | grep -E ':(80|443|9100) ' | awk '{print " "$4}' ''', timeout=240, quiet=True) print(r[1] if isinstance(r, tuple) else r) print() print(" ⚠ 还要在 **AWS Lightsail 控制台**开放 9100 端口,外面才连得上") return 0 if __name__ == "__main__": sys.exit(main())