@livedesk/client 0.1.235 → 0.1.236
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/bin/livedesk-client-node.js +82 -190
- package/bin/livedesk-client.js +111 -482
- package/package.json +6 -6
- package/src/runtime/client-runtime-server.js +20 -114
- package/src/runtime/fast-runtime-repair.js +0 -300
- package/src/security/device-credential-store.js +0 -219
- package/src/security/secure-direct-client.js +0 -224
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import net from 'net';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, promises as fs, statfsSync } from 'fs';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
|
-
import { createRequire } from 'node:module';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { createClientDeviceCredentialStore } from '../src/security/device-credential-store.js';
|
|
12
|
-
import { connectSecureDirect } from '../src/security/secure-direct-client.js';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
13
11
|
|
|
14
12
|
const require = createRequire(import.meta.url);
|
|
15
13
|
const CLIENT_UPDATE_BOOTSTRAP_PATH = fileURLToPath(
|
|
@@ -659,7 +657,7 @@ function sanitizePathSegment(value, fallback = 'file') {
|
|
|
659
657
|
return normalized.slice(0, 160);
|
|
660
658
|
}
|
|
661
659
|
|
|
662
|
-
function sanitizeRelativeFilePath(value, fallbackName = 'file') {
|
|
660
|
+
function sanitizeRelativeFilePath(value, fallbackName = 'file') {
|
|
663
661
|
const parts = String(value || '')
|
|
664
662
|
.replace(/\0/g, '')
|
|
665
663
|
.split(/[\\/]+/)
|
|
@@ -670,52 +668,7 @@ function sanitizeRelativeFilePath(value, fallbackName = 'file') {
|
|
|
670
668
|
return sanitizePathSegment(fallbackName, 'file');
|
|
671
669
|
}
|
|
672
670
|
return path.join(...parts.slice(-8));
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function isPathInsideRoot(root, candidate) {
|
|
676
|
-
const relative = path.relative(root, candidate);
|
|
677
|
-
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
async function resolveReceiveDirectory(options, remoteDirectory) {
|
|
681
|
-
const configuredRoot = path.resolve(options.filesDir || getDefaultFilesDir());
|
|
682
|
-
await fs.mkdir(configuredRoot, { recursive: true });
|
|
683
|
-
const rootStat = await fs.lstat(configuredRoot);
|
|
684
|
-
if (rootStat.isSymbolicLink()) throw new Error('The LiveDesk receive root cannot be a symbolic link or junction.');
|
|
685
|
-
const canonicalRoot = await fs.realpath(configuredRoot);
|
|
686
|
-
const relativeDirectory = String(remoteDirectory || '').replace(/\0/g, '').trim();
|
|
687
|
-
if (relativeDirectory.length > 600 || path.isAbsolute(relativeDirectory)) {
|
|
688
|
-
throw new Error('The remote receive directory must be relative to the LiveDesk files root.');
|
|
689
|
-
}
|
|
690
|
-
const candidate = path.resolve(canonicalRoot, relativeDirectory || '.');
|
|
691
|
-
if (!isPathInsideRoot(canonicalRoot, candidate)) throw new Error('The remote receive directory escapes the LiveDesk files root.');
|
|
692
|
-
await assertAgentPathDoesNotTraverseLink(canonicalRoot, candidate);
|
|
693
|
-
await fs.mkdir(candidate, { recursive: true });
|
|
694
|
-
await assertAgentPathDoesNotTraverseLink(canonicalRoot, candidate);
|
|
695
|
-
const canonicalCandidate = await fs.realpath(candidate);
|
|
696
|
-
if (!isPathInsideRoot(canonicalRoot, canonicalCandidate)) throw new Error('The remote receive directory resolves outside the LiveDesk files root.');
|
|
697
|
-
return { root: canonicalRoot, directory: canonicalCandidate };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function requireFileSha256(value) {
|
|
701
|
-
const hash = String(value || '').trim().toLowerCase();
|
|
702
|
-
if (!/^[a-f0-9]{64}$/.test(hash)) throw new Error('A valid SHA-256 file hash is required.');
|
|
703
|
-
return hash;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function hashBuffer(buffer) {
|
|
707
|
-
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
async function commitNewFile(tempPath, targetPath) {
|
|
711
|
-
try {
|
|
712
|
-
await fs.link(tempPath, targetPath);
|
|
713
|
-
} catch (error) {
|
|
714
|
-
if (error?.code === 'EEXIST') throw new Error(`Refusing to overwrite an existing file: ${path.basename(targetPath)}`);
|
|
715
|
-
throw error;
|
|
716
|
-
}
|
|
717
|
-
await fs.rm(tempPath, { force: true });
|
|
718
|
-
}
|
|
671
|
+
}
|
|
719
672
|
|
|
720
673
|
async function handleFileTransferCommand(options, payload = {}) {
|
|
721
674
|
const files = Array.isArray(payload.files) ? payload.files.slice(0, MAX_FILE_TRANSFER_FILES) : [];
|
|
@@ -723,46 +676,32 @@ async function handleFileTransferCommand(options, payload = {}) {
|
|
|
723
676
|
throw new Error('No files were included in the transfer.');
|
|
724
677
|
}
|
|
725
678
|
|
|
726
|
-
const
|
|
679
|
+
const baseDir = normalizeDirectoryPath(payload.remoteDirectory || options.filesDir);
|
|
680
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
727
681
|
|
|
728
682
|
let totalBytes = 0;
|
|
729
683
|
const saved = [];
|
|
730
684
|
for (const file of files) {
|
|
731
685
|
const name = sanitizePathSegment(file?.name, 'file');
|
|
732
686
|
const relativePath = sanitizeRelativeFilePath(file?.relativePath || name, name);
|
|
733
|
-
const targetPath = path.resolve(baseDir, relativePath);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
687
|
+
const targetPath = path.resolve(baseDir, relativePath);
|
|
688
|
+
const relativeFromBase = path.relative(baseDir, targetPath);
|
|
689
|
+
if (relativeFromBase.startsWith('..') || path.isAbsolute(relativeFromBase)) {
|
|
690
|
+
throw new Error(`Unsafe file path: ${relativePath}`);
|
|
691
|
+
}
|
|
737
692
|
|
|
738
693
|
const dataBase64 = String(file?.dataBase64 || '').replace(/^data:[^,]*,/i, '').trim();
|
|
739
694
|
if (!dataBase64) {
|
|
740
695
|
throw new Error(`Missing file data: ${name}`);
|
|
741
696
|
}
|
|
742
|
-
const buffer = Buffer.from(dataBase64, 'base64');
|
|
743
|
-
const expectedSha256 = requireFileSha256(file?.sha256);
|
|
744
|
-
if (hashBuffer(buffer) !== expectedSha256) throw new Error(`File SHA-256 mismatch: ${name}`);
|
|
697
|
+
const buffer = Buffer.from(dataBase64, 'base64');
|
|
745
698
|
totalBytes += buffer.length;
|
|
746
699
|
if (totalBytes > MAX_FILE_TRANSFER_BYTES) {
|
|
747
700
|
throw new Error('File transfer exceeded the local size limit.');
|
|
748
701
|
}
|
|
749
702
|
|
|
750
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
751
|
-
await
|
|
752
|
-
await assertAgentPathDoesNotTraverseLink(root, targetPath);
|
|
753
|
-
const tempPath = `${targetPath}.livedesk-${crypto.randomBytes(12).toString('hex')}.part`;
|
|
754
|
-
try {
|
|
755
|
-
const handle = await fs.open(tempPath, 'wx', 0o600);
|
|
756
|
-
try {
|
|
757
|
-
await handle.writeFile(buffer);
|
|
758
|
-
await handle.sync();
|
|
759
|
-
} finally {
|
|
760
|
-
await handle.close();
|
|
761
|
-
}
|
|
762
|
-
await commitNewFile(tempPath, targetPath);
|
|
763
|
-
} finally {
|
|
764
|
-
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
765
|
-
}
|
|
703
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
704
|
+
await fs.writeFile(targetPath, buffer);
|
|
766
705
|
saved.push({
|
|
767
706
|
name,
|
|
768
707
|
relativePath,
|
|
@@ -783,11 +722,12 @@ async function handleFileTransferCommand(options, payload = {}) {
|
|
|
783
722
|
}
|
|
784
723
|
|
|
785
724
|
async function handleFileTransferChunkCommand(options, payload = {}) {
|
|
786
|
-
const
|
|
725
|
+
const baseDir = normalizeDirectoryPath(payload.remoteDirectory || options.filesDir);
|
|
787
726
|
const name = sanitizePathSegment(payload.name, 'file');
|
|
788
727
|
const relativePath = sanitizeRelativeFilePath(payload.relativePath || name, name);
|
|
789
728
|
const targetPath = path.resolve(baseDir, relativePath);
|
|
790
|
-
|
|
729
|
+
const relativeFromBase = path.relative(baseDir, targetPath);
|
|
730
|
+
if (relativeFromBase.startsWith('..') || path.isAbsolute(relativeFromBase)) {
|
|
791
731
|
throw new Error(`Unsafe file path: ${relativePath}`);
|
|
792
732
|
}
|
|
793
733
|
|
|
@@ -796,31 +736,19 @@ async function handleFileTransferChunkCommand(options, payload = {}) {
|
|
|
796
736
|
const totalBytes = Math.max(0, Math.floor(Number(payload.totalBytes) || 0));
|
|
797
737
|
const final = payload.final === true;
|
|
798
738
|
const buffer = payload.dataBase64 ? Buffer.from(String(payload.dataBase64), 'base64') : Buffer.alloc(0);
|
|
799
|
-
if (!transferId ||
|
|
739
|
+
if (!transferId || offset > totalBytes || offset + buffer.length > totalBytes || buffer.length > 512 * 1024) {
|
|
800
740
|
throw new Error('Invalid file transfer chunk.');
|
|
801
741
|
}
|
|
802
742
|
if (buffer.length === 0 && !(final && totalBytes === 0 && offset === 0)) {
|
|
803
743
|
throw new Error('Empty file chunks are not allowed.');
|
|
804
744
|
}
|
|
805
745
|
|
|
806
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
807
|
-
|
|
808
|
-
await
|
|
809
|
-
const tempPath = `${targetPath}.livedesk-${transferId}.part`;
|
|
810
|
-
if (offset > 0) {
|
|
811
|
-
const staging = await fs.lstat(tempPath);
|
|
812
|
-
if (!staging.isFile() || staging.isSymbolicLink() || staging.nlink !== 1) {
|
|
813
|
-
throw new Error('Unsafe file transfer staging path.');
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
const openFlags = offset === 0
|
|
817
|
-
? fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR
|
|
818
|
-
: fsConstants.O_RDWR | (fsConstants.O_NOFOLLOW || 0);
|
|
819
|
-
const handle = await fs.open(tempPath, openFlags, 0o600);
|
|
746
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
747
|
+
const tempPath = `${targetPath}.livedesk-${transferId}.part`;
|
|
748
|
+
const handle = await fs.open(tempPath, offset === 0 ? 'w+' : 'r+');
|
|
820
749
|
let completedSize = 0;
|
|
821
750
|
try {
|
|
822
|
-
const before = await handle.stat();
|
|
823
|
-
if (!before.isFile() || before.nlink !== 1) throw new Error('Unsafe file transfer staging path.');
|
|
751
|
+
const before = await handle.stat();
|
|
824
752
|
if (offset > before.size) {
|
|
825
753
|
throw new Error(`File chunk gap detected at ${offset}; current length is ${before.size}.`);
|
|
826
754
|
}
|
|
@@ -839,19 +767,9 @@ async function handleFileTransferChunkCommand(options, payload = {}) {
|
|
|
839
767
|
await handle.close();
|
|
840
768
|
}
|
|
841
769
|
|
|
842
|
-
if (final) {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
if (hashBuffer(received) !== expectedSha256) {
|
|
846
|
-
await fs.rm(tempPath, { force: true });
|
|
847
|
-
throw new Error('File SHA-256 mismatch.');
|
|
848
|
-
}
|
|
849
|
-
try {
|
|
850
|
-
await commitNewFile(tempPath, targetPath);
|
|
851
|
-
} catch (error) {
|
|
852
|
-
await fs.rm(tempPath, { force: true });
|
|
853
|
-
throw error;
|
|
854
|
-
}
|
|
770
|
+
if (final) {
|
|
771
|
+
await fs.rm(targetPath, { force: true });
|
|
772
|
+
await fs.rename(tempPath, targetPath);
|
|
855
773
|
const lastModified = Number(payload.lastModified || 0);
|
|
856
774
|
if (lastModified > 0) {
|
|
857
775
|
const modifiedAt = new Date(lastModified);
|
|
@@ -1512,20 +1430,7 @@ function normalizeNodeAgentTaskResult(result) {
|
|
|
1512
1430
|
return { ...result, ok, status: ok ? 'completed' : 'failed', error };
|
|
1513
1431
|
}
|
|
1514
1432
|
|
|
1515
|
-
const NODE_AGENT_OPERATIONS = new Set(['file.read', 'file.list', 'network.status', 'logs.collect']);
|
|
1516
|
-
const RETIRED_NODE_AGENT_MUTATING_OPERATIONS = new Set([
|
|
1517
|
-
'process.control',
|
|
1518
|
-
'service.control',
|
|
1519
|
-
'application.launch',
|
|
1520
|
-
'application.close',
|
|
1521
|
-
'file.write',
|
|
1522
|
-
'file.delete',
|
|
1523
|
-
'command.run',
|
|
1524
|
-
'script.run',
|
|
1525
|
-
'software.install',
|
|
1526
|
-
'system.power',
|
|
1527
|
-
'system.configure'
|
|
1528
|
-
]);
|
|
1433
|
+
const NODE_AGENT_OPERATIONS = new Set(['process.control', 'service.control', 'application.launch', 'application.close', 'file.read', 'file.write', 'file.delete', 'file.list', 'command.run', 'script.run', 'software.install', 'network.status', 'system.power', 'system.configure', 'logs.collect']);
|
|
1529
1434
|
|
|
1530
1435
|
function remotePolicyAllows(options, command) {
|
|
1531
1436
|
const policy = options.effectivePolicy;
|
|
@@ -1682,8 +1587,8 @@ async function scheduleClientUpdate(options, payload = {}) {
|
|
|
1682
1587
|
}
|
|
1683
1588
|
}
|
|
1684
1589
|
|
|
1685
|
-
async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
|
|
1686
|
-
const command = String(message.command || '');
|
|
1590
|
+
async function handleRemoteCommand(socket, options, message, nextFrameSeq, activeStreams) {
|
|
1591
|
+
const command = String(message.command || '');
|
|
1687
1592
|
if (command === 'ping') {
|
|
1688
1593
|
writeJsonLine(socket, {
|
|
1689
1594
|
type: 'command.result',
|
|
@@ -1693,20 +1598,8 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
1693
1598
|
at: new Date().toISOString()
|
|
1694
1599
|
}
|
|
1695
1600
|
});
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
// LD-SEC-AI-001 is enforced before settings or permission modes. A stale
|
|
1700
|
-
// Hub can therefore never revive a retired mutation with full-access.
|
|
1701
|
-
if (RETIRED_NODE_AGENT_MUTATING_OPERATIONS.has(command)) {
|
|
1702
|
-
writeJsonLine(socket, {
|
|
1703
|
-
type: 'command.result',
|
|
1704
|
-
commandId: message.commandId,
|
|
1705
|
-
error: 'agent-mutating-tool-disabled',
|
|
1706
|
-
result: { ok: false, status: 'rejected', error: 'agent-mutating-tool-disabled', sideEffects: 'none' }
|
|
1707
|
-
});
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1710
1603
|
|
|
1711
1604
|
const policyDecision = remotePolicyAllows(options, command);
|
|
1712
1605
|
if (!policyDecision.ok) {
|
|
@@ -2007,20 +1900,18 @@ async function handleRemoteCommand(socket, options, message, nextFrameSeq, activ
|
|
|
2007
1900
|
});
|
|
2008
1901
|
}
|
|
2009
1902
|
|
|
2010
|
-
|
|
2011
|
-
const manager = parseManagerAddress(options.manager);
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
return new Promise((resolve, reject) => {
|
|
2023
|
-
let buffer = '';
|
|
1903
|
+
function connectOnce(options, deviceId) {
|
|
1904
|
+
const manager = parseManagerAddress(options.manager);
|
|
1905
|
+
if (!isPrivateLanHost(manager.host) && !isTruthy(process.env.LIVEDESK_ALLOW_UNENCRYPTED_LAN)) {
|
|
1906
|
+
return Promise.reject(new Error('Plain TCP LiveDesk connections are limited to loopback/private LAN. Use an encrypted endpoint for external connections.'));
|
|
1907
|
+
}
|
|
1908
|
+
return new Promise((resolve, reject) => {
|
|
1909
|
+
const socket = net.createConnection({
|
|
1910
|
+
host: manager.host,
|
|
1911
|
+
port: manager.port
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
let buffer = '';
|
|
2024
1915
|
let heartbeatTimer = null;
|
|
2025
1916
|
let resolved = false;
|
|
2026
1917
|
let frameSeq = 0;
|
|
@@ -2054,43 +1945,44 @@ async function connectOnce(options, deviceId) {
|
|
|
2054
1945
|
socket.setNoDelay(true);
|
|
2055
1946
|
socket.setKeepAlive(true, options.heartbeatMs);
|
|
2056
1947
|
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
1948
|
+
socket.once('connect', () => {
|
|
1949
|
+
writeJsonLine(socket, {
|
|
1950
|
+
type: 'hello',
|
|
1951
|
+
pairToken: options.pair,
|
|
1952
|
+
deviceId,
|
|
1953
|
+
deviceName: options.name,
|
|
1954
|
+
slotNumber: options.slotNumber || undefined,
|
|
1955
|
+
hostname: os.hostname(),
|
|
1956
|
+
platform: os.platform(),
|
|
1957
|
+
arch: os.arch(),
|
|
1958
|
+
pid: process.pid,
|
|
1959
|
+
agentVersion: AGENT_VERSION,
|
|
1960
|
+
productVersion: PRODUCT_VERSION,
|
|
1961
|
+
capabilities: {
|
|
1962
|
+
status: true,
|
|
1963
|
+
thumbnail: options.thumbnailEnabled,
|
|
1964
|
+
liveStream: options.liveEnabled,
|
|
1965
|
+
monitorSelection: true,
|
|
1966
|
+
screenCount: 1,
|
|
1967
|
+
monitorCount: 1,
|
|
1968
|
+
control: false,
|
|
1969
|
+
audio: false,
|
|
1970
|
+
remoteAudio: false,
|
|
1971
|
+
fileTransfer: true,
|
|
1972
|
+
remoteFiles: true,
|
|
1973
|
+
fileTransferMaxBytes: MAX_FILE_TRANSFER_BYTES,
|
|
1974
|
+
computerAgent: options.taskEnabled,
|
|
1975
|
+
taskDispatch: options.taskEnabled,
|
|
1976
|
+
clientUpdate: true,
|
|
1977
|
+
productVersion: PRODUCT_VERSION,
|
|
1978
|
+
agentApproval: options.taskEnabled,
|
|
1979
|
+
agentAudit: options.taskEnabled,
|
|
1980
|
+
agentTools: [...NODE_AGENT_OPERATIONS],
|
|
1981
|
+
elevation: typeof process.getuid === 'function' ? process.getuid() === 0 : false,
|
|
1982
|
+
externalEffects: options.taskEnabled
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
2094
1986
|
|
|
2095
1987
|
socket.on('data', chunk => {
|
|
2096
1988
|
buffer += chunk;
|