# -*- coding: utf-8 -*- r'''重写 halo 的 nginx 配置:修掉 301 循环。 == 怎么坏的 == 我上一版**故意没写 443 块**(没证书时 `nginx -t` 会失败), 打算"等证书好了让 certbot 自己加"。 结果 `certbot --nginx` 找不到一个"纯粹的 443 块", 就把 `listen 443 ssl` 和证书那几行**塞进了那个 80 端口的块里**。 而那个块的 `location /` 内容是: return 301 https://$host$request_uri; 于是 https 的请求进来也走这条 —— **301 到自己,无限循环**。 浏览器会报 "重定向次数过多"。 **certbot 还另外新建了一个纯 80 的块**(带两条 `if ($host = ...) return 301`), 跟原来的块功能重复。 == 教训 == **不要让 certbot 去改一个"结构不完整"的配置。** `--nginx` 插件是**按 server 块猜**的:它找 `server_name` 匹配的块, 把 ssl 指令加进去。如果那个块本来是 80 的、还带 301, 它就会造出一个"443 + 301"的自环。 正确做法:**自己把 80 / 443 两块都写清楚**(443 先注释掉但不留半截), 证书有了之后**只把证书路径填进去**,而不是让 certbot 重排结构。 == 这一版的结构 == server A: 80 default_server -> proxy 到 Halo(IP 访问用) server B: 80 + 域名 -> ACME 例外 + 301 到 https server C: 443 + 域名 -> proxy 到 Halo 三个块各司其职,没有重叠,也没有"同一块里既有 301 又有 ssl"。 ''' 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 CONF = """# xxmxxs.top —— Halo 个人博客 # # 入口: # http://<服务器IP>/ 博客(IP,HTTP) # http://<服务器IP>/console 管理后台(IP,HTTP) # https://xxmxxs.top/ 博客(HTTPS) # https://xxmxxs.top/console 管理后台(HTTPS) # https://www.xxmxxs.top/ 同上 # http://<服务器IP>:3001/ 直连 Halo(用户要求) # # 后端:容器 halo,监听 0.0.0.0:3001 # 控制台:Halo 原生 /console(2.26.1 没有可配置的路径前缀) # ====================================================================== # A. 80 端口默认站点 —— 用 IP 访问时直接出博客 # ====================================================================== # # nginx 规则:Host 匹配不上任何 server_name 时用该端口的 default_server。 # # ⚠ 这里**不做 https 跳转**:IP 没证书,跳过去浏览器报警告, # 用户会以为网站坏了。 server { listen 80 default_server; listen [::]:80 default_server; server_name _; access_log /var/log/nginx/halo-access.log; error_log /var/log/nginx/halo-error.log; server_tokens off; client_max_body_size 100m; location / { proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; 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 X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 300s; } } # ====================================================================== # B. 80 端口 + 域名 —— ACME 挑战 + 跳 https # ====================================================================== # # ⚠ 这个块**只监听 80**,不含任何 ssl 指令。 # 上一版 certbot 把 443 塞进来,造出了"443 上也 301"的自环。 server { listen 80; listen [::]:80; server_name xxmxxs.top www.xxmxxs.top; access_log /var/log/nginx/halo-access.log; error_log /var/log/nginx/halo-error.log; server_tokens off; # ACME 挑战必须在 301 之前 —— # certbot 的 HTTP-01 要在 80 端口直接拿到文件。 # 被 301 跳走就续期失败,而**最多 90 天后才暴露**(证书到期)。 location /.well-known/acme-challenge/ { root /var/www/html; default_type "text/plain"; try_files $uri =404; } location / { return 301 https://$host$request_uri; } } # ====================================================================== # C. 443 + 域名 —— 博客(HTTPS) # ====================================================================== # # ⚠ `listen ... ssl` 和证书那几行是 certbot 需要的形式, # 续期时它会检查。**别删**,也别把 80 的 301 混进来。 server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name xxmxxs.top www.xxmxxs.top; access_log /var/log/nginx/halo-access.log; error_log /var/log/nginx/halo-error.log; server_tokens off; client_max_body_size 100m; ssl_certificate /etc/letsencrypt/live/xxmxxs.top/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/xxmxxs.top/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; 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 X-Forwarded-Proto $scheme; # 事件总线(SSE)要用,而且不能缓冲 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_connect_timeout 30s; } } """ def main() -> int: print("=" * 62) print(" 重写配置,修掉 301 循环") print("=" * 62) print() tmp = ROOT / "_build" / "_halo_nginx.conf" tmp.write_text(CONF, encoding="utf-8", newline="\n") upload(str(tmp), "/tmp/halo-nginx.conf") tmp.unlink(missing_ok=True) out = run(r''' sudo cp /etc/nginx/sites-available/halo /etc/nginx/sites-available/halo.bak.$(date +%s) sudo mv /tmp/halo-nginx.conf /etc/nginx/sites-available/halo echo " 写好了" echo echo " ---- 检查 ----" sudo nginx -t 2>&1 | tail -3 | sed 's/^/ /' ''', timeout=240, quiet=True) o = out[1] if isinstance(out, tuple) else str(out) print(o) if "successful" not in o: print(" ✗ 配置有错,撤回") run("sudo cp $(sudo ls -t /etc/nginx/sites-available/halo.bak.* | head -1) " "/etc/nginx/sites-available/halo && sudo nginx -t", timeout=120, quiet=True) return 1 out = run(r''' sudo systemctl reload nginx && sleep 1 echo " nginx: $(systemctl is-active nginx)" echo echo "════════ 逐条复核 ════════" printf " %-44s " "http://<服务器IP>/ (博客)" curl -s -o /dev/null -w "HTTP %{http_code} %{size_download} 字节\n" --max-time 10 http://127.0.0.1/ printf " %-44s " "http://<服务器IP>/console (控制台)" curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 10 http://127.0.0.1/console echo printf " %-44s " "https://xxmxxs.top/ (博客)" curl -sk -o /dev/null -w "HTTP %{http_code} %{size_download} 字节\n" --max-time 15 --resolve xxmxxs.top:443:127.0.0.1 https://xxmxxs.top/ printf " %-44s " "https://xxmxxs.top/console (控制台)" curl -sk -o /dev/null -w "HTTP %{http_code}\n" --max-time 15 --resolve xxmxxs.top:443:127.0.0.1 https://xxmxxs.top/console printf " %-44s " "https://www.xxmxxs.top/ (博客)" curl -sk -o /dev/null -w "HTTP %{http_code}\n" --max-time 15 --resolve www.xxmxxs.top:443:127.0.0.1 https://www.xxmxxs.top/ printf " %-44s " "http://xxmxxs.top/ (应 301 到 https)" curl -s -o /dev/null -w "HTTP %{http_code} -> %{redirect_url}\n" --max-time 15 -H "Host: xxmxxs.top" http://127.0.0.1/ echo echo " 不能弄坏的:" printf " %-44s " "https://yk.xxmxxs.top/ (中转站)" curl -s -o /dev/null -w "HTTP %{http_code} %{size_download} 字节\n" --max-time 15 https://yk.xxmxxs.top/ printf " %-44s " "http://<服务器IP>:9100/ (官网)" curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 10 http://127.0.0.1:9100/ printf " %-44s " "http://<服务器IP>:3001/ (直连 Halo)" curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 10 http://127.0.0.1:3001/ echo echo "════════ ACME 挑战路径(续期的命根子)════════" sudo sh -c 'echo probe > /var/www/html/.well-known/acme-challenge/probe' printf " %-44s " "http://xxmxxs.top/.well-known/... (应 200)" curl -s -o /dev/null -w "HTTP %{http_code}\n" --max-time 8 -H "Host: xxmxxs.top" http://127.0.0.1/.well-known/acme-challenge/probe sudo rm -f /var/www/html/.well-known/acme-challenge/probe ''', timeout=400, quiet=True) print(out[1] if isinstance(out, tuple) else out) return 0 if __name__ == "__main__": sys.exit(main())