dominds 1.25.19 → 1.26.0
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/README.md +17 -0
- package/README.zh.md +17 -0
- package/dist/apps/runtime.d.ts +2 -4
- package/dist/apps-host/client.d.ts +2 -5
- package/dist/apps-host/ipc-types.d.ts +2 -5
- package/dist/apps-host/ipc-types.js +5 -1
- package/dist/cli/cert.d.ts +2 -0
- package/dist/cli/cert.js +198 -0
- package/dist/cli/webui.d.ts +1 -1
- package/dist/cli/webui.js +34 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +15 -3
- package/dist/docs/cli-usage.md +36 -0
- package/dist/docs/cli-usage.zh.md +36 -0
- package/dist/server/auth.d.ts +7 -0
- package/dist/server/auth.js +15 -4
- package/dist/server/certificates.d.ts +61 -0
- package/dist/server/certificates.js +418 -0
- package/dist/server/network-hosts.d.ts +5 -0
- package/dist/server/network-hosts.js +188 -0
- package/dist/server/server-core.d.ts +12 -1
- package/dist/server/server-core.js +26 -4
- package/dist/server/websocket-handler.d.ts +3 -2
- package/dist/server/websocket-handler.js +15 -8
- package/dist/server.d.ts +5 -0
- package/dist/server.js +101 -7
- package/package.json +3 -3
package/dist/server/auth.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.isAuthEnabled = isAuthEnabled;
|
|
|
53
53
|
exports.getHttpAuthCheck = getHttpAuthCheck;
|
|
54
54
|
exports.getHttpAuthCheckAllowUrlParam = getHttpAuthCheckAllowUrlParam;
|
|
55
55
|
exports.getWebSocketAuthCheck = getWebSocketAuthCheck;
|
|
56
|
+
exports.formatServerOrigin = formatServerOrigin;
|
|
56
57
|
exports.formatAutoAuthUrl = formatAutoAuthUrl;
|
|
57
58
|
const crypto = __importStar(require("crypto"));
|
|
58
59
|
function computeAuthConfig(params) {
|
|
@@ -129,14 +130,24 @@ function getWebSocketAuthCheck(req, auth) {
|
|
|
129
130
|
? { kind: 'ok' }
|
|
130
131
|
: { kind: 'unauthorized', reason: 'invalid' };
|
|
131
132
|
}
|
|
133
|
+
function formatServerOrigin(params) {
|
|
134
|
+
const visibleHost = normalizeHostForUrl(params.host, params.scheme);
|
|
135
|
+
return `${params.scheme}://${visibleHost}:${params.port}`;
|
|
136
|
+
}
|
|
132
137
|
function formatAutoAuthUrl(params) {
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
return `${formatServerOrigin({
|
|
139
|
+
scheme: params.scheme ?? 'http',
|
|
140
|
+
host: params.host,
|
|
141
|
+
port: params.port,
|
|
142
|
+
})}/?auth=${encodeURIComponent(params.authKey)}`;
|
|
135
143
|
}
|
|
136
|
-
function normalizeHostForUrl(host) {
|
|
144
|
+
function normalizeHostForUrl(host, scheme) {
|
|
137
145
|
// Binding to 0.0.0.0/:: is common, but not a browsable "host".
|
|
138
|
-
|
|
146
|
+
// For HTTPS, preserve the certificate-matched bind host to avoid creating a hostname mismatch.
|
|
147
|
+
if (scheme === 'http' && (host === '0.0.0.0' || host === '::'))
|
|
139
148
|
return 'localhost';
|
|
149
|
+
if (host.includes(':') && !host.startsWith('['))
|
|
150
|
+
return `[${host}]`;
|
|
140
151
|
return host;
|
|
141
152
|
}
|
|
142
153
|
function generateAuthKey() {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const DEFAULT_SELF_SIGNED_CERT_DAYS = 3650;
|
|
2
|
+
export type HttpsCertificateMaterial = {
|
|
3
|
+
cert: string;
|
|
4
|
+
key: string;
|
|
5
|
+
certPath: string;
|
|
6
|
+
keyPath: string;
|
|
7
|
+
matchedHost: string;
|
|
8
|
+
matchedHostPriority: number;
|
|
9
|
+
validTo: Date;
|
|
10
|
+
};
|
|
11
|
+
export type CertificateLookupEndpoint = Readonly<{
|
|
12
|
+
bindHost: string;
|
|
13
|
+
urlHost: string;
|
|
14
|
+
}>;
|
|
15
|
+
export type CertificateLookupDiagnostic = {
|
|
16
|
+
level: 'warn';
|
|
17
|
+
code: 'invalid_cert_file' | 'missing_key_file' | 'invalid_key_pair' | 'expired_or_not_yet_valid';
|
|
18
|
+
message: string;
|
|
19
|
+
certPath: string;
|
|
20
|
+
keyPath?: string;
|
|
21
|
+
} | {
|
|
22
|
+
level: 'warn';
|
|
23
|
+
code: 'multiple_matching_certs';
|
|
24
|
+
message: string;
|
|
25
|
+
host: string;
|
|
26
|
+
certPaths: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
export type CertificateLookupResult = {
|
|
29
|
+
kind: 'found';
|
|
30
|
+
certsDirAbs: string;
|
|
31
|
+
endpoint: CertificateLookupEndpoint;
|
|
32
|
+
certificate: HttpsCertificateMaterial;
|
|
33
|
+
diagnostics: readonly CertificateLookupDiagnostic[];
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'not_found';
|
|
36
|
+
certsDirAbs: string;
|
|
37
|
+
endpoint: CertificateLookupEndpoint;
|
|
38
|
+
diagnostics: readonly CertificateLookupDiagnostic[];
|
|
39
|
+
};
|
|
40
|
+
export type CreatedSelfSignedCertificate = {
|
|
41
|
+
host: string;
|
|
42
|
+
hosts: readonly string[];
|
|
43
|
+
days: number;
|
|
44
|
+
certsDirAbs: string;
|
|
45
|
+
certPath: string;
|
|
46
|
+
keyPath: string;
|
|
47
|
+
metadataPath: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function getDefaultDomindsCertsDir(): string;
|
|
50
|
+
export declare function findAutoHttpsCertificateForHost(params: {
|
|
51
|
+
host: string;
|
|
52
|
+
certsDirAbs?: string;
|
|
53
|
+
now?: Date;
|
|
54
|
+
}): Promise<CertificateLookupResult>;
|
|
55
|
+
export declare function createSelfSignedCertificate(params: {
|
|
56
|
+
host?: string;
|
|
57
|
+
altHosts?: readonly string[];
|
|
58
|
+
days?: number;
|
|
59
|
+
certsDirAbs?: string;
|
|
60
|
+
force?: boolean;
|
|
61
|
+
}): Promise<CreatedSelfSignedCertificate>;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_SELF_SIGNED_CERT_DAYS = void 0;
|
|
37
|
+
exports.getDefaultDomindsCertsDir = getDefaultDomindsCertsDir;
|
|
38
|
+
exports.findAutoHttpsCertificateForHost = findAutoHttpsCertificateForHost;
|
|
39
|
+
exports.createSelfSignedCertificate = createSelfSignedCertificate;
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
43
|
+
const net = __importStar(require("node:net"));
|
|
44
|
+
const os = __importStar(require("node:os"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const tls = __importStar(require("node:tls"));
|
|
47
|
+
const node_util_1 = require("node:util");
|
|
48
|
+
const network_hosts_1 = require("./network-hosts");
|
|
49
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
50
|
+
const CERT_EXTENSIONS = ['.crt', '.cert', '.pem'];
|
|
51
|
+
const KEY_SUFFIXES = ['.key.pem', '.key', '-key.pem'];
|
|
52
|
+
exports.DEFAULT_SELF_SIGNED_CERT_DAYS = 3650;
|
|
53
|
+
function getDefaultDomindsCertsDir() {
|
|
54
|
+
return path.join(os.homedir(), '.dominds', 'certs');
|
|
55
|
+
}
|
|
56
|
+
async function findAutoHttpsCertificateForHost(params) {
|
|
57
|
+
const host = (0, network_hosts_1.normalizeNetworkHost)(params.host);
|
|
58
|
+
const matchHosts = await (0, network_hosts_1.resolveLanHttpsHostsForBindHost)(host);
|
|
59
|
+
const endpoint = { bindHost: host, urlHost: matchHosts[0] ?? host };
|
|
60
|
+
const certsDirAbs = params.certsDirAbs ?? getDefaultDomindsCertsDir();
|
|
61
|
+
const now = params.now ?? new Date();
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
if (matchHosts.length === 0) {
|
|
64
|
+
return { kind: 'not_found', certsDirAbs, endpoint, diagnostics };
|
|
65
|
+
}
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await fs.readdir(certsDirAbs);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
72
|
+
return { kind: 'not_found', certsDirAbs, endpoint, diagnostics };
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const matches = [];
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!isCertificateFilename(entry))
|
|
79
|
+
continue;
|
|
80
|
+
const certPath = path.join(certsDirAbs, entry);
|
|
81
|
+
const keyPath = await findKeyPathForCert({ certsDirAbs, certEntry: entry });
|
|
82
|
+
if (keyPath === null) {
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
level: 'warn',
|
|
85
|
+
code: 'missing_key_file',
|
|
86
|
+
message: `Certificate has no matching private key: ${certPath}`,
|
|
87
|
+
certPath,
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const loaded = await loadAndValidateCandidate({
|
|
92
|
+
matchHosts,
|
|
93
|
+
now,
|
|
94
|
+
certPath,
|
|
95
|
+
keyPath,
|
|
96
|
+
});
|
|
97
|
+
if (loaded.kind === 'match') {
|
|
98
|
+
matches.push(loaded.certificate);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (loaded.kind === 'diagnostic') {
|
|
102
|
+
diagnostics.push(loaded.diagnostic);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (matches.length === 0) {
|
|
106
|
+
return { kind: 'not_found', certsDirAbs, endpoint, diagnostics };
|
|
107
|
+
}
|
|
108
|
+
matches.sort((a, b) => a.matchedHostPriority - b.matchedHostPriority || b.validTo.getTime() - a.validTo.getTime());
|
|
109
|
+
if (matches.length > 1) {
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
level: 'warn',
|
|
112
|
+
code: 'multiple_matching_certs',
|
|
113
|
+
message: `Multiple valid certificates match host '${host}'; using the best-priority match with the latest expiry.`,
|
|
114
|
+
host,
|
|
115
|
+
certPaths: matches.map((m) => m.certPath),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
kind: 'found',
|
|
120
|
+
certsDirAbs,
|
|
121
|
+
endpoint: { bindHost: host, urlHost: matches[0].matchedHost },
|
|
122
|
+
certificate: matches[0],
|
|
123
|
+
diagnostics,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function createSelfSignedCertificate(params) {
|
|
127
|
+
const hosts = normalizeCertificateHostsForCreate(params.host === undefined
|
|
128
|
+
? await (0, network_hosts_1.resolveDefaultLanHttpsHosts)()
|
|
129
|
+
: [params.host, ...(params.altHosts ?? [])], { skipInvalidHosts: params.host === undefined });
|
|
130
|
+
const host = hosts[0];
|
|
131
|
+
const days = normalizeDays(params.days ?? exports.DEFAULT_SELF_SIGNED_CERT_DAYS);
|
|
132
|
+
const certsDirAbs = params.certsDirAbs ?? getDefaultDomindsCertsDir();
|
|
133
|
+
const basename = certificateBasenameForHosts(hosts);
|
|
134
|
+
const certPath = path.join(certsDirAbs, `${basename}.crt.pem`);
|
|
135
|
+
const keyPath = path.join(certsDirAbs, `${basename}.key.pem`);
|
|
136
|
+
const metadataPath = path.join(certsDirAbs, `${basename}.json`);
|
|
137
|
+
await fs.mkdir(certsDirAbs, { recursive: true, mode: 0o700 });
|
|
138
|
+
if (params.force !== true) {
|
|
139
|
+
await assertPathDoesNotExist(certPath);
|
|
140
|
+
await assertPathDoesNotExist(keyPath);
|
|
141
|
+
await assertPathDoesNotExist(metadataPath);
|
|
142
|
+
}
|
|
143
|
+
const san = buildSubjectAltName(hosts);
|
|
144
|
+
await execFileAsync('openssl', [
|
|
145
|
+
'req',
|
|
146
|
+
'-x509',
|
|
147
|
+
'-newkey',
|
|
148
|
+
'rsa:2048',
|
|
149
|
+
'-sha256',
|
|
150
|
+
'-nodes',
|
|
151
|
+
'-days',
|
|
152
|
+
String(days),
|
|
153
|
+
'-keyout',
|
|
154
|
+
keyPath,
|
|
155
|
+
'-out',
|
|
156
|
+
certPath,
|
|
157
|
+
'-subj',
|
|
158
|
+
`/CN=${escapeOpenSslSubjectValue(host)}`,
|
|
159
|
+
'-addext',
|
|
160
|
+
`subjectAltName=${san}`,
|
|
161
|
+
], { maxBuffer: 1024 * 1024 });
|
|
162
|
+
await fs.chmod(keyPath, 0o600);
|
|
163
|
+
await fs.chmod(certPath, 0o644);
|
|
164
|
+
const metadata = {
|
|
165
|
+
kind: 'dominds_self_signed_cert',
|
|
166
|
+
host,
|
|
167
|
+
hosts,
|
|
168
|
+
days,
|
|
169
|
+
certPath,
|
|
170
|
+
keyPath,
|
|
171
|
+
createdAt: new Date().toISOString(),
|
|
172
|
+
subjectAltName: san,
|
|
173
|
+
};
|
|
174
|
+
await fs.writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
175
|
+
await fs.chmod(metadataPath, 0o644);
|
|
176
|
+
return { host, hosts, days, certsDirAbs, certPath, keyPath, metadataPath };
|
|
177
|
+
}
|
|
178
|
+
function normalizeCertificateHostsForCreate(rawHosts, options) {
|
|
179
|
+
const hosts = [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const rawHost of rawHosts) {
|
|
182
|
+
const hostCandidates = getCertificateMatchHostsForCreateHost(rawHost);
|
|
183
|
+
for (const host of hostCandidates) {
|
|
184
|
+
const validation = validateCertificateHost(host);
|
|
185
|
+
if (validation.kind === 'invalid') {
|
|
186
|
+
if (options.skipInvalidHosts)
|
|
187
|
+
continue;
|
|
188
|
+
throw new Error(validation.message);
|
|
189
|
+
}
|
|
190
|
+
const key = host.toLowerCase();
|
|
191
|
+
if (seen.has(key))
|
|
192
|
+
continue;
|
|
193
|
+
seen.add(key);
|
|
194
|
+
hosts.push(host);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (hosts.length === 0) {
|
|
198
|
+
throw new Error(options.skipInvalidHosts
|
|
199
|
+
? 'No certificate-valid non-loopback LAN host was detected; provide --host explicitly'
|
|
200
|
+
: '--host must be provided at least once');
|
|
201
|
+
}
|
|
202
|
+
return hosts;
|
|
203
|
+
}
|
|
204
|
+
function getCertificateMatchHostsForCreateHost(rawHost) {
|
|
205
|
+
const host = (0, network_hosts_1.normalizeNetworkHost)(rawHost);
|
|
206
|
+
return [host];
|
|
207
|
+
}
|
|
208
|
+
function normalizeDays(days) {
|
|
209
|
+
if (!Number.isInteger(days) || days < 1 || days > 36500) {
|
|
210
|
+
throw new Error('--days must be an integer in [1, 36500]');
|
|
211
|
+
}
|
|
212
|
+
return days;
|
|
213
|
+
}
|
|
214
|
+
function validateCertificateHost(host) {
|
|
215
|
+
if (host === '') {
|
|
216
|
+
return { kind: 'invalid', message: '--host must not be empty' };
|
|
217
|
+
}
|
|
218
|
+
if (host.includes('/')) {
|
|
219
|
+
return {
|
|
220
|
+
kind: 'invalid',
|
|
221
|
+
message: '--host must be an exact DNS name or IP address; CIDR/IP ranges are not valid',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!(0, network_hosts_1.isLanHttpsHost)(host)) {
|
|
225
|
+
return {
|
|
226
|
+
kind: 'invalid',
|
|
227
|
+
message: '--host must not be localhost, loopback, 127.0.0.0/8, ::1, 0.0.0.0, or ::',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (net.isIP(host) !== 0)
|
|
231
|
+
return { kind: 'valid' };
|
|
232
|
+
if (!isValidDnsHost(host)) {
|
|
233
|
+
return {
|
|
234
|
+
kind: 'invalid',
|
|
235
|
+
message: '--host must be a DNS name, an IPv4 address, or an IPv6 address',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return { kind: 'valid' };
|
|
239
|
+
}
|
|
240
|
+
function isValidDnsHost(host) {
|
|
241
|
+
if (host.length > 253)
|
|
242
|
+
return false;
|
|
243
|
+
const trimmed = host.endsWith('.') ? host.slice(0, -1) : host;
|
|
244
|
+
if (trimmed === '')
|
|
245
|
+
return false;
|
|
246
|
+
const labels = trimmed.split('.');
|
|
247
|
+
for (const label of labels) {
|
|
248
|
+
if (label.length < 1 || label.length > 63)
|
|
249
|
+
return false;
|
|
250
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label))
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
async function assertPathDoesNotExist(filePathAbs) {
|
|
256
|
+
try {
|
|
257
|
+
await fs.access(filePathAbs);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (isNodeErrorWithCode(error, 'ENOENT'))
|
|
261
|
+
return;
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`Refusing to overwrite existing certificate file: ${filePathAbs}`);
|
|
265
|
+
}
|
|
266
|
+
function certificateBasenameForHosts(hosts) {
|
|
267
|
+
const host = hosts[0];
|
|
268
|
+
const safeHost = host
|
|
269
|
+
.toLowerCase()
|
|
270
|
+
.replace(/[^a-z0-9.-]+/g, '_')
|
|
271
|
+
.replace(/^_+|_+$/g, '');
|
|
272
|
+
const digest = (0, node_crypto_1.createHash)('sha256').update(hosts.join('\0')).digest('hex').slice(0, 8);
|
|
273
|
+
return `dominds-${safeHost || 'host'}-${digest}`;
|
|
274
|
+
}
|
|
275
|
+
function buildSubjectAltName(hosts) {
|
|
276
|
+
return hosts
|
|
277
|
+
.map((host) => {
|
|
278
|
+
const ipVersion = net.isIP(host);
|
|
279
|
+
return ipVersion !== 0 ? `IP:${host}` : `DNS:${host}`;
|
|
280
|
+
})
|
|
281
|
+
.join(',');
|
|
282
|
+
}
|
|
283
|
+
function escapeOpenSslSubjectValue(value) {
|
|
284
|
+
return value.replace(/\\/g, '\\\\').replace(/\//g, '\\/');
|
|
285
|
+
}
|
|
286
|
+
function isCertificateFilename(filename) {
|
|
287
|
+
return CERT_EXTENSIONS.some((ext) => filename.endsWith(ext)) && !isKeyFilename(filename);
|
|
288
|
+
}
|
|
289
|
+
function isKeyFilename(filename) {
|
|
290
|
+
return KEY_SUFFIXES.some((suffix) => filename.endsWith(suffix));
|
|
291
|
+
}
|
|
292
|
+
async function findKeyPathForCert(params) {
|
|
293
|
+
const baseNames = certBaseNameVariants(params.certEntry);
|
|
294
|
+
for (const baseName of baseNames) {
|
|
295
|
+
for (const suffix of KEY_SUFFIXES) {
|
|
296
|
+
const keyPath = path.join(params.certsDirAbs, `${baseName}${suffix}`);
|
|
297
|
+
try {
|
|
298
|
+
await fs.access(keyPath);
|
|
299
|
+
return keyPath;
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (isNodeErrorWithCode(error, 'ENOENT'))
|
|
303
|
+
continue;
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function certBaseNameVariants(certEntry) {
|
|
311
|
+
const variants = [];
|
|
312
|
+
for (const ext of CERT_EXTENSIONS) {
|
|
313
|
+
if (certEntry.endsWith(ext)) {
|
|
314
|
+
variants.push(certEntry.slice(0, -ext.length));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (certEntry.endsWith('.crt.pem')) {
|
|
318
|
+
variants.push(certEntry.slice(0, -'.crt.pem'.length));
|
|
319
|
+
}
|
|
320
|
+
return Array.from(new Set(variants));
|
|
321
|
+
}
|
|
322
|
+
async function loadAndValidateCandidate(params) {
|
|
323
|
+
let certPem;
|
|
324
|
+
let keyPem;
|
|
325
|
+
let cert;
|
|
326
|
+
try {
|
|
327
|
+
certPem = await fs.readFile(params.certPath, 'utf8');
|
|
328
|
+
keyPem = await fs.readFile(params.keyPath, 'utf8');
|
|
329
|
+
cert = new node_crypto_1.X509Certificate(certPem);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
return {
|
|
333
|
+
kind: 'diagnostic',
|
|
334
|
+
diagnostic: {
|
|
335
|
+
level: 'warn',
|
|
336
|
+
code: 'invalid_cert_file',
|
|
337
|
+
message: `Invalid certificate file '${params.certPath}': ${formatUnknownError(error)}`,
|
|
338
|
+
certPath: params.certPath,
|
|
339
|
+
keyPath: params.keyPath,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const matchedHost = findCertificateMatchedHost(cert, params.matchHosts);
|
|
344
|
+
if (matchedHost === null) {
|
|
345
|
+
return { kind: 'no_match' };
|
|
346
|
+
}
|
|
347
|
+
if (!certificateIsCurrentlyValid(cert, params.now)) {
|
|
348
|
+
return {
|
|
349
|
+
kind: 'diagnostic',
|
|
350
|
+
diagnostic: {
|
|
351
|
+
level: 'warn',
|
|
352
|
+
code: 'expired_or_not_yet_valid',
|
|
353
|
+
message: `Certificate is not currently valid: ${params.certPath}`,
|
|
354
|
+
certPath: params.certPath,
|
|
355
|
+
keyPath: params.keyPath,
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
tls.createSecureContext({ cert: certPem, key: keyPem });
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
kind: 'diagnostic',
|
|
365
|
+
diagnostic: {
|
|
366
|
+
level: 'warn',
|
|
367
|
+
code: 'invalid_key_pair',
|
|
368
|
+
message: `Certificate and private key do not form a valid TLS pair: ${formatUnknownError(error)}`,
|
|
369
|
+
certPath: params.certPath,
|
|
370
|
+
keyPath: params.keyPath,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
kind: 'match',
|
|
376
|
+
certificate: {
|
|
377
|
+
cert: certPem,
|
|
378
|
+
key: keyPem,
|
|
379
|
+
certPath: params.certPath,
|
|
380
|
+
keyPath: params.keyPath,
|
|
381
|
+
matchedHost: matchedHost.host,
|
|
382
|
+
matchedHostPriority: matchedHost.priority,
|
|
383
|
+
validTo: new Date(Date.parse(cert.validTo)),
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function certificateIsCurrentlyValid(cert, now) {
|
|
388
|
+
const validFromMs = Date.parse(cert.validFrom);
|
|
389
|
+
const validToMs = Date.parse(cert.validTo);
|
|
390
|
+
if (!Number.isFinite(validFromMs) || !Number.isFinite(validToMs)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const nowMs = now.getTime();
|
|
394
|
+
return validFromMs <= nowMs && nowMs <= validToMs;
|
|
395
|
+
}
|
|
396
|
+
function findCertificateMatchedHost(cert, matchHosts) {
|
|
397
|
+
for (let i = 0; i < matchHosts.length; i += 1) {
|
|
398
|
+
const host = matchHosts[i];
|
|
399
|
+
if (certificateMatchesHost(cert, host)) {
|
|
400
|
+
return { host, priority: i };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
function certificateMatchesHost(cert, host) {
|
|
406
|
+
const normalizedHost = (0, network_hosts_1.normalizeNetworkHost)(host);
|
|
407
|
+
const ipVersion = net.isIP(normalizedHost);
|
|
408
|
+
if (ipVersion !== 0) {
|
|
409
|
+
return cert.checkIP(normalizedHost) !== undefined;
|
|
410
|
+
}
|
|
411
|
+
return cert.checkHost(normalizedHost) !== undefined;
|
|
412
|
+
}
|
|
413
|
+
function isNodeErrorWithCode(error, code) {
|
|
414
|
+
return error instanceof Error && error.code === code;
|
|
415
|
+
}
|
|
416
|
+
function formatUnknownError(error) {
|
|
417
|
+
return error instanceof Error ? error.message : String(error);
|
|
418
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function normalizeNetworkHost(host: string): string;
|
|
2
|
+
export declare function isLanHttpsHost(host: string): boolean;
|
|
3
|
+
export declare function resolveHttpUrlHostForBindHost(bindHost: string): string;
|
|
4
|
+
export declare function resolveLanHttpsHostsForBindHost(bindHost: string): Promise<readonly string[]>;
|
|
5
|
+
export declare function resolveDefaultLanHttpsHosts(): Promise<readonly string[]>;
|