termdock 1.4.98 → 1.4.99
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/.desktop-dist/main.js +211 -34
- package/dist/client/assets/{ModelPreview-Drqv0OTg.js → ModelPreview-CJK3Nmg4.js} +1 -1
- package/dist/client/assets/{abnfDiagram-VRR7QNED-Cl8CT45X.js → abnfDiagram-VRR7QNED-C-QKkcSr.js} +1 -1
- package/dist/client/assets/{arc-2Sc638oe.js → arc-BpjFutHZ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CNH9w9eN.js → architectureDiagram-ZJ3FMSHR-DeeuzGsk.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-ClvE_yP1.js → blockDiagram-677ZJIJ3-BkDiKR3-.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-CxIMU8l9.js → c4Diagram-LMCZKHZV-BX6424vk.js} +1 -1
- package/dist/client/assets/channel-BiaGwVDJ.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-CdvXmc6Z.js → chunk-2Q5K7J3B-DwNMfRvw.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-B1dA4l5A.js → chunk-32BRIVSS-D5g3chKu.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CVk_pzpf.js → chunk-5VM5RSS4-B2rw8Ldu.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-B46zhCa0.js → chunk-EX3LRPZG-j3CVdb45.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-1wRPE3eO.js → chunk-JWPE2WC7-iHTR9CAX.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-BzPrhNIS.js → chunk-MOJQB5TN-DVD1wXze.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-D0sDGnBf.js → chunk-RYQCIY6F-zZRopnRd.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-BRo0103H.js → chunk-V7JOEXUC-h4EZpc4k.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-CW-wSkDE.js → chunk-VR4S4FIN-C9MZH2Oy.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-BuWiQ0uq.js → chunk-XXDRQBXY-BNPDO5fQ.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-DYljVZD_.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-DYljVZD_.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-WzKVd03Q.js → cose-bilkent-JH36ORCC-DlqULumX.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-CfXYAqnq.js → cynefin-VYW2F7L2-D6pWZSwi.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BnJGle_X.js → cynefinDiagram-TSTJHNR4-B6SI7XOe.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-CDDlGCQy.js → dagre-VKFMJZFB-DXnKJ4XD.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-t8DNZS7P.js → diagram-FQU43EPY-B5NTAhw6.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-CZpQd91k.js → diagram-G47NLZAW-Dihaal0B.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-OET0EFlt.js → diagram-NH7WQ7WH-Be987DPJ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-COgju4NX.js → diagram-OA4YK3LP-CfgTeXAJ.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-6gNSoIek.js → diagram-WEI45ONY-BgrK8rP2.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-CTlIMc8W.js → ebnfDiagram-CCIWWBDH-BPsH1_QO.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-BDsEvZvq.js → erDiagram-Q63AITRT-F3s7Ytrc.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-ggl4zaY9.js → flowDiagram-23GEKE2U-ByL-8svD.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-D9Ca5oWS.js → ganttDiagram-NO4QXBWP-Dtv-fi06.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-THPUH6Bg.js → gitGraphDiagram-IHSO6WYX-CeD48POl.js} +1 -1
- package/dist/client/assets/{index-BuMMW0wS.js → index-C82Uc6Vy.js} +4 -4
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-CKF-vSTz.js → infoDiagram-FWYZ7A6U-BxvLe33p.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CBFlmgI3.js → ishikawaDiagram-FXEZZL3T-XPW1aoJK.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-lbXIqX5a.js → journeyDiagram-5HDEW3XC-Bhiae04Z.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BUuO_e54.js → kanban-definition-HUTT4EX6-D5iOY3Zc.js} +1 -1
- package/dist/client/assets/{linear-D_uoNzR8.js → linear-PCgLsFTe.js} +1 -1
- package/dist/client/assets/{mermaid.core-D4Yy5OxZ.js → mermaid.core-DWuc-xG5.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-B0Ml-jaq.js → mindmap-definition-LN4V7U3C-C0EFK91v.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-CKF9cuqx.js → pegDiagram-2B236MQR-ClTz8aHa.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-DL3XisAD.js → pieDiagram-ENE6RG2P-BuTv1DGN.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-DrsHXXqd.js → quadrantDiagram-ABIIQ3AL-FLZC-cqI.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-DMAz8hLg.js → railroadDiagram-RFXS5EU6-DK68FlFh.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-CCHretnQ.js → requirementDiagram-TGXJPOKE-B_701AJ1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-Btb7ZNwU.js → sankeyDiagram-HTMAVEWB-Bk87L7JG.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-DcZk1PdX.js → sequenceDiagram-DBY2YBRQ-Biy5tPTS.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-vMYb_VZU.js → sizeCapture-X5ZJPWSS-__4zzUuh.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-CltexqJj.js → stateDiagram-2N3HPSRC-B_400cf9.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CK77br3V.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-DPnwlpXX.js → swimlanes-5IMT3BWC-CxWZz0n0.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-DbYe8ax3.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-l78vfKln.js → timeline-definition-FHXFAJF6-DSrIUp8H.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-BggNEe_f.js → vennDiagram-L72KCM5P-CdwaUmpL.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-v04USz2e.js → wardleyDiagram-EHGQE667-DwxlF0Xk.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-CInHBOpK.js → xychartDiagram-FW5EYKEG-CQNmEc_n.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/sw.js +1 -1
- package/dist/server/agent/sessionSearchStore.js +38 -10
- package/dist/server/routes/terminal.js +206 -30
- package/package.json +3 -2
- package/runtime-manifest.json +2 -2
- package/dist/client/assets/channel-DPt818Jx.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-C2hZbTnO.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-C2hZbTnO.js +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BslbFNw3.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-B0V1XVQb.js +0 -8
package/.desktop-dist/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFile, spawn } from 'node:child_process';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import dgram from 'node:dgram';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
+
import https from 'node:https';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { promisify } from 'node:util';
|
|
@@ -11,6 +12,7 @@ import { checkForDesktopUpdates, configureDesktopUpdater, ensureLatestRuntime, g
|
|
|
11
12
|
import { resolvePackagedRuntime, rollbackDownloadedRuntime, } from './runtime.js';
|
|
12
13
|
import { isExternalLinkStagingUrl, isSafeExternalUrl } from './externalLinks.js';
|
|
13
14
|
import { shouldThrottleDesktopRenderer } from './windowPolicy.js';
|
|
15
|
+
import { canOfferCertificateTrust, downloadCertificateAuthority, isCertificateTrustError, isLocalNetworkHostname, } from './certificateTrust.js';
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const projectRoot = path.resolve(currentDir, '..');
|
|
@@ -22,6 +24,9 @@ const PROTOCOL_VERSION = 1;
|
|
|
22
24
|
const HEALTH_TIMEOUT_MS = 3_500;
|
|
23
25
|
const START_TIMEOUT_MS = 90_000;
|
|
24
26
|
const localServiceCertificatePath = path.join(termdockDir, 'certs', 'termdock-local.pem');
|
|
27
|
+
const sessionTrustedCertificateTargets = new Set();
|
|
28
|
+
const sessionTrustedCertificateAuthorities = new Map();
|
|
29
|
+
let managedLocalCertificateFingerprint = null;
|
|
25
30
|
/** Delivered-but-unseen desktop notifications, mirrored into the Dock badge. */
|
|
26
31
|
const activeNotifications = new Map();
|
|
27
32
|
let unreadNotificationCount = 0;
|
|
@@ -87,17 +92,11 @@ function openExternalLink(url) {
|
|
|
87
92
|
return;
|
|
88
93
|
void shell.openExternal(url);
|
|
89
94
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
catch {
|
|
96
|
-
// The certificate is created when the local service first enables HTTPS.
|
|
97
|
-
}
|
|
98
|
-
if (!localFingerprint)
|
|
99
|
-
return;
|
|
100
|
-
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
95
|
+
function certificateTrustKey(hostname, fingerprint) {
|
|
96
|
+
return `${hostname.toLowerCase().replace(/^\[|\]$/g, '')}\0${fingerprint}`;
|
|
97
|
+
}
|
|
98
|
+
function installCertificateVerifyProcedure(targetSession = session.defaultSession) {
|
|
99
|
+
targetSession.setCertificateVerifyProc((request, callback) => {
|
|
101
100
|
let isLocalTarget = false;
|
|
102
101
|
try {
|
|
103
102
|
isLocalTarget = isLocalNetworkTarget(new URL(`https://${request.hostname}`));
|
|
@@ -112,11 +111,30 @@ function configureLocalServiceCertificateTrust() {
|
|
|
112
111
|
catch {
|
|
113
112
|
// Keep Chromium's default verification if the certificate cannot be parsed.
|
|
114
113
|
}
|
|
115
|
-
|
|
114
|
+
const explicitlyTrustedTarget = presentedFingerprint
|
|
115
|
+
? sessionTrustedCertificateTargets.has(certificateTrustKey(request.hostname, presentedFingerprint))
|
|
116
|
+
: false;
|
|
117
|
+
const managedLocalCertificate = isLocalTarget
|
|
118
|
+
&& presentedFingerprint === managedLocalCertificateFingerprint;
|
|
119
|
+
callback(explicitlyTrustedTarget || managedLocalCertificate ? 0 : -3);
|
|
116
120
|
});
|
|
117
121
|
}
|
|
122
|
+
function configureLocalServiceCertificateTrust() {
|
|
123
|
+
try {
|
|
124
|
+
managedLocalCertificateFingerprint = new crypto.X509Certificate(fs.readFileSync(localServiceCertificatePath)).fingerprint256;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The certificate is created when the local service first enables HTTPS.
|
|
128
|
+
}
|
|
129
|
+
installCertificateVerifyProcedure();
|
|
130
|
+
}
|
|
118
131
|
function defaultConfig() {
|
|
119
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
version: 1,
|
|
134
|
+
connections: [],
|
|
135
|
+
lastConnectionUrl: null,
|
|
136
|
+
trustedCertificateAuthorities: [],
|
|
137
|
+
};
|
|
120
138
|
}
|
|
121
139
|
function normalizeServiceUrl(raw) {
|
|
122
140
|
const value = raw.trim();
|
|
@@ -134,25 +152,7 @@ function normalizeServiceUrl(raw) {
|
|
|
134
152
|
return parsed.toString().replace(/\/$/, '');
|
|
135
153
|
}
|
|
136
154
|
function isLocalNetworkTarget(url) {
|
|
137
|
-
|
|
138
|
-
if (hostname === 'localhost'
|
|
139
|
-
|| hostname.endsWith('.local')
|
|
140
|
-
|| !hostname.includes('.')) {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
if (/^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^169\.254\./.test(hostname)) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
const ipv4 = hostname.match(/^172\.(\d{1,3})\./);
|
|
147
|
-
if (ipv4) {
|
|
148
|
-
const secondOctet = Number(ipv4[1]);
|
|
149
|
-
if (secondOctet >= 16 && secondOctet <= 31)
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
return hostname === '::1'
|
|
153
|
-
|| hostname.startsWith('fe80:')
|
|
154
|
-
|| hostname.startsWith('fc')
|
|
155
|
-
|| hostname.startsWith('fd');
|
|
155
|
+
return isLocalNetworkHostname(url.hostname);
|
|
156
156
|
}
|
|
157
157
|
function networkErrorDetails(error) {
|
|
158
158
|
if (!(error instanceof Error))
|
|
@@ -163,6 +163,72 @@ function networkErrorDetails(error) {
|
|
|
163
163
|
function looksLikeLocalNetworkPermissionError(error) {
|
|
164
164
|
return /ERR_ADDRESS_UNREACHABLE|ERR_NETWORK_ACCESS_DENIED/i.test(networkErrorDetails(error));
|
|
165
165
|
}
|
|
166
|
+
function certificateDetail(target, certificate) {
|
|
167
|
+
return [
|
|
168
|
+
`目标:${target}`,
|
|
169
|
+
`CA:${certificate.subject.replace(/\n/g, ', ')}`,
|
|
170
|
+
`CA SHA-256:${certificate.fingerprint256}`,
|
|
171
|
+
`服务证书 SHA-256:${certificate.leafFingerprint256}`,
|
|
172
|
+
`有效期:${certificate.validFrom} — ${certificate.validTo}`,
|
|
173
|
+
'',
|
|
174
|
+
'继续后仅 Termdock 会信任该目标使用的这张 CA,不会修改 macOS 系统钥匙串。请只信任你确认属于该服务的证书。',
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
function trustedCertificateAuthorityFor(target) {
|
|
178
|
+
const origin = new URL(target).origin;
|
|
179
|
+
return readDesktopConfig().trustedCertificateAuthorities.find((entry) => entry.origin === origin);
|
|
180
|
+
}
|
|
181
|
+
async function requestCertificateTrust(target) {
|
|
182
|
+
let certificate;
|
|
183
|
+
try {
|
|
184
|
+
certificate = await downloadCertificateAuthority(target);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
await showDesktopMessageBox({
|
|
188
|
+
type: 'error',
|
|
189
|
+
title: '无法获取可信的 CA 证书',
|
|
190
|
+
message: `无法为 ${target} 准备证书信任`,
|
|
191
|
+
detail: networkErrorDetails(error),
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const existingTrust = trustedCertificateAuthorityFor(target);
|
|
196
|
+
if (existingTrust?.fingerprint256 === certificate.fingerprint256) {
|
|
197
|
+
sessionTrustedCertificateAuthorities.set(new URL(target).origin, certificate.certificatePem);
|
|
198
|
+
sessionTrustedCertificateTargets.add(certificateTrustKey(new URL(target).hostname, certificate.leafFingerprint256));
|
|
199
|
+
return certificate;
|
|
200
|
+
}
|
|
201
|
+
const confirmation = await showDesktopMessageBox({
|
|
202
|
+
type: 'warning',
|
|
203
|
+
title: '信任 HTTPS 服务证书',
|
|
204
|
+
message: existingTrust
|
|
205
|
+
? '此服务的 CA 证书已发生变化'
|
|
206
|
+
: '此服务使用了 Termdock 尚未信任的 HTTPS 证书',
|
|
207
|
+
detail: `${certificateDetail(target, certificate)}${existingTrust
|
|
208
|
+
? `\n\n此前信任的 CA SHA-256:${existingTrust.fingerprint256}`
|
|
209
|
+
: ''}`,
|
|
210
|
+
buttons: ['信任并连接', '取消'],
|
|
211
|
+
defaultId: 1,
|
|
212
|
+
cancelId: 1,
|
|
213
|
+
noLink: true,
|
|
214
|
+
});
|
|
215
|
+
if (confirmation.response !== 0)
|
|
216
|
+
return null;
|
|
217
|
+
const config = readDesktopConfig();
|
|
218
|
+
const origin = new URL(target).origin;
|
|
219
|
+
config.trustedCertificateAuthorities = config.trustedCertificateAuthorities
|
|
220
|
+
.filter((entry) => entry.origin !== origin);
|
|
221
|
+
config.trustedCertificateAuthorities.push({
|
|
222
|
+
origin,
|
|
223
|
+
fingerprint256: certificate.fingerprint256,
|
|
224
|
+
subject: certificate.subject,
|
|
225
|
+
trustedAt: Date.now(),
|
|
226
|
+
});
|
|
227
|
+
writeDesktopConfig(config);
|
|
228
|
+
sessionTrustedCertificateAuthorities.set(origin, certificate.certificatePem);
|
|
229
|
+
sessionTrustedCertificateTargets.add(certificateTrustKey(new URL(target).hostname, certificate.leafFingerprint256));
|
|
230
|
+
return certificate;
|
|
231
|
+
}
|
|
166
232
|
async function requestLocalNetworkPermissionRetry(target) {
|
|
167
233
|
const result = await showDesktopMessageBox({
|
|
168
234
|
type: 'warning',
|
|
@@ -190,6 +256,13 @@ function readDesktopConfig() {
|
|
|
190
256
|
&& typeof entry.label === 'string'
|
|
191
257
|
&& typeof entry.url === 'string'),
|
|
192
258
|
lastConnectionUrl: typeof parsed.lastConnectionUrl === 'string' ? parsed.lastConnectionUrl : null,
|
|
259
|
+
trustedCertificateAuthorities: Array.isArray(parsed.trustedCertificateAuthorities)
|
|
260
|
+
? parsed.trustedCertificateAuthorities.filter((entry) => Boolean(entry)
|
|
261
|
+
&& typeof entry.origin === 'string'
|
|
262
|
+
&& typeof entry.fingerprint256 === 'string'
|
|
263
|
+
&& typeof entry.subject === 'string'
|
|
264
|
+
&& typeof entry.trustedAt === 'number')
|
|
265
|
+
: [],
|
|
193
266
|
};
|
|
194
267
|
}
|
|
195
268
|
catch {
|
|
@@ -250,6 +323,78 @@ async function fetchWithTimeout(url) {
|
|
|
250
323
|
clearTimeout(timer);
|
|
251
324
|
}
|
|
252
325
|
}
|
|
326
|
+
async function fetchJsonWithCertificateAuthority(rawUrl, certificateAuthority) {
|
|
327
|
+
return await new Promise((resolve, reject) => {
|
|
328
|
+
const request = https.get(rawUrl, {
|
|
329
|
+
agent: false,
|
|
330
|
+
ca: certificateAuthority,
|
|
331
|
+
rejectUnauthorized: true,
|
|
332
|
+
headers: { Accept: 'application/json' },
|
|
333
|
+
}, (response) => {
|
|
334
|
+
const chunks = [];
|
|
335
|
+
let size = 0;
|
|
336
|
+
response.on('data', (chunk) => {
|
|
337
|
+
size += chunk.length;
|
|
338
|
+
if (size > 256 * 1024) {
|
|
339
|
+
request.destroy(new Error('服务响应过大'));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
chunks.push(chunk);
|
|
343
|
+
});
|
|
344
|
+
response.on('end', () => {
|
|
345
|
+
try {
|
|
346
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
347
|
+
resolve({
|
|
348
|
+
status: response.statusCode ?? 0,
|
|
349
|
+
json: body ? JSON.parse(body) : null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
reject(new Error('服务返回了无效的 JSON'));
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
request.setTimeout(HEALTH_TIMEOUT_MS, () => request.destroy(new Error('连接超时')));
|
|
358
|
+
request.on('error', reject);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async function probeServiceWithCertificateAuthority(rawUrl, certificateAuthority) {
|
|
362
|
+
const url = normalizeServiceUrl(rawUrl);
|
|
363
|
+
try {
|
|
364
|
+
const healthResponse = await fetchJsonWithCertificateAuthority(`${url}/health`, certificateAuthority);
|
|
365
|
+
if (healthResponse.status < 200 || healthResponse.status >= 300) {
|
|
366
|
+
return { ok: false, url, error: `健康检查返回 HTTP ${healthResponse.status}` };
|
|
367
|
+
}
|
|
368
|
+
const health = healthResponse.json;
|
|
369
|
+
if (health?.status !== 'ok') {
|
|
370
|
+
return { ok: false, url, error: '目标地址不是可识别的 Termdock 服务' };
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const metadataResponse = await fetchJsonWithCertificateAuthority(`${url}/api/meta`, certificateAuthority);
|
|
374
|
+
if (metadataResponse.status >= 200 && metadataResponse.status < 300) {
|
|
375
|
+
const metadata = metadataResponse.json;
|
|
376
|
+
if (metadata?.product && metadata.product !== 'termdock') {
|
|
377
|
+
return { ok: false, url, error: '目标服务的产品标识不是 Termdock' };
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
url,
|
|
382
|
+
version: typeof metadata?.version === 'string' ? metadata.version : undefined,
|
|
383
|
+
protocolVersion: typeof metadata?.protocolVersion === 'number'
|
|
384
|
+
? metadata.protocolVersion
|
|
385
|
+
: undefined,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Older Termdock releases do not expose /api/meta.
|
|
391
|
+
}
|
|
392
|
+
return { ok: true, url };
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
return { ok: false, url, error: networkErrorDetails(error) };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
253
398
|
async function probeService(rawUrl) {
|
|
254
399
|
let url;
|
|
255
400
|
try {
|
|
@@ -293,12 +438,27 @@ async function probeService(rawUrl) {
|
|
|
293
438
|
catch (error) {
|
|
294
439
|
const message = error instanceof Error && error.name === 'AbortError'
|
|
295
440
|
? '连接超时'
|
|
296
|
-
: (error
|
|
441
|
+
: networkErrorDetails(error);
|
|
297
442
|
return { ok: false, url, error: message };
|
|
298
443
|
}
|
|
299
444
|
}
|
|
300
445
|
async function probeServiceWithLocalNetworkPermission(rawUrl) {
|
|
301
|
-
let
|
|
446
|
+
let normalizedUrl;
|
|
447
|
+
try {
|
|
448
|
+
normalizedUrl = normalizeServiceUrl(rawUrl);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return await probeService(rawUrl);
|
|
452
|
+
}
|
|
453
|
+
// Do the first macOS HTTPS probe with Node TLS. Sending an untrusted
|
|
454
|
+
// certificate through Chromium first permanently caches that rejection for
|
|
455
|
+
// the process and prevents an immediate retry after the user approves it.
|
|
456
|
+
const approvedAuthority = sessionTrustedCertificateAuthorities.get(new URL(normalizedUrl).origin);
|
|
457
|
+
let probe = approvedAuthority
|
|
458
|
+
? await probeServiceWithCertificateAuthority(normalizedUrl, approvedAuthority)
|
|
459
|
+
: canOfferCertificateTrust(normalizedUrl)
|
|
460
|
+
? await probeServiceWithCertificateAuthority(normalizedUrl)
|
|
461
|
+
: await probeService(normalizedUrl);
|
|
302
462
|
if (probe.ok)
|
|
303
463
|
return probe;
|
|
304
464
|
let parsedProbeUrl;
|
|
@@ -308,6 +468,13 @@ async function probeServiceWithLocalNetworkPermission(rawUrl) {
|
|
|
308
468
|
catch {
|
|
309
469
|
return probe;
|
|
310
470
|
}
|
|
471
|
+
if (canOfferCertificateTrust(probe.url) && isCertificateTrustError(probe.error)) {
|
|
472
|
+
const certificate = await requestCertificateTrust(probe.url);
|
|
473
|
+
if (!certificate) {
|
|
474
|
+
return { ...probe, error: 'HTTPS 证书尚未受信任' };
|
|
475
|
+
}
|
|
476
|
+
return await probeServiceWithCertificateAuthority(rawUrl, certificate.certificatePem);
|
|
477
|
+
}
|
|
311
478
|
if (!isLocalNetworkTarget(parsedProbeUrl) || !looksLikeLocalNetworkPermissionError(probe.error)) {
|
|
312
479
|
return probe;
|
|
313
480
|
}
|
|
@@ -685,6 +852,15 @@ async function showConnectionCenter() {
|
|
|
685
852
|
showAndFocusWindow(mainWindow);
|
|
686
853
|
}
|
|
687
854
|
function createDesktopWindow(options) {
|
|
855
|
+
const serviceSession = options
|
|
856
|
+
? session.fromPartition(`persist:termdock-service-${crypto
|
|
857
|
+
.createHash('sha256')
|
|
858
|
+
.update(options.serviceOrigin)
|
|
859
|
+
.digest('hex')
|
|
860
|
+
.slice(0, 24)}`)
|
|
861
|
+
: null;
|
|
862
|
+
if (serviceSession)
|
|
863
|
+
installCertificateVerifyProcedure(serviceSession);
|
|
688
864
|
const window = new BrowserWindow({
|
|
689
865
|
title: options ? `Termdock — ${options.label}` : 'Termdock — 连接中心',
|
|
690
866
|
show: false,
|
|
@@ -699,6 +875,7 @@ function createDesktopWindow(options) {
|
|
|
699
875
|
titleBarStyle: 'hiddenInset',
|
|
700
876
|
trafficLightPosition: { x: 16, y: 16 },
|
|
701
877
|
webPreferences: {
|
|
878
|
+
...(serviceSession ? { session: serviceSession } : {}),
|
|
702
879
|
preload: path.join(currentDir, 'preload.cjs'),
|
|
703
880
|
nodeIntegration: false,
|
|
704
881
|
contextIsolation: true,
|