termdock 1.4.239 → 1.4.240
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.
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from 'util';
|
|
2
|
+
import { getMissingCertificateNames } from './utils/certificateNames.js';
|
|
4
3
|
import { getLocalAccessSetting } from './utils/settings.js';
|
|
5
4
|
import { getLanIPv4Addresses } from './utils/localAccess.js';
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
5
|
const CHECK_INTERVAL_MS = 15_000;
|
|
8
6
|
const RETRY_INTERVAL_MS = 60_000;
|
|
9
7
|
function fileExists(filePath) {
|
|
10
8
|
return typeof filePath === 'string' && filePath.length > 0;
|
|
11
9
|
}
|
|
12
|
-
async function readCertificateSans(certPath) {
|
|
13
|
-
try {
|
|
14
|
-
const { stdout } = await execFileAsync('openssl', ['x509', '-in', certPath, '-noout', '-ext', 'subjectAltName'], {
|
|
15
|
-
timeout: 5000,
|
|
16
|
-
maxBuffer: 256 * 1024,
|
|
17
|
-
});
|
|
18
|
-
return stdout
|
|
19
|
-
.split(/[\n,]/)
|
|
20
|
-
.map((part) => part.trim())
|
|
21
|
-
.filter(Boolean);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function sanMatches(required, sans) {
|
|
28
|
-
if (required === '::1') {
|
|
29
|
-
return sans.some((san) => san === 'IP Address:::1' || san === 'IP Address:0:0:0:0:0:0:0:1' || san === 'IP:::1');
|
|
30
|
-
}
|
|
31
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(required) || required.includes(':')) {
|
|
32
|
-
return sans.some((san) => san === `IP Address:${required}` || san === `IP:${required}`);
|
|
33
|
-
}
|
|
34
|
-
return sans.some((san) => san === `DNS:${required}`);
|
|
35
|
-
}
|
|
36
10
|
function requiredNames() {
|
|
37
11
|
const localName = getLocalAccessSetting().name;
|
|
38
12
|
return [
|
|
@@ -85,8 +59,7 @@ export class CertificateWatcher extends EventEmitter {
|
|
|
85
59
|
return;
|
|
86
60
|
this.checking = true;
|
|
87
61
|
try {
|
|
88
|
-
const
|
|
89
|
-
const missing = requiredNames().filter((name) => !sanMatches(name, sans));
|
|
62
|
+
const missing = await getMissingCertificateNames(this.options.certPath, requiredNames());
|
|
90
63
|
if (missing.length === 0) {
|
|
91
64
|
this.refreshInFlight = false;
|
|
92
65
|
this.pendingKey = null;
|
|
@@ -104,6 +77,11 @@ export class CertificateWatcher extends EventEmitter {
|
|
|
104
77
|
console.log(`[cert-watch] certificate missing SANs (${missing.join(', ')}); regenerating and reloading TLS context`);
|
|
105
78
|
this.emit('refresh-needed', missing);
|
|
106
79
|
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// An unreadable certificate is not evidence of missing SANs. Keep the
|
|
82
|
+
// active TLS context instead of repeatedly regenerating the certificate.
|
|
83
|
+
console.warn('[cert-watch] could not inspect certificate; keeping current TLS context:', error);
|
|
84
|
+
}
|
|
107
85
|
finally {
|
|
108
86
|
this.checking = false;
|
|
109
87
|
}
|
package/dist/server/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { detectLocalSessionContext, resolveLocalCollaborationContext } from './a
|
|
|
20
20
|
import { parseAutomationCommand, executeAutomationCommand, AUTOMATION_HELP } from './agent/automationCli.js';
|
|
21
21
|
import { parseCollaborationCommand, executeCollaborationCommand, COLLAB_HELP } from './agent/collaborationCli.js';
|
|
22
22
|
import fs from 'fs';
|
|
23
|
+
import { getMissingCertificateNames } from './utils/certificateNames.js';
|
|
23
24
|
import http from 'http';
|
|
24
25
|
import https from 'https';
|
|
25
26
|
import path from 'path';
|
|
@@ -417,50 +418,10 @@ function getRequiredLocalHttpsNames() {
|
|
|
417
418
|
'::1',
|
|
418
419
|
];
|
|
419
420
|
}
|
|
420
|
-
async function readCertificateSans(certPath) {
|
|
421
|
-
try {
|
|
422
|
-
const { stdout } = await execFileAsync('openssl', ['x509', '-in', certPath, '-noout', '-ext', 'subjectAltName'], {
|
|
423
|
-
timeout: 5000,
|
|
424
|
-
maxBuffer: 256 * 1024,
|
|
425
|
-
});
|
|
426
|
-
return stdout
|
|
427
|
-
.split(/[\n,]/)
|
|
428
|
-
.map((part) => part.trim())
|
|
429
|
-
.filter(Boolean);
|
|
430
|
-
}
|
|
431
|
-
catch {
|
|
432
|
-
return [];
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
function sanMatches(required, sans) {
|
|
436
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(required)) {
|
|
437
|
-
return sans.some((san) => san === `IP Address:${required}` || san === `IP:${required}`);
|
|
438
|
-
}
|
|
439
|
-
if (required.includes(':')) {
|
|
440
|
-
const normalizeIpv6 = (value) => {
|
|
441
|
-
const [left = '', right = ''] = value.toLowerCase().split('::');
|
|
442
|
-
const leftGroups = left ? left.split(':') : [];
|
|
443
|
-
const rightGroups = right ? right.split(':') : [];
|
|
444
|
-
const missing = Math.max(0, 8 - leftGroups.length - rightGroups.length);
|
|
445
|
-
return [...leftGroups, ...Array(missing).fill('0'), ...rightGroups]
|
|
446
|
-
.map((group) => Number.parseInt(group || '0', 16).toString(16))
|
|
447
|
-
.join(':');
|
|
448
|
-
};
|
|
449
|
-
const normalizedRequired = normalizeIpv6(required);
|
|
450
|
-
return sans.some((san) => {
|
|
451
|
-
const match = /^(?:IP Address|IP):(.+)$/.exec(san);
|
|
452
|
-
return match ? normalizeIpv6(match[1]) === normalizedRequired : false;
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
return sans.some((san) => san === `DNS:${required}`);
|
|
456
|
-
}
|
|
457
421
|
async function defaultCertificateNeedsRefresh() {
|
|
458
422
|
if (!fileExists(defaultHttpsCertPath) || !fileExists(defaultHttpsKeyPath))
|
|
459
423
|
return true;
|
|
460
|
-
|
|
461
|
-
if (sans.length === 0)
|
|
462
|
-
return true;
|
|
463
|
-
return getRequiredLocalHttpsNames().some((name) => !sanMatches(name, sans));
|
|
424
|
+
return (await getMissingCertificateNames(defaultHttpsCertPath, getRequiredLocalHttpsNames())).length > 0;
|
|
464
425
|
}
|
|
465
426
|
async function ensureDefaultHttpsCertificateFresh() {
|
|
466
427
|
if (!(await defaultCertificateNeedsRefresh()))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { X509Certificate } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { isIP } from 'node:net';
|
|
4
|
+
// Use Node's certificate parser: the system openssl (notably LibreSSL on
|
|
5
|
+
// macOS) may not support `x509 -ext`. Propagate read/parse errors so callers
|
|
6
|
+
// cannot mistake an inspection failure for an empty SAN extension.
|
|
7
|
+
export async function getMissingCertificateNames(certPath, required) {
|
|
8
|
+
const cert = new X509Certificate(await readFile(certPath));
|
|
9
|
+
return required.filter((name) => isIP(name)
|
|
10
|
+
? cert.checkIP(name) === undefined
|
|
11
|
+
: cert.checkHost(name, { subject: 'never', wildcards: false }) === undefined);
|
|
12
|
+
}
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.240",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-K4KyYJuVoQDpZsBM8ZX1Rw3MddOxAzVdp/cPm0iGZz0=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-OjLwoxfVNp7VlqCwrjCUGgjj0Yb3zlTm1sdlQGCGYIs=",
|
|
10
10
|
"clientBundleHash": "sha256-1wPbozETRlatzFMaBkX5wcha5zbSwUdAw4I0XckzdLk=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|