@livedesk/hub 0.1.31 → 0.1.33
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/filesystem/transfer-jobs.js +5 -1
- package/src/remote-hub.js +721 -658
- package/src/security/device-credential-authority.js +75 -27
- package/src/security/security-audit-store.js +24 -2
- package/src/server.js +978 -780
- package/src/settings/settings-schema.js +251 -239
- package/src/settings/settings-store.js +82 -77
- package/src/transport/relay-hub-control.js +2 -1
- package/src/transport/secure-direct-acceptor.js +440 -432
- package/src/transport/udp-hub-transport.js +19 -8
- package/src/transport/udp-rendezvous.js +143 -29
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;
|
|
@@ -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) {
|
|
@@ -1635,7 +1635,7 @@ function serializeDevice(device, options = {}) {
|
|
|
1635
1635
|
},
|
|
1636
1636
|
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
1637
1637
|
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
1638
|
-
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1638
|
+
activeLiveStream: serializeActiveLiveStream(device.activeLiveStream, device.deviceId),
|
|
1639
1639
|
liveCapturePause: device.liveCapturePause
|
|
1640
1640
|
? {
|
|
1641
1641
|
token: device.liveCapturePause.token,
|
|
@@ -2242,7 +2242,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2242
2242
|
: null;
|
|
2243
2243
|
const sampleHubRuntimeDiagnostics = createHubRuntimeDiagnosticsSampler();
|
|
2244
2244
|
|
|
2245
|
-
function getDevicePolicy(device) {
|
|
2245
|
+
function getDevicePolicy(device) {
|
|
2246
2246
|
try {
|
|
2247
2247
|
return getEffectiveDevicePolicy({
|
|
2248
2248
|
deviceId: device?.deviceId || '',
|
|
@@ -2250,18 +2250,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2250
2250
|
}) || {};
|
|
2251
2251
|
} catch {
|
|
2252
2252
|
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
|
-
}
|
|
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
|
+
}
|
|
2265
2265
|
|
|
2266
2266
|
function policyError(device, permission = '', command = '') {
|
|
2267
2267
|
const policy = getDevicePolicy(device);
|
|
@@ -2311,82 +2311,83 @@ export function createRemoteHub(options = {}) {
|
|
|
2311
2311
|
const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
|
|
2312
2312
|
const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
|
|
2313
2313
|
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, ttlMs }) => {
|
|
2335
|
-
const identity = getSecurityIdentity() || {};
|
|
2336
|
-
return deviceCredentialAuthority.issueRendezvousProof({
|
|
2337
|
-
roomId,
|
|
2338
|
-
deviceId,
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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())
|
|
2344
2345
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2345
2346
|
: generatePairingPin();
|
|
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
|
-
});
|
|
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
|
+
});
|
|
2390
2391
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2391
2392
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2392
2393
|
5000,
|
|
@@ -2411,8 +2412,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2411
2412
|
const transportDiagnostics = new Map();
|
|
2412
2413
|
const transportDiagnosticStartingDevices = new Set();
|
|
2413
2414
|
const duplicateDeviceLogAt = new Map();
|
|
2414
|
-
let server = null;
|
|
2415
|
-
let controlIdleSweepTimer = null;
|
|
2415
|
+
let server = null;
|
|
2416
|
+
let controlIdleSweepTimer = null;
|
|
2416
2417
|
let started = false;
|
|
2417
2418
|
let boundPort = requestedPort;
|
|
2418
2419
|
let lastError = '';
|
|
@@ -2734,8 +2735,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2734
2735
|
host,
|
|
2735
2736
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2736
2737
|
port: boundPort || requestedPort,
|
|
2737
|
-
protocol: 'secure-record-v1',
|
|
2738
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2738
|
+
protocol: 'secure-record-v1',
|
|
2739
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2739
2740
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2740
2741
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2741
2742
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2751,18 +2752,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2751
2752
|
agentEndpointCandidates: routeInfo.candidates,
|
|
2752
2753
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
2753
2754
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
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
|
-
},
|
|
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
|
+
},
|
|
2766
2767
|
deviceCount: devices.size,
|
|
2767
2768
|
connectedDeviceCount: connectedDevices,
|
|
2768
2769
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -4022,22 +4023,22 @@ export function createRemoteHub(options = {}) {
|
|
|
4022
4023
|
}
|
|
4023
4024
|
}
|
|
4024
4025
|
|
|
4025
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4026
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4026
4027
|
const socket = device?.inputSocket;
|
|
4027
4028
|
if (!socket) {
|
|
4028
4029
|
return;
|
|
4029
4030
|
}
|
|
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;
|
|
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;
|
|
4041
4042
|
device.inputWriteOwner = null;
|
|
4042
4043
|
device.inputSocketConnectionId = '';
|
|
4043
4044
|
device.inputOwnerConnectionId = '';
|
|
@@ -4067,17 +4068,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4067
4068
|
if (!device || device.inputSocket !== socket) {
|
|
4068
4069
|
return;
|
|
4069
4070
|
}
|
|
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;
|
|
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;
|
|
4081
4082
|
device.inputWriteOwner = null;
|
|
4082
4083
|
device.inputSocketConnectionId = '';
|
|
4083
4084
|
device.inputOwnerConnectionId = '';
|
|
@@ -4509,11 +4510,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4509
4510
|
inputWriteOwner: null,
|
|
4510
4511
|
inputSocketConnectionId: '',
|
|
4511
4512
|
inputOwnerConnectionId: '',
|
|
4512
|
-
inputOwnerBindingKey: '',
|
|
4513
|
-
controlOwnerSessionId: '',
|
|
4514
|
-
controlOwnerConnectionId: '',
|
|
4515
|
-
controlOwnerStartedAtMs: 0,
|
|
4516
|
-
controlOwnerLastInputAtMs: 0,
|
|
4513
|
+
inputOwnerBindingKey: '',
|
|
4514
|
+
controlOwnerSessionId: '',
|
|
4515
|
+
controlOwnerConnectionId: '',
|
|
4516
|
+
controlOwnerStartedAtMs: 0,
|
|
4517
|
+
controlOwnerLastInputAtMs: 0,
|
|
4517
4518
|
frameSocket: null,
|
|
4518
4519
|
audioSocket: null,
|
|
4519
4520
|
fileSocket: null,
|
|
@@ -7434,14 +7435,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7434
7435
|
return;
|
|
7435
7436
|
}
|
|
7436
7437
|
|
|
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) {
|
|
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) {
|
|
7445
7446
|
message = {
|
|
7446
7447
|
...message,
|
|
7447
7448
|
capabilities: {
|
|
@@ -7451,14 +7452,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7451
7452
|
dedicatedInputChannel: false
|
|
7452
7453
|
}
|
|
7453
7454
|
};
|
|
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
|
-
}
|
|
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
|
+
}
|
|
7462
7463
|
const result = assignDeviceSlot(
|
|
7463
7464
|
message.deviceId || message.DeviceId,
|
|
7464
7465
|
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
@@ -7472,54 +7473,57 @@ export function createRemoteHub(options = {}) {
|
|
|
7472
7473
|
return;
|
|
7473
7474
|
}
|
|
7474
7475
|
|
|
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
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
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
|
+
const deniedTransport = socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp';
|
|
7509
|
+
const deniedRemoteAddress = safeString(socket.remoteAddress, 256) || 'unknown';
|
|
7510
|
+
logWarn('security', 'Remote connection rejected reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress);
|
|
7511
|
+
writeJsonLine(socket, { type: 'error', error: connectionDenied });
|
|
7512
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
7513
|
+
result: 'rejected',
|
|
7514
|
+
reason: connectionDenied,
|
|
7515
|
+
deviceId: incomingDeviceId,
|
|
7516
|
+
sessionId: safeString(socket.__liveDeskSecurityContext?.sessionId, 160),
|
|
7517
|
+
accountId: safeString(socket.__liveDeskSecurityContext?.accountId, 128),
|
|
7518
|
+
hubId: safeString(socket.__liveDeskSecurityContext?.hubId, 128),
|
|
7519
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7520
|
+
remoteAddress: safeString(socket.remoteAddress, 256)
|
|
7521
|
+
});
|
|
7522
|
+
socket.destroy();
|
|
7523
|
+
return;
|
|
7524
|
+
}
|
|
7525
|
+
|
|
7526
|
+
const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
|
|
7523
7527
|
if (channel === 'input') {
|
|
7524
7528
|
const device = attachInputSocket(socket, message);
|
|
7525
7529
|
if (!device) {
|
|
@@ -7686,27 +7690,27 @@ export function createRemoteHub(options = {}) {
|
|
|
7686
7690
|
}
|
|
7687
7691
|
}
|
|
7688
7692
|
|
|
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) {
|
|
7693
|
+
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7694
|
+
if (state.messageRateGate.consume()) return true;
|
|
7695
|
+
const device = state.device;
|
|
7696
|
+
emitEvent('RemoteAbuseDefense', {
|
|
7697
|
+
result: 'rejected',
|
|
7698
|
+
reason: 'authenticated-message-rate-exceeded',
|
|
7699
|
+
deviceId: device?.deviceId || '',
|
|
7700
|
+
sessionId: device?.sessionId || '',
|
|
7701
|
+
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7702
|
+
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7703
|
+
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7704
|
+
channel,
|
|
7705
|
+
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7706
|
+
rate: state.messageRateGate.getStatus()
|
|
7707
|
+
});
|
|
7708
|
+
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7709
|
+
socket.destroy();
|
|
7710
|
+
return false;
|
|
7711
|
+
}
|
|
7712
|
+
|
|
7713
|
+
function handleWebSocketAgentSocket(socket) {
|
|
7710
7714
|
allSockets.add(socket);
|
|
7711
7715
|
socket.setNoDelay(true);
|
|
7712
7716
|
socket.setKeepAlive(true, heartbeatMs);
|
|
@@ -7720,11 +7724,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7720
7724
|
audioOnly: false,
|
|
7721
7725
|
fileOnly: false,
|
|
7722
7726
|
parserBuffer: socket.buffer,
|
|
7723
|
-
binaryIngress: null,
|
|
7724
|
-
binaryQueueDrops: 0,
|
|
7725
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7726
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7727
|
-
closed: false
|
|
7727
|
+
binaryIngress: null,
|
|
7728
|
+
binaryQueueDrops: 0,
|
|
7729
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7730
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7731
|
+
closed: false
|
|
7728
7732
|
};
|
|
7729
7733
|
attachAgentBinaryIngress(socket, state);
|
|
7730
7734
|
|
|
@@ -7735,9 +7739,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7735
7739
|
}
|
|
7736
7740
|
}, 10000);
|
|
7737
7741
|
|
|
7738
|
-
socket.onTextMessage = text => {
|
|
7739
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7740
|
-
try {
|
|
7742
|
+
socket.onTextMessage = text => {
|
|
7743
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7744
|
+
try {
|
|
7741
7745
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7742
7746
|
} catch (err) {
|
|
7743
7747
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
@@ -7745,9 +7749,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7745
7749
|
}
|
|
7746
7750
|
};
|
|
7747
7751
|
|
|
7748
|
-
socket.onBinaryMessage = payload => {
|
|
7749
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7750
|
-
try {
|
|
7752
|
+
socket.onBinaryMessage = payload => {
|
|
7753
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7754
|
+
try {
|
|
7751
7755
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7752
7756
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
7753
7757
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
@@ -7902,11 +7906,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7902
7906
|
buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
|
|
7903
7907
|
lineScanOffset: 0,
|
|
7904
7908
|
pendingBinaryFrame: null,
|
|
7905
|
-
binaryIngress: null,
|
|
7906
|
-
binaryQueueDrops: 0,
|
|
7907
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7908
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7909
|
-
closed: false
|
|
7909
|
+
binaryIngress: null,
|
|
7910
|
+
binaryQueueDrops: 0,
|
|
7911
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7912
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7913
|
+
closed: false
|
|
7910
7914
|
};
|
|
7911
7915
|
attachAgentBinaryIngress(socket, state);
|
|
7912
7916
|
|
|
@@ -8021,18 +8025,18 @@ export function createRemoteHub(options = {}) {
|
|
|
8021
8025
|
return;
|
|
8022
8026
|
}
|
|
8023
8027
|
|
|
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') {
|
|
8028
|
+
try {
|
|
8029
|
+
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8030
|
+
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8031
|
+
? 'frame'
|
|
8032
|
+
: state.audioOnly
|
|
8033
|
+
? 'audio'
|
|
8034
|
+
: state.inputOnly
|
|
8035
|
+
? 'input'
|
|
8036
|
+
: state.fileOnly ? 'file' : 'control')) {
|
|
8037
|
+
return;
|
|
8038
|
+
}
|
|
8039
|
+
if (message?.type === 'frame.binary') {
|
|
8036
8040
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
8037
8041
|
const maxBytes = frameKind === 'thumbnail'
|
|
8038
8042
|
? MAX_THUMBNAIL_BINARY_BYTES
|
|
@@ -8086,35 +8090,35 @@ export function createRemoteHub(options = {}) {
|
|
|
8086
8090
|
}
|
|
8087
8091
|
}
|
|
8088
8092
|
|
|
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
|
-
});
|
|
8093
|
+
function handleSocket(socket) {
|
|
8094
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8095
|
+
secureDirectAcceptor.accept(socket);
|
|
8096
|
+
return;
|
|
8097
|
+
}
|
|
8098
|
+
socket.once('data', chunk => {
|
|
8099
|
+
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8100
|
+
if (isSecureHandshakeStart(firstChunk)) {
|
|
8101
|
+
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
8104
|
+
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8105
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8106
|
+
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8107
|
+
socket.destroy();
|
|
8108
|
+
return;
|
|
8109
|
+
}
|
|
8110
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8111
|
+
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8112
|
+
return;
|
|
8113
|
+
}
|
|
8114
|
+
if (!allowLegacyPlaintextForTests) {
|
|
8115
|
+
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8116
|
+
socket.destroy();
|
|
8117
|
+
return;
|
|
8118
|
+
}
|
|
8119
|
+
socket.__liveDeskLegacyPlaintext = true;
|
|
8120
|
+
handleTcpAgentSocket(socket, firstChunk);
|
|
8121
|
+
});
|
|
8118
8122
|
|
|
8119
8123
|
socket.once('error', () => {
|
|
8120
8124
|
// The transport-specific handler owns logging after the first byte.
|
|
@@ -8136,10 +8140,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8136
8140
|
started = true;
|
|
8137
8141
|
boundPort = candidateServer.address()?.port || port;
|
|
8138
8142
|
lastError = '';
|
|
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
|
-
}
|
|
8143
|
+
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8144
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
8145
|
+
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8146
|
+
}
|
|
8143
8147
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8144
8148
|
resolve();
|
|
8145
8149
|
});
|
|
@@ -8151,14 +8155,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8151
8155
|
return getStatus({ includeSecrets: false });
|
|
8152
8156
|
}
|
|
8153
8157
|
|
|
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 {
|
|
8158
|
+
try {
|
|
8159
|
+
await udpTransport?.start?.();
|
|
8160
|
+
await listenOnPort(requestedPort);
|
|
8161
|
+
if (!controlIdleSweepTimer) {
|
|
8162
|
+
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8163
|
+
controlIdleSweepTimer.unref?.();
|
|
8164
|
+
}
|
|
8165
|
+
try {
|
|
8162
8166
|
await relayControl?.start?.();
|
|
8163
8167
|
} catch {
|
|
8164
8168
|
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
@@ -8179,12 +8183,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8179
8183
|
return getStatus({ includeSecrets: false });
|
|
8180
8184
|
}
|
|
8181
8185
|
|
|
8182
|
-
async function close() {
|
|
8183
|
-
if (controlIdleSweepTimer) {
|
|
8184
|
-
clearInterval(controlIdleSweepTimer);
|
|
8185
|
-
controlIdleSweepTimer = null;
|
|
8186
|
-
}
|
|
8187
|
-
for (const state of [...agentBinaryIngressStates]) {
|
|
8186
|
+
async function close() {
|
|
8187
|
+
if (controlIdleSweepTimer) {
|
|
8188
|
+
clearInterval(controlIdleSweepTimer);
|
|
8189
|
+
controlIdleSweepTimer = null;
|
|
8190
|
+
}
|
|
8191
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
8188
8192
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8189
8193
|
}
|
|
8190
8194
|
|
|
@@ -8390,7 +8394,61 @@ export function createRemoteHub(options = {}) {
|
|
|
8390
8394
|
return { ok: true, commandId };
|
|
8391
8395
|
}
|
|
8392
8396
|
|
|
8393
|
-
function
|
|
8397
|
+
async function sendCommandAwaitResult(deviceId, command, options = {}) {
|
|
8398
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
8399
|
+
const device = devices.get(normalizedDeviceId);
|
|
8400
|
+
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8401
|
+
const commandWithId = { ...(command || {}), commandId };
|
|
8402
|
+
|
|
8403
|
+
if (device?.synthetic === true && device.connected) {
|
|
8404
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8405
|
+
return {
|
|
8406
|
+
...sent,
|
|
8407
|
+
queued: sent.ok === true,
|
|
8408
|
+
acknowledged: sent.ok === true,
|
|
8409
|
+
acknowledgement: sent.ok === true
|
|
8410
|
+
? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
|
|
8411
|
+
: { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
|
|
8412
|
+
};
|
|
8413
|
+
}
|
|
8414
|
+
|
|
8415
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8416
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8417
|
+
return {
|
|
8418
|
+
...sent,
|
|
8419
|
+
queued: false,
|
|
8420
|
+
acknowledged: false,
|
|
8421
|
+
acknowledgement: {
|
|
8422
|
+
ok: false,
|
|
8423
|
+
commandId,
|
|
8424
|
+
result: null,
|
|
8425
|
+
error: sent.error || 'device-not-connected'
|
|
8426
|
+
}
|
|
8427
|
+
};
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
|
|
8431
|
+
// Register before writing: a loopback Agent can return command.result in
|
|
8432
|
+
// the same event-loop turn as the command write.
|
|
8433
|
+
const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
|
|
8434
|
+
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8435
|
+
if (!sent.ok) {
|
|
8436
|
+
failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
|
|
8437
|
+
}
|
|
8438
|
+
const acknowledgement = await acknowledgementPromise;
|
|
8439
|
+
return {
|
|
8440
|
+
...sent,
|
|
8441
|
+
ok: sent.ok === true && acknowledgement.ok === true,
|
|
8442
|
+
queued: sent.ok === true,
|
|
8443
|
+
acknowledged: acknowledgement.ok === true,
|
|
8444
|
+
acknowledgement,
|
|
8445
|
+
error: acknowledgement.ok === true
|
|
8446
|
+
? undefined
|
|
8447
|
+
: acknowledgement.error || sent.error || 'command-result-failed'
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
8450
|
+
|
|
8451
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8394
8452
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8395
8453
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8396
8454
|
: null;
|
|
@@ -8399,31 +8457,34 @@ export function createRemoteHub(options = {}) {
|
|
|
8399
8457
|
for (const device of devices.values()) {
|
|
8400
8458
|
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8401
8459
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8402
|
-
total += 1;
|
|
8403
|
-
const effectivePolicy = getDevicePolicy(device);
|
|
8404
|
-
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8405
|
-
if (connectionDenied) {
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
}
|
|
8426
|
-
if (
|
|
8460
|
+
total += 1;
|
|
8461
|
+
const effectivePolicy = getDevicePolicy(device);
|
|
8462
|
+
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8463
|
+
if (connectionDenied) {
|
|
8464
|
+
const deniedTransport = device.controlTransport || 'direct-tcp';
|
|
8465
|
+
const deniedRemoteAddress = safeString(device.remoteAddress, 256) || 'unknown';
|
|
8466
|
+
logWarn('security', 'Connected device rejected by refreshed policy reason=' + connectionDenied + ' transport=' + deniedTransport + ' remote=' + deniedRemoteAddress + ' device=' + device.deviceId);
|
|
8467
|
+
emitEvent('RemoteSecurityPolicyAudit', {
|
|
8468
|
+
result: 'rejected',
|
|
8469
|
+
reason: connectionDenied,
|
|
8470
|
+
deviceId: device.deviceId,
|
|
8471
|
+
sessionId: device.sessionId,
|
|
8472
|
+
transport: device.controlTransport,
|
|
8473
|
+
remoteAddress: device.remoteAddress
|
|
8474
|
+
});
|
|
8475
|
+
disconnectDevice(device.deviceId, connectionDenied);
|
|
8476
|
+
continue;
|
|
8477
|
+
}
|
|
8478
|
+
if (effectivePolicy.allowControl !== true && device.controlOwnerConnectionId) {
|
|
8479
|
+
releaseInputOwner(
|
|
8480
|
+
device.deviceId,
|
|
8481
|
+
device.controlOwnerConnectionId,
|
|
8482
|
+
'control-disabled-by-policy');
|
|
8483
|
+
}
|
|
8484
|
+
if (effectivePolicy.allowRemoteAudio !== true && device.activeAudioStream) {
|
|
8485
|
+
stopAudioStream(device.deviceId, { reason: 'remote-audio-disabled-by-policy' });
|
|
8486
|
+
}
|
|
8487
|
+
if (writeJsonLine(device.socket, {
|
|
8427
8488
|
type: 'policy.update',
|
|
8428
8489
|
effectivePolicy,
|
|
8429
8490
|
updatedAt: new Date().toISOString()
|
|
@@ -8502,7 +8563,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8502
8563
|
]);
|
|
8503
8564
|
}
|
|
8504
8565
|
|
|
8505
|
-
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8566
|
+
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8506
8567
|
return {
|
|
8507
8568
|
type: 'input.control',
|
|
8508
8569
|
payload: {
|
|
@@ -8524,114 +8585,114 @@ export function createRemoteHub(options = {}) {
|
|
|
8524
8585
|
hubForwardedAtEpochMs: Date.now(),
|
|
8525
8586
|
issuedAt: new Date().toISOString()
|
|
8526
8587
|
}
|
|
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
|
-
}
|
|
8588
|
+
};
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8592
|
+
const policy = getDevicePolicy(device);
|
|
8593
|
+
return {
|
|
8594
|
+
type,
|
|
8595
|
+
payload: {
|
|
8596
|
+
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8597
|
+
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8598
|
+
deviceId: safeString(device?.deviceId, 160),
|
|
8599
|
+
visibleIndicator: true,
|
|
8600
|
+
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8601
|
+
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8602
|
+
reason: safeString(reason, 160),
|
|
8603
|
+
issuedAt: new Date().toISOString()
|
|
8604
|
+
}
|
|
8605
|
+
};
|
|
8606
|
+
}
|
|
8607
|
+
|
|
8608
|
+
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8609
|
+
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8610
|
+
if (inputWriteOwner && bindingKey) {
|
|
8611
|
+
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8612
|
+
}
|
|
8613
|
+
if (!device?.socket || device.socket.destroyed) {
|
|
8614
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8615
|
+
}
|
|
8616
|
+
const sent = writeJsonLine(device.socket, {
|
|
8617
|
+
type: 'command',
|
|
8618
|
+
commandId: crypto.randomUUID(),
|
|
8619
|
+
command: type,
|
|
8620
|
+
payload: message.payload,
|
|
8621
|
+
issuedAt: new Date().toISOString()
|
|
8622
|
+
});
|
|
8623
|
+
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8624
|
+
}
|
|
8625
|
+
|
|
8626
|
+
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8627
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8628
|
+
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8629
|
+
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8630
|
+
return { ok: true, alreadyActive: true };
|
|
8631
|
+
}
|
|
8632
|
+
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8633
|
+
device.controlOwnerConnectionId = owner;
|
|
8634
|
+
device.controlOwnerStartedAtMs = Date.now();
|
|
8635
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8636
|
+
const sent = sendControlSessionMessage(
|
|
8637
|
+
device,
|
|
8638
|
+
owner,
|
|
8639
|
+
'control.session.start',
|
|
8640
|
+
'browser-control-owner-active',
|
|
8641
|
+
inputWriteOwner,
|
|
8642
|
+
bindingKey);
|
|
8643
|
+
if (!sent.ok) {
|
|
8644
|
+
device.controlOwnerSessionId = '';
|
|
8645
|
+
device.controlOwnerConnectionId = '';
|
|
8646
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8647
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8648
|
+
return sent;
|
|
8649
|
+
}
|
|
8650
|
+
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8651
|
+
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8652
|
+
hubConnectionId: owner
|
|
8653
|
+
});
|
|
8654
|
+
return sent;
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8658
|
+
const owner = safeString(ownerConnectionId, 128);
|
|
8659
|
+
if (!device?.controlOwnerSessionId
|
|
8660
|
+
|| !owner
|
|
8661
|
+
|| device.controlOwnerConnectionId !== owner) {
|
|
8662
|
+
return { ok: false, error: 'control-owner-not-current' };
|
|
8663
|
+
}
|
|
8664
|
+
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8665
|
+
const sent = sendControlSessionMessage(
|
|
8666
|
+
device,
|
|
8667
|
+
owner,
|
|
8668
|
+
'control.session.stop',
|
|
8669
|
+
reason,
|
|
8670
|
+
inputWriteOwner,
|
|
8671
|
+
bindingKey);
|
|
8672
|
+
device.controlOwnerSessionId = '';
|
|
8673
|
+
device.controlOwnerConnectionId = '';
|
|
8674
|
+
device.controlOwnerStartedAtMs = 0;
|
|
8675
|
+
device.controlOwnerLastInputAtMs = 0;
|
|
8676
|
+
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8677
|
+
controlOwnerSessionId,
|
|
8678
|
+
hubConnectionId: owner,
|
|
8679
|
+
reason,
|
|
8680
|
+
delivered: sent.ok === true
|
|
8681
|
+
});
|
|
8682
|
+
return sent;
|
|
8683
|
+
}
|
|
8684
|
+
|
|
8685
|
+
function sweepIdleControlSessions() {
|
|
8686
|
+
const nowMs = Date.now();
|
|
8687
|
+
for (const device of devices.values()) {
|
|
8688
|
+
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8689
|
+
const policy = getDevicePolicy(device);
|
|
8690
|
+
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8691
|
+
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8692
|
+
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8693
|
+
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8694
|
+
}
|
|
8695
|
+
}
|
|
8635
8696
|
|
|
8636
8697
|
function getCurrentInputWriteOwner(device) {
|
|
8637
8698
|
if (!device?.inputSocket
|
|
@@ -8774,16 +8835,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8774
8835
|
normalized,
|
|
8775
8836
|
activeMonitorIndex);
|
|
8776
8837
|
const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
|
|
8777
|
-
const ownerChanged = normalized.hubConnectionId
|
|
8838
|
+
const ownerChanged = normalized.hubConnectionId
|
|
8778
8839
|
&& previousOwnerConnectionId
|
|
8779
8840
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8780
8841
|
const bindingChanged = previousInputBindingKey
|
|
8781
8842
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8782
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8783
|
-
const startingControlOwner = !!normalized.hubConnectionId
|
|
8784
|
-
&& (!device.controlOwnerConnectionId
|
|
8785
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8786
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8843
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8844
|
+
const startingControlOwner = !!normalized.hubConnectionId
|
|
8845
|
+
&& (!device.controlOwnerConnectionId
|
|
8846
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8847
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8787
8848
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8788
8849
|
activeInputBindingKey,
|
|
8789
8850
|
buildRemoteInputResetMessage(
|
|
@@ -8798,28 +8859,28 @@ export function createRemoteHub(options = {}) {
|
|
|
8798
8859
|
activeMonitorIndex));
|
|
8799
8860
|
if (!resetSent.ok) {
|
|
8800
8861
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
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
|
-
}
|
|
8862
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8863
|
+
}
|
|
8864
|
+
if (startingControlOwner) {
|
|
8865
|
+
if (device.controlOwnerConnectionId) {
|
|
8866
|
+
endControlSession(
|
|
8867
|
+
device,
|
|
8868
|
+
device.controlOwnerConnectionId,
|
|
8869
|
+
'browser-input-owner-changed',
|
|
8870
|
+
inputWriteOwner,
|
|
8871
|
+
activeInputBindingKey);
|
|
8872
|
+
}
|
|
8873
|
+
const started = beginControlSession(
|
|
8874
|
+
device,
|
|
8875
|
+
normalized.hubConnectionId,
|
|
8876
|
+
inputWriteOwner,
|
|
8877
|
+
activeInputBindingKey);
|
|
8878
|
+
if (!started.ok) {
|
|
8879
|
+
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8880
|
+
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8823
8884
|
const sent = inputWriteOwner.enqueue({
|
|
8824
8885
|
type: 'input.control',
|
|
8825
8886
|
payload: {
|
|
@@ -8832,8 +8893,8 @@ export function createRemoteHub(options = {}) {
|
|
|
8832
8893
|
if (normalized.hubConnectionId) {
|
|
8833
8894
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8834
8895
|
}
|
|
8835
|
-
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8836
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8896
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8897
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8837
8898
|
device.counters.commandsSent += 1;
|
|
8838
8899
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8839
8900
|
return {
|
|
@@ -8862,27 +8923,27 @@ export function createRemoteHub(options = {}) {
|
|
|
8862
8923
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8863
8924
|
}
|
|
8864
8925
|
|
|
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, {
|
|
8926
|
+
const hubForwardedAtEpochMs = Date.now();
|
|
8927
|
+
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8928
|
+
&& (!device.controlOwnerConnectionId
|
|
8929
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8930
|
+
if (startingFallbackOwner) {
|
|
8931
|
+
if (device.controlOwnerConnectionId) {
|
|
8932
|
+
endControlSession(
|
|
8933
|
+
device,
|
|
8934
|
+
device.controlOwnerConnectionId,
|
|
8935
|
+
'browser-input-owner-changed',
|
|
8936
|
+
null,
|
|
8937
|
+
'');
|
|
8938
|
+
}
|
|
8939
|
+
const started = beginControlSession(
|
|
8940
|
+
device,
|
|
8941
|
+
normalized.hubConnectionId,
|
|
8942
|
+
null,
|
|
8943
|
+
'');
|
|
8944
|
+
if (!started.ok) return started;
|
|
8945
|
+
}
|
|
8946
|
+
const fallback = sendCommand(deviceId, {
|
|
8886
8947
|
command: 'input.control',
|
|
8887
8948
|
payload: {
|
|
8888
8949
|
...normalized,
|
|
@@ -8890,7 +8951,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8890
8951
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8891
8952
|
}
|
|
8892
8953
|
});
|
|
8893
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8954
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8894
8955
|
schedulePendingInputFallback(device, {
|
|
8895
8956
|
commandId: fallback.commandId,
|
|
8896
8957
|
inputSeq: normalized.inputSeq,
|
|
@@ -8902,26 +8963,26 @@ export function createRemoteHub(options = {}) {
|
|
|
8902
8963
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8903
8964
|
hubForwardedAtEpochMs,
|
|
8904
8965
|
fallback: true,
|
|
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
|
|
8966
|
+
timeoutTimer: null
|
|
8967
|
+
});
|
|
8968
|
+
}
|
|
8969
|
+
if (fallback.ok) {
|
|
8970
|
+
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8971
|
+
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
8972
|
+
device,
|
|
8973
|
+
controlStream,
|
|
8974
|
+
normalized,
|
|
8975
|
+
activeMonitorIndex);
|
|
8976
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8977
|
+
} else if (startingFallbackOwner) {
|
|
8978
|
+
endControlSession(
|
|
8979
|
+
device,
|
|
8980
|
+
normalized.hubConnectionId,
|
|
8981
|
+
'input-control-delivery-failed',
|
|
8982
|
+
null,
|
|
8983
|
+
'');
|
|
8984
|
+
}
|
|
8985
|
+
return fallback.ok
|
|
8925
8986
|
? {
|
|
8926
8987
|
...fallback,
|
|
8927
8988
|
inputSocket: false,
|
|
@@ -8947,20 +9008,20 @@ export function createRemoteHub(options = {}) {
|
|
|
8947
9008
|
return { ok: false, error: 'input-owner-not-current' };
|
|
8948
9009
|
}
|
|
8949
9010
|
|
|
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);
|
|
9011
|
+
const inputSocket = device.inputSocket;
|
|
9012
|
+
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9013
|
+
if (!inputSocket || !inputWriteOwner) {
|
|
9014
|
+
const ended = endControlSession(device, owner, reason, null, '');
|
|
9015
|
+
device.inputOwnerConnectionId = '';
|
|
9016
|
+
device.inputOwnerBindingKey = '';
|
|
9017
|
+
return {
|
|
9018
|
+
ok: ended.ok !== false,
|
|
9019
|
+
queued: false,
|
|
9020
|
+
controlSessionEnded: ended.ok === true
|
|
9021
|
+
};
|
|
9022
|
+
}
|
|
9023
|
+
const controlStream = getActiveControlStream(device);
|
|
9024
|
+
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
8964
9025
|
const sent = inputWriteOwner.replaceBinding(
|
|
8965
9026
|
releaseBindingKey,
|
|
8966
9027
|
buildRemoteInputResetMessage(
|
|
@@ -8969,24 +9030,24 @@ export function createRemoteHub(options = {}) {
|
|
|
8969
9030
|
owner,
|
|
8970
9031
|
reason,
|
|
8971
9032
|
normalizeMonitorIndex(controlStream?.monitorIndex)));
|
|
8972
|
-
if (!sent.ok) {
|
|
9033
|
+
if (!sent.ok) {
|
|
8973
9034
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
8974
9035
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
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
|
-
};
|
|
9036
|
+
}
|
|
9037
|
+
const ended = endControlSession(
|
|
9038
|
+
device,
|
|
9039
|
+
owner,
|
|
9040
|
+
reason,
|
|
9041
|
+
inputWriteOwner,
|
|
9042
|
+
releaseBindingKey);
|
|
9043
|
+
device.inputOwnerConnectionId = '';
|
|
9044
|
+
device.inputOwnerBindingKey = '';
|
|
9045
|
+
return {
|
|
9046
|
+
ok: ended.ok !== false,
|
|
9047
|
+
queued: true,
|
|
9048
|
+
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
9049
|
+
controlSessionEnded: ended.ok === true
|
|
9050
|
+
};
|
|
8990
9051
|
}
|
|
8991
9052
|
|
|
8992
9053
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -9026,12 +9087,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9026
9087
|
if (retiredRequestError) {
|
|
9027
9088
|
return { ok: false, error: retiredRequestError };
|
|
9028
9089
|
}
|
|
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);
|
|
9090
|
+
const device = devices.get(String(deviceId || ''));
|
|
9091
|
+
const requestedOperation = safeString(options.operation, 80);
|
|
9092
|
+
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9093
|
+
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9094
|
+
}
|
|
9095
|
+
const operation = normalizeAgentOperation(requestedOperation);
|
|
9035
9096
|
if (requestedOperation && !operation) {
|
|
9036
9097
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
9037
9098
|
}
|
|
@@ -11279,6 +11340,7 @@ export function createRemoteHub(options = {}) {
|
|
|
11279
11340
|
disconnectDevice,
|
|
11280
11341
|
assignDeviceSlot,
|
|
11281
11342
|
sendCommand,
|
|
11343
|
+
sendCommandAwaitResult,
|
|
11282
11344
|
refreshDevicePolicies,
|
|
11283
11345
|
sendLegacyClientUpdate,
|
|
11284
11346
|
sendInputControl,
|
|
@@ -11306,16 +11368,17 @@ export function createRemoteHub(options = {}) {
|
|
|
11306
11368
|
handleUdpFrame,
|
|
11307
11369
|
seedSyntheticFleet,
|
|
11308
11370
|
clearSyntheticFleet,
|
|
11309
|
-
getPairToken: () => pairToken,
|
|
11310
|
-
getPairingPin: () => pairingPin,
|
|
11311
|
-
getSecurityStatus: () => ({
|
|
11312
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
11313
|
-
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11371
|
+
getPairToken: () => pairToken,
|
|
11372
|
+
getPairingPin: () => pairingPin,
|
|
11373
|
+
getSecurityStatus: () => ({
|
|
11374
|
+
hubId: deviceCredentialAuthority.hubId,
|
|
11375
|
+
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11376
|
+
hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
|
|
11377
|
+
direct: secureDirectAcceptor.getStatus(),
|
|
11378
|
+
devices: deviceCredentialAuthority.listDevices()
|
|
11379
|
+
}),
|
|
11380
|
+
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11381
|
+
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
11319
11382
|
};
|
|
11320
11383
|
}
|
|
11321
11384
|
|