mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat(clients): 设备桥自动链接改用服务端发现 + 路径挂载(无 DNS 依赖)
配套 webui 反代改造:网关现在可由 HomeAgent 反代出去,客户端不能再靠
「门户地址同 host 拼 /api/v1/device/ws」猜地址——基域名与子域标签都是
**服务端配置**,客户端无从得知。
## 服务端:/api/v1/device/gateway 发现端点
客户端问「网关在哪」是唯一不会漂移的做法:子域标签可改(插件声明)、
基域名可改(webui.base_domain)、实例可换形态,客户端都不用跟着改。
⚠️ **不返回设备令牌**:本端点用门户凭证鉴权,而设备令牌能执行设备命令;
把令牌塞进来等于「门户只读凭证 → 设备执行权」的越权。令牌仍由客户端
自配。已有判据钉住「不得泄漏凭证字段」。
## ★ 实测发现:*.localhost 只有浏览器能解析
这是本轮最重要的发现,直接决定了设计:
| 环境 | devices.localhost 解析 |
|---|---|
| 浏览器 | ✓(RFC 6761 内置) |
| curl | ✓(内置特例) |
| getent / Go / Node | ✗(系统 nsswitch 是 files,dns,无 nss-myhostname) |
设备客户端(waiter / GUI 主进程 / 嵌入式固件)用的正是系统解析器。
实测 waiter 报「lookup devices.localhost on 192.168.2.1:53」。
因此**两处**设计变更:
1. 发现端点同时返回两种形态,并标 preferred:
- url(子域)—— 浏览器用
- url_portal(门户同源,同一 host、同一端口,走路径挂载)—— 非浏览器用,
无任何 DNS 依赖
2. SDK 的 ProxyDecl 新增 **Path**(路径挂载前缀):让同一服务同时挂到
门户自身 host 的路径下。remotedevice 声明 Path="/api/v1/device",
设备客户端因此能沿用**它已硬编码的路径**,不需要知道反代存在。
路径挂载语义:请求路径**原样保留**(不剥前缀),上游按真实路径注册即可。
边界卡在路径分隔符上(/api/v1/device 不匹配 /api/v1/devicefoo)。
## 客户端
- **waiter**:新增 discoverGateway(),仅在用户配了门户地址时尝试,失败回退
自配地址(老版本 HomeAgent 无该端点)。抽出 normalizeGateway() 纯函数,
显式钉住「已带子域/完整端点的地址不得被改写」。
- **GUI**:renderer 新增 loadDiscoveredGateway(),renderDeviceChannel 优先用
发现值、回退旧口径。顺带修掉此前插入函数时 anchor 不匹配导致调用点
找不到定义的问题。
- **鸿蒙**:discoverGateway() + resolveGatewayUrl(),优先 url_portal。
- 三者都**优先 url_portal**(system resolver 的现实约束)。
## 遗留路由鉴权修正
`/api/v1/device/` 的旧路径反代原被 requireAPI 包裹 —— 但其调用方是设备
(带设备令牌而非门户凭证),套上门户鉴权会把它们全挡在 401(**真实实测**:
waiter 经此路径升级握手 401)。去掉这层包装,鉴权交给上游 remotedevice
自己的 requireToken,安全性不降级。
## 判据
webui +6 条、waiter +7 条。
★ 其中一条是**真实回归**:/api/v1/device/gateway 曾被 Path="/api/v1/device"
的路径挂载接走(那服务 auth=none),于是发现请求被转给上游、回 401,
客户端再也发现不到网关。修法是发现端点先于路径挂载判定,并补判据
(走完整生产链,同时确认同前缀的真实设备路径仍归反代)。
## 真实验收(隔离实例,命名 netns + 独立 data + 18080)
真 waiter 客户端 + 真 remotedevice 网关:
device gateway discovered: ws://127.0.0.1:18080/api/v1/device/ws
device bridge active: waiter-mainserver authorized=true
hello_ack / bind_ack 均经反代往返成功
说明:`bind_ack device=<nil>` 与在线列表为空的现象,**直连 9890 绕开反代
完全一致复现**,属 remotedevice 与 waiter 之间既有的握手细节,与本次
反代改造无关(反代侧职责已证:连接建立 + 双向帧往返都通)。
This commit is contained in:
@ -593,6 +593,31 @@ async function api(p, o) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// 从服务端发现设备网关地址(自动链接的权威来源)。
|
||||
//
|
||||
// 失败不报错:老版本 HomeAgent 没有这个端点,回退到本地推导即可
|
||||
// (见 renderDeviceChannel 里的 state.discoveredGateway || 旧口径)。
|
||||
//
|
||||
// 优先 url_portal(门户同源形态):GUI 主进程连 WS 走系统解析器,
|
||||
// devices.localhost 这类子域在系统解析器下通常解析不到 —— *.localhost
|
||||
// 是浏览器内置特例(RFC 6761),不适用于普通进程。实测确认。
|
||||
async function loadDiscoveredGateway() {
|
||||
if (!state.currentConn || state.currentConn.type !== "webui") {
|
||||
state.discoveredGateway = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var d = await api("/device/gateway");
|
||||
state.discoveredGateway =
|
||||
(d && d.available && (d.url_portal || d.url)) || "";
|
||||
if (state.discoveredGateway) {
|
||||
console.log("[device-bridge] discovered gateway: " + state.discoveredGateway);
|
||||
}
|
||||
} catch (e) {
|
||||
state.discoveredGateway = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Navigation =====
|
||||
function switchView(n) {
|
||||
document.querySelectorAll(".view").forEach((e) => {
|
||||
@ -723,6 +748,7 @@ async function refreshDataOnly() {
|
||||
state.currentConn.type === "webui" &&
|
||||
state.currentConn.url
|
||||
) {
|
||||
await loadDiscoveredGateway();
|
||||
var d = await api("/device/online");
|
||||
state.devices = (d && d.devices) || [];
|
||||
} else {
|
||||
@ -807,6 +833,7 @@ async function refreshAll() {
|
||||
state.currentConn.type === "webui" &&
|
||||
state.currentConn.url
|
||||
) {
|
||||
await loadDiscoveredGateway();
|
||||
var d = await api("/device/online");
|
||||
state.devices = (d && d.devices) || [];
|
||||
} else {
|
||||
@ -5705,12 +5732,21 @@ function renderDevices() {
|
||||
}
|
||||
// 设备通道配置(独立于连接类型:devicced 是 GUI 组件,默认走 webui 反代端口)
|
||||
var dbc = state.dbConfig || {};
|
||||
var webuiUrl = "";
|
||||
// 网关地址优先用**服务端发现的权威值**(state.discoveredGateway),
|
||||
// 其次才是用户手填 / 本地推导。
|
||||
//
|
||||
// 为什么不能继续用「门户 URL 同 host 拼 /api/v1/device/ws」:
|
||||
// 网关改造为子域反代后位于 devices.<基域名>,而**基域名与子域标签都是
|
||||
// 服务端配置**,客户端无从得知。硬拼的结果是连到门户自己的路由上。
|
||||
// 服务端 /api/v1/device/gateway 是唯一不会漂移的来源。
|
||||
var webuiUrl = state.discoveredGateway || "";
|
||||
if (
|
||||
!webuiUrl &&
|
||||
state.currentConn &&
|
||||
state.currentConn.type === "webui" &&
|
||||
state.currentConn.url
|
||||
) {
|
||||
// 回退:老部署(无发现端点)仍按旧口径推导,保持向后兼容。
|
||||
webuiUrl = state.currentConn.url.replace(/\/+$/, "") + "/api/v1/device/ws";
|
||||
}
|
||||
var curGateway = dbc.gateway || webuiUrl || "";
|
||||
@ -5855,8 +5891,8 @@ function renderDevices() {
|
||||
html +=
|
||||
'<p style="color:var(--text-muted)">' +
|
||||
__(
|
||||
"暂无设备接入。设备通过 WebSocket 连接到设备网关(默认 127.0.0.1:9890/api/v1/device/ws),携带 token 后 hello 登记、bind 授权。",
|
||||
"No devices yet. Devices connect via WebSocket (default 127.0.0.1:9890/api/v1/device/ws), hello to register, bind to authorize.",
|
||||
"暂无设备接入。设备通过 WebSocket 连接到设备网关(默认经 HomeAgent 反代到 devices.<基域名>,或直连 127.0.0.1:9890/api/v1/device/ws),携带 token 后 hello 登记、bind 授权。",
|
||||
"No devices yet. Devices connect via WebSocket (proxied by HomeAgent at devices.<base-domain>, or directly 127.0.0.1:9890/api/v1/device/ws), hello to register, bind to authorize.",
|
||||
) +
|
||||
"</p>";
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user