@stacksjs/rpx 0.11.3 → 0.11.5
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 +1 -1
- package/dist/{chunk-dz3837t8.js → chunk-8yenn1z8.js} +1 -1
- package/dist/chunk-cvt0dqrv.js +49 -0
- package/dist/{chunk-61re8msk.js → chunk-cy653fq8.js} +1 -1
- package/dist/chunk-grcvjvzg.js +124 -0
- package/dist/{chunk-gbny098p.js → chunk-sqn04kae.js} +1 -1
- package/dist/chunk-wcerh8e8.js +1 -0
- package/dist/https.d.ts +0 -13
- package/dist/process-manager.d.ts +1 -0
- package/dist/src/index.js +1 -1
- package/dist/start.d.ts +3 -0
- package/dist/utils.d.ts +11 -1
- package/package.json +3 -11
- package/src/colors.ts +13 -0
- package/src/config.ts +45 -0
- package/src/dns.ts +399 -0
- package/src/hosts.ts +257 -0
- package/src/https.ts +780 -0
- package/src/index.ts +33 -0
- package/src/logger.ts +19 -0
- package/src/port-manager.ts +183 -0
- package/src/process-manager.ts +164 -0
- package/src/start.ts +1357 -0
- package/src/types.ts +93 -0
- package/src/utils.ts +156 -0
- package/dist/chunk-8mnzvjyr.js +0 -123
- package/dist/chunk-94pvxvt5.js +0 -1
- package/dist/chunk-g5db14m7.js +0 -19
- /package/dist/{chunk-3y886wa5.js → chunk-hj5q1vd6.js} +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{createRequire as a}from"node:module";var c=a(import.meta.url);
|
|
2
|
-
export{c as
|
|
2
|
+
export{c as I};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{A as e,B as f,C as g,D as h,E as i,F as j,G as k,H as l,w as a,x as b,y as c,z as d}from"./chunk-cvt0dqrv.js";import"./chunk-sqn04kae.js";export{l as safeDeleteFile,k as resolvePathRewrite,e as isValidRootCA,i as isSingleProxyOptions,j as isSingleProxyConfig,h as isMultiProxyOptions,g as isMultiProxyConfig,a as getSudoPassword,f as getPrimaryDomain,d as extractHostname,b as execSudoSync,c as debugLog};
|
package/dist/https.d.ts
CHANGED
|
@@ -24,16 +24,3 @@ export declare function loadSSLConfig(options: ProxyOption): Promise<SSLConfig |
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function forceTrustCertificate(certPath: string): Promise<boolean>;
|
|
26
26
|
export declare function generateCertificate(options: ProxyOptions): Promise<void>;
|
|
27
|
-
export declare function getSSLConfig(): { key: string, cert: string, ca?: string } | null;
|
|
28
|
-
// needs to accept the options
|
|
29
|
-
export declare function checkExistingCertificates(options?: ProxyOptions): Promise<SSLConfig | null>;
|
|
30
|
-
export declare function httpsConfig(options: ProxyOption | ProxyOptions, verbose?: boolean): TlsConfig;
|
|
31
|
-
/**
|
|
32
|
-
* Clean up SSL certificates for a specific domain
|
|
33
|
-
*/
|
|
34
|
-
export declare function cleanupCertificates(domain: string, verbose?: boolean): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
* Checks if a certificate is trusted by the system (macOS only for now)
|
|
37
|
-
* If options.regenerateUntrustedCerts is false, always returns true (skips trust check)
|
|
38
|
-
*/
|
|
39
|
-
export declare function isCertTrusted(certPath: string, options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean }): Promise<boolean>;
|
package/dist/src/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as t,c as r,d as e,e as f,f as a,g as i,h as s,i as p,j as c,k as x,l as m,m as n,n as g,o as C,p as l,q as P,s as d,t as u,u as v,v as o}from"../chunk-
|
|
1
|
+
import{a as t,c as r,d as e,e as f,f as a,g as i,h as s,i as p,j as c,k as x,l as m,m as n,n as g,o as C,p as l,q as P,s as d,t as u,u as v,v as o}from"../chunk-grcvjvzg.js";import"../chunk-hj5q1vd6.js";import{A as G,B as I,C as J,D as K,E as N,F as O,G as Q,H as R,w as j,x as q,y as z,z as B}from"../chunk-cvt0dqrv.js";import"../chunk-sqn04kae.js";var U=o;export{u as startServer,v as startProxy,o as startProxies,R as safeDeleteFile,Q as resolvePathRewrite,f as removeHosts,P as portManager,i as loadSSLConfig,G as isValidRootCA,N as isSingleProxyOptions,O as isSingleProxyConfig,g as isPortInUse,K as isMultiProxyOptions,J as isMultiProxyConfig,n as isCertTrusted,x as httpsConfig,j as getSudoPassword,I as getPrimaryDomain,p as generateCertificate,s as forceTrustCertificate,C as findAvailablePort,B as extractHostname,q as execSudoSync,r as defaultConfig,U as default,z as debugLog,r as config,t as colors,m as cleanupCertificates,d as cleanup,a as checkHosts,c as checkExistingCertificates,e as addHosts,l as DefaultPortManager};
|
package/dist/start.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as http2 from 'node:http2';
|
|
3
|
+
import * as https from 'node:https';
|
|
1
4
|
import type { CleanupOptions, ProxyOption, ProxyOptions, ProxySetupOptions, SingleProxyConfig } from './types';
|
|
2
5
|
export declare function cleanup(options?: CleanupOptions): Promise<void>;
|
|
3
6
|
export declare function startServer(options: SingleProxyConfig): Promise<void>;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MultiProxyConfig, ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig } from './types';
|
|
1
|
+
import type { MultiProxyConfig, PathRewrite, ProxyConfigs, ProxyOption, ProxyOptions, SingleProxyConfig } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Get sudo password from environment variable if set
|
|
4
4
|
*/
|
|
@@ -27,6 +27,16 @@ export declare function isMultiProxyOptions(options: ProxyOption | ProxyOptions)
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function isSingleProxyOptions(options: ProxyOption | ProxyOptions): options is SingleProxyConfig;
|
|
29
29
|
export declare function isSingleProxyConfig(options: ProxyConfigs | ProxyOptions): options is SingleProxyConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a path against a list of `pathRewrites`.
|
|
32
|
+
*
|
|
33
|
+
* Returns `null` if no rewrite matches; otherwise returns `{ targetHost, targetPath }`
|
|
34
|
+
* with the prefix preserved by default (or stripped when `stripPrefix === true`).
|
|
35
|
+
*
|
|
36
|
+
* Matching rule: rewrite matches if `pathname` is exactly `from` OR starts with
|
|
37
|
+
* `from + '/'`. So `/api` matches `/api`, `/api/`, `/api/cart` — but not `/apidocs`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePathRewrite(pathname: string, rewrites: PathRewrite[] | undefined): { targetHost: string, targetPath: string } | null;
|
|
30
40
|
/**
|
|
31
41
|
* Safely delete a file if it exists
|
|
32
42
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/rpx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.5",
|
|
5
5
|
"description": "A modern and smart reverse proxy.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -31,15 +31,6 @@
|
|
|
31
31
|
"import": "./dist/src/index.js"
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"exports": {
|
|
36
|
-
".": {
|
|
37
|
-
"types": "./dist/index.d.ts",
|
|
38
|
-
"bun": "./dist/src/index.js",
|
|
39
|
-
"import": "./dist/src/index.js"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
34
|
"module": "./dist/src/index.js",
|
|
44
35
|
"types": "./dist/index.d.ts",
|
|
45
36
|
"bin": {
|
|
@@ -48,7 +39,8 @@
|
|
|
48
39
|
},
|
|
49
40
|
"files": [
|
|
50
41
|
"README.md",
|
|
51
|
-
"dist"
|
|
42
|
+
"dist",
|
|
43
|
+
"src"
|
|
52
44
|
],
|
|
53
45
|
"scripts": {
|
|
54
46
|
"build": "bun build.ts && bun run compile",
|
package/src/colors.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const c = (open: number, close: number): (str: string) => string => (str: string): string => `\x1B[${open}m${str}\x1B[${close}m`
|
|
2
|
+
|
|
3
|
+
export const colors: {
|
|
4
|
+
bold: (str: string) => string
|
|
5
|
+
dim: (str: string) => string
|
|
6
|
+
green: (str: string) => string
|
|
7
|
+
cyan: (str: string) => string
|
|
8
|
+
} = {
|
|
9
|
+
bold: c(1, 22),
|
|
10
|
+
dim: c(2, 22),
|
|
11
|
+
green: c(32, 39),
|
|
12
|
+
cyan: c(36, 39),
|
|
13
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ProxyConfig } from './types'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { loadConfig } from 'bunfig'
|
|
5
|
+
|
|
6
|
+
export const defaultConfig: ProxyConfig = {
|
|
7
|
+
from: 'localhost:5173',
|
|
8
|
+
to: 'stacks.localhost',
|
|
9
|
+
cleanUrls: false,
|
|
10
|
+
https: {
|
|
11
|
+
basePath: '',
|
|
12
|
+
caCertPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.ca.crt`),
|
|
13
|
+
certPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.crt`),
|
|
14
|
+
keyPath: join(homedir(), '.stacks', 'ssl', `stacks.localhost.crt.key`),
|
|
15
|
+
},
|
|
16
|
+
cleanup: {
|
|
17
|
+
certs: false,
|
|
18
|
+
hosts: false,
|
|
19
|
+
},
|
|
20
|
+
vitePluginUsage: false,
|
|
21
|
+
verbose: true,
|
|
22
|
+
changeOrigin: false,
|
|
23
|
+
/**
|
|
24
|
+
* If true, will regenerate and re-trust certs that exist but are not trusted by the system.
|
|
25
|
+
* If false, will use the existing cert even if not trusted (may result in browser warnings).
|
|
26
|
+
*/
|
|
27
|
+
regenerateUntrustedCerts: true,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Lazy-loaded config to avoid top-level await (enables bun --compile)
|
|
31
|
+
let _config: ProxyConfig | null = null
|
|
32
|
+
|
|
33
|
+
export async function getConfig(): Promise<ProxyConfig> {
|
|
34
|
+
if (!_config) {
|
|
35
|
+
_config = await loadConfig({
|
|
36
|
+
name: 'rpx',
|
|
37
|
+
cwd: resolve(__dirname, '..'),
|
|
38
|
+
defaultConfig,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
return _config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// For backwards compatibility - synchronous access with default fallback
|
|
45
|
+
export const config: ProxyConfig = defaultConfig
|
package/src/dns.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal DNS server for local development
|
|
3
|
+
* Handles DNS queries for configured domains and responds with localhost IPs
|
|
4
|
+
*/
|
|
5
|
+
import dgram from 'node:dgram'
|
|
6
|
+
import { debugLog } from './utils'
|
|
7
|
+
|
|
8
|
+
// Use a high port that doesn't require root
|
|
9
|
+
const DNS_PORT = 15353
|
|
10
|
+
|
|
11
|
+
interface DnsHeader {
|
|
12
|
+
id: number
|
|
13
|
+
flags: number
|
|
14
|
+
qdcount: number
|
|
15
|
+
ancount: number
|
|
16
|
+
nscount: number
|
|
17
|
+
arcount: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DnsQuestion {
|
|
21
|
+
name: string
|
|
22
|
+
type: number
|
|
23
|
+
class: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse DNS header from buffer
|
|
28
|
+
*/
|
|
29
|
+
function parseHeader(buffer: Buffer): DnsHeader {
|
|
30
|
+
return {
|
|
31
|
+
id: buffer.readUInt16BE(0),
|
|
32
|
+
flags: buffer.readUInt16BE(2),
|
|
33
|
+
qdcount: buffer.readUInt16BE(4),
|
|
34
|
+
ancount: buffer.readUInt16BE(6),
|
|
35
|
+
nscount: buffer.readUInt16BE(8),
|
|
36
|
+
arcount: buffer.readUInt16BE(10),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse domain name from DNS message
|
|
42
|
+
*/
|
|
43
|
+
function parseName(buffer: Buffer, offset: number): { name: string, newOffset: number } {
|
|
44
|
+
const labels: string[] = []
|
|
45
|
+
let currentOffset = offset
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
const length = buffer[currentOffset]
|
|
49
|
+
|
|
50
|
+
if (length === 0) {
|
|
51
|
+
currentOffset++
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for pointer (compression)
|
|
56
|
+
if ((length & 0xC0) === 0xC0) {
|
|
57
|
+
const pointer = buffer.readUInt16BE(currentOffset) & 0x3FFF
|
|
58
|
+
const { name } = parseName(buffer, pointer)
|
|
59
|
+
labels.push(name)
|
|
60
|
+
currentOffset += 2
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
currentOffset++
|
|
65
|
+
labels.push(buffer.subarray(currentOffset, currentOffset + length).toString('ascii'))
|
|
66
|
+
currentOffset += length
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { name: labels.join('.'), newOffset: currentOffset }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse DNS question section
|
|
74
|
+
*/
|
|
75
|
+
function parseQuestion(buffer: Buffer, offset: number): { question: DnsQuestion, newOffset: number } {
|
|
76
|
+
const { name, newOffset } = parseName(buffer, offset)
|
|
77
|
+
const type = buffer.readUInt16BE(newOffset)
|
|
78
|
+
const qclass = buffer.readUInt16BE(newOffset + 2)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
question: { name, type, class: qclass },
|
|
82
|
+
newOffset: newOffset + 4,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Encode domain name for DNS response
|
|
88
|
+
*/
|
|
89
|
+
function encodeName(name: string): Buffer {
|
|
90
|
+
const labels = name.split('.')
|
|
91
|
+
const parts: Buffer[] = []
|
|
92
|
+
|
|
93
|
+
for (const label of labels) {
|
|
94
|
+
parts.push(Buffer.from([label.length]))
|
|
95
|
+
parts.push(Buffer.from(label, 'ascii'))
|
|
96
|
+
}
|
|
97
|
+
parts.push(Buffer.from([0])) // Null terminator
|
|
98
|
+
|
|
99
|
+
return Buffer.concat(parts)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build DNS response
|
|
104
|
+
*/
|
|
105
|
+
function buildResponse(
|
|
106
|
+
queryId: number,
|
|
107
|
+
question: DnsQuestion,
|
|
108
|
+
ip: string,
|
|
109
|
+
): Buffer {
|
|
110
|
+
const parts: Buffer[] = []
|
|
111
|
+
|
|
112
|
+
// Header
|
|
113
|
+
const header = Buffer.alloc(12)
|
|
114
|
+
header.writeUInt16BE(queryId, 0) // ID
|
|
115
|
+
header.writeUInt16BE(0x8180, 2) // Flags: Response, Authoritative, No error
|
|
116
|
+
header.writeUInt16BE(1, 4) // Questions: 1
|
|
117
|
+
header.writeUInt16BE(1, 6) // Answers: 1
|
|
118
|
+
header.writeUInt16BE(0, 8) // Authority: 0
|
|
119
|
+
header.writeUInt16BE(0, 10) // Additional: 0
|
|
120
|
+
parts.push(header)
|
|
121
|
+
|
|
122
|
+
// Question section (echo back)
|
|
123
|
+
parts.push(encodeName(question.name))
|
|
124
|
+
const qtype = Buffer.alloc(4)
|
|
125
|
+
qtype.writeUInt16BE(question.type, 0)
|
|
126
|
+
qtype.writeUInt16BE(question.class, 2)
|
|
127
|
+
parts.push(qtype)
|
|
128
|
+
|
|
129
|
+
// Answer section
|
|
130
|
+
parts.push(encodeName(question.name))
|
|
131
|
+
|
|
132
|
+
const answer = Buffer.alloc(10)
|
|
133
|
+
answer.writeUInt16BE(question.type, 0) // Type
|
|
134
|
+
answer.writeUInt16BE(1, 2) // Class: IN
|
|
135
|
+
answer.writeUInt32BE(300, 4) // TTL: 5 minutes
|
|
136
|
+
|
|
137
|
+
if (question.type === 1) {
|
|
138
|
+
// A record (IPv4)
|
|
139
|
+
answer.writeUInt16BE(4, 8) // Data length
|
|
140
|
+
parts.push(answer)
|
|
141
|
+
const ipParts = ip.split('.').map(p => Number.parseInt(p, 10))
|
|
142
|
+
parts.push(Buffer.from(ipParts))
|
|
143
|
+
}
|
|
144
|
+
else if (question.type === 28) {
|
|
145
|
+
// AAAA record (IPv6)
|
|
146
|
+
answer.writeUInt16BE(16, 8) // Data length
|
|
147
|
+
parts.push(answer)
|
|
148
|
+
// ::1 as bytes
|
|
149
|
+
parts.push(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]))
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Unsupported type - return NXDOMAIN
|
|
153
|
+
header.writeUInt16BE(0x8183, 2) // Flags with NXDOMAIN
|
|
154
|
+
header.writeUInt16BE(0, 6) // No answers
|
|
155
|
+
return Buffer.concat([header, encodeName(question.name), qtype])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Buffer.concat(parts)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build NXDOMAIN response for unknown domains
|
|
163
|
+
*/
|
|
164
|
+
function buildNxdomainResponse(queryId: number, question: DnsQuestion): Buffer {
|
|
165
|
+
const parts: Buffer[] = []
|
|
166
|
+
|
|
167
|
+
// Header with NXDOMAIN
|
|
168
|
+
const header = Buffer.alloc(12)
|
|
169
|
+
header.writeUInt16BE(queryId, 0) // ID
|
|
170
|
+
header.writeUInt16BE(0x8183, 2) // Flags: Response, Authoritative, NXDOMAIN
|
|
171
|
+
header.writeUInt16BE(1, 4) // Questions: 1
|
|
172
|
+
header.writeUInt16BE(0, 6) // Answers: 0
|
|
173
|
+
header.writeUInt16BE(0, 8) // Authority: 0
|
|
174
|
+
header.writeUInt16BE(0, 10) // Additional: 0
|
|
175
|
+
parts.push(header)
|
|
176
|
+
|
|
177
|
+
// Question section (echo back)
|
|
178
|
+
parts.push(encodeName(question.name))
|
|
179
|
+
const qtype = Buffer.alloc(4)
|
|
180
|
+
qtype.writeUInt16BE(question.type, 0)
|
|
181
|
+
qtype.writeUInt16BE(question.class, 2)
|
|
182
|
+
parts.push(qtype)
|
|
183
|
+
|
|
184
|
+
return Buffer.concat(parts)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let dnsServer: dgram.Socket | null = null
|
|
188
|
+
let configuredDomains: Set<string> = new Set()
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Start the DNS server
|
|
192
|
+
*/
|
|
193
|
+
export async function startDnsServer(domains: string[], verbose?: boolean): Promise<boolean> {
|
|
194
|
+
if (dnsServer) {
|
|
195
|
+
debugLog('dns', 'DNS server already running', verbose)
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
configuredDomains = new Set(domains.map(d => d.toLowerCase()))
|
|
200
|
+
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
dnsServer = dgram.createSocket('udp4')
|
|
203
|
+
|
|
204
|
+
dnsServer.on('error', (err) => {
|
|
205
|
+
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
|
+
dnsServer?.close()
|
|
210
|
+
dnsServer = null
|
|
211
|
+
resolve(false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
dnsServer.on('message', (msg, rinfo) => {
|
|
215
|
+
try {
|
|
216
|
+
const header = parseHeader(msg)
|
|
217
|
+
const { question } = parseQuestion(msg, 12)
|
|
218
|
+
|
|
219
|
+
debugLog('dns', `Query for ${question.name} type ${question.type} from ${rinfo.address}`, verbose)
|
|
220
|
+
|
|
221
|
+
// Check if this domain should be handled
|
|
222
|
+
const domainLower = question.name.toLowerCase()
|
|
223
|
+
let shouldHandle = false
|
|
224
|
+
|
|
225
|
+
for (const configured of configuredDomains) {
|
|
226
|
+
if (domainLower === configured || domainLower.endsWith(`.${configured}`)) {
|
|
227
|
+
shouldHandle = true
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Note: Only configured domains are handled, no hardcoded TLDs
|
|
233
|
+
|
|
234
|
+
let response: Buffer
|
|
235
|
+
if (shouldHandle && (question.type === 1 || question.type === 28)) {
|
|
236
|
+
response = buildResponse(header.id, question, '127.0.0.1')
|
|
237
|
+
debugLog('dns', `Responding with localhost for ${question.name}`, verbose)
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
response = buildNxdomainResponse(header.id, question)
|
|
241
|
+
debugLog('dns', `NXDOMAIN for ${question.name}`, verbose)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
dnsServer?.send(response, rinfo.port, rinfo.address)
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
debugLog('dns', `Error processing DNS query: ${err}`, verbose)
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
dnsServer.on('listening', () => {
|
|
252
|
+
const address = dnsServer?.address()
|
|
253
|
+
debugLog('dns', `DNS server listening on ${address?.address}:${address?.port}`, verbose)
|
|
254
|
+
resolve(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Try to bind to port 53 with sudo
|
|
258
|
+
try {
|
|
259
|
+
dnsServer.bind(DNS_PORT, '127.0.0.1')
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
debugLog('dns', `Failed to bind DNS server: ${err}`, verbose)
|
|
263
|
+
resolve(false)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stop the DNS server
|
|
270
|
+
*/
|
|
271
|
+
export function stopDnsServer(verbose?: boolean): void {
|
|
272
|
+
if (dnsServer) {
|
|
273
|
+
debugLog('dns', 'Stopping DNS server', verbose)
|
|
274
|
+
dnsServer.close()
|
|
275
|
+
dnsServer = null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if DNS server is running
|
|
281
|
+
*/
|
|
282
|
+
export function isDnsServerRunning(): boolean {
|
|
283
|
+
return dnsServer !== null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Extract unique TLDs from domain list
|
|
288
|
+
*/
|
|
289
|
+
function extractTLDs(domains: string[]): string[] {
|
|
290
|
+
const tlds = new Set<string>()
|
|
291
|
+
for (const domain of domains) {
|
|
292
|
+
const parts = domain.split('.')
|
|
293
|
+
if (parts.length >= 2) {
|
|
294
|
+
tlds.add(parts[parts.length - 1])
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return Array.from(tlds)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Track which TLDs we've created resolvers for
|
|
301
|
+
const createdResolvers = new Set<string>()
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Flush macOS DNS cache to ensure resolver changes take effect
|
|
305
|
+
*/
|
|
306
|
+
async function flushDnsCache(verbose?: boolean): Promise<void> {
|
|
307
|
+
if (process.platform !== 'darwin') {
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { execSudoSync, getSudoPassword } = await import('./utils')
|
|
312
|
+
const sudoPassword = getSudoPassword()
|
|
313
|
+
|
|
314
|
+
if (!sudoPassword) {
|
|
315
|
+
debugLog('dns', 'Cannot flush DNS cache without SUDO_PASSWORD', verbose)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
// Flush DNS cache and restart mDNSResponder
|
|
321
|
+
execSudoSync('dscacheutil -flushcache')
|
|
322
|
+
execSudoSync('killall -HUP mDNSResponder 2>/dev/null || true')
|
|
323
|
+
debugLog('dns', 'DNS cache flushed', verbose)
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
// Non-fatal - DNS cache flush failure shouldn't block startup
|
|
327
|
+
debugLog('dns', `Could not flush DNS cache: ${err}`, verbose)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Set up the macOS resolver for configured domains
|
|
333
|
+
* Creates /etc/resolver/<tld> files pointing to our local DNS server
|
|
334
|
+
*/
|
|
335
|
+
export async function setupResolver(verbose?: boolean, domains?: string[]): Promise<boolean> {
|
|
336
|
+
if (process.platform !== 'darwin') {
|
|
337
|
+
debugLog('dns', 'Resolver setup only needed on macOS', verbose)
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const { execSudoSync, getSudoPassword } = await import('./utils')
|
|
342
|
+
const sudoPassword = getSudoPassword()
|
|
343
|
+
|
|
344
|
+
if (!sudoPassword) {
|
|
345
|
+
debugLog('dns', 'SUDO_PASSWORD not set, cannot create resolver files', verbose)
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Get TLDs from configured domains
|
|
350
|
+
const tlds = domains ? extractTLDs(domains) : ['test']
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
for (const tld of tlds) {
|
|
354
|
+
if (createdResolvers.has(tld)) {
|
|
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
|
|
366
|
+
await flushDnsCache(verbose)
|
|
367
|
+
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
debugLog('dns', `Failed to create resolver file: ${err}`, verbose)
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Remove the macOS resolver files we created
|
|
378
|
+
*/
|
|
379
|
+
export async function removeResolver(verbose?: boolean): Promise<void> {
|
|
380
|
+
if (process.platform !== 'darwin') {
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const { execSudoSync, getSudoPassword } = await import('./utils')
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const sudoPassword = getSudoPassword()
|
|
388
|
+
if (sudoPassword) {
|
|
389
|
+
for (const tld of createdResolvers) {
|
|
390
|
+
execSudoSync(`rm -f /etc/resolver/${tld}`)
|
|
391
|
+
debugLog('dns', `Removed /etc/resolver/${tld}`, verbose)
|
|
392
|
+
}
|
|
393
|
+
createdResolvers.clear()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
debugLog('dns', `Failed to remove resolver files: ${err}`, verbose)
|
|
398
|
+
}
|
|
399
|
+
}
|