@livedesk/hub 0.1.32 → 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/remote-hub.js +728 -722
- package/src/server.js +1006 -979
- package/src/settings/settings-schema.js +251 -239
- package/src/settings/settings-store.js +82 -77
- package/src/transport/secure-direct-acceptor.js +440 -433
package/src/remote-hub.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import net from 'net';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import { isSecureHandshakeStart } from '@livedesk/runtime-core';
|
|
5
|
-
import { createDeviceCredentialAuthority } from './security/device-credential-authority.js';
|
|
6
|
-
import { createHubRelayControl, createSecureHubRelayControl } from './transport/relay-hub-control.js';
|
|
7
|
-
import { createSecureDirectAcceptor } from './transport/secure-direct-acceptor.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { isSecureHandshakeStart } from '@livedesk/runtime-core';
|
|
5
|
+
import { createDeviceCredentialAuthority } from './security/device-credential-authority.js';
|
|
6
|
+
import { createHubRelayControl, createSecureHubRelayControl } from './transport/relay-hub-control.js';
|
|
7
|
+
import { createSecureDirectAcceptor } from './transport/secure-direct-acceptor.js';
|
|
8
8
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
9
9
|
import {
|
|
10
10
|
BoundedSegmentedBuffer,
|
|
@@ -35,10 +35,10 @@ const MAX_AGENT_TCP_BINARY_PACKET_BYTES = TCP_BINARY_FRAME_HEADER_BYTES
|
|
|
35
35
|
const MAX_AGENT_WEBSOCKET_MESSAGE_BYTES = 4
|
|
36
36
|
+ MAX_AGENT_BINARY_META_BYTES
|
|
37
37
|
+ MAX_AGENT_BINARY_PAYLOAD_BYTES;
|
|
38
|
-
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
|
-
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
|
-
+ (256 * 1024);
|
|
41
|
-
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
38
|
+
const MAX_AGENT_SOCKET_ACCUMULATOR_BYTES = MAX_LINE_CHARS
|
|
39
|
+
+ MAX_AGENT_TCP_BINARY_PACKET_BYTES
|
|
40
|
+
+ (256 * 1024);
|
|
41
|
+
const MAX_AGENT_MESSAGES_PER_SECOND = 240;
|
|
42
42
|
const HTTP_HEADER_TERMINATOR = Buffer.from('\r\n\r\n', 'ascii');
|
|
43
43
|
const MAX_AGENT_TASK_CHARS = 4000;
|
|
44
44
|
const MAX_AGENT_TASK_RESULT_CHARS = 3000;
|
|
@@ -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,83 +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, 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())
|
|
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())
|
|
2345
2345
|
? String(options.pairingPin || env.LIVEDESK_PAIRING_PIN).trim()
|
|
2346
2346
|
: generatePairingPin();
|
|
2347
|
-
let pairingPinUpdatedAt = new Date().toISOString();
|
|
2348
|
-
const consumeEnrollmentToken = expected => {
|
|
2349
|
-
if (!timingSafeStringEqual(expected, pairToken)) return false;
|
|
2350
|
-
pairToken = crypto.randomBytes(32).toString('base64url');
|
|
2351
|
-
pairingPin = generatePairingPin();
|
|
2352
|
-
pairingPinUpdatedAt = new Date().toISOString();
|
|
2353
|
-
emitRemoteEvent('RemoteEnrollmentTokenRotated', null, {
|
|
2354
|
-
pairingPinUpdatedAt,
|
|
2355
|
-
hubId: deviceCredentialAuthority.hubId
|
|
2356
|
-
});
|
|
2357
|
-
return true;
|
|
2358
|
-
};
|
|
2359
|
-
const relayControl = options.relayControl && typeof options.relayControl === 'object'
|
|
2360
|
-
? options.relayControl
|
|
2361
|
-
: allowLegacyPlaintextForTests
|
|
2362
|
-
? createHubRelayControl({
|
|
2363
|
-
env,
|
|
2364
|
-
pairToken,
|
|
2365
|
-
logEvent,
|
|
2366
|
-
logWarn,
|
|
2367
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2368
|
-
})
|
|
2369
|
-
: createSecureHubRelayControl({
|
|
2370
|
-
env,
|
|
2371
|
-
authority: deviceCredentialAuthority,
|
|
2372
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2373
|
-
getEnrollmentToken: () => pairToken,
|
|
2374
|
-
consumeEnrollmentToken,
|
|
2375
|
-
logEvent,
|
|
2376
|
-
logWarn,
|
|
2377
|
-
onPeerSocket: socket => handleTcpAgentSocket(socket)
|
|
2378
|
-
});
|
|
2379
|
-
const secureDirectAcceptor = createSecureDirectAcceptor({
|
|
2380
|
-
authority: deviceCredentialAuthority,
|
|
2381
|
-
getIdentity: () => getSecurityIdentity() || {},
|
|
2382
|
-
getEnrollmentToken: () => pairToken,
|
|
2383
|
-
consumeEnrollmentToken,
|
|
2384
|
-
onSecureSocket: socket => handleTcpAgentSocket(socket),
|
|
2385
|
-
onAudit: audit => emitEvent('RemoteSecurityHandshakeAudit', {
|
|
2386
|
-
...audit,
|
|
2387
|
-
remoteHub: getStatus({ includeSecrets: false })
|
|
2388
|
-
}),
|
|
2389
|
-
logWarn
|
|
2390
|
-
});
|
|
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
|
+
});
|
|
2391
2391
|
const duplicateDeviceLogThrottleMs = clampNumber(
|
|
2392
2392
|
env.MINDEXEC_REMOTE_DUPLICATE_DEVICE_LOG_MS || env.REMOTE_HUB_DUPLICATE_DEVICE_LOG_MS,
|
|
2393
2393
|
5000,
|
|
@@ -2412,8 +2412,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2412
2412
|
const transportDiagnostics = new Map();
|
|
2413
2413
|
const transportDiagnosticStartingDevices = new Set();
|
|
2414
2414
|
const duplicateDeviceLogAt = new Map();
|
|
2415
|
-
let server = null;
|
|
2416
|
-
let controlIdleSweepTimer = null;
|
|
2415
|
+
let server = null;
|
|
2416
|
+
let controlIdleSweepTimer = null;
|
|
2417
2417
|
let started = false;
|
|
2418
2418
|
let boundPort = requestedPort;
|
|
2419
2419
|
let lastError = '';
|
|
@@ -2735,8 +2735,8 @@ export function createRemoteHub(options = {}) {
|
|
|
2735
2735
|
host,
|
|
2736
2736
|
agentHost: routeInfo.host || getAnnouncedHost(),
|
|
2737
2737
|
port: boundPort || requestedPort,
|
|
2738
|
-
protocol: 'secure-record-v1',
|
|
2739
|
-
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2738
|
+
protocol: 'secure-record-v1',
|
|
2739
|
+
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2740
2740
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2741
2741
|
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2742
2742
|
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
@@ -2752,18 +2752,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2752
2752
|
agentEndpointCandidates: routeInfo.candidates,
|
|
2753
2753
|
agentEndpointCandidateDetails: routeInfo.candidateDetails,
|
|
2754
2754
|
pairToken: includeSecrets ? pairToken : undefined,
|
|
2755
|
-
pairTokenPreview: maskToken(pairToken),
|
|
2756
|
-
pairingPin: includeSecrets ? pairingPin : '',
|
|
2757
|
-
pairingPinUpdatedAt,
|
|
2758
|
-
security: {
|
|
2759
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
2760
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
2761
|
-
hubPublicKeyFingerprint: crypto.createHash('sha256')
|
|
2762
|
-
.update(Buffer.from(deviceCredentialAuthority.hubPublicKey, 'base64url'))
|
|
2763
|
-
.digest('hex'),
|
|
2764
|
-
enrolledDeviceCount: deviceCredentialAuthority.listDevices().filter(item => !item.revokedAt).length,
|
|
2765
|
-
legacyPlaintextForTests: allowLegacyPlaintextForTests
|
|
2766
|
-
},
|
|
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
|
+
},
|
|
2767
2767
|
deviceCount: devices.size,
|
|
2768
2768
|
connectedDeviceCount: connectedDevices,
|
|
2769
2769
|
canvasDeviceListMode: 'all-devices',
|
|
@@ -4023,22 +4023,22 @@ export function createRemoteHub(options = {}) {
|
|
|
4023
4023
|
}
|
|
4024
4024
|
}
|
|
4025
4025
|
|
|
4026
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4026
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4027
4027
|
const socket = device?.inputSocket;
|
|
4028
4028
|
if (!socket) {
|
|
4029
4029
|
return;
|
|
4030
4030
|
}
|
|
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;
|
|
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;
|
|
4042
4042
|
device.inputWriteOwner = null;
|
|
4043
4043
|
device.inputSocketConnectionId = '';
|
|
4044
4044
|
device.inputOwnerConnectionId = '';
|
|
@@ -4068,17 +4068,17 @@ export function createRemoteHub(options = {}) {
|
|
|
4068
4068
|
if (!device || device.inputSocket !== socket) {
|
|
4069
4069
|
return;
|
|
4070
4070
|
}
|
|
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;
|
|
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;
|
|
4082
4082
|
device.inputWriteOwner = null;
|
|
4083
4083
|
device.inputSocketConnectionId = '';
|
|
4084
4084
|
device.inputOwnerConnectionId = '';
|
|
@@ -4510,11 +4510,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4510
4510
|
inputWriteOwner: null,
|
|
4511
4511
|
inputSocketConnectionId: '',
|
|
4512
4512
|
inputOwnerConnectionId: '',
|
|
4513
|
-
inputOwnerBindingKey: '',
|
|
4514
|
-
controlOwnerSessionId: '',
|
|
4515
|
-
controlOwnerConnectionId: '',
|
|
4516
|
-
controlOwnerStartedAtMs: 0,
|
|
4517
|
-
controlOwnerLastInputAtMs: 0,
|
|
4513
|
+
inputOwnerBindingKey: '',
|
|
4514
|
+
controlOwnerSessionId: '',
|
|
4515
|
+
controlOwnerConnectionId: '',
|
|
4516
|
+
controlOwnerStartedAtMs: 0,
|
|
4517
|
+
controlOwnerLastInputAtMs: 0,
|
|
4518
4518
|
frameSocket: null,
|
|
4519
4519
|
audioSocket: null,
|
|
4520
4520
|
fileSocket: null,
|
|
@@ -7435,14 +7435,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7435
7435
|
return;
|
|
7436
7436
|
}
|
|
7437
7437
|
|
|
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) {
|
|
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) {
|
|
7446
7446
|
message = {
|
|
7447
7447
|
...message,
|
|
7448
7448
|
capabilities: {
|
|
@@ -7452,14 +7452,14 @@ export function createRemoteHub(options = {}) {
|
|
|
7452
7452
|
dedicatedInputChannel: false
|
|
7453
7453
|
}
|
|
7454
7454
|
};
|
|
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
|
-
}
|
|
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
|
+
}
|
|
7463
7463
|
const result = assignDeviceSlot(
|
|
7464
7464
|
message.deviceId || message.DeviceId,
|
|
7465
7465
|
message.slotNumber ?? message.slot ?? message.SlotNumber);
|
|
@@ -7473,54 +7473,57 @@ export function createRemoteHub(options = {}) {
|
|
|
7473
7473
|
return;
|
|
7474
7474
|
}
|
|
7475
7475
|
|
|
7476
|
-
if (secureDeviceAuthenticated) {
|
|
7477
|
-
if (safeString(message.deviceId || message.DeviceId, 128) !== securityContext.deviceId) {
|
|
7478
|
-
writeJsonLine(socket, { type: 'error', error: 'device-credential-binding-invalid' });
|
|
7479
|
-
socket.destroy();
|
|
7480
|
-
return;
|
|
7481
|
-
}
|
|
7482
|
-
const helloChannel = safeString(message.channel || message.Channel || 'control', 40).toLowerCase() || 'control';
|
|
7483
|
-
if (helloChannel !== securityContext.channel) {
|
|
7484
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-channel-binding-invalid' });
|
|
7485
|
-
socket.destroy();
|
|
7486
|
-
return;
|
|
7487
|
-
}
|
|
7488
|
-
} else {
|
|
7489
|
-
const legacyAllowed = socket.__liveDeskRelayControl === true
|
|
7490
|
-
|| socket.__liveDeskLegacyPlaintext === true && allowLegacyPlaintextForTests;
|
|
7491
|
-
if (!legacyAllowed || !timingSafeStringEqual(message.pairToken, pairToken)) {
|
|
7492
|
-
writeJsonLine(socket, { type: 'error', error: 'authenticated-device-credential-required' });
|
|
7493
|
-
socket.destroy();
|
|
7494
|
-
return;
|
|
7495
|
-
}
|
|
7496
|
-
}
|
|
7497
|
-
|
|
7498
|
-
const incomingDeviceId = safeString(message.deviceId || message.DeviceId, 128);
|
|
7499
|
-
const incomingCapabilities = message.capabilities && typeof message.capabilities === 'object'
|
|
7500
|
-
? message.capabilities
|
|
7501
|
-
: {};
|
|
7502
|
-
const incomingPolicy = getWelcomeDevicePolicy({
|
|
7503
|
-
deviceId: incomingDeviceId,
|
|
7504
|
-
capabilities: incomingCapabilities
|
|
7505
|
-
}) || {};
|
|
7506
|
-
const connectionDenied = connectionPolicyError(socket, incomingPolicy);
|
|
7507
|
-
if (connectionDenied) {
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
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();
|
|
7524
7527
|
if (channel === 'input') {
|
|
7525
7528
|
const device = attachInputSocket(socket, message);
|
|
7526
7529
|
if (!device) {
|
|
@@ -7687,27 +7690,27 @@ export function createRemoteHub(options = {}) {
|
|
|
7687
7690
|
}
|
|
7688
7691
|
}
|
|
7689
7692
|
|
|
7690
|
-
function enforceAgentMessageRate(socket, state, channel = 'control') {
|
|
7691
|
-
if (state.messageRateGate.consume()) return true;
|
|
7692
|
-
const device = state.device;
|
|
7693
|
-
emitEvent('RemoteAbuseDefense', {
|
|
7694
|
-
result: 'rejected',
|
|
7695
|
-
reason: 'authenticated-message-rate-exceeded',
|
|
7696
|
-
deviceId: device?.deviceId || '',
|
|
7697
|
-
sessionId: device?.sessionId || '',
|
|
7698
|
-
accountId: socket.__liveDeskSecurityContext?.accountId || '',
|
|
7699
|
-
hubId: socket.__liveDeskSecurityContext?.hubId || '',
|
|
7700
|
-
transport: socket.__liveDeskRelayControl === true ? 'relay' : 'direct-tcp',
|
|
7701
|
-
channel,
|
|
7702
|
-
remoteAddress: safeString(socket.remoteAddress, 256),
|
|
7703
|
-
rate: state.messageRateGate.getStatus()
|
|
7704
|
-
});
|
|
7705
|
-
writeJsonLine(socket, { type: 'error', error: 'message-rate-exceeded' });
|
|
7706
|
-
socket.destroy();
|
|
7707
|
-
return false;
|
|
7708
|
-
}
|
|
7709
|
-
|
|
7710
|
-
function handleWebSocketAgentSocket(socket) {
|
|
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) {
|
|
7711
7714
|
allSockets.add(socket);
|
|
7712
7715
|
socket.setNoDelay(true);
|
|
7713
7716
|
socket.setKeepAlive(true, heartbeatMs);
|
|
@@ -7721,11 +7724,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7721
7724
|
audioOnly: false,
|
|
7722
7725
|
fileOnly: false,
|
|
7723
7726
|
parserBuffer: socket.buffer,
|
|
7724
|
-
binaryIngress: null,
|
|
7725
|
-
binaryQueueDrops: 0,
|
|
7726
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7727
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7728
|
-
closed: false
|
|
7727
|
+
binaryIngress: null,
|
|
7728
|
+
binaryQueueDrops: 0,
|
|
7729
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7730
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7731
|
+
closed: false
|
|
7729
7732
|
};
|
|
7730
7733
|
attachAgentBinaryIngress(socket, state);
|
|
7731
7734
|
|
|
@@ -7736,9 +7739,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7736
7739
|
}
|
|
7737
7740
|
}, 10000);
|
|
7738
7741
|
|
|
7739
|
-
socket.onTextMessage = text => {
|
|
7740
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7741
|
-
try {
|
|
7742
|
+
socket.onTextMessage = text => {
|
|
7743
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-text')) return;
|
|
7744
|
+
try {
|
|
7742
7745
|
handleAgentMessage(socket, state, parseJsonLine(String(text || '')));
|
|
7743
7746
|
} catch (err) {
|
|
7744
7747
|
writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
|
|
@@ -7746,9 +7749,9 @@ export function createRemoteHub(options = {}) {
|
|
|
7746
7749
|
}
|
|
7747
7750
|
};
|
|
7748
7751
|
|
|
7749
|
-
socket.onBinaryMessage = payload => {
|
|
7750
|
-
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7751
|
-
try {
|
|
7752
|
+
socket.onBinaryMessage = payload => {
|
|
7753
|
+
if (!enforceAgentMessageRate(socket, state, 'websocket-binary')) return;
|
|
7754
|
+
try {
|
|
7752
7755
|
const packet = parseRemoteHubWebSocketBinaryFrame(payload);
|
|
7753
7756
|
if (!packet?.header || !Buffer.isBuffer(packet.payload)) {
|
|
7754
7757
|
writeJsonLine(socket, { type: 'error', error: 'invalid-websocket-binary-frame' });
|
|
@@ -7903,11 +7906,11 @@ export function createRemoteHub(options = {}) {
|
|
|
7903
7906
|
buffer: new BoundedSegmentedBuffer(MAX_AGENT_SOCKET_ACCUMULATOR_BYTES),
|
|
7904
7907
|
lineScanOffset: 0,
|
|
7905
7908
|
pendingBinaryFrame: null,
|
|
7906
|
-
binaryIngress: null,
|
|
7907
|
-
binaryQueueDrops: 0,
|
|
7908
|
-
binaryQueueEpoch: crypto.randomUUID(),
|
|
7909
|
-
messageRateGate: createRemoteMessageRateGate(),
|
|
7910
|
-
closed: false
|
|
7909
|
+
binaryIngress: null,
|
|
7910
|
+
binaryQueueDrops: 0,
|
|
7911
|
+
binaryQueueEpoch: crypto.randomUUID(),
|
|
7912
|
+
messageRateGate: createRemoteMessageRateGate(),
|
|
7913
|
+
closed: false
|
|
7911
7914
|
};
|
|
7912
7915
|
attachAgentBinaryIngress(socket, state);
|
|
7913
7916
|
|
|
@@ -8022,18 +8025,18 @@ export function createRemoteHub(options = {}) {
|
|
|
8022
8025
|
return;
|
|
8023
8026
|
}
|
|
8024
8027
|
|
|
8025
|
-
try {
|
|
8026
|
-
const message = parseJsonLine(lineBuffer.toString('utf8'));
|
|
8027
|
-
if (!enforceAgentMessageRate(socket, state, state.frameOnly
|
|
8028
|
-
? 'frame'
|
|
8029
|
-
: state.audioOnly
|
|
8030
|
-
? 'audio'
|
|
8031
|
-
: state.inputOnly
|
|
8032
|
-
? 'input'
|
|
8033
|
-
: state.fileOnly ? 'file' : 'control')) {
|
|
8034
|
-
return;
|
|
8035
|
-
}
|
|
8036
|
-
if (message?.type === 'frame.binary') {
|
|
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') {
|
|
8037
8040
|
const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
|
|
8038
8041
|
const maxBytes = frameKind === 'thumbnail'
|
|
8039
8042
|
? MAX_THUMBNAIL_BINARY_BYTES
|
|
@@ -8087,35 +8090,35 @@ export function createRemoteHub(options = {}) {
|
|
|
8087
8090
|
}
|
|
8088
8091
|
}
|
|
8089
8092
|
|
|
8090
|
-
function handleSocket(socket) {
|
|
8091
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8092
|
-
secureDirectAcceptor.accept(socket);
|
|
8093
|
-
return;
|
|
8094
|
-
}
|
|
8095
|
-
socket.once('data', chunk => {
|
|
8096
|
-
const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8097
|
-
if (isSecureHandshakeStart(firstChunk)) {
|
|
8098
|
-
secureDirectAcceptor.accept(socket, firstChunk);
|
|
8099
|
-
return;
|
|
8100
|
-
}
|
|
8101
|
-
if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
|
|
8102
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8103
|
-
socket.write('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
8104
|
-
socket.destroy();
|
|
8105
|
-
return;
|
|
8106
|
-
}
|
|
8107
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
8108
|
-
handleWebSocketUpgradeSocket(socket, firstChunk);
|
|
8109
|
-
return;
|
|
8110
|
-
}
|
|
8111
|
-
if (!allowLegacyPlaintextForTests) {
|
|
8112
|
-
writeJsonLine(socket, { type: 'error', error: 'secure-direct-required' });
|
|
8113
|
-
socket.destroy();
|
|
8114
|
-
return;
|
|
8115
|
-
}
|
|
8116
|
-
socket.__liveDeskLegacyPlaintext = true;
|
|
8117
|
-
handleTcpAgentSocket(socket, firstChunk);
|
|
8118
|
-
});
|
|
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
|
+
});
|
|
8119
8122
|
|
|
8120
8123
|
socket.once('error', () => {
|
|
8121
8124
|
// The transport-specific handler owns logging after the first byte.
|
|
@@ -8137,10 +8140,10 @@ export function createRemoteHub(options = {}) {
|
|
|
8137
8140
|
started = true;
|
|
8138
8141
|
boundPort = candidateServer.address()?.port || port;
|
|
8139
8142
|
lastError = '';
|
|
8140
|
-
logEvent('remote', `LiveDesk Hub client endpoint listening with authenticated encryption on tcp://${host}:${boundPort}`, 'success');
|
|
8141
|
-
if (host === '0.0.0.0' || host === '::') {
|
|
8142
|
-
logWarn('remote', 'LiveDesk Hub client endpoint is externally reachable; Secure Direct device credentials are required.');
|
|
8143
|
-
}
|
|
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
|
+
}
|
|
8144
8147
|
emitRemoteEvent('RemoteHubStarted', null);
|
|
8145
8148
|
resolve();
|
|
8146
8149
|
});
|
|
@@ -8152,14 +8155,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8152
8155
|
return getStatus({ includeSecrets: false });
|
|
8153
8156
|
}
|
|
8154
8157
|
|
|
8155
|
-
try {
|
|
8156
|
-
await udpTransport?.start?.();
|
|
8157
|
-
await listenOnPort(requestedPort);
|
|
8158
|
-
if (!controlIdleSweepTimer) {
|
|
8159
|
-
controlIdleSweepTimer = setInterval(sweepIdleControlSessions, 1_000);
|
|
8160
|
-
controlIdleSweepTimer.unref?.();
|
|
8161
|
-
}
|
|
8162
|
-
try {
|
|
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 {
|
|
8163
8166
|
await relayControl?.start?.();
|
|
8164
8167
|
} catch {
|
|
8165
8168
|
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
@@ -8180,12 +8183,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8180
8183
|
return getStatus({ includeSecrets: false });
|
|
8181
8184
|
}
|
|
8182
8185
|
|
|
8183
|
-
async function close() {
|
|
8184
|
-
if (controlIdleSweepTimer) {
|
|
8185
|
-
clearInterval(controlIdleSweepTimer);
|
|
8186
|
-
controlIdleSweepTimer = null;
|
|
8187
|
-
}
|
|
8188
|
-
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]) {
|
|
8189
8192
|
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8190
8193
|
}
|
|
8191
8194
|
|
|
@@ -8322,7 +8325,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8322
8325
|
};
|
|
8323
8326
|
}
|
|
8324
8327
|
|
|
8325
|
-
function sendCommand(deviceId, command) {
|
|
8328
|
+
function sendCommand(deviceId, command) {
|
|
8326
8329
|
const device = devices.get(String(deviceId || ''));
|
|
8327
8330
|
const commandName = safeString(command?.command || 'ping', 80);
|
|
8328
8331
|
const requiredPermission = commandName === 'input.control'
|
|
@@ -8388,64 +8391,64 @@ export function createRemoteHub(options = {}) {
|
|
|
8388
8391
|
command: payload.command,
|
|
8389
8392
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8390
8393
|
});
|
|
8391
|
-
return { ok: true, commandId };
|
|
8392
|
-
}
|
|
8393
|
-
|
|
8394
|
-
async function sendCommandAwaitResult(deviceId, command, options = {}) {
|
|
8395
|
-
const normalizedDeviceId = safeString(deviceId, 160);
|
|
8396
|
-
const device = devices.get(normalizedDeviceId);
|
|
8397
|
-
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8398
|
-
const commandWithId = { ...(command || {}), commandId };
|
|
8399
|
-
|
|
8400
|
-
if (device?.synthetic === true && device.connected) {
|
|
8401
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8402
|
-
return {
|
|
8403
|
-
...sent,
|
|
8404
|
-
queued: sent.ok === true,
|
|
8405
|
-
acknowledged: sent.ok === true,
|
|
8406
|
-
acknowledgement: sent.ok === true
|
|
8407
|
-
? { ok: true, commandId, result: { ok: true, synthetic: true }, error: '' }
|
|
8408
|
-
: { ok: false, commandId, result: null, error: sent.error || 'command-not-sent' }
|
|
8409
|
-
};
|
|
8410
|
-
}
|
|
8411
|
-
|
|
8412
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8413
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8414
|
-
return {
|
|
8415
|
-
...sent,
|
|
8416
|
-
queued: false,
|
|
8417
|
-
acknowledged: false,
|
|
8418
|
-
acknowledgement: {
|
|
8419
|
-
ok: false,
|
|
8420
|
-
commandId,
|
|
8421
|
-
result: null,
|
|
8422
|
-
error: sent.error || 'device-not-connected'
|
|
8423
|
-
}
|
|
8424
|
-
};
|
|
8425
|
-
}
|
|
8426
|
-
|
|
8427
|
-
const timeoutMs = clampNumber(options.timeoutMs, 1000, 120_000, 30_000);
|
|
8428
|
-
// Register before writing: a loopback Agent can return command.result in
|
|
8429
|
-
// the same event-loop turn as the command write.
|
|
8430
|
-
const acknowledgementPromise = waitForCommandResult(device, commandId, timeoutMs);
|
|
8431
|
-
const sent = sendCommand(normalizedDeviceId, commandWithId);
|
|
8432
|
-
if (!sent.ok) {
|
|
8433
|
-
failPendingCommandResultWaiter(device, commandId, sent.error || 'command-not-sent');
|
|
8434
|
-
}
|
|
8435
|
-
const acknowledgement = await acknowledgementPromise;
|
|
8436
|
-
return {
|
|
8437
|
-
...sent,
|
|
8438
|
-
ok: sent.ok === true && acknowledgement.ok === true,
|
|
8439
|
-
queued: sent.ok === true,
|
|
8440
|
-
acknowledged: acknowledgement.ok === true,
|
|
8441
|
-
acknowledgement,
|
|
8442
|
-
error: acknowledgement.ok === true
|
|
8443
|
-
? undefined
|
|
8444
|
-
: acknowledgement.error || sent.error || 'command-result-failed'
|
|
8445
|
-
};
|
|
8446
|
-
}
|
|
8447
|
-
|
|
8448
|
-
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8394
|
+
return { ok: true, commandId };
|
|
8395
|
+
}
|
|
8396
|
+
|
|
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) {
|
|
8449
8452
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8450
8453
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8451
8454
|
: null;
|
|
@@ -8454,31 +8457,34 @@ export function createRemoteHub(options = {}) {
|
|
|
8454
8457
|
for (const device of devices.values()) {
|
|
8455
8458
|
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8456
8459
|
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8457
|
-
total += 1;
|
|
8458
|
-
const effectivePolicy = getDevicePolicy(device);
|
|
8459
|
-
const connectionDenied = connectionPolicyError(device.socket, effectivePolicy);
|
|
8460
|
-
if (connectionDenied) {
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
}
|
|
8481
|
-
if (
|
|
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, {
|
|
8482
8488
|
type: 'policy.update',
|
|
8483
8489
|
effectivePolicy,
|
|
8484
8490
|
updatedAt: new Date().toISOString()
|
|
@@ -8557,7 +8563,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8557
8563
|
]);
|
|
8558
8564
|
}
|
|
8559
8565
|
|
|
8560
|
-
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8566
|
+
function buildRemoteInputResetMessage(device, controlStream, ownerConnectionId, reason, activeMonitorIndex) {
|
|
8561
8567
|
return {
|
|
8562
8568
|
type: 'input.control',
|
|
8563
8569
|
payload: {
|
|
@@ -8579,114 +8585,114 @@ export function createRemoteHub(options = {}) {
|
|
|
8579
8585
|
hubForwardedAtEpochMs: Date.now(),
|
|
8580
8586
|
issuedAt: new Date().toISOString()
|
|
8581
8587
|
}
|
|
8582
|
-
};
|
|
8583
|
-
}
|
|
8584
|
-
|
|
8585
|
-
function controlSessionMessage(device, ownerConnectionId, type, reason = '') {
|
|
8586
|
-
const policy = getDevicePolicy(device);
|
|
8587
|
-
return {
|
|
8588
|
-
type,
|
|
8589
|
-
payload: {
|
|
8590
|
-
controlOwnerSessionId: safeString(device?.controlOwnerSessionId, 160),
|
|
8591
|
-
hubConnectionId: safeString(ownerConnectionId, 128),
|
|
8592
|
-
deviceId: safeString(device?.deviceId, 160),
|
|
8593
|
-
visibleIndicator: true,
|
|
8594
|
-
lockOnEnd: type === 'control.session.stop' && policy.lockOnControlEnd === true,
|
|
8595
|
-
idleControlMinutes: clampNumber(policy.idleControlMinutes, 5, 1440, 30),
|
|
8596
|
-
reason: safeString(reason, 160),
|
|
8597
|
-
issuedAt: new Date().toISOString()
|
|
8598
|
-
}
|
|
8599
|
-
};
|
|
8600
|
-
}
|
|
8601
|
-
|
|
8602
|
-
function sendControlSessionMessage(device, ownerConnectionId, type, reason, inputWriteOwner, bindingKey) {
|
|
8603
|
-
const message = controlSessionMessage(device, ownerConnectionId, type, reason);
|
|
8604
|
-
if (inputWriteOwner && bindingKey) {
|
|
8605
|
-
return inputWriteOwner.enqueue(message, bindingKey);
|
|
8606
|
-
}
|
|
8607
|
-
if (!device?.socket || device.socket.destroyed) {
|
|
8608
|
-
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
-
}
|
|
8610
|
-
const sent = writeJsonLine(device.socket, {
|
|
8611
|
-
type: 'command',
|
|
8612
|
-
commandId: crypto.randomUUID(),
|
|
8613
|
-
command: type,
|
|
8614
|
-
payload: message.payload,
|
|
8615
|
-
issuedAt: new Date().toISOString()
|
|
8616
|
-
});
|
|
8617
|
-
return sent ? { ok: true, inputSocket: false } : { ok: false, error: 'device-not-connected' };
|
|
8618
|
-
}
|
|
8619
|
-
|
|
8620
|
-
function beginControlSession(device, ownerConnectionId, inputWriteOwner, bindingKey) {
|
|
8621
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8622
|
-
if (!device || !owner) return { ok: false, error: 'control-owner-required' };
|
|
8623
|
-
if (device.controlOwnerConnectionId === owner && device.controlOwnerSessionId) {
|
|
8624
|
-
return { ok: true, alreadyActive: true };
|
|
8625
|
-
}
|
|
8626
|
-
device.controlOwnerSessionId = crypto.randomUUID();
|
|
8627
|
-
device.controlOwnerConnectionId = owner;
|
|
8628
|
-
device.controlOwnerStartedAtMs = Date.now();
|
|
8629
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8630
|
-
const sent = sendControlSessionMessage(
|
|
8631
|
-
device,
|
|
8632
|
-
owner,
|
|
8633
|
-
'control.session.start',
|
|
8634
|
-
'browser-control-owner-active',
|
|
8635
|
-
inputWriteOwner,
|
|
8636
|
-
bindingKey);
|
|
8637
|
-
if (!sent.ok) {
|
|
8638
|
-
device.controlOwnerSessionId = '';
|
|
8639
|
-
device.controlOwnerConnectionId = '';
|
|
8640
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8641
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8642
|
-
return sent;
|
|
8643
|
-
}
|
|
8644
|
-
emitRemoteEvent('RemoteControlSessionStarted', device, {
|
|
8645
|
-
controlOwnerSessionId: device.controlOwnerSessionId,
|
|
8646
|
-
hubConnectionId: owner
|
|
8647
|
-
});
|
|
8648
|
-
return sent;
|
|
8649
|
-
}
|
|
8650
|
-
|
|
8651
|
-
function endControlSession(device, ownerConnectionId, reason, inputWriteOwner, bindingKey) {
|
|
8652
|
-
const owner = safeString(ownerConnectionId, 128);
|
|
8653
|
-
if (!device?.controlOwnerSessionId
|
|
8654
|
-
|| !owner
|
|
8655
|
-
|| device.controlOwnerConnectionId !== owner) {
|
|
8656
|
-
return { ok: false, error: 'control-owner-not-current' };
|
|
8657
|
-
}
|
|
8658
|
-
const controlOwnerSessionId = device.controlOwnerSessionId;
|
|
8659
|
-
const sent = sendControlSessionMessage(
|
|
8660
|
-
device,
|
|
8661
|
-
owner,
|
|
8662
|
-
'control.session.stop',
|
|
8663
|
-
reason,
|
|
8664
|
-
inputWriteOwner,
|
|
8665
|
-
bindingKey);
|
|
8666
|
-
device.controlOwnerSessionId = '';
|
|
8667
|
-
device.controlOwnerConnectionId = '';
|
|
8668
|
-
device.controlOwnerStartedAtMs = 0;
|
|
8669
|
-
device.controlOwnerLastInputAtMs = 0;
|
|
8670
|
-
emitRemoteEvent('RemoteControlSessionEnded', device, {
|
|
8671
|
-
controlOwnerSessionId,
|
|
8672
|
-
hubConnectionId: owner,
|
|
8673
|
-
reason,
|
|
8674
|
-
delivered: sent.ok === true
|
|
8675
|
-
});
|
|
8676
|
-
return sent;
|
|
8677
|
-
}
|
|
8678
|
-
|
|
8679
|
-
function sweepIdleControlSessions() {
|
|
8680
|
-
const nowMs = Date.now();
|
|
8681
|
-
for (const device of devices.values()) {
|
|
8682
|
-
if (!device?.connected || !device.controlOwnerConnectionId) continue;
|
|
8683
|
-
const policy = getDevicePolicy(device);
|
|
8684
|
-
if (policy.disconnectIdleControlSessions !== true) continue;
|
|
8685
|
-
const idleMs = clampNumber(policy.idleControlMinutes, 5, 1440, 30) * 60 * 1000;
|
|
8686
|
-
if (nowMs - Number(device.controlOwnerLastInputAtMs || 0) < idleMs) continue;
|
|
8687
|
-
releaseInputOwner(device.deviceId, device.controlOwnerConnectionId, 'control-idle-timeout');
|
|
8688
|
-
}
|
|
8689
|
-
}
|
|
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
|
+
}
|
|
8690
8696
|
|
|
8691
8697
|
function getCurrentInputWriteOwner(device) {
|
|
8692
8698
|
if (!device?.inputSocket
|
|
@@ -8829,16 +8835,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8829
8835
|
normalized,
|
|
8830
8836
|
activeMonitorIndex);
|
|
8831
8837
|
const previousInputBindingKey = String(device.inputOwnerBindingKey || '');
|
|
8832
|
-
const ownerChanged = normalized.hubConnectionId
|
|
8838
|
+
const ownerChanged = normalized.hubConnectionId
|
|
8833
8839
|
&& previousOwnerConnectionId
|
|
8834
8840
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8835
8841
|
const bindingChanged = previousInputBindingKey
|
|
8836
8842
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8837
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8838
|
-
const startingControlOwner = !!normalized.hubConnectionId
|
|
8839
|
-
&& (!device.controlOwnerConnectionId
|
|
8840
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8841
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8843
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8844
|
+
const startingControlOwner = !!normalized.hubConnectionId
|
|
8845
|
+
&& (!device.controlOwnerConnectionId
|
|
8846
|
+
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8847
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8842
8848
|
const resetSent = inputWriteOwner.replaceBinding(
|
|
8843
8849
|
activeInputBindingKey,
|
|
8844
8850
|
buildRemoteInputResetMessage(
|
|
@@ -8853,28 +8859,28 @@ export function createRemoteHub(options = {}) {
|
|
|
8853
8859
|
activeMonitorIndex));
|
|
8854
8860
|
if (!resetSent.ok) {
|
|
8855
8861
|
closeInputSocket(device, resetSent.error || 'input-owner-reset-write-failed');
|
|
8856
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8857
|
-
}
|
|
8858
|
-
if (startingControlOwner) {
|
|
8859
|
-
if (device.controlOwnerConnectionId) {
|
|
8860
|
-
endControlSession(
|
|
8861
|
-
device,
|
|
8862
|
-
device.controlOwnerConnectionId,
|
|
8863
|
-
'browser-input-owner-changed',
|
|
8864
|
-
inputWriteOwner,
|
|
8865
|
-
activeInputBindingKey);
|
|
8866
|
-
}
|
|
8867
|
-
const started = beginControlSession(
|
|
8868
|
-
device,
|
|
8869
|
-
normalized.hubConnectionId,
|
|
8870
|
-
inputWriteOwner,
|
|
8871
|
-
activeInputBindingKey);
|
|
8872
|
-
if (!started.ok) {
|
|
8873
|
-
closeInputSocket(device, started.error || 'control-session-indicator-start-failed');
|
|
8874
|
-
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8875
|
-
}
|
|
8876
|
-
}
|
|
8877
|
-
}
|
|
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
|
+
}
|
|
8878
8884
|
const sent = inputWriteOwner.enqueue({
|
|
8879
8885
|
type: 'input.control',
|
|
8880
8886
|
payload: {
|
|
@@ -8887,8 +8893,8 @@ export function createRemoteHub(options = {}) {
|
|
|
8887
8893
|
if (normalized.hubConnectionId) {
|
|
8888
8894
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8889
8895
|
}
|
|
8890
|
-
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8891
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8896
|
+
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8897
|
+
device.controlOwnerLastInputAtMs = Date.now();
|
|
8892
8898
|
device.counters.commandsSent += 1;
|
|
8893
8899
|
device.inputLastSeenAt = new Date().toISOString();
|
|
8894
8900
|
return {
|
|
@@ -8917,27 +8923,27 @@ export function createRemoteHub(options = {}) {
|
|
|
8917
8923
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
8918
8924
|
}
|
|
8919
8925
|
|
|
8920
|
-
const hubForwardedAtEpochMs = Date.now();
|
|
8921
|
-
const startingFallbackOwner = !!normalized.hubConnectionId
|
|
8922
|
-
&& (!device.controlOwnerConnectionId
|
|
8923
|
-
|| device.controlOwnerConnectionId !== normalized.hubConnectionId);
|
|
8924
|
-
if (startingFallbackOwner) {
|
|
8925
|
-
if (device.controlOwnerConnectionId) {
|
|
8926
|
-
endControlSession(
|
|
8927
|
-
device,
|
|
8928
|
-
device.controlOwnerConnectionId,
|
|
8929
|
-
'browser-input-owner-changed',
|
|
8930
|
-
null,
|
|
8931
|
-
'');
|
|
8932
|
-
}
|
|
8933
|
-
const started = beginControlSession(
|
|
8934
|
-
device,
|
|
8935
|
-
normalized.hubConnectionId,
|
|
8936
|
-
null,
|
|
8937
|
-
'');
|
|
8938
|
-
if (!started.ok) return started;
|
|
8939
|
-
}
|
|
8940
|
-
const fallback = sendCommand(deviceId, {
|
|
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, {
|
|
8941
8947
|
command: 'input.control',
|
|
8942
8948
|
payload: {
|
|
8943
8949
|
...normalized,
|
|
@@ -8945,7 +8951,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8945
8951
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8946
8952
|
}
|
|
8947
8953
|
});
|
|
8948
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8954
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8949
8955
|
schedulePendingInputFallback(device, {
|
|
8950
8956
|
commandId: fallback.commandId,
|
|
8951
8957
|
inputSeq: normalized.inputSeq,
|
|
@@ -8957,26 +8963,26 @@ export function createRemoteHub(options = {}) {
|
|
|
8957
8963
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8958
8964
|
hubForwardedAtEpochMs,
|
|
8959
8965
|
fallback: true,
|
|
8960
|
-
timeoutTimer: null
|
|
8961
|
-
});
|
|
8962
|
-
}
|
|
8963
|
-
if (fallback.ok) {
|
|
8964
|
-
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8965
|
-
device.inputOwnerBindingKey = buildRemoteInputBindingKey(
|
|
8966
|
-
device,
|
|
8967
|
-
controlStream,
|
|
8968
|
-
normalized,
|
|
8969
|
-
activeMonitorIndex);
|
|
8970
|
-
device.controlOwnerLastInputAtMs = Date.now();
|
|
8971
|
-
} else if (startingFallbackOwner) {
|
|
8972
|
-
endControlSession(
|
|
8973
|
-
device,
|
|
8974
|
-
normalized.hubConnectionId,
|
|
8975
|
-
'input-control-delivery-failed',
|
|
8976
|
-
null,
|
|
8977
|
-
'');
|
|
8978
|
-
}
|
|
8979
|
-
return fallback.ok
|
|
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
|
|
8980
8986
|
? {
|
|
8981
8987
|
...fallback,
|
|
8982
8988
|
inputSocket: false,
|
|
@@ -9002,20 +9008,20 @@ export function createRemoteHub(options = {}) {
|
|
|
9002
9008
|
return { ok: false, error: 'input-owner-not-current' };
|
|
9003
9009
|
}
|
|
9004
9010
|
|
|
9005
|
-
const inputSocket = device.inputSocket;
|
|
9006
|
-
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
9007
|
-
if (!inputSocket || !inputWriteOwner) {
|
|
9008
|
-
const ended = endControlSession(device, owner, reason, null, '');
|
|
9009
|
-
device.inputOwnerConnectionId = '';
|
|
9010
|
-
device.inputOwnerBindingKey = '';
|
|
9011
|
-
return {
|
|
9012
|
-
ok: ended.ok !== false,
|
|
9013
|
-
queued: false,
|
|
9014
|
-
controlSessionEnded: ended.ok === true
|
|
9015
|
-
};
|
|
9016
|
-
}
|
|
9017
|
-
const controlStream = getActiveControlStream(device);
|
|
9018
|
-
const releaseBindingKey = buildRemoteInputReleaseBindingKey(device, owner);
|
|
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);
|
|
9019
9025
|
const sent = inputWriteOwner.replaceBinding(
|
|
9020
9026
|
releaseBindingKey,
|
|
9021
9027
|
buildRemoteInputResetMessage(
|
|
@@ -9024,24 +9030,24 @@ export function createRemoteHub(options = {}) {
|
|
|
9024
9030
|
owner,
|
|
9025
9031
|
reason,
|
|
9026
9032
|
normalizeMonitorIndex(controlStream?.monitorIndex)));
|
|
9027
|
-
if (!sent.ok) {
|
|
9033
|
+
if (!sent.ok) {
|
|
9028
9034
|
closeInputSocket(device, sent.error || 'input-owner-release-write-failed');
|
|
9029
9035
|
return { ok: false, error: 'INPUT_CHANNEL_RECONNECTING' };
|
|
9030
|
-
}
|
|
9031
|
-
const ended = endControlSession(
|
|
9032
|
-
device,
|
|
9033
|
-
owner,
|
|
9034
|
-
reason,
|
|
9035
|
-
inputWriteOwner,
|
|
9036
|
-
releaseBindingKey);
|
|
9037
|
-
device.inputOwnerConnectionId = '';
|
|
9038
|
-
device.inputOwnerBindingKey = '';
|
|
9039
|
-
return {
|
|
9040
|
-
ok: ended.ok !== false,
|
|
9041
|
-
queued: true,
|
|
9042
|
-
backpressured: sent.backpressured === true || ended.backpressured === true,
|
|
9043
|
-
controlSessionEnded: ended.ok === true
|
|
9044
|
-
};
|
|
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
|
+
};
|
|
9045
9051
|
}
|
|
9046
9052
|
|
|
9047
9053
|
function notifyAgentProgress(deviceIds, progress = {}) {
|
|
@@ -9081,12 +9087,12 @@ export function createRemoteHub(options = {}) {
|
|
|
9081
9087
|
if (retiredRequestError) {
|
|
9082
9088
|
return { ok: false, error: retiredRequestError };
|
|
9083
9089
|
}
|
|
9084
|
-
const device = devices.get(String(deviceId || ''));
|
|
9085
|
-
const requestedOperation = safeString(options.operation, 80);
|
|
9086
|
-
if (RETIRED_AGENT_MUTATING_OPERATIONS.has(requestedOperation.toLowerCase())) {
|
|
9087
|
-
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
9088
|
-
}
|
|
9089
|
-
const operation = normalizeAgentOperation(requestedOperation);
|
|
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);
|
|
9090
9096
|
if (requestedOperation && !operation) {
|
|
9091
9097
|
return { ok: false, error: 'unsupported-agent-operation' };
|
|
9092
9098
|
}
|
|
@@ -11332,10 +11338,10 @@ export function createRemoteHub(options = {}) {
|
|
|
11332
11338
|
retryTaskBatch,
|
|
11333
11339
|
listDeviceFrames,
|
|
11334
11340
|
disconnectDevice,
|
|
11335
|
-
assignDeviceSlot,
|
|
11336
|
-
sendCommand,
|
|
11337
|
-
sendCommandAwaitResult,
|
|
11338
|
-
refreshDevicePolicies,
|
|
11341
|
+
assignDeviceSlot,
|
|
11342
|
+
sendCommand,
|
|
11343
|
+
sendCommandAwaitResult,
|
|
11344
|
+
refreshDevicePolicies,
|
|
11339
11345
|
sendLegacyClientUpdate,
|
|
11340
11346
|
sendInputControl,
|
|
11341
11347
|
releaseInputOwner,
|
|
@@ -11362,17 +11368,17 @@ export function createRemoteHub(options = {}) {
|
|
|
11362
11368
|
handleUdpFrame,
|
|
11363
11369
|
seedSyntheticFleet,
|
|
11364
11370
|
clearSyntheticFleet,
|
|
11365
|
-
getPairToken: () => pairToken,
|
|
11366
|
-
getPairingPin: () => pairingPin,
|
|
11367
|
-
getSecurityStatus: () => ({
|
|
11368
|
-
hubId: deviceCredentialAuthority.hubId,
|
|
11369
|
-
hubPublicKey: deviceCredentialAuthority.hubPublicKey,
|
|
11370
|
-
hubIssuerKeyId: deviceCredentialAuthority.hubIssuerKeyId,
|
|
11371
|
-
direct: secureDirectAcceptor.getStatus(),
|
|
11372
|
-
devices: deviceCredentialAuthority.listDevices()
|
|
11373
|
-
}),
|
|
11374
|
-
revokeDeviceCredential: (deviceId, reason) => deviceCredentialAuthority.revokeDevice(deviceId, reason),
|
|
11375
|
-
clearDeviceCredentials: () => deviceCredentialAuthority.clearDevices()
|
|
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()
|
|
11376
11382
|
};
|
|
11377
11383
|
}
|
|
11378
11384
|
|