mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +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:
5
third_party/hologramagentsdk/sdk/plugin.go
vendored
5
third_party/hologramagentsdk/sdk/plugin.go
vendored
@ -638,6 +638,11 @@ type PluginSDK struct {
|
||||
// 起的 goroutine。
|
||||
proxyReg ProxyRegistrar
|
||||
|
||||
// proxyWarn 是「RegisterProxy 调用但无注册回调」时的告警钩子。
|
||||
// 外部插件(子进程)就是这种情况 —— 见 RegisterProxy 的说明。
|
||||
// 未注入时为 nil(工具链/单测保持安静)。
|
||||
proxyWarn ProxyRegistrarMissing
|
||||
|
||||
// apiMu 保护上面这些由内核注入的 API 字段,以及 autoRestart。
|
||||
//
|
||||
// 这些字段的写方与读方天然跨 goroutine:
|
||||
|
||||
72
third_party/hologramagentsdk/sdk/proxy.go
vendored
72
third_party/hologramagentsdk/sdk/proxy.go
vendored
@ -22,8 +22,25 @@ import (
|
||||
// 插件、或旧 HologramAgent 读新插件都不会报错。
|
||||
//
|
||||
// 与 ToolDef / ChannelDef / ConfigDef 同族:SDK 定义声明契约,内核实现行为。
|
||||
// 声明方式与其它能力一致 —— 在 Start() 里调 RegisterProxy(name, def),
|
||||
// 或写进 plugin.json 的 proxies 字段(外部插件两种都支持)。
|
||||
//
|
||||
// # 两种声明入口,以及各自**谁能用**(重要)
|
||||
//
|
||||
// - **plugin.json 的 proxies 字段**:这是**外部插件唯一的入口**。
|
||||
// 由 HologramAgent 加载插件时读取 plugin.json 聚合,不经过插件进程的 SDK。
|
||||
//
|
||||
// - **RegisterProxy(name, def)**:**仅对内置插件有效**(编译进内核、
|
||||
// 没有插件目录与 plugin.json 的那些,如 remotedevice)。
|
||||
// 外部插件(独立 plugin.bin 子进程)调它**不会生效**,也不会报错。
|
||||
//
|
||||
// 为什么外部插件不能用运行期注册:插件跑在子进程里,声明要跨 RPC 协议回传
|
||||
// 才可能落地,而 proc 协议目前**没有** proxy 注册方法(grep "proxy" in
|
||||
// internal/plugin/proc/protocol.go 零命中),生成到插件工程的运行时模板也
|
||||
// 不注入该注册回调(同 grep 零命中)。所以外部插件的 RegisterProxy 是
|
||||
// 一个**空实现**。
|
||||
//
|
||||
// 历史提醒:本文件早期版本写的是「外部插件两种都支持」,那是错的,
|
||||
// 造成了“声明了却没被反代、排查时只查端口”的真实困境。若将来要让运行期
|
||||
// 声明对外部插件生效,必须先补协议,而不是只把这句话改回去。
|
||||
//
|
||||
// 安全性:**不声明 = 不被反代**。声明本身就是能力声明,因此不需要在
|
||||
// capabilities 里另外开一个开关——最小权限默认生效。
|
||||
@ -328,6 +345,28 @@ func ValidateProxyDef(d ProxyDef) string {
|
||||
// - 内置插件 → RegisterProxy(运行期,随 Start 注册)
|
||||
type ProxyRegistrar func(name string, def ProxyDef)
|
||||
|
||||
// ProxyRegistrarMissing 是「RegisterProxy 被调用、但没有注册回调」时的告警钩子。
|
||||
//
|
||||
// 存在理由:外部插件跑在子进程里,内核从未向它注入 ProxyRegistrar
|
||||
// (proc 协议无 proxy 注册方法),于是它的 RegisterProxy 是空实现。
|
||||
// 不告警的话,声明就**彻底静默消失** —— 用户只能看到“声明了却没被反代”。
|
||||
//
|
||||
// sdk 包不直接打日志(它被插件进程与工具链复用,不应绑定日志实现),
|
||||
// 由内核侧通过 SetProxyRegistrarMissing 注入。未注入时保持完全安静
|
||||
// (工具链、单元测试不需要噪声)。
|
||||
type ProxyRegistrarMissing func(name string, def ProxyDef)
|
||||
|
||||
// SetProxyRegistrarMissing 由内核注入「声明无处落地」的告警实现。
|
||||
// 插件不调用它。
|
||||
func (s *PluginSDK) SetProxyRegistrarMissing(fn ProxyRegistrarMissing) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.apiMu.Lock()
|
||||
s.proxyWarn = fn
|
||||
s.apiMu.Unlock()
|
||||
}
|
||||
|
||||
// SetProxyRegistrar 由内核注入。插件不直接调它(与 SetInputChannelRegistrar 同族)。
|
||||
func (s *PluginSDK) SetProxyRegistrar(r ProxyRegistrar) {
|
||||
if s == nil {
|
||||
@ -350,19 +389,42 @@ func (s *PluginSDK) SetProxyRegistrar(r ProxyRegistrar) {
|
||||
// Host: "myapp", Target: "127.0.0.1:12100",
|
||||
// })
|
||||
//
|
||||
// 声明立即生效(反代表在下一次请求时重建)。**不做去重**:同一 Host/Path
|
||||
// ## ⚠ 仅对内置插件有效
|
||||
//
|
||||
// 插件跑在**子进程**里时(外部插件 / plugin.bin),本方法没有任何效果:
|
||||
// 内核从未向插件进程注入 proxyReg 回调(proc 协议里没有 proxy 注册方法)。
|
||||
// 调用会静默返回,不报错。
|
||||
//
|
||||
// **外部插件请写 plugin.json 的 proxies 字段** —— 那是唯一生效的入口。
|
||||
// 详见包注释的「两种声明入口,以及各自谁能用」。
|
||||
//
|
||||
// 内置插件(编译进内核、无 plugin.json)用本方法声明,立即生效
|
||||
// (反代表在下一次请求时重建)。**不做去重**:同一 Host/Path
|
||||
// 被两条声明占用时由反代层判定冲突并明确报错,而不是在这里静默吞掉 ——
|
||||
// 插件作者需要看见冲突。
|
||||
//
|
||||
// 与 plugin.json 的 proxies 字段等价:写哪个都行,两者会合并(同名以本调用为准)。
|
||||
func (s *PluginSDK) RegisterProxy(name string, def ProxyDef) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.apiMu.RLock()
|
||||
r := s.proxyReg
|
||||
warn := s.proxyWarn
|
||||
s.apiMu.RUnlock()
|
||||
if r != nil {
|
||||
r(name, def)
|
||||
return
|
||||
}
|
||||
// ★ 没有注册回调时必须**留痕**,不能静默丢弃。
|
||||
//
|
||||
// 外部插件(独立 plugin.bin 子进程)就是这样:内核从未向插件进程注入
|
||||
// proxyReg(proc 协议无 proxy 注册方法),所以本调用永远走不到上面
|
||||
// 那个分支。历史实测代价:插件作者按旧文档写的声明完全没生效,
|
||||
// 排查时只想到端口/防火墙/base_domain —— 因为**没有任何信号**说
|
||||
// 「声明根本没发出去」。
|
||||
//
|
||||
// 这里只调可选钩子而不直接打日志:sdk 包不依赖 log(它被插件进程与
|
||||
// 工具链复用),日志实现由内核侧注入。
|
||||
if warn != nil {
|
||||
warn(name, def)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user