@stacksjs/rpx 0.11.8 → 0.11.10
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/bin/cli.js +167 -162
- package/dist/cert-inspect.d.ts +15 -0
- package/dist/chunk-3yvvmvc0.js +1 -0
- package/dist/{chunk-jpf41gb9.js → chunk-97manwts.js} +1 -1
- package/dist/chunk-krj531r2.js +1 -0
- package/dist/chunk-ppbddztz.js +156 -0
- package/dist/daemon.d.ts +4 -0
- package/dist/dns-state.d.ts +27 -0
- package/dist/dns.d.ts +43 -0
- package/dist/https.d.ts +30 -2
- package/dist/index.d.ts +48 -0
- package/dist/index.js +6 -154
- package/dist/macos-trust.d.ts +40 -0
- package/package.json +1 -1
- package/src/cert-inspect.ts +69 -0
- package/src/daemon.ts +41 -2
- package/src/dns-state.ts +116 -0
- package/src/dns.ts +252 -142
- package/src/https.ts +78 -67
- package/src/index.ts +53 -0
- package/src/macos-trust.ts +175 -0
- package/src/start.ts +7 -9
- package/dist/chunk-6z1nzq0x.js +0 -1
- package/dist/chunk-qcdcnadb.js +0 -1
package/src/https.ts
CHANGED
|
@@ -9,8 +9,39 @@ import * as process from 'node:process'
|
|
|
9
9
|
import { addCertToSystemTrustStoreAndSaveCert, createRootCA, generateCertificate as generateCert } from '@stacksjs/tlsx'
|
|
10
10
|
import { log } from './logger'
|
|
11
11
|
import { config } from './config'
|
|
12
|
+
import {
|
|
13
|
+
MACOS_CA_TRUST_FLAGS,
|
|
14
|
+
MACOS_SYSTEM_KEYCHAIN,
|
|
15
|
+
getMacosLoginKeychainPath,
|
|
16
|
+
isRootCaFingerprintInKeychains,
|
|
17
|
+
isRootCaTrustedForSsl,
|
|
18
|
+
pruneStaleRootCas,
|
|
19
|
+
trustRootCaForBrowsers,
|
|
20
|
+
} from './macos-trust'
|
|
12
21
|
import { debugLog, execSudoSync, getPrimaryDomain, isMultiProxyConfig, isMultiProxyOptions, isSingleProxyOptions, isValidRootCA, safeDeleteFile } from './utils'
|
|
13
22
|
|
|
23
|
+
export {
|
|
24
|
+
MACOS_CA_TRUST_FLAGS,
|
|
25
|
+
MACOS_SYSTEM_KEYCHAIN,
|
|
26
|
+
RPX_ROOT_CA_COMMON_NAME,
|
|
27
|
+
getMacosLoginKeychainPath,
|
|
28
|
+
getMacosTrustKeychains,
|
|
29
|
+
isRootCaFingerprintInKeychains,
|
|
30
|
+
isRootCaTrustedForSsl,
|
|
31
|
+
listCertSha256HashesByCommonName,
|
|
32
|
+
pruneStaleRootCas,
|
|
33
|
+
trustRootCaForBrowsers,
|
|
34
|
+
} from './macos-trust'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
certIncludesSanHostnames,
|
|
38
|
+
normalizeSha256Fingerprint,
|
|
39
|
+
parseSha256HashesFromSecurityListing,
|
|
40
|
+
readCertCommonName,
|
|
41
|
+
readCertSha256Fingerprint,
|
|
42
|
+
verifyHttpsChain,
|
|
43
|
+
} from './cert-inspect'
|
|
44
|
+
|
|
14
45
|
let cachedSSLConfig: { key: string, cert: string, ca?: string } | null = null
|
|
15
46
|
|
|
16
47
|
// Canonical filenames for the shared Root CA. The CA is a singleton across all
|
|
@@ -34,6 +65,21 @@ export function getRootCAPaths(basePath: string): RootCAPaths {
|
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
|
|
68
|
+
/** Paths for the shared multi-host daemon cert under `~/.stacks/ssl`. */
|
|
69
|
+
export function getSharedDaemonCertPaths(sslDir: string): {
|
|
70
|
+
certPath: string
|
|
71
|
+
keyPath: string
|
|
72
|
+
caCertPath: string
|
|
73
|
+
rootCA: RootCAPaths
|
|
74
|
+
} {
|
|
75
|
+
return {
|
|
76
|
+
certPath: join(sslDir, 'rpx.localhost.crt'),
|
|
77
|
+
keyPath: join(sslDir, 'rpx.localhost.key'),
|
|
78
|
+
caCertPath: join(sslDir, 'rpx.localhost.ca.crt'),
|
|
79
|
+
rootCA: getRootCAPaths(sslDir),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
37
83
|
/**
|
|
38
84
|
* Load a previously-persisted Root CA (cert + private key). Returns `null` if
|
|
39
85
|
* either file is missing or unreadable, signalling that a fresh CA needs to be
|
|
@@ -230,9 +276,12 @@ export async function loadSSLConfig(options: ProxyOption): Promise<SSLConfig | n
|
|
|
230
276
|
/**
|
|
231
277
|
* Force trust a certificate - exposing for direct use
|
|
232
278
|
*/
|
|
233
|
-
export async function forceTrustCertificate(
|
|
279
|
+
export async function forceTrustCertificate(
|
|
280
|
+
certPath: string,
|
|
281
|
+
options?: { serverName?: string, verbose?: boolean },
|
|
282
|
+
): Promise<boolean> {
|
|
234
283
|
if (process.platform === 'darwin')
|
|
235
|
-
return forceTrustCertificateMacOS(certPath)
|
|
284
|
+
return forceTrustCertificateMacOS(certPath, options)
|
|
236
285
|
|
|
237
286
|
if (process.platform === 'linux') {
|
|
238
287
|
try {
|
|
@@ -270,37 +319,24 @@ export async function forceTrustCertificate(certPath: string): Promise<boolean>
|
|
|
270
319
|
* Force trust a certificate on macOS using direct security command
|
|
271
320
|
* This function first checks if the certificate is already trusted to avoid unnecessary sudo prompts
|
|
272
321
|
*/
|
|
273
|
-
async function forceTrustCertificateMacOS(
|
|
322
|
+
async function forceTrustCertificateMacOS(
|
|
323
|
+
certPath: string,
|
|
324
|
+
options?: { serverName?: string, verbose?: boolean },
|
|
325
|
+
): Promise<boolean> {
|
|
274
326
|
if (process.platform !== 'darwin')
|
|
275
327
|
return false
|
|
276
328
|
|
|
329
|
+
const serverName = options?.serverName ?? 'rpx.localhost'
|
|
330
|
+
const verbose = options?.verbose ?? false
|
|
331
|
+
|
|
277
332
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (alreadyTrusted) {
|
|
281
|
-
debugLog('ssl', 'Certificate is already trusted, skipping trust operation', false)
|
|
333
|
+
if (isRootCaTrustedForSsl(certPath, serverName, { verbose })) {
|
|
334
|
+
debugLog('ssl', 'Root CA already trusted for SSL, skipping trust operation', verbose)
|
|
282
335
|
return true
|
|
283
336
|
}
|
|
284
337
|
|
|
285
|
-
debugLog('ssl', 'Trusting
|
|
286
|
-
|
|
287
|
-
// Login keychain — no sudo; Chrome/Arc often read this store on macOS.
|
|
288
|
-
const loginKeychain = join(homedir(), 'Library/Keychains/login.keychain-db')
|
|
289
|
-
try {
|
|
290
|
-
execSync(`security add-trusted-cert -d -r trustRoot -p ssl -p basic -k "${loginKeychain}" "${certPath}"`, { stdio: 'ignore' })
|
|
291
|
-
if (await isCertTrusted(certPath, { verbose: false, regenerateUntrustedCerts: true }))
|
|
292
|
-
return true
|
|
293
|
-
}
|
|
294
|
-
catch { /* fall through to system keychain */ }
|
|
295
|
-
|
|
296
|
-
// System keychain — requires sudo / SUDO_PASSWORD.
|
|
297
|
-
try {
|
|
298
|
-
execSudoSync(`security add-trusted-cert -d -r trustRoot -p ssl -p basic -k /Library/Keychains/System.keychain "${certPath}"`)
|
|
299
|
-
return await isCertTrusted(certPath, { verbose: false, regenerateUntrustedCerts: true })
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
return false
|
|
303
|
-
}
|
|
338
|
+
debugLog('ssl', 'Trusting Root CA for browsers (login + system keychains)', verbose)
|
|
339
|
+
return trustRootCaForBrowsers(certPath, { serverName, verbose })
|
|
304
340
|
}
|
|
305
341
|
catch {
|
|
306
342
|
return false
|
|
@@ -413,7 +449,13 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
413
449
|
// We install only the Root CA — host certs derive their trust from it.
|
|
414
450
|
if (process.platform === 'darwin') {
|
|
415
451
|
try {
|
|
416
|
-
|
|
452
|
+
pruneStaleRootCas({ caPath: rootCAPaths.caCertPath, verbose: options.verbose })
|
|
453
|
+
const loginKeychain = getMacosLoginKeychainPath()
|
|
454
|
+
try {
|
|
455
|
+
execSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${loginKeychain}" "${rootCAPaths.caCertPath}"`, { stdio: 'ignore' })
|
|
456
|
+
}
|
|
457
|
+
catch { /* login keychain optional */ }
|
|
458
|
+
execSudoSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"`)
|
|
417
459
|
if (options.verbose)
|
|
418
460
|
log.success('Successfully added Root CA to system trust store')
|
|
419
461
|
|
|
@@ -423,7 +465,7 @@ export async function generateCertificate(options: ProxyOptions): Promise<void>
|
|
|
423
465
|
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
424
466
|
const scriptContent = `#!/bin/bash
|
|
425
467
|
echo "Trusting RPX Root CA"
|
|
426
|
-
sudo security add-trusted-cert
|
|
468
|
+
sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
|
|
427
469
|
echo "Root CA trusted! Please restart your browser."
|
|
428
470
|
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
429
471
|
`
|
|
@@ -436,7 +478,7 @@ echo "If you still see certificate warnings, type 'thisisunsafe' on the warning
|
|
|
436
478
|
const scriptPath = join(sslDir, 'trust-rpx-cert.sh')
|
|
437
479
|
const scriptContent = `#!/bin/bash
|
|
438
480
|
echo "Trusting RPX Root CA"
|
|
439
|
-
sudo security add-trusted-cert
|
|
481
|
+
sudo security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${rootCAPaths.caCertPath}"
|
|
440
482
|
echo "Root CA trusted! Please restart your browser."
|
|
441
483
|
echo "If you still see certificate warnings, type 'thisisunsafe' on the warning page in Chrome/Arc browsers."
|
|
442
484
|
`
|
|
@@ -731,49 +773,18 @@ export async function cleanupCertificates(domain: string, verbose?: boolean): Pr
|
|
|
731
773
|
* Checks if a certificate is trusted by the system (macOS only for now)
|
|
732
774
|
* If options.regenerateUntrustedCerts is false, always returns true (skips trust check)
|
|
733
775
|
*/
|
|
734
|
-
export async function isCertTrusted(
|
|
776
|
+
export async function isCertTrusted(
|
|
777
|
+
certPath: string,
|
|
778
|
+
options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean, serverName?: string },
|
|
779
|
+
): Promise<boolean> {
|
|
735
780
|
try {
|
|
736
781
|
debugLog('ssl', `Checking if certificate is trusted: ${certPath}`, options?.verbose)
|
|
737
782
|
|
|
738
783
|
// Different check methods per platform
|
|
739
784
|
if (process.platform === 'darwin') {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const certFingerprint = execSync(`openssl x509 -noout -fingerprint -sha256 -in "${certPath}"`).toString().trim()
|
|
744
|
-
const normalize = (raw: string) => raw.split('=').pop()!.replace(/SHA-256\s+hash:\s*/gi, '').replace(/:/g, '').trim().toUpperCase()
|
|
745
|
-
const fingerprintValue = normalize(certFingerprint)
|
|
746
|
-
|
|
747
|
-
if (!fingerprintValue) {
|
|
748
|
-
debugLog('ssl', 'Could not extract certificate fingerprint', options?.verbose)
|
|
749
|
-
return false
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const keychains = [
|
|
753
|
-
'/Library/Keychains/System.keychain',
|
|
754
|
-
join(homedir(), 'Library/Keychains/login.keychain-db'),
|
|
755
|
-
]
|
|
756
|
-
|
|
757
|
-
for (const keychain of keychains) {
|
|
758
|
-
try {
|
|
759
|
-
const listing = execSync(`security find-certificate -a -Z "${keychain}" 2>/dev/null || true`).toString()
|
|
760
|
-
for (const line of listing.split('\n')) {
|
|
761
|
-
if (line.toUpperCase().includes('SHA-256') && normalize(line) === fingerprintValue) {
|
|
762
|
-
debugLog('ssl', `Certificate fingerprint found in ${keychain}`, options?.verbose)
|
|
763
|
-
return true
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
catch { /* try next keychain */ }
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
debugLog('ssl', 'Certificate fingerprint not found in system keychains', options?.verbose)
|
|
771
|
-
return false
|
|
772
|
-
}
|
|
773
|
-
catch (error) {
|
|
774
|
-
debugLog('ssl', `Error checking certificate trust: ${error}`, options?.verbose)
|
|
775
|
-
return false
|
|
776
|
-
}
|
|
785
|
+
if (options?.serverName)
|
|
786
|
+
return isRootCaTrustedForSsl(certPath, options.serverName, { verbose: options.verbose })
|
|
787
|
+
return isRootCaFingerprintInKeychains(certPath, { verbose: options.verbose })
|
|
777
788
|
}
|
|
778
789
|
else if (process.platform === 'win32') {
|
|
779
790
|
// On Windows, use PowerShell to check the certificate store
|
package/src/index.ts
CHANGED
|
@@ -16,11 +16,35 @@ export {
|
|
|
16
16
|
clearSslConfigCache,
|
|
17
17
|
forceTrustCertificate,
|
|
18
18
|
generateCertificate,
|
|
19
|
+
getRootCAPaths,
|
|
20
|
+
getSharedDaemonCertPaths,
|
|
19
21
|
httpsConfig,
|
|
20
22
|
isCertTrusted,
|
|
21
23
|
loadSSLConfig,
|
|
22
24
|
} from './https'
|
|
23
25
|
|
|
26
|
+
export {
|
|
27
|
+
MACOS_CA_TRUST_FLAGS,
|
|
28
|
+
MACOS_SYSTEM_KEYCHAIN,
|
|
29
|
+
RPX_ROOT_CA_COMMON_NAME,
|
|
30
|
+
getMacosLoginKeychainPath,
|
|
31
|
+
getMacosTrustKeychains,
|
|
32
|
+
isRootCaFingerprintInKeychains,
|
|
33
|
+
isRootCaTrustedForSsl,
|
|
34
|
+
listCertSha256HashesByCommonName,
|
|
35
|
+
pruneStaleRootCas,
|
|
36
|
+
trustRootCaForBrowsers,
|
|
37
|
+
} from './macos-trust'
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
certIncludesSanHostnames,
|
|
41
|
+
normalizeSha256Fingerprint,
|
|
42
|
+
parseSha256HashesFromSecurityListing,
|
|
43
|
+
readCertCommonName,
|
|
44
|
+
readCertSha256Fingerprint,
|
|
45
|
+
verifyHttpsChain,
|
|
46
|
+
} from './cert-inspect'
|
|
47
|
+
|
|
24
48
|
export { DefaultPortManager, findAvailablePort, isPortInUse, portManager } from './port-manager'
|
|
25
49
|
|
|
26
50
|
export {
|
|
@@ -37,6 +61,34 @@ export {
|
|
|
37
61
|
|
|
38
62
|
export type { RegistryEntry, WatchHandle, WatchOptions } from './registry'
|
|
39
63
|
|
|
64
|
+
export {
|
|
65
|
+
DNS_PORT,
|
|
66
|
+
RPX_RESOLVER_MARKER,
|
|
67
|
+
contentLooksLikeRpxResolver,
|
|
68
|
+
isDnsServerRunning,
|
|
69
|
+
reconcileStaleDevelopmentDns,
|
|
70
|
+
removeLegacyTldResolvers,
|
|
71
|
+
removeResolver,
|
|
72
|
+
resolverFilePath,
|
|
73
|
+
setupDevelopmentDns,
|
|
74
|
+
setupResolver,
|
|
75
|
+
startDnsServer,
|
|
76
|
+
stopDnsServer,
|
|
77
|
+
syncDevelopmentDnsFromRegistry,
|
|
78
|
+
tearDownDevelopmentDns,
|
|
79
|
+
} from './dns'
|
|
80
|
+
|
|
81
|
+
export type { DevelopmentDnsOptions } from './dns'
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
DNS_STATE_VERSION,
|
|
85
|
+
LEGACY_TLD_RESOLVER_LABELS,
|
|
86
|
+
devDomainsFromHosts,
|
|
87
|
+
normalizeDevDomain,
|
|
88
|
+
resolverBasenameForDomain,
|
|
89
|
+
resolverBasenamesForDomains,
|
|
90
|
+
} from './dns-state'
|
|
91
|
+
|
|
40
92
|
export {
|
|
41
93
|
acquireDaemonLock,
|
|
42
94
|
defaultDaemonSpawnCommand,
|
|
@@ -45,6 +97,7 @@ export {
|
|
|
45
97
|
getDaemonRpxDir,
|
|
46
98
|
isDaemonRunning,
|
|
47
99
|
readDaemonPid,
|
|
100
|
+
reconcileDevelopmentDnsOnIdle,
|
|
48
101
|
releaseDaemonLock,
|
|
49
102
|
runDaemon,
|
|
50
103
|
stopDaemon,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { readCertSha256Fingerprint } from './cert-inspect'
|
|
5
|
+
import { debugLog, execSudoSync } from './utils'
|
|
6
|
+
|
|
7
|
+
/** Chrome/Edge need SSL + basic trust policies — plain trustRoot often leaves "trust settings: 0". */
|
|
8
|
+
export const MACOS_CA_TRUST_FLAGS = '-d -r trustRoot -p ssl -p basic'
|
|
9
|
+
|
|
10
|
+
export const MACOS_SYSTEM_KEYCHAIN = '/Library/Keychains/System.keychain'
|
|
11
|
+
|
|
12
|
+
/** Default CN label for the shared rpx Root CA in macOS keychain listings. */
|
|
13
|
+
export const RPX_ROOT_CA_COMMON_NAME = 'rpx.localhost'
|
|
14
|
+
|
|
15
|
+
export function getMacosLoginKeychainPath(): string {
|
|
16
|
+
return join(homedir(), 'Library/Keychains/login.keychain-db')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getMacosTrustKeychains(): string[] {
|
|
20
|
+
return [MACOS_SYSTEM_KEYCHAIN, getMacosLoginKeychainPath()]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ListCertsByCommonNameOptions {
|
|
24
|
+
keychain: string
|
|
25
|
+
commonName?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listCertSha256HashesByCommonName(
|
|
29
|
+
keychain: string,
|
|
30
|
+
commonName: string = RPX_ROOT_CA_COMMON_NAME,
|
|
31
|
+
): string[] {
|
|
32
|
+
const listing = execSync(
|
|
33
|
+
`security find-certificate -a -c "${commonName}" -Z "${keychain}" 2>/dev/null || true`,
|
|
34
|
+
{ encoding: 'utf8' },
|
|
35
|
+
)
|
|
36
|
+
const hashes: string[] = []
|
|
37
|
+
for (const line of listing.split('\n')) {
|
|
38
|
+
const match = line.match(/SHA-256 hash:\s*([A-F0-9]+)/i)
|
|
39
|
+
if (match)
|
|
40
|
+
hashes.push(match[1]!.toUpperCase())
|
|
41
|
+
}
|
|
42
|
+
return hashes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PruneStaleRootCasOptions {
|
|
46
|
+
caPath: string
|
|
47
|
+
commonName?: string
|
|
48
|
+
keychains?: string[]
|
|
49
|
+
verbose?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove older rpx Root CA copies from keychains, keeping only the fingerprint
|
|
54
|
+
* that matches the on-disk `caPath` file.
|
|
55
|
+
*/
|
|
56
|
+
export function pruneStaleRootCas(options: PruneStaleRootCasOptions): void {
|
|
57
|
+
if (process.platform !== 'darwin')
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
const keep = readCertSha256Fingerprint(options.caPath)
|
|
61
|
+
if (!keep)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
const commonName = options.commonName ?? RPX_ROOT_CA_COMMON_NAME
|
|
65
|
+
const keychains = options.keychains ?? getMacosTrustKeychains()
|
|
66
|
+
|
|
67
|
+
for (const keychain of keychains) {
|
|
68
|
+
for (const hash of listCertSha256HashesByCommonName(keychain, commonName)) {
|
|
69
|
+
if (hash === keep)
|
|
70
|
+
continue
|
|
71
|
+
try {
|
|
72
|
+
if (keychain.startsWith('/Library'))
|
|
73
|
+
execSudoSync(`security delete-certificate -Z ${hash} "${keychain}"`)
|
|
74
|
+
else
|
|
75
|
+
execSync(`security delete-certificate -Z ${hash} "${keychain}"`, { stdio: 'ignore' })
|
|
76
|
+
debugLog('ssl', `Removed stale Root CA ${hash} from ${keychain}`, options.verbose)
|
|
77
|
+
}
|
|
78
|
+
catch { /* already removed */ }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* True when the Root CA is trusted for SSL to `serverName` (macOS verify-cert).
|
|
85
|
+
* On other platforms, falls back to fingerprint presence in trust stores.
|
|
86
|
+
*/
|
|
87
|
+
export function isRootCaTrustedForSsl(
|
|
88
|
+
caPath: string,
|
|
89
|
+
serverName: string,
|
|
90
|
+
options?: { verbose?: boolean },
|
|
91
|
+
): boolean {
|
|
92
|
+
if (process.platform !== 'darwin')
|
|
93
|
+
return isRootCaFingerprintInKeychains(caPath, options)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const out = execSync(
|
|
97
|
+
`security verify-cert -c "${caPath}" -s "${serverName}" -l -L -R ssl 2>&1`,
|
|
98
|
+
{ encoding: 'utf8' },
|
|
99
|
+
)
|
|
100
|
+
const ok = out.includes('successful')
|
|
101
|
+
debugLog('ssl', `verify-cert ${serverName}: ${ok ? 'trusted' : 'not trusted'}`, options?.verbose)
|
|
102
|
+
return ok
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isRootCaFingerprintInKeychains(
|
|
110
|
+
caPath: string,
|
|
111
|
+
options?: { verbose?: boolean },
|
|
112
|
+
): boolean {
|
|
113
|
+
const fp = readCertSha256Fingerprint(caPath)
|
|
114
|
+
if (!fp)
|
|
115
|
+
return false
|
|
116
|
+
|
|
117
|
+
for (const keychain of getMacosTrustKeychains()) {
|
|
118
|
+
try {
|
|
119
|
+
const listing = execSync(`security find-certificate -a -Z "${keychain}" 2>/dev/null || true`, { encoding: 'utf8' })
|
|
120
|
+
for (const line of listing.split('\n')) {
|
|
121
|
+
if (line.toUpperCase().includes('SHA-256')) {
|
|
122
|
+
const lineFp = line.split('=').pop()!.replace(/SHA-256\s+hash:\s*/gi, '').replace(/:/g, '').trim().toUpperCase()
|
|
123
|
+
if (lineFp === fp) {
|
|
124
|
+
debugLog('ssl', `Root CA fingerprint found in ${keychain}`, options?.verbose)
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch { /* try next keychain */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface TrustRootCaForBrowsersOptions {
|
|
137
|
+
serverName: string
|
|
138
|
+
commonName?: string
|
|
139
|
+
verbose?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Install the Root CA into login + system keychains with SSL/basic policies,
|
|
144
|
+
* pruning stale copies first. Returns true when SSL verification succeeds or
|
|
145
|
+
* the cert fingerprint is present in a keychain.
|
|
146
|
+
*/
|
|
147
|
+
export function trustRootCaForBrowsers(
|
|
148
|
+
caPath: string,
|
|
149
|
+
options: TrustRootCaForBrowsersOptions,
|
|
150
|
+
): boolean {
|
|
151
|
+
if (process.platform !== 'darwin')
|
|
152
|
+
return false
|
|
153
|
+
|
|
154
|
+
const serverName = options.serverName
|
|
155
|
+
pruneStaleRootCas({ caPath, commonName: options.commonName, verbose: options.verbose })
|
|
156
|
+
|
|
157
|
+
const loginKeychain = getMacosLoginKeychainPath()
|
|
158
|
+
try {
|
|
159
|
+
execSync(
|
|
160
|
+
`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k "${loginKeychain}" "${caPath}"`,
|
|
161
|
+
{ stdio: 'ignore' },
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
catch { /* may already exist — re-apply trust below */ }
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
execSudoSync(`security add-trusted-cert ${MACOS_CA_TRUST_FLAGS} -k ${MACOS_SYSTEM_KEYCHAIN} "${caPath}"`)
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return isRootCaTrustedForSsl(caPath, serverName, { verbose: options.verbose })
|
|
174
|
+
|| isRootCaFingerprintInKeychains(caPath, { verbose: options.verbose })
|
|
175
|
+
}
|
package/src/start.ts
CHANGED
|
@@ -877,10 +877,10 @@ export function startProxy(options: ProxyOption): void {
|
|
|
877
877
|
}
|
|
878
878
|
|
|
879
879
|
if (isCustomDomain) {
|
|
880
|
-
import('./dns').then(({
|
|
881
|
-
|
|
880
|
+
import('./dns').then(({ setupDevelopmentDns }) => {
|
|
881
|
+
setupDevelopmentDns({ domains: [targetDomain], verbose: mergedOptions.verbose }).then((started) => {
|
|
882
882
|
if (started) {
|
|
883
|
-
|
|
883
|
+
Promise.resolve().then(() => {
|
|
884
884
|
if (mergedOptions.verbose) {
|
|
885
885
|
if (reservedTlds.includes(tld)) {
|
|
886
886
|
log.success(`DNS server started for .${tld} domains`)
|
|
@@ -1152,10 +1152,9 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
1154
|
if (process.platform === 'darwin' && customDomains.length > 0) {
|
|
1155
|
-
const {
|
|
1156
|
-
const dnsStarted = await
|
|
1155
|
+
const { setupDevelopmentDns } = await import('./dns')
|
|
1156
|
+
const dnsStarted = await setupDevelopmentDns({ domains: customDomains, verbose })
|
|
1157
1157
|
if (dnsStarted) {
|
|
1158
|
-
await setupResolver(verbose, customDomains)
|
|
1159
1158
|
if (verbose) {
|
|
1160
1159
|
const hasReservedOnly = uniqueTlds.every((t): t is string => !!t && reservedTlds.includes(t as string))
|
|
1161
1160
|
if (hasReservedOnly) {
|
|
@@ -1177,9 +1176,8 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
|
|
|
1177
1176
|
|
|
1178
1177
|
try {
|
|
1179
1178
|
// Stop DNS server
|
|
1180
|
-
const {
|
|
1181
|
-
|
|
1182
|
-
await removeResolver(mergedOptions.verbose)
|
|
1179
|
+
const { tearDownDevelopmentDns } = await import('./dns')
|
|
1180
|
+
await tearDownDevelopmentDns({ verbose: mergedOptions.verbose })
|
|
1183
1181
|
}
|
|
1184
1182
|
catch (err) {
|
|
1185
1183
|
debugLog('cleanup', `Error stopping DNS server: ${err}`, mergedOptions.verbose)
|
package/dist/chunk-6z1nzq0x.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as X,d as F}from"./chunk-jpf41gb9.js";import Q from"node:dgram";var $=15353;function _(k){return{id:k.readUInt16BE(0),flags:k.readUInt16BE(2),qdcount:k.readUInt16BE(4),ancount:k.readUInt16BE(6),nscount:k.readUInt16BE(8),arcount:k.readUInt16BE(10)}}function C(k,z){let A=[],j=z;while(!0){let B=k[j];if(B===0){j++;break}if((B&192)===192){let G=k.readUInt16BE(j)&16383,{name:E}=C(k,G);A.push(E),j+=2;break}j++,A.push(k.subarray(j,j+B).toString("ascii")),j+=B}return{name:A.join("."),newOffset:j}}function H(k,z){let{name:A,newOffset:j}=C(k,z),B=k.readUInt16BE(j),G=k.readUInt16BE(j+2);return{question:{name:A,type:B,class:G},newOffset:j+4}}function U(k){let z=k.split("."),A=[];for(let j of z)A.push(Buffer.from([j.length])),A.push(Buffer.from(j,"ascii"));return A.push(Buffer.from([0])),Buffer.concat(A)}function T(k,z,A){let j=[],B=Buffer.alloc(12);B.writeUInt16BE(k,0),B.writeUInt16BE(33152,2),B.writeUInt16BE(1,4),B.writeUInt16BE(1,6),B.writeUInt16BE(0,8),B.writeUInt16BE(0,10),j.push(B),j.push(U(z.name));let G=Buffer.alloc(4);G.writeUInt16BE(z.type,0),G.writeUInt16BE(z.class,2),j.push(G),j.push(U(z.name));let E=Buffer.alloc(10);if(E.writeUInt16BE(z.type,0),E.writeUInt16BE(1,2),E.writeUInt32BE(300,4),z.type===1){E.writeUInt16BE(4,8),j.push(E);let K=A.split(".").map((M)=>Number.parseInt(M,10));j.push(Buffer.from(K))}else if(z.type===28)E.writeUInt16BE(16,8),j.push(E),j.push(Buffer.from([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]));else return B.writeUInt16BE(33155,2),B.writeUInt16BE(0,6),Buffer.concat([B,U(z.name),G]);return Buffer.concat(j)}function I(k,z){let A=[],j=Buffer.alloc(12);j.writeUInt16BE(k,0),j.writeUInt16BE(33155,2),j.writeUInt16BE(1,4),j.writeUInt16BE(0,6),j.writeUInt16BE(0,8),j.writeUInt16BE(0,10),A.push(j),A.push(U(z.name));let B=Buffer.alloc(4);return B.writeUInt16BE(z.type,0),B.writeUInt16BE(z.class,2),A.push(B),Buffer.concat(A)}var J=null,Z=new Set;async function R(k,z){if(J)return F("dns","DNS server already running",z),!0;return Z=new Set(k.map((A)=>A.toLowerCase())),new Promise((A)=>{J=Q.createSocket("udp4"),J.on("error",(j)=>{if(F("dns",`DNS server error: ${j.message}`,z),j.message.includes("EACCES")||j.message.includes("permission"))F("dns","DNS server requires root privileges to bind to port 53",z);J?.close(),J=null,A(!1)}),J.on("message",(j,B)=>{try{let G=_(j),{question:E}=H(j,12);F("dns",`Query for ${E.name} type ${E.type} from ${B.address}`,z);let K=E.name.toLowerCase(),M=!1;for(let Y of Z)if(K===Y||K.endsWith(`.${Y}`)){M=!0;break}let W;if(M&&(E.type===1||E.type===28))W=T(G.id,E,"127.0.0.1"),F("dns",`Responding with localhost for ${E.name}`,z);else W=I(G.id,E),F("dns",`NXDOMAIN for ${E.name}`,z);J?.send(W,B.port,B.address)}catch(G){F("dns",`Error processing DNS query: ${G}`,z)}}),J.on("listening",()=>{let j=J?.address();F("dns",`DNS server listening on ${j?.address}:${j?.port}`,z),A(!0)});try{J.bind($,"127.0.0.1")}catch(j){F("dns",`Failed to bind DNS server: ${j}`,z),A(!1)}})}function O(k){if(J)F("dns","Stopping DNS server",k),J.close(),J=null}function w(){return J!==null}function x(k){let z=new Set;for(let A of k){let j=A.split(".");if(j.length>=2)z.add(j[j.length-1])}return Array.from(z)}var V=new Set;async function D(k){if(process.platform!=="darwin")return;let{execSudoSync:z,getSudoPassword:A}=await import("./chunk-qcdcnadb.js");if(!A()){F("dns","Cannot flush DNS cache without SUDO_PASSWORD",k);return}try{z("dscacheutil -flushcache"),z("killall -HUP mDNSResponder 2>/dev/null || true"),F("dns","DNS cache flushed",k)}catch(B){F("dns",`Could not flush DNS cache: ${B}`,k)}}async function y(k,z){if(process.platform!=="darwin")return F("dns","Resolver setup only needed on macOS",k),!0;let{execSudoSync:A,getSudoPassword:j}=await import("./chunk-qcdcnadb.js");if(!j())return F("dns","SUDO_PASSWORD not set, cannot create resolver files",k),!1;let G=z?x(z):["test"];try{for(let E of G){if(V.has(E))continue;let K=`bash -c 'mkdir -p /etc/resolver && echo -e "nameserver 127.0.0.1\\nport ${$}" > /etc/resolver/${E}'`;A(K),V.add(E),F("dns",`Created /etc/resolver/${E} for .${E} TLD`,k)}return await D(k),!0}catch(E){return F("dns",`Failed to create resolver file: ${E}`,k),!1}}async function L(k){if(process.platform!=="darwin")return;let{execSudoSync:z,getSudoPassword:A}=await import("./chunk-qcdcnadb.js");try{if(A()){for(let B of V)z(`rm -f /etc/resolver/${B}`),F("dns",`Removed /etc/resolver/${B}`,k);V.clear()}}catch(j){F("dns",`Failed to remove resolver files: ${j}`,k)}}export{O as stopDnsServer,R as startDnsServer,y as setupResolver,L as removeResolver,w as isDnsServerRunning};
|
package/dist/chunk-qcdcnadb.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as a,c as b,d as c,e as d,f as e,g as f,h as g,i as h,j as i,k as j,l as k,m as l,n as m,o as n}from"./chunk-jpf41gb9.js";export{e as safeStringify,n as safeDeleteFile,m as resolvePathRewrite,d as redactSensitive,g as isValidRootCA,k as isSingleProxyOptions,l as isSingleProxyConfig,j as isMultiProxyOptions,i as isMultiProxyConfig,a as getSudoPassword,h as getPrimaryDomain,f as extractHostname,b as execSudoSync,c as debugLog};
|