dshost-plugin 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core.js +309 -0
- package/package.json +2 -1
package/lib/core.js
CHANGED
|
@@ -940,6 +940,12 @@ export function startAgent(opts) {
|
|
|
940
940
|
return;
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
// WebRTC P2P 穿透打洞信令交互
|
|
944
|
+
if (msg.type === 'webrtc:signal') {
|
|
945
|
+
handleWebRtcSignalMessage(ws, msg);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
943
949
|
// 门户托管业务下发(仅宿主开启 remoteServices 时接受)
|
|
944
950
|
if (msg.type === 'services:config') {
|
|
945
951
|
if (!remoteServices) { log.warn?.('[agent] services:config ignored (remoteServices disabled)'); return; }
|
|
@@ -1386,6 +1392,305 @@ export function startAgent(opts) {
|
|
|
1386
1392
|
streams.set(streamId, { dshWs, pending });
|
|
1387
1393
|
}
|
|
1388
1394
|
|
|
1395
|
+
// ── WebRTC P2P Hole Punching Engine (werift) ──────────────────────
|
|
1396
|
+
let weriftModule = null;
|
|
1397
|
+
async function loadWerift() {
|
|
1398
|
+
if (weriftModule) return weriftModule;
|
|
1399
|
+
try {
|
|
1400
|
+
weriftModule = await import('werift');
|
|
1401
|
+
return weriftModule;
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
log.warn?.(`[agent] werift WebRTC module load failed: ${e.message}`);
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const activeRtcPeers = new Map(); // clientId -> { pc, dc, wsStreams }
|
|
1409
|
+
|
|
1410
|
+
async function handleWebRtcSignalMessage(ws, msg) {
|
|
1411
|
+
const { reqId, signalType, sdp, candidate, clientId = 'default' } = msg;
|
|
1412
|
+
if (signalType === 'offer') {
|
|
1413
|
+
const werift = await loadWerift();
|
|
1414
|
+
if (!werift || !werift.RTCPeerConnection) {
|
|
1415
|
+
send(ws, {
|
|
1416
|
+
type: 'webrtc:signal-response',
|
|
1417
|
+
reqId,
|
|
1418
|
+
ok: false,
|
|
1419
|
+
error: '宿主机未安装或加载 werift WebRTC 模块',
|
|
1420
|
+
});
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// 确保本地 Auth Cookie 就绪
|
|
1425
|
+
if (!authCookie && webToken) {
|
|
1426
|
+
try { await mintAuthCookie(); } catch {}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
try {
|
|
1430
|
+
const existing = activeRtcPeers.get(clientId);
|
|
1431
|
+
if (existing) {
|
|
1432
|
+
try { existing.pc.close(); } catch {}
|
|
1433
|
+
activeRtcPeers.delete(clientId);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const pc = new werift.RTCPeerConnection({
|
|
1437
|
+
iceServers: [
|
|
1438
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
1439
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
1440
|
+
],
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
const peer = {
|
|
1444
|
+
clientId,
|
|
1445
|
+
pc,
|
|
1446
|
+
dc: null,
|
|
1447
|
+
wsStreams: new Map(),
|
|
1448
|
+
};
|
|
1449
|
+
activeRtcPeers.set(clientId, peer);
|
|
1450
|
+
|
|
1451
|
+
pc.ondatachannel = (event) => {
|
|
1452
|
+
const dc = event.channel;
|
|
1453
|
+
peer.dc = dc;
|
|
1454
|
+
log.log?.(`[agent-webrtc] DataChannel opened (${dc.label}) for client ${clientId}`);
|
|
1455
|
+
|
|
1456
|
+
dc.onmessage = (e) => {
|
|
1457
|
+
handleRtcDataChannelMessage(peer, dc, e.data);
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
dc.onclose = () => {
|
|
1461
|
+
log.log?.(`[agent-webrtc] DataChannel closed for client ${clientId}`);
|
|
1462
|
+
for (const [, sWs] of peer.wsStreams) {
|
|
1463
|
+
try { sWs.close(); } catch {}
|
|
1464
|
+
}
|
|
1465
|
+
peer.wsStreams.clear();
|
|
1466
|
+
activeRtcPeers.delete(clientId);
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
dc.onerror = (err) => {
|
|
1470
|
+
log.warn?.(`[agent-webrtc] DataChannel error: ${err.message}`);
|
|
1471
|
+
};
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
await pc.setRemoteDescription({ type: 'offer', sdp });
|
|
1475
|
+
const answer = await pc.createAnswer();
|
|
1476
|
+
await pc.setLocalDescription(answer);
|
|
1477
|
+
|
|
1478
|
+
// 等待 ICE 候选收集(嵌入至 localDescription 中返回,单次 RTT 快速握手)
|
|
1479
|
+
await new Promise((resolve) => {
|
|
1480
|
+
if (pc.iceGatheringState === 'complete') return resolve();
|
|
1481
|
+
const timer = setTimeout(resolve, 800);
|
|
1482
|
+
pc.onicegatheringstatechange = () => {
|
|
1483
|
+
if (pc.iceGatheringState === 'complete') {
|
|
1484
|
+
clearTimeout(timer);
|
|
1485
|
+
resolve();
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
send(ws, {
|
|
1491
|
+
type: 'webrtc:signal-response',
|
|
1492
|
+
reqId,
|
|
1493
|
+
ok: true,
|
|
1494
|
+
signalType: 'answer',
|
|
1495
|
+
sdp: pc.localDescription.sdp,
|
|
1496
|
+
});
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
log.warn?.(`[agent-webrtc] Offer 协商异常: ${err.message}`);
|
|
1499
|
+
send(ws, {
|
|
1500
|
+
type: 'webrtc:signal-response',
|
|
1501
|
+
reqId,
|
|
1502
|
+
ok: false,
|
|
1503
|
+
error: err.message,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (signalType === 'candidate') {
|
|
1510
|
+
const peer = activeRtcPeers.get(clientId);
|
|
1511
|
+
if (peer && peer.pc && candidate) {
|
|
1512
|
+
try { await peer.pc.addIceCandidate(candidate); } catch {}
|
|
1513
|
+
}
|
|
1514
|
+
send(ws, { type: 'webrtc:signal-response', reqId, ok: true });
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function handleRtcDataChannelMessage(peer, dc, raw) {
|
|
1520
|
+
let msg;
|
|
1521
|
+
try {
|
|
1522
|
+
msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString('utf8'));
|
|
1523
|
+
} catch {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (!msg || !msg.type) return;
|
|
1528
|
+
|
|
1529
|
+
// 1. P2P 测速与探测 (Ping / Pong)
|
|
1530
|
+
if (msg.type === 'ping') {
|
|
1531
|
+
try {
|
|
1532
|
+
dc.send(JSON.stringify({
|
|
1533
|
+
type: 'pong',
|
|
1534
|
+
reqId: msg.reqId,
|
|
1535
|
+
ts: msg.ts,
|
|
1536
|
+
agentTs: Date.now(),
|
|
1537
|
+
instanceId: getEffectiveInstanceId(),
|
|
1538
|
+
hostname: os.hostname(),
|
|
1539
|
+
}));
|
|
1540
|
+
} catch {}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// 2. HTTP 虚拟流转发
|
|
1545
|
+
if (msg.type === 'http:request') {
|
|
1546
|
+
const { streamId, method = 'GET', url = '/', headers = {}, body } = msg;
|
|
1547
|
+
const targetHeaders = { ...headers };
|
|
1548
|
+
targetHeaders.host = `${dshHost}:${dshPort}`;
|
|
1549
|
+
targetHeaders.origin = `http://${dshHost}:${dshPort}`;
|
|
1550
|
+
if (authCookie) targetHeaders.cookie = authCookie;
|
|
1551
|
+
delete targetHeaders['content-length'];
|
|
1552
|
+
|
|
1553
|
+
const dshReq = http.request(`http://${dshHost}:${dshPort}${url}`, {
|
|
1554
|
+
method,
|
|
1555
|
+
headers: targetHeaders,
|
|
1556
|
+
}, (dshRes) => {
|
|
1557
|
+
const resHeaders = { ...dshRes.headers };
|
|
1558
|
+
const sc = resHeaders['set-cookie'];
|
|
1559
|
+
if (sc) {
|
|
1560
|
+
const list = Array.isArray(sc) ? sc : [sc];
|
|
1561
|
+
resHeaders['set-cookie'] = list.map(c => c.replace(/;\s*SameSite=Strict/i, '; SameSite=Lax'));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
try {
|
|
1565
|
+
dc.send(JSON.stringify({
|
|
1566
|
+
type: 'http:response',
|
|
1567
|
+
streamId,
|
|
1568
|
+
status: dshRes.statusCode,
|
|
1569
|
+
headers: resHeaders,
|
|
1570
|
+
}));
|
|
1571
|
+
} catch {}
|
|
1572
|
+
|
|
1573
|
+
dshRes.on('data', (chunk) => {
|
|
1574
|
+
try {
|
|
1575
|
+
dc.send(JSON.stringify({
|
|
1576
|
+
type: 'http:chunk',
|
|
1577
|
+
streamId,
|
|
1578
|
+
data: chunk.toString('base64'),
|
|
1579
|
+
}));
|
|
1580
|
+
} catch {}
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
dshRes.on('end', () => {
|
|
1584
|
+
try {
|
|
1585
|
+
dc.send(JSON.stringify({ type: 'http:end', streamId }));
|
|
1586
|
+
} catch {}
|
|
1587
|
+
});
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
dshReq.on('error', (err) => {
|
|
1591
|
+
try {
|
|
1592
|
+
dc.send(JSON.stringify({
|
|
1593
|
+
type: 'http:response',
|
|
1594
|
+
streamId,
|
|
1595
|
+
status: 502,
|
|
1596
|
+
headers: { 'content-type': 'text/plain' },
|
|
1597
|
+
}));
|
|
1598
|
+
dc.send(JSON.stringify({
|
|
1599
|
+
type: 'http:chunk',
|
|
1600
|
+
streamId,
|
|
1601
|
+
data: Buffer.from(`DSH WebRTC proxy error: ${err.message}`).toString('base64'),
|
|
1602
|
+
}));
|
|
1603
|
+
dc.send(JSON.stringify({ type: 'http:end', streamId }));
|
|
1604
|
+
} catch {}
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
if (body) {
|
|
1608
|
+
dshReq.write(Buffer.from(body, 'base64'));
|
|
1609
|
+
}
|
|
1610
|
+
dshReq.end();
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// 3. WebSocket 虚拟流转发 (终端 / RPC)
|
|
1615
|
+
if (msg.type === 'ws:open') {
|
|
1616
|
+
const { streamId, url = '/api/remote.mux' } = msg;
|
|
1617
|
+
const wsHeaders = {
|
|
1618
|
+
host: `${dshHost}:${dshPort}`,
|
|
1619
|
+
origin: `http://${dshHost}:${dshPort}`,
|
|
1620
|
+
};
|
|
1621
|
+
if (authCookie) wsHeaders.cookie = authCookie;
|
|
1622
|
+
|
|
1623
|
+
const dshWs = new WebSocket(`ws://${dshHost}:${dshPort}${url}`, {
|
|
1624
|
+
headers: wsHeaders,
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
peer.wsStreams.set(streamId, dshWs);
|
|
1628
|
+
|
|
1629
|
+
dshWs.on('open', () => {
|
|
1630
|
+
try {
|
|
1631
|
+
dc.send(JSON.stringify({ type: 'ws:opened', streamId }));
|
|
1632
|
+
} catch {}
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
dshWs.on('message', (data, isBinary) => {
|
|
1636
|
+
try {
|
|
1637
|
+
dc.send(JSON.stringify({
|
|
1638
|
+
type: 'ws:message',
|
|
1639
|
+
streamId,
|
|
1640
|
+
data: isBinary ? data.toString('base64') : data.toString(),
|
|
1641
|
+
binary: !!isBinary,
|
|
1642
|
+
}));
|
|
1643
|
+
} catch {}
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
dshWs.on('close', (code, reason) => {
|
|
1647
|
+
peer.wsStreams.delete(streamId);
|
|
1648
|
+
try {
|
|
1649
|
+
dc.send(JSON.stringify({
|
|
1650
|
+
type: 'ws:close',
|
|
1651
|
+
streamId,
|
|
1652
|
+
code,
|
|
1653
|
+
reason: reason?.toString(),
|
|
1654
|
+
}));
|
|
1655
|
+
} catch {}
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
dshWs.on('error', (err) => {
|
|
1659
|
+
try {
|
|
1660
|
+
dc.send(JSON.stringify({
|
|
1661
|
+
type: 'ws:error',
|
|
1662
|
+
streamId,
|
|
1663
|
+
message: err.message,
|
|
1664
|
+
}));
|
|
1665
|
+
} catch {}
|
|
1666
|
+
});
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (msg.type === 'ws:message') {
|
|
1671
|
+
const { streamId, data, binary } = msg;
|
|
1672
|
+
const dshWs = peer.wsStreams.get(streamId);
|
|
1673
|
+
if (dshWs && dshWs.readyState === WebSocket.OPEN) {
|
|
1674
|
+
if (binary) {
|
|
1675
|
+
dshWs.send(Buffer.from(data, 'base64'), { binary: true });
|
|
1676
|
+
} else {
|
|
1677
|
+
dshWs.send(data);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
if (msg.type === 'ws:close') {
|
|
1684
|
+
const { streamId, code, reason } = msg;
|
|
1685
|
+
const dshWs = peer.wsStreams.get(streamId);
|
|
1686
|
+
if (dshWs) {
|
|
1687
|
+
try { dshWs.close(code, reason); } catch {}
|
|
1688
|
+
peer.wsStreams.delete(streamId);
|
|
1689
|
+
}
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1389
1694
|
connect();
|
|
1390
1695
|
|
|
1391
1696
|
return {
|
|
@@ -1395,6 +1700,10 @@ export function startAgent(opts) {
|
|
|
1395
1700
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
1396
1701
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1397
1702
|
if (lanBridge) lanBridge.close();
|
|
1703
|
+
for (const [, peer] of activeRtcPeers) {
|
|
1704
|
+
try { peer.pc.close(); } catch {}
|
|
1705
|
+
}
|
|
1706
|
+
activeRtcPeers.clear();
|
|
1398
1707
|
},
|
|
1399
1708
|
};
|
|
1400
1709
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshost-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Official remote cloud relay plugin for DSHost (dshost.me): securely access your dsh Web UI from anywhere",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"node": ">=22.5.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"werift": "^0.26.0",
|
|
43
44
|
"ws": "^8.18.0"
|
|
44
45
|
},
|
|
45
46
|
"dsh": {
|