@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/dns-state.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import * as path from 'node:path'
|
|
4
|
-
|
|
5
|
-
export const DNS_STATE_VERSION = 1 as const
|
|
6
|
-
export const RPX_DNS_STATE_FILE = 'dns-state.json'
|
|
7
|
-
|
|
8
|
-
/** Single-label /etc/resolver files created by older rpx versions (whole-TLD hijack). */
|
|
9
|
-
export const LEGACY_TLD_RESOLVER_LABELS = [
|
|
10
|
-
'com',
|
|
11
|
-
'test',
|
|
12
|
-
'dev',
|
|
13
|
-
'app',
|
|
14
|
-
'page',
|
|
15
|
-
'local',
|
|
16
|
-
'localhost',
|
|
17
|
-
'example',
|
|
18
|
-
'invalid',
|
|
19
|
-
] as const
|
|
20
|
-
|
|
21
|
-
export interface DnsState {
|
|
22
|
-
version: typeof DNS_STATE_VERSION
|
|
23
|
-
/** Basenames under /etc/resolver/ (e.g. `postline.test`, not `test`). */
|
|
24
|
-
resolvers: string[]
|
|
25
|
-
domains: string[]
|
|
26
|
-
ownerPid: number | null
|
|
27
|
-
updatedAt: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function defaultRpxDir(): string {
|
|
31
|
-
return path.join(homedir(), '.stacks', 'rpx')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function getDnsStatePath(rpxDir: string = defaultRpxDir()): string {
|
|
35
|
-
return path.join(rpxDir, RPX_DNS_STATE_FILE)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function loadDnsState(rpxDir: string = defaultRpxDir()): Promise<DnsState | null> {
|
|
39
|
-
try {
|
|
40
|
-
const raw = await fsp.readFile(getDnsStatePath(rpxDir), 'utf8')
|
|
41
|
-
const parsed = JSON.parse(raw) as Partial<DnsState>
|
|
42
|
-
if (parsed.version !== DNS_STATE_VERSION || !Array.isArray(parsed.resolvers))
|
|
43
|
-
return null
|
|
44
|
-
return {
|
|
45
|
-
version: DNS_STATE_VERSION,
|
|
46
|
-
resolvers: parsed.resolvers.filter((r): r is string => typeof r === 'string'),
|
|
47
|
-
domains: Array.isArray(parsed.domains)
|
|
48
|
-
? parsed.domains.filter((d): d is string => typeof d === 'string')
|
|
49
|
-
: [],
|
|
50
|
-
ownerPid: typeof parsed.ownerPid === 'number' ? parsed.ownerPid : null,
|
|
51
|
-
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
|
|
56
|
-
return null
|
|
57
|
-
throw err
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function saveDnsState(rpxDir: string, state: DnsState): Promise<void> {
|
|
62
|
-
await fsp.mkdir(rpxDir, { recursive: true })
|
|
63
|
-
await fsp.writeFile(getDnsStatePath(rpxDir), `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function clearDnsState(rpxDir: string): Promise<void> {
|
|
67
|
-
await fsp.rm(getDnsStatePath(rpxDir), { force: true })
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Normalize a dev hostname. Returns null for localhost / IPs — those use /etc/hosts only.
|
|
72
|
-
*/
|
|
73
|
-
export function normalizeDevDomain(raw: string): string | null {
|
|
74
|
-
const domain = raw.trim().toLowerCase().replace(/\.$/, '')
|
|
75
|
-
if (!domain || domain.includes('127.0.0.1'))
|
|
76
|
-
return null
|
|
77
|
-
if (domain === 'localhost' || domain.endsWith('.localhost'))
|
|
78
|
-
return null
|
|
79
|
-
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(domain))
|
|
80
|
-
return null
|
|
81
|
-
return domain
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* macOS resolver basename for a dev domain. Uses the registrable base (last two labels)
|
|
86
|
-
* so `api.postline.test` and `postline.test` share one `/etc/resolver/postline.test` file.
|
|
87
|
-
*/
|
|
88
|
-
export function resolverBasenameForDomain(raw: string): string | null {
|
|
89
|
-
const domain = normalizeDevDomain(raw)
|
|
90
|
-
if (!domain)
|
|
91
|
-
return null
|
|
92
|
-
const parts = domain.split('.')
|
|
93
|
-
if (parts.length < 2)
|
|
94
|
-
return null
|
|
95
|
-
return parts.slice(-2).join('.')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function resolverBasenamesForDomains(domains: string[]): string[] {
|
|
99
|
-
const basenames = new Set<string>()
|
|
100
|
-
for (const raw of domains) {
|
|
101
|
-
const basename = resolverBasenameForDomain(raw)
|
|
102
|
-
if (basename)
|
|
103
|
-
basenames.add(basename)
|
|
104
|
-
}
|
|
105
|
-
return Array.from(basenames).sort()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function devDomainsFromHosts(hosts: string[]): string[] {
|
|
109
|
-
const out = new Set<string>()
|
|
110
|
-
for (const raw of hosts) {
|
|
111
|
-
const domain = normalizeDevDomain(raw)
|
|
112
|
-
if (domain)
|
|
113
|
-
out.add(domain)
|
|
114
|
-
}
|
|
115
|
-
return Array.from(out).sort()
|
|
116
|
-
}
|
package/src/dns.ts
DELETED
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
/**
|
|
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.
|
|
6
|
-
*/
|
|
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'
|
|
24
|
-
import { debugLog } from './utils'
|
|
25
|
-
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
interface DnsHeader {
|
|
49
|
-
id: number
|
|
50
|
-
flags: number
|
|
51
|
-
qdcount: number
|
|
52
|
-
ancount: number
|
|
53
|
-
nscount: number
|
|
54
|
-
arcount: number
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface DnsQuestion {
|
|
58
|
-
name: string
|
|
59
|
-
type: number
|
|
60
|
-
class: number
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parseHeader(buffer: Buffer): DnsHeader {
|
|
64
|
-
return {
|
|
65
|
-
id: buffer.readUInt16BE(0),
|
|
66
|
-
flags: buffer.readUInt16BE(2),
|
|
67
|
-
qdcount: buffer.readUInt16BE(4),
|
|
68
|
-
ancount: buffer.readUInt16BE(6),
|
|
69
|
-
nscount: buffer.readUInt16BE(8),
|
|
70
|
-
arcount: buffer.readUInt16BE(10),
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function parseName(buffer: Buffer, offset: number): { name: string, newOffset: number } {
|
|
75
|
-
const labels: string[] = []
|
|
76
|
-
let currentOffset = offset
|
|
77
|
-
|
|
78
|
-
while (true) {
|
|
79
|
-
const length = buffer[currentOffset]
|
|
80
|
-
|
|
81
|
-
if (length === 0) {
|
|
82
|
-
currentOffset++
|
|
83
|
-
break
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if ((length & 0xC0) === 0xC0) {
|
|
87
|
-
const pointer = buffer.readUInt16BE(currentOffset) & 0x3FFF
|
|
88
|
-
const { name } = parseName(buffer, pointer)
|
|
89
|
-
labels.push(name)
|
|
90
|
-
currentOffset += 2
|
|
91
|
-
break
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
currentOffset++
|
|
95
|
-
labels.push(buffer.subarray(currentOffset, currentOffset + length).toString('ascii'))
|
|
96
|
-
currentOffset += length
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { name: labels.join('.'), newOffset: currentOffset }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseQuestion(buffer: Buffer, offset: number): { question: DnsQuestion, newOffset: number } {
|
|
103
|
-
const { name, newOffset } = parseName(buffer, offset)
|
|
104
|
-
const type = buffer.readUInt16BE(newOffset)
|
|
105
|
-
const qclass = buffer.readUInt16BE(newOffset + 2)
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
question: { name, type, class: qclass },
|
|
109
|
-
newOffset: newOffset + 4,
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function encodeName(name: string): Buffer {
|
|
114
|
-
const labels = name.split('.')
|
|
115
|
-
const parts: Buffer[] = []
|
|
116
|
-
|
|
117
|
-
for (const label of labels) {
|
|
118
|
-
parts.push(Buffer.from([label.length]))
|
|
119
|
-
parts.push(Buffer.from(label, 'ascii'))
|
|
120
|
-
}
|
|
121
|
-
parts.push(Buffer.from([0]))
|
|
122
|
-
|
|
123
|
-
return Buffer.concat(parts)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function buildResponse(queryId: number, question: DnsQuestion, ip: string): Buffer {
|
|
127
|
-
const parts: Buffer[] = []
|
|
128
|
-
|
|
129
|
-
const header = Buffer.alloc(12)
|
|
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)
|
|
136
|
-
parts.push(header)
|
|
137
|
-
|
|
138
|
-
parts.push(encodeName(question.name))
|
|
139
|
-
const qtype = Buffer.alloc(4)
|
|
140
|
-
qtype.writeUInt16BE(question.type, 0)
|
|
141
|
-
qtype.writeUInt16BE(question.class, 2)
|
|
142
|
-
parts.push(qtype)
|
|
143
|
-
|
|
144
|
-
parts.push(encodeName(question.name))
|
|
145
|
-
|
|
146
|
-
const answer = Buffer.alloc(10)
|
|
147
|
-
answer.writeUInt16BE(question.type, 0)
|
|
148
|
-
answer.writeUInt16BE(1, 2)
|
|
149
|
-
answer.writeUInt32BE(300, 4)
|
|
150
|
-
|
|
151
|
-
if (question.type === 1) {
|
|
152
|
-
answer.writeUInt16BE(4, 8)
|
|
153
|
-
parts.push(answer)
|
|
154
|
-
const ipParts = ip.split('.').map(p => Number.parseInt(p, 10))
|
|
155
|
-
parts.push(Buffer.from(ipParts))
|
|
156
|
-
}
|
|
157
|
-
else if (question.type === 28) {
|
|
158
|
-
answer.writeUInt16BE(16, 8)
|
|
159
|
-
parts.push(answer)
|
|
160
|
-
parts.push(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
header.writeUInt16BE(0x8183, 2)
|
|
164
|
-
header.writeUInt16BE(0, 6)
|
|
165
|
-
return Buffer.concat([header, encodeName(question.name), qtype])
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return Buffer.concat(parts)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function buildNxdomainResponse(queryId: number, question: DnsQuestion): Buffer {
|
|
172
|
-
const parts: Buffer[] = []
|
|
173
|
-
|
|
174
|
-
const header = Buffer.alloc(12)
|
|
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)
|
|
181
|
-
parts.push(header)
|
|
182
|
-
|
|
183
|
-
parts.push(encodeName(question.name))
|
|
184
|
-
const qtype = Buffer.alloc(4)
|
|
185
|
-
qtype.writeUInt16BE(question.type, 0)
|
|
186
|
-
qtype.writeUInt16BE(question.class, 2)
|
|
187
|
-
parts.push(qtype)
|
|
188
|
-
|
|
189
|
-
return Buffer.concat(parts)
|
|
190
|
-
}
|
|
191
|
-
|
|
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
|
-
|
|
200
|
-
if (dnsServer) {
|
|
201
|
-
for (const d of devDomains)
|
|
202
|
-
configuredDomains.add(d)
|
|
203
|
-
debugLog('dns', 'DNS server already running — merged domains', verbose)
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
configuredDomains = new Set(devDomains)
|
|
208
|
-
|
|
209
|
-
return new Promise((resolve) => {
|
|
210
|
-
dnsServer = dgram.createSocket('udp4')
|
|
211
|
-
|
|
212
|
-
dnsServer.on('error', (err) => {
|
|
213
|
-
debugLog('dns', `DNS server error: ${err.message}`, verbose)
|
|
214
|
-
dnsServer?.close()
|
|
215
|
-
dnsServer = null
|
|
216
|
-
resolve(false)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
dnsServer.on('message', (msg, rinfo) => {
|
|
220
|
-
try {
|
|
221
|
-
const header = parseHeader(msg)
|
|
222
|
-
const { question } = parseQuestion(msg, 12)
|
|
223
|
-
|
|
224
|
-
debugLog('dns', `Query for ${question.name} type ${question.type} from ${rinfo.address}`, verbose)
|
|
225
|
-
|
|
226
|
-
const domainLower = question.name.toLowerCase()
|
|
227
|
-
let shouldHandle = false
|
|
228
|
-
|
|
229
|
-
for (const configured of configuredDomains) {
|
|
230
|
-
if (domainLower === configured || domainLower.endsWith(`.${configured}`)) {
|
|
231
|
-
shouldHandle = true
|
|
232
|
-
break
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let response: Buffer
|
|
237
|
-
if (shouldHandle && (question.type === 1 || question.type === 28)) {
|
|
238
|
-
response = buildResponse(header.id, question, '127.0.0.1')
|
|
239
|
-
debugLog('dns', `Responding with localhost for ${question.name}`, verbose)
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
response = buildNxdomainResponse(header.id, question)
|
|
243
|
-
debugLog('dns', `NXDOMAIN for ${question.name}`, verbose)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
dnsServer?.send(response, rinfo.port, rinfo.address)
|
|
247
|
-
}
|
|
248
|
-
catch (err) {
|
|
249
|
-
debugLog('dns', `Error processing DNS query: ${err}`, verbose)
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
dnsServer.on('listening', () => {
|
|
254
|
-
const address = dnsServer?.address()
|
|
255
|
-
debugLog('dns', `DNS server listening on ${address?.address}:${address?.port}`, verbose)
|
|
256
|
-
resolve(true)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
dnsServer.bind(DNS_PORT, '127.0.0.1')
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
debugLog('dns', `Failed to bind DNS server: ${err}`, verbose)
|
|
264
|
-
resolve(false)
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function stopDnsServer(verbose?: boolean): void {
|
|
270
|
-
if (dnsServer) {
|
|
271
|
-
debugLog('dns', 'Stopping DNS server', verbose)
|
|
272
|
-
dnsServer.close()
|
|
273
|
-
dnsServer = null
|
|
274
|
-
configuredDomains = new Set()
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function isDnsServerRunning(): boolean {
|
|
279
|
-
return dnsServer !== null
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** True when something is answering A queries for `host` on the rpx dev DNS port. */
|
|
283
|
-
export async function isRpxDevelopmentDnsAnswering(host: string, timeoutMs = 500): Promise<boolean> {
|
|
284
|
-
const domain = host.trim().toLowerCase().replace(/\.$/, '')
|
|
285
|
-
if (!domain)
|
|
286
|
-
return false
|
|
287
|
-
|
|
288
|
-
return new Promise((resolve) => {
|
|
289
|
-
const socket = dgram.createSocket('udp4')
|
|
290
|
-
const timer = setTimeout(() => {
|
|
291
|
-
socket.close()
|
|
292
|
-
resolve(false)
|
|
293
|
-
}, timeoutMs)
|
|
294
|
-
|
|
295
|
-
socket.on('message', (msg) => {
|
|
296
|
-
clearTimeout(timer)
|
|
297
|
-
socket.close()
|
|
298
|
-
try {
|
|
299
|
-
const header = parseHeader(msg)
|
|
300
|
-
resolve(header.ancount > 0)
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
resolve(false)
|
|
304
|
-
}
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
socket.on('error', () => {
|
|
308
|
-
clearTimeout(timer)
|
|
309
|
-
socket.close()
|
|
310
|
-
resolve(false)
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
const query = buildQuery(domain, 1)
|
|
314
|
-
socket.send(query, DNS_PORT, '127.0.0.1', (err) => {
|
|
315
|
-
if (err) {
|
|
316
|
-
clearTimeout(timer)
|
|
317
|
-
socket.close()
|
|
318
|
-
resolve(false)
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function buildQuery(name: string, type: number): Buffer {
|
|
325
|
-
const header = Buffer.alloc(12)
|
|
326
|
-
header.writeUInt16BE(1, 0)
|
|
327
|
-
header.writeUInt16BE(0x0100, 2)
|
|
328
|
-
header.writeUInt16BE(1, 4)
|
|
329
|
-
const question = Buffer.concat([encodeName(name), Buffer.from([0, type, 0, 1])])
|
|
330
|
-
return Buffer.concat([header, question])
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// macOS resolver files
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
function resolverFileContent(): string {
|
|
338
|
-
return `${RPX_RESOLVER_MARKER}\nnameserver 127.0.0.1\nport ${DNS_PORT}\n`
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export function resolverFilePath(basename: string): string {
|
|
342
|
-
return path.join(MACOS_RESOLVER_DIR, basename)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** True when a resolver file points at the rpx local DNS port. */
|
|
346
|
-
export function contentLooksLikeRpxResolver(content: string): boolean {
|
|
347
|
-
return content.includes('127.0.0.1') && content.includes(String(DNS_PORT))
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function readResolverFile(basename: string): Promise<string | null> {
|
|
351
|
-
try {
|
|
352
|
-
return await fsp.readFile(resolverFilePath(basename), 'utf8')
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
|
|
356
|
-
return null
|
|
357
|
-
throw err
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function flushDnsCache(verbose?: boolean): Promise<void> {
|
|
362
|
-
if (process.platform !== 'darwin')
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
const { execSudoSync, getSudoPassword, isProcessElevated } = await import('./utils')
|
|
366
|
-
|
|
367
|
-
if (!isProcessElevated() && !getSudoPassword()) {
|
|
368
|
-
debugLog('dns', 'Cannot flush DNS cache without SUDO_PASSWORD', verbose)
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
execSudoSync('dscacheutil -flushcache')
|
|
374
|
-
execSudoSync('killall -HUP mDNSResponder 2>/dev/null || true')
|
|
375
|
-
debugLog('dns', 'DNS cache flushed', verbose)
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
debugLog('dns', `Could not flush DNS cache: ${err}`, verbose)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function writeResolverFile(basename: string, verbose?: boolean): Promise<void> {
|
|
383
|
-
const { execSudoSync } = await import('./utils')
|
|
384
|
-
const content = resolverFileContent().replace(/\n/g, '\\n')
|
|
385
|
-
const cmd = `bash -c 'mkdir -p ${MACOS_RESOLVER_DIR} && printf "%b" "${content}" > ${resolverFilePath(basename)}'`
|
|
386
|
-
execSudoSync(cmd)
|
|
387
|
-
debugLog('dns', `Created ${resolverFilePath(basename)}`, verbose)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function removeResolverFile(basename: string, verbose?: boolean): Promise<void> {
|
|
391
|
-
const { execSudoSync } = await import('./utils')
|
|
392
|
-
execSudoSync(`rm -f ${resolverFilePath(basename)}`)
|
|
393
|
-
debugLog('dns', `Removed ${resolverFilePath(basename)}`, verbose)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* @deprecated Use {@link setupDevelopmentDns}. Domain-scoped resolver files only.
|
|
398
|
-
*/
|
|
399
|
-
export async function setupResolver(verbose?: boolean, domains?: string[]): Promise<boolean> {
|
|
400
|
-
return setupDevelopmentDns({ domains: domains ?? [], verbose })
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function installResolvers(basenames: string[], verbose?: boolean): Promise<boolean> {
|
|
404
|
-
if (process.platform !== 'darwin')
|
|
405
|
-
return true
|
|
406
|
-
|
|
407
|
-
const { getSudoPassword, isProcessElevated } = await import('./utils')
|
|
408
|
-
if (!isProcessElevated() && !getSudoPassword()) {
|
|
409
|
-
debugLog('dns', 'SUDO_PASSWORD not set, cannot create resolver files', verbose)
|
|
410
|
-
return false
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
for (const basename of basenames)
|
|
415
|
-
await writeResolverFile(basename, verbose)
|
|
416
|
-
await flushDnsCache(verbose)
|
|
417
|
-
return true
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
debugLog('dns', `Failed to create resolver file: ${err}`, verbose)
|
|
421
|
-
return false
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async function uninstallResolvers(basenames: string[], verbose?: boolean): Promise<void> {
|
|
426
|
-
if (process.platform !== 'darwin')
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
const { getSudoPassword, isProcessElevated } = await import('./utils')
|
|
430
|
-
if (!isProcessElevated() && !getSudoPassword())
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
for (const basename of basenames)
|
|
435
|
-
await removeResolverFile(basename, verbose)
|
|
436
|
-
await flushDnsCache(verbose)
|
|
437
|
-
}
|
|
438
|
-
catch (err) {
|
|
439
|
-
debugLog('dns', `Failed to remove resolver files: ${err}`, verbose)
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/** Remove legacy whole-TLD resolver files (e.g. `/etc/resolver/com`). */
|
|
444
|
-
export async function removeLegacyTldResolvers(verbose?: boolean): Promise<string[]> {
|
|
445
|
-
if (process.platform !== 'darwin')
|
|
446
|
-
return []
|
|
447
|
-
|
|
448
|
-
const removed: string[] = []
|
|
449
|
-
for (const label of LEGACY_TLD_RESOLVER_LABELS) {
|
|
450
|
-
const content = await readResolverFile(label)
|
|
451
|
-
if (content && contentLooksLikeRpxResolver(content)) {
|
|
452
|
-
await uninstallResolvers([label], verbose)
|
|
453
|
-
removed.push(label)
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return removed
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Start the local DNS server and install domain-scoped macOS resolver files.
|
|
461
|
-
*/
|
|
462
|
-
export async function setupDevelopmentDns(opts: DevelopmentDnsOptions): Promise<boolean> {
|
|
463
|
-
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
464
|
-
const domains = devDomainsFromHosts(opts.domains)
|
|
465
|
-
if (domains.length === 0)
|
|
466
|
-
return false
|
|
467
|
-
|
|
468
|
-
const basenames = resolverBasenamesForDomains(domains)
|
|
469
|
-
let started = await startDnsServer(domains, opts.verbose)
|
|
470
|
-
if (!started) {
|
|
471
|
-
const probeHost = domains[0]
|
|
472
|
-
if (probeHost && await isRpxDevelopmentDnsAnswering(probeHost))
|
|
473
|
-
started = true
|
|
474
|
-
else
|
|
475
|
-
debugLog('dns', 'Dev DNS server not available on 127.0.0.1:15353', opts.verbose)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (!started)
|
|
479
|
-
return false
|
|
480
|
-
|
|
481
|
-
const installed = await installResolvers(basenames, opts.verbose)
|
|
482
|
-
if (!installed)
|
|
483
|
-
return false
|
|
484
|
-
|
|
485
|
-
const state: DnsState = {
|
|
486
|
-
version: DNS_STATE_VERSION,
|
|
487
|
-
resolvers: basenames,
|
|
488
|
-
domains,
|
|
489
|
-
ownerPid: opts.ownerPid ?? process.pid,
|
|
490
|
-
updatedAt: new Date().toISOString(),
|
|
491
|
-
}
|
|
492
|
-
await saveDnsState(rpxDir, state)
|
|
493
|
-
return true
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Sync resolver + DNS state to the current set of registry hosts (daemon mode).
|
|
498
|
-
*/
|
|
499
|
-
export async function syncDevelopmentDnsFromRegistry(
|
|
500
|
-
entries: RegistryEntry[],
|
|
501
|
-
opts: { rpxDir?: string, verbose?: boolean, ownerPid?: number } = {},
|
|
502
|
-
): Promise<void> {
|
|
503
|
-
const domains = entries.map(e => e.to).filter(Boolean)
|
|
504
|
-
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
505
|
-
const wanted = resolverBasenamesForDomains(domains)
|
|
506
|
-
const state = await loadDnsState(rpxDir)
|
|
507
|
-
const previous = state?.resolvers ?? []
|
|
508
|
-
const toRemove = previous.filter(b => !wanted.includes(b))
|
|
509
|
-
|
|
510
|
-
if (toRemove.length > 0)
|
|
511
|
-
await uninstallResolvers(toRemove, opts.verbose)
|
|
512
|
-
|
|
513
|
-
if (wanted.length === 0) {
|
|
514
|
-
stopDnsServer(opts.verbose)
|
|
515
|
-
await clearDnsState(rpxDir)
|
|
516
|
-
return
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
await setupDevelopmentDns({
|
|
520
|
-
domains,
|
|
521
|
-
rpxDir,
|
|
522
|
-
verbose: opts.verbose,
|
|
523
|
-
ownerPid: opts.ownerPid ?? process.pid,
|
|
524
|
-
})
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Stop DNS and remove all resolver files recorded in state (plus legacy TLD files).
|
|
529
|
-
*/
|
|
530
|
-
export async function tearDownDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
|
|
531
|
-
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
532
|
-
stopDnsServer(opts.verbose)
|
|
533
|
-
|
|
534
|
-
const state = await loadDnsState(rpxDir)
|
|
535
|
-
const fromState = state?.resolvers ?? []
|
|
536
|
-
await uninstallResolvers(fromState, opts.verbose)
|
|
537
|
-
await removeLegacyTldResolvers(opts.verbose)
|
|
538
|
-
await clearDnsState(rpxDir)
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* @deprecated Use {@link tearDownDevelopmentDns}.
|
|
543
|
-
*/
|
|
544
|
-
export async function removeResolver(verbose?: boolean): Promise<void> {
|
|
545
|
-
await tearDownDevelopmentDns({ verbose })
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Remove stale DNS overrides left after a crashed dev session or legacy TLD hijacks.
|
|
550
|
-
* Safe to call before starting the daemon or `./buddy dev`.
|
|
551
|
-
*/
|
|
552
|
-
export async function reconcileStaleDevelopmentDns(opts: { rpxDir?: string, verbose?: boolean } = {}): Promise<void> {
|
|
553
|
-
const rpxDir = opts.rpxDir ?? getDaemonRpxDir()
|
|
554
|
-
const state = await loadDnsState(rpxDir)
|
|
555
|
-
const ownerAlive = state?.ownerPid != null && isPidAlive(state.ownerPid)
|
|
556
|
-
|
|
557
|
-
if (state && !ownerAlive) {
|
|
558
|
-
debugLog('dns', `reconcile: owner pid ${state.ownerPid} is gone — tearing down DNS`, opts.verbose)
|
|
559
|
-
await tearDownDevelopmentDns(opts)
|
|
560
|
-
return
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const legacyRemoved = await removeLegacyTldResolvers(opts.verbose)
|
|
564
|
-
if (legacyRemoved.length > 0)
|
|
565
|
-
debugLog('dns', `reconcile: removed legacy TLD resolvers: ${legacyRemoved.join(', ')}`, opts.verbose)
|
|
566
|
-
|
|
567
|
-
await flushDnsCache(opts.verbose)
|
|
568
|
-
}
|