mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
fix(webui): 反代三处缺陷 —— 入口不显示、子域链接失效、前缀重定向逃逸
全部来自本机 systemd 实例的实测,不是推测。 1. 插件页「服务入口」永远显示"暂无" TABS.plugins.fetch 漏了 proxyServices,而 renderPlugins 第一行就调 renderProxyServicesCard()(读 state.proxyServices)。于是该页签 永远不请求 /api/v1/proxy/services —— journalctl 实测:用户正常使用 期间零请求,只有我手工 curl 的那一条。 判据 TestProxyServicesReachesPluginsTab 钉住三环(A 调用 B、B 读 state、C fetch 含块名),已做变异验证(删掉该项立刻红)。 不用静态调用图做通用依赖检查:实测它在 dashboard.js 上不可靠 (中文注释引号破坏配平、分发器展开导致大量误报),已在注释里 写明原因,避免后人重复踩。 2. 面板生成的子域链接 100% 进不了反代 生成链接走 base_url 的主机名,路由匹配只认 base_domain。 生产实例 base_url=homeagent.jianfgit.xyz、base_domain=localhost, 于是链接落到门户 mux → 302 /login,而手拼 *.localhost 反而能进。 新增 proxyBaseDomains 返回全部候选基域(配置值 → base_url 主机名 → localhost),serveProxyHost 用它匹配。两者都认,不是二选一: *.localhost 是 RFC 6761 的零配置基座,不能丢。 3. 前缀模式下上游根绝对路径重定向逃出挂载点 实测 NapCat:上游 GET / → 301 Location: /webui,浏览器跟随到 https://<门户>/webui(已跑出 /napcat),门户 mux 接住 → 302 /login。 httputil.ReverseProxy 不改写 Location,在 ModifyResponse 里补: 只改以 / 开头且未带前缀的,避开 //host 与已经带前缀的(否则会出现 /napcat/napcat/webui)。 顺带纠正:base_domain 保持原配置不动,改由代码兼容两种基域 —— 改配置会抢走别的 *.jianfgit.xyz 子域,且 *.jianfgit.xyz 是单层通配, huawei-smarthome.homeagent.jianfgit.xyz 本来就证书不匹配(实测 verify error 62)。要外网可用子域形态,应把 base_domain 配成 jianfgit.xyz,那是运维决策,不在本次改动内。
This commit is contained in:
@ -159,6 +159,73 @@ func proxyHostLabel(host, base string) string {
|
||||
return label
|
||||
}
|
||||
|
||||
// proxyBaseDomains 返回**全部**应当被当子域基座接受的基域,按优先级排列。
|
||||
//
|
||||
// ## 为什么需要多个(2026-11-04 生产实测缺陷)
|
||||
//
|
||||
// 生成链接与路由匹配之前用了**两个不同的源**:
|
||||
//
|
||||
// 生成链接 resolveEntry → base_url 的主机名 → huawei-smarthome.homeagent.jianfgit.xyz
|
||||
// 路由匹配 proxyBaseDomain(settings) → base_domain → localhost
|
||||
//
|
||||
// 生产实例上 base_url=https://homeagent.jianfgit.xyz、base_domain=localhost,
|
||||
// 于是面板里给出的子域链接**100% 进不了反代**(落到门户 mux → 302 /login),
|
||||
// 而按 localhost 手拼的链接却能进反代。用户看到的是「点开是登录页」。
|
||||
//
|
||||
// 修法不是二选一(改成只认 base_url 会让 *.localhost 失效,而那是 RFC 6761
|
||||
// 的零配置基座),而是**两者都认**:明确列出本次部署可能用到的基域。
|
||||
//
|
||||
// 顺序有意义:configBase 在前(用户显式配置的 base_domain),
|
||||
// 其次是 base_url 的主机名,最后回落到 localhost。
|
||||
func proxyBaseDomains(settings sdk.SettingsAPI) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
add := func(d string) {
|
||||
d = strings.Trim(strings.ToLower(strings.TrimSpace(d)), ".")
|
||||
if d == "" || seen[d] {
|
||||
return
|
||||
}
|
||||
seen[d] = true
|
||||
out = append(out, d)
|
||||
}
|
||||
|
||||
configured := ""
|
||||
if settings != nil {
|
||||
if v, err := settings.Get("base_domain"); err == nil && v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
configured = strings.Trim(strings.ToLower(strings.TrimSpace(s)), ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
add(configured)
|
||||
|
||||
// base_url 的主机名:外部入口是 homeagent.example.com 时,
|
||||
// 插件服务自然是 <标签>.homeagent.example.com。面板生成的链接正是这个,
|
||||
// 所以路由**必须**认它,否则链接与路由分叉。
|
||||
if bu := baseURLSetting(settings); bu != "" {
|
||||
if u, err := url.Parse(bu); err == nil && u.Hostname() != "" {
|
||||
add(u.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
add("localhost")
|
||||
return out
|
||||
}
|
||||
|
||||
// proxyHostLabelAny 在多个基域中任一个匹配时返回子域标签。
|
||||
//
|
||||
// 为什么不是「先算 base 再调用 proxyHostLabel」:基域是**一组**候选,
|
||||
// 调用方(serveProxyHost)不该自己拼循环,否则它一改就会与生成链接的
|
||||
// 逻辑再次分叉 —— 那正是本次缺陷的形成方式。
|
||||
func proxyHostLabelAny(host string, bases []string) string {
|
||||
for _, b := range bases {
|
||||
if label := proxyHostLabel(host, b); label != "" {
|
||||
return label
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildProxyTable 由「插件声明 + 手填条目」构建路由表。
|
||||
//
|
||||
// 冲突与非法条目的处理原则:**宁可报出来,不可静默丢弃**。被拒绝的条目
|
||||
@ -359,6 +426,28 @@ func newReverseProxy(u *url.URL, auth, stripPrefix string) *httputil.ReverseProx
|
||||
resp.Header.Add("Set-Cookie", c.String())
|
||||
}
|
||||
}
|
||||
// ★ 前缀模式下必须把上游的**根绝对路径重定向**改回挂载前缀下。
|
||||
//
|
||||
// 实测(2026-11-04,NapCat):
|
||||
// GET 上游 / → 301 Location: /webui
|
||||
// 浏览器跟随 → https://<门户>/webui ← 已经跑出 /napcat 挂载点
|
||||
// 门户 mux 接住 → 302 /login ← 用户看到登录页,以为是认证问题
|
||||
//
|
||||
// 这就是「路径形态白屏」的直接成因。httputil.ReverseProxy
|
||||
// **不**改写 Location(实测反向确认过),必须自己做。
|
||||
//
|
||||
// 只改**以 / 开头的相对绝对路径**:
|
||||
// - "http://..." 绝对 URL 可能指向上游之外,原样保留;
|
||||
// - 已经是 stripPrefix+"/" 开头的(上游自己就带了)不重复加,
|
||||
// 否则重定向会变成 /napcat/napcat/webui;
|
||||
// - 空值与相对路径(不带前导 /)原样保留。
|
||||
if stripPrefix != "" {
|
||||
if loc := resp.Header.Get("Location"); strings.HasPrefix(loc, "/") &&
|
||||
!strings.HasPrefix(loc, "//") &&
|
||||
!(loc == stripPrefix || strings.HasPrefix(loc, stripPrefix+"/")) {
|
||||
resp.Header.Set("Location", stripPrefix+loc)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@ -551,6 +640,14 @@ func (h *Handler) serveProxyHost(w http.ResponseWriter, r *http.Request) bool {
|
||||
if h.settings != nil {
|
||||
base = proxyBaseDomain(h.settings)
|
||||
}
|
||||
// ★ 路由匹配必须接受**全部**候选基域,不能只认 base_domain。
|
||||
//
|
||||
// 生成链接用的是 base_url 的主机名(resolveEntry),而这里曾只认
|
||||
// base_domain ⇒ 生产实例 base_url=homeagent.jianfgit.xyz /
|
||||
// base_domain=localhost 时,面板给出的子域链接 **100% 进不了反代**
|
||||
// (落到门户 mux → 302 /login),而手拼 *.localhost 却能进。
|
||||
// 用户看到的是「点开是登录页」。详见 proxyBaseDomains 的说明。
|
||||
bases := proxyBaseDomains(h.settings)
|
||||
// ★ 发现端点必须先于路径挂载判定。
|
||||
//
|
||||
// 否则它会被 /api/v1/device 这类前缀接走:声明该前缀的服务通常
|
||||
@ -603,7 +700,7 @@ func (h *Handler) serveProxyHost(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
label := proxyHostLabel(r.Host, base)
|
||||
label := proxyHostLabelAny(r.Host, bases)
|
||||
if label == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user