@stacksjs/rpx 0.11.19 → 0.11.21
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/dist/acme-challenge.d.ts +13 -0
- package/dist/auth.d.ts +21 -0
- package/dist/bin/cli.js +252 -200
- package/dist/cert-inspect.d.ts +5 -3
- package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
- package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
- package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
- package/dist/chunk-qs36t2rf.js +224 -0
- package/dist/daemon.d.ts +4 -1
- package/dist/https.d.ts +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +7 -7
- package/dist/proxy-handler.d.ts +18 -1
- package/dist/proxy-pool.d.ts +5 -0
- package/dist/redirect.d.ts +40 -0
- package/dist/registry.d.ts +2 -1
- package/dist/site-resolver.d.ts +106 -0
- package/dist/site-splash.d.ts +19 -0
- package/dist/site-supervisor.d.ts +56 -0
- package/dist/start.d.ts +31 -8
- package/dist/types.d.ts +64 -0
- package/package.json +6 -7
- package/dist/chunk-rs8gqpax.js +0 -174
- package/src/cert-inspect.ts +0 -69
- package/src/colors.ts +0 -13
- package/src/config.ts +0 -45
- package/src/daemon-runner.ts +0 -180
- package/src/daemon.ts +0 -1209
- package/src/dns-state.ts +0 -116
- package/src/dns.ts +0 -568
- package/src/host-match.ts +0 -52
- package/src/host-routes.ts +0 -147
- package/src/hosts.ts +0 -283
- package/src/https.ts +0 -905
- package/src/index.ts +0 -161
- package/src/logger.ts +0 -19
- package/src/macos-trust.ts +0 -175
- package/src/on-demand.ts +0 -264
- package/src/origin-guard.ts +0 -127
- package/src/port-manager.ts +0 -183
- package/src/process-manager.ts +0 -164
- package/src/proxy-handler.ts +0 -387
- package/src/proxy-pool.ts +0 -1003
- package/src/registry.ts +0 -366
- package/src/sni.ts +0 -93
- package/src/start.ts +0 -1421
- package/src/static-files.ts +0 -201
- package/src/types.ts +0 -267
- package/src/utils.ts +0 -243
package/src/https.ts
DELETED
|
@@ -1,905 +0,0 @@
|
|
|
1
|
-
import type { ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig, SSLConfig, TlsConfig } from './types'
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
3
|
-
import fs from 'node:fs/promises'
|
|
4
|
-
import * as os from 'node:os'
|
|
5
|
-
import { homedir } from 'node:os'
|
|
6
|
-
import * as path from 'node:path'
|
|
7
|
-
import { join } from 'node:path'
|
|
8
|
-
import * as process from 'node:process'
|
|
9
|
-
import {
|
|
10
|
-
addCertToSystemTrustStoreAndSaveCert,
|
|
11
|
-
createRootCA,
|
|
12
|
-
generateCertificate as generateCert,
|
|
13
|
-
} from '@stacksjs/tlsx'
|
|
14
|
-
import { log } from './logger'
|
|
15
|
-
import { config } from './config'
|
|
16
|
-
import {
|
|
17
|
-
MACOS_CA_TRUST_FLAGS,
|
|
18
|
-
MACOS_SYSTEM_KEYCHAIN,
|
|
19
|
-
getMacosLoginKeychainPath,
|
|
20
|
-
isRootCaFingerprintInKeychains,
|
|
21
|
-
isRootCaTrustedForSsl,
|
|
22
|
-
pruneStaleRootCas,
|
|
23
|
-
trustRootCaForBrowsers,
|
|
24
|
-
} from './macos-trust'
|
|
25
|
-
import { debugLog, execSudoSync, getPrimaryDomain, isMultiProxyConfig, isMultiProxyOptions, isSingleProxyOptions, isValidRootCA, safeDeleteFile } from './utils'
|
|
26
|
-
|
|
27
|
-
export {
|
|
28
|
-
MACOS_CA_TRUST_FLAGS,
|
|
29
|
-
MACOS_SYSTEM_KEYCHAIN,
|
|
30
|
-
RPX_ROOT_CA_COMMON_NAME,
|
|
31
|
-
getMacosLoginKeychainPath,
|
|
32
|
-
getMacosTrustKeychains,
|
|
33
|
-
isRootCaFingerprintInKeychains,
|
|
34
|
-
isRootCaTrustedForSsl,
|
|
35
|
-
listCertSha256HashesByCommonName,
|
|
36
|
-
pruneStaleRootCas,
|
|
37
|
-
trustRootCaForBrowsers,
|
|
38
|
-
} from './macos-trust'
|
|
39
|
-
|
|
40
|
-
export {
|
|
41
|
-
certIncludesSanHostnames,
|
|
42
|
-
normalizeSha256Fingerprint,
|
|
43
|
-
parseSha256HashesFromSecurityListing,
|
|
44
|
-
readCertCommonName,
|
|
45
|
-
readCertSha256Fingerprint,
|
|
46
|
-
verifyHttpsChain,
|
|
47
|
-
} from './cert-inspect'
|
|
48
|
-
|
|
49
|
-
let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null
|
|
50
|
-
|
|
51
|
-
/** Shared dev host cert path used by the rpx daemon and `./buddy dev`. */
|
|
52
|
-
export const SHARED_DEV_HOST_CERT_PATH: string = join(homedir(), '.stacks', 'ssl', 'rpx.localhost.crt')
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Bun needs one `tls[]` entry per SNI name even when a single PEM covers every SAN.
|
|
56
|
-
* Without this, :443 serves the default cert (wrong CN → ERR_CERT_COMMON_NAME_INVALID).
|
|
57
|
-
*/
|
|
58
|
-
export function devSslToSniEntries(
|
|
59
|
-
hosts: string[],
|
|
60
|
-
ssl: SSLConfig,
|
|
61
|
-
): Array<{ serverName: string, cert: string, key: string }> {
|
|
62
|
-
return [...new Set(hosts.filter(Boolean))].map(serverName => ({
|
|
63
|
-
serverName,
|
|
64
|
-
cert: ssl.cert,
|
|
65
|
-
key: ssl.key,
|
|
66
|
-
}))
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** ProxyOptions for the shared multi-app dev certificate (all registry hosts as SANs). */
|
|
70
|
-
export function buildRegistryTlsProxyOptions(
|
|
71
|
-
registryHosts: string[],
|
|
72
|
-
primary: string,
|
|
73
|
-
verbose?: boolean,
|
|
74
|
-
): ProxyOptions {
|
|
75
|
-
const sslDir = join(homedir(), '.stacks', 'ssl')
|
|
76
|
-
const hostnames = [...new Set([primary, ...registryHosts, 'rpx.localhost'])]
|
|
77
|
-
return {
|
|
78
|
-
https: {
|
|
79
|
-
certPath: SHARED_DEV_HOST_CERT_PATH,
|
|
80
|
-
keyPath: join(sslDir, 'rpx.localhost.key'),
|
|
81
|
-
caCertPath: join(sslDir, 'rpx.localhost.ca.crt'),
|
|
82
|
-
commonName: primary,
|
|
83
|
-
},
|
|
84
|
-
verbose,
|
|
85
|
-
regenerateUntrustedCerts: true,
|
|
86
|
-
proxies: hostnames.map(to => ({ from: 'localhost:1', to, cleanUrls: false })),
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Canonical filenames for the shared Root CA. The CA is a singleton across all
|
|
91
|
-
// rpx-managed domains so that browsers only need to trust it once. Host certs
|
|
92
|
-
// for individual domains are issued from this CA on demand.
|
|
93
|
-
const ROOT_CA_CERT_FILENAME = 'rpx-root-ca.crt'
|
|
94
|
-
const ROOT_CA_KEY_FILENAME = 'rpx-root-ca.key'
|
|
95
|
-
|
|
96
|
-
export interface RootCAPaths {
|
|
97
|
-
caCertPath: string
|
|
98
|
-
caKeyPath: string
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Returns the canonical Root CA cert + key paths inside `basePath`.
|
|
103
|
-
*/
|
|
104
|
-
export function getRootCAPaths(basePath: string): RootCAPaths {
|
|
105
|
-
return {
|
|
106
|
-
caCertPath: join(basePath, ROOT_CA_CERT_FILENAME),
|
|
107
|
-
caKeyPath: join(basePath, ROOT_CA_KEY_FILENAME),
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Paths for the shared multi-host daemon cert under `~/.stacks/ssl`. */
|
|
112
|
-
export function getSharedDaemonCertPaths(sslDir: string): {
|
|
113
|
-
certPath: string
|
|
114
|
-
keyPath: string
|
|
115
|
-
caCertPath: string
|
|
116
|
-
rootCA: RootCAPaths
|
|
117
|
-
} {
|
|
118
|
-
return {
|
|
119
|
-
certPath: join(sslDir, 'rpx.localhost.crt'),
|
|
120
|
-
keyPath: join(sslDir, 'rpx.localhost.key'),
|
|
121
|
-
caCertPath: join(sslDir, 'rpx.localhost.ca.crt'),
|
|
122
|
-
rootCA: getRootCAPaths(sslDir),
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Load a previously-persisted Root CA (cert + private key). Returns `null` if
|
|
128
|
-
* either file is missing or unreadable, signalling that a fresh CA needs to be
|
|
129
|
-
* created.
|
|
130
|
-
*/
|
|
131
|
-
async function loadRootCA(paths: RootCAPaths, verbose?: boolean): Promise<{ certificate: string, privateKey: string } | null> {
|
|
132
|
-
try {
|
|
133
|
-
const [certificate, privateKey] = await Promise.all([
|
|
134
|
-
fs.readFile(paths.caCertPath, 'utf8'),
|
|
135
|
-
fs.readFile(paths.caKeyPath, 'utf8'),
|
|
136
|
-
])
|
|
137
|
-
if (!certificate.includes('-----BEGIN CERTIFICATE-----') || !privateKey.includes('PRIVATE KEY-----')) {
|
|
138
|
-
debugLog('ssl', `Root CA files at ${paths.caCertPath} look malformed, will regenerate`, verbose)
|
|
139
|
-
return null
|
|
140
|
-
}
|
|
141
|
-
return { certificate, privateKey }
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
debugLog('ssl', `No existing Root CA at ${paths.caCertPath} (${(err as NodeJS.ErrnoException).code || err}), will create one`, verbose)
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Resolves SSL paths based on configuration
|
|
151
|
-
*/
|
|
152
|
-
export function resolveSSLPaths(options: ProxyConfigs, defaultConfig: typeof config): TlsConfig {
|
|
153
|
-
const domain = isMultiProxyConfig(options)
|
|
154
|
-
? options.proxies[0].to || 'rpx.localhost'
|
|
155
|
-
: options.to || 'rpx.localhost'
|
|
156
|
-
|
|
157
|
-
// If HTTPS is an object and has explicit paths defined, use those
|
|
158
|
-
if (typeof options.https === 'object' && typeof defaultConfig.https === 'object') {
|
|
159
|
-
const hasAllPaths = options.https.caCertPath && options.https.certPath && options.https.keyPath
|
|
160
|
-
if (hasAllPaths) {
|
|
161
|
-
// Create base TLS config
|
|
162
|
-
const baseConfig = httpsConfig({
|
|
163
|
-
...options,
|
|
164
|
-
to: domain,
|
|
165
|
-
https: defaultConfig.https,
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Filter out undefined values from arrays
|
|
169
|
-
const altNameIPs = options.https.altNameIPs?.filter((ip: string | undefined): ip is string => ip !== undefined) || baseConfig.altNameIPs
|
|
170
|
-
const altNameURIs = options.https.altNameURIs?.filter((uri: string | undefined): uri is string => uri !== undefined) || baseConfig.altNameURIs
|
|
171
|
-
|
|
172
|
-
// Override with provided paths
|
|
173
|
-
return {
|
|
174
|
-
...baseConfig,
|
|
175
|
-
caCertPath: options.https.caCertPath || baseConfig.caCertPath,
|
|
176
|
-
certPath: options.https.certPath || baseConfig.certPath,
|
|
177
|
-
keyPath: options.https.keyPath || baseConfig.keyPath,
|
|
178
|
-
basePath: options.https.basePath || baseConfig.basePath,
|
|
179
|
-
commonName: options.https.commonName || baseConfig.commonName,
|
|
180
|
-
organizationName: options.https.organizationName || baseConfig.organizationName,
|
|
181
|
-
countryName: options.https.countryName || baseConfig.countryName,
|
|
182
|
-
stateName: options.https.stateName || baseConfig.stateName,
|
|
183
|
-
localityName: options.https.localityName || baseConfig.localityName,
|
|
184
|
-
validityDays: options.https.validityDays || baseConfig.validityDays,
|
|
185
|
-
altNameIPs,
|
|
186
|
-
altNameURIs,
|
|
187
|
-
verbose: options.verbose || baseConfig.verbose,
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Otherwise, generate paths based on the domain
|
|
193
|
-
return httpsConfig({
|
|
194
|
-
...options,
|
|
195
|
-
to: domain,
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Generate wildcard patterns for a domain
|
|
200
|
-
export function generateWildcardPatterns(domain: string): string[] {
|
|
201
|
-
const patterns = new Set<string>()
|
|
202
|
-
patterns.add(domain)
|
|
203
|
-
|
|
204
|
-
const parts = domain.split('.')
|
|
205
|
-
if (parts.length >= 2)
|
|
206
|
-
patterns.add(`*.${parts.slice(1).join('.')}`)
|
|
207
|
-
|
|
208
|
-
return Array.from(patterns)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Generates SSL file paths based on domain
|
|
213
|
-
*/
|
|
214
|
-
export function generateSSLPaths(options?: ProxyOptions): {
|
|
215
|
-
caCertPath: string
|
|
216
|
-
certPath: string
|
|
217
|
-
keyPath: string
|
|
218
|
-
} {
|
|
219
|
-
const domain = getPrimaryDomain(options)
|
|
220
|
-
const sanitizedDomain = domain.replace(/\*/g, 'wildcard')
|
|
221
|
-
|
|
222
|
-
// Get basePath from options or use default
|
|
223
|
-
const defaultBasePath = join(homedir(), '.stacks', 'ssl')
|
|
224
|
-
let basePath = defaultBasePath
|
|
225
|
-
|
|
226
|
-
if (typeof options?.https === 'object') {
|
|
227
|
-
// Only use options.https.basePath if it's a non-empty string
|
|
228
|
-
basePath = options.https.basePath && options.https.basePath.trim() !== ''
|
|
229
|
-
? options.https.basePath
|
|
230
|
-
: defaultBasePath
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
caCertPath: options.https.caCertPath || join(basePath, `${sanitizedDomain}.ca.crt`),
|
|
234
|
-
certPath: options.https.certPath || join(basePath, `${sanitizedDomain}.crt`),
|
|
235
|
-
keyPath: options.https.keyPath || join(basePath, `${sanitizedDomain}.key`),
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
caCertPath: join(basePath, `${sanitizedDomain}.ca.crt`),
|
|
241
|
-
certPath: join(basePath, `${sanitizedDomain}.crt`),
|
|
242
|
-
keyPath: join(basePath, `${sanitizedDomain}.key`),
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function getAllDomains(options: ProxyOption | ProxyOptions): Set<string> {
|
|
247
|
-
const domains = new Set<string>()
|
|
248
|
-
|
|
249
|
-
if (isMultiProxyOptions(options)) {
|
|
250
|
-
options.proxies.forEach((proxy) => {
|
|
251
|
-
const domain = proxy.to || 'rpx.localhost'
|
|
252
|
-
generateWildcardPatterns(domain).forEach(pattern => domains.add(pattern))
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
else if (isSingleProxyOptions(options)) {
|
|
256
|
-
const domain = options.to || 'rpx.localhost'
|
|
257
|
-
generateWildcardPatterns(domain).forEach(pattern => domains.add(pattern))
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
domains.add('rpx.localhost')
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Add localhost patterns
|
|
264
|
-
domains.add('localhost')
|
|
265
|
-
domains.add('*.localhost')
|
|
266
|
-
|
|
267
|
-
return domains
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Load SSL certificates from files or use provided strings
|
|
272
|
-
*/
|
|
273
|
-
export async function loadSSLConfig(options: ProxyOption): Promise<SSLConfig | null> {
|
|
274
|
-
debugLog('ssl', `Loading SSL configuration`, options.verbose)
|
|
275
|
-
|
|
276
|
-
const mergedOptions = {
|
|
277
|
-
...config,
|
|
278
|
-
...options,
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
options.https = httpsConfig(mergedOptions)
|
|
282
|
-
|
|
283
|
-
// Early return for non-SSL configuration
|
|
284
|
-
if (!options.https?.keyPath && !options.https?.certPath) {
|
|
285
|
-
debugLog('ssl', 'No SSL configuration provided', options.verbose)
|
|
286
|
-
return null
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if ((options.https?.keyPath && !options.https?.certPath) || (!options.https?.keyPath && options.https?.certPath)) {
|
|
290
|
-
const missing = !options.https?.keyPath ? 'keyPath' : 'certPath'
|
|
291
|
-
debugLog('ssl', `Invalid SSL configuration - missing ${missing}`, options.verbose)
|
|
292
|
-
throw new Error(`SSL Configuration requires both keyPath and certPath. Missing: ${missing}`)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
if (!options.https?.keyPath || !options.https?.certPath)
|
|
297
|
-
return null
|
|
298
|
-
|
|
299
|
-
// Try to read existing certificates
|
|
300
|
-
try {
|
|
301
|
-
debugLog('ssl', 'Reading SSL certificate files', options.verbose)
|
|
302
|
-
const key = await fs.readFile(options.https?.keyPath, 'utf8')
|
|
303
|
-
const cert = await fs.readFile(options.https?.certPath, 'utf8')
|
|
304
|
-
|
|
305
|
-
debugLog('ssl', 'SSL configuration loaded successfully', options.verbose)
|
|
306
|
-
return { key, cert }
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
debugLog('ssl', `Failed to read certificates: ${error}`, options.verbose)
|
|
310
|
-
return null
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
debugLog('ssl', `SSL configuration error: ${err}`, options.verbose)
|
|
315
|
-
throw err
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Force trust a certificate - exposing for direct use
|
|
321
|
-
*/
|
|
322
|
-
export async function forceTrustCertificate(
|
|
323
|
-
certPath: string,
|
|
324
|
-
options?: { serverName?: string, verbose?: boolean },
|
|
325
|
-
): Promise<boolean> {
|
|
326
|
-
if (process.platform === 'darwin')
|
|
327
|
-
return forceTrustCertificateMacOS(certPath, options)
|
|
328
|
-
|
|
329
|
-
if (process.platform === 'linux') {
|
|
330
|
-
try {
|
|
331
|
-
const { exec } = await import('node:child_process')
|
|
332
|
-
return await new Promise((resolve) => {
|
|
333
|
-
// Try Ubuntu/Debian way
|
|
334
|
-
exec(
|
|
335
|
-
`sudo cp "${certPath}" /usr/local/share/ca-certificates/ && sudo update-ca-certificates`,
|
|
336
|
-
(error) => {
|
|
337
|
-
if (!error) {
|
|
338
|
-
resolve(true)
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
// Try Fedora/RHEL way
|
|
342
|
-
exec(
|
|
343
|
-
`sudo cp "${certPath}" /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust extract`,
|
|
344
|
-
(err2) => {
|
|
345
|
-
resolve(!err2)
|
|
346
|
-
},
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
)
|
|
351
|
-
})
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return false
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return false
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Force trust a certificate on macOS using direct security command
|
|
363
|
-
* This function first checks if the certificate is already trusted to avoid unnecessary sudo prompts
|
|
364
|
-
*/
|
|
365
|
-
async function forceTrustCertificateMacOS(
|
|
366
|
-
certPath: string,
|
|
367
|
-
options?: { serverName?: string, verbose?: boolean },
|
|
368
|
-
): Promise<boolean> {
|
|
369
|
-
if (process.platform !== 'darwin')
|
|
370
|
-
return false
|
|
371
|
-
|
|
372
|
-
const serverName = options?.serverName ?? 'rpx.localhost'
|
|
373
|
-
const verbose = options?.verbose ?? false
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
if (isRootCaTrustedForSsl(certPath, serverName, { verbose })) {
|
|
377
|
-
debugLog('ssl', 'Root CA already trusted for SSL, skipping trust operation', verbose)
|
|
378
|
-
return true
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
debugLog('ssl', 'Trusting Root CA for browsers (login + system keychains)', verbose)
|
|
382
|
-
return trustRootCaForBrowsers(certPath, { serverName, verbose })
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
return false
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export async function generateCertificate(options: ProxyOptions): Promise<void> {
|
|
390
|
-
if (cachedSSLConfig && !(options as { forceRegenerate?: boolean }).forceRegenerate) {
|
|
391
|
-
debugLog('ssl', 'Using cached SSL configuration', options.verbose)
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
|
-
clearSslConfigCache()
|
|
395
|
-
|
|
396
|
-
// Get all unique domains from the configuration
|
|
397
|
-
const domains: string[] = isMultiProxyOptions(options)
|
|
398
|
-
? options.proxies.map(proxy => proxy.to)
|
|
399
|
-
: [(options as SingleProxyConfig).to]
|
|
400
|
-
|
|
401
|
-
debugLog('ssl', `Generating certificate for domains: ${domains.join(', ')}`, options.verbose)
|
|
402
|
-
|
|
403
|
-
const hostConfig = httpsConfig(options, options.verbose)
|
|
404
|
-
const sslDir = hostConfig.basePath || join(homedir(), '.stacks', 'ssl')
|
|
405
|
-
await fs.mkdir(sslDir, { recursive: true })
|
|
406
|
-
const rootCAPaths = getRootCAPaths(sslDir)
|
|
407
|
-
|
|
408
|
-
// Reuse the persisted Root CA when present so the user only has to trust it
|
|
409
|
-
// once. A fresh CA gets created (and persisted) on first run.
|
|
410
|
-
let caCert = await loadRootCA(rootCAPaths, options.verbose)
|
|
411
|
-
let caIsNew = false
|
|
412
|
-
if (!caCert) {
|
|
413
|
-
if (options.verbose)
|
|
414
|
-
log.info('Generating Root CA certificate (one-time)...')
|
|
415
|
-
caCert = await createRootCA(hostConfig)
|
|
416
|
-
try {
|
|
417
|
-
await Promise.all([
|
|
418
|
-
fs.writeFile(rootCAPaths.caCertPath, caCert.certificate),
|
|
419
|
-
fs.writeFile(rootCAPaths.caKeyPath, caCert.privateKey, { mode: 0o600 }),
|
|
420
|
-
])
|
|
421
|
-
caIsNew = true
|
|
422
|
-
debugLog('ssl', `Persisted Root CA at ${rootCAPaths.caCertPath}`, options.verbose)
|
|
423
|
-
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
debugLog('ssl', `Error saving Root CA files: ${err}`, options.verbose)
|
|
426
|
-
throw new Error(`Failed to save Root CA files: ${err}`)
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
debugLog('ssl', `Reusing existing Root CA from ${rootCAPaths.caCertPath}`, options.verbose)
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Issue the host cert with all SANs from the (possibly reused) CA.
|
|
434
|
-
if (options.verbose)
|
|
435
|
-
log.info(`Generating host certificate for: ${domains.join(', ')}`)
|
|
436
|
-
|
|
437
|
-
const hostCert = await generateCert({
|
|
438
|
-
...hostConfig,
|
|
439
|
-
rootCA: {
|
|
440
|
-
certificate: caCert.certificate,
|
|
441
|
-
privateKey: caCert.privateKey,
|
|
442
|
-
},
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
// Persist host cert + key. Also write a copy of the CA cert at the per-domain
|
|
446
|
-
// `caCertPath` for back-compat with anything that still reads from there.
|
|
447
|
-
try {
|
|
448
|
-
await Promise.all([
|
|
449
|
-
fs.writeFile(hostConfig.certPath, hostCert.certificate),
|
|
450
|
-
fs.writeFile(hostConfig.keyPath, hostCert.privateKey),
|
|
451
|
-
fs.writeFile(hostConfig.caCertPath, caCert.certificate),
|
|
452
|
-
])
|
|
453
|
-
|
|
454
|
-
debugLog('ssl', 'Certificate files saved successfully', options.verbose)
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
debugLog('ssl', `Error saving certificate files: ${err}`, options.verbose)
|
|
458
|
-
throw new Error(`Failed to save certificate files: ${err}`)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// The CA is the trust anchor — only it needs to live in the trust store.
|
|
462
|
-
// Skip the install step when the CA is already trusted (a freshly minted CA
|
|
463
|
-
// is never trusted yet; a reused one might still need a one-time install if
|
|
464
|
-
// someone wiped their keychain).
|
|
465
|
-
const caTrusted = caIsNew
|
|
466
|
-
? false
|
|
467
|
-
: await isCertTrusted(rootCAPaths.caCertPath, { verbose: options.verbose, regenerateUntrustedCerts: true })
|
|
468
|
-
|
|
469
|
-
if (caTrusted) {
|
|
470
|
-
debugLog('ssl', 'Root CA already trusted, skipping trust store update', options.verbose)
|
|
471
|
-
if (options.verbose)
|
|
472
|
-
log.success('Root CA is already trusted in system trust store')
|
|
473
|
-
|
|
474
|
-
cachedSSLConfig = {
|
|
475
|
-
key: hostCert.privateKey,
|
|
476
|
-
cert: hostCert.certificate,
|
|
477
|
-
ca: caCert.certificate,
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (options.verbose)
|
|
481
|
-
log.success(`Certificate generated successfully for ${domains.length} domain${domains.length > 1 ? 's' : ''}`)
|
|
482
|
-
return
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Now add to system trust store with a single operation
|
|
486
|
-
// This will require only one sudo password prompt
|
|
487
|
-
if (options.verbose)
|
|
488
|
-
log.info('Adding certificate to system trust store (may require sudo permission)...')
|
|
489
|
-
|
|
490
|
-
let isTrusted = false
|
|
491
|
-
|
|
492
|
-
// We install only the Root CA — host certs derive their trust from it.
|
|
493
|
-
if (process.platform === 'darwin') {
|
|
494
|
-
try {
|
|
495
|
-
pruneStaleRootCas({ caPath: rootCAPaths.caCertPath, verbose: options.verbose })
|
|
496
|
-
const loginKeychain = getMacosLoginKeychainPath()
|
|
497
|
-
try {
|
|
498
|
-
execSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${loginKeychain}" "${rootCAPaths.caCertPath}"`, { stdio: 'ignore' })
|
|
499
|
-
}
|
|
500
|
-
catch { /* login keychain optional */ }
|
|
501
|
-
execSudoSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"`)
|
|
502
|
-
if (options.verbose)
|
|
503
|
-
log.success('Successfully added Root CA to system trust store')
|
|
504
|
-
|
|
505
|
-
isTrusted = true
|
|
506
|
-
|
|
507
|
-
// Helper script for manual re-trust if the user ever wipes their keychain.
|
|
508
|
-
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
509
|
-
const scriptContent = `#!/bin/bash
|
|
510
|
-
echo "Trusting RPX Root CA"
|
|
511
|
-
sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
|
|
512
|
-
echo "Root CA trusted! Please restart your browser."
|
|
513
|
-
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
514
|
-
`
|
|
515
|
-
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 }) // Make it executable
|
|
516
|
-
}
|
|
517
|
-
catch (err) {
|
|
518
|
-
if (options.verbose)
|
|
519
|
-
log.warn(`Could not add Root CA to trust store automatically: ${err}`)
|
|
520
|
-
|
|
521
|
-
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
522
|
-
const scriptContent = `#!/bin/bash
|
|
523
|
-
echo "Trusting RPX Root CA"
|
|
524
|
-
sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
|
|
525
|
-
echo "Root CA trusted! Please restart your browser."
|
|
526
|
-
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
527
|
-
`
|
|
528
|
-
await fs.writeFile(scriptPath, scriptContent, { mode: 0o755 })
|
|
529
|
-
if (options.verbose) {
|
|
530
|
-
log.info(`Created a trust helper script at: ${scriptPath}`)
|
|
531
|
-
log.info(`If you're still having certificate issues, run: sh ${scriptPath}`)
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
else if (process.platform === 'linux') {
|
|
536
|
-
try {
|
|
537
|
-
// On Linux, we need to copy to the trusted certificates directory
|
|
538
|
-
const { exec } = await import('node:child_process')
|
|
539
|
-
const certDir = '/usr/local/share/ca-certificates/rpx'
|
|
540
|
-
|
|
541
|
-
// Create a more reliable trust script
|
|
542
|
-
const trustScript = `
|
|
543
|
-
mkdir -p "${certDir}" 2>/dev/null || true
|
|
544
|
-
cp "${rootCAPaths.caCertPath}" "${certDir}/"
|
|
545
|
-
update-ca-certificates
|
|
546
|
-
echo "RPX Root CA installed. Please restart your browser."
|
|
547
|
-
`
|
|
548
|
-
// Use a temp file for the script
|
|
549
|
-
const tmpScript = join(os.tmpdir(), `rpx-trust-${Date.now()}.sh`)
|
|
550
|
-
await fs.writeFile(tmpScript, trustScript, { mode: 0o755 })
|
|
551
|
-
|
|
552
|
-
// Run with one sudo prompt
|
|
553
|
-
await new Promise((resolve) => {
|
|
554
|
-
exec(`sudo bash "${tmpScript}"`, (error) => {
|
|
555
|
-
if (error) {
|
|
556
|
-
if (options.verbose)
|
|
557
|
-
log.warn(`Could not trust certificates: ${error}`)
|
|
558
|
-
resolve(false)
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
if (options.verbose)
|
|
562
|
-
log.success('Successfully added certificates to system trust store')
|
|
563
|
-
resolve(true)
|
|
564
|
-
}
|
|
565
|
-
})
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
// Clean up
|
|
569
|
-
await fs.unlink(tmpScript).catch(() => {})
|
|
570
|
-
|
|
571
|
-
isTrusted = true
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
if (options.verbose)
|
|
575
|
-
log.warn(`Failed to trust certificates: ${error}`)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
else if (process.platform === 'win32') {
|
|
579
|
-
// Windows certificate trust
|
|
580
|
-
try {
|
|
581
|
-
// Windows is different - use a PowerShell approach
|
|
582
|
-
const winScript = `
|
|
583
|
-
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("${rootCAPaths.caCertPath.replace(/\//g, '\\')}")
|
|
584
|
-
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store("ROOT", "LocalMachine")
|
|
585
|
-
$store.Open("ReadWrite")
|
|
586
|
-
$store.Add($cert)
|
|
587
|
-
$store.Close()
|
|
588
|
-
Write-Host "Root CA trusted successfully!"
|
|
589
|
-
`
|
|
590
|
-
const psPath = join(os.tmpdir(), 'rpx-trust.ps1')
|
|
591
|
-
await fs.writeFile(psPath, winScript)
|
|
592
|
-
|
|
593
|
-
execSync(`powershell -ExecutionPolicy Bypass -File "${psPath}"`)
|
|
594
|
-
if (options.verbose)
|
|
595
|
-
log.success('Successfully added certificate to Windows trust store')
|
|
596
|
-
isTrusted = true
|
|
597
|
-
}
|
|
598
|
-
catch (error) {
|
|
599
|
-
if (options.verbose)
|
|
600
|
-
log.warn(`Could not trust certificate: ${error}`)
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
// Use the built-in trust mechanism for other platforms
|
|
605
|
-
try {
|
|
606
|
-
await addCertToSystemTrustStoreAndSaveCert(hostCert, caCert.certificate, hostConfig)
|
|
607
|
-
// Assume this worked for now
|
|
608
|
-
isTrusted = true
|
|
609
|
-
}
|
|
610
|
-
catch (err) {
|
|
611
|
-
if (options.verbose)
|
|
612
|
-
log.warn(`Could not add certificate to trust store: ${err}`)
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Cache the SSL config for reuse
|
|
617
|
-
cachedSSLConfig = {
|
|
618
|
-
key: hostCert.privateKey,
|
|
619
|
-
cert: hostCert.certificate,
|
|
620
|
-
ca: caCert.certificate,
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (options.verbose)
|
|
624
|
-
log.success(`Certificate generated successfully for ${domains.length} domain${domains.length > 1 ? 's' : ''}`)
|
|
625
|
-
|
|
626
|
-
// Show Chrome bypass tip if trust might have issues
|
|
627
|
-
if (!isTrusted && options.verbose) {
|
|
628
|
-
log.warn('If you see certificate warnings in Chrome/Arc, type "thisisunsafe" on the warning page')
|
|
629
|
-
log.warn('This will bypass the warning and you should only need to do it once')
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export function getSSLConfig(): { key: string, cert: string, ca?: string } | null {
|
|
634
|
-
return cachedSSLConfig
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/** Clear in-process TLS cache so the next generate/load picks up new files on disk. */
|
|
638
|
-
export function clearSslConfigCache(): void {
|
|
639
|
-
cachedSSLConfig = null
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// needs to accept the options
|
|
643
|
-
export async function checkExistingCertificates(options?: ProxyOptions): Promise<SSLConfig | null> {
|
|
644
|
-
if (!options)
|
|
645
|
-
return null
|
|
646
|
-
|
|
647
|
-
if (cachedSSLConfig)
|
|
648
|
-
return cachedSSLConfig
|
|
649
|
-
|
|
650
|
-
// Use httpsConfig to get the path configuration
|
|
651
|
-
const sslConfig = httpsConfig(options)
|
|
652
|
-
|
|
653
|
-
try {
|
|
654
|
-
// Check if certificate files exist
|
|
655
|
-
const [keyExists, certExists, caExists] = await Promise.all([
|
|
656
|
-
fs.access(sslConfig.keyPath).then(() => true).catch(() => false),
|
|
657
|
-
fs.access(sslConfig.certPath).then(() => true).catch(() => false),
|
|
658
|
-
sslConfig.caCertPath ? fs.access(sslConfig.caCertPath).then(() => true).catch(() => false) : Promise.resolve(false),
|
|
659
|
-
])
|
|
660
|
-
|
|
661
|
-
if (!keyExists || !certExists) {
|
|
662
|
-
debugLog('ssl', `Certificate files don't exist: key=${keyExists}, cert=${certExists}, paths: ${sslConfig.keyPath}, ${sslConfig.certPath}`, options.verbose)
|
|
663
|
-
return null
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Check if certificate is trusted
|
|
667
|
-
// But only if regenerateUntrustedCerts is enabled (default is true)
|
|
668
|
-
const hasFlag = 'regenerateUntrustedCerts' in options
|
|
669
|
-
const flagValue = options.regenerateUntrustedCerts
|
|
670
|
-
const shouldCheckTrust = hasFlag ? flagValue !== false : true
|
|
671
|
-
debugLog('ssl', `Trust check: hasFlag=${hasFlag}, flagValue=${flagValue}, shouldCheckTrust=${shouldCheckTrust}`, options.verbose)
|
|
672
|
-
// Trust anchor is the shared Root CA — host certs are not installed individually.
|
|
673
|
-
const sslDir = sslConfig.basePath || join(homedir(), '.stacks', 'ssl')
|
|
674
|
-
const rootCAPaths = getRootCAPaths(sslDir)
|
|
675
|
-
const caIsTrusted = shouldCheckTrust
|
|
676
|
-
? await isCertTrusted(rootCAPaths.caCertPath, options)
|
|
677
|
-
: true
|
|
678
|
-
|
|
679
|
-
if (!caIsTrusted) {
|
|
680
|
-
debugLog('ssl', 'Root CA exists but is not trusted, will regenerate', options.verbose)
|
|
681
|
-
// Don't attempt to trust here - let generateCertificate handle it in one place
|
|
682
|
-
// This avoids multiple sudo prompts
|
|
683
|
-
return null
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Load the certificates
|
|
687
|
-
const [key, cert, ca] = await Promise.all([
|
|
688
|
-
fs.readFile(sslConfig.keyPath, 'utf8'),
|
|
689
|
-
fs.readFile(sslConfig.certPath, 'utf8'),
|
|
690
|
-
caExists && sslConfig.caCertPath ? fs.readFile(sslConfig.caCertPath, 'utf8') : Promise.resolve(undefined),
|
|
691
|
-
])
|
|
692
|
-
|
|
693
|
-
// Validate root CA PEM content if present
|
|
694
|
-
if (ca && !ca.includes('-----BEGIN CERTIFICATE-----')) {
|
|
695
|
-
debugLog('ssl', 'Invalid root CA certificate content, will regenerate', options.verbose)
|
|
696
|
-
return null
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// For multi-proxy configs, verify the cert covers ALL requested domains
|
|
700
|
-
if (isMultiProxyOptions(options)) {
|
|
701
|
-
try {
|
|
702
|
-
const { X509Certificate } = await import('node:crypto')
|
|
703
|
-
const x509 = new X509Certificate(cert)
|
|
704
|
-
const san = x509.subjectAltName || ''
|
|
705
|
-
const requiredDomains = options.proxies.map(p => p.to)
|
|
706
|
-
const missingDomains = requiredDomains.filter(d => !san.includes(`DNS:${d}`))
|
|
707
|
-
if (missingDomains.length > 0) {
|
|
708
|
-
debugLog('ssl', `Certificate missing SANs for: ${missingDomains.join(', ')}, will regenerate`, options.verbose)
|
|
709
|
-
return null
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch (err) {
|
|
713
|
-
debugLog('ssl', `Could not verify cert SANs: ${err}`, options.verbose)
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
debugLog('ssl', 'Successfully loaded existing certificates', options.verbose)
|
|
718
|
-
|
|
719
|
-
// Cache the result
|
|
720
|
-
cachedSSLConfig = { key, cert, ca }
|
|
721
|
-
return cachedSSLConfig
|
|
722
|
-
}
|
|
723
|
-
catch (err) {
|
|
724
|
-
debugLog('ssl', `Error checking existing certificates: ${err}`, options.verbose)
|
|
725
|
-
return null
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
export function httpsConfig(options: ProxyOption | ProxyOptions, verbose?: boolean): TlsConfig {
|
|
730
|
-
const primaryDomain = getPrimaryDomain(options)
|
|
731
|
-
debugLog('ssl', `Primary domain: ${primaryDomain}`, verbose)
|
|
732
|
-
|
|
733
|
-
// Generate paths based on domain if not explicitly provided
|
|
734
|
-
const defaultPaths = generateSSLPaths(options)
|
|
735
|
-
const defaultBasePath = join(homedir(), '.stacks', 'ssl')
|
|
736
|
-
|
|
737
|
-
// If HTTPS paths are explicitly provided, use those
|
|
738
|
-
if (typeof options.https === 'object') {
|
|
739
|
-
// Use provided basePath if non-empty, otherwise use default
|
|
740
|
-
const basePath = options.https.basePath && options.https.basePath.trim() !== ''
|
|
741
|
-
? options.https.basePath
|
|
742
|
-
: defaultBasePath
|
|
743
|
-
|
|
744
|
-
const config: TlsConfig = {
|
|
745
|
-
domain: primaryDomain,
|
|
746
|
-
hostCertCN: primaryDomain,
|
|
747
|
-
basePath,
|
|
748
|
-
caCertPath: options.https.caCertPath || defaultPaths.caCertPath,
|
|
749
|
-
certPath: options.https.certPath || defaultPaths.certPath,
|
|
750
|
-
keyPath: options.https.keyPath || defaultPaths.keyPath,
|
|
751
|
-
altNameIPs: ['127.0.0.1', '::1'],
|
|
752
|
-
altNameURIs: [],
|
|
753
|
-
commonName: options.https.commonName || primaryDomain,
|
|
754
|
-
organizationName: options.https.organizationName || 'Local Development',
|
|
755
|
-
countryName: options.https.countryName || 'US',
|
|
756
|
-
stateName: options.https.stateName || 'California',
|
|
757
|
-
localityName: options.https.localityName || 'Playa Vista',
|
|
758
|
-
validityDays: options.https.validityDays || 825,
|
|
759
|
-
verbose: verbose || false,
|
|
760
|
-
subjectAltNames: Array.from(getAllDomains(options)).map(domain => ({
|
|
761
|
-
type: 2,
|
|
762
|
-
value: domain,
|
|
763
|
-
})),
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Add optional properties if they exist and are valid
|
|
767
|
-
if (isValidRootCA(options.https.rootCA)) {
|
|
768
|
-
config.rootCA = options.https.rootCA
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
return config
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Return default configuration
|
|
775
|
-
return {
|
|
776
|
-
domain: primaryDomain,
|
|
777
|
-
hostCertCN: primaryDomain,
|
|
778
|
-
basePath: defaultBasePath,
|
|
779
|
-
...defaultPaths,
|
|
780
|
-
altNameIPs: ['127.0.0.1', '::1'],
|
|
781
|
-
altNameURIs: [],
|
|
782
|
-
commonName: primaryDomain,
|
|
783
|
-
organizationName: 'Local Development',
|
|
784
|
-
countryName: 'US',
|
|
785
|
-
stateName: 'California',
|
|
786
|
-
localityName: 'Playa Vista',
|
|
787
|
-
validityDays: 825,
|
|
788
|
-
verbose: verbose || false,
|
|
789
|
-
subjectAltNames: Array.from(getAllDomains(options)).map(domain => ({
|
|
790
|
-
type: 2,
|
|
791
|
-
value: domain,
|
|
792
|
-
})),
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Clean up SSL certificates for a specific domain
|
|
798
|
-
*/
|
|
799
|
-
export async function cleanupCertificates(domain: string, verbose?: boolean): Promise<void> {
|
|
800
|
-
const certPaths = generateSSLPaths({ to: domain, verbose })
|
|
801
|
-
|
|
802
|
-
// Define all possible certificate files
|
|
803
|
-
const filesToDelete = [
|
|
804
|
-
certPaths.caCertPath,
|
|
805
|
-
certPaths.certPath,
|
|
806
|
-
certPaths.keyPath,
|
|
807
|
-
]
|
|
808
|
-
|
|
809
|
-
debugLog('certificates', `Attempting to clean up relating certificates`, verbose)
|
|
810
|
-
|
|
811
|
-
// Delete all files concurrently
|
|
812
|
-
await Promise.all(filesToDelete.map(file => safeDeleteFile(file, verbose)))
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Checks if a certificate is trusted by the system (macOS only for now)
|
|
817
|
-
* If options.regenerateUntrustedCerts is false, always returns true (skips trust check)
|
|
818
|
-
*/
|
|
819
|
-
export async function isCertTrusted(
|
|
820
|
-
certPath: string,
|
|
821
|
-
options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean, serverName?: string },
|
|
822
|
-
): Promise<boolean> {
|
|
823
|
-
try {
|
|
824
|
-
debugLog('ssl', `Checking if certificate is trusted: ${certPath}`, options?.verbose)
|
|
825
|
-
|
|
826
|
-
// Different check methods per platform
|
|
827
|
-
if (process.platform === 'darwin') {
|
|
828
|
-
if (options?.serverName)
|
|
829
|
-
return isRootCaTrustedForSsl(certPath, options.serverName, { verbose: options?.verbose })
|
|
830
|
-
return isRootCaFingerprintInKeychains(certPath, { verbose: options?.verbose })
|
|
831
|
-
}
|
|
832
|
-
else if (process.platform === 'win32') {
|
|
833
|
-
// On Windows, use PowerShell to check the certificate store
|
|
834
|
-
try {
|
|
835
|
-
// Get certificate subject from file
|
|
836
|
-
const certSubject = execSync(`openssl x509 -noout -subject -in "${certPath}"`).toString().trim()
|
|
837
|
-
const subjectName = certSubject.split('=').slice(1).join('=').trim() || ''
|
|
838
|
-
|
|
839
|
-
if (!subjectName) {
|
|
840
|
-
debugLog('ssl', 'Could not extract certificate subject', options?.verbose)
|
|
841
|
-
return false
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Check if the certificate exists in the trusted root store
|
|
845
|
-
const powershellCmd = `powershell -Command "Get-ChildItem -Path Cert:\\LocalMachine\\Root | Where-Object { $_.Subject -like '*${subjectName}*' } | Select-Object Subject"`
|
|
846
|
-
const storeOutput = execSync(powershellCmd).toString()
|
|
847
|
-
|
|
848
|
-
if (storeOutput.includes(subjectName)) {
|
|
849
|
-
debugLog('ssl', 'Certificate found in trusted root store', options?.verbose)
|
|
850
|
-
return true
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
debugLog('ssl', 'Certificate not found in trusted root store', options?.verbose)
|
|
854
|
-
return false
|
|
855
|
-
}
|
|
856
|
-
catch (error) {
|
|
857
|
-
debugLog('ssl', `Error checking certificate trust on Windows: ${error}`, options?.verbose)
|
|
858
|
-
return false
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
else if (process.platform === 'linux') {
|
|
862
|
-
// On Linux, check using OpenSSL against the system trust store
|
|
863
|
-
try {
|
|
864
|
-
// This is a simplified check and may need to be adjusted per distribution
|
|
865
|
-
const certFingerprint = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`).toString().trim()
|
|
866
|
-
const fingerprintValue = certFingerprint.split('=')[1]?.trim() || ''
|
|
867
|
-
|
|
868
|
-
// Different distros store certs in different locations
|
|
869
|
-
const trustStores = [
|
|
870
|
-
'/etc/ssl/certs', // Debian/Ubuntu
|
|
871
|
-
'/etc/pki/tls/certs', // RedHat/CentOS
|
|
872
|
-
]
|
|
873
|
-
|
|
874
|
-
for (const store of trustStores) {
|
|
875
|
-
try {
|
|
876
|
-
const storeOutput = execSync(`find ${store} -type f -exec openssl x509 -noout -fingerprint -sha256 -in {} \\; 2>/dev/null | grep "${fingerprintValue}"`).toString()
|
|
877
|
-
|
|
878
|
-
if (storeOutput.includes(fingerprintValue)) {
|
|
879
|
-
debugLog('ssl', `Certificate fingerprint found in ${store}`, options?.verbose)
|
|
880
|
-
return true
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
catch {
|
|
884
|
-
// Ignore errors searching specific stores
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
debugLog('ssl', 'Certificate not found in system trust stores', options?.verbose)
|
|
889
|
-
return false
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
debugLog('ssl', `Error checking certificate trust on Linux: ${error}`, options?.verbose)
|
|
893
|
-
return false
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Default to false for unsupported platforms
|
|
898
|
-
debugLog('ssl', `Platform ${process.platform} not supported for certificate trust check`, options?.verbose)
|
|
899
|
-
return false
|
|
900
|
-
}
|
|
901
|
-
catch (err) {
|
|
902
|
-
debugLog('ssl', `Error checking if certificate is trusted: ${err}`, options?.verbose)
|
|
903
|
-
return false
|
|
904
|
-
}
|
|
905
|
-
}
|