livedesk 0.1.479 → 0.1.481
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.js +15 -0
- package/client/package.json +6 -6
- package/hub/package.json +2 -2
- package/hub/src/filesystem/transfer-jobs.js +5 -1
- package/hub/src/remote-hub.js +64 -7
- package/hub/src/security/device-credential-authority.js +75 -27
- package/hub/src/security/security-audit-store.js +24 -2
- package/hub/src/server.js +298 -127
- package/hub/src/transport/relay-hub-control.js +2 -1
- package/hub/src/transport/secure-direct-acceptor.js +2 -1
- package/hub/src/transport/udp-hub-transport.js +19 -8
- package/hub/src/transport/udp-rendezvous.js +143 -29
- package/package.json +7 -6
- package/runtime-core/package.json +1 -1
- package/runtime-core/src/os-secret-store.js +2 -0
- package/web/dist/assets/{index-WS5p6ygO.css → index-Cp814ic9.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/livedesk-build-evidence.json +12 -12
- /package/web/dist/assets/{index-CNcg85GR.js → index-N8fgE2ML.js} +0 -0
package/hub/src/server.js
CHANGED
|
@@ -814,17 +814,41 @@ function requestAuditHash(value) {
|
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
-
function
|
|
818
|
-
if (!securityAuditStore) return;
|
|
817
|
+
function securityAuditEvent(event = {}) {
|
|
819
818
|
const runtime = runtimeManager.getSnapshot();
|
|
820
|
-
|
|
819
|
+
return {
|
|
821
820
|
actorAccountId: event.actorAccountId || runtime.userId || '',
|
|
822
821
|
actorUserId: event.actorUserId || runtime.userId || '',
|
|
823
822
|
hubId: event.hubId || runtime.deviceId || '',
|
|
824
823
|
...event
|
|
825
|
-
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function markSecurityAuditFailed(error) {
|
|
828
|
+
if (securityAuditHealthy) {
|
|
826
829
|
securityAuditHealthy = false;
|
|
827
830
|
console.error(`[LiveDesk Hub] SECURITY AUDIT FAILURE: ${error?.message || error}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function recordSecurityAuditRequired(event = {}) {
|
|
835
|
+
if (!securityAuditStore || !securityAuditHealthy) {
|
|
836
|
+
const error = new Error('security-audit-unavailable');
|
|
837
|
+
error.code = 'security-audit-unavailable';
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
return await securityAuditStore.record(securityAuditEvent(event));
|
|
842
|
+
} catch (error) {
|
|
843
|
+
markSecurityAuditFailed(error);
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function recordSecurityAudit(event = {}) {
|
|
849
|
+
void recordSecurityAuditRequired(event).catch(() => {
|
|
850
|
+
// Required mutation paths await recordSecurityAuditRequired directly.
|
|
851
|
+
// Best-effort lifecycle telemetry still marks the global audit gate failed.
|
|
828
852
|
});
|
|
829
853
|
}
|
|
830
854
|
|
|
@@ -1519,12 +1543,16 @@ async function synchronizeAgentEnablement() {
|
|
|
1519
1543
|
return enabled;
|
|
1520
1544
|
}
|
|
1521
1545
|
|
|
1522
|
-
const hubFilesystem = createHubFilesystem();
|
|
1546
|
+
const hubFilesystem = createHubFilesystem();
|
|
1547
|
+
const fileTransferCommandAckTimeoutMs = readPositiveIntegerEnv(
|
|
1548
|
+
'LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS',
|
|
1549
|
+
30_000
|
|
1550
|
+
);
|
|
1523
1551
|
hubTransferJobs = createHubTransferJobs({
|
|
1524
1552
|
filesystem: hubFilesystem,
|
|
1525
1553
|
remoteHub,
|
|
1526
1554
|
maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
|
|
1527
|
-
commandResultTimeoutMs:
|
|
1555
|
+
commandResultTimeoutMs: fileTransferCommandAckTimeoutMs,
|
|
1528
1556
|
getMaxFileSizeBytes: () => Number(
|
|
1529
1557
|
liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
|
|
1530
1558
|
|| 1024 * 1024 * 1024)
|
|
@@ -1677,7 +1705,7 @@ app.use((req, res, next) => {
|
|
|
1677
1705
|
}
|
|
1678
1706
|
const mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
|
|
1679
1707
|
const sensitiveRead = req.method === 'GET'
|
|
1680
|
-
&& ['/api/security/audit', '/api/privacy/inventory'].includes(req.path);
|
|
1708
|
+
&& ['/api/security/audit', '/api/security/rendezvous-issuer', '/api/privacy/inventory'].includes(req.path);
|
|
1681
1709
|
if (!mutating && !sensitiveRead) {
|
|
1682
1710
|
next();
|
|
1683
1711
|
return;
|
|
@@ -1713,7 +1741,58 @@ app.use((req, res, next) => {
|
|
|
1713
1741
|
return;
|
|
1714
1742
|
}
|
|
1715
1743
|
req.liveDeskAdminSessionId = localAdminSessionId;
|
|
1716
|
-
|
|
1744
|
+
if (!mutating) {
|
|
1745
|
+
next();
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
const mutationRequestHash = requestAuditHash({ method: req.method, path: req.path });
|
|
1750
|
+
void recordSecurityAuditRequired({
|
|
1751
|
+
action: 'local-admin.mutation',
|
|
1752
|
+
phase: 'accepted',
|
|
1753
|
+
result: 'accepted',
|
|
1754
|
+
sessionId: localAdminSessionId,
|
|
1755
|
+
requestHash: mutationRequestHash,
|
|
1756
|
+
authMethod: 'local-admin-session',
|
|
1757
|
+
details: { method: req.method, path: req.path }
|
|
1758
|
+
}).then(() => {
|
|
1759
|
+
const originalEnd = res.end.bind(res);
|
|
1760
|
+
let completionStarted = false;
|
|
1761
|
+
res.end = (...args) => {
|
|
1762
|
+
if (completionStarted) return res;
|
|
1763
|
+
completionStarted = true;
|
|
1764
|
+
const statusCode = Number(res.statusCode || 200);
|
|
1765
|
+
void recordSecurityAuditRequired({
|
|
1766
|
+
action: 'local-admin.mutation',
|
|
1767
|
+
phase: 'complete',
|
|
1768
|
+
result: statusCode >= 200 && statusCode < 400 ? 'success' : 'rejected',
|
|
1769
|
+
reason: statusCode >= 400 ? `http-${statusCode}` : '',
|
|
1770
|
+
sessionId: localAdminSessionId,
|
|
1771
|
+
requestHash: mutationRequestHash,
|
|
1772
|
+
authMethod: 'local-admin-session',
|
|
1773
|
+
details: { method: req.method, path: req.path, statusCode }
|
|
1774
|
+
}).then(() => {
|
|
1775
|
+
originalEnd(...args);
|
|
1776
|
+
}).catch(error => {
|
|
1777
|
+
if (res.headersSent) {
|
|
1778
|
+
res.destroy(error);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const callback = [...args].reverse().find(value => typeof value === 'function');
|
|
1782
|
+
const body = JSON.stringify({ ok: false, error: 'security-audit-unavailable' });
|
|
1783
|
+
res.statusCode = 503;
|
|
1784
|
+
res.removeHeader('Content-Length');
|
|
1785
|
+
res.removeHeader('Transfer-Encoding');
|
|
1786
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1787
|
+
res.setHeader('Content-Length', Buffer.byteLength(body));
|
|
1788
|
+
originalEnd(body, 'utf8', callback);
|
|
1789
|
+
});
|
|
1790
|
+
return res;
|
|
1791
|
+
};
|
|
1792
|
+
next();
|
|
1793
|
+
}).catch(() => {
|
|
1794
|
+
res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
|
|
1795
|
+
});
|
|
1717
1796
|
});
|
|
1718
1797
|
app.use(express.json({ limit: '32mb' }));
|
|
1719
1798
|
app.use((req, res, next) => {
|
|
@@ -4247,6 +4326,19 @@ app.get('/api/security/trusted-devices', (_req, res) => {
|
|
|
4247
4326
|
res.json({ ok: true, devices });
|
|
4248
4327
|
});
|
|
4249
4328
|
|
|
4329
|
+
app.get('/api/security/rendezvous-issuer', (_req, res) => {
|
|
4330
|
+
noStore(res);
|
|
4331
|
+
const security = remoteHub.getSecurityStatus();
|
|
4332
|
+
res.json({
|
|
4333
|
+
ok: true,
|
|
4334
|
+
hubId: security.hubId,
|
|
4335
|
+
issuerKeyId: security.hubIssuerKeyId,
|
|
4336
|
+
publicKey: security.hubPublicKey,
|
|
4337
|
+
algorithm: 'P-256/SHA-256',
|
|
4338
|
+
environmentVariable: 'LIVEDESK_UDP_RENDEZVOUS_TRUSTED_ISSUER_PUBLIC_KEYS'
|
|
4339
|
+
});
|
|
4340
|
+
});
|
|
4341
|
+
|
|
4250
4342
|
app.get('/api/security/audit', async (req, res) => {
|
|
4251
4343
|
noStore(res);
|
|
4252
4344
|
try {
|
|
@@ -5479,11 +5571,37 @@ app.post('/api/remote/files/from-hub', requireHubFeatureAccess, (req, res) => {
|
|
|
5479
5571
|
res.status(400).json({ ok: false, error: 'no-filesystem-items-selected' });
|
|
5480
5572
|
return;
|
|
5481
5573
|
}
|
|
5482
|
-
const job = hubTransferJobs.create({
|
|
5483
|
-
itemIds,
|
|
5484
|
-
deviceIds,
|
|
5485
|
-
remoteDirectory: req.body?.remoteDirectory
|
|
5486
|
-
|
|
5574
|
+
const job = hubTransferJobs.create({
|
|
5575
|
+
itemIds,
|
|
5576
|
+
deviceIds,
|
|
5577
|
+
remoteDirectory: req.body?.remoteDirectory,
|
|
5578
|
+
onComplete: async ({ completed, job: completedJob }) => {
|
|
5579
|
+
await recordSecurityAuditRequired({
|
|
5580
|
+
action: 'remote.file.job',
|
|
5581
|
+
phase: 'complete',
|
|
5582
|
+
result: completed ? 'success' : 'failed',
|
|
5583
|
+
reason: completed ? '' : completedJob?.error || 'file-transfer-failed',
|
|
5584
|
+
deviceIds,
|
|
5585
|
+
sessionId: completedJob?.jobId || '',
|
|
5586
|
+
requestHash: requestAuditHash({
|
|
5587
|
+
jobId: completedJob?.jobId || '',
|
|
5588
|
+
deviceIds,
|
|
5589
|
+
remoteDirectory: req.body?.remoteDirectory || ''
|
|
5590
|
+
}),
|
|
5591
|
+
authMethod: 'local-admin-session',
|
|
5592
|
+
details: {
|
|
5593
|
+
state: completedJob?.state || '',
|
|
5594
|
+
totalFiles: Number(completedJob?.totalFiles || 0),
|
|
5595
|
+
completedFiles: Number(completedJob?.completedFiles || 0),
|
|
5596
|
+
totalBytes: Number(completedJob?.totalBytes || 0),
|
|
5597
|
+
sentBytes: Number(completedJob?.sentBytes || 0),
|
|
5598
|
+
failedTargets: Array.isArray(completedJob?.failedTargets)
|
|
5599
|
+
? completedJob.failedTargets.length
|
|
5600
|
+
: 0
|
|
5601
|
+
}
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
});
|
|
5487
5605
|
res.status(202).json({ ok: true, jobId: job.jobId, state: job.state });
|
|
5488
5606
|
} catch (error) {
|
|
5489
5607
|
sendFilesystemError(res, error);
|
|
@@ -5564,7 +5682,7 @@ app.post('/api/remote/filesystem/shared-folders/:folderId/sync', requireHubFeatu
|
|
|
5564
5682
|
}
|
|
5565
5683
|
});
|
|
5566
5684
|
|
|
5567
|
-
app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
|
|
5685
|
+
app.post('/api/remote/files/transfer', requireHubFeatureAccess, async (req, res) => {
|
|
5568
5686
|
noStore(res);
|
|
5569
5687
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
|
|
5570
5688
|
if (deviceIds.length === 0) {
|
|
@@ -5581,44 +5699,53 @@ app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
|
|
|
5581
5699
|
const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
|
|
5582
5700
|
const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
|
|
5583
5701
|
const queuedAt = new Date().toISOString();
|
|
5584
|
-
|
|
5585
|
-
deviceId
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5702
|
+
try {
|
|
5703
|
+
const results = await Promise.all(deviceIds.map(async deviceId => ({
|
|
5704
|
+
deviceId,
|
|
5705
|
+
...await remoteHub.sendCommandAwaitResult(deviceId, {
|
|
5706
|
+
command: 'file.transfer',
|
|
5707
|
+
payload: {
|
|
5708
|
+
transferId,
|
|
5709
|
+
remoteDirectory,
|
|
5710
|
+
files: normalized.files,
|
|
5711
|
+
totalBytes: normalized.totalBytes,
|
|
5712
|
+
requestedAt: queuedAt
|
|
5713
|
+
}
|
|
5714
|
+
}, { timeoutMs: fileTransferCommandAckTimeoutMs })
|
|
5715
|
+
})));
|
|
5716
|
+
const queued = results.filter(result => result.queued).length;
|
|
5717
|
+
const acknowledged = results.filter(result => result.acknowledged).length;
|
|
5718
|
+
const success = acknowledged === deviceIds.length;
|
|
5719
|
+
const partial = acknowledged > 0 && !success;
|
|
5720
|
+
const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
|
|
5721
|
+
await recordSecurityAuditRequired({
|
|
5722
|
+
action: 'remote.file.transfer',
|
|
5723
|
+
phase: 'complete',
|
|
5724
|
+
result: success ? 'success' : partial ? 'partial' : 'rejected',
|
|
5725
|
+
reason: error || '',
|
|
5726
|
+
deviceIds,
|
|
5727
|
+
sessionId: transferId,
|
|
5728
|
+
requestHash: requestAuditHash({ transferId, remoteDirectory, files: normalized.files.map(file => ({ name: file.name, relativePath: file.relativePath, byteLength: file.size })), totalBytes: normalized.totalBytes }),
|
|
5729
|
+
authMethod: 'local-admin-session',
|
|
5730
|
+
details: { queued, acknowledged, total: deviceIds.length, totalBytes: normalized.totalBytes, files: normalized.files.length }
|
|
5731
|
+
});
|
|
5732
|
+
res.json({
|
|
5733
|
+
ok: success,
|
|
5734
|
+
transferId,
|
|
5735
|
+
queued,
|
|
5736
|
+
acknowledged,
|
|
5737
|
+
total: deviceIds.length,
|
|
5738
|
+
totalBytes: normalized.totalBytes,
|
|
5739
|
+
files: normalized.files.length,
|
|
5740
|
+
results,
|
|
5741
|
+
error
|
|
5742
|
+
});
|
|
5743
|
+
} catch (error) {
|
|
5744
|
+
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
|
|
5748
|
+
app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) => {
|
|
5622
5749
|
noStore(res);
|
|
5623
5750
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
|
|
5624
5751
|
if (deviceIds.length === 0) {
|
|
@@ -5636,42 +5763,51 @@ app.post('/api/remote/files/chunk', requireHubFeatureAccess, (req, res) => {
|
|
|
5636
5763
|
const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
|
|
5637
5764
|
const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
|
|
5638
5765
|
const queuedAt = new Date().toISOString();
|
|
5639
|
-
|
|
5640
|
-
deviceId
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5766
|
+
try {
|
|
5767
|
+
const results = await Promise.all(deviceIds.map(async deviceId => ({
|
|
5768
|
+
deviceId,
|
|
5769
|
+
...await remoteHub.sendCommandAwaitResult(deviceId, {
|
|
5770
|
+
command: 'file.transfer.chunk',
|
|
5771
|
+
payload: {
|
|
5772
|
+
transferId,
|
|
5773
|
+
remoteDirectory,
|
|
5774
|
+
...normalized.chunk,
|
|
5775
|
+
requestedAt: queuedAt
|
|
5776
|
+
}
|
|
5777
|
+
}, { timeoutMs: fileTransferCommandAckTimeoutMs })
|
|
5778
|
+
})));
|
|
5779
|
+
const queued = results.filter(result => result.queued).length;
|
|
5780
|
+
const acknowledged = results.filter(result => result.acknowledged).length;
|
|
5781
|
+
const success = acknowledged === deviceIds.length;
|
|
5782
|
+
const partial = acknowledged > 0 && !success;
|
|
5783
|
+
const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
|
|
5784
|
+
await recordSecurityAuditRequired({
|
|
5785
|
+
action: 'remote.file.chunk',
|
|
5786
|
+
phase: normalized.chunk.final ? 'complete' : 'progress',
|
|
5787
|
+
result: success ? 'success' : partial ? 'partial' : 'rejected',
|
|
5788
|
+
reason: error || '',
|
|
5789
|
+
deviceIds,
|
|
5790
|
+
sessionId: transferId,
|
|
5791
|
+
requestHash: requestAuditHash({ transferId, remoteDirectory, offset: normalized.chunk.offset, byteLength: normalized.chunk.byteLength, final: normalized.chunk.final }),
|
|
5792
|
+
authMethod: 'local-admin-session',
|
|
5793
|
+
details: { queued, acknowledged, total: deviceIds.length, byteLength: normalized.chunk.byteLength, offset: normalized.chunk.offset, final: normalized.chunk.final }
|
|
5794
|
+
});
|
|
5795
|
+
res.json({
|
|
5796
|
+
ok: success,
|
|
5797
|
+
transferId,
|
|
5798
|
+
queued,
|
|
5799
|
+
acknowledged,
|
|
5800
|
+
total: deviceIds.length,
|
|
5801
|
+
byteLength: normalized.chunk.byteLength,
|
|
5802
|
+
offset: normalized.chunk.offset,
|
|
5803
|
+
final: normalized.chunk.final,
|
|
5804
|
+
results,
|
|
5805
|
+
error
|
|
5806
|
+
});
|
|
5807
|
+
} catch (error) {
|
|
5808
|
+
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5809
|
+
}
|
|
5810
|
+
});
|
|
5675
5811
|
|
|
5676
5812
|
const MANUAL_POWER_ACTIONS = new Set(['lock', 'sleep', 'restart', 'shutdown']);
|
|
5677
5813
|
|
|
@@ -5990,11 +6126,37 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
5990
6126
|
socket.destroy();
|
|
5991
6127
|
return;
|
|
5992
6128
|
}
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
6129
|
+
let parsed;
|
|
6130
|
+
try {
|
|
6131
|
+
parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
6132
|
+
} catch {
|
|
6133
|
+
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
6134
|
+
socket.destroy();
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
const websocketPath = parsed.pathname;
|
|
6138
|
+
if (![
|
|
6139
|
+
'/api/remote/frames/ws',
|
|
6140
|
+
'/api/remote/atlas/ws',
|
|
6141
|
+
'/api/remote/input/ws',
|
|
6142
|
+
'/api/remote/audio/ws'
|
|
6143
|
+
].includes(websocketPath)) {
|
|
6144
|
+
socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
|
|
6145
|
+
socket.destroy();
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
void recordSecurityAuditRequired({
|
|
6149
|
+
action: 'local-admin.websocket',
|
|
6150
|
+
phase: 'accepted',
|
|
6151
|
+
result: 'accepted',
|
|
6152
|
+
sessionId: localAdminSessionId,
|
|
6153
|
+
requestHash: requestAuditHash({ path: websocketPath }),
|
|
6154
|
+
authMethod: 'local-admin-websocket',
|
|
6155
|
+
details: { path: websocketPath }
|
|
6156
|
+
}).then(() => {
|
|
6157
|
+
if (parsed.pathname === '/api/remote/frames/ws') {
|
|
6158
|
+
frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
|
|
6159
|
+
return;
|
|
5998
6160
|
}
|
|
5999
6161
|
if (parsed.pathname === '/api/remote/atlas/ws') {
|
|
6000
6162
|
atlasWss.handleUpgrade(req, socket, head, ws => atlasWss.emit('connection', ws, req));
|
|
@@ -6005,16 +6167,14 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
6005
6167
|
return;
|
|
6006
6168
|
}
|
|
6007
6169
|
if (parsed.pathname === '/api/remote/audio/ws') {
|
|
6008
|
-
audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
|
|
6009
|
-
return;
|
|
6010
|
-
}
|
|
6011
|
-
|
|
6012
|
-
socket.
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
}
|
|
6017
|
-
});
|
|
6170
|
+
audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
|
|
6171
|
+
return;
|
|
6172
|
+
}
|
|
6173
|
+
}).catch(() => {
|
|
6174
|
+
socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
|
6175
|
+
socket.destroy();
|
|
6176
|
+
});
|
|
6177
|
+
});
|
|
6018
6178
|
|
|
6019
6179
|
frameWss.on('connection', (ws, req) => {
|
|
6020
6180
|
frameClients.add(ws);
|
|
@@ -6127,23 +6287,21 @@ audioWss.on('connection', (ws, req) => {
|
|
|
6127
6287
|
});
|
|
6128
6288
|
|
|
6129
6289
|
inputWss.on('connection', ws => {
|
|
6130
|
-
inputClients.add(ws);
|
|
6131
|
-
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
6290
|
+
inputClients.add(ws);
|
|
6291
|
+
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
6132
6292
|
ws.liveDeskInputDeviceIds = new Set();
|
|
6133
|
-
|
|
6134
|
-
action: 'remote.control.browser-session',
|
|
6135
|
-
phase: 'start',
|
|
6136
|
-
result: 'success',
|
|
6137
|
-
sessionId: ws.liveDeskInputClientId,
|
|
6138
|
-
authMethod: 'local-admin-websocket'
|
|
6139
|
-
});
|
|
6293
|
+
ws.liveDeskInputAuditReady = false;
|
|
6140
6294
|
try {
|
|
6141
6295
|
ws._socket?.setNoDelay?.(true);
|
|
6142
6296
|
} catch {
|
|
6143
6297
|
// Best-effort latency hint for browser input sockets.
|
|
6144
|
-
}
|
|
6145
|
-
ws.on('message', data => {
|
|
6146
|
-
|
|
6298
|
+
}
|
|
6299
|
+
ws.on('message', data => {
|
|
6300
|
+
if (ws.liveDeskInputAuditReady !== true) {
|
|
6301
|
+
sendJson(ws, { type: 'RemoteInputError', error: 'security-audit-pending' });
|
|
6302
|
+
return;
|
|
6303
|
+
}
|
|
6304
|
+
let payload;
|
|
6147
6305
|
try {
|
|
6148
6306
|
payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
|
|
6149
6307
|
} catch {
|
|
@@ -6189,7 +6347,7 @@ inputWss.on('connection', ws => {
|
|
|
6189
6347
|
for (const deviceId of ws.liveDeskInputDeviceIds || []) {
|
|
6190
6348
|
remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
|
|
6191
6349
|
}
|
|
6192
|
-
|
|
6350
|
+
void recordSecurityAuditRequired({
|
|
6193
6351
|
action: 'remote.control.browser-session',
|
|
6194
6352
|
phase: 'complete',
|
|
6195
6353
|
result: 'success',
|
|
@@ -6197,18 +6355,31 @@ inputWss.on('connection', ws => {
|
|
|
6197
6355
|
deviceIds: [...(ws.liveDeskInputDeviceIds || [])],
|
|
6198
6356
|
sessionId: ws.liveDeskInputClientId,
|
|
6199
6357
|
authMethod: 'local-admin-websocket'
|
|
6358
|
+
}).catch(() => {
|
|
6359
|
+
// The global fail-closed gate is marked by recordSecurityAuditRequired.
|
|
6200
6360
|
});
|
|
6201
|
-
ws.liveDeskInputDeviceIds?.clear?.();
|
|
6202
|
-
};
|
|
6203
|
-
ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
|
|
6204
|
-
ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
})
|
|
6361
|
+
ws.liveDeskInputDeviceIds?.clear?.();
|
|
6362
|
+
};
|
|
6363
|
+
ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
|
|
6364
|
+
ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
|
|
6365
|
+
void recordSecurityAuditRequired({
|
|
6366
|
+
action: 'remote.control.browser-session',
|
|
6367
|
+
phase: 'start',
|
|
6368
|
+
result: 'success',
|
|
6369
|
+
sessionId: ws.liveDeskInputClientId,
|
|
6370
|
+
authMethod: 'local-admin-websocket'
|
|
6371
|
+
}).then(() => {
|
|
6372
|
+
ws.liveDeskInputAuditReady = true;
|
|
6373
|
+
sendJson(ws, {
|
|
6374
|
+
type: 'RemoteInputSocketReady',
|
|
6375
|
+
protocol: 'livedesk.remote.input.json.v1',
|
|
6376
|
+
clientId: ws.liveDeskInputClientId,
|
|
6377
|
+
timestamp: new Date().toISOString()
|
|
6378
|
+
});
|
|
6379
|
+
}).catch(() => {
|
|
6380
|
+
ws.close(1011, 'security-audit-unavailable');
|
|
6381
|
+
});
|
|
6382
|
+
});
|
|
6212
6383
|
|
|
6213
6384
|
await remoteHub.start();
|
|
6214
6385
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
@@ -1029,8 +1029,9 @@ export function createHubRelayControl({
|
|
|
1029
1029
|
throw relayError('secure-enrollment-proof-invalid');
|
|
1030
1030
|
}
|
|
1031
1031
|
verifyP256Signature(devicePublic.key, clientTranscript, message.deviceSignature);
|
|
1032
|
+
authority.assertEnrollmentAllowed?.({ accountId, deviceId, devicePublicKey: devicePublic.text });
|
|
1032
1033
|
if (!secureSession.consumeEnrollmentToken?.(enrollmentSecret)) throw relayError('secure-enrollment-token-used');
|
|
1033
|
-
issued = authority.issueCredential({ accountId, deviceId, devicePublicKey: devicePublic.text
|
|
1034
|
+
issued = authority.issueCredential({ accountId, deviceId, devicePublicKey: devicePublic.text });
|
|
1034
1035
|
} else {
|
|
1035
1036
|
const verified = authority.verifyCredential(message.credential, { accountId, deviceId });
|
|
1036
1037
|
if (verified.payload.devicePublicKey !== devicePublic.text) throw relayError('device-credential-binding-invalid');
|
|
@@ -237,8 +237,9 @@ export function createSecureDirectAcceptor({
|
|
|
237
237
|
throw directError('secure-enrollment-proof-invalid');
|
|
238
238
|
}
|
|
239
239
|
verifyP256Signature(devicePublic.key, clientTranscript, hello.deviceSignature);
|
|
240
|
+
authority.assertEnrollmentAllowed?.({ accountId, deviceId, devicePublicKey: devicePublic.text });
|
|
240
241
|
if (!consumeEnrollmentToken(token)) throw directError('secure-enrollment-token-used');
|
|
241
|
-
const issued = authority.issueCredential({ accountId, deviceId, devicePublicKey: devicePublic.text
|
|
242
|
+
const issued = authority.issueCredential({ accountId, deviceId, devicePublicKey: devicePublic.text });
|
|
242
243
|
credential = issued.credential;
|
|
243
244
|
credentialPayload = issued.payload;
|
|
244
245
|
enrollmentCount += 1;
|
|
@@ -441,12 +441,23 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
441
441
|
udpSessionId = crypto.randomUUID();
|
|
442
442
|
routeKey = udpSessionRouteKey(udpSessionId);
|
|
443
443
|
} while (sessionsByRouteKey.has(routeKey));
|
|
444
|
-
const
|
|
445
|
-
? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, ttlMs: 60_000 })
|
|
444
|
+
const hubProof = typeof rendezvousProofIssuer === 'function'
|
|
445
|
+
? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'hub', ttlMs: 60_000 })
|
|
446
446
|
: null;
|
|
447
|
-
const
|
|
447
|
+
const clientProof = typeof rendezvousProofIssuer === 'function'
|
|
448
|
+
? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'client', ttlMs: 60_000 })
|
|
449
|
+
: null;
|
|
450
|
+
const signedHubRendezvousProof = safeText(hubProof?.proof, 2048);
|
|
451
|
+
const signedClientRendezvousProof = safeText(clientProof?.proof, 2048);
|
|
448
452
|
const allowLegacyUnsignedProofForTests = env.LIVEDESK_TEST_MODE === '1'
|
|
449
453
|
|| env.LIVEDESK_TEST_ALLOW_LEGACY_UDP_RENDEZVOUS === '1';
|
|
454
|
+
const legacyRendezvousToken = allowLegacyUnsignedProofForTests
|
|
455
|
+
&& (!signedHubRendezvousProof || !signedClientRendezvousProof)
|
|
456
|
+
? crypto.randomBytes(16).toString('hex')
|
|
457
|
+
: '';
|
|
458
|
+
const proofExpiries = [hubProof?.payload?.expiresAt, clientProof?.payload?.expiresAt]
|
|
459
|
+
.map(Number)
|
|
460
|
+
.filter(Number.isSafeInteger);
|
|
450
461
|
const session = {
|
|
451
462
|
deviceId: id,
|
|
452
463
|
tcpSessionId: safeText(tcpSessionId, 160),
|
|
@@ -455,9 +466,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
455
466
|
routeKey,
|
|
456
467
|
key,
|
|
457
468
|
keyBase64: key.toString('base64'),
|
|
458
|
-
rendezvousToken:
|
|
459
|
-
|
|
460
|
-
rendezvousProofExpiresAt:
|
|
469
|
+
rendezvousToken: signedHubRendezvousProof || legacyRendezvousToken,
|
|
470
|
+
clientRendezvousToken: signedClientRendezvousProof || legacyRendezvousToken,
|
|
471
|
+
rendezvousProofExpiresAt: proofExpiries.length === 2 ? Math.min(...proofExpiries) : 0,
|
|
461
472
|
sendControl,
|
|
462
473
|
clientEndpoint: null,
|
|
463
474
|
peerEndpoint: null,
|
|
@@ -493,8 +504,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
|
|
|
493
504
|
frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
|
|
494
505
|
frameTimeoutMs,
|
|
495
506
|
maxPendingFrames,
|
|
496
|
-
rendezvous: rendezvousHost && session.
|
|
497
|
-
? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.
|
|
507
|
+
rendezvous: rendezvousHost && session.clientRendezvousToken
|
|
508
|
+
? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.clientRendezvousToken }
|
|
498
509
|
: null
|
|
499
510
|
});
|
|
500
511
|
sendRendezvousRegister(session);
|