@livedesk/hub 0.1.32 → 0.1.34
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 +32 -32
- package/src/remote-hub.js +782 -728
- package/src/server.js +1058 -980
- package/src/settings/settings-schema.js +251 -239
- package/src/settings/settings-store.js +82 -77
- package/src/transport/secure-direct-acceptor.js +440 -433
package/src/remote-hub.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import { isSecureHandshakeStart } from '@livedesk/runtime-core';
|
|
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';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { isSecureHandshakeStart } from '@livedesk/runtime-core';
|
|
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';
|
|
8
8
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
9
9
|
import {
|
|
10
10
|
BoundedSegmentedBuffer,
|
|
@@ -35,10 +35,10 @@ const MAX_AGENT_TCP_BINARY_PACKET_BYTES = TCP_BINARY_FRAME_HEADER_BYTES
|
|
|
35
35
|
const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
|
|
36
36
|
+ MAX_AGENT_BINARY_META_BYTES
|
|
37
37
|
+ MAX_AGENT_BINARY_PAYLOAD_BYTES;
|
|
38
|
-
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
|
-
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
|
-
+ (256 * 1024);
|
|
41
|
-
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
38
|
+
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
|
+
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
|
+
+ (256 * 1024);
|
|
41
|
+
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
42
42
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
43
43
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
44
44
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
@@ -47,7 +47,7 @@ const RECENT_TASK_LIMIT = 12;
|
|
|
47
47
|
const RECENT_TASK_BATCH_LIMIT = 16;
|
|
48
48
|
const RECENT_FRAME_CACHE_TTL_MS = 4000;
|
|
49
49
|
const RECENT_THUMBNAIL_FRAME_CACHE_LIMIT = 2;
|
|
50
|
-
const RECENT_LIVE_FRAME_CACHE_LIMIT =
|
|
50
|
+
const RECENT_LIVE_FRAME_CACHE_LIMIT = 2;
|
|
51
51
|
const RECENT_THUMBNAIL_FRAME_CACHE_MAX_BYTES = 2 * 1024 * 1024;
|
|
52
52
|
const RECENT_LIVE_FRAME_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
53
53
|
const LIVE_STREAM_PENDING_REUSE_MS = 5000;
|
|
@@ -110,7 +110,7 @@ const MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS = Object.freeze([
|
|
|
110
110
|
'handoffOldestFrameAgeMs'
|
|
111
111
|
]);
|
|
112
112
|
|
|
113
|
-
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
113
|
+
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
114
114
|
return Number(frame?.nativeCaptureTelemetryVersion) >= MAC_NATIVE_RESOURCE_TELEMETRY_VERSION
|
|
115
115
|
&& MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS.every(field =>
|
|
116
116
|
frame?.[field] !== null
|
|
@@ -828,61 +828,61 @@ function isPrivateIPv4(value) {
|
|
|
828
828
|
|| (parts[0] === 192 && parts[1] === 168);
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
-
function isPrivateNetworkAddress(value) {
|
|
831
|
+
function isPrivateNetworkAddress(value) {
|
|
832
832
|
const address = String(value || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
833
833
|
return isLoopbackHost(address)
|
|
834
834
|
|| isPrivateIPv4(address)
|
|
835
835
|
|| address.startsWith('fe80:')
|
|
836
836
|
|| address.startsWith('fc')
|
|
837
|
-
|| address.startsWith('fd');
|
|
838
|
-
}
|
|
839
|
-
|
|
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
|
-
}
|
|
837
|
+
|| address.startsWith('fd');
|
|
838
|
+
}
|
|
839
|
+
|
|
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
886
|
|
|
887
887
|
function readCpuTimes() {
|
|
888
888
|
let idle = 0;
|
|
@@ -1206,37 +1206,37 @@ function getRetiredAgentTaskError(options = {}) {
|
|
|
1206
1206
|
: '';
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
|
-
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
1210
|
-
'system.health',
|
|
1211
|
-
'gpu.status',
|
|
1212
|
-
'disk.status',
|
|
1213
|
-
'process.list',
|
|
1214
|
-
'service.status',
|
|
1215
|
-
'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
|
-
'process.control',
|
|
1224
|
-
'service.control',
|
|
1225
|
-
'application.launch',
|
|
1226
|
-
'application.close',
|
|
1227
|
-
'file.write',
|
|
1228
|
-
'file.delete',
|
|
1229
|
-
'command.run',
|
|
1230
|
-
'script.run',
|
|
1231
|
-
'software.install',
|
|
1232
|
-
'system.power',
|
|
1233
|
-
'system.configure'
|
|
1234
|
-
]);
|
|
1235
|
-
|
|
1236
|
-
function normalizeAgentOperation(value) {
|
|
1237
|
-
const operation = safeString(value, 80).toLowerCase();
|
|
1238
|
-
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
1239
|
-
}
|
|
1209
|
+
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
1210
|
+
'system.health',
|
|
1211
|
+
'gpu.status',
|
|
1212
|
+
'disk.status',
|
|
1213
|
+
'process.list',
|
|
1214
|
+
'service.status',
|
|
1215
|
+
'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
|
+
'process.control',
|
|
1224
|
+
'service.control',
|
|
1225
|
+
'application.launch',
|
|
1226
|
+
'application.close',
|
|
1227
|
+
'file.write',
|
|
1228
|
+
'file.delete',
|
|
1229
|
+
'command.run',
|
|
1230
|
+
'script.run',
|
|
1231
|
+
'software.install',
|
|
1232
|
+
'system.power',
|
|
1233
|
+
'system.configure'
|
|
1234
|
+
]);
|
|
1235
|
+
|
|
1236
|
+
function normalizeAgentOperation(value) {
|
|
1237
|
+
const operation = safeString(value, 80).toLowerCase();
|
|
1238
|
+
return SUPPORTED_AGENT_OPERATIONS.has(operation) ? operation : '';
|
|
1239
|
+
}
|
|
1240
1240
|
|
|
1241
1241
|
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
1242
1242
|
'monitorIndex',
|
|
@@ -1402,7 +1402,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
|
|
|
1402
1402
|
return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
|
-
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1405
|
+
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1406
1406
|
if (!frame) {
|
|
1407
1407
|
return null;
|
|
1408
1408
|
}
|
|
@@ -1424,27 +1424,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
|
1424
1424
|
serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
|
|
1425
1425
|
}
|
|
1426
1426
|
|
|
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. Keep its internal frame
|
|
1438
|
-
// owner intact for immediate delivery, but never duplicate encoded
|
|
1439
|
-
// bytes or its bearer token into React/status snapshots.
|
|
1440
|
-
latestFrame: serializeRemoteFrame(
|
|
1441
|
-
activeLiveStream.latestFrame,
|
|
1442
|
-
deviceId,
|
|
1443
|
-
'live',
|
|
1444
|
-
{ includeDataUrl: false }
|
|
1445
|
-
)
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
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. Keep its internal frame
|
|
1438
|
+
// owner intact for immediate delivery, but never duplicate encoded
|
|
1439
|
+
// bytes or its bearer token into React/status snapshots.
|
|
1440
|
+
latestFrame: serializeRemoteFrame(
|
|
1441
|
+
activeLiveStream.latestFrame,
|
|
1442
|
+
deviceId,
|
|
1443
|
+
'live',
|
|
1444
|
+
{ includeDataUrl: false }
|
|
1445
|
+
)
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
1448
|
|
|
1449
1449
|
function getRecentFrameCache(device, frameKind) {
|
|
1450
1450
|
if (!device) {
|
|
@@ -1478,7 +1478,7 @@ function getRecentFrameCacheMaxBytes(frameKind) {
|
|
|
1478
1478
|
: RECENT_LIVE_FRAME_CACHE_MAX_BYTES;
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
1481
|
+
export function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
1482
1482
|
if (!Array.isArray(cache)) {
|
|
1483
1483
|
return;
|
|
1484
1484
|
}
|
|
@@ -1499,6 +1499,20 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1499
1499
|
}
|
|
1500
1500
|
}
|
|
1501
1501
|
|
|
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
|
+
|
|
1502
1516
|
for (let index = 0; index < cache.length; index += 1) {
|
|
1503
1517
|
const entry = cache[index];
|
|
1504
1518
|
const byteLength = Number(entry?.byteLength || entry?.frame?.payload?.length || 0) || 0;
|
|
@@ -1510,7 +1524,7 @@ function pruneRecentFrameCache(cache, frameKind, nowMs = Date.now()) {
|
|
|
1510
1524
|
}
|
|
1511
1525
|
}
|
|
1512
1526
|
|
|
1513
|
-
function rememberRecentFramePayload(device, frameKind, frame) {
|
|
1527
|
+
export function rememberRecentFramePayload(device, frameKind, frame) {
|
|
1514
1528
|
if (!device || !frame || !Buffer.isBuffer(frame.payload)) {
|
|
1515
1529
|
return;
|
|
1516
1530
|
}
|
|
@@ -1546,7 +1560,7 @@ function rememberRecentFramePayload(device, frameKind, frame) {
|
|
|
1546
1560
|
pruneRecentFrameCache(cache, kind, nowMs);
|
|
1547
1561
|
}
|
|
1548
1562
|
|
|
1549
|
-
function isFramePayloadRequestMatch(frame, options = {}) {
|
|
1563
|
+
export function isFramePayloadRequestMatch(frame, options = {}) {
|
|
1550
1564
|
if (!frame || !Buffer.isBuffer(frame.payload)) {
|
|
1551
1565
|
return false;
|
|
1552
1566
|
}
|
|
@@ -1565,10 +1579,37 @@ function isFramePayloadRequestMatch(frame, options = {}) {
|
|
|
1565
1579
|
return false;
|
|
1566
1580
|
}
|
|
1567
1581
|
|
|
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
|
+
|
|
1568
1609
|
return true;
|
|
1569
1610
|
}
|
|
1570
1611
|
|
|
1571
|
-
function findRecentFramePayload(device, frameKind, options = {}) {
|
|
1612
|
+
export function findRecentFramePayload(device, frameKind, options = {}) {
|
|
1572
1613
|
const kind = frameKind === 'thumbnail' ? 'thumbnail' : 'live';
|
|
1573
1614
|
const latest = kind === 'thumbnail'
|
|
1574
1615
|
? device?.latestThumbnail
|
|
@@ -1579,7 +1620,14 @@ function findRecentFramePayload(device, frameKind, options = {}) {
|
|
|
1579
1620
|
|
|
1580
1621
|
const token = safeString(options.token, 128);
|
|
1581
1622
|
const requestedSeq = Number(options.frameSeq);
|
|
1582
|
-
|
|
1623
|
+
const ownerQuery = options.requireKeyFrame === true
|
|
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) {
|
|
1583
1631
|
return null;
|
|
1584
1632
|
}
|
|
1585
1633
|
|
|
@@ -1635,7 +1683,7 @@ function serializeDevice(device, options = {}) {
|
|
|
1635
1683
|
},
|
|
1636
1684
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1637
1685
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1638
|
-
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1686
|
+
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1639
1687
|
liveCapturePause: device.liveCapturePause
|
|
1640
1688
|
? {
|
|
1641
1689
|
token: device.liveCapturePause.token,
|
|
@@ -2242,7 +2290,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2242
2290
|
: null;
|
|
2243
2291
|
const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
|
|
2244
2292
|
|
|
2245
|
-
function getDevicePolicy(device) {
|
|
2293
|
+
function getDevicePolicy(device) {
|
|
2246
2294
|
try {
|
|
2247
2295
|
return getEffectiveDevicePolicy({
|
|
2248
2296
|
deviceId: device?.deviceId || '',
|
|
@@ -2250,18 +2298,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2250
2298
|
}) || {};
|
|
2251
2299
|
} catch {
|
|
2252
2300
|
return {};
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
function connectionPolicyError(socket, policy = {}) {
|
|
2257
|
-
return evaluateRemoteConnectionPolicy({
|
|
2258
|
-
remoteAddress: socket?.remoteAddress || '',
|
|
2259
|
-
relayConnection: socket?.__liveDeskRelayControl === true,
|
|
2260
|
-
encrypted: socket?.__liveDeskSecurityContext?.encrypted === true,
|
|
2261
|
-
allowLegacyPlaintextForTests,
|
|
2262
|
-
policy
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
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
|
+
}
|
|
2265
2313
|
|
|
2266
2314
|
function policyError(device, permission = '', command = '') {
|
|
2267
2315
|
const policy = getDevicePolicy(device);
|
|
@@ -2311,83 +2359,83 @@ export function createRemoteHub(options = {}) {
|
|
|
2311
2359
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
2312
2360
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
2313
2361
|
const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
|
|
2314
|
-
const configuredEnrollmentToken = safeString(
|
|
2315
|
-
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
|
|
2316
|
-
256);
|
|
2317
|
-
const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
|
|
2318
|
-
|| isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
|
|
2319
|
-
|| env.LIVEDESK_TEST_MODE === '1';
|
|
2320
|
-
const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
|
|
2321
|
-
&& Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
|
|
2322
|
-
&& Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
|
|
2323
|
-
? configuredEnrollmentToken
|
|
2324
|
-
: '';
|
|
2325
|
-
let pairToken = allowLegacyPlaintextForTests && configuredEnrollmentToken
|
|
2326
|
-
? configuredEnrollmentToken
|
|
2327
|
-
: canonicalEnrollmentToken || crypto.randomBytes(32).toString('base64url');
|
|
2328
|
-
const deviceCredentialAuthority = options.deviceCredentialAuthority || createDeviceCredentialAuthority({
|
|
2329
|
-
dataDir: options.dataDir || env.LIVEDESK_DATA_DIR || undefined
|
|
2330
|
-
});
|
|
2331
|
-
const getSecurityIdentity = typeof options.getSecurityIdentity === 'function'
|
|
2332
|
-
? options.getSecurityIdentity
|
|
2333
|
-
: () => ({ accountId: safeString(options.accountId || env.LIVEDESK_ACCOUNT_ID, 128) });
|
|
2334
|
-
udpTransport?.setRendezvousProofIssuer?.(({ roomId, deviceId, role, ttlMs }) => {
|
|
2335
|
-
const identity = getSecurityIdentity() || {};
|
|
2336
|
-
return deviceCredentialAuthority.issueRendezvousProof({
|
|
2337
|
-
roomId,
|
|
2338
|
-
deviceId,
|
|
2339
|
-
role,
|
|
2340
|
-
ttlMs,
|
|
2341
|
-
accountId: safeString(identity.accountId, 128)
|
|
2342
|
-
});
|
|
2343
|
-
});
|
|
2344
|
-
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2362
|
+
const configuredEnrollmentToken = safeString(
|
|
2363
|
+
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN,
|
|
2364
|
+
256);
|
|
2365
|
+
const allowLegacyPlaintextForTests = options.allowLegacyPlaintextForTests === true
|
|
2366
|
+
|| isEnabledValue(env.LIVEDESK_TEST_ALLOW_LEGACY_PLAINTEXT, false)
|
|
2367
|
+
|| env.LIVEDESK_TEST_MODE === '1';
|
|
2368
|
+
const canonicalEnrollmentToken = /^[A-Za-z0-9_-]{43}$/.test(configuredEnrollmentToken)
|
|
2369
|
+
&& Buffer.from(configuredEnrollmentToken, 'base64url').length === 32
|
|
2370
|
+
&& Buffer.from(configuredEnrollmentToken, 'base64url').toString('base64url') === configuredEnrollmentToken
|
|
2371
|
+
? configuredEnrollmentToken
|
|
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)
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2345
2393
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2346
2394
|
: generatePairingPin();
|
|
2347
|
-
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2348
|
-
const consumeEnrollmentToken = expected => {
|
|
2349
|
-
if (!timingSafeStringEqual(expected, pairToken)) return false;
|
|
2350
|
-
pairToken = crypto.randomBytes(32).toString('base64url');
|
|
2351
|
-
pairingPin = generatePairingPin();
|
|
2352
|
-
pairingPinUpdatedAt = new Date().toISOString();
|
|
2353
|
-
emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
|
|
2354
|
-
pairingPinUpdatedAt,
|
|
2355
|
-
hubId: deviceCredentialAuthority.hubId
|
|
2356
|
-
});
|
|
2357
|
-
return true;
|
|
2358
|
-
};
|
|
2359
|
-
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2360
|
-
? options.relayControl
|
|
2361
|
-
: allowLegacyPlaintextForTests
|
|
2362
|
-
? createHubRelayControl({
|
|
2363
|
-
env,
|
|
2364
|
-
pairToken,
|
|
2365
|
-
logEvent,
|
|
2366
|
-
logWarn,
|
|
2367
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2368
|
-
})
|
|
2369
|
-
: createSecureHubRelayControl({
|
|
2370
|
-
env,
|
|
2371
|
-
authority: deviceCredentialAuthority,
|
|
2372
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2373
|
-
getEnrollmentToken: () => pairToken,
|
|
2374
|
-
consumeEnrollmentToken,
|
|
2375
|
-
logEvent,
|
|
2376
|
-
logWarn,
|
|
2377
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2378
|
-
});
|
|
2379
|
-
const secureDirectAcceptor = createSecureDirectAcceptor({
|
|
2380
|
-
authority: deviceCredentialAuthority,
|
|
2381
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2382
|
-
getEnrollmentToken: () => pairToken,
|
|
2383
|
-
consumeEnrollmentToken,
|
|
2384
|
-
onSecureSocket: socket => handleTcpAgentSocket(socket),
|
|
2385
|
-
onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
|
|
2386
|
-
...audit,
|
|
2387
|
-
remoteHub: getStatus({ includeSecrets: false })
|
|
2388
|
-
}),
|
|
2389
|
-
logWarn
|
|
2390
|
-
});
|
|
2395
|
+
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
|
+
});
|
|
2391
2439
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2392
2440
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2393
2441
|
5000,
|
|
@@ -2412,8 +2460,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2412
2460
|
const transportDiagnostics = new Map();
|
|
2413
2461
|
const transportDiagnosticStartingDevices = new Set();
|
|
2414
2462
|
const duplicateDeviceLogAt = new Map();
|
|
2415
|
-
let server = null;
|
|
2416
|
-
let controlIdleSweepTimer = null;
|
|
2463
|
+
let server = null;
|
|
2464
|
+
let controlIdleSweepTimer = null;
|
|
2417
2465
|
let started = false;
|
|
2418
2466
|
let boundPort = requestedPort;
|
|
2419
2467
|
let lastError = '';
|
|
@@ -2735,8 +2783,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2735
2783
|
host,
|
|
2736
2784
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2737
2785
|
port: boundPort || requestedPort,
|
|
2738
|
-
protocol: 'secure-record-v1',
|
|
2739
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2786
|
+
protocol: 'secure-record-v1',
|
|
2787
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2740
2788
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2741
2789
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2742
2790
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2752,18 +2800,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2752
2800
|
agentEndpointCandidates: routeInfo.candidates,
|
|
2753
2801
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
2754
2802
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
2755
|
-
pairTokenPreview: maskToken(pairToken),
|
|
2756
|
-
pairingPin: includeSecrets ? pairingPin : '',
|
|
2757
|
-
pairingPinUpdatedAt,
|
|
2758
|
-
security: {
|
|
2759
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
2760
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
2761
|
-
hubPublicKeyFingerprint: crypto.createHash('sha256')
|
|
2762
|
-
.update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
|
|
2763
|
-
.digest('hex'),
|
|
2764
|
-
enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
|
|
2765
|
-
legacyPlaintextForTests: allowLegacyPlaintextForTests
|
|
2766
|
-
},
|
|
2803
|
+
pairTokenPreview: maskToken(pairToken),
|
|
2804
|
+
pairingPin: includeSecrets ? pairingPin : '',
|
|
2805
|
+
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
|
+
},
|
|
2767
2815
|
deviceCount: devices.size,
|
|
2768
2816
|
connectedDeviceCount: connectedDevices,
|
|
2769
2817
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -4023,22 +4071,22 @@ export function createRemoteHub(options = {}) {
|
|
|
4023
4071
|
}
|
|
4024
4072
|
}
|
|
4025
4073
|
|
|
4026
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4074
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4027
4075
|
const socket = device?.inputSocket;
|
|
4028
4076
|
if (!socket) {
|
|
4029
4077
|
return;
|
|
4030
4078
|
}
|
|
4031
|
-
|
|
4032
|
-
const writeOwner = device.inputWriteOwner;
|
|
4033
|
-
if (device.controlOwnerConnectionId) {
|
|
4034
|
-
endControlSession(
|
|
4035
|
-
device,
|
|
4036
|
-
device.controlOwnerConnectionId,
|
|
4037
|
-
reason,
|
|
4038
|
-
writeOwner,
|
|
4039
|
-
writeOwner?.getBindingKey?.() || '');
|
|
4040
|
-
}
|
|
4041
|
-
device.inputSocket = null;
|
|
4079
|
+
|
|
4080
|
+
const writeOwner = device.inputWriteOwner;
|
|
4081
|
+
if (device.controlOwnerConnectionId) {
|
|
4082
|
+
endControlSession(
|
|
4083
|
+
device,
|
|
4084
|
+
device.controlOwnerConnectionId,
|
|
4085
|
+
reason,
|
|
4086
|
+
writeOwner,
|
|
4087
|
+
writeOwner?.getBindingKey?.() || '');
|
|
4088
|
+
}
|
|
4089
|
+
device.inputSocket = null;
|
|
4042
4090
|
device.inputWriteOwner = null;
|
|
4043
4091
|
device.inputSocketConnectionId = '';
|
|
4044
4092
|
device.inputOwnerConnectionId = '';
|
|
@@ -4068,17 +4116,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4068
4116
|
if (!device || device.inputSocket !== socket) {
|
|
4069
4117
|
return;
|
|
4070
4118
|
}
|
|
4071
|
-
|
|
4072
|
-
const writeOwner = device.inputWriteOwner;
|
|
4073
|
-
if (device.controlOwnerConnectionId) {
|
|
4074
|
-
endControlSession(
|
|
4075
|
-
device,
|
|
4076
|
-
device.controlOwnerConnectionId,
|
|
4077
|
-
reason,
|
|
4078
|
-
null,
|
|
4079
|
-
'');
|
|
4080
|
-
}
|
|
4081
|
-
device.inputSocket = null;
|
|
4119
|
+
|
|
4120
|
+
const writeOwner = device.inputWriteOwner;
|
|
4121
|
+
if (device.controlOwnerConnectionId) {
|
|
4122
|
+
endControlSession(
|
|
4123
|
+
device,
|
|
4124
|
+
device.controlOwnerConnectionId,
|
|
4125
|
+
reason,
|
|
4126
|
+
null,
|
|
4127
|
+
'');
|
|
4128
|
+
}
|
|
4129
|
+
device.inputSocket = null;
|
|
4082
4130
|
device.inputWriteOwner = null;
|
|
4083
4131
|
device.inputSocketConnectionId = '';
|
|
4084
4132
|
device.inputOwnerConnectionId = '';
|
|
@@ -4510,11 +4558,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4510
4558
|
inputWriteOwner: null,
|
|
4511
4559
|
inputSocketConnectionId: '',
|
|
4512
4560
|
inputOwnerConnectionId: '',
|
|
4513
|
-
inputOwnerBindingKey: '',
|
|
4514
|
-
controlOwnerSessionId: '',
|
|
4515
|
-
controlOwnerConnectionId: '',
|
|
4516
|
-
controlOwnerStartedAtMs: 0,
|
|
4517
|
-
controlOwnerLastInputAtMs: 0,
|
|
4561
|
+
inputOwnerBindingKey: '',
|
|
4562
|
+
controlOwnerSessionId: '',
|
|
4563
|
+
controlOwnerConnectionId: '',
|
|
4564
|
+
controlOwnerStartedAtMs: 0,
|
|
4565
|
+
controlOwnerLastInputAtMs: 0,
|
|
4518
4566
|
frameSocket: null,
|
|
4519
4567
|
audioSocket: null,
|
|
4520
4568
|
fileSocket: null,
|
|
@@ -7435,14 +7483,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7435
7483
|
return;
|
|
7436
7484
|
}
|
|
7437
7485
|
|
|
7438
|
-
if (!state.authenticated) {
|
|
7439
|
-
const securityContext = socket.__liveDeskSecurityContext;
|
|
7440
|
-
const secureDeviceAuthenticated = securityContext?.authenticated === true
|
|
7441
|
-
&& securityContext?.encrypted === true
|
|
7442
|
-
&& safeString(securityContext.accountId, 128)
|
|
7443
|
-
&& safeString(securityContext.hubId, 128)
|
|
7444
|
-
&& safeString(securityContext.deviceId, 128);
|
|
7445
|
-
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
7486
|
+
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
|
+
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
7446
7494
|
message = {
|
|
7447
7495
|
...message,
|
|
7448
7496
|
capabilities: {
|
|
@@ -7452,14 +7500,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7452
7500
|
dedicatedInputChannel: false
|
|
7453
7501
|
}
|
|
7454
7502
|
};
|
|
7455
|
-
}
|
|
7456
|
-
if (message.type === 'slot.assign') {
|
|
7457
|
-
if (!secureDeviceAuthenticated
|
|
7458
|
-
|| safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7459
|
-
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
|
|
7460
|
-
socket.end();
|
|
7461
|
-
return;
|
|
7462
|
-
}
|
|
7503
|
+
}
|
|
7504
|
+
if (message.type === 'slot.assign') {
|
|
7505
|
+
if (!secureDeviceAuthenticated
|
|
7506
|
+
|| safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7507
|
+
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
|
|
7508
|
+
socket.end();
|
|
7509
|
+
return;
|
|
7510
|
+
}
|
|
7463
7511
|
const result = assignDeviceSlot(
|
|
7464
7512
|
message.deviceId || message.DeviceId,
|
|
7465
7513
|
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
@@ -7473,54 +7521,57 @@ export function createRemoteHub(options = {}) {
|
|
|
7473
7521
|
return;
|
|
7474
7522
|
}
|
|
7475
7523
|
|
|
7476
|
-
if (secureDeviceAuthenticated) {
|
|
7477
|
-
if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7478
|
-
writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
|
|
7479
|
-
socket.destroy();
|
|
7480
|
-
return;
|
|
7481
|
-
}
|
|
7482
|
-
const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
|
|
7483
|
-
if (helloChannel !== securityContext.channel) {
|
|
7484
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
|
|
7485
|
-
socket.destroy();
|
|
7486
|
-
return;
|
|
7487
|
-
}
|
|
7488
|
-
} else {
|
|
7489
|
-
const legacyAllowed = socket.__liveDeskRelayControl === true
|
|
7490
|
-
|| socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
|
|
7491
|
-
if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7492
|
-
writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
|
|
7493
|
-
socket.destroy();
|
|
7494
|
-
return;
|
|
7495
|
-
}
|
|
7496
|
-
}
|
|
7497
|
-
|
|
7498
|
-
const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
|
|
7499
|
-
const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
|
|
7500
|
-
? message.capabilities
|
|
7501
|
-
: {};
|
|
7502
|
-
const incomingPolicy = getWelcomeDevicePolicy({
|
|
7503
|
-
deviceId: incomingDeviceId,
|
|
7504
|
-
capabilities: incomingCapabilities
|
|
7505
|
-
}) || {};
|
|
7506
|
-
const connectionDenied = connectionPolicyError(socket, incomingPolicy);
|
|
7507
|
-
if (connectionDenied) {
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
+
if (secureDeviceAuthenticated) {
|
|
7525
|
+
if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
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
|
+
});
|
|
7570
|
+
socket.destroy();
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7574
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
7524
7575
|
if (channel === 'input') {
|
|
7525
7576
|
const device = attachInputSocket(socket, message);
|
|
7526
7577
|
if (!device) {
|
|
@@ -7687,27 +7738,27 @@ export function createRemoteHub(options = {}) {
|
|
|
7687
7738
|
}
|
|
7688
7739
|
}
|
|
7689
7740
|
|
|
7690
|
-
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7691
|
-
if (state.messageRateGate.consume()) return true;
|
|
7692
|
-
const device = state.device;
|
|
7693
|
-
emitEvent('RemoteAbuseDefense', {
|
|
7694
|
-
result: 'rejected',
|
|
7695
|
-
reason: 'authenticated-message-rate-exceeded',
|
|
7696
|
-
deviceId: device?.deviceId || '',
|
|
7697
|
-
sessionId: device?.sessionId || '',
|
|
7698
|
-
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7699
|
-
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7700
|
-
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7701
|
-
channel,
|
|
7702
|
-
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7703
|
-
rate: state.messageRateGate.getStatus()
|
|
7704
|
-
});
|
|
7705
|
-
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7706
|
-
socket.destroy();
|
|
7707
|
-
return false;
|
|
7708
|
-
}
|
|
7709
|
-
|
|
7710
|
-
function handleWebSocketAgentSocket(socket) {
|
|
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
|
+
function handleWebSocketAgentSocket(socket) {
|
|
7711
7762
|
allSockets.add(socket);
|
|
7712
7763
|
socket.setNoDelay(true);
|
|
7713
7764
|
socket.setKeepAlive(true, heartbeatMs);
|
|
@@ -7721,11 +7772,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7721
7772
|
audioOnly: false,
|
|
7722
7773
|
fileOnly: false,
|
|
7723
7774
|
parserBuffer: socket.buffer,
|
|
7724
|
-
binaryIngress: null,
|
|
7725
|
-
binaryQueueDrops: 0,
|
|
7726
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7727
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7728
|
-
closed: false
|
|
7775
|
+
binaryIngress: null,
|
|
7776
|
+
binaryQueueDrops: 0,
|
|
7777
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7778
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7779
|
+
closed: false
|
|
7729
7780
|
};
|
|
7730
7781
|
attachAgentBinaryIngress(socket, state);
|
|
7731
7782
|
|
|
@@ -7736,9 +7787,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7736
7787
|
}
|
|
7737
7788
|
}, 10000);
|
|
7738
7789
|
|
|
7739
|
-
socket.onTextMessage = text => {
|
|
7740
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7741
|
-
try {
|
|
7790
|
+
socket.onTextMessage = text => {
|
|
7791
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7792
|
+
try {
|
|
7742
7793
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7743
7794
|
} catch (err) {
|
|
7744
7795
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
@@ -7746,9 +7797,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7746
7797
|
}
|
|
7747
7798
|
};
|
|
7748
7799
|
|
|
7749
|
-
socket.onBinaryMessage = payload => {
|
|
7750
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7751
|
-
try {
|
|
7800
|
+
socket.onBinaryMessage = payload => {
|
|
7801
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7802
|
+
try {
|
|
7752
7803
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7753
7804
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
7754
7805
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
@@ -7903,11 +7954,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7903
7954
|
buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
|
|
7904
7955
|
lineScanOffset: 0,
|
|
7905
7956
|
pendingBinaryFrame: null,
|
|
7906
|
-
binaryIngress: null,
|
|
7907
|
-
binaryQueueDrops: 0,
|
|
7908
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7909
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7910
|
-
closed: false
|
|
7957
|
+
binaryIngress: null,
|
|
7958
|
+
binaryQueueDrops: 0,
|
|
7959
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7960
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7961
|
+
closed: false
|
|
7911
7962
|
};
|
|
7912
7963
|
attachAgentBinaryIngress(socket, state);
|
|
7913
7964
|
|
|
@@ -8022,18 +8073,18 @@ export function createRemoteHub(options = {}) {
|
|
|
8022
8073
|
return;
|
|
8023
8074
|
}
|
|
8024
8075
|
|
|
8025
|
-
try {
|
|
8026
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8027
|
-
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8028
|
-
? 'frame'
|
|
8029
|
-
: state.audioOnly
|
|
8030
|
-
? 'audio'
|
|
8031
|
-
: state.inputOnly
|
|
8032
|
-
? 'input'
|
|
8033
|
-
: state.fileOnly ? 'file' : 'control')) {
|
|
8034
|
-
return;
|
|
8035
|
-
}
|
|
8036
|
-
if (message?.type === 'frame.binary') {
|
|
8076
|
+
try {
|
|
8077
|
+
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
|
+
if (message?.type === 'frame.binary') {
|
|
8037
8088
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
8038
8089
|
const maxBytes = frameKind === 'thumbnail'
|
|
8039
8090
|
? MAX_THUMBNAIL_BINARY_BYTES
|
|
@@ -8087,35 +8138,35 @@ export function createRemoteHub(options = {}) {
|
|
|
8087
8138
|
}
|
|
8088
8139
|
}
|
|
8089
8140
|
|
|
8090
|
-
function handleSocket(socket) {
|
|
8091
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8092
|
-
secureDirectAcceptor.accept(socket);
|
|
8093
|
-
return;
|
|
8094
|
-
}
|
|
8095
|
-
socket.once('data', chunk => {
|
|
8096
|
-
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8097
|
-
if (isSecureHandshakeStart(firstChunk)) {
|
|
8098
|
-
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8099
|
-
return;
|
|
8100
|
-
}
|
|
8101
|
-
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8102
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8103
|
-
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8104
|
-
socket.destroy();
|
|
8105
|
-
return;
|
|
8106
|
-
}
|
|
8107
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
8108
|
-
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8109
|
-
return;
|
|
8110
|
-
}
|
|
8111
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8112
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8113
|
-
socket.destroy();
|
|
8114
|
-
return;
|
|
8115
|
-
}
|
|
8116
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
8117
|
-
handleTcpAgentSocket(socket, firstChunk);
|
|
8118
|
-
});
|
|
8141
|
+
function handleSocket(socket) {
|
|
8142
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8143
|
+
secureDirectAcceptor.accept(socket);
|
|
8144
|
+
return;
|
|
8145
|
+
}
|
|
8146
|
+
socket.once('data', chunk => {
|
|
8147
|
+
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8148
|
+
if (isSecureHandshakeStart(firstChunk)) {
|
|
8149
|
+
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8150
|
+
return;
|
|
8151
|
+
}
|
|
8152
|
+
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
|
+
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8160
|
+
return;
|
|
8161
|
+
}
|
|
8162
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8163
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8164
|
+
socket.destroy();
|
|
8165
|
+
return;
|
|
8166
|
+
}
|
|
8167
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8168
|
+
handleTcpAgentSocket(socket, firstChunk);
|
|
8169
|
+
});
|
|
8119
8170
|
|
|
8120
8171
|
socket.once('error', () => {
|
|
8121
8172
|
// The transport-specific handler owns logging after the first byte.
|
|
@@ -8137,10 +8188,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8137
8188
|
started = true;
|
|
8138
8189
|
boundPort = candidateServer.address()?.port || port;
|
|
8139
8190
|
lastError = '';
|
|
8140
|
-
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8141
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
8142
|
-
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8143
|
-
}
|
|
8191
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8192
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
8193
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8194
|
+
}
|
|
8144
8195
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8145
8196
|
resolve();
|
|
8146
8197
|
});
|
|
@@ -8152,14 +8203,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8152
8203
|
return getStatus({ includeSecrets: false });
|
|
8153
8204
|
}
|
|
8154
8205
|
|
|
8155
|
-
try {
|
|
8156
|
-
await udpTransport?.start?.();
|
|
8157
|
-
await listenOnPort(requestedPort);
|
|
8158
|
-
if (!controlIdleSweepTimer) {
|
|
8159
|
-
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8160
|
-
controlIdleSweepTimer.unref?.();
|
|
8161
|
-
}
|
|
8162
|
-
try {
|
|
8206
|
+
try {
|
|
8207
|
+
await udpTransport?.start?.();
|
|
8208
|
+
await listenOnPort(requestedPort);
|
|
8209
|
+
if (!controlIdleSweepTimer) {
|
|
8210
|
+
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8211
|
+
controlIdleSweepTimer.unref?.();
|
|
8212
|
+
}
|
|
8213
|
+
try {
|
|
8163
8214
|
await relayControl?.start?.();
|
|
8164
8215
|
} catch {
|
|
8165
8216
|
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
@@ -8180,12 +8231,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8180
8231
|
return getStatus({ includeSecrets: false });
|
|
8181
8232
|
}
|
|
8182
8233
|
|
|
8183
|
-
async function close() {
|
|
8184
|
-
if (controlIdleSweepTimer) {
|
|
8185
|
-
clearInterval(controlIdleSweepTimer);
|
|
8186
|
-
controlIdleSweepTimer = null;
|
|
8187
|
-
}
|
|
8188
|
-
for (const state of [...agentBinaryIngressStates]) {
|
|
8234
|
+
async function close() {
|
|
8235
|
+
if (controlIdleSweepTimer) {
|
|
8236
|
+
clearInterval(controlIdleSweepTimer);
|
|
8237
|
+
controlIdleSweepTimer = null;
|
|
8238
|
+
}
|
|
8239
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
8189
8240
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8190
8241
|
}
|
|
8191
8242
|
|
|
@@ -8322,7 +8373,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8322
8373
|
};
|
|
8323
8374
|
}
|
|
8324
8375
|
|
|
8325
|
-
function sendCommand(deviceId, command) {
|
|
8376
|
+
function sendCommand(deviceId, command) {
|
|
8326
8377
|
const device = devices.get(String(deviceId || ''));
|
|
8327
8378
|
const commandName = safeString(command?.command || 'ping', 80);
|
|
8328
8379
|
const requiredPermission = commandName === 'input.control'
|
|
@@ -8388,64 +8439,64 @@ export function createRemoteHub(options = {}) {
|
|
|
8388
8439
|
command: payload.command,
|
|
8389
8440
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8390
8441
|
});
|
|
8391
|
-
return { ok: true, commandId };
|
|
8392
|
-
}
|
|
8393
|
-
|
|
8394
|
-
async function sendCommandAwaitResult(deviceId, command, options = {}) {
|
|
8395
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
8396
|
-
const device = devices.get(normalizedDeviceId);
|
|
8397
|
-
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8398
|
-
const commandWithId = { ...(command || {}), commandId };
|
|
8399
|
-
|
|
8400
|
-
if (device?.synthetic === true && device.connected) {
|
|
8401
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8402
|
-
return {
|
|
8403
|
-
...sent,
|
|
8404
|
-
queued: sent.ok === true,
|
|
8405
|
-
acknowledged: sent.ok === true,
|
|
8406
|
-
acknowledgement: sent.ok === true
|
|
8407
|
-
? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
|
|
8408
|
-
: { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
|
|
8409
|
-
};
|
|
8410
|
-
}
|
|
8411
|
-
|
|
8412
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8413
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8414
|
-
return {
|
|
8415
|
-
...sent,
|
|
8416
|
-
queued: false,
|
|
8417
|
-
acknowledged: false,
|
|
8418
|
-
acknowledgement: {
|
|
8419
|
-
ok: false,
|
|
8420
|
-
commandId,
|
|
8421
|
-
result: null,
|
|
8422
|
-
error: sent.error || 'device-not-connected'
|
|
8423
|
-
}
|
|
8424
|
-
};
|
|
8425
|
-
}
|
|
8426
|
-
|
|
8427
|
-
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
|
|
8428
|
-
// Register before writing: a loopback Agent can return command.result in
|
|
8429
|
-
// the same event-loop turn as the command write.
|
|
8430
|
-
const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
|
|
8431
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8432
|
-
if (!sent.ok) {
|
|
8433
|
-
failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
|
|
8434
|
-
}
|
|
8435
|
-
const acknowledgement = await acknowledgementPromise;
|
|
8436
|
-
return {
|
|
8437
|
-
...sent,
|
|
8438
|
-
ok: sent.ok === true && acknowledgement.ok === true,
|
|
8439
|
-
queued: sent.ok === true,
|
|
8440
|
-
acknowledged: acknowledgement.ok === true,
|
|
8441
|
-
acknowledgement,
|
|
8442
|
-
error: acknowledgement.ok === true
|
|
8443
|
-
? undefined
|
|
8444
|
-
: acknowledgement.error || sent.error || 'command-result-failed'
|
|
8445
|
-
};
|
|
8446
|
-
}
|
|
8447
|
-
|
|
8448
|
-
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8442
|
+
return { ok: true, commandId };
|
|
8443
|
+
}
|
|
8444
|
+
|
|
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
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8449
8500
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8450
8501
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8451
8502
|
: null;
|
|
@@ -8454,31 +8505,34 @@ export function createRemoteHub(options = {}) {
|
|
|
8454
8505
|
for (const device of devices.values()) {
|
|
8455
8506
|
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8456
8507
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8457
|
-
total += 1;
|
|
8458
|
-
const effectivePolicy = getDevicePolicy(device);
|
|
8459
|
-
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8460
|
-
if (connectionDenied) {
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
}
|
|
8481
|
-
if (
|
|
8508
|
+
total += 1;
|
|
8509
|
+
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
|
+
if (writeJsonLine(device.socket, {
|
|
8482
8536
|
type: 'policy.update',
|
|
8483
8537
|
effectivePolicy,
|
|
8484
8538
|
updatedAt: new Date().toISOString()
|
|
@@ -8557,7 +8611,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8557
8611
|
]);
|
|
8558
8612
|
}
|
|
8559
8613
|
|
|
8560
|
-
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8614
|
+
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8561
8615
|
return {
|
|
8562
8616
|
type: 'input.control',
|
|
8563
8617
|
payload: {
|
|
@@ -8579,114 +8633,114 @@ export function createRemoteHub(options = {}) {
|
|
|
8579
8633
|
hubForwardedAtEpochMs: Date.now(),
|
|
8580
8634
|
issuedAt: new Date().toISOString()
|
|
8581
8635
|
}
|
|
8582
|
-
};
|
|
8583
|
-
}
|
|
8584
|
-
|
|
8585
|
-
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8586
|
-
const policy = getDevicePolicy(device);
|
|
8587
|
-
return {
|
|
8588
|
-
type,
|
|
8589
|
-
payload: {
|
|
8590
|
-
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8591
|
-
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8592
|
-
deviceId: safeString(device?.deviceId, 160),
|
|
8593
|
-
visibleIndicator: true,
|
|
8594
|
-
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8595
|
-
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8596
|
-
reason: safeString(reason, 160),
|
|
8597
|
-
issuedAt: new Date().toISOString()
|
|
8598
|
-
}
|
|
8599
|
-
};
|
|
8600
|
-
}
|
|
8601
|
-
|
|
8602
|
-
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8603
|
-
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8604
|
-
if (inputWriteOwner && bindingKey) {
|
|
8605
|
-
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8606
|
-
}
|
|
8607
|
-
if (!device?.socket || device.socket.destroyed) {
|
|
8608
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
-
}
|
|
8610
|
-
const sent = writeJsonLine(device.socket, {
|
|
8611
|
-
type: 'command',
|
|
8612
|
-
commandId: crypto.randomUUID(),
|
|
8613
|
-
command: type,
|
|
8614
|
-
payload: message.payload,
|
|
8615
|
-
issuedAt: new Date().toISOString()
|
|
8616
|
-
});
|
|
8617
|
-
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8618
|
-
}
|
|
8619
|
-
|
|
8620
|
-
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8621
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8622
|
-
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8623
|
-
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8624
|
-
return { ok: true, alreadyActive: true };
|
|
8625
|
-
}
|
|
8626
|
-
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8627
|
-
device.controlOwnerConnectionId = owner;
|
|
8628
|
-
device.controlOwnerStartedAtMs = Date.now();
|
|
8629
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8630
|
-
const sent = sendControlSessionMessage(
|
|
8631
|
-
device,
|
|
8632
|
-
owner,
|
|
8633
|
-
'control.session.start',
|
|
8634
|
-
'browser-control-owner-active',
|
|
8635
|
-
inputWriteOwner,
|
|
8636
|
-
bindingKey);
|
|
8637
|
-
if (!sent.ok) {
|
|
8638
|
-
device.controlOwnerSessionId = '';
|
|
8639
|
-
device.controlOwnerConnectionId = '';
|
|
8640
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8641
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8642
|
-
return sent;
|
|
8643
|
-
}
|
|
8644
|
-
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8645
|
-
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8646
|
-
hubConnectionId: owner
|
|
8647
|
-
});
|
|
8648
|
-
return sent;
|
|
8649
|
-
}
|
|
8650
|
-
|
|
8651
|
-
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8652
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8653
|
-
if (!device?.controlOwnerSessionId
|
|
8654
|
-
|| !owner
|
|
8655
|
-
|| device.controlOwnerConnectionId !== owner) {
|
|
8656
|
-
return { ok: false, error: 'control-owner-not-current' };
|
|
8657
|
-
}
|
|
8658
|
-
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8659
|
-
const sent = sendControlSessionMessage(
|
|
8660
|
-
device,
|
|
8661
|
-
owner,
|
|
8662
|
-
'control.session.stop',
|
|
8663
|
-
reason,
|
|
8664
|
-
inputWriteOwner,
|
|
8665
|
-
bindingKey);
|
|
8666
|
-
device.controlOwnerSessionId = '';
|
|
8667
|
-
device.controlOwnerConnectionId = '';
|
|
8668
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8669
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8670
|
-
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8671
|
-
controlOwnerSessionId,
|
|
8672
|
-
hubConnectionId: owner,
|
|
8673
|
-
reason,
|
|
8674
|
-
delivered: sent.ok === true
|
|
8675
|
-
});
|
|
8676
|
-
return sent;
|
|
8677
|
-
}
|
|
8678
|
-
|
|
8679
|
-
function sweepIdleControlSessions() {
|
|
8680
|
-
const nowMs = Date.now();
|
|
8681
|
-
for (const device of devices.values()) {
|
|
8682
|
-
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8683
|
-
const policy = getDevicePolicy(device);
|
|
8684
|
-
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8685
|
-
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8686
|
-
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8687
|
-
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8688
|
-
}
|
|
8689
|
-
}
|
|
8636
|
+
};
|
|
8637
|
+
}
|
|
8638
|
+
|
|
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
|
+
}
|
|
8690
8744
|
|
|
8691
8745
|
function getCurrentInputWriteOwner(device) {
|
|
8692
8746
|
if (!device?.inputSocket
|
|
@@ -8829,16 +8883,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8829
8883
|
normalized,
|
|
8830
8884
|
activeMonitorIndex);
|
|
8831
8885
|
const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
|
|
8832
|
-
const ownerChanged = normalized.hubConnectionId
|
|
8886
|
+
const ownerChanged = normalized.hubConnectionId
|
|
8833
8887
|
&& previousOwnerConnectionId
|
|
8834
8888
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8835
8889
|
const bindingChanged = previousInputBindingKey
|
|
8836
8890
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8837
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8838
|
-
const startingControlOwner = !!normalized.hubConnectionId
|
|
8839
|
-
&& (!device.controlOwnerConnectionId
|
|
8840
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8841
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8891
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8892
|
+
const startingControlOwner = !!normalized.hubConnectionId
|
|
8893
|
+
&& (!device.controlOwnerConnectionId
|
|
8894
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8895
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8842
8896
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8843
8897
|
activeInputBindingKey,
|
|
8844
8898
|
buildRemoteInputResetMessage(
|
|
@@ -8853,28 +8907,28 @@ export function createRemoteHub(options = {}) {
|
|
|
8853
8907
|
activeMonitorIndex));
|
|
8854
8908
|
if (!resetSent.ok) {
|
|
8855
8909
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
8856
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8857
|
-
}
|
|
8858
|
-
if (startingControlOwner) {
|
|
8859
|
-
if (device.controlOwnerConnectionId) {
|
|
8860
|
-
endControlSession(
|
|
8861
|
-
device,
|
|
8862
|
-
device.controlOwnerConnectionId,
|
|
8863
|
-
'browser-input-owner-changed',
|
|
8864
|
-
inputWriteOwner,
|
|
8865
|
-
activeInputBindingKey);
|
|
8866
|
-
}
|
|
8867
|
-
const started = beginControlSession(
|
|
8868
|
-
device,
|
|
8869
|
-
normalized.hubConnectionId,
|
|
8870
|
-
inputWriteOwner,
|
|
8871
|
-
activeInputBindingKey);
|
|
8872
|
-
if (!started.ok) {
|
|
8873
|
-
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8874
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8875
|
-
}
|
|
8876
|
-
}
|
|
8877
|
-
}
|
|
8910
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8911
|
+
}
|
|
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
|
+
}
|
|
8878
8932
|
const sent = inputWriteOwner.enqueue({
|
|
8879
8933
|
type: 'input.control',
|
|
8880
8934
|
payload: {
|
|
@@ -8887,8 +8941,8 @@ export function createRemoteHub(options = {}) {
|
|
|
8887
8941
|
if (normalized.hubConnectionId) {
|
|
8888
8942
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8889
8943
|
}
|
|
8890
|
-
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8891
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8944
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8945
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8892
8946
|
device.counters.commandsSent += 1;
|
|
8893
8947
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8894
8948
|
return {
|
|
@@ -8917,27 +8971,27 @@ export function createRemoteHub(options = {}) {
|
|
|
8917
8971
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8918
8972
|
}
|
|
8919
8973
|
|
|
8920
|
-
const hubForwardedAtEpochMs = Date.now();
|
|
8921
|
-
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8922
|
-
&& (!device.controlOwnerConnectionId
|
|
8923
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8924
|
-
if (startingFallbackOwner) {
|
|
8925
|
-
if (device.controlOwnerConnectionId) {
|
|
8926
|
-
endControlSession(
|
|
8927
|
-
device,
|
|
8928
|
-
device.controlOwnerConnectionId,
|
|
8929
|
-
'browser-input-owner-changed',
|
|
8930
|
-
null,
|
|
8931
|
-
'');
|
|
8932
|
-
}
|
|
8933
|
-
const started = beginControlSession(
|
|
8934
|
-
device,
|
|
8935
|
-
normalized.hubConnectionId,
|
|
8936
|
-
null,
|
|
8937
|
-
'');
|
|
8938
|
-
if (!started.ok) return started;
|
|
8939
|
-
}
|
|
8940
|
-
const fallback = sendCommand(deviceId, {
|
|
8974
|
+
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
|
+
const fallback = sendCommand(deviceId, {
|
|
8941
8995
|
command: 'input.control',
|
|
8942
8996
|
payload: {
|
|
8943
8997
|
...normalized,
|
|
@@ -8945,7 +8999,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8945
8999
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8946
9000
|
}
|
|
8947
9001
|
});
|
|
8948
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9002
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8949
9003
|
schedulePendingInputFallback(device, {
|
|
8950
9004
|
commandId: fallback.commandId,
|
|
8951
9005
|
inputSeq: normalized.inputSeq,
|
|
@@ -8957,26 +9011,26 @@ export function createRemoteHub(options = {}) {
|
|
|
8957
9011
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8958
9012
|
hubForwardedAtEpochMs,
|
|
8959
9013
|
fallback: true,
|
|
8960
|
-
timeoutTimer: null
|
|
8961
|
-
});
|
|
8962
|
-
}
|
|
8963
|
-
if (fallback.ok) {
|
|
8964
|
-
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8965
|
-
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
8966
|
-
device,
|
|
8967
|
-
controlStream,
|
|
8968
|
-
normalized,
|
|
8969
|
-
activeMonitorIndex);
|
|
8970
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8971
|
-
} else if (startingFallbackOwner) {
|
|
8972
|
-
endControlSession(
|
|
8973
|
-
device,
|
|
8974
|
-
normalized.hubConnectionId,
|
|
8975
|
-
'input-control-delivery-failed',
|
|
8976
|
-
null,
|
|
8977
|
-
'');
|
|
8978
|
-
}
|
|
8979
|
-
return fallback.ok
|
|
9014
|
+
timeoutTimer: null
|
|
9015
|
+
});
|
|
9016
|
+
}
|
|
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
|
+
return fallback.ok
|
|
8980
9034
|
? {
|
|
8981
9035
|
...fallback,
|
|
8982
9036
|
inputSocket: false,
|
|
@@ -9002,20 +9056,20 @@ export function createRemoteHub(options = {}) {
|
|
|
9002
9056
|
return { ok: false, error: 'input-owner-not-current' };
|
|
9003
9057
|
}
|
|
9004
9058
|
|
|
9005
|
-
const inputSocket = device.inputSocket;
|
|
9006
|
-
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9007
|
-
if (!inputSocket || !inputWriteOwner) {
|
|
9008
|
-
const ended = endControlSession(device, owner, reason, null, '');
|
|
9009
|
-
device.inputOwnerConnectionId = '';
|
|
9010
|
-
device.inputOwnerBindingKey = '';
|
|
9011
|
-
return {
|
|
9012
|
-
ok: ended.ok !== false,
|
|
9013
|
-
queued: false,
|
|
9014
|
-
controlSessionEnded: ended.ok === true
|
|
9015
|
-
};
|
|
9016
|
-
}
|
|
9017
|
-
const controlStream = getActiveControlStream(device);
|
|
9018
|
-
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
9059
|
+
const inputSocket = device.inputSocket;
|
|
9060
|
+
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9061
|
+
if (!inputSocket || !inputWriteOwner) {
|
|
9062
|
+
const ended = endControlSession(device, owner, reason, null, '');
|
|
9063
|
+
device.inputOwnerConnectionId = '';
|
|
9064
|
+
device.inputOwnerBindingKey = '';
|
|
9065
|
+
return {
|
|
9066
|
+
ok: ended.ok !== false,
|
|
9067
|
+
queued: false,
|
|
9068
|
+
controlSessionEnded: ended.ok === true
|
|
9069
|
+
};
|
|
9070
|
+
}
|
|
9071
|
+
const controlStream = getActiveControlStream(device);
|
|
9072
|
+
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
9019
9073
|
const sent = inputWriteOwner.replaceBinding(
|
|
9020
9074
|
releaseBindingKey,
|
|
9021
9075
|
buildRemoteInputResetMessage(
|
|
@@ -9024,24 +9078,24 @@ export function createRemoteHub(options = {}) {
|
|
|
9024
9078
|
owner,
|
|
9025
9079
|
reason,
|
|
9026
9080
|
normalizeMonitorIndex(controlStream?.monitorIndex)));
|
|
9027
|
-
if (!sent.ok) {
|
|
9081
|
+
if (!sent.ok) {
|
|
9028
9082
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
9029
9083
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
9030
|
-
}
|
|
9031
|
-
const ended = endControlSession(
|
|
9032
|
-
device,
|
|
9033
|
-
owner,
|
|
9034
|
-
reason,
|
|
9035
|
-
inputWriteOwner,
|
|
9036
|
-
releaseBindingKey);
|
|
9037
|
-
device.inputOwnerConnectionId = '';
|
|
9038
|
-
device.inputOwnerBindingKey = '';
|
|
9039
|
-
return {
|
|
9040
|
-
ok: ended.ok !== false,
|
|
9041
|
-
queued: true,
|
|
9042
|
-
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
9043
|
-
controlSessionEnded: ended.ok === true
|
|
9044
|
-
};
|
|
9084
|
+
}
|
|
9085
|
+
const ended = endControlSession(
|
|
9086
|
+
device,
|
|
9087
|
+
owner,
|
|
9088
|
+
reason,
|
|
9089
|
+
inputWriteOwner,
|
|
9090
|
+
releaseBindingKey);
|
|
9091
|
+
device.inputOwnerConnectionId = '';
|
|
9092
|
+
device.inputOwnerBindingKey = '';
|
|
9093
|
+
return {
|
|
9094
|
+
ok: ended.ok !== false,
|
|
9095
|
+
queued: true,
|
|
9096
|
+
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
9097
|
+
controlSessionEnded: ended.ok === true
|
|
9098
|
+
};
|
|
9045
9099
|
}
|
|
9046
9100
|
|
|
9047
9101
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -9081,12 +9135,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9081
9135
|
if (retiredRequestError) {
|
|
9082
9136
|
return { ok: false, error: retiredRequestError };
|
|
9083
9137
|
}
|
|
9084
|
-
const device = devices.get(String(deviceId || ''));
|
|
9085
|
-
const requestedOperation = safeString(options.operation, 80);
|
|
9086
|
-
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9087
|
-
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9088
|
-
}
|
|
9089
|
-
const operation = normalizeAgentOperation(requestedOperation);
|
|
9138
|
+
const device = devices.get(String(deviceId || ''));
|
|
9139
|
+
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
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
9090
9144
|
if (requestedOperation && !operation) {
|
|
9091
9145
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
9092
9146
|
}
|
|
@@ -11332,10 +11386,10 @@ export function createRemoteHub(options = {}) {
|
|
|
11332
11386
|
retryTaskBatch,
|
|
11333
11387
|
listDeviceFrames,
|
|
11334
11388
|
disconnectDevice,
|
|
11335
|
-
assignDeviceSlot,
|
|
11336
|
-
sendCommand,
|
|
11337
|
-
sendCommandAwaitResult,
|
|
11338
|
-
refreshDevicePolicies,
|
|
11389
|
+
assignDeviceSlot,
|
|
11390
|
+
sendCommand,
|
|
11391
|
+
sendCommandAwaitResult,
|
|
11392
|
+
refreshDevicePolicies,
|
|
11339
11393
|
sendLegacyClientUpdate,
|
|
11340
11394
|
sendInputControl,
|
|
11341
11395
|
releaseInputOwner,
|
|
@@ -11362,17 +11416,17 @@ export function createRemoteHub(options = {}) {
|
|
|
11362
11416
|
handleUdpFrame,
|
|
11363
11417
|
seedSyntheticFleet,
|
|
11364
11418
|
clearSyntheticFleet,
|
|
11365
|
-
getPairToken: () => pairToken,
|
|
11366
|
-
getPairingPin: () => pairingPin,
|
|
11367
|
-
getSecurityStatus: () => ({
|
|
11368
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
11369
|
-
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11370
|
-
hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
|
|
11371
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
11372
|
-
devices: deviceCredentialAuthority.listDevices()
|
|
11373
|
-
}),
|
|
11374
|
-
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11375
|
-
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
11419
|
+
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()
|
|
11376
11430
|
};
|
|
11377
11431
|
}
|
|
11378
11432
|
|