@livedesk/hub 0.1.35 → 0.1.37
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/package.json +2 -2
- package/src/agents/agent-audit-store.js +6 -16
- package/src/agents/agent-permissions.js +27 -33
- package/src/agents/agent-tool-registry.js +322 -340
- package/src/captures/capture-store.js +252 -299
- package/src/filesystem/shared-folders.js +181 -189
- package/src/filesystem/transfer-jobs.js +314 -345
- package/src/live-desk-update.js +615 -683
- package/src/remote-hub.js +59 -643
- package/src/server.js +111 -924
- package/src/settings/settings-schema.js +40 -32
- package/src/settings/settings-store.js +2 -7
- package/src/transport/relay-hub-control.js +1376 -1703
- package/src/transport/udp-hub-transport.js +520 -543
- package/src/transport/udp-rendezvous.js +408 -574
- package/src/security/device-credential-authority.js +0 -406
- package/src/security/security-audit-store.js +0 -260
- package/src/transport/secure-direct-acceptor.js +0 -543
package/src/remote-hub.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
|
-
import {
|
|
5
|
-
import { createDeviceCredentialAuthority } from './security/device-credential-authority.js';
|
|
6
|
-
import { createHubRelayControl, createSecureHubRelayControl } from './transport/relay-hub-control.js';
|
|
7
|
-
import { createSecureDirectAcceptor } from './transport/secure-direct-acceptor.js';
|
|
4
|
+
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
8
5
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
9
6
|
import {
|
|
10
7
|
BoundedSegmentedBuffer,
|
|
@@ -38,7 +35,6 @@ const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
|
|
|
38
35
|
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
36
|
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
37
|
+ (256 * 1024);
|
|
41
|
-
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
42
38
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
43
39
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
44
40
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
@@ -47,7 +43,7 @@ const RECENT_TASK_LIMIT = 12;
|
|
|
47
43
|
const RECENT_TASK_BATCH_LIMIT = 16;
|
|
48
44
|
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
49
45
|
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
50
|
-
const RECENT_LIVE_FRAME_CACHE_LIMIT =
|
|
46
|
+
const RECENT_LIVE_FRAME_CACHE_LIMIT = 1;
|
|
51
47
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
52
48
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
53
49
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
@@ -837,53 +833,6 @@ function isPrivateNetworkAddress(value) {
|
|
|
837
833
|
|| address.startsWith('fd');
|
|
838
834
|
}
|
|
839
835
|
|
|
840
|
-
export function createRemoteMessageRateGate({
|
|
841
|
-
limit = MAX_AGENT_MESSAGES_PER_SECOND,
|
|
842
|
-
windowMs = 1_000,
|
|
843
|
-
now = () => Date.now()
|
|
844
|
-
} = {}) {
|
|
845
|
-
const boundedLimit = Math.max(1, Math.min(10_000, Math.floor(Number(limit) || MAX_AGENT_MESSAGES_PER_SECOND)));
|
|
846
|
-
const boundedWindowMs = Math.max(100, Math.min(60_000, Math.floor(Number(windowMs) || 1_000)));
|
|
847
|
-
let windowStartedAt = Number(now());
|
|
848
|
-
let count = 0;
|
|
849
|
-
return Object.freeze({
|
|
850
|
-
consume() {
|
|
851
|
-
const current = Number(now());
|
|
852
|
-
if (!Number.isFinite(current) || current - windowStartedAt >= boundedWindowMs || current < windowStartedAt) {
|
|
853
|
-
windowStartedAt = current;
|
|
854
|
-
count = 0;
|
|
855
|
-
}
|
|
856
|
-
count += 1;
|
|
857
|
-
return count <= boundedLimit;
|
|
858
|
-
},
|
|
859
|
-
getStatus: () => ({ count, limit: boundedLimit, windowMs: boundedWindowMs, windowStartedAt })
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
export function evaluateRemoteConnectionPolicy({
|
|
864
|
-
remoteAddress = '',
|
|
865
|
-
relayConnection = false,
|
|
866
|
-
encrypted = false,
|
|
867
|
-
allowLegacyPlaintextForTests = false,
|
|
868
|
-
policy = {}
|
|
869
|
-
} = {}) {
|
|
870
|
-
const address = String(remoteAddress || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
871
|
-
if (policy.requireEncryptedConnections === true
|
|
872
|
-
&& encrypted !== true
|
|
873
|
-
&& allowLegacyPlaintextForTests !== true) {
|
|
874
|
-
return 'encrypted-connection-required';
|
|
875
|
-
}
|
|
876
|
-
if (relayConnection === true || !isPrivateNetworkAddress(address)) {
|
|
877
|
-
return policy.allowInternetConnections === false
|
|
878
|
-
? 'internet-connections-blocked-by-settings'
|
|
879
|
-
: '';
|
|
880
|
-
}
|
|
881
|
-
if (!isLoopbackHost(address) && policy.allowLanConnections === false) {
|
|
882
|
-
return 'lan-connections-blocked-by-settings';
|
|
883
|
-
}
|
|
884
|
-
return '';
|
|
885
|
-
}
|
|
886
|
-
|
|
887
836
|
function readCpuTimes() {
|
|
888
837
|
let idle = 0;
|
|
889
838
|
let total = 0;
|
|
@@ -1213,24 +1162,21 @@ const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
|
1213
1162
|
'process.list',
|
|
1214
1163
|
'service.status',
|
|
1215
1164
|
'diagnostics.collect',
|
|
1216
|
-
'file.read',
|
|
1217
|
-
'file.list',
|
|
1218
|
-
'network.status',
|
|
1219
|
-
'logs.collect'
|
|
1220
|
-
]);
|
|
1221
|
-
|
|
1222
|
-
const RETIRED_AGENT_MUTATING_OPERATIONS = new Set([
|
|
1223
1165
|
'process.control',
|
|
1224
1166
|
'service.control',
|
|
1225
1167
|
'application.launch',
|
|
1226
1168
|
'application.close',
|
|
1169
|
+
'file.read',
|
|
1227
1170
|
'file.write',
|
|
1228
1171
|
'file.delete',
|
|
1172
|
+
'file.list',
|
|
1229
1173
|
'command.run',
|
|
1230
1174
|
'script.run',
|
|
1231
1175
|
'software.install',
|
|
1176
|
+
'network.status',
|
|
1232
1177
|
'system.power',
|
|
1233
|
-
'system.configure'
|
|
1178
|
+
'system.configure',
|
|
1179
|
+
'logs.collect'
|
|
1234
1180
|
]);
|
|
1235
1181
|
|
|
1236
1182
|
function normalizeAgentOperation(value) {
|
|
@@ -1402,7 +1348,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
|
|
|
1402
1348
|
return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
|
|
1403
1349
|
}
|
|
1404
1350
|
|
|
1405
|
-
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1351
|
+
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1406
1352
|
if (!frame) {
|
|
1407
1353
|
return null;
|
|
1408
1354
|
}
|
|
@@ -1424,27 +1370,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
|
1424
1370
|
serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
|
|
1425
1371
|
}
|
|
1426
1372
|
|
|
1427
|
-
return serialized;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function serializeActiveLiveStream(activeLiveStream, deviceId) {
|
|
1431
|
-
if (!activeLiveStream) {
|
|
1432
|
-
return null;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
return {
|
|
1436
|
-
...activeLiveStream,
|
|
1437
|
-
// The active descriptor is status metadata.
|
|
1438
|
-
// owner
|
|
1439
|
-
//
|
|
1440
|
-
latestFrame: serializeRemoteFrame(
|
|
1441
|
-
activeLiveStream.latestFrame,
|
|
1442
|
-
deviceId,
|
|
1443
|
-
'live',
|
|
1444
|
-
{ includeDataUrl: false }
|
|
1445
|
-
)
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1373
|
+
return serialized;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function serializeActiveLiveStream(activeLiveStream, deviceId) {
|
|
1377
|
+
if (!activeLiveStream) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return {
|
|
1382
|
+
...activeLiveStream,
|
|
1383
|
+
// The active descriptor is status metadata. Preserve the Hub's exact
|
|
1384
|
+
// internal frame owner for delivery, but never copy encoded bytes or
|
|
1385
|
+
// its access token into a browser status snapshot.
|
|
1386
|
+
latestFrame: serializeRemoteFrame(
|
|
1387
|
+
activeLiveStream.latestFrame,
|
|
1388
|
+
deviceId,
|
|
1389
|
+
'live',
|
|
1390
|
+
{ includeDataUrl: false }
|
|
1391
|
+
)
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1448
1394
|
|
|
1449
1395
|
function getRecentFrameCache(device, frameKind) {
|
|
1450
1396
|
if (!device) {
|
|
@@ -1478,7 +1424,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
|
|
|
1478
1424
|
: RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
|
|
1479
1425
|
}
|
|
1480
1426
|
|
|
1481
|
-
|
|
1427
|
+
function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
1482
1428
|
if (!Array.isArray(cache)) {
|
|
1483
1429
|
return;
|
|
1484
1430
|
}
|
|
@@ -1499,20 +1445,6 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1499
1445
|
}
|
|
1500
1446
|
}
|
|
1501
1447
|
|
|
1502
|
-
if (frameKind !== 'thumbnail' && cache.length > 1) {
|
|
1503
|
-
// Keep the newest frame plus the newest independently decodable key
|
|
1504
|
-
// frame. A browser can subscribe after capture has already started;
|
|
1505
|
-
// retaining only the latest 30 fps delta makes that browser wait for a
|
|
1506
|
-
// future GOP even though the Hub accepted a current key frame moments
|
|
1507
|
-
// earlier. Exact stream-owner matching below prevents stale replay.
|
|
1508
|
-
const latest = cache[0];
|
|
1509
|
-
const keyFrame = cache.find(entry =>
|
|
1510
|
-
entry !== latest
|
|
1511
|
-
&& (entry?.frame?.isKeyFrame === true
|
|
1512
|
-
|| safeString(entry?.frame?.chunkType, 20).toLowerCase() === 'key'));
|
|
1513
|
-
cache.splice(0, cache.length, ...[latest, keyFrame].filter(Boolean));
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
1448
|
for (let index = 0; index < cache.length; index += 1) {
|
|
1517
1449
|
const entry = cache[index];
|
|
1518
1450
|
const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
|
|
@@ -1524,7 +1456,7 @@ export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1524
1456
|
}
|
|
1525
1457
|
}
|
|
1526
1458
|
|
|
1527
|
-
|
|
1459
|
+
function rememberRecentFramePayload(device, frameKind, frame) {
|
|
1528
1460
|
if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
|
|
1529
1461
|
return;
|
|
1530
1462
|
}
|
|
@@ -1560,7 +1492,7 @@ export function rememberRecentFramePayload(device, frameKind, frame) {
|
|
|
1560
1492
|
pruneRecentFrameCache(cache, kind, nowMs);
|
|
1561
1493
|
}
|
|
1562
1494
|
|
|
1563
|
-
|
|
1495
|
+
function isFramePayloadRequestMatch(frame, options = {}) {
|
|
1564
1496
|
if (!frame || !Buffer.isBuffer(frame.payload)) {
|
|
1565
1497
|
return false;
|
|
1566
1498
|
}
|
|
@@ -1579,37 +1511,10 @@ export function isFramePayloadRequestMatch(frame, options = {}) {
|
|
|
1579
1511
|
return false;
|
|
1580
1512
|
}
|
|
1581
1513
|
|
|
1582
|
-
if (options.requireKeyFrame === true
|
|
1583
|
-
&& frame.isKeyFrame !== true
|
|
1584
|
-
&& safeString(frame.chunkType, 20).toLowerCase() !== 'key') {
|
|
1585
|
-
return false;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
for (const key of ['streamId', 'sessionId', 'commandId', 'streamPurpose']) {
|
|
1589
|
-
const expected = safeString(options[key], 160);
|
|
1590
|
-
if (expected && safeString(frame[key], 160) !== expected) {
|
|
1591
|
-
return false;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
const requestedCaptureGeneration = Number(options.captureGeneration);
|
|
1596
|
-
if (Number.isFinite(requestedCaptureGeneration)
|
|
1597
|
-
&& requestedCaptureGeneration > 0
|
|
1598
|
-
&& Number(frame.captureGeneration || 0) !== requestedCaptureGeneration) {
|
|
1599
|
-
return false;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const requestedMonitorIndex = Number(options.monitorIndex);
|
|
1603
|
-
if (Number.isFinite(requestedMonitorIndex)
|
|
1604
|
-
&& requestedMonitorIndex >= 0
|
|
1605
|
-
&& Number(frame.monitorIndex ?? -1) !== requestedMonitorIndex) {
|
|
1606
|
-
return false;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
1514
|
return true;
|
|
1610
1515
|
}
|
|
1611
1516
|
|
|
1612
|
-
|
|
1517
|
+
function findRecentFramePayload(device, frameKind, options = {}) {
|
|
1613
1518
|
const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
|
|
1614
1519
|
const latest = kind === 'thumbnail'
|
|
1615
1520
|
? device?.latestThumbnail
|
|
@@ -1620,14 +1525,7 @@ export function findRecentFramePayload(device, frameKind, options = {}) {
|
|
|
1620
1525
|
|
|
1621
1526
|
const token = safeString(options.token, 128);
|
|
1622
1527
|
const requestedSeq = Number(options.frameSeq);
|
|
1623
|
-
|
|
1624
|
-
|| ['streamId', 'sessionId', 'commandId', 'streamPurpose']
|
|
1625
|
-
.some(key => safeString(options[key], 160))
|
|
1626
|
-
|| (Number.isFinite(Number(options.captureGeneration))
|
|
1627
|
-
&& Number(options.captureGeneration) > 0)
|
|
1628
|
-
|| (Number.isFinite(Number(options.monitorIndex))
|
|
1629
|
-
&& Number(options.monitorIndex) >= 0);
|
|
1630
|
-
if (!token && !Number.isFinite(requestedSeq) && !ownerQuery) {
|
|
1528
|
+
if (!token && !Number.isFinite(requestedSeq)) {
|
|
1631
1529
|
return null;
|
|
1632
1530
|
}
|
|
1633
1531
|
|
|
@@ -1683,7 +1581,7 @@ function serializeDevice(device, options = {}) {
|
|
|
1683
1581
|
},
|
|
1684
1582
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1685
1583
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1686
|
-
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1584
|
+
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1687
1585
|
liveCapturePause: device.liveCapturePause
|
|
1688
1586
|
? {
|
|
1689
1587
|
token: device.liveCapturePause.token,
|
|
@@ -2301,16 +2199,6 @@ export function createRemoteHub(options = {}) {
|
|
|
2301
2199
|
}
|
|
2302
2200
|
}
|
|
2303
2201
|
|
|
2304
|
-
function connectionPolicyError(socket, policy = {}) {
|
|
2305
|
-
return evaluateRemoteConnectionPolicy({
|
|
2306
|
-
remoteAddress: socket?.remoteAddress || '',
|
|
2307
|
-
relayConnection: socket?.__liveDeskRelayControl === true,
|
|
2308
|
-
encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
|
|
2309
|
-
allowLegacyPlaintextForTests,
|
|
2310
|
-
policy
|
|
2311
|
-
});
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
2202
|
function policyError(device, permission = '', command = '') {
|
|
2315
2203
|
const policy = getDevicePolicy(device);
|
|
2316
2204
|
const commandName = safeString(command, 80);
|
|
@@ -2359,83 +2247,22 @@ export function createRemoteHub(options = {}) {
|
|
|
2359
2247
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
2360
2248
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
2361
2249
|
const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
|
|
2362
|
-
const
|
|
2363
|
-
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
|
|
2250
|
+
const pairToken = safeString(
|
|
2251
|
+
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
|
|
2364
2252
|
256);
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
|
|
2374
|
-
? configuredEnrollmentToken
|
|
2375
|
-
: canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
|
|
2376
|
-
const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
|
|
2377
|
-
dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
|
|
2378
|
-
});
|
|
2379
|
-
const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
|
|
2380
|
-
? options.getSecurityIdentity
|
|
2381
|
-
: () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
|
|
2382
|
-
udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
|
|
2383
|
-
const identity = getSecurityIdentity() || {};
|
|
2384
|
-
return deviceCredentialAuthority.issueRendezvousProof({
|
|
2385
|
-
roomId,
|
|
2386
|
-
deviceId,
|
|
2387
|
-
role,
|
|
2388
|
-
ttlMs,
|
|
2389
|
-
accountId: safeString(identity.accountId, 128)
|
|
2253
|
+
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2254
|
+
? options.relayControl
|
|
2255
|
+
: createHubRelayControl({
|
|
2256
|
+
env,
|
|
2257
|
+
pairToken,
|
|
2258
|
+
logEvent,
|
|
2259
|
+
logWarn,
|
|
2260
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2390
2261
|
});
|
|
2391
|
-
});
|
|
2392
2262
|
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2393
2263
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2394
2264
|
: generatePairingPin();
|
|
2395
2265
|
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2396
|
-
const consumeEnrollmentToken = expected => {
|
|
2397
|
-
if (!timingSafeStringEqual(expected, pairToken)) return false;
|
|
2398
|
-
pairToken = crypto.randomBytes(32).toString('base64url');
|
|
2399
|
-
pairingPin = generatePairingPin();
|
|
2400
|
-
pairingPinUpdatedAt = new Date().toISOString();
|
|
2401
|
-
emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
|
|
2402
|
-
pairingPinUpdatedAt,
|
|
2403
|
-
hubId: deviceCredentialAuthority.hubId
|
|
2404
|
-
});
|
|
2405
|
-
return true;
|
|
2406
|
-
};
|
|
2407
|
-
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2408
|
-
? options.relayControl
|
|
2409
|
-
: allowLegacyPlaintextForTests
|
|
2410
|
-
? createHubRelayControl({
|
|
2411
|
-
env,
|
|
2412
|
-
pairToken,
|
|
2413
|
-
logEvent,
|
|
2414
|
-
logWarn,
|
|
2415
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2416
|
-
})
|
|
2417
|
-
: createSecureHubRelayControl({
|
|
2418
|
-
env,
|
|
2419
|
-
authority: deviceCredentialAuthority,
|
|
2420
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2421
|
-
getEnrollmentToken: () => pairToken,
|
|
2422
|
-
consumeEnrollmentToken,
|
|
2423
|
-
logEvent,
|
|
2424
|
-
logWarn,
|
|
2425
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2426
|
-
});
|
|
2427
|
-
const secureDirectAcceptor = createSecureDirectAcceptor({
|
|
2428
|
-
authority: deviceCredentialAuthority,
|
|
2429
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2430
|
-
getEnrollmentToken: () => pairToken,
|
|
2431
|
-
consumeEnrollmentToken,
|
|
2432
|
-
onSecureSocket: socket => handleTcpAgentSocket(socket),
|
|
2433
|
-
onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
|
|
2434
|
-
...audit,
|
|
2435
|
-
remoteHub: getStatus({ includeSecrets: false })
|
|
2436
|
-
}),
|
|
2437
|
-
logWarn
|
|
2438
|
-
});
|
|
2439
2266
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2440
2267
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2441
2268
|
5000,
|
|
@@ -2461,7 +2288,6 @@ export function createRemoteHub(options = {}) {
|
|
|
2461
2288
|
const transportDiagnosticStartingDevices = new Set();
|
|
2462
2289
|
const duplicateDeviceLogAt = new Map();
|
|
2463
2290
|
let server = null;
|
|
2464
|
-
let controlIdleSweepTimer = null;
|
|
2465
2291
|
let started = false;
|
|
2466
2292
|
let boundPort = requestedPort;
|
|
2467
2293
|
let lastError = '';
|
|
@@ -2783,7 +2609,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2783
2609
|
host,
|
|
2784
2610
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2785
2611
|
port: boundPort || requestedPort,
|
|
2786
|
-
protocol: '
|
|
2612
|
+
protocol: 'tcp-jsonl',
|
|
2787
2613
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2788
2614
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2789
2615
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
@@ -2803,15 +2629,6 @@ export function createRemoteHub(options = {}) {
|
|
|
2803
2629
|
pairTokenPreview: maskToken(pairToken),
|
|
2804
2630
|
pairingPin: includeSecrets ? pairingPin : '',
|
|
2805
2631
|
pairingPinUpdatedAt,
|
|
2806
|
-
security: {
|
|
2807
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
2808
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
2809
|
-
hubPublicKeyFingerprint: crypto.createHash('sha256')
|
|
2810
|
-
.update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
|
|
2811
|
-
.digest('hex'),
|
|
2812
|
-
enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
|
|
2813
|
-
legacyPlaintextForTests: allowLegacyPlaintextForTests
|
|
2814
|
-
},
|
|
2815
2632
|
deviceCount: devices.size,
|
|
2816
2633
|
connectedDeviceCount: connectedDevices,
|
|
2817
2634
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -4078,14 +3895,6 @@ export function createRemoteHub(options = {}) {
|
|
|
4078
3895
|
}
|
|
4079
3896
|
|
|
4080
3897
|
const writeOwner = device.inputWriteOwner;
|
|
4081
|
-
if (device.controlOwnerConnectionId) {
|
|
4082
|
-
endControlSession(
|
|
4083
|
-
device,
|
|
4084
|
-
device.controlOwnerConnectionId,
|
|
4085
|
-
reason,
|
|
4086
|
-
writeOwner,
|
|
4087
|
-
writeOwner?.getBindingKey?.() || '');
|
|
4088
|
-
}
|
|
4089
3898
|
device.inputSocket = null;
|
|
4090
3899
|
device.inputWriteOwner = null;
|
|
4091
3900
|
device.inputSocketConnectionId = '';
|
|
@@ -4118,14 +3927,6 @@ export function createRemoteHub(options = {}) {
|
|
|
4118
3927
|
}
|
|
4119
3928
|
|
|
4120
3929
|
const writeOwner = device.inputWriteOwner;
|
|
4121
|
-
if (device.controlOwnerConnectionId) {
|
|
4122
|
-
endControlSession(
|
|
4123
|
-
device,
|
|
4124
|
-
device.controlOwnerConnectionId,
|
|
4125
|
-
reason,
|
|
4126
|
-
null,
|
|
4127
|
-
'');
|
|
4128
|
-
}
|
|
4129
3930
|
device.inputSocket = null;
|
|
4130
3931
|
device.inputWriteOwner = null;
|
|
4131
3932
|
device.inputSocketConnectionId = '';
|
|
@@ -4559,10 +4360,6 @@ export function createRemoteHub(options = {}) {
|
|
|
4559
4360
|
inputSocketConnectionId: '',
|
|
4560
4361
|
inputOwnerConnectionId: '',
|
|
4561
4362
|
inputOwnerBindingKey: '',
|
|
4562
|
-
controlOwnerSessionId: '',
|
|
4563
|
-
controlOwnerConnectionId: '',
|
|
4564
|
-
controlOwnerStartedAtMs: 0,
|
|
4565
|
-
controlOwnerLastInputAtMs: 0,
|
|
4566
4363
|
frameSocket: null,
|
|
4567
4364
|
audioSocket: null,
|
|
4568
4365
|
fileSocket: null,
|
|
@@ -7484,12 +7281,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7484
7281
|
}
|
|
7485
7282
|
|
|
7486
7283
|
if (!state.authenticated) {
|
|
7487
|
-
const securityContext = socket.__liveDeskSecurityContext;
|
|
7488
|
-
const secureDeviceAuthenticated = securityContext?.authenticated === true
|
|
7489
|
-
&& securityContext?.encrypted === true
|
|
7490
|
-
&& safeString(securityContext.accountId, 128)
|
|
7491
|
-
&& safeString(securityContext.hubId, 128)
|
|
7492
|
-
&& safeString(securityContext.deviceId, 128);
|
|
7493
7284
|
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
7494
7285
|
message = {
|
|
7495
7286
|
...message,
|
|
@@ -7502,9 +7293,8 @@ export function createRemoteHub(options = {}) {
|
|
|
7502
7293
|
};
|
|
7503
7294
|
}
|
|
7504
7295
|
if (message.type === 'slot.assign') {
|
|
7505
|
-
if (!
|
|
7506
|
-
|
|
7507
|
-
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
|
|
7296
|
+
if (!timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7297
|
+
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'invalid-pair-token' });
|
|
7508
7298
|
socket.end();
|
|
7509
7299
|
return;
|
|
7510
7300
|
}
|
|
@@ -7521,52 +7311,8 @@ export function createRemoteHub(options = {}) {
|
|
|
7521
7311
|
return;
|
|
7522
7312
|
}
|
|
7523
7313
|
|
|
7524
|
-
if (
|
|
7525
|
-
|
|
7526
|
-
writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
|
|
7527
|
-
socket.destroy();
|
|
7528
|
-
return;
|
|
7529
|
-
}
|
|
7530
|
-
const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
|
|
7531
|
-
if (helloChannel !== securityContext.channel) {
|
|
7532
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
|
|
7533
|
-
socket.destroy();
|
|
7534
|
-
return;
|
|
7535
|
-
}
|
|
7536
|
-
} else {
|
|
7537
|
-
const legacyAllowed = socket.__liveDeskRelayControl === true
|
|
7538
|
-
|| socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
|
|
7539
|
-
if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7540
|
-
writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
|
|
7541
|
-
socket.destroy();
|
|
7542
|
-
return;
|
|
7543
|
-
}
|
|
7544
|
-
}
|
|
7545
|
-
|
|
7546
|
-
const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
|
|
7547
|
-
const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
|
|
7548
|
-
? message.capabilities
|
|
7549
|
-
: {};
|
|
7550
|
-
const incomingPolicy = getWelcomeDevicePolicy({
|
|
7551
|
-
deviceId: incomingDeviceId,
|
|
7552
|
-
capabilities: incomingCapabilities
|
|
7553
|
-
}) || {};
|
|
7554
|
-
const connectionDenied = connectionPolicyError(socket, incomingPolicy);
|
|
7555
|
-
if (connectionDenied) {
|
|
7556
|
-
const deniedTransport = socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp';
|
|
7557
|
-
const deniedRemoteAddress = safeString(socket.remoteAddress, 256) || 'unknown';
|
|
7558
|
-
logWarn('security', 'Remote connection rejected reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress);
|
|
7559
|
-
writeJsonLine(socket, { type: 'error', error: connectionDenied });
|
|
7560
|
-
emitEvent('RemoteSecurityPolicyAudit', {
|
|
7561
|
-
result: 'rejected',
|
|
7562
|
-
reason: connectionDenied,
|
|
7563
|
-
deviceId: incomingDeviceId,
|
|
7564
|
-
sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
|
|
7565
|
-
accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
|
|
7566
|
-
hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
|
|
7567
|
-
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7568
|
-
remoteAddress: safeString(socket.remoteAddress, 256)
|
|
7569
|
-
});
|
|
7314
|
+
if (!timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7315
|
+
writeJsonLine(socket, { type: 'error', error: 'invalid-pair-token' });
|
|
7570
7316
|
socket.destroy();
|
|
7571
7317
|
return;
|
|
7572
7318
|
}
|
|
@@ -7738,26 +7484,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7738
7484
|
}
|
|
7739
7485
|
}
|
|
7740
7486
|
|
|
7741
|
-
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7742
|
-
if (state.messageRateGate.consume()) return true;
|
|
7743
|
-
const device = state.device;
|
|
7744
|
-
emitEvent('RemoteAbuseDefense', {
|
|
7745
|
-
result: 'rejected',
|
|
7746
|
-
reason: 'authenticated-message-rate-exceeded',
|
|
7747
|
-
deviceId: device?.deviceId || '',
|
|
7748
|
-
sessionId: device?.sessionId || '',
|
|
7749
|
-
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7750
|
-
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7751
|
-
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7752
|
-
channel,
|
|
7753
|
-
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7754
|
-
rate: state.messageRateGate.getStatus()
|
|
7755
|
-
});
|
|
7756
|
-
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7757
|
-
socket.destroy();
|
|
7758
|
-
return false;
|
|
7759
|
-
}
|
|
7760
|
-
|
|
7761
7487
|
function handleWebSocketAgentSocket(socket) {
|
|
7762
7488
|
allSockets.add(socket);
|
|
7763
7489
|
socket.setNoDelay(true);
|
|
@@ -7775,7 +7501,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7775
7501
|
binaryIngress: null,
|
|
7776
7502
|
binaryQueueDrops: 0,
|
|
7777
7503
|
binaryQueueEpoch: crypto.randomUUID(),
|
|
7778
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7779
7504
|
closed: false
|
|
7780
7505
|
};
|
|
7781
7506
|
attachAgentBinaryIngress(socket, state);
|
|
@@ -7788,7 +7513,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7788
7513
|
}, 10000);
|
|
7789
7514
|
|
|
7790
7515
|
socket.onTextMessage = text => {
|
|
7791
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7792
7516
|
try {
|
|
7793
7517
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7794
7518
|
} catch (err) {
|
|
@@ -7798,7 +7522,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7798
7522
|
};
|
|
7799
7523
|
|
|
7800
7524
|
socket.onBinaryMessage = payload => {
|
|
7801
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7802
7525
|
try {
|
|
7803
7526
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7804
7527
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
@@ -7957,7 +7680,6 @@ export function createRemoteHub(options = {}) {
|
|
|
7957
7680
|
binaryIngress: null,
|
|
7958
7681
|
binaryQueueDrops: 0,
|
|
7959
7682
|
binaryQueueEpoch: crypto.randomUUID(),
|
|
7960
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7961
7683
|
closed: false
|
|
7962
7684
|
};
|
|
7963
7685
|
attachAgentBinaryIngress(socket, state);
|
|
@@ -8075,15 +7797,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8075
7797
|
|
|
8076
7798
|
try {
|
|
8077
7799
|
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8078
|
-
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8079
|
-
? 'frame'
|
|
8080
|
-
: state.audioOnly
|
|
8081
|
-
? 'audio'
|
|
8082
|
-
: state.inputOnly
|
|
8083
|
-
? 'input'
|
|
8084
|
-
: state.fileOnly ? 'file' : 'control')) {
|
|
8085
|
-
return;
|
|
8086
|
-
}
|
|
8087
7800
|
if (message?.type === 'frame.binary') {
|
|
8088
7801
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
8089
7802
|
const maxBytes = frameKind === 'thumbnail'
|
|
@@ -8139,32 +7852,13 @@ export function createRemoteHub(options = {}) {
|
|
|
8139
7852
|
}
|
|
8140
7853
|
|
|
8141
7854
|
function handleSocket(socket) {
|
|
8142
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8143
|
-
secureDirectAcceptor.accept(socket);
|
|
8144
|
-
return;
|
|
8145
|
-
}
|
|
8146
7855
|
socket.once('data', chunk => {
|
|
8147
7856
|
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8148
|
-
if (isSecureHandshakeStart(firstChunk)) {
|
|
8149
|
-
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8150
|
-
return;
|
|
8151
|
-
}
|
|
8152
7857
|
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8153
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8154
|
-
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8155
|
-
socket.destroy();
|
|
8156
|
-
return;
|
|
8157
|
-
}
|
|
8158
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
8159
7858
|
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8160
7859
|
return;
|
|
8161
7860
|
}
|
|
8162
|
-
|
|
8163
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8164
|
-
socket.destroy();
|
|
8165
|
-
return;
|
|
8166
|
-
}
|
|
8167
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
7861
|
+
|
|
8168
7862
|
handleTcpAgentSocket(socket, firstChunk);
|
|
8169
7863
|
});
|
|
8170
7864
|
|
|
@@ -8188,9 +7882,9 @@ export function createRemoteHub(options = {}) {
|
|
|
8188
7882
|
started = true;
|
|
8189
7883
|
boundPort = candidateServer.address()?.port || port;
|
|
8190
7884
|
lastError = '';
|
|
8191
|
-
logEvent('remote', `LiveDesk Hub client endpoint listening
|
|
7885
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
8192
7886
|
if (host === '0.0.0.0' || host === '::') {
|
|
8193
|
-
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable
|
|
7887
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
|
|
8194
7888
|
}
|
|
8195
7889
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8196
7890
|
resolve();
|
|
@@ -8206,10 +7900,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8206
7900
|
try {
|
|
8207
7901
|
await udpTransport?.start?.();
|
|
8208
7902
|
await listenOnPort(requestedPort);
|
|
8209
|
-
if (!controlIdleSweepTimer) {
|
|
8210
|
-
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8211
|
-
controlIdleSweepTimer.unref?.();
|
|
8212
|
-
}
|
|
8213
7903
|
try {
|
|
8214
7904
|
await relayControl?.start?.();
|
|
8215
7905
|
} catch {
|
|
@@ -8232,10 +7922,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8232
7922
|
}
|
|
8233
7923
|
|
|
8234
7924
|
async function close() {
|
|
8235
|
-
if (controlIdleSweepTimer) {
|
|
8236
|
-
clearInterval(controlIdleSweepTimer);
|
|
8237
|
-
controlIdleSweepTimer = null;
|
|
8238
|
-
}
|
|
8239
7925
|
for (const state of [...agentBinaryIngressStates]) {
|
|
8240
7926
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8241
7927
|
}
|
|
@@ -8442,60 +8128,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8442
8128
|
return { ok: true, commandId };
|
|
8443
8129
|
}
|
|
8444
8130
|
|
|
8445
|
-
async function sendCommandAwaitResult(deviceId, command, options = {}) {
|
|
8446
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
8447
|
-
const device = devices.get(normalizedDeviceId);
|
|
8448
|
-
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8449
|
-
const commandWithId = { ...(command || {}), commandId };
|
|
8450
|
-
|
|
8451
|
-
if (device?.synthetic === true && device.connected) {
|
|
8452
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8453
|
-
return {
|
|
8454
|
-
...sent,
|
|
8455
|
-
queued: sent.ok === true,
|
|
8456
|
-
acknowledged: sent.ok === true,
|
|
8457
|
-
acknowledgement: sent.ok === true
|
|
8458
|
-
? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
|
|
8459
|
-
: { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
|
|
8460
|
-
};
|
|
8461
|
-
}
|
|
8462
|
-
|
|
8463
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8464
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8465
|
-
return {
|
|
8466
|
-
...sent,
|
|
8467
|
-
queued: false,
|
|
8468
|
-
acknowledged: false,
|
|
8469
|
-
acknowledgement: {
|
|
8470
|
-
ok: false,
|
|
8471
|
-
commandId,
|
|
8472
|
-
result: null,
|
|
8473
|
-
error: sent.error || 'device-not-connected'
|
|
8474
|
-
}
|
|
8475
|
-
};
|
|
8476
|
-
}
|
|
8477
|
-
|
|
8478
|
-
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
|
|
8479
|
-
// Register before writing: a loopback Agent can return command.result in
|
|
8480
|
-
// the same event-loop turn as the command write.
|
|
8481
|
-
const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
|
|
8482
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8483
|
-
if (!sent.ok) {
|
|
8484
|
-
failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
|
|
8485
|
-
}
|
|
8486
|
-
const acknowledgement = await acknowledgementPromise;
|
|
8487
|
-
return {
|
|
8488
|
-
...sent,
|
|
8489
|
-
ok: sent.ok === true && acknowledgement.ok === true,
|
|
8490
|
-
queued: sent.ok === true,
|
|
8491
|
-
acknowledged: acknowledgement.ok === true,
|
|
8492
|
-
acknowledgement,
|
|
8493
|
-
error: acknowledgement.ok === true
|
|
8494
|
-
? undefined
|
|
8495
|
-
: acknowledgement.error || sent.error || 'command-result-failed'
|
|
8496
|
-
};
|
|
8497
|
-
}
|
|
8498
|
-
|
|
8499
8131
|
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8500
8132
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8501
8133
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
@@ -8507,31 +8139,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8507
8139
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8508
8140
|
total += 1;
|
|
8509
8141
|
const effectivePolicy = getDevicePolicy(device);
|
|
8510
|
-
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8511
|
-
if (connectionDenied) {
|
|
8512
|
-
const deniedTransport = device.controlTransport || 'direct-tcp';
|
|
8513
|
-
const deniedRemoteAddress = safeString(device.remoteAddress, 256) || 'unknown';
|
|
8514
|
-
logWarn('security', 'Connected device rejected by refreshed policy reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress + ' device=' + device.deviceId);
|
|
8515
|
-
emitEvent('RemoteSecurityPolicyAudit', {
|
|
8516
|
-
result: 'rejected',
|
|
8517
|
-
reason: connectionDenied,
|
|
8518
|
-
deviceId: device.deviceId,
|
|
8519
|
-
sessionId: device.sessionId,
|
|
8520
|
-
transport: device.controlTransport,
|
|
8521
|
-
remoteAddress: device.remoteAddress
|
|
8522
|
-
});
|
|
8523
|
-
disconnectDevice(device.deviceId, connectionDenied);
|
|
8524
|
-
continue;
|
|
8525
|
-
}
|
|
8526
|
-
if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
|
|
8527
|
-
releaseInputOwner(
|
|
8528
|
-
device.deviceId,
|
|
8529
|
-
device.controlOwnerConnectionId,
|
|
8530
|
-
'control-disabled-by-policy');
|
|
8531
|
-
}
|
|
8532
|
-
if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
|
|
8533
|
-
stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
|
|
8534
|
-
}
|
|
8535
8142
|
if (writeJsonLine(device.socket, {
|
|
8536
8143
|
type: 'policy.update',
|
|
8537
8144
|
effectivePolicy,
|
|
@@ -8636,112 +8243,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8636
8243
|
};
|
|
8637
8244
|
}
|
|
8638
8245
|
|
|
8639
|
-
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8640
|
-
const policy = getDevicePolicy(device);
|
|
8641
|
-
return {
|
|
8642
|
-
type,
|
|
8643
|
-
payload: {
|
|
8644
|
-
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8645
|
-
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8646
|
-
deviceId: safeString(device?.deviceId, 160),
|
|
8647
|
-
visibleIndicator: true,
|
|
8648
|
-
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8649
|
-
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8650
|
-
reason: safeString(reason, 160),
|
|
8651
|
-
issuedAt: new Date().toISOString()
|
|
8652
|
-
}
|
|
8653
|
-
};
|
|
8654
|
-
}
|
|
8655
|
-
|
|
8656
|
-
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8657
|
-
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8658
|
-
if (inputWriteOwner && bindingKey) {
|
|
8659
|
-
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8660
|
-
}
|
|
8661
|
-
if (!device?.socket || device.socket.destroyed) {
|
|
8662
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8663
|
-
}
|
|
8664
|
-
const sent = writeJsonLine(device.socket, {
|
|
8665
|
-
type: 'command',
|
|
8666
|
-
commandId: crypto.randomUUID(),
|
|
8667
|
-
command: type,
|
|
8668
|
-
payload: message.payload,
|
|
8669
|
-
issuedAt: new Date().toISOString()
|
|
8670
|
-
});
|
|
8671
|
-
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8672
|
-
}
|
|
8673
|
-
|
|
8674
|
-
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8675
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8676
|
-
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8677
|
-
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8678
|
-
return { ok: true, alreadyActive: true };
|
|
8679
|
-
}
|
|
8680
|
-
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8681
|
-
device.controlOwnerConnectionId = owner;
|
|
8682
|
-
device.controlOwnerStartedAtMs = Date.now();
|
|
8683
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8684
|
-
const sent = sendControlSessionMessage(
|
|
8685
|
-
device,
|
|
8686
|
-
owner,
|
|
8687
|
-
'control.session.start',
|
|
8688
|
-
'browser-control-owner-active',
|
|
8689
|
-
inputWriteOwner,
|
|
8690
|
-
bindingKey);
|
|
8691
|
-
if (!sent.ok) {
|
|
8692
|
-
device.controlOwnerSessionId = '';
|
|
8693
|
-
device.controlOwnerConnectionId = '';
|
|
8694
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8695
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8696
|
-
return sent;
|
|
8697
|
-
}
|
|
8698
|
-
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8699
|
-
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8700
|
-
hubConnectionId: owner
|
|
8701
|
-
});
|
|
8702
|
-
return sent;
|
|
8703
|
-
}
|
|
8704
|
-
|
|
8705
|
-
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8706
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8707
|
-
if (!device?.controlOwnerSessionId
|
|
8708
|
-
|| !owner
|
|
8709
|
-
|| device.controlOwnerConnectionId !== owner) {
|
|
8710
|
-
return { ok: false, error: 'control-owner-not-current' };
|
|
8711
|
-
}
|
|
8712
|
-
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8713
|
-
const sent = sendControlSessionMessage(
|
|
8714
|
-
device,
|
|
8715
|
-
owner,
|
|
8716
|
-
'control.session.stop',
|
|
8717
|
-
reason,
|
|
8718
|
-
inputWriteOwner,
|
|
8719
|
-
bindingKey);
|
|
8720
|
-
device.controlOwnerSessionId = '';
|
|
8721
|
-
device.controlOwnerConnectionId = '';
|
|
8722
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8723
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8724
|
-
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8725
|
-
controlOwnerSessionId,
|
|
8726
|
-
hubConnectionId: owner,
|
|
8727
|
-
reason,
|
|
8728
|
-
delivered: sent.ok === true
|
|
8729
|
-
});
|
|
8730
|
-
return sent;
|
|
8731
|
-
}
|
|
8732
|
-
|
|
8733
|
-
function sweepIdleControlSessions() {
|
|
8734
|
-
const nowMs = Date.now();
|
|
8735
|
-
for (const device of devices.values()) {
|
|
8736
|
-
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8737
|
-
const policy = getDevicePolicy(device);
|
|
8738
|
-
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8739
|
-
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8740
|
-
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8741
|
-
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8742
|
-
}
|
|
8743
|
-
}
|
|
8744
|
-
|
|
8745
8246
|
function getCurrentInputWriteOwner(device) {
|
|
8746
8247
|
if (!device?.inputSocket
|
|
8747
8248
|
|| device.inputSocket.destroyed
|
|
@@ -8889,9 +8390,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8889
8390
|
const bindingChanged = previousInputBindingKey
|
|
8890
8391
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8891
8392
|
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8892
|
-
const startingControlOwner = !!normalized.hubConnectionId
|
|
8893
|
-
&& (!device.controlOwnerConnectionId
|
|
8894
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8895
8393
|
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8896
8394
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8897
8395
|
activeInputBindingKey,
|
|
@@ -8909,25 +8407,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8909
8407
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
8910
8408
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8911
8409
|
}
|
|
8912
|
-
if (startingControlOwner) {
|
|
8913
|
-
if (device.controlOwnerConnectionId) {
|
|
8914
|
-
endControlSession(
|
|
8915
|
-
device,
|
|
8916
|
-
device.controlOwnerConnectionId,
|
|
8917
|
-
'browser-input-owner-changed',
|
|
8918
|
-
inputWriteOwner,
|
|
8919
|
-
activeInputBindingKey);
|
|
8920
|
-
}
|
|
8921
|
-
const started = beginControlSession(
|
|
8922
|
-
device,
|
|
8923
|
-
normalized.hubConnectionId,
|
|
8924
|
-
inputWriteOwner,
|
|
8925
|
-
activeInputBindingKey);
|
|
8926
|
-
if (!started.ok) {
|
|
8927
|
-
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8928
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8929
|
-
}
|
|
8930
|
-
}
|
|
8931
8410
|
}
|
|
8932
8411
|
const sent = inputWriteOwner.enqueue({
|
|
8933
8412
|
type: 'input.control',
|
|
@@ -8942,7 +8421,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8942
8421
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8943
8422
|
}
|
|
8944
8423
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8945
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8946
8424
|
device.counters.commandsSent += 1;
|
|
8947
8425
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8948
8426
|
return {
|
|
@@ -8972,25 +8450,6 @@ export function createRemoteHub(options = {}) {
|
|
|
8972
8450
|
}
|
|
8973
8451
|
|
|
8974
8452
|
const hubForwardedAtEpochMs = Date.now();
|
|
8975
|
-
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8976
|
-
&& (!device.controlOwnerConnectionId
|
|
8977
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8978
|
-
if (startingFallbackOwner) {
|
|
8979
|
-
if (device.controlOwnerConnectionId) {
|
|
8980
|
-
endControlSession(
|
|
8981
|
-
device,
|
|
8982
|
-
device.controlOwnerConnectionId,
|
|
8983
|
-
'browser-input-owner-changed',
|
|
8984
|
-
null,
|
|
8985
|
-
'');
|
|
8986
|
-
}
|
|
8987
|
-
const started = beginControlSession(
|
|
8988
|
-
device,
|
|
8989
|
-
normalized.hubConnectionId,
|
|
8990
|
-
null,
|
|
8991
|
-
'');
|
|
8992
|
-
if (!started.ok) return started;
|
|
8993
|
-
}
|
|
8994
8453
|
const fallback = sendCommand(deviceId, {
|
|
8995
8454
|
command: 'input.control',
|
|
8996
8455
|
payload: {
|
|
@@ -9014,22 +8473,6 @@ export function createRemoteHub(options = {}) {
|
|
|
9014
8473
|
timeoutTimer: null
|
|
9015
8474
|
});
|
|
9016
8475
|
}
|
|
9017
|
-
if (fallback.ok) {
|
|
9018
|
-
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
9019
|
-
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
9020
|
-
device,
|
|
9021
|
-
controlStream,
|
|
9022
|
-
normalized,
|
|
9023
|
-
activeMonitorIndex);
|
|
9024
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
9025
|
-
} else if (startingFallbackOwner) {
|
|
9026
|
-
endControlSession(
|
|
9027
|
-
device,
|
|
9028
|
-
normalized.hubConnectionId,
|
|
9029
|
-
'input-control-delivery-failed',
|
|
9030
|
-
null,
|
|
9031
|
-
'');
|
|
9032
|
-
}
|
|
9033
8476
|
return fallback.ok
|
|
9034
8477
|
? {
|
|
9035
8478
|
...fallback,
|
|
@@ -9056,17 +8499,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9056
8499
|
return { ok: false, error: 'input-owner-not-current' };
|
|
9057
8500
|
}
|
|
9058
8501
|
|
|
8502
|
+
device.inputOwnerConnectionId = '';
|
|
8503
|
+
device.inputOwnerBindingKey = '';
|
|
9059
8504
|
const inputSocket = device.inputSocket;
|
|
9060
8505
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9061
8506
|
if (!inputSocket || !inputWriteOwner) {
|
|
9062
|
-
|
|
9063
|
-
device.inputOwnerConnectionId = '';
|
|
9064
|
-
device.inputOwnerBindingKey = '';
|
|
9065
|
-
return {
|
|
9066
|
-
ok: ended.ok !== false,
|
|
9067
|
-
queued: false,
|
|
9068
|
-
controlSessionEnded: ended.ok === true
|
|
9069
|
-
};
|
|
8507
|
+
return { ok: true, queued: false };
|
|
9070
8508
|
}
|
|
9071
8509
|
const controlStream = getActiveControlStream(device);
|
|
9072
8510
|
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
@@ -9082,19 +8520,10 @@ export function createRemoteHub(options = {}) {
|
|
|
9082
8520
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
9083
8521
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
9084
8522
|
}
|
|
9085
|
-
const ended = endControlSession(
|
|
9086
|
-
device,
|
|
9087
|
-
owner,
|
|
9088
|
-
reason,
|
|
9089
|
-
inputWriteOwner,
|
|
9090
|
-
releaseBindingKey);
|
|
9091
|
-
device.inputOwnerConnectionId = '';
|
|
9092
|
-
device.inputOwnerBindingKey = '';
|
|
9093
8523
|
return {
|
|
9094
|
-
ok:
|
|
8524
|
+
ok: true,
|
|
9095
8525
|
queued: true,
|
|
9096
|
-
backpressured: sent.backpressured === true
|
|
9097
|
-
controlSessionEnded: ended.ok === true
|
|
8526
|
+
backpressured: sent.backpressured === true
|
|
9098
8527
|
};
|
|
9099
8528
|
}
|
|
9100
8529
|
|
|
@@ -9137,9 +8566,6 @@ export function createRemoteHub(options = {}) {
|
|
|
9137
8566
|
}
|
|
9138
8567
|
const device = devices.get(String(deviceId || ''));
|
|
9139
8568
|
const requestedOperation = safeString(options.operation, 80);
|
|
9140
|
-
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9141
|
-
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9142
|
-
}
|
|
9143
8569
|
const operation = normalizeAgentOperation(requestedOperation);
|
|
9144
8570
|
if (requestedOperation && !operation) {
|
|
9145
8571
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
@@ -11388,7 +10814,6 @@ export function createRemoteHub(options = {}) {
|
|
|
11388
10814
|
disconnectDevice,
|
|
11389
10815
|
assignDeviceSlot,
|
|
11390
10816
|
sendCommand,
|
|
11391
|
-
sendCommandAwaitResult,
|
|
11392
10817
|
refreshDevicePolicies,
|
|
11393
10818
|
sendLegacyClientUpdate,
|
|
11394
10819
|
sendInputControl,
|
|
@@ -11417,16 +10842,7 @@ export function createRemoteHub(options = {}) {
|
|
|
11417
10842
|
seedSyntheticFleet,
|
|
11418
10843
|
clearSyntheticFleet,
|
|
11419
10844
|
getPairToken: () => pairToken,
|
|
11420
|
-
getPairingPin: () => pairingPin
|
|
11421
|
-
getSecurityStatus: () => ({
|
|
11422
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
11423
|
-
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11424
|
-
hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
|
|
11425
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
11426
|
-
devices: deviceCredentialAuthority.listDevices()
|
|
11427
|
-
}),
|
|
11428
|
-
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11429
|
-
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
10845
|
+
getPairingPin: () => pairingPin
|
|
11430
10846
|
};
|
|
11431
10847
|
}
|
|
11432
10848
|
|