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:
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user