diff --git a/admin-server/gateway.go b/admin-server/gateway.go index 9baaf8f..ec6c083 100644 --- a/admin-server/gateway.go +++ b/admin-server/gateway.go @@ -306,6 +306,24 @@ func writeRouteServer(b *strings.Builder, cfg *GatewayConfig, r Route) error { for _, p := range r.BypassPaths { writeProxyLocation(b, r, p, true) } + // favicon 免登录直通上游。 + // 理由:① 图标不是敏感资源,未登录时浏览器标签也该显示站点自己的图标; + // ② 导航项/历史条目的图标抓取依赖它在未登录时可取。 + // 之前这里是"在门户 root 里 try_files",等于把每个被代理站点的图标都换成门户图标 + // (实测 pi / ittools / vikunja 存下了同一张门户图标)。 + if up := strings.TrimSpace(r.Upstream); up != "" { + for _, fa := range []string{"/favicon.ico", "/favicon.svg"} { + fmt.Fprintf(b, " location = %s {\n auth_request off;\n proxy_pass %s;\n", fa, up) + b.WriteString(" proxy_set_header Host $host;\n") + b.WriteString(" proxy_set_header X-Real-IP $remote_addr;\n") + b.WriteString(" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n") + b.WriteString(" proxy_set_header X-Forwarded-Proto $scheme;\n") + if r.InsecureTLS || strings.HasPrefix(strings.ToLower(up), "https://") { + b.WriteString(" proxy_ssl_verify off;\n") + } + b.WriteString(" proxy_redirect off;\n }\n") + } + } writeProxyLocation(b, r, "", false) b.WriteString("}\n\n") return nil diff --git a/admin-server/gateway_test.go b/admin-server/gateway_test.go index f4245e2..fe20e56 100644 --- a/admin-server/gateway_test.go +++ b/admin-server/gateway_test.go @@ -494,21 +494,23 @@ func TestBeianInjectionSanitized(t *testing.T) { } } -// 代理到上游的 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"}, - }}) +// 代理到上游的 server 块:favicon 必须"免登录直通上游",而不是在门户 root 里 try_files。 +// try_files 会把被代理站点的 /favicon.ico 顶成门户图标(实测 pi / ittools / vikunja 三站同图)。 +func TestProxyRoutePassesFaviconThrough(t *testing.T) { + out, err := RenderGatewayConf(&GatewayConfig{ + ListenPort: 3081, + AuthURL: "http://127.0.0.1:3005/auth/verify", + LoginURL: "https://login.example/login", + Routes: []Route{ + {Host: "proxy.example", Upstream: "http://127.0.0.1:8080", RequireLogin: true, Websocket: true}, + {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 { + for _, b := range strings.Split(out, "server {") { if strings.Contains(b, "server_name proxy.example;") { proxyBlock = b } @@ -519,13 +521,16 @@ func TestProxyRouteDoesNotShadowUpstreamFavicon(t *testing.T) { 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.ico") { + t.Errorf("代理块应有 favicon 直通 location:\n%s", proxyBlock) } - if strings.Contains(proxyBlock, "location = /favicon.svg") { - t.Errorf("代理块不应提供门户 favicon.svg:\n%s", proxyBlock) + if strings.Contains(proxyBlock, "try_files $uri =404;\n }") && strings.Contains(proxyBlock, "location = /favicon.ico { auth_request off; try_files") { + t.Errorf("代理块的 favicon 不应走门户静态文件(会顶掉上游图标):\n%s", proxyBlock) } - if !strings.Contains(portalBlock, "location = /favicon.ico") { - t.Errorf("门户块应保留门户 favicon:\n%s", portalBlock) + if !strings.Contains(proxyBlock, "proxy_pass http://127.0.0.1:8080;") { + t.Errorf("代理块 favicon 应 proxy_pass 到上游:\n%s", proxyBlock) + } + if !strings.Contains(portalBlock, "location = /favicon.ico { auth_request off; try_files $uri =404; }") { + t.Errorf("门户块应保留门户自己的 favicon:\n%s", portalBlock) } }