@livedesk/hub 0.1.30 → 0.1.31
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 +16 -6
- package/src/agents/agent-permissions.js +9 -3
- package/src/agents/agent-tool-registry.js +19 -1
- package/src/captures/capture-store.js +50 -3
- package/src/filesystem/shared-folders.js +8 -0
- package/src/filesystem/transfer-jobs.js +31 -4
- package/src/live-desk-update.js +87 -19
- package/src/remote-hub.js +658 -166
- package/src/security/device-credential-authority.js +358 -0
- package/src/security/security-audit-store.js +238 -0
- package/src/server.js +755 -191
- package/src/settings/settings-schema.js +19 -39
- package/src/transport/relay-hub-control.js +329 -3
- package/src/transport/secure-direct-acceptor.js +432 -0
- package/src/transport/udp-hub-transport.js +17 -5
- package/src/transport/udp-rendezvous.js +54 -2
package/src/remote-hub.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import {
|
|
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';
|
|
5
8
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
6
9
|
import {
|
|
7
10
|
BoundedSegmentedBuffer,
|
|
@@ -32,9 +35,10 @@ const MAX_AGENT_TCP_BINARY_PACKET_BYTES = TCP_BINARY_FRAME_HEADER_BYTES
|
|
|
32
35
|
const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
|
|
33
36
|
+ MAX_AGENT_BINARY_META_BYTES
|
|
34
37
|
+ MAX_AGENT_BINARY_PAYLOAD_BYTES;
|
|
35
|
-
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
36
|
-
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
37
|
-
+ (256 * 1024);
|
|
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
42
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
39
43
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
40
44
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
@@ -106,7 +110,7 @@ const MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS = Object.freeze([
|
|
|
106
110
|
'handoffOldestFrameAgeMs'
|
|
107
111
|
]);
|
|
108
112
|
|
|
109
|
-
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
113
|
+
export function hasCompleteMacNativeResourceTelemetry(frame) {
|
|
110
114
|
return Number(frame?.nativeCaptureTelemetryVersion) >= MAC_NATIVE_RESOURCE_TELEMETRY_VERSION
|
|
111
115
|
&& MAC_NATIVE_RESOURCE_TELEMETRY_FIELDS.every(field =>
|
|
112
116
|
frame?.[field] !== null
|
|
@@ -824,14 +828,61 @@ function isPrivateIPv4(value) {
|
|
|
824
828
|
|| (parts[0] === 192 && parts[1] === 168);
|
|
825
829
|
}
|
|
826
830
|
|
|
827
|
-
function isPrivateNetworkAddress(value) {
|
|
831
|
+
function isPrivateNetworkAddress(value) {
|
|
828
832
|
const address = String(value || '').replace(/^::ffff:/i, '').trim().toLowerCase();
|
|
829
833
|
return isLoopbackHost(address)
|
|
830
834
|
|| isPrivateIPv4(address)
|
|
831
835
|
|| address.startsWith('fe80:')
|
|
832
836
|
|| address.startsWith('fc')
|
|
833
|
-
|| address.startsWith('fd');
|
|
834
|
-
}
|
|
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
|
+
}
|
|
835
886
|
|
|
836
887
|
function readCpuTimes() {
|
|
837
888
|
let idle = 0;
|
|
@@ -1155,34 +1206,37 @@ function getRetiredAgentTaskError(options = {}) {
|
|
|
1155
1206
|
: '';
|
|
1156
1207
|
}
|
|
1157
1208
|
|
|
1158
|
-
const SUPPORTED_AGENT_OPERATIONS = new Set([
|
|
1159
|
-
'system.health',
|
|
1160
|
-
'gpu.status',
|
|
1161
|
-
'disk.status',
|
|
1162
|
-
'process.list',
|
|
1163
|
-
'service.status',
|
|
1164
|
-
'diagnostics.collect',
|
|
1165
|
-
'
|
|
1166
|
-
'
|
|
1167
|
-
'
|
|
1168
|
-
'
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
'
|
|
1173
|
-
'
|
|
1174
|
-
'
|
|
1175
|
-
'
|
|
1176
|
-
'
|
|
1177
|
-
'
|
|
1178
|
-
'
|
|
1179
|
-
'
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
+
}
|
|
1186
1240
|
|
|
1187
1241
|
const REMOTE_INPUT_MONITOR_KEYS = Object.freeze([
|
|
1188
1242
|
'monitorIndex',
|
|
@@ -1348,7 +1402,7 @@ function buildRemoteFramePath(deviceId, frameKind, frame) {
|
|
|
1348
1402
|
return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
|
|
1349
1403
|
}
|
|
1350
1404
|
|
|
1351
|
-
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1405
|
+
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
1352
1406
|
if (!frame) {
|
|
1353
1407
|
return null;
|
|
1354
1408
|
}
|
|
@@ -1370,8 +1424,27 @@ function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
|
1370
1424
|
serialized.dataUrl = dataUrl || buildFrameDataUrl(payload, '', publicFrame.mimeType || publicFrame.format || 'image/jpeg');
|
|
1371
1425
|
}
|
|
1372
1426
|
|
|
1373
|
-
return serialized;
|
|
1374
|
-
}
|
|
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
|
+
}
|
|
1375
1448
|
|
|
1376
1449
|
function getRecentFrameCache(device, frameKind) {
|
|
1377
1450
|
if (!device) {
|
|
@@ -1562,7 +1635,7 @@ function serializeDevice(device, options = {}) {
|
|
|
1562
1635
|
},
|
|
1563
1636
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1564
1637
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1565
|
-
activeLiveStream: device.activeLiveStream
|
|
1638
|
+
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1566
1639
|
liveCapturePause: device.liveCapturePause
|
|
1567
1640
|
? {
|
|
1568
1641
|
token: device.liveCapturePause.token,
|
|
@@ -2169,7 +2242,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2169
2242
|
: null;
|
|
2170
2243
|
const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
|
|
2171
2244
|
|
|
2172
|
-
function getDevicePolicy(device) {
|
|
2245
|
+
function getDevicePolicy(device) {
|
|
2173
2246
|
try {
|
|
2174
2247
|
return getEffectiveDevicePolicy({
|
|
2175
2248
|
deviceId: device?.deviceId || '',
|
|
@@ -2177,8 +2250,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2177
2250
|
}) || {};
|
|
2178
2251
|
} catch {
|
|
2179
2252
|
return {};
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
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
|
+
}
|
|
2182
2265
|
|
|
2183
2266
|
function policyError(device, permission = '', command = '') {
|
|
2184
2267
|
const policy = getDevicePolicy(device);
|
|
@@ -2228,22 +2311,82 @@ export function createRemoteHub(options = {}) {
|
|
|
2228
2311
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
2229
2312
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
2230
2313
|
const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
|
|
2231
|
-
const
|
|
2232
|
-
options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN
|
|
2233
|
-
256);
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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, ttlMs }) => {
|
|
2335
|
+
const identity = getSecurityIdentity() || {};
|
|
2336
|
+
return deviceCredentialAuthority.issueRendezvousProof({
|
|
2337
|
+
roomId,
|
|
2338
|
+
deviceId,
|
|
2339
|
+
ttlMs,
|
|
2340
|
+
accountId: safeString(identity.accountId, 128)
|
|
2341
|
+
});
|
|
2342
|
+
});
|
|
2343
|
+
let pairingPin = /^\d{6}$/.test(String(options.pairingPin || env.LIVEDESK_PAIRING_PIN || '').trim())
|
|
2244
2344
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2245
2345
|
: generatePairingPin();
|
|
2246
|
-
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2346
|
+
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2347
|
+
const consumeEnrollmentToken = expected => {
|
|
2348
|
+
if (!timingSafeStringEqual(expected, pairToken)) return false;
|
|
2349
|
+
pairToken = crypto.randomBytes(32).toString('base64url');
|
|
2350
|
+
pairingPin = generatePairingPin();
|
|
2351
|
+
pairingPinUpdatedAt = new Date().toISOString();
|
|
2352
|
+
emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
|
|
2353
|
+
pairingPinUpdatedAt,
|
|
2354
|
+
hubId: deviceCredentialAuthority.hubId
|
|
2355
|
+
});
|
|
2356
|
+
return true;
|
|
2357
|
+
};
|
|
2358
|
+
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2359
|
+
? options.relayControl
|
|
2360
|
+
: allowLegacyPlaintextForTests
|
|
2361
|
+
? createHubRelayControl({
|
|
2362
|
+
env,
|
|
2363
|
+
pairToken,
|
|
2364
|
+
logEvent,
|
|
2365
|
+
logWarn,
|
|
2366
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2367
|
+
})
|
|
2368
|
+
: createSecureHubRelayControl({
|
|
2369
|
+
env,
|
|
2370
|
+
authority: deviceCredentialAuthority,
|
|
2371
|
+
getIdentity: () => getSecurityIdentity() || {},
|
|
2372
|
+
getEnrollmentToken: () => pairToken,
|
|
2373
|
+
consumeEnrollmentToken,
|
|
2374
|
+
logEvent,
|
|
2375
|
+
logWarn,
|
|
2376
|
+
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2377
|
+
});
|
|
2378
|
+
const secureDirectAcceptor = createSecureDirectAcceptor({
|
|
2379
|
+
authority: deviceCredentialAuthority,
|
|
2380
|
+
getIdentity: () => getSecurityIdentity() || {},
|
|
2381
|
+
getEnrollmentToken: () => pairToken,
|
|
2382
|
+
consumeEnrollmentToken,
|
|
2383
|
+
onSecureSocket: socket => handleTcpAgentSocket(socket),
|
|
2384
|
+
onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
|
|
2385
|
+
...audit,
|
|
2386
|
+
remoteHub: getStatus({ includeSecrets: false })
|
|
2387
|
+
}),
|
|
2388
|
+
logWarn
|
|
2389
|
+
});
|
|
2247
2390
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2248
2391
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2249
2392
|
5000,
|
|
@@ -2268,7 +2411,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2268
2411
|
const transportDiagnostics = new Map();
|
|
2269
2412
|
const transportDiagnosticStartingDevices = new Set();
|
|
2270
2413
|
const duplicateDeviceLogAt = new Map();
|
|
2271
|
-
let server = null;
|
|
2414
|
+
let server = null;
|
|
2415
|
+
let controlIdleSweepTimer = null;
|
|
2272
2416
|
let started = false;
|
|
2273
2417
|
let boundPort = requestedPort;
|
|
2274
2418
|
let lastError = '';
|
|
@@ -2590,8 +2734,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2590
2734
|
host,
|
|
2591
2735
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2592
2736
|
port: boundPort || requestedPort,
|
|
2593
|
-
protocol: '
|
|
2594
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2737
|
+
protocol: 'secure-record-v1',
|
|
2738
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2595
2739
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2596
2740
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2597
2741
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2607,9 +2751,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2607
2751
|
agentEndpointCandidates: routeInfo.candidates,
|
|
2608
2752
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
2609
2753
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
2610
|
-
pairTokenPreview: maskToken(pairToken),
|
|
2611
|
-
pairingPin: includeSecrets ? pairingPin : '',
|
|
2612
|
-
pairingPinUpdatedAt,
|
|
2754
|
+
pairTokenPreview: maskToken(pairToken),
|
|
2755
|
+
pairingPin: includeSecrets ? pairingPin : '',
|
|
2756
|
+
pairingPinUpdatedAt,
|
|
2757
|
+
security: {
|
|
2758
|
+
direct: secureDirectAcceptor.getStatus(),
|
|
2759
|
+
hubId: deviceCredentialAuthority.hubId,
|
|
2760
|
+
hubPublicKeyFingerprint: crypto.createHash('sha256')
|
|
2761
|
+
.update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
|
|
2762
|
+
.digest('hex'),
|
|
2763
|
+
enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
|
|
2764
|
+
legacyPlaintextForTests: allowLegacyPlaintextForTests
|
|
2765
|
+
},
|
|
2613
2766
|
deviceCount: devices.size,
|
|
2614
2767
|
connectedDeviceCount: connectedDevices,
|
|
2615
2768
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -3869,14 +4022,22 @@ export function createRemoteHub(options = {}) {
|
|
|
3869
4022
|
}
|
|
3870
4023
|
}
|
|
3871
4024
|
|
|
3872
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4025
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
3873
4026
|
const socket = device?.inputSocket;
|
|
3874
4027
|
if (!socket) {
|
|
3875
4028
|
return;
|
|
3876
4029
|
}
|
|
3877
|
-
|
|
3878
|
-
const writeOwner = device.inputWriteOwner;
|
|
3879
|
-
device.
|
|
4030
|
+
|
|
4031
|
+
const writeOwner = device.inputWriteOwner;
|
|
4032
|
+
if (device.controlOwnerConnectionId) {
|
|
4033
|
+
endControlSession(
|
|
4034
|
+
device,
|
|
4035
|
+
device.controlOwnerConnectionId,
|
|
4036
|
+
reason,
|
|
4037
|
+
writeOwner,
|
|
4038
|
+
writeOwner?.getBindingKey?.() || '');
|
|
4039
|
+
}
|
|
4040
|
+
device.inputSocket = null;
|
|
3880
4041
|
device.inputWriteOwner = null;
|
|
3881
4042
|
device.inputSocketConnectionId = '';
|
|
3882
4043
|
device.inputOwnerConnectionId = '';
|
|
@@ -3906,9 +4067,17 @@ export function createRemoteHub(options = {}) {
|
|
|
3906
4067
|
if (!device || device.inputSocket !== socket) {
|
|
3907
4068
|
return;
|
|
3908
4069
|
}
|
|
3909
|
-
|
|
3910
|
-
const writeOwner = device.inputWriteOwner;
|
|
3911
|
-
device.
|
|
4070
|
+
|
|
4071
|
+
const writeOwner = device.inputWriteOwner;
|
|
4072
|
+
if (device.controlOwnerConnectionId) {
|
|
4073
|
+
endControlSession(
|
|
4074
|
+
device,
|
|
4075
|
+
device.controlOwnerConnectionId,
|
|
4076
|
+
reason,
|
|
4077
|
+
null,
|
|
4078
|
+
'');
|
|
4079
|
+
}
|
|
4080
|
+
device.inputSocket = null;
|
|
3912
4081
|
device.inputWriteOwner = null;
|
|
3913
4082
|
device.inputSocketConnectionId = '';
|
|
3914
4083
|
device.inputOwnerConnectionId = '';
|
|
@@ -4340,7 +4509,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4340
4509
|
inputWriteOwner: null,
|
|
4341
4510
|
inputSocketConnectionId: '',
|
|
4342
4511
|
inputOwnerConnectionId: '',
|
|
4343
|
-
inputOwnerBindingKey: '',
|
|
4512
|
+
inputOwnerBindingKey: '',
|
|
4513
|
+
controlOwnerSessionId: '',
|
|
4514
|
+
controlOwnerConnectionId: '',
|
|
4515
|
+
controlOwnerStartedAtMs: 0,
|
|
4516
|
+
controlOwnerLastInputAtMs: 0,
|
|
4344
4517
|
frameSocket: null,
|
|
4345
4518
|
audioSocket: null,
|
|
4346
4519
|
fileSocket: null,
|
|
@@ -7261,8 +7434,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7261
7434
|
return;
|
|
7262
7435
|
}
|
|
7263
7436
|
|
|
7264
|
-
if (!state.authenticated) {
|
|
7265
|
-
|
|
7437
|
+
if (!state.authenticated) {
|
|
7438
|
+
const securityContext = socket.__liveDeskSecurityContext;
|
|
7439
|
+
const secureDeviceAuthenticated = securityContext?.authenticated === true
|
|
7440
|
+
&& securityContext?.encrypted === true
|
|
7441
|
+
&& safeString(securityContext.accountId, 128)
|
|
7442
|
+
&& safeString(securityContext.hubId, 128)
|
|
7443
|
+
&& safeString(securityContext.deviceId, 128);
|
|
7444
|
+
if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
|
|
7266
7445
|
message = {
|
|
7267
7446
|
...message,
|
|
7268
7447
|
capabilities: {
|
|
@@ -7272,13 +7451,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7272
7451
|
dedicatedInputChannel: false
|
|
7273
7452
|
}
|
|
7274
7453
|
};
|
|
7275
|
-
}
|
|
7276
|
-
if (message.type === 'slot.assign') {
|
|
7277
|
-
if (!
|
|
7278
|
-
|
|
7279
|
-
socket.
|
|
7280
|
-
|
|
7281
|
-
|
|
7454
|
+
}
|
|
7455
|
+
if (message.type === 'slot.assign') {
|
|
7456
|
+
if (!secureDeviceAuthenticated
|
|
7457
|
+
|| safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7458
|
+
writeJsonLine(socket, { type: 'slot.assignment', ok: false, error: 'authenticated-device-credential-required' });
|
|
7459
|
+
socket.end();
|
|
7460
|
+
return;
|
|
7461
|
+
}
|
|
7282
7462
|
const result = assignDeviceSlot(
|
|
7283
7463
|
message.deviceId || message.DeviceId,
|
|
7284
7464
|
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
@@ -7292,13 +7472,54 @@ export function createRemoteHub(options = {}) {
|
|
|
7292
7472
|
return;
|
|
7293
7473
|
}
|
|
7294
7474
|
|
|
7295
|
-
if (
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7475
|
+
if (secureDeviceAuthenticated) {
|
|
7476
|
+
if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7477
|
+
writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
|
|
7478
|
+
socket.destroy();
|
|
7479
|
+
return;
|
|
7480
|
+
}
|
|
7481
|
+
const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
|
|
7482
|
+
if (helloChannel !== securityContext.channel) {
|
|
7483
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
|
|
7484
|
+
socket.destroy();
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
} else {
|
|
7488
|
+
const legacyAllowed = socket.__liveDeskRelayControl === true
|
|
7489
|
+
|| socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
|
|
7490
|
+
if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7491
|
+
writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
|
|
7492
|
+
socket.destroy();
|
|
7493
|
+
return;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
|
|
7497
|
+
const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
|
|
7498
|
+
const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
|
|
7499
|
+
? message.capabilities
|
|
7500
|
+
: {};
|
|
7501
|
+
const incomingPolicy = getWelcomeDevicePolicy({
|
|
7502
|
+
deviceId: incomingDeviceId,
|
|
7503
|
+
capabilities: incomingCapabilities
|
|
7504
|
+
}) || {};
|
|
7505
|
+
const connectionDenied = connectionPolicyError(socket, incomingPolicy);
|
|
7506
|
+
if (connectionDenied) {
|
|
7507
|
+
writeJsonLine(socket, { type: 'error', error: connectionDenied });
|
|
7508
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
7509
|
+
result: 'rejected',
|
|
7510
|
+
reason: connectionDenied,
|
|
7511
|
+
deviceId: incomingDeviceId,
|
|
7512
|
+
sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
|
|
7513
|
+
accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
|
|
7514
|
+
hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
|
|
7515
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7516
|
+
remoteAddress: safeString(socket.remoteAddress, 256)
|
|
7517
|
+
});
|
|
7518
|
+
socket.destroy();
|
|
7519
|
+
return;
|
|
7520
|
+
}
|
|
7521
|
+
|
|
7522
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
7302
7523
|
if (channel === 'input') {
|
|
7303
7524
|
const device = attachInputSocket(socket, message);
|
|
7304
7525
|
if (!device) {
|
|
@@ -7465,7 +7686,27 @@ export function createRemoteHub(options = {}) {
|
|
|
7465
7686
|
}
|
|
7466
7687
|
}
|
|
7467
7688
|
|
|
7468
|
-
function
|
|
7689
|
+
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7690
|
+
if (state.messageRateGate.consume()) return true;
|
|
7691
|
+
const device = state.device;
|
|
7692
|
+
emitEvent('RemoteAbuseDefense', {
|
|
7693
|
+
result: 'rejected',
|
|
7694
|
+
reason: 'authenticated-message-rate-exceeded',
|
|
7695
|
+
deviceId: device?.deviceId || '',
|
|
7696
|
+
sessionId: device?.sessionId || '',
|
|
7697
|
+
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7698
|
+
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7699
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7700
|
+
channel,
|
|
7701
|
+
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7702
|
+
rate: state.messageRateGate.getStatus()
|
|
7703
|
+
});
|
|
7704
|
+
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7705
|
+
socket.destroy();
|
|
7706
|
+
return false;
|
|
7707
|
+
}
|
|
7708
|
+
|
|
7709
|
+
function handleWebSocketAgentSocket(socket) {
|
|
7469
7710
|
allSockets.add(socket);
|
|
7470
7711
|
socket.setNoDelay(true);
|
|
7471
7712
|
socket.setKeepAlive(true, heartbeatMs);
|
|
@@ -7479,10 +7720,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7479
7720
|
audioOnly: false,
|
|
7480
7721
|
fileOnly: false,
|
|
7481
7722
|
parserBuffer: socket.buffer,
|
|
7482
|
-
binaryIngress: null,
|
|
7483
|
-
binaryQueueDrops: 0,
|
|
7484
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7485
|
-
|
|
7723
|
+
binaryIngress: null,
|
|
7724
|
+
binaryQueueDrops: 0,
|
|
7725
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7726
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7727
|
+
closed: false
|
|
7486
7728
|
};
|
|
7487
7729
|
attachAgentBinaryIngress(socket, state);
|
|
7488
7730
|
|
|
@@ -7493,8 +7735,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7493
7735
|
}
|
|
7494
7736
|
}, 10000);
|
|
7495
7737
|
|
|
7496
|
-
socket.onTextMessage = text => {
|
|
7497
|
-
|
|
7738
|
+
socket.onTextMessage = text => {
|
|
7739
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7740
|
+
try {
|
|
7498
7741
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7499
7742
|
} catch (err) {
|
|
7500
7743
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
@@ -7502,8 +7745,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7502
7745
|
}
|
|
7503
7746
|
};
|
|
7504
7747
|
|
|
7505
|
-
socket.onBinaryMessage = payload => {
|
|
7506
|
-
|
|
7748
|
+
socket.onBinaryMessage = payload => {
|
|
7749
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7750
|
+
try {
|
|
7507
7751
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7508
7752
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
7509
7753
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
@@ -7658,10 +7902,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7658
7902
|
buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
|
|
7659
7903
|
lineScanOffset: 0,
|
|
7660
7904
|
pendingBinaryFrame: null,
|
|
7661
|
-
binaryIngress: null,
|
|
7662
|
-
binaryQueueDrops: 0,
|
|
7663
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7664
|
-
|
|
7905
|
+
binaryIngress: null,
|
|
7906
|
+
binaryQueueDrops: 0,
|
|
7907
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7908
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7909
|
+
closed: false
|
|
7665
7910
|
};
|
|
7666
7911
|
attachAgentBinaryIngress(socket, state);
|
|
7667
7912
|
|
|
@@ -7776,9 +8021,18 @@ export function createRemoteHub(options = {}) {
|
|
|
7776
8021
|
return;
|
|
7777
8022
|
}
|
|
7778
8023
|
|
|
7779
|
-
try {
|
|
7780
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
7781
|
-
if (
|
|
8024
|
+
try {
|
|
8025
|
+
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8026
|
+
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8027
|
+
? 'frame'
|
|
8028
|
+
: state.audioOnly
|
|
8029
|
+
? 'audio'
|
|
8030
|
+
: state.inputOnly
|
|
8031
|
+
? 'input'
|
|
8032
|
+
: state.fileOnly ? 'file' : 'control')) {
|
|
8033
|
+
return;
|
|
8034
|
+
}
|
|
8035
|
+
if (message?.type === 'frame.binary') {
|
|
7782
8036
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
7783
8037
|
const maxBytes = frameKind === 'thumbnail'
|
|
7784
8038
|
? MAX_THUMBNAIL_BINARY_BYTES
|
|
@@ -7832,16 +8086,35 @@ export function createRemoteHub(options = {}) {
|
|
|
7832
8086
|
}
|
|
7833
8087
|
}
|
|
7834
8088
|
|
|
7835
|
-
function handleSocket(socket) {
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
8089
|
+
function handleSocket(socket) {
|
|
8090
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8091
|
+
secureDirectAcceptor.accept(socket);
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
socket.once('data', chunk => {
|
|
8095
|
+
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8096
|
+
if (isSecureHandshakeStart(firstChunk)) {
|
|
8097
|
+
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8098
|
+
return;
|
|
8099
|
+
}
|
|
8100
|
+
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8101
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8102
|
+
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8103
|
+
socket.destroy();
|
|
8104
|
+
return;
|
|
8105
|
+
}
|
|
8106
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8107
|
+
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8108
|
+
return;
|
|
8109
|
+
}
|
|
8110
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8111
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8112
|
+
socket.destroy();
|
|
8113
|
+
return;
|
|
8114
|
+
}
|
|
8115
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8116
|
+
handleTcpAgentSocket(socket, firstChunk);
|
|
8117
|
+
});
|
|
7845
8118
|
|
|
7846
8119
|
socket.once('error', () => {
|
|
7847
8120
|
// The transport-specific handler owns logging after the first byte.
|
|
@@ -7863,10 +8136,10 @@ export function createRemoteHub(options = {}) {
|
|
|
7863
8136
|
started = true;
|
|
7864
8137
|
boundPort = candidateServer.address()?.port || port;
|
|
7865
8138
|
lastError = '';
|
|
7866
|
-
logEvent('remote', `LiveDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
|
|
7867
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
7868
|
-
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable
|
|
7869
|
-
}
|
|
8139
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8140
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
8141
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8142
|
+
}
|
|
7870
8143
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
7871
8144
|
resolve();
|
|
7872
8145
|
});
|
|
@@ -7878,10 +8151,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7878
8151
|
return getStatus({ includeSecrets: false });
|
|
7879
8152
|
}
|
|
7880
8153
|
|
|
7881
|
-
try {
|
|
7882
|
-
await udpTransport?.start?.();
|
|
7883
|
-
await listenOnPort(requestedPort);
|
|
7884
|
-
|
|
8154
|
+
try {
|
|
8155
|
+
await udpTransport?.start?.();
|
|
8156
|
+
await listenOnPort(requestedPort);
|
|
8157
|
+
if (!controlIdleSweepTimer) {
|
|
8158
|
+
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8159
|
+
controlIdleSweepTimer.unref?.();
|
|
8160
|
+
}
|
|
8161
|
+
try {
|
|
7885
8162
|
await relayControl?.start?.();
|
|
7886
8163
|
} catch {
|
|
7887
8164
|
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
@@ -7902,8 +8179,12 @@ export function createRemoteHub(options = {}) {
|
|
|
7902
8179
|
return getStatus({ includeSecrets: false });
|
|
7903
8180
|
}
|
|
7904
8181
|
|
|
7905
|
-
async function close() {
|
|
7906
|
-
|
|
8182
|
+
async function close() {
|
|
8183
|
+
if (controlIdleSweepTimer) {
|
|
8184
|
+
clearInterval(controlIdleSweepTimer);
|
|
8185
|
+
controlIdleSweepTimer = null;
|
|
8186
|
+
}
|
|
8187
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
7907
8188
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
7908
8189
|
}
|
|
7909
8190
|
|
|
@@ -8109,7 +8390,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8109
8390
|
return { ok: true, commandId };
|
|
8110
8391
|
}
|
|
8111
8392
|
|
|
8112
|
-
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8393
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8113
8394
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8114
8395
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8115
8396
|
: null;
|
|
@@ -8118,9 +8399,31 @@ export function createRemoteHub(options = {}) {
|
|
|
8118
8399
|
for (const device of devices.values()) {
|
|
8119
8400
|
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8120
8401
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8121
|
-
total += 1;
|
|
8122
|
-
const effectivePolicy = getDevicePolicy(device);
|
|
8123
|
-
|
|
8402
|
+
total += 1;
|
|
8403
|
+
const effectivePolicy = getDevicePolicy(device);
|
|
8404
|
+
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8405
|
+
if (connectionDenied) {
|
|
8406
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
8407
|
+
result: 'rejected',
|
|
8408
|
+
reason: connectionDenied,
|
|
8409
|
+
deviceId: device.deviceId,
|
|
8410
|
+
sessionId: device.sessionId,
|
|
8411
|
+
transport: device.controlTransport,
|
|
8412
|
+
remoteAddress: device.remoteAddress
|
|
8413
|
+
});
|
|
8414
|
+
disconnectDevice(device.deviceId, connectionDenied);
|
|
8415
|
+
continue;
|
|
8416
|
+
}
|
|
8417
|
+
if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
|
|
8418
|
+
releaseInputOwner(
|
|
8419
|
+
device.deviceId,
|
|
8420
|
+
device.controlOwnerConnectionId,
|
|
8421
|
+
'control-disabled-by-policy');
|
|
8422
|
+
}
|
|
8423
|
+
if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
|
|
8424
|
+
stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
|
|
8425
|
+
}
|
|
8426
|
+
if (writeJsonLine(device.socket, {
|
|
8124
8427
|
type: 'policy.update',
|
|
8125
8428
|
effectivePolicy,
|
|
8126
8429
|
updatedAt: new Date().toISOString()
|
|
@@ -8199,7 +8502,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8199
8502
|
]);
|
|
8200
8503
|
}
|
|
8201
8504
|
|
|
8202
|
-
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8505
|
+
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8203
8506
|
return {
|
|
8204
8507
|
type: 'input.control',
|
|
8205
8508
|
payload: {
|
|
@@ -8221,8 +8524,114 @@ export function createRemoteHub(options = {}) {
|
|
|
8221
8524
|
hubForwardedAtEpochMs: Date.now(),
|
|
8222
8525
|
issuedAt: new Date().toISOString()
|
|
8223
8526
|
}
|
|
8224
|
-
};
|
|
8225
|
-
}
|
|
8527
|
+
};
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8530
|
+
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8531
|
+
const policy = getDevicePolicy(device);
|
|
8532
|
+
return {
|
|
8533
|
+
type,
|
|
8534
|
+
payload: {
|
|
8535
|
+
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8536
|
+
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8537
|
+
deviceId: safeString(device?.deviceId, 160),
|
|
8538
|
+
visibleIndicator: true,
|
|
8539
|
+
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8540
|
+
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8541
|
+
reason: safeString(reason, 160),
|
|
8542
|
+
issuedAt: new Date().toISOString()
|
|
8543
|
+
}
|
|
8544
|
+
};
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8547
|
+
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8548
|
+
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8549
|
+
if (inputWriteOwner && bindingKey) {
|
|
8550
|
+
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8551
|
+
}
|
|
8552
|
+
if (!device?.socket || device.socket.destroyed) {
|
|
8553
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8554
|
+
}
|
|
8555
|
+
const sent = writeJsonLine(device.socket, {
|
|
8556
|
+
type: 'command',
|
|
8557
|
+
commandId: crypto.randomUUID(),
|
|
8558
|
+
command: type,
|
|
8559
|
+
payload: message.payload,
|
|
8560
|
+
issuedAt: new Date().toISOString()
|
|
8561
|
+
});
|
|
8562
|
+
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8563
|
+
}
|
|
8564
|
+
|
|
8565
|
+
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8566
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8567
|
+
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8568
|
+
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8569
|
+
return { ok: true, alreadyActive: true };
|
|
8570
|
+
}
|
|
8571
|
+
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8572
|
+
device.controlOwnerConnectionId = owner;
|
|
8573
|
+
device.controlOwnerStartedAtMs = Date.now();
|
|
8574
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8575
|
+
const sent = sendControlSessionMessage(
|
|
8576
|
+
device,
|
|
8577
|
+
owner,
|
|
8578
|
+
'control.session.start',
|
|
8579
|
+
'browser-control-owner-active',
|
|
8580
|
+
inputWriteOwner,
|
|
8581
|
+
bindingKey);
|
|
8582
|
+
if (!sent.ok) {
|
|
8583
|
+
device.controlOwnerSessionId = '';
|
|
8584
|
+
device.controlOwnerConnectionId = '';
|
|
8585
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8586
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8587
|
+
return sent;
|
|
8588
|
+
}
|
|
8589
|
+
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8590
|
+
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8591
|
+
hubConnectionId: owner
|
|
8592
|
+
});
|
|
8593
|
+
return sent;
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8596
|
+
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8597
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8598
|
+
if (!device?.controlOwnerSessionId
|
|
8599
|
+
|| !owner
|
|
8600
|
+
|| device.controlOwnerConnectionId !== owner) {
|
|
8601
|
+
return { ok: false, error: 'control-owner-not-current' };
|
|
8602
|
+
}
|
|
8603
|
+
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8604
|
+
const sent = sendControlSessionMessage(
|
|
8605
|
+
device,
|
|
8606
|
+
owner,
|
|
8607
|
+
'control.session.stop',
|
|
8608
|
+
reason,
|
|
8609
|
+
inputWriteOwner,
|
|
8610
|
+
bindingKey);
|
|
8611
|
+
device.controlOwnerSessionId = '';
|
|
8612
|
+
device.controlOwnerConnectionId = '';
|
|
8613
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8614
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8615
|
+
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8616
|
+
controlOwnerSessionId,
|
|
8617
|
+
hubConnectionId: owner,
|
|
8618
|
+
reason,
|
|
8619
|
+
delivered: sent.ok === true
|
|
8620
|
+
});
|
|
8621
|
+
return sent;
|
|
8622
|
+
}
|
|
8623
|
+
|
|
8624
|
+
function sweepIdleControlSessions() {
|
|
8625
|
+
const nowMs = Date.now();
|
|
8626
|
+
for (const device of devices.values()) {
|
|
8627
|
+
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8628
|
+
const policy = getDevicePolicy(device);
|
|
8629
|
+
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8630
|
+
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8631
|
+
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8632
|
+
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8226
8635
|
|
|
8227
8636
|
function getCurrentInputWriteOwner(device) {
|
|
8228
8637
|
if (!device?.inputSocket
|
|
@@ -8365,13 +8774,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8365
8774
|
normalized,
|
|
8366
8775
|
activeMonitorIndex);
|
|
8367
8776
|
const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
|
|
8368
|
-
const ownerChanged = normalized.hubConnectionId
|
|
8777
|
+
const ownerChanged = normalized.hubConnectionId
|
|
8369
8778
|
&& previousOwnerConnectionId
|
|
8370
8779
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8371
8780
|
const bindingChanged = previousInputBindingKey
|
|
8372
8781
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8373
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8374
|
-
|
|
8782
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8783
|
+
const startingControlOwner = !!normalized.hubConnectionId
|
|
8784
|
+
&& (!device.controlOwnerConnectionId
|
|
8785
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8786
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8375
8787
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8376
8788
|
activeInputBindingKey,
|
|
8377
8789
|
buildRemoteInputResetMessage(
|
|
@@ -8386,9 +8798,28 @@ export function createRemoteHub(options = {}) {
|
|
|
8386
8798
|
activeMonitorIndex));
|
|
8387
8799
|
if (!resetSent.ok) {
|
|
8388
8800
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
8389
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8390
|
-
}
|
|
8391
|
-
|
|
8801
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8802
|
+
}
|
|
8803
|
+
if (startingControlOwner) {
|
|
8804
|
+
if (device.controlOwnerConnectionId) {
|
|
8805
|
+
endControlSession(
|
|
8806
|
+
device,
|
|
8807
|
+
device.controlOwnerConnectionId,
|
|
8808
|
+
'browser-input-owner-changed',
|
|
8809
|
+
inputWriteOwner,
|
|
8810
|
+
activeInputBindingKey);
|
|
8811
|
+
}
|
|
8812
|
+
const started = beginControlSession(
|
|
8813
|
+
device,
|
|
8814
|
+
normalized.hubConnectionId,
|
|
8815
|
+
inputWriteOwner,
|
|
8816
|
+
activeInputBindingKey);
|
|
8817
|
+
if (!started.ok) {
|
|
8818
|
+
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8819
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8820
|
+
}
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8392
8823
|
const sent = inputWriteOwner.enqueue({
|
|
8393
8824
|
type: 'input.control',
|
|
8394
8825
|
payload: {
|
|
@@ -8401,7 +8832,8 @@ export function createRemoteHub(options = {}) {
|
|
|
8401
8832
|
if (normalized.hubConnectionId) {
|
|
8402
8833
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8403
8834
|
}
|
|
8404
|
-
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8835
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8836
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8405
8837
|
device.counters.commandsSent += 1;
|
|
8406
8838
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8407
8839
|
return {
|
|
@@ -8430,8 +8862,27 @@ export function createRemoteHub(options = {}) {
|
|
|
8430
8862
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8431
8863
|
}
|
|
8432
8864
|
|
|
8433
|
-
const hubForwardedAtEpochMs = Date.now();
|
|
8434
|
-
const
|
|
8865
|
+
const hubForwardedAtEpochMs = Date.now();
|
|
8866
|
+
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8867
|
+
&& (!device.controlOwnerConnectionId
|
|
8868
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8869
|
+
if (startingFallbackOwner) {
|
|
8870
|
+
if (device.controlOwnerConnectionId) {
|
|
8871
|
+
endControlSession(
|
|
8872
|
+
device,
|
|
8873
|
+
device.controlOwnerConnectionId,
|
|
8874
|
+
'browser-input-owner-changed',
|
|
8875
|
+
null,
|
|
8876
|
+
'');
|
|
8877
|
+
}
|
|
8878
|
+
const started = beginControlSession(
|
|
8879
|
+
device,
|
|
8880
|
+
normalized.hubConnectionId,
|
|
8881
|
+
null,
|
|
8882
|
+
'');
|
|
8883
|
+
if (!started.ok) return started;
|
|
8884
|
+
}
|
|
8885
|
+
const fallback = sendCommand(deviceId, {
|
|
8435
8886
|
command: 'input.control',
|
|
8436
8887
|
payload: {
|
|
8437
8888
|
...normalized,
|
|
@@ -8439,7 +8890,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8439
8890
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8440
8891
|
}
|
|
8441
8892
|
});
|
|
8442
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8893
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8443
8894
|
schedulePendingInputFallback(device, {
|
|
8444
8895
|
commandId: fallback.commandId,
|
|
8445
8896
|
inputSeq: normalized.inputSeq,
|
|
@@ -8451,10 +8902,26 @@ export function createRemoteHub(options = {}) {
|
|
|
8451
8902
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8452
8903
|
hubForwardedAtEpochMs,
|
|
8453
8904
|
fallback: true,
|
|
8454
|
-
timeoutTimer: null
|
|
8455
|
-
});
|
|
8456
|
-
}
|
|
8457
|
-
|
|
8905
|
+
timeoutTimer: null
|
|
8906
|
+
});
|
|
8907
|
+
}
|
|
8908
|
+
if (fallback.ok) {
|
|
8909
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8910
|
+
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
8911
|
+
device,
|
|
8912
|
+
controlStream,
|
|
8913
|
+
normalized,
|
|
8914
|
+
activeMonitorIndex);
|
|
8915
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8916
|
+
} else if (startingFallbackOwner) {
|
|
8917
|
+
endControlSession(
|
|
8918
|
+
device,
|
|
8919
|
+
normalized.hubConnectionId,
|
|
8920
|
+
'input-control-delivery-failed',
|
|
8921
|
+
null,
|
|
8922
|
+
'');
|
|
8923
|
+
}
|
|
8924
|
+
return fallback.ok
|
|
8458
8925
|
? {
|
|
8459
8926
|
...fallback,
|
|
8460
8927
|
inputSocket: false,
|
|
@@ -8480,15 +8947,20 @@ export function createRemoteHub(options = {}) {
|
|
|
8480
8947
|
return { ok: false, error: 'input-owner-not-current' };
|
|
8481
8948
|
}
|
|
8482
8949
|
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8950
|
+
const inputSocket = device.inputSocket;
|
|
8951
|
+
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
8952
|
+
if (!inputSocket || !inputWriteOwner) {
|
|
8953
|
+
const ended = endControlSession(device, owner, reason, null, '');
|
|
8954
|
+
device.inputOwnerConnectionId = '';
|
|
8955
|
+
device.inputOwnerBindingKey = '';
|
|
8956
|
+
return {
|
|
8957
|
+
ok: ended.ok !== false,
|
|
8958
|
+
queued: false,
|
|
8959
|
+
controlSessionEnded: ended.ok === true
|
|
8960
|
+
};
|
|
8961
|
+
}
|
|
8962
|
+
const controlStream = getActiveControlStream(device);
|
|
8963
|
+
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
8492
8964
|
const sent = inputWriteOwner.replaceBinding(
|
|
8493
8965
|
releaseBindingKey,
|
|
8494
8966
|
buildRemoteInputResetMessage(
|
|
@@ -8497,15 +8969,24 @@ export function createRemoteHub(options = {}) {
|
|
|
8497
8969
|
owner,
|
|
8498
8970
|
reason,
|
|
8499
8971
|
normalizeMonitorIndex(controlStream?.monitorIndex)));
|
|
8500
|
-
if (!sent.ok) {
|
|
8972
|
+
if (!sent.ok) {
|
|
8501
8973
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
8502
8974
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8503
|
-
}
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8975
|
+
}
|
|
8976
|
+
const ended = endControlSession(
|
|
8977
|
+
device,
|
|
8978
|
+
owner,
|
|
8979
|
+
reason,
|
|
8980
|
+
inputWriteOwner,
|
|
8981
|
+
releaseBindingKey);
|
|
8982
|
+
device.inputOwnerConnectionId = '';
|
|
8983
|
+
device.inputOwnerBindingKey = '';
|
|
8984
|
+
return {
|
|
8985
|
+
ok: ended.ok !== false,
|
|
8986
|
+
queued: true,
|
|
8987
|
+
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
8988
|
+
controlSessionEnded: ended.ok === true
|
|
8989
|
+
};
|
|
8509
8990
|
}
|
|
8510
8991
|
|
|
8511
8992
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -8545,9 +9026,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8545
9026
|
if (retiredRequestError) {
|
|
8546
9027
|
return { ok: false, error: retiredRequestError };
|
|
8547
9028
|
}
|
|
8548
|
-
const device = devices.get(String(deviceId || ''));
|
|
8549
|
-
const requestedOperation = safeString(options.operation, 80);
|
|
8550
|
-
|
|
9029
|
+
const device = devices.get(String(deviceId || ''));
|
|
9030
|
+
const requestedOperation = safeString(options.operation, 80);
|
|
9031
|
+
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9032
|
+
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9033
|
+
}
|
|
9034
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
8551
9035
|
if (requestedOperation && !operation) {
|
|
8552
9036
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
8553
9037
|
}
|
|
@@ -10822,8 +11306,16 @@ export function createRemoteHub(options = {}) {
|
|
|
10822
11306
|
handleUdpFrame,
|
|
10823
11307
|
seedSyntheticFleet,
|
|
10824
11308
|
clearSyntheticFleet,
|
|
10825
|
-
getPairToken: () => pairToken,
|
|
10826
|
-
getPairingPin: () => pairingPin
|
|
11309
|
+
getPairToken: () => pairToken,
|
|
11310
|
+
getPairingPin: () => pairingPin,
|
|
11311
|
+
getSecurityStatus: () => ({
|
|
11312
|
+
hubId: deviceCredentialAuthority.hubId,
|
|
11313
|
+
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11314
|
+
direct: secureDirectAcceptor.getStatus(),
|
|
11315
|
+
devices: deviceCredentialAuthority.listDevices()
|
|
11316
|
+
}),
|
|
11317
|
+
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11318
|
+
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
10827
11319
|
};
|
|
10828
11320
|
}
|
|
10829
11321
|
|