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 { execFile } from 'child_process';
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 sans = await readCertificateSans(this.options.certPath);
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
  }
@@ -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
- const sans = await readCertificateSans(defaultHttpsCertPath);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.239",
3
+ "version": "1.4.240",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.239",
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-b2hW5XzG58ErGFDet7CETfl7sHtPoDhHjTnL3mzU63s=",
9
+ "serverBundleHash": "sha256-OjLwoxfVNp7VlqCwrjCUGgjj0Yb3zlTm1sdlQGCGYIs=",
10
10
  "clientBundleHash": "sha256-1wPbozETRlatzFMaBkX5wcha5zbSwUdAw4I0XckzdLk=",
11
11
  "entrypoint": "dist/server/cli.js",
12
12
  "clientEntrypoint": "dist/client/index.html"