@livedesk/hub 0.1.47 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/filesystem/roots.js +20 -2
- package/src/remote-clipboard-contract.mjs +482 -0
- package/src/remote-hub.js +1140 -221
- package/src/server.js +137 -9
- package/src/settings/effective-device-policy.js +31 -9
package/src/remote-hub.js
CHANGED
|
@@ -3,10 +3,18 @@ import os from 'os';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { createHubRelayControl } from './transport/relay-hub-control.js';
|
|
5
5
|
import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
|
|
6
|
-
import {
|
|
7
|
-
BoundedSegmentedBuffer,
|
|
8
|
-
createBoundedAgentBinaryIngressLane
|
|
9
|
-
} from './transport/agent-binary-ingress.js';
|
|
6
|
+
import {
|
|
7
|
+
BoundedSegmentedBuffer,
|
|
8
|
+
createBoundedAgentBinaryIngressLane
|
|
9
|
+
} from './transport/agent-binary-ingress.js';
|
|
10
|
+
import {
|
|
11
|
+
REMOTE_CLIPBOARD_FILE_ACTIONS,
|
|
12
|
+
REMOTE_CLIPBOARD_LIMITS,
|
|
13
|
+
REMOTE_CLIPBOARD_PROTOCOL,
|
|
14
|
+
normalizeRemoteClipboardCommandResult,
|
|
15
|
+
normalizeRemoteClipboardManifest,
|
|
16
|
+
remoteClipboardContentNeedsFileTransfer
|
|
17
|
+
} from './remote-clipboard-contract.mjs';
|
|
10
18
|
|
|
11
19
|
const DEFAULT_REMOTE_HUB_PORT = 5197;
|
|
12
20
|
const DEFAULT_REMOTE_HUB_HOST = '0.0.0.0';
|
|
@@ -228,12 +236,13 @@ const REMOTE_FRAME_MODE_ALIASES = new Map([
|
|
|
228
236
|
]);
|
|
229
237
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
230
238
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
231
|
-
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
232
|
-
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
239
|
+
const DEFAULT_PUBLIC_IP_REFRESH_MS = 5 * 60 * 1000;
|
|
240
|
+
const DEFAULT_PUBLIC_IP_TIMEOUT_MS = 1800;
|
|
241
|
+
const DEFAULT_DEVICE_CONNECTION_STALE_MS = 20000;
|
|
242
|
+
const DEFAULT_DEVICE_CONNECTION_SWEEP_MS = 5000;
|
|
243
|
+
const DUPLICATE_DEVICE_LOG_THROTTLE_MS = 60000;
|
|
244
|
+
const DUPLICATE_DEVICE_RETRY_AFTER_MS = 30000;
|
|
245
|
+
const AGENT_BINARY_INGRESS_QUEUE_PACKETS = 16;
|
|
237
246
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_BYTES = 12 * 1024 * 1024;
|
|
238
247
|
const AGENT_BINARY_INGRESS_QUEUE_MAX_AGE_MS = 120;
|
|
239
248
|
const MODE5_AGENT_BINARY_INGRESS_QUEUE_PACKETS = 2;
|
|
@@ -797,6 +806,25 @@ function normalizeIncomingFrameModeProfile(value) {
|
|
|
797
806
|
};
|
|
798
807
|
}
|
|
799
808
|
|
|
809
|
+
function deviceExplicitlyRejectsFrameMode(device, frameMode) {
|
|
810
|
+
const requestedMode = safeString(frameMode, 80).toLowerCase();
|
|
811
|
+
if (!requestedMode) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const advertisedModes = [
|
|
816
|
+
...(Array.isArray(device?.frameModes) ? device.frameModes : []),
|
|
817
|
+
...(Array.isArray(device?.frameProtocol?.modes) ? device.frameProtocol.modes : []),
|
|
818
|
+
...(Array.isArray(device?.capabilities?.frameModes) ? device.capabilities.frameModes : []),
|
|
819
|
+
...(Array.isArray(device?.capabilities?.frameProtocol?.modes)
|
|
820
|
+
? device.capabilities.frameProtocol.modes
|
|
821
|
+
: [])
|
|
822
|
+
];
|
|
823
|
+
return advertisedModes.some(profile =>
|
|
824
|
+
safeString(profile?.mode || profile?.frameMode, 80).toLowerCase() === requestedMode
|
|
825
|
+
&& profile?.implemented === false);
|
|
826
|
+
}
|
|
827
|
+
|
|
800
828
|
function isLoopbackHost(value) {
|
|
801
829
|
const host = String(value || '').trim().toLowerCase();
|
|
802
830
|
return host === 'localhost'
|
|
@@ -1164,12 +1192,12 @@ function normalizeRemoteKeyboardKey(value) {
|
|
|
1164
1192
|
return raw === ' ' ? ' ' : safeString(raw, 80);
|
|
1165
1193
|
}
|
|
1166
1194
|
|
|
1167
|
-
function normalizeRemoteKeyboardText(value) {
|
|
1168
|
-
const raw = String(value ?? '').replace(/\0/g, '');
|
|
1169
|
-
// Remote text is input data, not a label. Preserve leading/trailing spaces
|
|
1170
|
-
// and IME whitespace while keeping every ordered message bounded.
|
|
1171
|
-
return raw.slice(0, 256);
|
|
1172
|
-
}
|
|
1195
|
+
function normalizeRemoteKeyboardText(value) {
|
|
1196
|
+
const raw = String(value ?? '').replace(/\0/g, '');
|
|
1197
|
+
// Remote text is input data, not a label. Preserve leading/trailing spaces
|
|
1198
|
+
// and IME whitespace while keeping every ordered message bounded.
|
|
1199
|
+
return raw.slice(0, 256);
|
|
1200
|
+
}
|
|
1173
1201
|
|
|
1174
1202
|
function safeTaskData(value) {
|
|
1175
1203
|
if (value === undefined || value === null) return undefined;
|
|
@@ -1250,7 +1278,7 @@ function readRawRemoteInputMonitorIndex(input) {
|
|
|
1250
1278
|
return undefined;
|
|
1251
1279
|
}
|
|
1252
1280
|
|
|
1253
|
-
function normalizeRemoteInputEvent(value = {}) {
|
|
1281
|
+
function normalizeRemoteInputEvent(value = {}) {
|
|
1254
1282
|
const input = value && typeof value === 'object' ? value : {};
|
|
1255
1283
|
const type = safeString(input.type || input.Type, 48);
|
|
1256
1284
|
const normalizedX = Number(input.normalizedX ?? input.NormalizedX);
|
|
@@ -1271,7 +1299,14 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1271
1299
|
.filter(Boolean))]
|
|
1272
1300
|
.slice(0, 64)
|
|
1273
1301
|
: null;
|
|
1274
|
-
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1302
|
+
const key = normalizeRemoteKeyboardKey(input.key ?? input.Key);
|
|
1303
|
+
const clipboardOperationId = safeString(
|
|
1304
|
+
input.clipboardOperationId || input.operationId || input.ClipboardOperationId || input.OperationId,
|
|
1305
|
+
128);
|
|
1306
|
+
const clipboardDirection = safeString(
|
|
1307
|
+
input.clipboardDirection || input.ClipboardDirection,
|
|
1308
|
+
16).toLowerCase();
|
|
1309
|
+
const clipboardManifest = input.clipboardManifest || input.manifest || input.ClipboardManifest || input.Manifest;
|
|
1275
1310
|
let code = safeString(input.code ?? input.Code, 80);
|
|
1276
1311
|
if ((!code || code === 'Unidentified') && (key === ' ' || key === 'Spacebar' || keyCode === 32)) {
|
|
1277
1312
|
code = 'Space';
|
|
@@ -1296,7 +1331,13 @@ function normalizeRemoteInputEvent(value = {}) {
|
|
|
1296
1331
|
shiftKey: input.shiftKey === true || input.ShiftKey === true,
|
|
1297
1332
|
ctrlKey: input.ctrlKey === true || input.ControlKey === true || input.CtrlKey === true,
|
|
1298
1333
|
altKey: input.altKey === true || input.AltKey === true,
|
|
1299
|
-
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1334
|
+
metaKey: input.metaKey === true || input.MetaKey === true || input.commandKey === true || input.CommandKey === true,
|
|
1335
|
+
clipboardOperationId,
|
|
1336
|
+
operationId: clipboardOperationId,
|
|
1337
|
+
clipboardDirection,
|
|
1338
|
+
manifest: clipboardManifest && typeof clipboardManifest === 'object' && !Array.isArray(clipboardManifest)
|
|
1339
|
+
? clipboardManifest
|
|
1340
|
+
: null,
|
|
1300
1341
|
controlLeaseId: safeString(input.controlLeaseId || input.ControlLeaseId, 128),
|
|
1301
1342
|
controlSessionId: safeString(input.controlSessionId || input.sessionId || input.ControlSessionId || input.SessionId, 160),
|
|
1302
1343
|
controlCommandId: safeString(input.controlCommandId || input.commandId || input.ControlCommandId || input.CommandId, 128),
|
|
@@ -1718,6 +1759,26 @@ function writeJsonLine(socket, payload) {
|
|
|
1718
1759
|
return true;
|
|
1719
1760
|
}
|
|
1720
1761
|
|
|
1762
|
+
function writeJsonLineAndClose(socket, payload) {
|
|
1763
|
+
if (!socket || socket.destroyed) {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (socket.__remoteHubWebSocket === true) {
|
|
1768
|
+
const written = writeJsonLine(socket, payload);
|
|
1769
|
+
socket.destroy();
|
|
1770
|
+
return written;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
try {
|
|
1774
|
+
socket.end(`${JSON.stringify(payload)}\n`);
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch {
|
|
1777
|
+
socket.destroy();
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1721
1782
|
function isRemoteHubWebSocketUpgradeStart(buffer) {
|
|
1722
1783
|
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
1723
1784
|
return false;
|
|
@@ -2268,12 +2329,29 @@ export function createRemoteHub(options = {}) {
|
|
|
2268
2329
|
|
|
2269
2330
|
const enabled = isEnabledValue(env.LIVEDESK_REMOTE_HUB ?? env.MINDEXEC_REMOTE_HUB ?? env.REMOTE_HUB_ENABLED, true)
|
|
2270
2331
|
&& !isDisabledValue(env.REMOTE_HUB_DISABLED);
|
|
2271
|
-
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2272
|
-
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2273
|
-
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2332
|
+
const host = safeString(env.REMOTE_HUB_HOST || DEFAULT_REMOTE_HUB_HOST, 128);
|
|
2333
|
+
const requestedPort = normalizePort(env.REMOTE_HUB_PORT || DEFAULT_REMOTE_HUB_PORT);
|
|
2334
|
+
const heartbeatMs = clampNumber(env.REMOTE_HUB_HEARTBEAT_MS, 1000, 60000, DEFAULT_HEARTBEAT_MS);
|
|
2335
|
+
const defaultDeviceConnectionStaleMs = Math.max(
|
|
2336
|
+
DEFAULT_DEVICE_CONNECTION_STALE_MS,
|
|
2337
|
+
heartbeatMs * 4);
|
|
2338
|
+
const deviceConnectionStaleMs = Number.isFinite(Number(options.deviceConnectionStaleMs))
|
|
2339
|
+
? clampNumber(options.deviceConnectionStaleMs, 100, 10 * 60 * 1000, defaultDeviceConnectionStaleMs)
|
|
2340
|
+
: clampNumber(
|
|
2341
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_STALE_MS,
|
|
2342
|
+
defaultDeviceConnectionStaleMs,
|
|
2343
|
+
10 * 60 * 1000,
|
|
2344
|
+
defaultDeviceConnectionStaleMs);
|
|
2345
|
+
const deviceConnectionSweepMs = Number.isFinite(Number(options.deviceConnectionSweepMs))
|
|
2346
|
+
? clampNumber(options.deviceConnectionSweepMs, 25, 60000, DEFAULT_DEVICE_CONNECTION_SWEEP_MS)
|
|
2347
|
+
: clampNumber(
|
|
2348
|
+
env.REMOTE_HUB_DEVICE_CONNECTION_SWEEP_MS,
|
|
2349
|
+
1000,
|
|
2350
|
+
60000,
|
|
2351
|
+
Math.min(DEFAULT_DEVICE_CONNECTION_SWEEP_MS, heartbeatMs));
|
|
2352
|
+
const taskTimeoutMs = clampNumber(
|
|
2353
|
+
options.taskTimeoutMs ?? env.REMOTE_HUB_TASK_TIMEOUT_MS,
|
|
2354
|
+
50,
|
|
2277
2355
|
30 * 60 * 1000,
|
|
2278
2356
|
DEFAULT_AGENT_TASK_TIMEOUT_MS);
|
|
2279
2357
|
const inputAckTimeoutMs = clampNumber(
|
|
@@ -2336,16 +2414,18 @@ export function createRemoteHub(options = {}) {
|
|
|
2336
2414
|
const audioSockets = new Map();
|
|
2337
2415
|
const fileSockets = new Map();
|
|
2338
2416
|
const allSockets = new Set();
|
|
2339
|
-
const agentBinaryIngressStates = new Set();
|
|
2340
|
-
const pendingCommandResultWaiters = new Map();
|
|
2417
|
+
const agentBinaryIngressStates = new Set();
|
|
2418
|
+
const pendingCommandResultWaiters = new Map();
|
|
2419
|
+
const clipboardOperations = new Map();
|
|
2341
2420
|
const transportDiagnostics = new Map();
|
|
2342
2421
|
const transportDiagnosticStartingDevices = new Set();
|
|
2343
|
-
const duplicateDeviceLogAt = new Map();
|
|
2344
|
-
let server = null;
|
|
2345
|
-
let started = false;
|
|
2346
|
-
let
|
|
2347
|
-
let
|
|
2348
|
-
let
|
|
2422
|
+
const duplicateDeviceLogAt = new Map();
|
|
2423
|
+
let server = null;
|
|
2424
|
+
let started = false;
|
|
2425
|
+
let deviceConnectionSweepTimer = null;
|
|
2426
|
+
let boundPort = requestedPort;
|
|
2427
|
+
let lastError = '';
|
|
2428
|
+
let hostTarget = null;
|
|
2349
2429
|
let publicIPv4Cache = {
|
|
2350
2430
|
address: safeString(env.LIVEDESK_REMOTE_PUBLIC_IPV4 || env.LIVEDESK_PUBLIC_IPV4 || env.REMOTE_HUB_PUBLIC_IPV4, 64),
|
|
2351
2431
|
updatedAt: 0,
|
|
@@ -2666,12 +2746,14 @@ export function createRemoteHub(options = {}) {
|
|
|
2666
2746
|
protocol: 'tcp-jsonl',
|
|
2667
2747
|
protocolVersion: REMOTE_PROTOCOL_VERSION,
|
|
2668
2748
|
agentProtocol: REMOTE_AGENT_PROTOCOL,
|
|
2669
|
-
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2670
|
-
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2671
|
-
heartbeatMs,
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2749
|
+
frameProtocol: buildRemoteFrameProtocolDescriptor(),
|
|
2750
|
+
frameModes: getSupportedRemoteFrameModeProfiles(),
|
|
2751
|
+
heartbeatMs,
|
|
2752
|
+
deviceConnectionStaleMs,
|
|
2753
|
+
deviceConnectionSweepMs,
|
|
2754
|
+
taskTimeoutMs,
|
|
2755
|
+
managerPackage,
|
|
2756
|
+
managerVersion,
|
|
2675
2757
|
hostInstanceId,
|
|
2676
2758
|
agentPackage: '@livedesk/client',
|
|
2677
2759
|
agentEndpoint: routeInfo.endpoint,
|
|
@@ -2934,7 +3016,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2934
3016
|
return true;
|
|
2935
3017
|
}
|
|
2936
3018
|
|
|
2937
|
-
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
3019
|
+
function failPendingCommandResultWaiters(device, error = 'device-disconnected') {
|
|
2938
3020
|
if (!device?.deviceId) {
|
|
2939
3021
|
return;
|
|
2940
3022
|
}
|
|
@@ -2951,10 +3033,31 @@ export function createRemoteHub(options = {}) {
|
|
|
2951
3033
|
result: null,
|
|
2952
3034
|
error: safeString(error, 500) || 'command-result-unavailable'
|
|
2953
3035
|
});
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
function
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
function failPendingFileCommandResultWaiters(device, error = 'file-channel-disconnected') {
|
|
3040
|
+
if (!device?.deviceId) {
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
for (const [waiterKey, waiter] of pendingCommandResultWaiters) {
|
|
3044
|
+
if (waiter.deviceId !== device.deviceId
|
|
3045
|
+
|| waiter.sessionId !== device.sessionId
|
|
3046
|
+
|| waiter.channel !== 'file') {
|
|
3047
|
+
continue;
|
|
3048
|
+
}
|
|
3049
|
+
pendingCommandResultWaiters.delete(waiterKey);
|
|
3050
|
+
clearPendingCommandResultWaiterDeadline(waiter);
|
|
3051
|
+
waiter.resolve({
|
|
3052
|
+
ok: false,
|
|
3053
|
+
commandId: waiter.commandId,
|
|
3054
|
+
result: null,
|
|
3055
|
+
error: safeString(error, 500) || 'file-channel-disconnected'
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function failPendingCommandResultWaiter(device, commandId, error = 'command-not-sent') {
|
|
2958
3061
|
const normalizedCommandId = safeString(commandId, 128);
|
|
2959
3062
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
2960
3063
|
device?.deviceId,
|
|
@@ -2975,7 +3078,7 @@ export function createRemoteHub(options = {}) {
|
|
|
2975
3078
|
return true;
|
|
2976
3079
|
}
|
|
2977
3080
|
|
|
2978
|
-
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs) {
|
|
3081
|
+
function waitForCommandResult(device, commandId, timeoutMs = liveCaptureStopAckTimeoutMs, metadata = {}) {
|
|
2979
3082
|
const normalizedCommandId = safeString(commandId, 128);
|
|
2980
3083
|
const waiterKey = buildPendingCommandResultWaiterKey(
|
|
2981
3084
|
device?.deviceId,
|
|
@@ -3001,11 +3104,13 @@ export function createRemoteHub(options = {}) {
|
|
|
3001
3104
|
error: 'command-result-wait-replaced'
|
|
3002
3105
|
});
|
|
3003
3106
|
}
|
|
3004
|
-
const waiterRecord = {
|
|
3005
|
-
deviceId: device.deviceId,
|
|
3006
|
-
sessionId: device.sessionId,
|
|
3007
|
-
commandId: normalizedCommandId,
|
|
3008
|
-
|
|
3107
|
+
const waiterRecord = {
|
|
3108
|
+
deviceId: device.deviceId,
|
|
3109
|
+
sessionId: device.sessionId,
|
|
3110
|
+
commandId: normalizedCommandId,
|
|
3111
|
+
channel: safeString(metadata.channel, 24),
|
|
3112
|
+
operationId: safeString(metadata.operationId, 128),
|
|
3113
|
+
timer: null,
|
|
3009
3114
|
deadlineImmediate: null,
|
|
3010
3115
|
resolve
|
|
3011
3116
|
};
|
|
@@ -3038,9 +3143,246 @@ export function createRemoteHub(options = {}) {
|
|
|
3038
3143
|
});
|
|
3039
3144
|
}, timeoutMs);
|
|
3040
3145
|
waiterRecord.timer.unref?.();
|
|
3041
|
-
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3042
|
-
});
|
|
3043
|
-
}
|
|
3146
|
+
pendingCommandResultWaiters.set(waiterKey, waiterRecord);
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function buildClipboardOperationKey(deviceId, sessionId, operationId) {
|
|
3151
|
+
const normalizedDeviceId = safeString(deviceId, 160);
|
|
3152
|
+
const normalizedSessionId = safeString(sessionId, 160);
|
|
3153
|
+
const normalizedOperationId = safeString(operationId, 128);
|
|
3154
|
+
return normalizedDeviceId && normalizedSessionId && normalizedOperationId
|
|
3155
|
+
? `${normalizedDeviceId.length}:${normalizedDeviceId}`
|
|
3156
|
+
+ `${normalizedSessionId.length}:${normalizedSessionId}`
|
|
3157
|
+
+ `${normalizedOperationId.length}:${normalizedOperationId}`
|
|
3158
|
+
: '';
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function queueClipboardAgentCleanup(device, operation, reason) {
|
|
3162
|
+
if (operation?.inFlight || !device?.fileSocket || device.fileSocket.destroyed) {
|
|
3163
|
+
return false;
|
|
3164
|
+
}
|
|
3165
|
+
const cleanupCommand = operation.direction === 'paste'
|
|
3166
|
+
? 'clipboard.paste.cancel'
|
|
3167
|
+
: 'clipboard.copy.end';
|
|
3168
|
+
const commandId = crypto.randomUUID();
|
|
3169
|
+
try {
|
|
3170
|
+
writeJsonLine(device.fileSocket, {
|
|
3171
|
+
type: 'command',
|
|
3172
|
+
commandId,
|
|
3173
|
+
command: cleanupCommand,
|
|
3174
|
+
payload: {
|
|
3175
|
+
operationId: operation.operationId,
|
|
3176
|
+
reason: safeString(reason, 120) || 'clipboard-owner-closed'
|
|
3177
|
+
},
|
|
3178
|
+
issuedAt: new Date().toISOString()
|
|
3179
|
+
});
|
|
3180
|
+
device.counters.commandsSent += 1;
|
|
3181
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
3182
|
+
commandId,
|
|
3183
|
+
command: cleanupCommand,
|
|
3184
|
+
channel: 'file',
|
|
3185
|
+
operationId: operation.operationId,
|
|
3186
|
+
cleanup: true
|
|
3187
|
+
});
|
|
3188
|
+
return true;
|
|
3189
|
+
} catch {
|
|
3190
|
+
// The exact side channel is already closing. Native session
|
|
3191
|
+
// teardown remains the authoritative cleanup.
|
|
3192
|
+
return false;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function clearClipboardOperationsForDevice(device, reason = 'clipboard-owner-closed', ownerConnectionId = '') {
|
|
3197
|
+
const requestedOwner = safeString(ownerConnectionId, 128);
|
|
3198
|
+
let cleared = 0;
|
|
3199
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3200
|
+
if (operation.deviceId !== device?.deviceId
|
|
3201
|
+
|| operation.sessionId !== device?.sessionId
|
|
3202
|
+
|| (requestedOwner && operation.hubConnectionId !== requestedOwner)) {
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
queueClipboardAgentCleanup(device, operation, reason);
|
|
3206
|
+
clipboardOperations.delete(key);
|
|
3207
|
+
cleared += 1;
|
|
3208
|
+
}
|
|
3209
|
+
if (cleared > 0) {
|
|
3210
|
+
emitRemoteEvent('RemoteClipboardOperationsCleared', device, {
|
|
3211
|
+
reason,
|
|
3212
|
+
ownerConnectionId: requestedOwner,
|
|
3213
|
+
count: cleared
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
return cleared;
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function pruneClipboardOperations(nowMs = Date.now()) {
|
|
3220
|
+
for (const [key, operation] of clipboardOperations) {
|
|
3221
|
+
const currentDevice = devices.get(operation.deviceId);
|
|
3222
|
+
const expired = nowMs - Number(operation.touchedAt || 0)
|
|
3223
|
+
> REMOTE_CLIPBOARD_LIMITS.operationIdleTimeoutMs;
|
|
3224
|
+
const staleOwner = !currentDevice
|
|
3225
|
+
|| currentDevice.sessionId !== operation.sessionId
|
|
3226
|
+
|| safeString(currentDevice.inputOwnerConnectionId, 128) !== operation.hubConnectionId;
|
|
3227
|
+
if (expired || staleOwner) {
|
|
3228
|
+
if (currentDevice?.sessionId === operation.sessionId) {
|
|
3229
|
+
queueClipboardAgentCleanup(
|
|
3230
|
+
currentDevice,
|
|
3231
|
+
operation,
|
|
3232
|
+
expired ? 'clipboard-operation-expired' : 'clipboard-owner-stale');
|
|
3233
|
+
}
|
|
3234
|
+
clipboardOperations.delete(key);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
function clipboardCapabilityError(device, direction, contentKind = 'text') {
|
|
3240
|
+
const capabilities = device?.capabilities || {};
|
|
3241
|
+
if (capabilities.clipboardProtocol !== REMOTE_CLIPBOARD_PROTOCOL
|
|
3242
|
+
|| capabilities.clipboardText !== true) {
|
|
3243
|
+
return 'device-clipboard-text-unavailable';
|
|
3244
|
+
}
|
|
3245
|
+
if (capabilities.sessionBoundSideChannels !== true) {
|
|
3246
|
+
return 'device-clipboard-session-bound-channel-unavailable';
|
|
3247
|
+
}
|
|
3248
|
+
if (direction === 'copy' && capabilities.clipboardCopy !== true) {
|
|
3249
|
+
return 'device-clipboard-copy-unavailable';
|
|
3250
|
+
}
|
|
3251
|
+
if (direction === 'paste' && capabilities.clipboardPaste !== true) {
|
|
3252
|
+
return 'device-clipboard-paste-unavailable';
|
|
3253
|
+
}
|
|
3254
|
+
if (contentKind === 'image' && capabilities.clipboardImage !== true) {
|
|
3255
|
+
return 'device-clipboard-image-unavailable';
|
|
3256
|
+
}
|
|
3257
|
+
if (contentKind === 'files' && capabilities.clipboardFiles !== true) {
|
|
3258
|
+
return 'device-clipboard-files-unavailable';
|
|
3259
|
+
}
|
|
3260
|
+
for (const key of ['clipboardMaxTextBytes', 'clipboardMaxTotalBytes', 'clipboardMaxChunkBytes']) {
|
|
3261
|
+
if (!Number.isSafeInteger(Number(capabilities[key])) || Number(capabilities[key]) <= 0) {
|
|
3262
|
+
return 'device-clipboard-limits-unavailable';
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
if (Number(capabilities.clipboardMaxChunkBytes) < REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
3266
|
+
return 'device-clipboard-chunk-limit-unavailable';
|
|
3267
|
+
}
|
|
3268
|
+
if (contentKind === 'image'
|
|
3269
|
+
&& (!Number.isSafeInteger(Number(
|
|
3270
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes))
|
|
3271
|
+
|| Number(capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes) <= 0)) {
|
|
3272
|
+
return 'device-clipboard-limits-unavailable';
|
|
3273
|
+
}
|
|
3274
|
+
if (contentKind === 'files'
|
|
3275
|
+
&& (!Number.isSafeInteger(Number(capabilities.clipboardMaxFiles))
|
|
3276
|
+
|| Number(capabilities.clipboardMaxFiles) <= 0)) {
|
|
3277
|
+
return 'device-clipboard-limits-unavailable';
|
|
3278
|
+
}
|
|
3279
|
+
return '';
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function clipboardManifestCapabilityError(device, manifest) {
|
|
3283
|
+
if (!manifest) {
|
|
3284
|
+
return '';
|
|
3285
|
+
}
|
|
3286
|
+
const capabilities = device?.capabilities || {};
|
|
3287
|
+
if (manifest.totalBytes > Number(capabilities.clipboardMaxTotalBytes || 0)) {
|
|
3288
|
+
return 'device-clipboard-total-limit-exceeded';
|
|
3289
|
+
}
|
|
3290
|
+
if (manifest.contentKind === 'text'
|
|
3291
|
+
&& manifest.totalBytes > Number(capabilities.clipboardMaxTextBytes || 0)) {
|
|
3292
|
+
return 'device-clipboard-text-limit-exceeded';
|
|
3293
|
+
}
|
|
3294
|
+
if (manifest.contentKind === 'image'
|
|
3295
|
+
&& manifest.totalBytes > Number(
|
|
3296
|
+
capabilities.clipboardMaxImageBytes ?? capabilities.clipboardMaxPngBytes ?? 0)) {
|
|
3297
|
+
return 'device-clipboard-image-limit-exceeded';
|
|
3298
|
+
}
|
|
3299
|
+
if (manifest.contentKind === 'files'
|
|
3300
|
+
&& manifest.items.length > Number(capabilities.clipboardMaxFiles || 0)) {
|
|
3301
|
+
return 'device-clipboard-file-count-limit-exceeded';
|
|
3302
|
+
}
|
|
3303
|
+
return '';
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function validateCurrentClipboardBinding(deviceId, request = {}, contentKind = 'text', options = {}) {
|
|
3307
|
+
const device = devices.get(String(deviceId || ''));
|
|
3308
|
+
if (!device) {
|
|
3309
|
+
return { ok: false, error: 'device-not-found' };
|
|
3310
|
+
}
|
|
3311
|
+
if (device.synthetic === true) {
|
|
3312
|
+
return { ok: false, error: 'device-clipboard-unavailable' };
|
|
3313
|
+
}
|
|
3314
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
3315
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
3316
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3317
|
+
}
|
|
3318
|
+
if (!device.connected || !device.socket || device.socket.destroyed) {
|
|
3319
|
+
return { ok: false, error: 'device-not-connected' };
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
const policy = getDevicePolicy(device);
|
|
3323
|
+
if (policy.accessMode === 'block-remote-access') {
|
|
3324
|
+
return { ok: false, error: 'remote-access-blocked-by-settings' };
|
|
3325
|
+
}
|
|
3326
|
+
if (policy.allowControl !== true) {
|
|
3327
|
+
return { ok: false, error: 'allowControl-blocked-by-settings' };
|
|
3328
|
+
}
|
|
3329
|
+
if (policy.allowClipboardText !== true) {
|
|
3330
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
3331
|
+
}
|
|
3332
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
3333
|
+
&& policy.allowFileTransfer !== true) {
|
|
3334
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
const direction = request.clipboardDirection
|
|
3338
|
+
|| (String(request.action || '').includes('.paste') ? 'paste' : 'copy');
|
|
3339
|
+
const capabilityError = clipboardCapabilityError(device, direction, contentKind);
|
|
3340
|
+
if (capabilityError) {
|
|
3341
|
+
return { ok: false, error: capabilityError };
|
|
3342
|
+
}
|
|
3343
|
+
const manifestCapabilityError = clipboardManifestCapabilityError(
|
|
3344
|
+
device,
|
|
3345
|
+
request.payload?.manifest || request.manifest);
|
|
3346
|
+
if (manifestCapabilityError) {
|
|
3347
|
+
return { ok: false, error: manifestCapabilityError };
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
const controlStream = getActiveControlStream(device);
|
|
3351
|
+
if (!controlStream
|
|
3352
|
+
|| !liveStreamHasCurrentFrame(controlStream)
|
|
3353
|
+
|| !safeString(controlStream.commandId, 128)
|
|
3354
|
+
|| Number(controlStream.captureGeneration || 0) <= 0) {
|
|
3355
|
+
return { ok: false, error: 'CONTROL_NOT_READY' };
|
|
3356
|
+
}
|
|
3357
|
+
if (getPendingLiveStreamDescriptor(controlStream)) {
|
|
3358
|
+
return { ok: false, error: 'CAPTURE_TRANSITION_IN_PROGRESS' };
|
|
3359
|
+
}
|
|
3360
|
+
const binding = request.binding || request;
|
|
3361
|
+
if (safeString(binding.controlSessionId || binding.sessionId, 160)
|
|
3362
|
+
!== safeString(device.sessionId, 160)
|
|
3363
|
+
|| safeString(binding.controlCommandId || binding.commandId, 128)
|
|
3364
|
+
!== safeString(controlStream.commandId, 128)) {
|
|
3365
|
+
return { ok: false, error: 'STALE_CONTROL_SESSION' };
|
|
3366
|
+
}
|
|
3367
|
+
if (Number(binding.captureGeneration || 0)
|
|
3368
|
+
!== Number(controlStream.captureGeneration || 0)) {
|
|
3369
|
+
return { ok: false, error: 'STALE_CAPTURE_GENERATION' };
|
|
3370
|
+
}
|
|
3371
|
+
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
3372
|
+
if (Number(binding.monitorIndex) !== activeMonitorIndex) {
|
|
3373
|
+
return { ok: false, error: 'STALE_CONTROL_MONITOR' };
|
|
3374
|
+
}
|
|
3375
|
+
const hubConnectionId = safeString(request.hubConnectionId, 128);
|
|
3376
|
+
if (!hubConnectionId
|
|
3377
|
+
|| hubConnectionId !== safeString(device.inputOwnerConnectionId, 128)) {
|
|
3378
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
3379
|
+
}
|
|
3380
|
+
if (options.requireFileSocket === true
|
|
3381
|
+
&& (!device.fileSocket || device.fileSocket.destroyed)) {
|
|
3382
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
3383
|
+
}
|
|
3384
|
+
return { ok: true, device, controlStream, activeMonitorIndex, policy };
|
|
3385
|
+
}
|
|
3044
3386
|
|
|
3045
3387
|
function inspectAnnexBH264(payload) {
|
|
3046
3388
|
if (!Buffer.isBuffer(payload) || payload.length < 5) {
|
|
@@ -3922,13 +4264,15 @@ export function createRemoteHub(options = {}) {
|
|
|
3922
4264
|
}
|
|
3923
4265
|
}
|
|
3924
4266
|
|
|
3925
|
-
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
3926
|
-
const socket = device?.fileSocket;
|
|
3927
|
-
if (!socket) {
|
|
3928
|
-
return;
|
|
3929
|
-
}
|
|
3930
|
-
|
|
3931
|
-
device
|
|
4267
|
+
function closeFileSocket(device, reason = 'file-socket-closed') {
|
|
4268
|
+
const socket = device?.fileSocket;
|
|
4269
|
+
if (!socket) {
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4274
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4275
|
+
device.fileSocket = null;
|
|
3932
4276
|
fileSockets.delete(socket);
|
|
3933
4277
|
try {
|
|
3934
4278
|
writeJsonLine(socket, { type: 'disconnect', channel: 'file', reason });
|
|
@@ -3942,13 +4286,14 @@ export function createRemoteHub(options = {}) {
|
|
|
3942
4286
|
}
|
|
3943
4287
|
}
|
|
3944
4288
|
|
|
3945
|
-
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
4289
|
+
function closeInputSocket(device, reason = 'input-socket-closed') {
|
|
3946
4290
|
const socket = device?.inputSocket;
|
|
3947
4291
|
if (!socket) {
|
|
3948
4292
|
return;
|
|
3949
4293
|
}
|
|
3950
4294
|
|
|
3951
|
-
const writeOwner = device.inputWriteOwner;
|
|
4295
|
+
const writeOwner = device.inputWriteOwner;
|
|
4296
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
3952
4297
|
device.inputSocket = null;
|
|
3953
4298
|
device.inputWriteOwner = null;
|
|
3954
4299
|
device.inputSocketConnectionId = '';
|
|
@@ -3968,7 +4313,7 @@ export function createRemoteHub(options = {}) {
|
|
|
3968
4313
|
}
|
|
3969
4314
|
}
|
|
3970
4315
|
|
|
3971
|
-
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
4316
|
+
function detachInputSocket(socket, reason = 'input-socket-closed') {
|
|
3972
4317
|
const deviceId = inputSockets.get(socket);
|
|
3973
4318
|
inputSockets.delete(socket);
|
|
3974
4319
|
if (!deviceId) {
|
|
@@ -3980,7 +4325,8 @@ export function createRemoteHub(options = {}) {
|
|
|
3980
4325
|
return;
|
|
3981
4326
|
}
|
|
3982
4327
|
|
|
3983
|
-
const writeOwner = device.inputWriteOwner;
|
|
4328
|
+
const writeOwner = device.inputWriteOwner;
|
|
4329
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
3984
4330
|
device.inputSocket = null;
|
|
3985
4331
|
device.inputWriteOwner = null;
|
|
3986
4332
|
device.inputSocketConnectionId = '';
|
|
@@ -4025,7 +4371,7 @@ export function createRemoteHub(options = {}) {
|
|
|
4025
4371
|
emitRemoteEvent('RemoteAudioSocketDisconnected', device, { reason });
|
|
4026
4372
|
}
|
|
4027
4373
|
|
|
4028
|
-
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4374
|
+
function detachFileSocket(socket, reason = 'file-socket-closed') {
|
|
4029
4375
|
const deviceId = fileSockets.get(socket);
|
|
4030
4376
|
fileSockets.delete(socket);
|
|
4031
4377
|
if (!deviceId) {
|
|
@@ -4033,38 +4379,107 @@ export function createRemoteHub(options = {}) {
|
|
|
4033
4379
|
}
|
|
4034
4380
|
|
|
4035
4381
|
const device = devices.get(deviceId);
|
|
4036
|
-
if (!device || device.fileSocket !== socket) {
|
|
4037
|
-
return;
|
|
4038
|
-
}
|
|
4039
|
-
|
|
4040
|
-
device
|
|
4382
|
+
if (!device || device.fileSocket !== socket) {
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
clearClipboardOperationsForDevice(device, reason);
|
|
4387
|
+
failPendingFileCommandResultWaiters(device, 'clipboard-file-channel-disconnected');
|
|
4388
|
+
device.fileSocket = null;
|
|
4041
4389
|
device.fileLastSeenAt = new Date().toISOString();
|
|
4042
|
-
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
function
|
|
4046
|
-
return
|
|
4047
|
-
Date.parse(device?.
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4390
|
+
emitRemoteEvent('RemoteFileSocketDisconnected', device, { reason });
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function getDeviceConnectionHeartbeatMs(device) {
|
|
4394
|
+
return Date.parse(device?.lastStatusAt || '')
|
|
4395
|
+
|| Date.parse(device?.connectedAt || '')
|
|
4396
|
+
|| 0;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
function isDeviceConnectionFresh(device, nowMs = Date.now()) {
|
|
4054
4400
|
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4055
|
-
return false;
|
|
4056
|
-
}
|
|
4057
|
-
|
|
4058
|
-
const
|
|
4059
|
-
if (!
|
|
4060
|
-
return true;
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4401
|
+
return false;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
const lastHeartbeatMs = getDeviceConnectionHeartbeatMs(device);
|
|
4405
|
+
if (!lastHeartbeatMs) {
|
|
4406
|
+
return true;
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
return nowMs - lastHeartbeatMs <= deviceConnectionStaleMs;
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
function retireStaleDeviceConnection(device, reason = 'status-heartbeat-timeout') {
|
|
4413
|
+
const deviceId = safeString(device?.deviceId, 160);
|
|
4414
|
+
const sessionId = safeString(device?.sessionId, 160);
|
|
4415
|
+
const socket = device?.socket;
|
|
4416
|
+
if (!deviceId
|
|
4417
|
+
|| !sessionId
|
|
4418
|
+
|| !socket
|
|
4419
|
+
|| socket.destroyed
|
|
4420
|
+
|| devices.get(deviceId) !== device
|
|
4421
|
+
|| device.socket !== socket
|
|
4422
|
+
|| device.sessionId !== sessionId) {
|
|
4423
|
+
return false;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
closeInputSocket(device, reason);
|
|
4427
|
+
closeFrameSocket(device, reason);
|
|
4428
|
+
closeAudioSocket(device, reason);
|
|
4429
|
+
closeFileSocket(device, reason);
|
|
4430
|
+
try {
|
|
4431
|
+
writeJsonLine(socket, { type: 'disconnect', reason, sessionId });
|
|
4432
|
+
} catch {
|
|
4433
|
+
// The stale connection is retired below even if the final notice fails.
|
|
4434
|
+
}
|
|
4435
|
+
detachSocket(socket, reason);
|
|
4436
|
+
try {
|
|
4437
|
+
socket.destroy?.();
|
|
4438
|
+
} catch {
|
|
4439
|
+
// detachSocket already made the exact session terminal.
|
|
4440
|
+
}
|
|
4441
|
+
return true;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
function sweepStaleDeviceConnections(nowMs = Date.now()) {
|
|
4445
|
+
pruneClipboardOperations(nowMs);
|
|
4446
|
+
let retired = 0;
|
|
4447
|
+
for (const device of devices.values()) {
|
|
4448
|
+
if (device?.synthetic === true || isDeviceConnectionFresh(device, nowMs)) {
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
const ageMs = Math.max(0, nowMs - getDeviceConnectionHeartbeatMs(device));
|
|
4455
|
+
if (retireStaleDeviceConnection(device, 'status-heartbeat-timeout')) {
|
|
4456
|
+
retired += 1;
|
|
4457
|
+
logWarn(
|
|
4458
|
+
'remote',
|
|
4459
|
+
`retired stale device session ${device.deviceName} (${device.deviceId}) `
|
|
4460
|
+
+ `session=${device.sessionId} heartbeatAgeMs=${ageMs} `
|
|
4461
|
+
+ `staleAfterMs=${deviceConnectionStaleMs}`);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
return retired;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
function scheduleDeviceConnectionSweep() {
|
|
4468
|
+
if (!started || deviceConnectionSweepTimer) {
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
deviceConnectionSweepTimer = setTimeout(() => {
|
|
4472
|
+
deviceConnectionSweepTimer = null;
|
|
4473
|
+
if (!started) {
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
sweepStaleDeviceConnections();
|
|
4477
|
+
scheduleDeviceConnectionSweep();
|
|
4478
|
+
}, deviceConnectionSweepMs);
|
|
4479
|
+
deviceConnectionSweepTimer.unref?.();
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
function rejectDuplicateActiveDevice(socket, existing, attemptedSessionId) {
|
|
4068
4483
|
const retryAfterMs = Math.max(heartbeatMs, duplicateDeviceRetryAfterMs);
|
|
4069
4484
|
writeJsonLine(socket, {
|
|
4070
4485
|
type: 'disconnect',
|
|
@@ -4516,16 +4931,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4516
4931
|
return device;
|
|
4517
4932
|
}
|
|
4518
4933
|
|
|
4519
|
-
function attachInputSocket(socket, hello) {
|
|
4520
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4521
|
-
const device = devices.get(deviceId);
|
|
4934
|
+
function attachInputSocket(socket, hello) {
|
|
4935
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4936
|
+
const device = devices.get(deviceId);
|
|
4522
4937
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4523
4938
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4524
4939
|
socket.destroy();
|
|
4525
|
-
return null;
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
|
-
|
|
4940
|
+
return null;
|
|
4941
|
+
}
|
|
4942
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
4943
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
closeInputSocket(device, 'replaced-by-new-input-socket');
|
|
4529
4948
|
|
|
4530
4949
|
const now = new Date().toISOString();
|
|
4531
4950
|
const inputSocketConnectionId = crypto.randomUUID();
|
|
@@ -4639,16 +5058,20 @@ export function createRemoteHub(options = {}) {
|
|
|
4639
5058
|
return device;
|
|
4640
5059
|
}
|
|
4641
5060
|
|
|
4642
|
-
function attachFileSocket(socket, hello) {
|
|
4643
|
-
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
4644
|
-
const device = devices.get(deviceId);
|
|
5061
|
+
function attachFileSocket(socket, hello) {
|
|
5062
|
+
const deviceId = normalizeDeviceId(hello.deviceId);
|
|
5063
|
+
const device = devices.get(deviceId);
|
|
4645
5064
|
if (!device || !device.connected || !device.socket || device.socket.destroyed) {
|
|
4646
5065
|
writeJsonLine(socket, { type: 'error', error: 'device-not-connected' });
|
|
4647
5066
|
socket.destroy();
|
|
4648
|
-
return null;
|
|
4649
|
-
}
|
|
4650
|
-
|
|
4651
|
-
|
|
5067
|
+
return null;
|
|
5068
|
+
}
|
|
5069
|
+
if (safeString(hello.parentSessionId, 160) !== safeString(device.sessionId, 160)) {
|
|
5070
|
+
writeJsonLineAndClose(socket, { type: 'error', error: 'side-channel-parent-session-stale' });
|
|
5071
|
+
return null;
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
closeFileSocket(device, 'replaced-by-new-file-socket');
|
|
4652
5075
|
const now = new Date().toISOString();
|
|
4653
5076
|
device.fileSocket = socket;
|
|
4654
5077
|
device.fileConnectedAt = now;
|
|
@@ -7971,12 +8394,13 @@ export function createRemoteHub(options = {}) {
|
|
|
7971
8394
|
await listenOnPort(requestedPort);
|
|
7972
8395
|
try {
|
|
7973
8396
|
await relayControl?.start?.();
|
|
7974
|
-
} catch {
|
|
7975
|
-
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
7976
|
-
}
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
8397
|
+
} catch {
|
|
8398
|
+
logWarn('relay', 'Encrypted relay control could not start; direct TCP remains available.');
|
|
8399
|
+
}
|
|
8400
|
+
scheduleDeviceConnectionSweep();
|
|
8401
|
+
} catch (error) {
|
|
8402
|
+
if (error?.code === 'EADDRINUSE') {
|
|
8403
|
+
const conflict = new Error(
|
|
7980
8404
|
`LiveDesk Hub requires client endpoint TCP ${requestedPort}, but that port is already in use. Close the owning process or start Hub with an explicit --remote-port value.`);
|
|
7981
8405
|
conflict.code = 'EADDRINUSE';
|
|
7982
8406
|
conflict.port = requestedPort;
|
|
@@ -7988,12 +8412,16 @@ export function createRemoteHub(options = {}) {
|
|
|
7988
8412
|
}
|
|
7989
8413
|
|
|
7990
8414
|
return getStatus({ includeSecrets: false });
|
|
7991
|
-
}
|
|
7992
|
-
|
|
7993
|
-
async function close() {
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8417
|
+
async function close() {
|
|
8418
|
+
if (deviceConnectionSweepTimer) {
|
|
8419
|
+
clearTimeout(deviceConnectionSweepTimer);
|
|
8420
|
+
deviceConnectionSweepTimer = null;
|
|
8421
|
+
}
|
|
8422
|
+
for (const state of [...agentBinaryIngressStates]) {
|
|
8423
|
+
closeAgentBinaryIngress(state, 'hub-shutdown');
|
|
8424
|
+
}
|
|
7997
8425
|
|
|
7998
8426
|
for (const device of devices.values()) {
|
|
7999
8427
|
abortTransportDiagnosticsForDevice(device, 'hub-shutdown');
|
|
@@ -8128,9 +8556,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8128
8556
|
};
|
|
8129
8557
|
}
|
|
8130
8558
|
|
|
8131
|
-
function sendCommand(deviceId, command) {
|
|
8132
|
-
const device = devices.get(String(deviceId || ''));
|
|
8133
|
-
const commandName = safeString(command?.command || 'ping', 80);
|
|
8559
|
+
function sendCommand(deviceId, command) {
|
|
8560
|
+
const device = devices.get(String(deviceId || ''));
|
|
8561
|
+
const commandName = safeString(command?.command || 'ping', 80);
|
|
8562
|
+
if (REMOTE_CLIPBOARD_FILE_ACTIONS.has(commandName)) {
|
|
8563
|
+
return { ok: false, error: 'clipboard-command-requires-awaited-file-channel' };
|
|
8564
|
+
}
|
|
8134
8565
|
const requiredPermission = commandName === 'input.control'
|
|
8135
8566
|
? 'allowControl'
|
|
8136
8567
|
: commandName === 'system.power'
|
|
@@ -8166,12 +8597,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8166
8597
|
result: { ok: true, synthetic: true },
|
|
8167
8598
|
error: ''
|
|
8168
8599
|
});
|
|
8169
|
-
return { ok: true, commandId, synthetic: true };
|
|
8170
|
-
}
|
|
8171
|
-
|
|
8172
|
-
if (
|
|
8173
|
-
|
|
8174
|
-
|
|
8600
|
+
return { ok: true, commandId, synthetic: true };
|
|
8601
|
+
}
|
|
8602
|
+
|
|
8603
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
8604
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8605
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8606
|
+
}
|
|
8607
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8608
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8609
|
+
}
|
|
8175
8610
|
|
|
8176
8611
|
const commandId = safeString(command?.commandId, 128) || crypto.randomUUID();
|
|
8177
8612
|
const payload = {
|
|
@@ -8184,29 +8619,300 @@ export function createRemoteHub(options = {}) {
|
|
|
8184
8619
|
|
|
8185
8620
|
const dedicatedFileSocket = commandName.startsWith('file.transfer')
|
|
8186
8621
|
&& device.fileSocket
|
|
8187
|
-
&& !device.fileSocket.destroyed
|
|
8188
|
-
? device.fileSocket
|
|
8189
|
-
: null;
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8622
|
+
&& !device.fileSocket.destroyed
|
|
8623
|
+
? device.fileSocket
|
|
8624
|
+
: null;
|
|
8625
|
+
try {
|
|
8626
|
+
writeJsonLine(dedicatedFileSocket || device.socket, payload);
|
|
8627
|
+
} catch {
|
|
8628
|
+
retireStaleDeviceConnection(device, 'command-channel-write-failed');
|
|
8629
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8630
|
+
}
|
|
8631
|
+
device.counters.commandsSent += 1;
|
|
8632
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8633
|
+
commandId,
|
|
8194
8634
|
command: payload.command,
|
|
8195
8635
|
channel: dedicatedFileSocket ? 'file' : 'control'
|
|
8196
8636
|
});
|
|
8197
|
-
return { ok: true, commandId };
|
|
8198
|
-
}
|
|
8199
|
-
|
|
8200
|
-
function
|
|
8637
|
+
return { ok: true, commandId };
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
async function sendClipboardCommand(deviceId, request = {}) {
|
|
8641
|
+
pruneClipboardOperations();
|
|
8642
|
+
const action = safeString(request.action, 80).toLowerCase();
|
|
8643
|
+
if (!REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
8644
|
+
return { ok: false, error: 'clipboard-file-action-invalid' };
|
|
8645
|
+
}
|
|
8646
|
+
const operationId = safeString(request.operationId, 128);
|
|
8647
|
+
if (!operationId) {
|
|
8648
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
8649
|
+
}
|
|
8650
|
+
|
|
8651
|
+
const direction = action.startsWith('clipboard.paste.')
|
|
8652
|
+
? 'paste'
|
|
8653
|
+
: action.startsWith('clipboard.copy.')
|
|
8654
|
+
? 'copy'
|
|
8655
|
+
: '';
|
|
8656
|
+
const operationKey = buildClipboardOperationKey(deviceId, request.binding?.controlSessionId, operationId);
|
|
8657
|
+
let operation = clipboardOperations.get(operationKey);
|
|
8658
|
+
const beginsOperation = action === 'clipboard.copy.begin' || action === 'clipboard.paste.begin';
|
|
8659
|
+
const endsOperation = action === 'clipboard.copy.end' || action === 'clipboard.paste.cancel';
|
|
8660
|
+
const requestedManifest = request.payload?.manifest || null;
|
|
8661
|
+
|
|
8662
|
+
if (beginsOperation) {
|
|
8663
|
+
if (operation) {
|
|
8664
|
+
return { ok: false, error: 'clipboard-operation-already-exists' };
|
|
8665
|
+
}
|
|
8666
|
+
const activeForDevice = [...clipboardOperations.values()].filter(candidate =>
|
|
8667
|
+
candidate.deviceId === String(deviceId || '')
|
|
8668
|
+
&& candidate.sessionId === request.binding?.controlSessionId);
|
|
8669
|
+
if (activeForDevice.some(candidate => candidate.direction === direction)) {
|
|
8670
|
+
return { ok: false, error: 'clipboard-operation-direction-busy' };
|
|
8671
|
+
}
|
|
8672
|
+
if (activeForDevice.length >= REMOTE_CLIPBOARD_LIMITS.maxOperationsPerDevice) {
|
|
8673
|
+
return { ok: false, error: 'clipboard-operation-capacity-reached' };
|
|
8674
|
+
}
|
|
8675
|
+
operation = {
|
|
8676
|
+
deviceId: String(deviceId || ''),
|
|
8677
|
+
sessionId: safeString(request.binding?.controlSessionId, 160),
|
|
8678
|
+
operationId,
|
|
8679
|
+
direction,
|
|
8680
|
+
hubConnectionId: safeString(request.hubConnectionId, 128),
|
|
8681
|
+
controlCommandId: safeString(request.binding?.controlCommandId, 128),
|
|
8682
|
+
captureGeneration: Number(request.binding?.captureGeneration || 0),
|
|
8683
|
+
monitorIndex: Number(request.binding?.monitorIndex),
|
|
8684
|
+
contentKind: requestedManifest?.contentKind || 'text',
|
|
8685
|
+
manifest: requestedManifest,
|
|
8686
|
+
offsets: new Map(),
|
|
8687
|
+
status: action === 'clipboard.paste.begin' ? 'preparing' : 'copying',
|
|
8688
|
+
inFlight: false,
|
|
8689
|
+
touchedAt: Date.now()
|
|
8690
|
+
};
|
|
8691
|
+
clipboardOperations.set(operationKey, operation);
|
|
8692
|
+
} else {
|
|
8693
|
+
if (!operation) {
|
|
8694
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8695
|
+
}
|
|
8696
|
+
const requestedDirection = direction || operation.direction;
|
|
8697
|
+
if (operation.direction !== requestedDirection
|
|
8698
|
+
|| operation.hubConnectionId !== safeString(request.hubConnectionId, 128)
|
|
8699
|
+
|| operation.controlCommandId !== safeString(request.binding?.controlCommandId, 128)
|
|
8700
|
+
|| operation.captureGeneration !== Number(request.binding?.captureGeneration || 0)
|
|
8701
|
+
|| operation.monitorIndex !== Number(request.binding?.monitorIndex)) {
|
|
8702
|
+
return { ok: false, error: 'clipboard-operation-owner-stale' };
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
|
|
8706
|
+
if (operation.inFlight) {
|
|
8707
|
+
return { ok: false, error: 'clipboard-operation-busy' };
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
const contentKind = operation.contentKind || requestedManifest?.contentKind || 'text';
|
|
8711
|
+
const bindingResult = validateCurrentClipboardBinding(
|
|
8712
|
+
deviceId,
|
|
8713
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8714
|
+
contentKind,
|
|
8715
|
+
{ requireFileSocket: true });
|
|
8716
|
+
if (!bindingResult.ok) {
|
|
8717
|
+
if (beginsOperation || bindingResult.error?.startsWith('STALE_') || bindingResult.error === 'input-owner-not-current') {
|
|
8718
|
+
clipboardOperations.delete(operationKey);
|
|
8719
|
+
}
|
|
8720
|
+
return bindingResult;
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8723
|
+
const { device } = bindingResult;
|
|
8724
|
+
const fileSocket = device.fileSocket;
|
|
8725
|
+
const itemIndex = request.payload?.itemIndex;
|
|
8726
|
+
const expectedOffset = Number.isInteger(itemIndex)
|
|
8727
|
+
? Number(operation.offsets.get(itemIndex) || 0)
|
|
8728
|
+
: 0;
|
|
8729
|
+
let outgoingChunkBytes = 0;
|
|
8730
|
+
if (action === 'clipboard.copy.chunk' || action === 'clipboard.paste.chunk') {
|
|
8731
|
+
const item = operation.manifest?.items?.[itemIndex];
|
|
8732
|
+
if (!item) {
|
|
8733
|
+
return { ok: false, error: 'clipboard-item-not-found' };
|
|
8734
|
+
}
|
|
8735
|
+
if (request.payload.offset !== expectedOffset) {
|
|
8736
|
+
return { ok: false, error: 'clipboard-chunk-offset-stale', expectedOffset };
|
|
8737
|
+
}
|
|
8738
|
+
if (action === 'clipboard.copy.chunk'
|
|
8739
|
+
&& request.payload.maxBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8740
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8741
|
+
}
|
|
8742
|
+
if (action === 'clipboard.paste.chunk') {
|
|
8743
|
+
outgoingChunkBytes = Buffer.from(request.payload.dataBase64, 'base64').length;
|
|
8744
|
+
if (outgoingChunkBytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8745
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8746
|
+
}
|
|
8747
|
+
const nextOffset = expectedOffset + outgoingChunkBytes;
|
|
8748
|
+
if (outgoingChunkBytes === 0
|
|
8749
|
+
&& !(item.size === 0 && expectedOffset === 0 && request.payload.final === true)) {
|
|
8750
|
+
return { ok: false, error: 'clipboard-empty-chunk-invalid' };
|
|
8751
|
+
}
|
|
8752
|
+
if (nextOffset > item.size
|
|
8753
|
+
|| request.payload.final !== (nextOffset === item.size)) {
|
|
8754
|
+
return { ok: false, error: 'clipboard-chunk-boundary-invalid' };
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
|
|
8759
|
+
operation.inFlight = true;
|
|
8760
|
+
operation.touchedAt = Date.now();
|
|
8761
|
+
let commandCompleted = false;
|
|
8762
|
+
const commandId = crypto.randomUUID();
|
|
8763
|
+
const resultPromise = waitForCommandResult(
|
|
8764
|
+
device,
|
|
8765
|
+
commandId,
|
|
8766
|
+
REMOTE_CLIPBOARD_LIMITS.commandTimeoutMs,
|
|
8767
|
+
{ channel: 'file', operationId });
|
|
8768
|
+
const commandPayload = {
|
|
8769
|
+
type: 'command',
|
|
8770
|
+
commandId,
|
|
8771
|
+
command: action,
|
|
8772
|
+
payload: {
|
|
8773
|
+
operationId,
|
|
8774
|
+
controlSessionId: operation.sessionId,
|
|
8775
|
+
controlCommandId: operation.controlCommandId,
|
|
8776
|
+
captureGeneration: operation.captureGeneration,
|
|
8777
|
+
monitorIndex: operation.monitorIndex,
|
|
8778
|
+
hubConnectionId: operation.hubConnectionId,
|
|
8779
|
+
...(request.payload || {})
|
|
8780
|
+
},
|
|
8781
|
+
issuedAt: new Date().toISOString()
|
|
8782
|
+
};
|
|
8783
|
+
|
|
8784
|
+
try {
|
|
8785
|
+
try {
|
|
8786
|
+
// Clipboard payloads are never allowed onto the concurrent
|
|
8787
|
+
// main command socket. The dedicated file side channel owns
|
|
8788
|
+
// the one bounded command and its exact-session waiter.
|
|
8789
|
+
writeJsonLine(fileSocket, commandPayload);
|
|
8790
|
+
} catch {
|
|
8791
|
+
failPendingCommandResultWaiter(device, commandId, 'clipboard-file-channel-write-failed');
|
|
8792
|
+
if (device.fileSocket === fileSocket) {
|
|
8793
|
+
closeFileSocket(device, 'clipboard-file-channel-write-failed');
|
|
8794
|
+
}
|
|
8795
|
+
return { ok: false, error: 'clipboard-file-channel-unavailable' };
|
|
8796
|
+
}
|
|
8797
|
+
device.counters.commandsSent += 1;
|
|
8798
|
+
emitRemoteEvent('RemoteCommandQueued', device, {
|
|
8799
|
+
commandId,
|
|
8800
|
+
command: action,
|
|
8801
|
+
channel: 'file',
|
|
8802
|
+
operationId
|
|
8803
|
+
});
|
|
8804
|
+
|
|
8805
|
+
const awaited = await resultPromise;
|
|
8806
|
+
if (!awaited.ok) {
|
|
8807
|
+
return {
|
|
8808
|
+
ok: false,
|
|
8809
|
+
error: awaited.error || 'clipboard-command-failed',
|
|
8810
|
+
commandId
|
|
8811
|
+
};
|
|
8812
|
+
}
|
|
8813
|
+
if (device.fileSocket !== fileSocket) {
|
|
8814
|
+
return { ok: false, error: 'clipboard-file-channel-owner-changed' };
|
|
8815
|
+
}
|
|
8816
|
+
const currentBinding = validateCurrentClipboardBinding(
|
|
8817
|
+
deviceId,
|
|
8818
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8819
|
+
operation.contentKind || 'text',
|
|
8820
|
+
{ requireFileSocket: true });
|
|
8821
|
+
if (!currentBinding.ok) {
|
|
8822
|
+
return currentBinding;
|
|
8823
|
+
}
|
|
8824
|
+
|
|
8825
|
+
let response;
|
|
8826
|
+
try {
|
|
8827
|
+
response = normalizeRemoteClipboardCommandResult(action, operationId, awaited.result);
|
|
8828
|
+
} catch (error) {
|
|
8829
|
+
return {
|
|
8830
|
+
ok: false,
|
|
8831
|
+
error: safeString(error?.code || error?.message, 160) || 'clipboard-response-invalid'
|
|
8832
|
+
};
|
|
8833
|
+
}
|
|
8834
|
+
if (response.manifest) {
|
|
8835
|
+
const responseManifestLimitError = clipboardManifestCapabilityError(device, response.manifest);
|
|
8836
|
+
if (responseManifestLimitError) {
|
|
8837
|
+
clipboardOperations.delete(operationKey);
|
|
8838
|
+
return { ok: false, error: responseManifestLimitError };
|
|
8839
|
+
}
|
|
8840
|
+
const responsePolicy = validateCurrentClipboardBinding(
|
|
8841
|
+
deviceId,
|
|
8842
|
+
{ ...request, clipboardDirection: operation.direction },
|
|
8843
|
+
response.manifest.contentKind,
|
|
8844
|
+
{ requireFileSocket: true });
|
|
8845
|
+
if (!responsePolicy.ok) {
|
|
8846
|
+
clipboardOperations.delete(operationKey);
|
|
8847
|
+
return responsePolicy;
|
|
8848
|
+
}
|
|
8849
|
+
operation.manifest = response.manifest;
|
|
8850
|
+
operation.contentKind = response.manifest.contentKind;
|
|
8851
|
+
}
|
|
8852
|
+
if (action === 'clipboard.copy.chunk') {
|
|
8853
|
+
const item = operation.manifest?.items?.[response.itemIndex];
|
|
8854
|
+
const nextOffset = response.offset + response.bytes;
|
|
8855
|
+
if (!item
|
|
8856
|
+
|| response.itemIndex !== itemIndex
|
|
8857
|
+
|| response.offset !== expectedOffset
|
|
8858
|
+
|| (response.bytes === 0
|
|
8859
|
+
&& !(item.size === 0 && response.offset === 0 && response.final === true))
|
|
8860
|
+
|| nextOffset > item.size
|
|
8861
|
+
|| response.final !== (nextOffset === item.size)) {
|
|
8862
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8863
|
+
}
|
|
8864
|
+
if (response.bytes > Number(device.capabilities?.clipboardMaxChunkBytes || 0)) {
|
|
8865
|
+
return { ok: false, error: 'device-clipboard-chunk-limit-exceeded' };
|
|
8866
|
+
}
|
|
8867
|
+
operation.offsets.set(response.itemIndex, nextOffset);
|
|
8868
|
+
} else if (action === 'clipboard.paste.chunk') {
|
|
8869
|
+
if (response.itemIndex !== itemIndex
|
|
8870
|
+
|| response.offset !== expectedOffset
|
|
8871
|
+
|| response.bytes !== outgoingChunkBytes
|
|
8872
|
+
|| response.totalBytes !== operation.manifest.items[itemIndex].size
|
|
8873
|
+
|| response.final !== request.payload.final) {
|
|
8874
|
+
return { ok: false, error: 'clipboard-chunk-response-boundary-invalid' };
|
|
8875
|
+
}
|
|
8876
|
+
operation.offsets.set(itemIndex, expectedOffset + outgoingChunkBytes);
|
|
8877
|
+
}
|
|
8878
|
+
operation.status = response.status || operation.status;
|
|
8879
|
+
operation.touchedAt = Date.now();
|
|
8880
|
+
if (action === 'clipboard.operation.status' && operation.status === 'not-found') {
|
|
8881
|
+
clipboardOperations.delete(operationKey);
|
|
8882
|
+
return { ok: false, error: 'clipboard-operation-not-found' };
|
|
8883
|
+
}
|
|
8884
|
+
if (action === 'clipboard.operation.status'
|
|
8885
|
+
&& ['completed', 'complete', 'applied', 'pasted', 'cancelled', 'cleaned', 'failed', 'expired', 'already-ended']
|
|
8886
|
+
.includes(operation.status)) {
|
|
8887
|
+
clipboardOperations.delete(operationKey);
|
|
8888
|
+
}
|
|
8889
|
+
commandCompleted = true;
|
|
8890
|
+
return response;
|
|
8891
|
+
} finally {
|
|
8892
|
+
const currentOperation = clipboardOperations.get(operationKey);
|
|
8893
|
+
if (currentOperation === operation) {
|
|
8894
|
+
operation.inFlight = false;
|
|
8895
|
+
if (endsOperation || (beginsOperation && !commandCompleted)) {
|
|
8896
|
+
clipboardOperations.delete(operationKey);
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
}
|
|
8901
|
+
|
|
8902
|
+
function refreshDevicePolicies(deviceIds = undefined) {
|
|
8201
8903
|
const requestedIds = Array.isArray(deviceIds) && deviceIds.length > 0
|
|
8202
8904
|
? new Set(deviceIds.map(deviceId => safeString(deviceId, 160)).filter(Boolean))
|
|
8203
8905
|
: null;
|
|
8204
8906
|
let updated = 0;
|
|
8205
8907
|
let total = 0;
|
|
8206
|
-
for (const device of devices.values()) {
|
|
8207
|
-
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8208
|
-
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8209
|
-
|
|
8908
|
+
for (const device of devices.values()) {
|
|
8909
|
+
if (requestedIds && !requestedIds.has(device.deviceId)) continue;
|
|
8910
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) continue;
|
|
8911
|
+
if (device.synthetic !== true && !isDeviceConnectionFresh(device)) {
|
|
8912
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8913
|
+
continue;
|
|
8914
|
+
}
|
|
8915
|
+
total += 1;
|
|
8210
8916
|
const effectivePolicy = getDevicePolicy(device);
|
|
8211
8917
|
if (writeJsonLine(device.socket, {
|
|
8212
8918
|
type: 'policy.update',
|
|
@@ -8225,8 +8931,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8225
8931
|
const device = devices.get(String(deviceId || ''));
|
|
8226
8932
|
const denied = policyError(device);
|
|
8227
8933
|
if (denied) return { ok: false, error: denied };
|
|
8228
|
-
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8229
|
-
if (
|
|
8934
|
+
if (!device) return { ok: false, error: 'device-not-found' };
|
|
8935
|
+
if (device.synthetic !== true
|
|
8936
|
+
&& device.connected
|
|
8937
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
8938
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
8939
|
+
return { ok: false, error: 'device-not-connected' };
|
|
8940
|
+
}
|
|
8941
|
+
if (!device.socket || device.socket.destroyed || !device.connected) {
|
|
8230
8942
|
return { ok: false, error: 'device-not-connected' };
|
|
8231
8943
|
}
|
|
8232
8944
|
|
|
@@ -8322,15 +9034,21 @@ export function createRemoteHub(options = {}) {
|
|
|
8322
9034
|
return device.inputWriteOwner;
|
|
8323
9035
|
}
|
|
8324
9036
|
|
|
8325
|
-
function sendInputControl(deviceId, input = {}) {
|
|
8326
|
-
const device = devices.get(String(deviceId || ''));
|
|
8327
|
-
if (!device) {
|
|
8328
|
-
return { ok: false, error: 'device-not-found' };
|
|
8329
|
-
}
|
|
8330
|
-
|
|
8331
|
-
if (
|
|
8332
|
-
|
|
8333
|
-
|
|
9037
|
+
function sendInputControl(deviceId, input = {}) {
|
|
9038
|
+
const device = devices.get(String(deviceId || ''));
|
|
9039
|
+
if (!device) {
|
|
9040
|
+
return { ok: false, error: 'device-not-found' };
|
|
9041
|
+
}
|
|
9042
|
+
|
|
9043
|
+
if (device.synthetic !== true
|
|
9044
|
+
&& device.connected
|
|
9045
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
9046
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9047
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9048
|
+
}
|
|
9049
|
+
if (!device.connected) {
|
|
9050
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9051
|
+
}
|
|
8334
9052
|
|
|
8335
9053
|
const normalized = normalizeRemoteInputEvent(input);
|
|
8336
9054
|
if (!normalized.type) {
|
|
@@ -8396,9 +9114,51 @@ export function createRemoteHub(options = {}) {
|
|
|
8396
9114
|
|| device.capabilities?.accessibilityPermission === false) {
|
|
8397
9115
|
return { ok: false, error: 'INPUT_PERMISSION_DENIED' };
|
|
8398
9116
|
}
|
|
8399
|
-
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
8400
|
-
return { ok: false, error: 'device-input-control-unavailable' };
|
|
8401
|
-
}
|
|
9117
|
+
if (!readCapabilityFlag(device.capabilities, 'control')) {
|
|
9118
|
+
return { ok: false, error: 'device-input-control-unavailable' };
|
|
9119
|
+
}
|
|
9120
|
+
|
|
9121
|
+
const normalizedInputType = normalized.type.toLowerCase();
|
|
9122
|
+
const isClipboardInput = normalizedInputType === 'clipboard.prepare'
|
|
9123
|
+
|| normalizedInputType === 'clipboard.copy'
|
|
9124
|
+
|| normalizedInputType === 'clipboard.paste';
|
|
9125
|
+
let clipboardManifest = null;
|
|
9126
|
+
if (isClipboardInput) {
|
|
9127
|
+
if (!normalized.clipboardOperationId) {
|
|
9128
|
+
return { ok: false, error: 'clipboard-operation-id-invalid' };
|
|
9129
|
+
}
|
|
9130
|
+
if (!['copy', 'paste'].includes(normalized.clipboardDirection)) {
|
|
9131
|
+
return { ok: false, error: 'clipboard-direction-required' };
|
|
9132
|
+
}
|
|
9133
|
+
if ((normalizedInputType === 'clipboard.copy' && normalized.clipboardDirection !== 'copy')
|
|
9134
|
+
|| (normalizedInputType === 'clipboard.paste' && normalized.clipboardDirection !== 'paste')) {
|
|
9135
|
+
return { ok: false, error: 'clipboard-direction-mismatch' };
|
|
9136
|
+
}
|
|
9137
|
+
try {
|
|
9138
|
+
clipboardManifest = normalized.manifest
|
|
9139
|
+
? normalizeRemoteClipboardManifest(normalized.manifest, normalized.clipboardOperationId)
|
|
9140
|
+
: null;
|
|
9141
|
+
} catch (error) {
|
|
9142
|
+
return { ok: false, error: safeString(error?.code || error?.message, 160) || 'clipboard-manifest-invalid' };
|
|
9143
|
+
}
|
|
9144
|
+
const clipboardPolicy = getDevicePolicy(device);
|
|
9145
|
+
if (clipboardPolicy.allowClipboardText !== true) {
|
|
9146
|
+
return { ok: false, error: 'allowClipboardText-blocked-by-settings' };
|
|
9147
|
+
}
|
|
9148
|
+
const contentKind = clipboardManifest?.contentKind || normalized.contentKind || 'text';
|
|
9149
|
+
if (remoteClipboardContentNeedsFileTransfer(contentKind)
|
|
9150
|
+
&& clipboardPolicy.allowFileTransfer !== true) {
|
|
9151
|
+
return { ok: false, error: 'allowFileTransfer-blocked-by-settings' };
|
|
9152
|
+
}
|
|
9153
|
+
const clipboardCapabilityDenied = clipboardCapabilityError(
|
|
9154
|
+
device,
|
|
9155
|
+
normalized.clipboardDirection,
|
|
9156
|
+
contentKind);
|
|
9157
|
+
if (clipboardCapabilityDenied) {
|
|
9158
|
+
return { ok: false, error: clipboardCapabilityDenied };
|
|
9159
|
+
}
|
|
9160
|
+
normalized.manifest = clipboardManifest;
|
|
9161
|
+
}
|
|
8402
9162
|
|
|
8403
9163
|
const controlStream = getActiveControlStream(device);
|
|
8404
9164
|
if (!controlStream
|
|
@@ -8434,14 +9194,32 @@ export function createRemoteHub(options = {}) {
|
|
|
8434
9194
|
};
|
|
8435
9195
|
}
|
|
8436
9196
|
const activeMonitorIndex = normalizeMonitorIndex(controlStream.monitorIndex);
|
|
8437
|
-
if (!Number.isInteger(normalized.monitorIndex)
|
|
8438
|
-
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
9197
|
+
if (!Number.isInteger(normalized.monitorIndex)
|
|
9198
|
+
|| normalized.monitorIndex !== activeMonitorIndex) {
|
|
8439
9199
|
return {
|
|
8440
9200
|
ok: false,
|
|
8441
9201
|
error: 'STALE_CONTROL_MONITOR',
|
|
8442
|
-
activeMonitorIndex
|
|
8443
|
-
};
|
|
8444
|
-
}
|
|
9202
|
+
activeMonitorIndex
|
|
9203
|
+
};
|
|
9204
|
+
}
|
|
9205
|
+
|
|
9206
|
+
let clipboardOperationKey = '';
|
|
9207
|
+
if (normalizedInputType === 'clipboard.paste') {
|
|
9208
|
+
clipboardOperationKey = buildClipboardOperationKey(
|
|
9209
|
+
device.deviceId,
|
|
9210
|
+
device.sessionId,
|
|
9211
|
+
normalized.clipboardOperationId);
|
|
9212
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9213
|
+
if (!operation
|
|
9214
|
+
|| operation.direction !== 'paste'
|
|
9215
|
+
|| operation.hubConnectionId !== normalized.hubConnectionId
|
|
9216
|
+
|| operation.controlCommandId !== normalized.controlCommandId
|
|
9217
|
+
|| operation.captureGeneration !== normalized.captureGeneration
|
|
9218
|
+
|| operation.monitorIndex !== normalized.monitorIndex
|
|
9219
|
+
|| operation.status !== 'ready') {
|
|
9220
|
+
return { ok: false, error: 'clipboard-paste-not-ready' };
|
|
9221
|
+
}
|
|
9222
|
+
}
|
|
8445
9223
|
|
|
8446
9224
|
const inputSocket = device.inputSocket;
|
|
8447
9225
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -8458,9 +9236,17 @@ export function createRemoteHub(options = {}) {
|
|
|
8458
9236
|
&& normalized.hubConnectionId !== previousOwnerConnectionId;
|
|
8459
9237
|
const bindingChanged = previousInputBindingKey
|
|
8460
9238
|
&& previousInputBindingKey !== activeInputBindingKey;
|
|
8461
|
-
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
8462
|
-
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
8463
|
-
|
|
9239
|
+
const writeBindingChanged = inputWriteOwner.getBindingKey() !== activeInputBindingKey;
|
|
9240
|
+
if (ownerChanged || bindingChanged || writeBindingChanged) {
|
|
9241
|
+
if (ownerChanged) {
|
|
9242
|
+
clearClipboardOperationsForDevice(
|
|
9243
|
+
device,
|
|
9244
|
+
'browser-input-owner-changed',
|
|
9245
|
+
previousOwnerConnectionId);
|
|
9246
|
+
} else if (bindingChanged || writeBindingChanged) {
|
|
9247
|
+
clearClipboardOperationsForDevice(device, 'control-input-binding-changed');
|
|
9248
|
+
}
|
|
9249
|
+
const resetSent = inputWriteOwner.replaceBinding(
|
|
8464
9250
|
activeInputBindingKey,
|
|
8465
9251
|
buildRemoteInputResetMessage(
|
|
8466
9252
|
device,
|
|
@@ -8490,9 +9276,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8490
9276
|
device.inputOwnerConnectionId = normalized.hubConnectionId;
|
|
8491
9277
|
}
|
|
8492
9278
|
device.inputOwnerBindingKey = activeInputBindingKey;
|
|
8493
|
-
device.counters.commandsSent += 1;
|
|
8494
|
-
device.inputLastSeenAt = new Date().toISOString();
|
|
8495
|
-
|
|
9279
|
+
device.counters.commandsSent += 1;
|
|
9280
|
+
device.inputLastSeenAt = new Date().toISOString();
|
|
9281
|
+
if (clipboardOperationKey) {
|
|
9282
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9283
|
+
if (operation) {
|
|
9284
|
+
operation.status = 'pasting';
|
|
9285
|
+
operation.touchedAt = Date.now();
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
return {
|
|
8496
9289
|
ok: true,
|
|
8497
9290
|
inputSocket: true,
|
|
8498
9291
|
sessionId: device.sessionId,
|
|
@@ -8527,7 +9320,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8527
9320
|
issuedAt: normalized.issuedAt || new Date().toISOString()
|
|
8528
9321
|
}
|
|
8529
9322
|
});
|
|
8530
|
-
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
9323
|
+
if (fallback.ok && normalized.requestAck && normalized.inputSeq > 0) {
|
|
8531
9324
|
schedulePendingInputFallback(device, {
|
|
8532
9325
|
commandId: fallback.commandId,
|
|
8533
9326
|
inputSeq: normalized.inputSeq,
|
|
@@ -8539,9 +9332,16 @@ export function createRemoteHub(options = {}) {
|
|
|
8539
9332
|
hubReceivedAtEpochMs: normalized.hubReceivedAtEpochMs,
|
|
8540
9333
|
hubForwardedAtEpochMs,
|
|
8541
9334
|
fallback: true,
|
|
8542
|
-
timeoutTimer: null
|
|
8543
|
-
});
|
|
8544
|
-
}
|
|
9335
|
+
timeoutTimer: null
|
|
9336
|
+
});
|
|
9337
|
+
}
|
|
9338
|
+
if (fallback.ok && clipboardOperationKey) {
|
|
9339
|
+
const operation = clipboardOperations.get(clipboardOperationKey);
|
|
9340
|
+
if (operation) {
|
|
9341
|
+
operation.status = 'pasting';
|
|
9342
|
+
operation.touchedAt = Date.now();
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
8545
9345
|
return fallback.ok
|
|
8546
9346
|
? {
|
|
8547
9347
|
...fallback,
|
|
@@ -8561,14 +9361,15 @@ export function createRemoteHub(options = {}) {
|
|
|
8561
9361
|
};
|
|
8562
9362
|
}
|
|
8563
9363
|
|
|
8564
|
-
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
9364
|
+
function releaseInputOwner(deviceId, ownerConnectionId, reason = 'browser-input-owner-closed') {
|
|
8565
9365
|
const device = devices.get(String(deviceId || ''));
|
|
8566
9366
|
const owner = safeString(ownerConnectionId, 128);
|
|
8567
|
-
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
8568
|
-
return { ok: false, error: 'input-owner-not-current' };
|
|
8569
|
-
}
|
|
8570
|
-
|
|
8571
|
-
device
|
|
9367
|
+
if (!device || !owner || safeString(device.inputOwnerConnectionId, 128) !== owner) {
|
|
9368
|
+
return { ok: false, error: 'input-owner-not-current' };
|
|
9369
|
+
}
|
|
9370
|
+
|
|
9371
|
+
clearClipboardOperationsForDevice(device, reason, owner);
|
|
9372
|
+
device.inputOwnerConnectionId = '';
|
|
8572
9373
|
device.inputOwnerBindingKey = '';
|
|
8573
9374
|
const inputSocket = device.inputSocket;
|
|
8574
9375
|
const inputWriteOwner = getCurrentInputWriteOwner(device);
|
|
@@ -8612,8 +9413,12 @@ export function createRemoteHub(options = {}) {
|
|
|
8612
9413
|
};
|
|
8613
9414
|
const results = targets.map(deviceId => {
|
|
8614
9415
|
const device = devices.get(deviceId);
|
|
8615
|
-
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
8616
|
-
if (
|
|
9416
|
+
if (device?.synthetic === true && device.connected) return { deviceId, ok: true, synthetic: true };
|
|
9417
|
+
if (device?.connected && !isDeviceConnectionFresh(device)) {
|
|
9418
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9419
|
+
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
9420
|
+
}
|
|
9421
|
+
if (!device?.connected || !device.socket || device.socket.destroyed) {
|
|
8617
9422
|
return { deviceId, ok: false, error: 'device-not-connected' };
|
|
8618
9423
|
}
|
|
8619
9424
|
return writeJsonLine(device.socket, payload)
|
|
@@ -8645,7 +9450,7 @@ export function createRemoteHub(options = {}) {
|
|
|
8645
9450
|
const denied = policyError(device, 'allowAgent');
|
|
8646
9451
|
if (denied) return { ok: false, error: denied };
|
|
8647
9452
|
|
|
8648
|
-
if (device?.synthetic === true && device.connected) {
|
|
9453
|
+
if (device?.synthetic === true && device.connected) {
|
|
8649
9454
|
const instruction = safeText(options.instruction, MAX_AGENT_TASK_CHARS);
|
|
8650
9455
|
if (!instruction) {
|
|
8651
9456
|
return { ok: false, error: 'missing-instruction' };
|
|
@@ -8705,10 +9510,14 @@ export function createRemoteHub(options = {}) {
|
|
|
8705
9510
|
error: '',
|
|
8706
9511
|
synthetic: true
|
|
8707
9512
|
});
|
|
8708
|
-
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
8709
|
-
}
|
|
8710
|
-
|
|
8711
|
-
if (
|
|
9513
|
+
return { ok: true, commandId, taskId, approvalLevel, synthetic: true };
|
|
9514
|
+
}
|
|
9515
|
+
|
|
9516
|
+
if (device.connected && !isDeviceConnectionFresh(device)) {
|
|
9517
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
9518
|
+
return { ok: false, error: 'device-not-connected' };
|
|
9519
|
+
}
|
|
9520
|
+
if (!device?.socket || device.socket.destroyed || !device.connected) {
|
|
8712
9521
|
return { ok: false, error: 'device-not-connected' };
|
|
8713
9522
|
}
|
|
8714
9523
|
|
|
@@ -9528,11 +10337,58 @@ export function createRemoteHub(options = {}) {
|
|
|
9528
10337
|
=== normalized.streamPurpose;
|
|
9529
10338
|
}
|
|
9530
10339
|
|
|
9531
|
-
function
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
10340
|
+
function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
|
|
10341
|
+
if (!activeLiveStream) {
|
|
10342
|
+
return false;
|
|
10343
|
+
}
|
|
10344
|
+
return activeLiveStream.frameMode === normalized.transfer.frameMode
|
|
10345
|
+
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
|
|
10346
|
+
&& (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
|
|
10347
|
+
=== normalized.streamPurpose;
|
|
10348
|
+
}
|
|
10349
|
+
|
|
10350
|
+
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
10351
|
+
return {
|
|
10352
|
+
ok: true,
|
|
10353
|
+
commandId: activeLiveStream.commandId,
|
|
10354
|
+
sessionId: device.sessionId,
|
|
10355
|
+
streamId: activeLiveStream.streamId,
|
|
10356
|
+
streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
|
|
10357
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
10358
|
+
mode: activeLiveStream.mode || normalized.transfer.mode,
|
|
10359
|
+
frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
|
|
10360
|
+
monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
|
|
10361
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10362
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
10363
|
+
reused: true,
|
|
10364
|
+
sharedProfileReused: true,
|
|
10365
|
+
requestedProfile: {
|
|
10366
|
+
fps: normalized.fps,
|
|
10367
|
+
maxWidth: normalized.maxWidth,
|
|
10368
|
+
maxHeight: normalized.maxHeight,
|
|
10369
|
+
quality: normalized.quality
|
|
10370
|
+
},
|
|
10371
|
+
effectiveProfile: {
|
|
10372
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
10373
|
+
maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
10374
|
+
maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
|
|
10375
|
+
quality: Number(activeLiveStream.quality || normalized.quality)
|
|
10376
|
+
},
|
|
10377
|
+
...extra
|
|
10378
|
+
};
|
|
10379
|
+
}
|
|
10380
|
+
|
|
10381
|
+
function startLiveStream(deviceId, options = {}) {
|
|
10382
|
+
const device = devices.get(String(deviceId || ''));
|
|
10383
|
+
const denied = policyError(device);
|
|
10384
|
+
if (denied) return { ok: false, error: denied };
|
|
10385
|
+
if (device?.synthetic !== true
|
|
10386
|
+
&& device?.connected
|
|
10387
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10388
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10389
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10390
|
+
}
|
|
10391
|
+
if (device?.liveCapturePause?.token) {
|
|
9536
10392
|
return {
|
|
9537
10393
|
ok: false,
|
|
9538
10394
|
error: 'LIVE_CAPTURE_PAUSED',
|
|
@@ -9664,6 +10520,13 @@ export function createRemoteHub(options = {}) {
|
|
|
9664
10520
|
const modePolicyError = liveStreamModePolicyError(normalized);
|
|
9665
10521
|
if (modePolicyError) return { ok: false, error: modePolicyError };
|
|
9666
10522
|
const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
|
|
10523
|
+
if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
|
|
10524
|
+
return {
|
|
10525
|
+
ok: false,
|
|
10526
|
+
error: 'device-frame-mode-unavailable',
|
|
10527
|
+
frameMode: transfer.frameMode
|
|
10528
|
+
};
|
|
10529
|
+
}
|
|
9667
10530
|
const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
|
|
9668
10531
|
if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
|
|
9669
10532
|
return {
|
|
@@ -9678,13 +10541,24 @@ export function createRemoteHub(options = {}) {
|
|
|
9678
10541
|
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
9679
10542
|
if (pendingDescriptor?.commandId) {
|
|
9680
10543
|
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
9681
|
-
|
|
10544
|
+
const pendingMatchesSharedOwner = options.forceRestart !== true
|
|
10545
|
+
&& options.reuseExisting === true
|
|
10546
|
+
&& options.reuseSharedExisting === true
|
|
10547
|
+
&& liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
|
|
10548
|
+
if ((pendingMatchesRequest || pendingMatchesSharedOwner)
|
|
10549
|
+
&& liveStreamReplacementStillPending(activeLiveStream)) {
|
|
9682
10550
|
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
9683
10551
|
streamId,
|
|
9684
10552
|
commandId: pendingDescriptor.commandId,
|
|
9685
10553
|
activeCommandId: activeLiveStream.commandId,
|
|
9686
10554
|
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
9687
10555
|
});
|
|
10556
|
+
if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
|
|
10557
|
+
return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
|
|
10558
|
+
ready: false,
|
|
10559
|
+
pending: true
|
|
10560
|
+
});
|
|
10561
|
+
}
|
|
9688
10562
|
return {
|
|
9689
10563
|
ok: true,
|
|
9690
10564
|
commandId: pendingDescriptor.commandId,
|
|
@@ -9758,6 +10632,25 @@ export function createRemoteHub(options = {}) {
|
|
|
9758
10632
|
reused: true
|
|
9759
10633
|
};
|
|
9760
10634
|
}
|
|
10635
|
+
if (activeLiveStream
|
|
10636
|
+
&& options.forceRestart !== true
|
|
10637
|
+
&& options.reuseExisting === true
|
|
10638
|
+
&& options.reuseSharedExisting === true
|
|
10639
|
+
&& liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
|
|
10640
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
10641
|
+
emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
|
|
10642
|
+
streamId,
|
|
10643
|
+
commandId: activeLiveStream.commandId,
|
|
10644
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
10645
|
+
requestedFps: normalized.fps,
|
|
10646
|
+
effectiveFps: Number(activeLiveStream.fps || normalized.fps),
|
|
10647
|
+
requestedMaxWidth: normalized.maxWidth,
|
|
10648
|
+
requestedMaxHeight: normalized.maxHeight,
|
|
10649
|
+
effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
10650
|
+
effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
|
|
10651
|
+
});
|
|
10652
|
+
return sharedLiveProfileResult(device, activeLiveStream, normalized);
|
|
10653
|
+
}
|
|
9761
10654
|
if (activeLiveStream
|
|
9762
10655
|
&& options.forceRestart !== true
|
|
9763
10656
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
@@ -9926,9 +10819,15 @@ export function createRemoteHub(options = {}) {
|
|
|
9926
10819
|
};
|
|
9927
10820
|
}
|
|
9928
10821
|
|
|
9929
|
-
function stopLiveStream(deviceId, options = {}) {
|
|
9930
|
-
const device = devices.get(String(deviceId || ''));
|
|
9931
|
-
|
|
10822
|
+
function stopLiveStream(deviceId, options = {}) {
|
|
10823
|
+
const device = devices.get(String(deviceId || ''));
|
|
10824
|
+
if (device?.synthetic !== true
|
|
10825
|
+
&& device?.connected
|
|
10826
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
10827
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
10828
|
+
return { ok: false, error: 'device-not-connected' };
|
|
10829
|
+
}
|
|
10830
|
+
const stopAll = options.stopAll === true;
|
|
9932
10831
|
const streamPurpose = safeString(options.streamPurpose || options.purpose, 24).toLowerCase();
|
|
9933
10832
|
const requestedStreamId = safeString(options.streamId, 128);
|
|
9934
10833
|
const purposeStreams = !stopAll && !requestedStreamId && streamPurpose && device
|
|
@@ -10529,12 +11428,18 @@ export function createRemoteHub(options = {}) {
|
|
|
10529
11428
|
};
|
|
10530
11429
|
}
|
|
10531
11430
|
|
|
10532
|
-
function startAudioStream(deviceId, options = {}) {
|
|
10533
|
-
const device = devices.get(String(deviceId || ''));
|
|
10534
|
-
if (!device) {
|
|
10535
|
-
return { ok: false, error: 'device-not-found' };
|
|
10536
|
-
}
|
|
10537
|
-
if (
|
|
11431
|
+
function startAudioStream(deviceId, options = {}) {
|
|
11432
|
+
const device = devices.get(String(deviceId || ''));
|
|
11433
|
+
if (!device) {
|
|
11434
|
+
return { ok: false, error: 'device-not-found' };
|
|
11435
|
+
}
|
|
11436
|
+
if (device.synthetic !== true
|
|
11437
|
+
&& device.connected
|
|
11438
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11439
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11440
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11441
|
+
}
|
|
11442
|
+
if (!device.connected) {
|
|
10538
11443
|
return { ok: false, error: 'device-not-connected' };
|
|
10539
11444
|
}
|
|
10540
11445
|
const denied = policyError(device, 'allowRemoteAudio');
|
|
@@ -10611,12 +11516,18 @@ export function createRemoteHub(options = {}) {
|
|
|
10611
11516
|
return { ok: true, commandId, streamId };
|
|
10612
11517
|
}
|
|
10613
11518
|
|
|
10614
|
-
function stopAudioStream(deviceId, options = {}) {
|
|
10615
|
-
const device = devices.get(String(deviceId || ''));
|
|
10616
|
-
if (!device) {
|
|
10617
|
-
return { ok: false, error: 'device-not-found' };
|
|
10618
|
-
}
|
|
10619
|
-
if (
|
|
11519
|
+
function stopAudioStream(deviceId, options = {}) {
|
|
11520
|
+
const device = devices.get(String(deviceId || ''));
|
|
11521
|
+
if (!device) {
|
|
11522
|
+
return { ok: false, error: 'device-not-found' };
|
|
11523
|
+
}
|
|
11524
|
+
if (device.synthetic !== true
|
|
11525
|
+
&& device.connected
|
|
11526
|
+
&& !isDeviceConnectionFresh(device)) {
|
|
11527
|
+
retireStaleDeviceConnection(device, 'status-heartbeat-timeout');
|
|
11528
|
+
return { ok: false, error: 'device-not-connected' };
|
|
11529
|
+
}
|
|
11530
|
+
if (!device.connected) {
|
|
10620
11531
|
return { ok: false, error: 'device-not-connected' };
|
|
10621
11532
|
}
|
|
10622
11533
|
if (device.synthetic === true || !device.socket || device.socket.destroyed) {
|
|
@@ -10829,10 +11740,14 @@ export function createRemoteHub(options = {}) {
|
|
|
10829
11740
|
replacementTimerCount: device.liveStreamReplacementTimers instanceof Map
|
|
10830
11741
|
? device.liveStreamReplacementTimers.size
|
|
10831
11742
|
: 0,
|
|
10832
|
-
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
10833
|
-
.filter(waiter => waiter.deviceId === device.deviceId
|
|
10834
|
-
&& waiter.sessionId === device.sessionId)
|
|
10835
|
-
.length,
|
|
11743
|
+
commandResultWaiterCount: [...pendingCommandResultWaiters.values()]
|
|
11744
|
+
.filter(waiter => waiter.deviceId === device.deviceId
|
|
11745
|
+
&& waiter.sessionId === device.sessionId)
|
|
11746
|
+
.length,
|
|
11747
|
+
clipboardOperationCount: [...clipboardOperations.values()]
|
|
11748
|
+
.filter(operation => operation.deviceId === device.deviceId
|
|
11749
|
+
&& operation.sessionId === device.sessionId)
|
|
11750
|
+
.length,
|
|
10836
11751
|
retainedPayloadCount: retainedPayloads.size,
|
|
10837
11752
|
retainedPayloadBytes: [...retainedPayloads].reduce(
|
|
10838
11753
|
(total, payload) => total + payload.length,
|
|
@@ -10842,8 +11757,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10842
11757
|
&& snapshot.stopPendingCount === 0
|
|
10843
11758
|
&& snapshot.pendingStartCount === 0
|
|
10844
11759
|
&& snapshot.pendingStopPromiseCount === 0
|
|
10845
|
-
&& snapshot.replacementTimerCount === 0
|
|
10846
|
-
&& snapshot.commandResultWaiterCount === 0
|
|
11760
|
+
&& snapshot.replacementTimerCount === 0
|
|
11761
|
+
&& snapshot.commandResultWaiterCount === 0
|
|
11762
|
+
&& snapshot.clipboardOperationCount === 0
|
|
10847
11763
|
&& snapshot.retainedPayloadCount === 0
|
|
10848
11764
|
&& snapshot.retainedPayloadBytes === 0;
|
|
10849
11765
|
return snapshot;
|
|
@@ -10855,8 +11771,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10855
11771
|
'stopPendingCount',
|
|
10856
11772
|
'pendingStartCount',
|
|
10857
11773
|
'pendingStopPromiseCount',
|
|
10858
|
-
'replacementTimerCount',
|
|
10859
|
-
'commandResultWaiterCount',
|
|
11774
|
+
'replacementTimerCount',
|
|
11775
|
+
'commandResultWaiterCount',
|
|
11776
|
+
'clipboardOperationCount',
|
|
10860
11777
|
'retainedPayloadCount',
|
|
10861
11778
|
'retainedPayloadBytes'
|
|
10862
11779
|
]) {
|
|
@@ -10869,9 +11786,10 @@ export function createRemoteHub(options = {}) {
|
|
|
10869
11786
|
stopPendingCount: 0,
|
|
10870
11787
|
pendingStartCount: 0,
|
|
10871
11788
|
pendingStopPromiseCount: 0,
|
|
10872
|
-
replacementTimerCount: 0,
|
|
10873
|
-
commandResultWaiterCount: 0,
|
|
10874
|
-
|
|
11789
|
+
replacementTimerCount: 0,
|
|
11790
|
+
commandResultWaiterCount: 0,
|
|
11791
|
+
clipboardOperationCount: 0,
|
|
11792
|
+
retainedPayloadCount: 0,
|
|
10875
11793
|
retainedPayloadBytes: 0
|
|
10876
11794
|
});
|
|
10877
11795
|
return {
|
|
@@ -10911,8 +11829,9 @@ export function createRemoteHub(options = {}) {
|
|
|
10911
11829
|
listDeviceFrames,
|
|
10912
11830
|
disconnectDevice,
|
|
10913
11831
|
assignDeviceSlot,
|
|
10914
|
-
sendCommand,
|
|
10915
|
-
|
|
11832
|
+
sendCommand,
|
|
11833
|
+
sendClipboardCommand,
|
|
11834
|
+
refreshDevicePolicies,
|
|
10916
11835
|
sendLegacyClientUpdate,
|
|
10917
11836
|
sendInputControl,
|
|
10918
11837
|
releaseInputOwner,
|