mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 04:43:11 +00:00
fix(webui): 反代两处真实故障 —— 凭证头按 auth 区分 + Host 分发先于门户路由
两处都是**隔离实例上跑真实端到端**才暴露的,单测(用不校验凭证的假上游、
直接调 serveProxyHost)全绿却线上出错。记录在此以免重蹈。
## 故障 1:auth=none 路由的凭证被无条件剥掉 ⇒ 设备链路全 401
Rewrite 里原本无条件 Del("Cookie"/"Authorization"/"X-API-Key")。但
auth=none 的语义是"请求原样交给上游",凭证本来就是给**上游**的——
remotedevice 的接入令牌正是走 X-API-Key 传的。
实测症状:带设备令牌经反代访问 /api/v1/device/online → 401;
直连 127.0.0.1:9890 → 200。差异极难定位,因为两侧状态码语义相同。
修法:按路由 auth 分流。
- auth=homeagent:凭证是门户的,剥掉(避免泄漏给插件)
- auth=none:保留(上游要用)
复验:经反代与直连**逐字节一致**(HTTP 200 / 17B,cmp 相同)。
## 故障 2:插件子域被门户路由截走 ⇒ 401 且响应体是门户的 JSON
原实现把 Host 分发放在 mux 的 "/" 兜底里。但 stdlib ServeMux 是**最长前缀
优先**:任何更具体的模式都先命中。插件子域上的 /api/v1/device/online 被门户
为「旧路径反代」注册的 /api/v1/device/ 接走(requireAPI 包裹)→ 401。
判据(响应体格式)是定位关键:
webui requireAPI → {"error":"unauthorized"} 25B ← 实际拿到
remotedevice requireToken → "unauthorized" text/plain 13B
看响应体格式就能区分是谁拒的,比看状态码有效。
修法:Host 分发提为**最外层中间件**,包在整个 mux 之外,先于任何路径匹配。
## 顺带:消除判据与生产接线错位的可能
新增 Handler.Handler() 返回生产用的完整链(Host 分发 → 日志 → mux),
plugin.go 与测试共用同一条。本次踩过:测试自己组装 mux、中间件却挂在
plugin.go,判据全绿而线上 401;共享同一条链可结构性避免。
(写这条判据时还发现测试里 h.mux 为 nil 导致 panic——也正是这种错位的表现。)
## 新增判据 2 条
- TestProxyCredentialHeadersDependOnAuth:auth=none 必须转发上游令牌、
auth=homeagent 必须剥掉门户凭证(两个方向都钉)
- TestProxyHostTakesPrecedenceOverPortalRoutes:走**完整生产链**,确认
插件子域上的 /api/v1/device/online、/api/v1/status、/api/v1/plugins/ 都
归反代;同时确认门户自身的 /api/v1/status 仍返回门户 JSON(没被反代吞掉)
## 真实验收(隔离实例:命名 netns + 独立 data + 端口 18080)
- 设备网关:令牌经反代 200,与直连逐字节一致;WS 升级 101
- 未声明子域:404 且错误信息含具体标签
- huawei_smarthome(用新 hmapdev 重打包、真装载):
匿名 401 + 可操作提示;带门户 key 拿到真实 UI(9444B,
<title>华为智慧生活管家);页面内根绝对路径 /api/status 正确透传
This commit is contained in:
@ -102,6 +102,9 @@ type Handler struct {
|
||||
// hostPort 是 webui 实际监听的 ":port"(用于推导服务入口链接)。
|
||||
hostPort string
|
||||
|
||||
// mux 由 RegisterRoutes 记下,供 Handler() 组装生产链。
|
||||
mux *http.ServeMux
|
||||
|
||||
sessionMu sync.Mutex
|
||||
sessions map[string]time.Time
|
||||
|
||||
@ -398,7 +401,37 @@ func (sw *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return nil, nil, fmt.Errorf("hijack not supported")
|
||||
}
|
||||
|
||||
// proxyDispatch 是**最外层**的 Host 分发中间件。
|
||||
//
|
||||
// 为什么不放在 mux 的 "/" 兜底里(这是实测踩出来的):stdlib ServeMux 是
|
||||
// 最长前缀优先,任何更具体的模式都会先命中。插件子域上的路径若与门户某条
|
||||
// 路由同名(/api/v1/device/online 就是——门户为「旧路径反代」注册了
|
||||
// /api/v1/device/),请求会被那条**面向门户**的路由截走:实测表现为 401,
|
||||
// 且响应体是门户的 {"error":"unauthorized"} 而不是上游的响应,极难排查。
|
||||
//
|
||||
// 包在 mux 外层后,Host 判定先于任何路径匹配发生:插件子域整体交给反代,
|
||||
// 主门户 Host 则原样下沉给 mux 走各自路由,两边互不干扰。
|
||||
// Handler 返回**生产用的完整处理链**(外 → 内:Host 分发 → 日志 → mux)。
|
||||
//
|
||||
// 抽成一个方法而非在 plugin.go 里手写组合:测试必须能拿到与线上**逐字节
|
||||
// 相同**的链,否则很容易测出错位的东西——本次就踩过:测 mux 而中间件挂在
|
||||
// plugin.go,判据全绿却在真实实例上 401。共享同一条链可以结构性地避免
|
||||
// 这类漂移。
|
||||
func (h *Handler) Handler() http.Handler {
|
||||
return h.proxyDispatch(h.logged(h.mux))
|
||||
}
|
||||
|
||||
func (h *Handler) proxyDispatch(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.serveProxyHost(w, r) {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
h.mux = mux
|
||||
mux.HandleFunc("/login", h.handleLoginPage)
|
||||
mux.HandleFunc("/api/v1/login", h.handleLogin)
|
||||
mux.HandleFunc("/api/v1/logout", h.handleLogout)
|
||||
@ -447,17 +480,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
// 走 requireAPI:清单本身含上游地址,属于管理面信息,不该匿名可读。
|
||||
mux.HandleFunc("/api/v1/proxy/services", h.requireAPI(h.handleProxyServices))
|
||||
mux.HandleFunc("/api/v1/proxy", h.requireAPI(h.handleProxyInfo))
|
||||
// "/" 兜底:**先**尝试按 Host 分发到插件反代,不是插件子域才落到主站静态页。
|
||||
// 顺序很重要——ServeMux 只会把未被更具体模式匹配的请求交给这里。
|
||||
mux.HandleFunc("/", h.handleRoot)
|
||||
}
|
||||
|
||||
// handleRoot 是根路由兜底:先看是不是插件反代的子域,不是再走主站。
|
||||
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
if h.serveProxyHost(w, r) {
|
||||
return
|
||||
}
|
||||
h.requireWeb(h.handleStatic)(w, r)
|
||||
mux.HandleFunc("/", h.requireWeb(h.handleStatic))
|
||||
}
|
||||
|
||||
func (h *Handler) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user