mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat(webui): 路径挂载的 strip_path 两态 + 尾斜杠重定向(修 /p/huawei 打不开数据)
用户要求用方案 A(路径挂载)让 huawei 插件 UI 在外部可用,
并把「通过反代的插件必须使用单一入口」写入 SDK 声明。
## 实测暴露的两个真问题
1. **Path 的语义不能一刀切**。原设计「原样保留」只对**机器接口**成立
(设备客户端硬编码 /api/v1/device/ws,不可能知道反代的存在);
而自带 UI 的服务需要**剥掉前缀**(/p/huawei/api/status → 上游 /api/status)。
猜错的结果是全部请求 404,且看起来像上游故障 —— 所以由声明者选:
strip_path=false 别名模式 / true 前缀模式。非法组合被 validate 挡住。
2. **前缀模式的尾斜杠是必需的**(自测发现的 bug)。
访问 /p/huawei(无尾斜杠)时页面能开,但页面里所有 fetch 都 404 ——
相对路径以「当前文档目录」为基准,没尾斜杠时浏览器把最后一段当文件名,
目录退回上一级,fetch('api/status') 打到 /p/api/status。
修:前缀模式且路径恰等于前缀时 301 到 /p/huawei/(保留查询串)。
**别名模式不做此事** —— 那类路径是上游真实语义,加斜杠会改坏它。
## 插件侧(huawei_smarthome)
- 前端 4 处根绝对路径(fetch('/api/status') 等)改为相对路径,
基准由 location.pathname 推导(BASE)。这是 Path 形态能成立的**前提** ——
否则请求会打到门户自己身上。
- plg.json 声明:host + path=/p/huawei + strip_path=true + auth=homeagent。
- SDK 升到 1.4.0,并用 hmapdev 1.4.0 重新打包(1.3.0 的 hmapdev 无
proxies 支持,会把声明**静默丢弃** —— 实测确认过,这是打包链路上
一个不报警的坑,值得记住)。
## 判据
+6 条:TestProxyPathAliasVsStrip(两态各自正确)、
TestProxyPathLongestPrefixWins(/p/app 不得劫持 /p/apple,
且长前缀胜出)、TestProxyStripPathRedirectsToTrailingSlash(尾斜杠,
含查询串保留 + 别名模式不得重定向)。
变异验证(4 条,均按预期打红后还原回绿):
- 删尾斜杠重定向 → 判红(还原了真实 bug 形态)
- 让别名模式也重定向 → 判红(设备网关语义被毁)
- 从 hmapdev schema 探测体删 StripPath → 判红(漂移检测有效)
- 删 SDK 里的「单一入口原则」字样 → 判红(契约不能只剩口头约定)
全量:35 包全绿。
This commit is contained in:
72
third_party/homeagent-sdk/sdk/proxy_test.go
vendored
72
third_party/homeagent-sdk/sdk/proxy_test.go
vendored
@ -1,6 +1,10 @@
|
||||
package sdk
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProxyAuthDefaultsToHomeAgent(t *testing.T) {
|
||||
// 空串必须归一化为「HomeAgent 统一保护」——这是安全默认。
|
||||
@ -76,8 +80,8 @@ func TestNormalizeProxyHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProxyDecl(t *testing.T) {
|
||||
valid := []ProxyDecl{
|
||||
func TestValidateProxyDef(t *testing.T) {
|
||||
valid := []ProxyDef{
|
||||
{Target: "127.0.0.1:12100"},
|
||||
{Target: "http://127.0.0.1:12100"},
|
||||
{Target: "127.0.0.1:12100", Host: "huawei"},
|
||||
@ -87,25 +91,73 @@ func TestValidateProxyDecl(t *testing.T) {
|
||||
{Target: "https://example.com", Host: "ext"}, // 远程上游也允许(由 auth 决定安全性)
|
||||
}
|
||||
for _, d := range valid {
|
||||
if msg := ValidateProxyDecl(d); msg != "" {
|
||||
if msg := ValidateProxyDef(d); msg != "" {
|
||||
t.Errorf("%+v 应合法,却报: %s", d, msg)
|
||||
}
|
||||
}
|
||||
|
||||
bad := []ProxyDecl{
|
||||
{}, // 无 target
|
||||
{Target: " "}, // 空白 target
|
||||
bad := []ProxyDef{
|
||||
{}, // 无 target
|
||||
{Target: " "}, // 空白 target
|
||||
{Target: "127.0.0.1:12100", Auth: "yes"}, // auth 非法
|
||||
{Target: "127.0.0.1:12100", Host: "a_b"}, // host 非法
|
||||
{Target: "127.0.0.1:12100", Host: "-x"},
|
||||
{Target: "127.0.0.1:12100", Host: "X"},
|
||||
{Target: "://12100"}, // 无主机
|
||||
{Target: "http:///path"}, // 无主机
|
||||
{Target: "://12100"}, // 无主机
|
||||
{Target: "http:///path"}, // 无主机
|
||||
{Target: "127.0.0.1:notaport"}, // 端口非数字
|
||||
}
|
||||
for _, d := range bad {
|
||||
if msg := ValidateProxyDecl(d); msg == "" {
|
||||
if msg := ValidateProxyDef(d); msg == "" {
|
||||
t.Errorf("%+v 应被拒绝,却通过了", d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 单一入口原则 ----
|
||||
|
||||
// 被反代的插件必须能同时适配 Host 形态与 Path 形态。这两条判据把
|
||||
// 「插件内部不得用根绝对路径」这条契约钉在**可执行**的层面:
|
||||
// 声明合法不代表它的资源能被两种形态访问到 —— 后者取决于插件前端的写法,
|
||||
// 而 SDK 只能把要求写清楚并给出校验工具。
|
||||
func TestSingleEntryPrincipleDocumented(t *testing.T) {
|
||||
// Path 形态下插件前端必须用相对路径,否则请求会打到门户自己。
|
||||
// 这是**文档级约定**,只能靠 review 与这份判据共同保证:
|
||||
// 判据确保 SDK 里确实写明了这条要求(防止后来者删掉注释)。
|
||||
src, err := os.ReadFile("proxy.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"单一入口原则",
|
||||
"相对路径",
|
||||
"根绝对路径",
|
||||
} {
|
||||
if !strings.Contains(string(src), want) {
|
||||
t.Errorf("SDK 文档缺少「%s」—— 单一入口原则是反代的硬要求,不能只存在于口头约定里", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strip_path 的两种语义必须由声明者显式选,且非法组合要被挡住。
|
||||
func TestStripPathValidation(t *testing.T) {
|
||||
// 合法:两种模式
|
||||
for _, d := range []ProxyDef{
|
||||
{Target: "127.0.0.1:1", Path: "/p/app", StripPath: true},
|
||||
{Target: "127.0.0.1:1", Path: "/api/v1/device", StripPath: false},
|
||||
} {
|
||||
if msg := ValidateProxyDef(d); msg != "" {
|
||||
t.Errorf("应合法却被拒: %+v → %s", d, msg)
|
||||
}
|
||||
}
|
||||
// 非法:strip_path 但没有 path(没有可剥的前缀)
|
||||
if msg := ValidateProxyDef(ProxyDef{Target: "127.0.0.1:1", StripPath: true}); msg == "" {
|
||||
t.Error("strip_path=true 而无 path 应被拒(没有可剥的前缀)")
|
||||
}
|
||||
// 非法:前缀模式挂到根会吞掉整个门户。
|
||||
// 实际由「不应以 / 结尾」规则挡下("/" 同时是前缀又以 / 结尾),
|
||||
// 这里断言的是**行为**:这种声明无论如何都不能通过。
|
||||
if msg := ValidateProxyDef(ProxyDef{Target: "127.0.0.1:1", Path: "/", StripPath: true}); msg == "" {
|
||||
t.Error("path=\"/\" + strip_path 应被拒(会覆盖整个门户)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user