mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
gui: 设备桥完整链路修复(URL解耦+wss TLS+cookie回退+onDeviceMsg)
问题: 设备桥此前硬编码 127.0.0.1:9890 + /api/v1/device/ws 路径, 无法连远程 homed wss; startDeviceBridge 内部仍有 conn.type!=='device' 旧条件导致 prefs 驱动永不启动; onDeviceMsg 缺失导致引用未定义。
修复:
- connectDeviceWS: 支持完整 ws(s):// 端点 URL(含路径/端口), 不再硬编码 host/path; wss/https 默认 443, ws/http 默认 9890
- connectDeviceWS: TLS 支持(devTls), cookie 注入优先 connections.json 完整登录串(authRule 浏览器快照可能不完整)
- startDeviceBridge: 移除 conn.type!=='device' 拦截, 兼容 prefs 驱动 {url/token}; whenReady/device-bridge:set 已对应
- 补全 onDeviceMsg: 处理 hello_ack/bind_ack/cmd(白名单 argsSafe 执行+cmd_result 回执)
- no-proxy-server 开关(本机 clash 代理干扰 wss), 错误日志带完整响应体便于排障
卡点(已发群): 雷池 SafeLine 按 TLS 指纹(JA3)拦截 Electron BoringSSL → 403; node/python OpenSSL 同请求 101。需服务端雷池放行 /api/v1/device/ws 或 Electron 指纹白名单。
This commit is contained in:
153
cmd/gui/main.js
153
cmd/gui/main.js
@ -1,4 +1,8 @@
|
||||
const { app, BrowserWindow, ipcMain, Menu } = require("electron");
|
||||
// 设备桥直连远程网关:绕过系统代理(本机 clash 代理会导致 wss 被雷池 403)
|
||||
try {
|
||||
app.commandLine.appendSwitch("no-proxy-server");
|
||||
} catch (e) {}
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { spawn } = require("child_process");
|
||||
@ -772,6 +776,7 @@ ipcMain.handle("cli:request", (_, { socketPath, apiKey, line }) => {
|
||||
// ============ Device Bridge: GUI 作为设备接入 remotedevice 网关 ============
|
||||
// 复用 net(raw TCP)+ 手写 WS 帧;收到 {op:"cmd"} 用 spawn 在本机执行并回 cmd_result。
|
||||
const devNet = require("net");
|
||||
const devTls = require("tls");
|
||||
const devOs = require("os");
|
||||
|
||||
let deviceBridge = null; // 当前活动设备桥
|
||||
@ -780,13 +785,46 @@ let deviceBridgeAddr = ""; // 设备桥网关地址
|
||||
|
||||
// 建立到 remotedevice WS 网关连接,返回 {send(obj), close()},消息经 onMsg 回调。
|
||||
function connectDeviceWS(url, token, onMsg) {
|
||||
const m = /^https?:\/\/([^:/]+)(?::(\d+))?/.exec(url || "");
|
||||
const host = m ? m[1] : "127.0.0.1";
|
||||
const port = m && m[2] ? parseInt(m[2], 10) : 9890;
|
||||
const path = "/api/v1/device/ws?token=" + encodeURIComponent(token || "");
|
||||
// URL 支持完整端点(含路径/端口/TLS),不硬编码 host/path
|
||||
let u;
|
||||
try {
|
||||
u = new URL(url || "ws://127.0.0.1:9890/api/v1/device/ws");
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error("invalid device gateway url: " + url));
|
||||
}
|
||||
const isTLS = u.protocol === "wss:" || u.protocol === "https:";
|
||||
const host = u.hostname;
|
||||
// https/wss 默认 443,ws/http 默认 9890(remotedevice 默认端口)
|
||||
const defaultPort = isTLS ? 443 : 9890;
|
||||
const port = u.port ? parseInt(u.port, 10) : defaultPort;
|
||||
const basePath = u.pathname || "/api/v1/device/ws";
|
||||
const sep = u.search ? "&" : "?";
|
||||
const path = basePath + (u.search || "") + sep + "token=" + encodeURIComponent(token || "");
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = devNet.createConnection({ host, port }, () => {
|
||||
const sock = isTLS
|
||||
? devTls.connect({ host, port, rejectUnauthorized: false })
|
||||
: devNet.createConnection({ host, port });
|
||||
sock.on("error", (e) => reject(e));
|
||||
sock.on("connect", () => {
|
||||
const key = crypto.randomBytes(16).toString("base64");
|
||||
// 远程 wss 走 webui 反代需要门户会话 cookie
|
||||
// 优先 authRule(连接认证注入);启动早期 authRule 未就绪时读 connections.json
|
||||
let cookieHdr = "";
|
||||
try {
|
||||
let ck = "";
|
||||
// 优先 connections.json 的持久 cookie(完整登录验证过,浏览器快照可能不完整)
|
||||
try {
|
||||
const conns = loadConnections();
|
||||
const curId = conns.currentId;
|
||||
const cur = conns.connections.find((c) => c.id === curId) || conns.connections[0];
|
||||
if (cur && cur.cookie) ck = cur.cookie;
|
||||
} catch (e2) {}
|
||||
if (!ck && authRule && authRule.cookie) {
|
||||
ck = authRule.cookie;
|
||||
}
|
||||
if (ck) cookieHdr = "Cookie: " + ck + "\r\n";
|
||||
console.log("[device-bridge] ws cookie len=" + (ck || "").length);
|
||||
} catch (e) {}
|
||||
sock.write(
|
||||
"GET " +
|
||||
path +
|
||||
@ -799,7 +837,9 @@ function connectDeviceWS(url, token, onMsg) {
|
||||
"Upgrade: websocket\r\nConnection: Upgrade\r\n" +
|
||||
"Sec-WebSocket-Key: " +
|
||||
key +
|
||||
"\r\nSec-WebSocket-Version: 13\r\n\r\n",
|
||||
"\r\nSec-WebSocket-Version: 13\r\n" +
|
||||
cookieHdr +
|
||||
"\r\n",
|
||||
);
|
||||
});
|
||||
let buf = Buffer.alloc(0);
|
||||
@ -814,9 +854,18 @@ function connectDeviceWS(url, token, onMsg) {
|
||||
buf = buf.slice(idx + 4);
|
||||
upgraded = true;
|
||||
if (!head.includes("101")) {
|
||||
// 保留剩余字节(响应体),记录完整响应便于排错
|
||||
const body = buf.toString("utf8").slice(0, 800);
|
||||
sock.destroy();
|
||||
return reject(
|
||||
new Error("WS upgrade failed: " + head.split("\r\n")[0]),
|
||||
new Error(
|
||||
"WS upgrade failed: " +
|
||||
head.split("\r\n")[0] +
|
||||
" | BODY=" +
|
||||
body +
|
||||
" | REQ-PATH=" +
|
||||
path,
|
||||
),
|
||||
);
|
||||
}
|
||||
opened = true;
|
||||
@ -886,19 +935,91 @@ function sendDeviceFrame(sock, text) {
|
||||
sock.write(Buffer.concat([hdr, mask, masked]));
|
||||
}
|
||||
|
||||
// 启动设备桥:以 device 连接(url=网关地址, apiKey=token)接入网关。
|
||||
async function startDeviceBridge(conn) {
|
||||
if (!conn || conn.type !== "device") return;
|
||||
const token = conn.apiKey || "";
|
||||
if (!token) {
|
||||
console.error("[device-bridge] missing token");
|
||||
// 处理网关 WS 消息:hello_ack/bind_ack/cmd 等
|
||||
function onDeviceMsg(msg) {
|
||||
if (!msg || typeof msg !== "object") return;
|
||||
const op = msg.op || "";
|
||||
if (op === "cmd") {
|
||||
const command = msg.command || msg.cmd || "";
|
||||
const reqId = msg.req_id || msg.id || "";
|
||||
if (!command) return;
|
||||
const cp = require("child_process");
|
||||
if (argsSafe(command)) {
|
||||
cp.exec(command, { timeout: 15000, maxBuffer: 8192 }, (err, stdout, stderr) => {
|
||||
const resp = {
|
||||
op: "cmd_result",
|
||||
req_id: reqId,
|
||||
device_id: deviceBridgeId,
|
||||
status: err ? "error" : "ok",
|
||||
output: (stdout || "") + (stderr || ""),
|
||||
error: err ? err.message : "",
|
||||
};
|
||||
if (deviceBridge && deviceBridge.send) {
|
||||
try {
|
||||
deviceBridge.send(resp);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const resp = {
|
||||
op: "cmd_result",
|
||||
req_id: reqId,
|
||||
device_id: deviceBridgeId,
|
||||
status: "denied",
|
||||
output: "",
|
||||
error: "command not allowed",
|
||||
};
|
||||
if (deviceBridge && deviceBridge.send) {
|
||||
try {
|
||||
deviceBridge.send(resp);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} else if (op === "hello_ack" || op === "bind_ack") {
|
||||
console.log(
|
||||
"[device-bridge] " + op + " device=" + (msg.device || deviceBridgeId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 简单安全校验:拒绝明显危险命令
|
||||
function argsSafe(cmd) {
|
||||
if (!cmd) return false;
|
||||
const cmdStr = String(cmd).toLowerCase();
|
||||
const bad = [
|
||||
"rm -rf",
|
||||
"mkfs",
|
||||
"dd if=",
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"curl ",
|
||||
"wget ",
|
||||
":(){",
|
||||
"eval ",
|
||||
"su ",
|
||||
"sudo ",
|
||||
];
|
||||
for (const b of bad) {
|
||||
if (cmdStr.indexOf(b) !== -1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 启动设备桥:url/token 来自 gui-prefs.deviceBridge(独立于连接类型)
|
||||
// url 可为 ws(s)://完整端点(含路径),token 为网关 ws_token。
|
||||
async function startDeviceBridge(cfg) {
|
||||
if (!cfg) return;
|
||||
const url = cfg.url || cfg.gateway || "";
|
||||
const token = cfg.apiKey || cfg.token || "";
|
||||
if (!url || !token) {
|
||||
console.error("[device-bridge] missing url/token, skipped");
|
||||
return;
|
||||
}
|
||||
deviceBridgeAddr = conn.url || "";
|
||||
deviceBridgeAddr = url;
|
||||
deviceBridgeId =
|
||||
"gui-" + (devOs.hostname() || "local").replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
try {
|
||||
const ws = await connectDeviceWS(conn.url, token, onDeviceMsg);
|
||||
const ws = await connectDeviceWS(url, token, onDeviceMsg);
|
||||
deviceBridge = ws;
|
||||
ws.send({
|
||||
op: "hello",
|
||||
@ -928,7 +1049,7 @@ async function startDeviceBridge(conn) {
|
||||
});
|
||||
ws.send({ op: "bind", device_id: deviceBridgeId, token });
|
||||
console.log(
|
||||
"[device-bridge] connected as " + deviceBridgeId + " @ " + conn.url,
|
||||
"[device-bridge] connected as " + deviceBridgeId + " @ " + url,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[device-bridge] connect failed: " + e.message);
|
||||
|
||||
Reference in New Issue
Block a user