fix(gateway/favicon): 被代理域名的 /favicon.ico 被门户图标顶掉(图标全错的真因)

用户问"同时被路由的网站为什么图标不正确"。实测三处证据:
  ① gateway.conf 里**每个** server 块都有
       location = /favicon.ico { auth_request off; try_files $uri =404; }
     而 try_files 在门户自己的 root(/app/portal) 里找文件;
  ② 于是 https://pi|ittools|vikunja.jianfgit.xyz/favicon.ico 三者返回**同一个**
     门户图标(md5 80498a88…),而它们内部上游的真实图标各不相同
     (ittools dc36588f… / vikunja 6d9507b6… / pi cf3332ac…);
  ③ 导航图标抓取优先抓 /favicon.ico → pi/ittools/vikunja 存下了同一张 1091B 门户图标。

修法两处:
- admin-server/gateway.go:门户 favicon 的两个 location 只在"门户自己渲染"的
  server 块(kind=portal/auth、默认 404 块)里输出;代理到上游的块不再输出,
  让 /favicon.ico 正常走 location / 落到上游。新增断言测试。
- history-server/favicon.go:抓取改为**优先走内部上游**(读 gateway.yml 把对外域名
  映射到内部地址,带 Host 头、容忍自签证书),失败才回落对外地址 —— 这样受统一登录
  保护的站点也能拿到真实图标,且不再依赖门户的 favicon 行为。
This commit is contained in:
HomeDash Bot
2026-09-13 08:48:42 +08:00
parent 30aff4ffd8
commit 3d1ed788f3
3 changed files with 161 additions and 46 deletions

View File

@ -195,7 +195,16 @@ func writeListeners(b *strings.Builder, cfg *GatewayConfig, defaultServer bool)
fmt.Fprintf(b, " listen %d%s;\n", cfg.ListenPort, suffix)
}
func writeErrorPages(b *strings.Builder, include404 bool) {
// writeErrorPages 写公共 location。portalFavicon 决定是否提供**门户自己的** favicon:
// 只应写在门户页/登录页/默认 404 页这些由门户自身渲染的 server 块里。
//
// 踩坑(2026-09-13 实测):这两行曾被无条件写进每个 server 块,而 try_files 是在
// 门户的 root(/app/portal)里找文件 —— 于是**所有被代理域名的 /favicon.ico 都返回门户图标**:
//
// · 浏览器里每个站点的标签图标都变成门户图标;
// · 导航项图标抓取(优先抓 /favicon.ico)把门户图标当成了站点图标,
// 实测 pi / ittools / vikunja 三个站点存下了同一张图标。
func writeErrorPages(b *strings.Builder, include404, portalFavicon bool) {
b.WriteString(" root /app/portal;\n")
// 关键:错误页与它自己的静态资源必须 `auth_request off`。
// 上游故障时 nginx 会内部跳转到 /500.html,而服务端的 auth_request 会被
@ -209,8 +218,10 @@ func writeErrorPages(b *strings.Builder, include404 bool) {
b.WriteString(" error_page 500 502 503 504 /500.html;\n")
b.WriteString(" location = /500.html { internal; auth_request off; }\n")
b.WriteString(" location ^~ /css/ { auth_request off; try_files $uri =404; }\n")
b.WriteString(" location = /favicon.svg { auth_request off; try_files $uri =404; }\n")
b.WriteString(" location = /favicon.ico { auth_request off; try_files $uri =404; }\n")
if portalFavicon {
b.WriteString(" location = /favicon.svg { auth_request off; try_files $uri =404; }\n")
b.WriteString(" location = /favicon.ico { auth_request off; try_files $uri =404; }\n")
}
}
func writeProxyLocation(b *strings.Builder, r Route, path string, authOff bool) {
@ -264,7 +275,8 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error {
if r.MaxBodyMB > 0 {
fmt.Fprintf(b, " client_max_body_size %dm;\n", r.MaxBodyMB)
}
writeErrorPages(b, routeKind(r) == "portal")
// 只有"门户自己渲染"的块才给门户图标;代理到上游的块必须让 /favicon.ico 走 location /
writeErrorPages(b, routeKind(r) == "portal", strings.TrimSpace(r.Upstream) == "")
if routeKind(r) == "portal" {
// 门户首页交给 admin-server 渲染(pageInfo 可配置 + 支持数据卷整页覆盖),
@ -376,7 +388,7 @@ func RenderGatewayConf(cfg *GatewayConfig) (string, error) {
if cfg.ListenHTTPS {
writeTLS(&b, cfg)
}
writeErrorPages(&b, true)
writeErrorPages(&b, true, true)
b.WriteString(" location / { return 404; }\n}\n\n")
for _, r := range routes {

View File

@ -493,3 +493,39 @@ func TestBeianInjectionSanitized(t *testing.T) {
t.Fatalf("危险字符未清理:\n%s", out)
}
}
// 代理到上游的 server 块**不得**包含门户自己的 favicon location:
// try_files 是在门户 root 里找文件,会把被代理站点的 /favicon.ico 顶成门户图标
// (实测 pi / ittools / vikunja 三个站点的导航图标因此变成同一张门户图标)。
func TestProxyRouteDoesNotShadowUpstreamFavicon(t *testing.T) {
out, err := RenderGatewayConf(&GatewayConfig{ListenPort: 3081, Routes: []Route{
{Host: "proxy.example", Upstream: "http://127.0.0.1:8080"},
{Host: "portal.example", Kind: "portal"},
}})
if err != nil {
t.Fatal(err)
}
// 切出 proxy.example 的 server 块单独断言,避免被门户块的正常输出干扰
blocks := strings.Split(out, "server {")
var proxyBlock, portalBlock string
for _, b := range blocks {
if strings.Contains(b, "server_name proxy.example;") {
proxyBlock = b
}
if strings.Contains(b, "server_name portal.example;") {
portalBlock = b
}
}
if proxyBlock == "" || portalBlock == "" {
t.Fatalf("server blocks not found\n%s", out)
}
if strings.Contains(proxyBlock, "location = /favicon.ico") {
t.Errorf("代理块不应提供门户 favicon(会顶掉上游图标):\n%s", proxyBlock)
}
if strings.Contains(proxyBlock, "location = /favicon.svg") {
t.Errorf("代理块不应提供门户 favicon.svg:\n%s", proxyBlock)
}
if !strings.Contains(portalBlock, "location = /favicon.ico") {
t.Errorf("门户块应保留门户 favicon:\n%s", portalBlock)
}
}