dshost-plugin 0.1.21 → 0.1.22
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 +160 -25
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -940,6 +940,21 @@ export function startAgent(opts) {
|
|
|
940
940
|
return;
|
|
941
941
|
}
|
|
942
942
|
|
|
943
|
+
// WebRTC ICE 配置(relay 现签 TURN 临时凭证)
|
|
944
|
+
if (msg.type === 'webrtc:config') {
|
|
945
|
+
const cb = pendingWebrtcConfig.get(msg.reqId);
|
|
946
|
+
if (cb) { pendingWebrtcConfig.delete(msg.reqId); cb(msg); }
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// relay 吊销某个 P2P 连接(被顶替 / 登出 / 封禁 / 超配额)
|
|
951
|
+
if (msg.type === 'webrtc:revoke') {
|
|
952
|
+
if (closePeer(msg.clientId)) {
|
|
953
|
+
log.log?.(`[agent-webrtc] revoked client ${msg.clientId}`);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
943
958
|
// WebRTC P2P 穿透打洞信令交互
|
|
944
959
|
if (msg.type === 'webrtc:signal') {
|
|
945
960
|
handleWebRtcSignalMessage(ws, msg);
|
|
@@ -1428,6 +1443,82 @@ export function startAgent(opts) {
|
|
|
1428
1443
|
|
|
1429
1444
|
const activeRtcPeers = new Map(); // clientId -> { pc, dc, wsStreams }
|
|
1430
1445
|
|
|
1446
|
+
// ── ICE 配置 ──────────────────────────────────────────────────────
|
|
1447
|
+
// TURN 凭证由 relay 现签(relay 持有 TURN_SECRET,agent 不该有)。缓存到
|
|
1448
|
+
// 剩余 10 分钟时刷新;relay 不可达则退化为仅 STUN,P2P 可能失败但不会崩。
|
|
1449
|
+
const STUN_ONLY = [
|
|
1450
|
+
{ urls: 'stun:39.106.35.187:3478' },
|
|
1451
|
+
{ urls: 'stun:111.206.174.2:3478' },
|
|
1452
|
+
{ urls: 'stun:111.206.174.3:3478' },
|
|
1453
|
+
{ urls: 'stun:47.100.127.239:3478' },
|
|
1454
|
+
{ urls: 'stun:stun.qq.com:3478' },
|
|
1455
|
+
{ urls: 'stun:stun.miwifi.com:3478' },
|
|
1456
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
1457
|
+
];
|
|
1458
|
+
let iceCache = { servers: null, expiresAt: 0 };
|
|
1459
|
+
const pendingWebrtcConfig = new Map(); // reqId -> resolve
|
|
1460
|
+
|
|
1461
|
+
function requestWebrtcConfig(ws) {
|
|
1462
|
+
const reqId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1463
|
+
return new Promise((resolve) => {
|
|
1464
|
+
const timer = setTimeout(() => {
|
|
1465
|
+
pendingWebrtcConfig.delete(reqId);
|
|
1466
|
+
resolve(null);
|
|
1467
|
+
}, 5000);
|
|
1468
|
+
pendingWebrtcConfig.set(reqId, (msg) => {
|
|
1469
|
+
clearTimeout(timer);
|
|
1470
|
+
resolve(msg);
|
|
1471
|
+
});
|
|
1472
|
+
send(ws, { type: 'webrtc:config-request', reqId });
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function getIceServers(ws) {
|
|
1477
|
+
const now = Date.now();
|
|
1478
|
+
if (iceCache.servers && iceCache.expiresAt - now > 600_000) return iceCache.servers;
|
|
1479
|
+
const cfg = await requestWebrtcConfig(ws);
|
|
1480
|
+
if (cfg && Array.isArray(cfg.iceServers) && cfg.iceServers.length) {
|
|
1481
|
+
const ttlMs = (Number(cfg.ttl) || 3600) * 1000;
|
|
1482
|
+
iceCache = { servers: cfg.iceServers, expiresAt: now + ttlMs };
|
|
1483
|
+
return iceCache.servers;
|
|
1484
|
+
}
|
|
1485
|
+
return STUN_ONLY;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// ── P2P 用量统计(每 60s 汇总上报,供 relay 记账与配额)─────────────
|
|
1489
|
+
function bump(peer, dir, n) {
|
|
1490
|
+
if (!peer || !peer.usage || !n) return;
|
|
1491
|
+
if (dir === 'up') peer.usage.up += n;
|
|
1492
|
+
else peer.usage.down += n;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function flushUsage(peer) {
|
|
1496
|
+
if (!peer || !peer.usage || !peer.ws || peer.ws.readyState !== 1) return;
|
|
1497
|
+
const { up, down } = peer.usage;
|
|
1498
|
+
if (!up && !down) return;
|
|
1499
|
+
peer.usage.up = 0;
|
|
1500
|
+
peer.usage.down = 0;
|
|
1501
|
+
try {
|
|
1502
|
+
send(peer.ws, {
|
|
1503
|
+
type: 'webrtc:usage',
|
|
1504
|
+
clientId: peer.clientId,
|
|
1505
|
+
bytesUp: up,
|
|
1506
|
+
bytesDown: down,
|
|
1507
|
+
streams: peer.wsStreams.size,
|
|
1508
|
+
windowMs: 60_000,
|
|
1509
|
+
});
|
|
1510
|
+
} catch {}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function closePeer(clientId) {
|
|
1514
|
+
const peer = activeRtcPeers.get(clientId);
|
|
1515
|
+
if (!peer) return false;
|
|
1516
|
+
flushUsage(peer);
|
|
1517
|
+
try { peer.pc.close(); } catch {}
|
|
1518
|
+
activeRtcPeers.delete(clientId);
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1431
1522
|
async function handleWebRtcSignalMessage(ws, msg) {
|
|
1432
1523
|
const { reqId, signalType, sdp, candidate, clientId = 'default' } = msg;
|
|
1433
1524
|
if (signalType === 'offer') {
|
|
@@ -1449,36 +1540,21 @@ export function startAgent(opts) {
|
|
|
1449
1540
|
|
|
1450
1541
|
try {
|
|
1451
1542
|
const existing = activeRtcPeers.get(clientId);
|
|
1452
|
-
if (existing)
|
|
1453
|
-
try { existing.pc.close(); } catch {}
|
|
1454
|
-
activeRtcPeers.delete(clientId);
|
|
1455
|
-
}
|
|
1543
|
+
if (existing) closePeer(clientId);
|
|
1456
1544
|
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
{ urls: 'stun:39.106.35.187:3478' },
|
|
1460
|
-
{ urls: 'stun:111.206.174.2:3478' },
|
|
1461
|
-
{ urls: 'stun:111.206.174.3:3478' },
|
|
1462
|
-
{ urls: 'stun:47.100.127.239:3478' },
|
|
1463
|
-
{ urls: 'stun:stun.qq.com:3478' },
|
|
1464
|
-
{ urls: 'stun:stun.miwifi.com:3478' },
|
|
1465
|
-
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
1466
|
-
{
|
|
1467
|
-
urls: [
|
|
1468
|
-
'turn:39.106.35.187:3478?transport=udp',
|
|
1469
|
-
'turn:39.106.35.187:3478?transport=tcp',
|
|
1470
|
-
],
|
|
1471
|
-
username: 'dshost',
|
|
1472
|
-
credential: 'dshost_p2p_secret',
|
|
1473
|
-
},
|
|
1474
|
-
],
|
|
1475
|
-
});
|
|
1545
|
+
const iceServers = await getIceServers(ws);
|
|
1546
|
+
const pc = new werift.RTCPeerConnection({ iceServers });
|
|
1476
1547
|
|
|
1477
1548
|
const peer = {
|
|
1478
1549
|
clientId,
|
|
1479
1550
|
pc,
|
|
1480
1551
|
dc: null,
|
|
1552
|
+
ws,
|
|
1481
1553
|
wsStreams: new Map(),
|
|
1554
|
+
httpReqs: new Map(),
|
|
1555
|
+
nonce: typeof msg.peerNonce === 'string' ? msg.peerNonce : '',
|
|
1556
|
+
authed: !msg.peerNonce,
|
|
1557
|
+
usage: { up: 0, down: 0, timer: null },
|
|
1482
1558
|
};
|
|
1483
1559
|
activeRtcPeers.set(clientId, peer);
|
|
1484
1560
|
|
|
@@ -1491,13 +1567,36 @@ export function startAgent(opts) {
|
|
|
1491
1567
|
handleRtcDataChannelMessage(peer, dc, e.data);
|
|
1492
1568
|
};
|
|
1493
1569
|
|
|
1570
|
+
// 首帧必须是 auth。5s 内未认证就直接断开,避免半开连接长期挂着。
|
|
1571
|
+
if (!peer.authed) {
|
|
1572
|
+
peer.authTimer = setTimeout(() => {
|
|
1573
|
+
if (!peer.authed) {
|
|
1574
|
+
log.warn?.(`[agent-webrtc] client ${clientId} failed to authenticate, closing`);
|
|
1575
|
+
closePeer(clientId);
|
|
1576
|
+
}
|
|
1577
|
+
}, 5000);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// 用量每 60s 汇总一次;DC 关闭时再冲一次尾账。
|
|
1581
|
+
peer.usage.timer = setInterval(() => flushUsage(peer), 60_000);
|
|
1582
|
+
|
|
1494
1583
|
dc.onclose = () => {
|
|
1495
1584
|
log.log?.(`[agent-webrtc] DataChannel closed for client ${clientId}`);
|
|
1496
1585
|
for (const [, sWs] of peer.wsStreams) {
|
|
1497
1586
|
try { sWs.close(); } catch {}
|
|
1498
1587
|
}
|
|
1499
1588
|
peer.wsStreams.clear();
|
|
1589
|
+
for (const [, req] of peer.httpReqs) {
|
|
1590
|
+
try { req.destroy(); } catch {}
|
|
1591
|
+
}
|
|
1592
|
+
peer.httpReqs.clear();
|
|
1593
|
+
flushUsage(peer);
|
|
1594
|
+
if (peer.usage.timer) clearInterval(peer.usage.timer);
|
|
1595
|
+
if (peer.authTimer) clearTimeout(peer.authTimer);
|
|
1500
1596
|
activeRtcPeers.delete(clientId);
|
|
1597
|
+
if (peer.authed && peer.ws && peer.ws.readyState === 1) {
|
|
1598
|
+
try { send(peer.ws, { type: 'webrtc:peer-state', clientId, state: 'closed' }); } catch {}
|
|
1599
|
+
}
|
|
1501
1600
|
};
|
|
1502
1601
|
|
|
1503
1602
|
dc.onerror = (err) => {
|
|
@@ -1510,9 +1609,11 @@ export function startAgent(opts) {
|
|
|
1510
1609
|
await pc.setLocalDescription(answer);
|
|
1511
1610
|
|
|
1512
1611
|
// 等待 ICE 候选收集(嵌入至 localDescription 中返回,单次 RTT 快速握手)
|
|
1612
|
+
// 2500ms:TURN-only 路径经常超过原来的 1200ms,窗口太短会拿到没有
|
|
1613
|
+
// relay candidate 的 answer,握手必然失败。
|
|
1513
1614
|
await new Promise((resolve) => {
|
|
1514
1615
|
if (pc.iceGatheringState === 'complete') return resolve();
|
|
1515
|
-
const timer = setTimeout(resolve,
|
|
1616
|
+
const timer = setTimeout(resolve, 2500);
|
|
1516
1617
|
pc.onicegatheringstatechange = () => {
|
|
1517
1618
|
if (pc.iceGatheringState === 'complete') {
|
|
1518
1619
|
clearTimeout(timer);
|
|
@@ -1560,6 +1661,19 @@ export function startAgent(opts) {
|
|
|
1560
1661
|
|
|
1561
1662
|
if (!msg || !msg.type) return;
|
|
1562
1663
|
|
|
1664
|
+
// 0. 认证门禁:未认证前只接受 auth,其余消息一律丢弃。
|
|
1665
|
+
if (!peer.authed) {
|
|
1666
|
+
if (msg.type === 'auth' && peer.nonce && msg.nonce === peer.nonce) {
|
|
1667
|
+
peer.authed = true;
|
|
1668
|
+
if (peer.authTimer) { clearTimeout(peer.authTimer); peer.authTimer = null; }
|
|
1669
|
+
log.log?.(`[agent-webrtc] client ${peer.clientId} authenticated`);
|
|
1670
|
+
if (peer.ws && peer.ws.readyState === 1) {
|
|
1671
|
+
try { send(peer.ws, { type: 'webrtc:peer-state', clientId: peer.clientId, state: 'open' }); } catch {}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1563
1677
|
// 1. P2P 测速与探测 (Ping / Pong)
|
|
1564
1678
|
if (msg.type === 'ping') {
|
|
1565
1679
|
try {
|
|
@@ -1583,6 +1697,8 @@ export function startAgent(opts) {
|
|
|
1583
1697
|
targetHeaders.origin = `http://${dshHost}:${dshPort}`;
|
|
1584
1698
|
if (authCookie) targetHeaders.cookie = authCookie;
|
|
1585
1699
|
delete targetHeaders['content-length'];
|
|
1700
|
+
// 压缩后的字节没法塞进构造型 Response 让浏览器解压,直接要明文。
|
|
1701
|
+
targetHeaders['accept-encoding'] = 'identity';
|
|
1586
1702
|
|
|
1587
1703
|
const dshReq = http.request(`http://${dshHost}:${dshPort}${url}`, {
|
|
1588
1704
|
method,
|
|
@@ -1605,6 +1721,7 @@ export function startAgent(opts) {
|
|
|
1605
1721
|
} catch {}
|
|
1606
1722
|
|
|
1607
1723
|
dshRes.on('data', (chunk) => {
|
|
1724
|
+
bump(peer, 'down', chunk.length);
|
|
1608
1725
|
try {
|
|
1609
1726
|
dc.send(JSON.stringify({
|
|
1610
1727
|
type: 'http:chunk',
|
|
@@ -1615,13 +1732,17 @@ export function startAgent(opts) {
|
|
|
1615
1732
|
});
|
|
1616
1733
|
|
|
1617
1734
|
dshRes.on('end', () => {
|
|
1735
|
+
peer.httpReqs.delete(streamId);
|
|
1618
1736
|
try {
|
|
1619
1737
|
dc.send(JSON.stringify({ type: 'http:end', streamId }));
|
|
1620
1738
|
} catch {}
|
|
1621
1739
|
});
|
|
1622
1740
|
});
|
|
1623
1741
|
|
|
1742
|
+
peer.httpReqs.set(streamId, dshReq);
|
|
1743
|
+
|
|
1624
1744
|
dshReq.on('error', (err) => {
|
|
1745
|
+
peer.httpReqs.delete(streamId);
|
|
1625
1746
|
try {
|
|
1626
1747
|
dc.send(JSON.stringify({
|
|
1627
1748
|
type: 'http:response',
|
|
@@ -1639,12 +1760,24 @@ export function startAgent(opts) {
|
|
|
1639
1760
|
});
|
|
1640
1761
|
|
|
1641
1762
|
if (body) {
|
|
1642
|
-
|
|
1763
|
+
const buf = Buffer.from(body, 'base64');
|
|
1764
|
+
bump(peer, 'up', buf.length);
|
|
1765
|
+
dshReq.write(buf);
|
|
1643
1766
|
}
|
|
1644
1767
|
dshReq.end();
|
|
1645
1768
|
return;
|
|
1646
1769
|
}
|
|
1647
1770
|
|
|
1771
|
+
// 2b. 浏览器中断 HTTP 流(AbortSignal)
|
|
1772
|
+
if (msg.type === 'http:abort') {
|
|
1773
|
+
const req = peer.httpReqs.get(msg.streamId);
|
|
1774
|
+
if (req) {
|
|
1775
|
+
try { req.destroy(); } catch {}
|
|
1776
|
+
peer.httpReqs.delete(msg.streamId);
|
|
1777
|
+
}
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1648
1781
|
// 3. WebSocket 虚拟流转发 (终端 / RPC)
|
|
1649
1782
|
if (msg.type === 'ws:open') {
|
|
1650
1783
|
const { streamId, url = '/api/remote.mux' } = msg;
|
|
@@ -1667,6 +1800,7 @@ export function startAgent(opts) {
|
|
|
1667
1800
|
});
|
|
1668
1801
|
|
|
1669
1802
|
dshWs.on('message', (data, isBinary) => {
|
|
1803
|
+
bump(peer, 'down', isBinary ? data.length : Buffer.byteLength(data, 'utf8'));
|
|
1670
1804
|
try {
|
|
1671
1805
|
dc.send(JSON.stringify({
|
|
1672
1806
|
type: 'ws:message',
|
|
@@ -1703,6 +1837,7 @@ export function startAgent(opts) {
|
|
|
1703
1837
|
|
|
1704
1838
|
if (msg.type === 'ws:message') {
|
|
1705
1839
|
const { streamId, data, binary } = msg;
|
|
1840
|
+
bump(peer, 'up', binary ? Buffer.from(data, 'base64').length : Buffer.byteLength(data, 'utf8'));
|
|
1706
1841
|
const dshWs = peer.wsStreams.get(streamId);
|
|
1707
1842
|
if (dshWs && dshWs.readyState === WebSocket.OPEN) {
|
|
1708
1843
|
if (binary) {
|
package/package.json
CHANGED