@stacksjs/rpx 0.11.7 → 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 +170 -164
- 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-runner.d.ts +3 -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 +32 -2
- package/dist/index.d.ts +49 -0
- package/dist/index.js +6 -153
- package/dist/macos-trust.d.ts +40 -0
- package/package.json +1 -1
- package/src/cert-inspect.ts +69 -0
- package/src/daemon-runner.ts +15 -2
- package/src/daemon.ts +70 -9
- package/src/dns-state.ts +116 -0
- package/src/dns.ts +252 -142
- package/src/https.ts +94 -53
- package/src/index.ts +54 -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/dns.ts
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Local development DNS for macOS.
|
|
3
|
+
*
|
|
4
|
+
* Uses domain-scoped `/etc/resolver/<base-domain>` files (never whole-TLD hijacks like
|
|
5
|
+
* `/etc/resolver/com`) so real sites keep working when the rpx DNS server is down.
|
|
4
6
|
*/
|
|
5
7
|
import dgram from 'node:dgram'
|
|
8
|
+
import * as fsp from 'node:fs/promises'
|
|
9
|
+
import * as path from 'node:path'
|
|
10
|
+
import * as process from 'node:process'
|
|
11
|
+
import type { RegistryEntry } from './registry'
|
|
12
|
+
import { getDaemonRpxDir } from './daemon'
|
|
13
|
+
import {
|
|
14
|
+
type DnsState,
|
|
15
|
+
DNS_STATE_VERSION,
|
|
16
|
+
devDomainsFromHosts,
|
|
17
|
+
LEGACY_TLD_RESOLVER_LABELS,
|
|
18
|
+
loadDnsState,
|
|
19
|
+
resolverBasenamesForDomains,
|
|
20
|
+
saveDnsState,
|
|
21
|
+
clearDnsState,
|
|
22
|
+
} from './dns-state'
|
|
23
|
+
import { isPidAlive } from './registry'
|
|
6
24
|
import { debugLog } from './utils'
|
|
7
25
|
|
|
8
|
-
|
|
9
|
-
const DNS_PORT = 15353
|
|
26
|
+
/** High port — does not require root. */
|
|
27
|
+
export const DNS_PORT = 15353
|
|
28
|
+
|
|
29
|
+
export const RPX_RESOLVER_MARKER = '# managed-by: rpx'
|
|
30
|
+
|
|
31
|
+
const MACOS_RESOLVER_DIR = '/etc/resolver'
|
|
32
|
+
|
|
33
|
+
export interface DevelopmentDnsOptions {
|
|
34
|
+
domains: string[]
|
|
35
|
+
rpxDir?: string
|
|
36
|
+
verbose?: boolean
|
|
37
|
+
/** Defaults to `process.pid` — stored so stale state can be reconciled after crashes. */
|
|
38
|
+
ownerPid?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let dnsServer: dgram.Socket | null = null
|
|
42
|
+
let configuredDomains: Set<string> = new Set()
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// DNS UDP server
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
10
47
|
|
|
11
48
|
interface DnsHeader {
|
|
12
49
|
id: number
|
|
@@ -23,9 +60,6 @@ interface DnsQuestion {
|
|
|
23
60
|
class: number
|
|
24
61
|
}
|
|
25
62
|
|
|
26
|
-
/**
|
|
27
|
-
* Parse DNS header from buffer
|
|
28
|
-
*/
|
|
29
63
|
function parseHeader(buffer: Buffer): DnsHeader {
|
|
30
64
|
return {
|
|
31
65
|
id: buffer.readUInt16BE(0),
|
|
@@ -37,9 +71,6 @@ function parseHeader(buffer: Buffer): DnsHeader {
|
|
|
37
71
|
}
|
|
38
72
|
}
|
|
39
73
|
|
|
40
|
-
/**
|
|
41
|
-
* Parse domain name from DNS message
|
|
42
|
-
*/
|
|
43
74
|
function parseName(buffer: Buffer, offset: number): { name: string, newOffset: number } {
|
|
44
75
|
const labels: string[] = []
|
|
45
76
|
let currentOffset = offset
|
|
@@ -52,7 +83,6 @@ function parseName(buffer: Buffer, offset: number): { name: string, newOffset: n
|
|
|
52
83
|
break
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
// Check for pointer (compression)
|
|
56
86
|
if ((length & 0xC0) === 0xC0) {
|
|
57
87
|
const pointer = buffer.readUInt16BE(currentOffset) & 0x3FFF
|
|
58
88
|
const { name } = parseName(buffer, pointer)
|
|
@@ -69,9 +99,6 @@ function parseName(buffer: Buffer, offset: number): { name: string, newOffset: n
|
|
|
69
99
|
return { name: labels.join('.'), newOffset: currentOffset }
|
|
70
100
|
}
|
|
71
101
|
|
|
72
|
-
/**
|
|
73
|
-
* Parse DNS question section
|
|
74
|
-
*/
|
|
75
102
|
function parseQuestion(buffer: Buffer, offset: number): { question: DnsQuestion, newOffset: number } {
|
|
76
103
|
const { name, newOffset } = parseName(buffer, offset)
|
|
77
104
|
const type = buffer.readUInt16BE(newOffset)
|
|
@@ -83,9 +110,6 @@ function parseQuestion(buffer: Buffer, offset: number): { question: DnsQuestion,
|
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
|
|
86
|
-
/**
|
|
87
|
-
* Encode domain name for DNS response
|
|
88
|
-
*/
|
|
89
113
|
function encodeName(name: string): Buffer {
|
|
90
114
|
const labels = name.split('.')
|
|
91
115
|
const parts: Buffer[] = []
|
|
@@ -94,87 +118,68 @@ function encodeName(name: string): Buffer {
|
|
|
94
118
|
parts.push(Buffer.from([label.length]))
|
|
95
119
|
parts.push(Buffer.from(label, 'ascii'))
|
|
96
120
|
}
|
|
97
|
-
parts.push(Buffer.from([0]))
|
|
121
|
+
parts.push(Buffer.from([0]))
|
|
98
122
|
|
|
99
123
|
return Buffer.concat(parts)
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
|
|
103
|
-
* Build DNS response
|
|
104
|
-
*/
|
|
105
|
-
function buildResponse(
|
|
106
|
-
queryId: number,
|
|
107
|
-
question: DnsQuestion,
|
|
108
|
-
ip: string,
|
|
109
|
-
): Buffer {
|
|
126
|
+
function buildResponse(queryId: number, question: DnsQuestion, ip: string): Buffer {
|
|
110
127
|
const parts: Buffer[] = []
|
|
111
128
|
|
|
112
|
-
// Header
|
|
113
129
|
const header = Buffer.alloc(12)
|
|
114
|
-
header.writeUInt16BE(queryId, 0)
|
|
115
|
-
header.writeUInt16BE(0x8180, 2)
|
|
116
|
-
header.writeUInt16BE(1, 4)
|
|
117
|
-
header.writeUInt16BE(1, 6)
|
|
118
|
-
header.writeUInt16BE(0, 8)
|
|
119
|
-
header.writeUInt16BE(0, 10)
|
|
130
|
+
header.writeUInt16BE(queryId, 0)
|
|
131
|
+
header.writeUInt16BE(0x8180, 2)
|
|
132
|
+
header.writeUInt16BE(1, 4)
|
|
133
|
+
header.writeUInt16BE(1, 6)
|
|
134
|
+
header.writeUInt16BE(0, 8)
|
|
135
|
+
header.writeUInt16BE(0, 10)
|
|
120
136
|
parts.push(header)
|
|
121
137
|
|
|
122
|
-
// Question section (echo back)
|
|
123
138
|
parts.push(encodeName(question.name))
|
|
124
139
|
const qtype = Buffer.alloc(4)
|
|
125
140
|
qtype.writeUInt16BE(question.type, 0)
|
|
126
141
|
qtype.writeUInt16BE(question.class, 2)
|
|
127
142
|
parts.push(qtype)
|
|
128
143
|
|
|
129
|
-
// Answer section
|
|
130
144
|
parts.push(encodeName(question.name))
|
|
131
145
|
|
|
132
146
|
const answer = Buffer.alloc(10)
|
|
133
|
-
answer.writeUInt16BE(question.type, 0)
|
|
134
|
-
answer.writeUInt16BE(1, 2)
|
|
135
|
-
answer.writeUInt32BE(300, 4)
|
|
147
|
+
answer.writeUInt16BE(question.type, 0)
|
|
148
|
+
answer.writeUInt16BE(1, 2)
|
|
149
|
+
answer.writeUInt32BE(300, 4)
|
|
136
150
|
|
|
137
151
|
if (question.type === 1) {
|
|
138
|
-
|
|
139
|
-
answer.writeUInt16BE(4, 8) // Data length
|
|
152
|
+
answer.writeUInt16BE(4, 8)
|
|
140
153
|
parts.push(answer)
|
|
141
154
|
const ipParts = ip.split('.').map(p => Number.parseInt(p, 10))
|
|
142
155
|
parts.push(Buffer.from(ipParts))
|
|
143
156
|
}
|
|
144
157
|
else if (question.type === 28) {
|
|
145
|
-
|
|
146
|
-
answer.writeUInt16BE(16, 8) // Data length
|
|
158
|
+
answer.writeUInt16BE(16, 8)
|
|
147
159
|
parts.push(answer)
|
|
148
|
-
// ::1 as bytes
|
|
149
160
|
parts.push(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))
|
|
150
161
|
}
|
|
151
162
|
else {
|
|
152
|
-
|
|
153
|
-
header.writeUInt16BE(
|
|
154
|
-
header.writeUInt16BE(0, 6) // No answers
|
|
163
|
+
header.writeUInt16BE(0x8183, 2)
|
|
164
|
+
header.writeUInt16BE(0, 6)
|
|
155
165
|
return Buffer.concat([header, encodeName(question.name), qtype])
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
return Buffer.concat(parts)
|
|
159
169
|
}
|
|
160
170
|
|
|
161
|
-
/**
|
|
162
|
-
* Build NXDOMAIN response for unknown domains
|
|
163
|
-
*/
|
|
164
171
|
function buildNxdomainResponse(queryId: number, question: DnsQuestion): Buffer {
|
|
165
172
|
const parts: Buffer[] = []
|
|
166
173
|
|
|
167
|
-
// Header with NXDOMAIN
|
|
168
174
|
const header = Buffer.alloc(12)
|
|
169
|
-
header.writeUInt16BE(queryId, 0)
|
|
170
|
-
header.writeUInt16BE(0x8183, 2)
|
|
171
|
-
header.writeUInt16BE(1, 4)
|
|
172
|
-
header.writeUInt16BE(0, 6)
|
|
173
|
-
header.writeUInt16BE(0, 8)
|
|
174
|
-
header.writeUInt16BE(0, 10)
|
|
175
|
+
header.writeUInt16BE(queryId, 0)
|
|
176
|
+
header.writeUInt16BE(0x8183, 2)
|
|
177
|
+
header.writeUInt16BE(1, 4)
|
|
178
|
+
header.writeUInt16BE(0, 6)
|
|
179
|
+
header.writeUInt16BE(0, 8)
|
|
180
|
+
header.writeUInt16BE(0, 10)
|
|
175
181
|
parts.push(header)
|
|
176
182
|
|
|
177
|
-
// Question section (echo back)
|
|
178
183
|
parts.push(encodeName(question.name))
|
|
179
184
|
const qtype = Buffer.alloc(4)
|
|
180
185
|
qtype.writeUInt16BE(question.type, 0)
|
|
@@ -184,28 +189,28 @@ function buildNxdomainResponse(queryId: number, question: DnsQuestion): Buffer {
|
|
|
184
189
|
return Buffer.concat(parts)
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
let dnsServer: dgram.Socket | null = null
|
|
188
|
-
let configuredDomains: Set<string> = new Set()
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Start the DNS server
|
|
192
|
-
*/
|
|
193
192
|
export async function startDnsServer(domains: string[], verbose?: boolean): Promise<boolean> {
|
|
193
|
+
if (process.platform !== 'darwin')
|
|
194
|
+
return false
|
|
195
|
+
|
|
196
|
+
const devDomains = devDomainsFromHosts(domains)
|
|
197
|
+
if (devDomains.length === 0)
|
|
198
|
+
return false
|
|
199
|
+
|
|
194
200
|
if (dnsServer) {
|
|
195
|
-
|
|
201
|
+
for (const d of devDomains)
|
|
202
|
+
configuredDomains.add(d)
|
|
203
|
+
debugLog('dns', 'DNS server already running — merged domains', verbose)
|
|
196
204
|
return true
|
|
197
205
|
}
|
|
198
206
|
|
|
199
|
-
configuredDomains = new Set(
|
|
207
|
+
configuredDomains = new Set(devDomains)
|
|
200
208
|
|
|
201
209
|
return new Promise((resolve) => {
|
|
202
210
|
dnsServer = dgram.createSocket('udp4')
|
|
203
211
|
|
|
204
212
|
dnsServer.on('error', (err) => {
|
|
205
213
|
debugLog('dns', `DNS server error: ${err.message}`, verbose)
|
|
206
|
-
if (err.message.includes('EACCES') || err.message.includes('permission')) {
|
|
207
|
-
debugLog('dns', 'DNS server requires root privileges to bind to port 53', verbose)
|
|
208
|
-
}
|
|
209
214
|
dnsServer?.close()
|
|
210
215
|
dnsServer = null
|
|
211
216
|
resolve(false)
|
|
@@ -218,7 +223,6 @@ export async function startDnsServer(domains: string[], verbose?: boolean): Prom
|
|
|
218
223
|
|
|
219
224
|
debugLog('dns', `Query for ${question.name} type ${question.type} from ${rinfo.address}`, verbose)
|
|
220
225
|
|
|
221
|
-
// Check if this domain should be handled
|
|
222
226
|
const domainLower = question.name.toLowerCase()
|
|
223
227
|
let shouldHandle = false
|
|
224
228
|
|
|
@@ -229,8 +233,6 @@ export async function startDnsServer(domains: string[], verbose?: boolean): Prom
|
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
|
|
232
|
-
// Note: Only configured domains are handled, no hardcoded TLDs
|
|
233
|
-
|
|
234
236
|
let response: Buffer
|
|
235
237
|
if (shouldHandle && (question.type === 1 || question.type === 28)) {
|
|
236
238
|
response = buildResponse(header.id, question, '127.0.0.1')
|
|
@@ -254,7 +256,6 @@ export async function startDnsServer(domains: string[], verbose?: boolean): Prom
|
|
|
254
256
|
resolve(true)
|
|
255
257
|
})
|
|
256
258
|
|
|
257
|
-
// Try to bind to port 53 with sudo
|
|
258
259
|
try {
|
|
259
260
|
dnsServer.bind(DNS_PORT, '127.0.0.1')
|
|
260
261
|
}
|
|
@@ -265,106 +266,103 @@ export async function startDnsServer(domains: string[], verbose?: boolean): Prom
|
|
|
265
266
|
})
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
/**
|
|
269
|
-
* Stop the DNS server
|
|
270
|
-
*/
|
|
271
269
|
export function stopDnsServer(verbose?: boolean): void {
|
|
272
270
|
if (dnsServer) {
|
|
273
271
|
debugLog('dns', 'Stopping DNS server', verbose)
|
|
274
272
|
dnsServer.close()
|
|
275
273
|
dnsServer = null
|
|
274
|
+
configuredDomains = new Set()
|
|
276
275
|
}
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
/**
|
|
280
|
-
* Check if DNS server is running
|
|
281
|
-
*/
|
|
282
278
|
export function isDnsServerRunning(): boolean {
|
|
283
279
|
return dnsServer !== null
|
|
284
280
|
}
|
|
285
281
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const parts = domain.split('.')
|
|
293
|
-
if (parts.length >= 2) {
|
|
294
|
-
tlds.add(parts[parts.length - 1])
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return Array.from(tlds)
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// macOS resolver files
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function resolverFileContent(): string {
|
|
287
|
+
return `${RPX_RESOLVER_MARKER}\nnameserver 127.0.0.1\nport ${DNS_PORT}\n`
|
|
298
288
|
}
|
|
299
289
|
|
|
300
|
-
|
|
301
|
-
|
|
290
|
+
export function resolverFilePath(basename: string): string {
|
|
291
|
+
return path.join(MACOS_RESOLVER_DIR, basename)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** True when a resolver file points at the rpx local DNS port. */
|
|
295
|
+
export function contentLooksLikeRpxResolver(content: string): boolean {
|
|
296
|
+
return content.includes('127.0.0.1') && content.includes(String(DNS_PORT))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function readResolverFile(basename: string): Promise<string | null> {
|
|
300
|
+
try {
|
|
301
|
+
return await fsp.readFile(resolverFilePath(basename), 'utf8')
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
|
|
305
|
+
return null
|
|
306
|
+
throw err
|
|
307
|
+
}
|
|
308
|
+
}
|
|
302
309
|
|
|
303
|
-
/**
|
|
304
|
-
* Flush macOS DNS cache to ensure resolver changes take effect
|
|
305
|
-
*/
|
|
306
310
|
async function flushDnsCache(verbose?: boolean): Promise<void> {
|
|
307
|
-
if (process.platform !== 'darwin')
|
|
311
|
+
if (process.platform !== 'darwin')
|
|
308
312
|
return
|
|
309
|
-
}
|
|
310
313
|
|
|
311
314
|
const { execSudoSync, getSudoPassword } = await import('./utils')
|
|
312
|
-
const sudoPassword = getSudoPassword()
|
|
313
315
|
|
|
314
|
-
if (!
|
|
316
|
+
if (!getSudoPassword()) {
|
|
315
317
|
debugLog('dns', 'Cannot flush DNS cache without SUDO_PASSWORD', verbose)
|
|
316
318
|
return
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
try {
|
|
320
|
-
// Flush DNS cache and restart mDNSResponder
|
|
321
322
|
execSudoSync('dscacheutil -flushcache')
|
|
322
323
|
execSudoSync('killall -HUP mDNSResponder 2>/dev/null || true')
|
|
323
324
|
debugLog('dns', 'DNS cache flushed', verbose)
|
|
324
325
|
}
|
|
325
326
|
catch (err) {
|
|
326
|
-
// Non-fatal - DNS cache flush failure shouldn't block startup
|
|
327
327
|
debugLog('dns', `Could not flush DNS cache: ${err}`, verbose)
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
async function writeResolverFile(basename: string, verbose?: boolean): Promise<void> {
|
|
332
|
+
const { execSudoSync } = await import('./utils')
|
|
333
|
+
const content = resolverFileContent().replace(/\n/g, '\\n')
|
|
334
|
+
const cmd = `bash -c 'mkdir -p ${MACOS_RESOLVER_DIR} && printf "%b" "${content}" > ${resolverFilePath(basename)}'`
|
|
335
|
+
execSudoSync(cmd)
|
|
336
|
+
debugLog('dns', `Created ${resolverFilePath(basename)}`, verbose)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function removeResolverFile(basename: string, verbose?: boolean): Promise<void> {
|
|
340
|
+
const { execSudoSync } = await import('./utils')
|
|
341
|
+
execSudoSync(`rm -f ${resolverFilePath(basename)}`)
|
|
342
|
+
debugLog('dns', `Removed ${resolverFilePath(basename)}`, verbose)
|
|
343
|
+
}
|
|
344
|
+
|
|
331
345
|
/**
|
|
332
|
-
*
|
|
333
|
-
* Creates /etc/resolver/<tld> files pointing to our local DNS server
|
|
346
|
+
* @deprecated Use {@link setupDevelopmentDns}. Domain-scoped resolver files only.
|
|
334
347
|
*/
|
|
335
348
|
export async function setupResolver(verbose?: boolean, domains?: string[]): Promise<boolean> {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return true
|
|
339
|
-
}
|
|
349
|
+
return setupDevelopmentDns({ domains: domains ?? [], verbose })
|
|
350
|
+
}
|
|
340
351
|
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
async function installResolvers(basenames: string[], verbose?: boolean): Promise<boolean> {
|
|
353
|
+
if (process.platform !== 'darwin')
|
|
354
|
+
return true
|
|
343
355
|
|
|
344
|
-
|
|
356
|
+
const { getSudoPassword } = await import('./utils')
|
|
357
|
+
if (!getSudoPassword()) {
|
|
345
358
|
debugLog('dns', 'SUDO_PASSWORD not set, cannot create resolver files', verbose)
|
|
346
359
|
return false
|
|
347
360
|
}
|
|
348
361
|
|
|
349
|
-
// Get TLDs from configured domains
|
|
350
|
-
const tlds = domains ? extractTLDs(domains) : ['test']
|
|
351
|
-
|
|
352
362
|
try {
|
|
353
|
-
for (const
|
|
354
|
-
|
|
355
|
-
continue
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Use bash -c to properly handle the echo with newlines
|
|
359
|
-
const cmd = `bash -c 'mkdir -p /etc/resolver && echo -e "nameserver 127.0.0.1\\nport ${DNS_PORT}" > /etc/resolver/${tld}'`
|
|
360
|
-
execSudoSync(cmd)
|
|
361
|
-
createdResolvers.add(tld)
|
|
362
|
-
debugLog('dns', `Created /etc/resolver/${tld} for .${tld} TLD`, verbose)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Flush DNS cache to ensure new resolver files take effect immediately
|
|
363
|
+
for (const basename of basenames)
|
|
364
|
+
await writeResolverFile(basename, verbose)
|
|
366
365
|
await flushDnsCache(verbose)
|
|
367
|
-
|
|
368
366
|
return true
|
|
369
367
|
}
|
|
370
368
|
catch (err) {
|
|
@@ -373,27 +371,139 @@ export async function setupResolver(verbose?: boolean, domains?: string[]): Prom
|
|
|
373
371
|
}
|
|
374
372
|
}
|
|
375
373
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
*/
|
|
379
|
-
export async function removeResolver(verbose?: boolean): Promise<void> {
|
|
380
|
-
if (process.platform !== 'darwin') {
|
|
374
|
+
async function uninstallResolvers(basenames: string[], verbose?: boolean): Promise<void> {
|
|
375
|
+
if (process.platform !== 'darwin')
|
|
381
376
|
return
|
|
382
|
-
}
|
|
383
377
|
|
|
384
|
-
const {
|
|
378
|
+
const { getSudoPassword } = await import('./utils')
|
|
379
|
+
if (!getSudoPassword())
|
|
380
|
+
return
|
|
385
381
|
|
|
386
382
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
execSudoSync(`rm -f /etc/resolver/${tld}`)
|
|
391
|
-
debugLog('dns', `Removed /etc/resolver/${tld}`, verbose)
|
|
392
|
-
}
|
|
393
|
-
createdResolvers.clear()
|
|
394
|
-
}
|
|
383
|
+
for (const basename of basenames)
|
|
384
|
+
await removeResolverFile(basename, verbose)
|
|
385
|
+
await flushDnsCache(verbose)
|
|
395
386
|
}
|
|
396
387
|
catch (err) {
|
|
397
388
|
debugLog('dns', `Failed to remove resolver files: ${err}`, verbose)
|
|
398
389
|
}
|
|
399
390
|
}
|
|
391
|
+
|
|
392
|
+
/** Remove legacy whole-TLD resolver files (e.g. `/etc/resolver/com`). */
|
|
393
|
+
export async function removeLegacyTldResolvers(verbose?: boolean): Promise<string[]> {
|
|
394
|
+
if (process.platform !== 'darwin')
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
const removed: string[] = []
|
|
398
|
+
for (const label of LEGACY_TLD_RESOLVER_LABELS) {
|
|
399
|
+
const content = await readResolverFile(label)
|
|
400
|
+
if (content && contentLooksLikeRpxResolver(content)) {
|
|
401
|
+
await uninstallResolvers([label], verbose)
|
|
402
|
+
removed.push(label)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return removed
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Start the local DNS server and install domain-scoped macOS resolver files.
|
|
410
|
+
*/
|
|
411
|
+
export async function setupDevelopmentDns(opts: DevelopmentDnsOptions): Promise<boolean> {
|
|
412
|
+
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
413
|
+
const domains = devDomainsFromHosts(opts.domains)
|
|
414
|
+
if (domains.length === 0)
|
|
415
|
+
return false
|
|
416
|
+
|
|
417
|
+
const basenames = resolverBasenamesForDomains(domains)
|
|
418
|
+
const started = await startDnsServer(domains, opts.verbose)
|
|
419
|
+
if (!started)
|
|
420
|
+
return false
|
|
421
|
+
|
|
422
|
+
const installed = await installResolvers(basenames, opts.verbose)
|
|
423
|
+
if (!installed)
|
|
424
|
+
return false
|
|
425
|
+
|
|
426
|
+
const state: DnsState = {
|
|
427
|
+
version: DNS_STATE_VERSION,
|
|
428
|
+
resolvers: basenames,
|
|
429
|
+
domains,
|
|
430
|
+
ownerPid: opts.ownerPid ?? process.pid,
|
|
431
|
+
updatedAt: new Date().toISOString(),
|
|
432
|
+
}
|
|
433
|
+
await saveDnsState(rpxDir, state)
|
|
434
|
+
return true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Sync resolver + DNS state to the current set of registry hosts (daemon mode).
|
|
439
|
+
*/
|
|
440
|
+
export async function syncDevelopmentDnsFromRegistry(
|
|
441
|
+
entries: RegistryEntry[],
|
|
442
|
+
opts: { rpxDir?: string, verbose?: boolean, ownerPid?: number } = {},
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const domains = entries.map(e => e.to).filter(Boolean)
|
|
445
|
+
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
446
|
+
const wanted = resolverBasenamesForDomains(domains)
|
|
447
|
+
const state = await loadDnsState(rpxDir)
|
|
448
|
+
const previous = state?.resolvers ?? []
|
|
449
|
+
const toRemove = previous.filter(b => !wanted.includes(b))
|
|
450
|
+
|
|
451
|
+
if (toRemove.length > 0)
|
|
452
|
+
await uninstallResolvers(toRemove, opts.verbose)
|
|
453
|
+
|
|
454
|
+
if (wanted.length === 0) {
|
|
455
|
+
stopDnsServer(opts.verbose)
|
|
456
|
+
await clearDnsState(rpxDir)
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await setupDevelopmentDns({
|
|
461
|
+
domains,
|
|
462
|
+
rpxDir,
|
|
463
|
+
verbose: opts.verbose,
|
|
464
|
+
ownerPid: opts.ownerPid ?? process.pid,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Stop DNS and remove all resolver files recorded in state (plus legacy TLD files).
|
|
470
|
+
*/
|
|
471
|
+
export async function tearDownDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
|
|
472
|
+
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
473
|
+
stopDnsServer(opts.verbose)
|
|
474
|
+
|
|
475
|
+
const state = await loadDnsState(rpxDir)
|
|
476
|
+
const fromState = state?.resolvers ?? []
|
|
477
|
+
await uninstallResolvers(fromState, opts.verbose)
|
|
478
|
+
await removeLegacyTldResolvers(opts.verbose)
|
|
479
|
+
await clearDnsState(rpxDir)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @deprecated Use {@link tearDownDevelopmentDns}.
|
|
484
|
+
*/
|
|
485
|
+
export async function removeResolver(verbose?: boolean): Promise<void> {
|
|
486
|
+
await tearDownDevelopmentDns({ verbose })
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Remove stale DNS overrides left after a crashed dev session or legacy TLD hijacks.
|
|
491
|
+
* Safe to call before starting the daemon or `./buddy dev`.
|
|
492
|
+
*/
|
|
493
|
+
export async function reconcileStaleDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
|
|
494
|
+
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
495
|
+
const state = await loadDnsState(rpxDir)
|
|
496
|
+
const ownerAlive = state?.ownerPid != null && isPidAlive(state.ownerPid)
|
|
497
|
+
|
|
498
|
+
if (state && !ownerAlive) {
|
|
499
|
+
debugLog('dns', `reconcile: owner pid ${state.ownerPid} is gone — tearing down DNS`, opts.verbose)
|
|
500
|
+
await tearDownDevelopmentDns(opts)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const legacyRemoved = await removeLegacyTldResolvers(opts.verbose)
|
|
505
|
+
if (legacyRemoved.length > 0)
|
|
506
|
+
debugLog('dns', `reconcile: removed legacy TLD resolvers: ${legacyRemoved.join(', ')}`, opts.verbose)
|
|
507
|
+
|
|
508
|
+
await flushDnsCache(opts.verbose)
|
|
509
|
+
}
|